"""Plugin mixin class for AppMixin."""

from importlib import reload
from pathlib import Path
from typing import Optional

from django.apps import apps
from django.conf import settings
from django.contrib import admin

import structlog

from InvenTree.config import get_plugin_dir
from plugin import PluginMixinEnum

logger = structlog.get_logger('inventree')


class AppMixin:
    """Mixin that enables full django app functions for a plugin."""

    class MixinMeta:
        """Meta options for this mixin."""

        MIXIN_NAME = 'App registration'

    def __init__(self):
        """Register mixin."""
        super().__init__()
        self.add_mixin(PluginMixinEnum.APP, 'has_app', __class__)

    @classmethod
    def _activate_mixin(
        cls,
        registry,
        plugins,
        force_reload=False,
        full_reload: bool = False,
        _internal: Optional[list] = None,
        *args,
        **kwargs,
    ):
        """Activate AppMixin plugins - add custom apps and reload.

        Args:
            registry (PluginRegistry): The registry that should be used
            plugins (dict): List of IntegrationPlugins that should be installed
            force_reload (bool, optional): Only reload base apps. Defaults to False.
            full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
            _internal (dict, optional): Internal use only (for testing). Defaults to None.
        """
        from common.settings import get_global_setting

        if settings.PLUGIN_TESTING or get_global_setting('ENABLE_PLUGINS_APP'):
            logger.info('Registering IntegrationPlugin apps')
            apps_changed = False

            # add them to the INSTALLED_APPS
            for _key, plugin in plugins:
                if plugin.mixin_enabled(PluginMixinEnum.APP):
                    plugin_path = cls._get_plugin_path(plugin)
                    if plugin_path not in settings.INSTALLED_APPS:
                        settings.INSTALLED_APPS += [plugin_path]
                        registry.installed_apps += [plugin_path]
                        apps_changed = True

            # if apps were changed or force loading base apps -> reload
            # Ignore reloading if we are in testing mode and apps are unchanged so that tests run faster
            # registry.reload_plugins(...) first unloads and then loads the plugins
            # always reload if we are not in testing mode so we can expect the second reload
            if not settings.TESTING or apps_changed or force_reload:
                # first startup or force loading of base apps -> registry is prob false
                if registry.apps_loading or force_reload:
                    registry.apps_loading = False
                    registry._reload_apps(
                        force_reload=True, full_reload=full_reload, _internal=_internal
                    )
                else:
                    registry._reload_apps(full_reload=full_reload, _internal=_internal)

                # rediscover models/ admin sites
                cls._reregister_contrib_apps(cls, registry)

                # update urls - must be last as models must be registered for creating admin routes
                registry._update_urls()

    @classmethod
    def _deactivate_mixin(cls, registry, force_reload: bool = False):
        """Deactivate AppMixin plugins - some magic required.

        Args:
            registry (PluginRegistry): The registry that should be used
            force_reload (bool, optional): Also reload base apps. Defaults to False.
        """
        # unregister models from admin
        for plugin_path in registry.installed_apps:
            models = []  # the modelrefs need to be collected as popping an item in a iter is not welcomed
            app_name = plugin_path.split('.')[-1]
            try:
                app_config = apps.get_app_config(app_name)

                # check all models
                for model in app_config.get_models():
                    # remove model from admin site
                    try:
                        admin.site.unregister(model)
                    except Exception:  # pragma: no cover
                        pass
                    models += [model._meta.model_name]
            except LookupError:  # pragma: no cover
                # if an error occurs the app was never loaded right -> so nothing to do anymore
                logger.debug('%s App was not found during deregistering', app_name)
                break

            # unregister the models (yes, models are just kept in multilevel dicts)
            # Note: Django registers models under the app_label (app_name),
            # not the full dotted plugin_path. For plugins with nested module
            # paths (e.g. "myplugin.myplugin"), plugin_path != app_name, so
            # using plugin_path here would look up the wrong key in the
            # defaultdict and raise KeyError on .pop().
            for model in models:
                # remove model from general registry
                apps.all_models[app_name].pop(model, None)

            # clear the registry for that app
            # so that the import trick will work on reloading the same plugin
            # -> the registry is kept for the whole lifecycle
            if models and app_name in apps.all_models:
                apps.all_models.pop(app_name)

        # remove plugin from installed_apps
        registry._clean_installed_apps()

        # reset load flag and reload apps
        settings.INTEGRATION_APPS_LOADED = False
        registry._reload_apps(force_reload=force_reload)

        # update urls to remove the apps from the site admin
        registry._update_urls()

    # region helpers
    def _reregister_contrib_apps(self, registry):
        """Fix reloading of contrib apps - models and admin.

        This is needed if plugins were loaded earlier and then reloaded as models and admins rely on imports.
        Those register models and admin in their respective objects (e.g. admin.site for admin).
        """
        for plugin_path in registry.installed_apps:
            try:
                app_name = plugin_path.split('.')[-1]
                app_config = apps.get_app_config(app_name)
            except LookupError:  # pragma: no cover
                # the plugin was never loaded correctly
                logger.debug('%s App was not found during deregistering', app_name)
                break

            # reload models if they were set
            # models_module gets set if models were defined - even after multiple loads
            # on a reload the models registry is empty but models_module is not
            if app_config.models_module and len(app_config.models) == 0:
                reload(app_config.models_module)

            # check for all models if they are registered with the site admin
            model_not_reg = False
            for model in app_config.get_models():
                if not admin.site.is_registered(model):
                    model_not_reg = True

            # reload admin if at least one model is not registered
            # models are registered with admin in the 'admin.py' file - so we check
            # if the app_config has an admin module before trying to load it
            if model_not_reg and hasattr(app_config.module, 'admin'):
                reload(app_config.module.admin)

    @classmethod
    def _get_plugin_path(cls, plugin):
        """Parse plugin path.

        The input can be either:
        - a local file / dir
        - a package
        """
        path = plugin.path()
        custom_plugins_dir = get_plugin_dir()

        if path.is_relative_to(settings.BASE_DIR):
            # Plugins which are located relative to the base code directory
            plugin_path = '.'.join(path.relative_to(settings.BASE_DIR).parts)
        elif custom_plugins_dir and path.is_relative_to(custom_plugins_dir):
            # Plugins which are located relative to the custom plugins directory
            plugin_path = '.'.join(path.relative_to(custom_plugins_dir).parts)

            # Ensure that the parent directory is added also
            plugin_path = Path(custom_plugins_dir).parts[-1] + '.' + plugin_path
        else:
            # plugin is shipped as package - extract plugin module name
            plugin_path = plugin.__module__.split('.')[0]

        return plugin_path

    # endregion

    @property
    def has_app(self):
        """This plugin is always an app with this plugin."""
        return True
