"""
:mod:`zsl.application.service_application`
------------------------------------------

The module contains the main Zsl class - :class:`.ServiceApplication`. It is
responsible for gluing everything together.

There is a simple profile support. Profile is usually tied to a configuration
file for an environment. Usually environments slightly differ in settings -
especially connection strings. These twists can be managed via additional
configuration files with the `cfg` extension usually placed in the `settings`
package. The name of the environment configuration file is controlled via
`ZSL_SETTINGS` environment variable.


.. data:: SETTINGS_ENV_VAR_NAME

    Name of the environment variable to be read for the profile configuration
    file.

"""
import os
from typing import Any, Callable

from flask import Config, Flask
from injector import Binder, Injector, singleton

from zsl._state import set_current_app
from zsl.application.initialization_context import InitializationContext
from zsl.utils.warnings import deprecated
from zsl.version import version

# Name of the environment variable to be read for the profile configuration.
SETTINGS_ENV_VAR_NAME = 'ZSL_SETTINGS'


def get_settings_from_profile(profile, profile_dir=None):
    # type: (str, Any)->str
    """"Returns the  configuration file path for the given profile.

    :param profile: Profile name to be used.
    :param profile_dir: The directory where the profile configuration file should reside. It
                        may be also a module, and then the directory of the module is used.
    :return: Configuration file path.
    """
    if profile_dir is None:
        import settings
        profile_dir = settings

    if hasattr(profile_dir, '__file__'):
        profile_dir = os.path.dirname(profile_dir.__file__)

    return os.path.join(profile_dir, '{0}.cfg'.format(profile))


def set_profile(profile):
    # type (str)->None
    """Sets the current profile.

    :param profile: The profile to be used."""
    os.environ[SETTINGS_ENV_VAR_NAME] = get_settings_from_profile(profile)


class ServiceApplication(Flask):
    """Atteq Service Flask application."""

    VERSION = version

    def __init__(
        self,
        import_name,
        static_url_path=None,
        static_folder="static",
        static_host=None,
        host_matching=False,
        subdomain_matching=False,
        template_folder="templates",
        instance_path=None,
        instance_relative_config=False,
        root_path=None,
        modules=None,
        version=None,
        config_object=None,
        default_settings_module='settings.default_settings'
    ):
        super().__init__(
            import_name,
            static_url_path,
            static_folder,
            static_host,
            host_matching,
            subdomain_matching,
            template_folder,
            instance_path,
            instance_relative_config,
            root_path
        )
        self._dependencies_initialized = False
        self._default_settings_module = default_settings_module
        self._is_initialized = False
        self._injector = None
        self._worker = None
        self._configure(config_object)
        self._app_version = version
        if not modules:
            from zsl.application.containers.core_container import CoreContainer
            modules = CoreContainer.modules()
        self._configure_injector(modules)
        self._initialize()
        self._dependencies_initialized = True

    def __str__(self):
        return "ZSL(application={0}, zsl_version={1}, app_version={2})".format(
            self.name, self.VERSION,
            self._app_version)

    def _configure(self, config_object=None):
        # type: (Any) -> None
        """Read the configuration from config files.
        Loads the default settings and the profile settings if available.
        Check :func:`.set_profile`.

        :param config_object:
            This parameter is the configuration decscription may be a dict or
            string describing the module from which the configuration is used.
            Default is settings.default_settings.
        """

        if config_object:
            self.config.from_mapping(config_object)
        else:
            self.config.from_object(self._default_settings_module)

        zsl_settings = os.environ.get(SETTINGS_ENV_VAR_NAME)
        if zsl_settings is not None:
            self.config.from_envvar(SETTINGS_ENV_VAR_NAME)

    def _initialize(self):
        """Run the initializers."""
        ctx = self.injector.get(InitializationContext)
        ctx.initialize()

    def _register(self):
        """Register the current instance into application stack."""
        set_current_app(self)

    def _get_app_module(self):
        # type: () -> Callable
        """Returns a module which binds the current app and configuration.

        :return: configuration callback
        :rtype: Callable
        """

        def configure(binder):
            # type: (Binder) -> Callable
            binder.bind(ServiceApplication, to=self, scope=singleton)
            binder.bind(Config, to=self.config, scope=singleton)

        return configure

    def _configure_injector(self, modules):
        """Create the injector and install the modules.

        There is a necessary order of calls. First we have to bind `Config` and
        `Zsl`, then we need to register the app into the global stack and then
        we can install all other modules, which can use `Zsl` and `Config`
        injection.

        :param modules: list of injection modules
        :type modules: list
        """
        self._register()
        self._create_injector()
        self._bind_core()
        self._bind_modules(modules)
        self.logger.debug("Injector configuration with modules {0}.".format(modules))
        self._dependencies_initialized = True

    @deprecated
    def get_initialization_context(self):
        return self.injector.get(InitializationContext)

    def is_initialized(self):
        return self._dependencies_initialized

    @property
    def injector(self):
        # type: () -> Injector
        return self._injector

    @injector.setter
    def injector(self, value):
        self._injector = value

    def get_version(self):
        return self.version

    @property
    def version(self):
        app_version = self.app_version
        if app_version is None:
            return self.zsl_version
        else:
            return self.zsl_version + ":" + app_version

    @property
    def zsl_version(self):
        return ServiceApplication.VERSION

    @property
    def app_version(self):
        return self._app_version

    def _create_injector(self):
        self.logger.debug('Creating injector')
        self._injector = Injector()

    def _bind_core(self):
        self._injector.binder.bind(ServiceApplication, self, singleton)
        self._injector.binder.bind(Config, self.config, singleton)

    def _bind_modules(self, modules):
        for module in modules:
            self.injector.binder.install(module)
