# -*- coding: utf-8 -*-
import collections
import json
from collections.abc import Collection
from itertools import chain

from django.apps.config import AppConfig
from django.core.exceptions import ImproperlyConfigured
from django.core.serializers.json import DjangoJSONEncoder

from .itertools import find_first, unique_everseen

__all__ = ["SegmentedInstalledApps"]


class AppNameTable:
    """
    This class is used to have unified app names. App names in INSTALLED_APPS
    can have the form oscar.apps.shipping.apps.ShippingConfig or just oscar.apps.shipping.
    For purposes of replacement and extension, there should only be 1 way of identifying
    an app, so we normalize the app names in this table.
    """

    def __init__(self, installed_apps=None):
        self._apps = {}
        if installed_apps:
            for installed_app_name in installed_apps:
                self.add(installed_app_name)

    def get_app_config(self, installed_app_name):
        try:
            return AppConfig.create(installed_app_name)
        except (ModuleNotFoundError, ImportError, ImproperlyConfigured, AttributeError):
            return None

    def add(self, installed_app_name):
        app_config = self.get_app_config(installed_app_name)
        if app_config is not None:
            self._apps[installed_app_name] = app_config.name
            self._apps[app_config.name] = app_config.name
        else:
            self._apps[installed_app_name] = installed_app_name

    def get(self, installed_app_name):
        if not installed_app_name:
            return None

        if installed_app_name not in self._apps:
            self.add(installed_app_name)

        return self._apps[installed_app_name]


class ExtendableInstalledAppsItem(list):
    """
    To allow multiple apps to contribute to one and the same oscar app, it
    is needed to setup a registry for this. This class implements the registry.
    >>>
    """

    def __init__(
        self, iterable=(), extends=None, replaced_apps=None, app_name_table=None
    ):
        super(ExtendableInstalledAppsItem, self).__init__(iterable)
        self.extensions = collections.defaultdict(list) if extends is None else extends
        self.replaced_apps = set() if replaced_apps is None else replaced_apps
        self.app_name_table = (
            AppNameTable(replaced_apps) if app_name_table is None else app_name_table
        )

    def normalize_app_name(self, app_name):
        if not app_name:
            return None

        return self.app_name_table.get(app_name)

    def is_section_for_app(self, app_name_):
        """
        >>> INSTALLED_APPS = ExtendableInstalledAppsItem(["oscar.apps.catalogue", "zupke.henkie"])
        >>> INSTALLED_APPS.is_section_for_app("koe")
        False
        >>> INSTALLED_APPS.is_section_for_app("oscar.apps.catalogue")
        True
        >>> INSTALLED_APPS.is_section_for_app("zupke")
        False
        >>> INSTALLED_APPS.is_section_for_app("zupke.henkie")
        True
        >>> INSTALLED_APPS.register_extension("koe", "rarare.vogels")
        >>> INSTALLED_APPS.is_section_for_app("koe")
        True
        >>> INSTALLED_APPS.is_section_for_app("rarare.vogels")
        True
        >>> INSTALLED_APPS.is_section_for_app("rarare.vog")
        False
        >>> INSTALLED_APPS.append("zakhooi.catalogue", replaces="oscar.apps.catalogue")
        >>> INSTALLED_APPS.is_section_for_app("oscar.apps.catalogue")
        True
        >>> INSTALLED_APPS.is_section_for_app("zakhooi.catalogue")
        True
        >>> INSTALLED_APPS.append("keigaaf.catalogue_extension", extends="oscar.apps.catalogue")
        >>> INSTALLED_APPS.is_section_for_app("keigaaf.catalogue_extension")
        True
        >>> INSTALLED_APPS.is_section_for_app("oscar.apps.cat")
        False
        >>> INSTALLED_APPS.register_extension("oscar.apps.cat", "oscar.apps.catalogue")
        >>> INSTALLED_APPS.is_section_for_app("oscar.apps.cat")
        True
        """
        app_name = self.normalize_app_name(app_name_)
        listed_apps = chain(self, chain.from_iterable(self.extensions.values()))
        return (
            app_name in self.replaced_apps
            or app_name in self.extensions
            or any((app_name == app for app in listed_apps))
        )

    def extend(self, installed_app_names):
        """
        Add apps to the list
        """
        for installed_app_name in installed_app_names:
            self.append(installed_app_name)

    def __iadd__(self, installed_app_names):
        """
        Use this to append in place to installed apps.

        >>> INSTALLED_APPS = ExtendableInstalledAppsItem(["oscar.apps.catalogue", "zupke.henkie"])

        We can just add stuff to the INSTALLED_APPS like  this
        >>> INSTALLED_APPS += ["rikje.harkmaken" , "nog.ee.app"]
        >>> INSTALLED_APPS
        ['oscar.apps.catalogue', 'zupke.henkie', 'rikje.harkmaken', 'nog.ee.app']

        If apps are replaced, they are not added again when using +=

        >>> INSTALLED_APPS.append("zakhooi.catalogue", replaces="oscar.apps.catalogue")
        >>> INSTALLED_APPS
        ['zupke.henkie', 'rikje.harkmaken', 'nog.ee.app', 'zakhooi.catalogue']
        >>> INSTALLED_APPS += ["oscar.apps.catalogue", "dongel.type"]
        >>> INSTALLED_APPS
        ['zupke.henkie', 'rikje.harkmaken', 'nog.ee.app', 'zakhooi.catalogue', 'dongel.type']
        """
        self.extend(installed_app_names)
        return self

    def __add__(self, installed_app_names):
        """
        Use this to add to the INSTALLED_APPS

        >>> INSTALLED_APPS = ExtendableInstalledAppsItem(["oscar.apps.catalogue", "zupke.henkie"])

        We can just add stuff to the INSTALLED_APPS like  this
        >>> INSTALLED_APPS + ["rikje.harkmaken" , "nog.ee.app"]
        ['oscar.apps.catalogue', 'zupke.henkie', 'rikje.harkmaken', 'nog.ee.app']

        If apps are replaced, they are not added again when using +=

        >>> INSTALLED_APPS.append("zakhooi.catalogue", replaces="oscar.apps.catalogue")
        >>> INSTALLED_APPS
        ['zupke.henkie', 'zakhooi.catalogue']
        >>> INSTALLED_APPS + ["oscar.apps.catalogue", "dongel.type"]
        ['zupke.henkie', 'zakhooi.catalogue', 'dongel.type']
        """
        new_thing = ExtendableInstalledAppsItem(
            self,
            extends=self.extensions,
            replaced_apps=self.replaced_apps,
            app_name_table=self.app_name_table,
        )
        new_thing.extend(installed_app_names)
        return new_thing

    def register_extension(self, module_name_, extended_app_):
        """
        Register a non replacing app extension, this extension is NOT added
        to INSTALLED_APPS.

        >>> INSTALLED_APPS = ExtendableInstalledAppsItem(["oscar.apps.catalogue", "zupke.henkie"])
        >>> INSTALLED_APPS.register_extension("ocyan.plugin.oscar_catalogue.extras", "oscar.apps.catalogue")
        >>> INSTALLED_APPS
        ['oscar.apps.catalogue', 'zupke.henkie']
        >>> INSTALLED_APPS.extensions_for_app("oscar.apps.catalogue")
        ['ocyan.plugin.oscar_catalogue.extras']

        If you try something stupid it won't work
        >>> INSTALLED_APPS.register_extension("oscar.apps.catalogue", "oscar.apps.catalogue")
        >>> INSTALLED_APPS.extensions_for_app("oscar.apps.catalogue")
        ['ocyan.plugin.oscar_catalogue.extras']

        You can register multiple extensions for the same app
        >>> INSTALLED_APPS.register_extension("gekkehenkie.slimmejongen", "oscar.apps.catalogue")
        >>> INSTALLED_APPS.extensions_for_app("oscar.apps.catalogue")
        ['ocyan.plugin.oscar_catalogue.extras', 'gekkehenkie.slimmejongen']

        You can even register extension for app extensions themselves
        >>> INSTALLED_APPS.register_extension("gekkehenkie.rarejongen",'gekkehenkie.slimmejongen')
        >>> INSTALLED_APPS.extensions_for_app("oscar.apps.catalogue")
        ['gekkehenkie.rarejongen', 'ocyan.plugin.oscar_catalogue.extras', 'gekkehenkie.slimmejongen']
        """
        module_name = self.normalize_app_name(module_name_)
        extended_app = self.normalize_app_name(extended_app_)
        if module_name != extended_app:
            self.extensions[extended_app].append(module_name)

    def append(self, installed_app_name, replaces=None, extends=()):
        """
        Add an app to installed apps. The app might want to replace or extend
        an existing oscar app.

        If your app extension needs to change models, you MUST replace the app.
        This means your app will be put in INSTALLED_APPS instead of the app
        it replaces. You have the responsibility to deal with migrations, urls
        views and admin registrations associated with the app you are replacing.

        If your app extension does NOT do anything with models, you can just
        use extends, your app will be installed next to the replacing app

        :param replaces: The module name of the oscar app the appended app replaces (optional)
        :param extends: List or single instance of oscar app modules names which the appended app extends.
        :type extends: Either a list or a single app module name as a string.

        >>> INSTALLED_APPS = ExtendableInstalledAppsItem(["oscar.apps.catalogue", "zupke.henkie"])
        >>> INSTALLED_APPS
        ['oscar.apps.catalogue', 'zupke.henkie']
        >>> INSTALLED_APPS.append("zakhooi.catalogue", replaces="oscar.apps.catalogue")
        >>> INSTALLED_APPS
        ['zupke.henkie', 'zakhooi.catalogue']
        >>> INSTALLED_APPS.append("keigaaf.catalogue_extension", extends="oscar.apps.catalogue")
        >>> INSTALLED_APPS
        ['zupke.henkie', 'zakhooi.catalogue', 'keigaaf.catalogue_extension']
        >>> INSTALLED_APPS.extensions_for_app("oscar.apps.catalogue")
        ['zakhooi.catalogue', 'keigaaf.catalogue_extension']

        If the order of adding is wrong apps should still chain to the root app

        >>> INSTALLED_APPS = ExtendableInstalledAppsItem(["oscar.apps.catalogue", "zupke.henkie"])
        >>> INSTALLED_APPS.append("keigaaf.catalogue_extension", replaces="deze.app.is.er.nog.niet")
        >>> INSTALLED_APPS
        ['oscar.apps.catalogue', 'zupke.henkie', 'keigaaf.catalogue_extension']
        >>> INSTALLED_APPS.append("deze.app.is.er.nog.niet", replaces="oscar.apps.catalogue")
        >>> INSTALLED_APPS
        ['zupke.henkie', 'keigaaf.catalogue_extension']

        >>> INSTALLED_APPS = ExtendableInstalledAppsItem(["django.contrib.auth"])
        >>> INSTALLED_APPS.append("django.contrib.auth.apps.AuthConfig", replaces="django.contrib.contenttypes.apps.ContentTypesConfig")
        >>> INSTALLED_APPS.extensions_for_app("django.contrib.contenttypes")
        ['django.contrib.auth']

        >>> INSTALLED_APPS = ExtendableInstalledAppsItem(["django.contrib.auth"])
        >>> INSTALLED_APPS.append("django.contrib.auth.apps.AuthConfig", replaces="django.contrib.contenttypes")
        >>> INSTALLED_APPS.extensions_for_app("django.contrib.contenttypes")
        ['django.contrib.auth']

        >>> INSTALLED_APPS = ExtendableInstalledAppsItem(["django.contrib.auth"])
        >>> INSTALLED_APPS.append("django.contrib.auth.apps.AuthConfig", replaces="kloekie.henkie")
        >>> INSTALLED_APPS.extensions_for_app("kloekie.henkie")
        ['django.contrib.auth']
        """
        if isinstance(extends, str):
            extends = [extends]

        if replaces is not None:
            self.replaced_apps.add(replaces)
            # remove apps that are replaced from INSTALLED_APPS
            while replaces in self:
                self.remove(replaces)

            # register the extension
            self.register_extension(installed_app_name, replaces)

        for extended_app in extends:
            # register the extensions
            self.register_extension(installed_app_name, extended_app)

        if installed_app_name not in self.replaced_apps:
            super(ExtendableInstalledAppsItem, self).append(installed_app_name)

    def _extensions_for_app(self, app_name_):
        app_name = self.normalize_app_name(app_name_)
        extensions = self.extensions.get(app_name, [])
        result = []
        for extension in extensions:
            result.extend(self._extensions_for_app(extension))

        return unique_everseen(result + extensions)

    def _history_depth(self, app_name_, target_app_):
        "Determine how many replacements are between app_name and target_app"
        app_name = self.normalize_app_name(app_name_)
        target_app = self.normalize_app_name(target_app_)

        if app_name == target_app:
            return 1  # the target_app was found omg lets count this one!

        # get all the extensions for app_name
        extensions = self.extensions.get(app_name, [])

        # determine miximim distance between extensions and target_app
        current_depth = 0
        for extension in extensions:
            found_depth = self._history_depth(extension, target_app)
            if found_depth > current_depth:
                current_depth = found_depth

        # if the current_depth has changed, increment it, otherwise, just
        # return 0, because the target_app was not found.
        return current_depth + 1 if current_depth else 0

    def extensions_from_app_root(self, app_name):
        """
        Find the app root for ``app_name`` and return the extensions from
        the app root.

        The root of an app is the original application, before it was replaced.

        >>> INSTALLED_APPS = ExtendableInstalledAppsItem(["oscar.apps.catalogue", "zupke.henkie"])
        >>> INSTALLED_APPS.extensions_from_app_root("zupke.henkie")
        ['zupke.henkie']
        >>> INSTALLED_APPS.append("henk.den.berter", replaces="zakhooi.catalogue")
        >>> INSTALLED_APPS.append("zakhooi.catalogue", replaces="oscar.apps.catalogue")
        >>> INSTALLED_APPS.register_extension("ocyan.plugin.oscar_catalogue.extras", "zakhooi.catalogue")
        >>> INSTALLED_APPS.extensions_from_app_root("henk.den.berter")
        ['henk.den.berter', 'ocyan.plugin.oscar_catalogue.extras', 'zakhooi.catalogue', 'oscar.apps.catalogue']
        """
        current_depth = 0
        root = app_name
        for repl in self.replaced_apps:
            depth = self._history_depth(repl, app_name)
            if depth > current_depth:
                current_depth = depth
                root = repl

        return self.extensions_for_app(root) + [root]

    def extensions_for_app(self, app_name):
        """
        Return all the extensions that should be searched for classes for the
        given app_name.

        >>> INSTALLED_APPS = ExtendableInstalledAppsItem(["oscar.apps.catalogue", "zupke.henkie"])
        >>> INSTALLED_APPS.register_extension("ocyan.plugin.oscar_catalogue.extras", "oscar.apps.catalogue")
        >>> INSTALLED_APPS.extensions_for_app("oscar.apps.catalogue")
        ['ocyan.plugin.oscar_catalogue.extras']
        >>> INSTALLED_APPS.append("zakhooi.catalogue", replaces="oscar.apps.catalogue")
        >>> catalogue_extensions = INSTALLED_APPS.extensions_for_app("oscar.apps.catalogue")
        >>> catalogue_extensions
        ['ocyan.plugin.oscar_catalogue.extras', 'zakhooi.catalogue']
        >>> INSTALLED_APPS.append("keigaaf.catalogue_extension", extends="oscar.apps.catalogue")
        >>> INSTALLED_APPS.extensions_for_app("oscar.apps.catalogue")
        ['ocyan.plugin.oscar_catalogue.extras', 'zakhooi.catalogue', 'keigaaf.catalogue_extension']
        >>> INSTALLED_APPS.register_extension("liljon.skeet.skeet", "keigaaf.catalogue_extension")
        >>> INSTALLED_APPS.extensions_for_app("oscar.apps.catalogue")
        ['liljon.skeet.skeet', 'ocyan.plugin.oscar_catalogue.extras', 'zakhooi.catalogue', 'keigaaf.catalogue_extension']
        >>> INSTALLED_APPS.register_extension("third.level.extrensions", "liljon.skeet.skeet")
        >>> INSTALLED_APPS.extensions_for_app("oscar.apps.catalogue")
        ['third.level.extrensions', 'liljon.skeet.skeet', 'ocyan.plugin.oscar_catalogue.extras', 'zakhooi.catalogue', 'keigaaf.catalogue_extension']
        """
        return list(self._extensions_for_app(app_name))

    @property
    def representation(self):
        return {
            "INSTALLED_APPS": self,
            "extensions": {
                app: self.extensions_for_app(app) for app in self.extensions
            },
            "replaced_apps": list(self.replaced_apps),
            "raw": self.extensions,
        }


def oscar_sort_order(oscar_apps):
    """
    Put the dashboard apps at the end.

    >>> apps = [
    ...    "oscar.apps.dashboard.henk",
    ...    "oscar.apps.henk",
    ...    "oscar.apps.klakkie",
    ...    "oscar.apps.dashboard.klakkie",
    ...    "oscar"
    ... ]
    >>> sorted_apps = oscar_sort_order(apps)
    >>> list(sorted_apps)
    ['oscar.apps.henk', 'oscar.apps.klakkie', 'oscar.apps.dashboard.henk', 'oscar.apps.dashboard.klakkie', 'oscar']
    >>> apps = [
    ...    "oscar.apps.dashboard.klakkie",
    ...    "oscar.apps.dashboard.henk",
    ...    "oscar.apps.klakkie",
    ...    "oscar.apps.henk",
    ...    "oscar"
    ... ]
    >>> sorted_apps = oscar_sort_order(apps)
    >>> list(sorted_apps)
    ['oscar.apps.klakkie', 'oscar.apps.henk', 'oscar.apps.dashboard.klakkie', 'oscar.apps.dashboard.henk', 'oscar']
    """
    dashboard_apps = []
    oscar_found = False
    oscar_app_found = False
    for oscar_app_name in oscar_apps:
        if isinstance(oscar_app_name, str) and "dashboard" in oscar_app_name:
            dashboard_apps.append(oscar_app_name)
        elif oscar_app_name == "oscar":
            oscar_found = True
        elif oscar_app_name == "ocyan.plugin.oscar.apps.Shop":
            oscar_app_found = True
        else:
            yield oscar_app_name
    for oscar_app_name in dashboard_apps:
        yield oscar_app_name

    if oscar_found:
        yield "oscar"

    if oscar_app_found:
        yield "ocyan.plugin.oscar.apps.Shop"


class SegmentedInstalledApps(Collection):
    """
    A DataStructure that can be iterated over like a list, but supports
    different named segments: `front`, `django`, `apps`, `oscar`, `wagtail`.

    >>> henk = SegmentedInstalledApps(front=[1,2,3,2], oscar=['ha', 'oscar', 'ha', 2])
    >>> len(henk)
    8
    >>> 1 in henk
    True
    >>> 'ha' in henk
    True
    >>> 'hoe' in henk
    False
    >>> 4 in henk
    False
    >>> list(henk)
    [1, 2, 3, 2, 'ha', 'ha', 2, 'oscar']
    >>> henk.unique_list()
    [1, 2, 3, 'ha', 'oscar']
    >>> henk.ocyan.append("koe")
    >>> list(henk)
    [1, 2, 3, 2, 'koe', 'ha', 'ha', 2, 'oscar']
    >>> henk.ocyan.append("runt")
    >>> list(henk)
    [1, 2, 3, 2, 'koe', 'runt', 'ha', 'ha', 2, 'oscar']
    >>> henk.unique_list()
    [1, 2, 3, 'koe', 'runt', 'ha', 'oscar']
    """

    def __init__(
        self,
        front=(),
        adminoverrides=(),
        django=(),
        ocyan=(),
        apps=(),
        oscar=(),
        wagtail=(),
    ):
        self.front = ExtendableInstalledAppsItem(front)
        self.adminoverrides = ExtendableInstalledAppsItem(adminoverrides)
        self.django = ExtendableInstalledAppsItem(django)
        self.ocyan = ExtendableInstalledAppsItem(ocyan)
        self.apps = ExtendableInstalledAppsItem(apps)
        self.oscar = ExtendableInstalledAppsItem(oscar)
        self.wagtail = ExtendableInstalledAppsItem(wagtail)
        self.segments = (
            self.front,
            self.adminoverrides,
            self.django,
            self.ocyan,
            self.apps,
            self.oscar,
            self.wagtail,
        )

    def unique_list(self):
        return list(unique_everseen(self))

    def section_for_app_extension(self, app_name):
        try:
            return find_first(lambda x: x.is_section_for_app(app_name), self.segments)
        except StopIteration:
            return None

    def __iter__(self):
        "Keep oscar apps sorted"
        return chain(
            self.front,
            self.adminoverrides,
            self.django,
            self.ocyan,
            self.apps,
            oscar_sort_order(self.oscar),
            self.wagtail,
        )

    def __contains__(self, value):
        for segment in self.segments:
            if value in segment:
                return True

        return False

    def __len__(self):
        return sum(map(len, self.segments))

    def __repr__(self):
        return "<SegmentedInstalledApps %s />" % json.dumps(
            {
                "front": self.front.representation,
                "adminoverrides": self.adminoverrides.representation,
                "django": self.django.representation,
                "ocyan": self.ocyan.representation,
                "apps": self.apps.representation,
                "oscar": self.oscar.representation,
                "wagtail": self.wagtail.representation,
            },
            indent=2,
            cls=DjangoJSONEncoder,
        )
