from __future__ import unicode_literals

import errno
import hashlib
from importlib import import_module
import logging
import os
import sys

import yaml

try:
    from yaml import CSafeLoader as SafeLoader, CSafeDumper as SafeDumper
except ImportError:
    from yaml import SafeLoader, SafeDumper

from django.apps import apps
from django.conf import settings
from django.utils.functional import Promise
from django.utils.encoding import (
    force_bytes, force_text, python_2_unicode_compatible
)

logger = logging.getLogger(__name__)
SMART_SETTINGS_NAMESPACES_NAME = 'SMART_SETTINGS_NAMESPACES'


@python_2_unicode_compatible
class Namespace(object):
    _registry = {}

    @staticmethod
    def initialize():
        for app in apps.get_app_configs():
            try:
                import_module('{}.settings'.format(app.name))
            except ImportError as exception:
                if force_text(exception) not in ('No module named settings', 'No module named \'{}.settings\''.format(app.name)):
                    logger.error(
                        'Error importing %s settings.py file; %s', app.name,
                        exception
                    )

    @classmethod
    def get(cls, name):
        return cls._registry[name]

    @classmethod
    def get_all(cls):
        return sorted(cls._registry.values(), key=lambda x: x.label)

    @classmethod
    def get_namespace_config(cls, name):
        return cls.get_namespaces_config().get(name, {})

    @classmethod
    def get_namespaces_config(cls):
        return getattr(settings, SMART_SETTINGS_NAMESPACES_NAME, {})

    @classmethod
    def invalidate_cache_all(cls):
        for namespace in cls.get_all():
            namespace.invalidate_cache()

    def __init__(self, name, label, version='0001'):
        if name in self.__class__._registry:
            raise Exception(
                'Namespace names must be unique; "%s" already exists.' % name
            )
        self.name = name
        self.label = label
        self.version = version
        self.__class__._registry[name] = self
        self._settings = []

    def __str__(self):
        return force_text(self.label)

    def add_setting(self, **kwargs):
        return Setting(namespace=self, **kwargs)

    def get_config_version(self):
        return Namespace.get_namespace_config(name=self.name).get('version', None)

    def invalidate_cache(self):
        for setting in self._settings:
            setting.invalidate_cache()

    @property
    def settings(self):
        return sorted(self._settings, key=lambda x: x.global_name)


@python_2_unicode_compatible
class Setting(object):
    _registry = {}
    _cache_hash = None

    @staticmethod
    def deserialize_value(value):
        return yaml.load(stream=value, Loader=SafeLoader)

    @staticmethod
    def express_promises(value):
        """
        Walk all the elements of a value and force promises to text
        """
        if isinstance(value, (list, tuple)):
            return [Setting.express_promises(item) for item in value]
        elif isinstance(value, Promise):
            return force_text(value)
        else:
            return value

    @staticmethod
    def serialize_value(value):
        result = yaml.dump(
            data=Setting.express_promises(value), allow_unicode=True,
            Dumper=SafeDumper
        )
        # safe_dump returns bytestrings
        # Disregard the last 3 dots that mark the end of the YAML document
        if force_text(result).endswith('...\n'):
            result = result[:-4]

        return result

    @classmethod
    def check_changed(cls):
        if not cls._cache_hash:
            cls._cache_hash = cls.get_hash()

        return cls._cache_hash != cls.get_hash()

    @classmethod
    def dump_data(cls, filter_term=None, namespace=None):
        dictionary = {}

        if not namespace:
            namespace_dictionary = {}
            for _namespace in Namespace.get_all():
                namespace_dictionary[_namespace.name] = {
                    'version': _namespace.version
                }

            dictionary[SMART_SETTINGS_NAMESPACES_NAME] = namespace_dictionary

        for setting in cls.get_all():
            # If a namespace is specified, filter the list by that namespace
            # otherwise return always True to include all (or not None == True)
            if (namespace and setting.namespace.name == namespace) or not namespace:
                if (filter_term and filter_term.lower() in setting.global_name.lower()) or not filter_term:
                    dictionary[setting.global_name] = Setting.express_promises(setting.value)

        return yaml.dump(
            data=dictionary, default_flow_style=False, Dumper=SafeDumper
        )

    @classmethod
    def get(cls, global_name):
        return cls._registry[global_name]

    @classmethod
    def get_all(cls):
        return sorted(cls._registry.values(), key=lambda x: x.global_name)

    @classmethod
    def get_hash(cls):
        return force_text(
            hashlib.sha256(force_bytes(cls.dump_data())).hexdigest()
        )

    @classmethod
    def save_configuration(cls, path=None):
        if not path:
            path = settings.CONFIGURATION_FILEPATH

        try:
            with open(path, mode='w') as file_object:
                file_object.write(cls.dump_data())
        except IOError as exception:
            if exception.errno == errno.ENOENT:
                logger.warning(
                    'The path to the configuration file doesn\'t '
                    'exist. It is not possible to save the backup file.'
                )

    @classmethod
    def save_last_known_good(cls):
        # Don't write over the last good configuration if we are trying
        # to restore the last good configuration
        if 'revertsettings' not in sys.argv:
            cls.save_configuration(
                path=settings.CONFIGURATION_LAST_GOOD_FILEPATH
            )

    def __init__(self, namespace, global_name, default, help_text=None, is_path=False, quoted=False):
        self.global_name = global_name
        self.default = default
        self.help_text = help_text
        self.loaded = False
        self.namespace = namespace
        self.quoted = quoted
        self.environment_variable = False
        namespace._settings.append(self)
        self.__class__._registry[global_name] = self

    def __str__(self):
        return force_text(self.global_name)

    def cache_value(self):
        environment_value = os.environ.get('MAYAN_{}'.format(self.global_name))
        if environment_value:
            self.environment_variable = True
            try:
                self.raw_value = environment_value
            except yaml.YAMLError as exception:
                raise type(exception)(
                    'Error interpreting environment variable: {} with '
                    'value: {}; {}'.format(
                        self.global_name, environment_value, exception
                    )
                )
        else:
            self.raw_value = getattr(settings, self.global_name, self.default)
        self.yaml = Setting.serialize_value(self.raw_value)
        self.loaded = True

    def invalidate_cache(self):
        self.loaded = False

    @property
    def serialized_value(self):
        """
        YAML serialize value of the setting.
        Used for UI display.
        """
        if not self.loaded:
            self.cache_value()

        return self.yaml

    @property
    def value(self):
        if not self.loaded:
            self.cache_value()

        return self.raw_value

    @value.setter
    def value(self, value):
        # value is in YAML format
        self.yaml = value
        self.raw_value = Setting.deserialize_value(value)
