import copy

from flatten_dict import flatten
from server.common.errors import ConfigurationError


class BaseConfig(object):
    """
    This class handles the mechanics of updating and checking attributes.
    Derived classes are expected to store the actual attributes
    Currently DatasetConfig and ServerConfig both inherit from BaseConfig.
    """

    def __init__(self, app_config, default_config, dictval_cases={}):
        # reference back to the app_config
        self.app_config = app_config
        # the complete set of attributes and their default values (unflattened)
        self.default_config = default_config
        # attributes where the value may be a dict (and therefore are not flattened)
        self.dictval_cases = dictval_cases
        # used to make sure every attribute value is checked
        self.attr_checked = {key_name: False for key_name in self.create_mapping(default_config).keys()}

    def create_mapping(self, config):
        """
        Create a dictionary where the keys are the name of attributes (using double underscore convention)
        For example: authentication__type

        The values are a tuple,
        - the first item of the tuple is a tuple of path elements (location in config 'tree')
        - the second item is the value of the config parameter

        For example: (('authentication', 'type'), 'session'))
        """
        config_copy = copy.deepcopy(config)
        mapping = {}

        # special cases where the value could be a dict.
        # If its value is not None, the entry is added to the mapping, and not included
        # in the flattening below.
        for dictval_case in self.dictval_cases:
            cur = config_copy
            for part in dictval_case[:-1]:
                cur = cur.get(part, {})
            val = cur.get(dictval_case[-1])
            if val is not None:
                key = "__".join(dictval_case)
                mapping[key] = (dictval_case, val)
                del cur[dictval_case[-1]]

        flat_config = flatten(config_copy)
        for key, value in flat_config.items():
            # name of the attribute
            attr = "__".join(key)
            mapping[attr] = (key, value)

        return mapping

    def validate_correct_type_of_configuration_attribute(self, attrname, vtype):
        val = getattr(self, attrname)
        if type(vtype) in (list, tuple):
            if type(val) not in vtype:
                tnames = ",".join([x.__name__ for x in vtype])
                raise ConfigurationError(
                    f"Invalid type for attribute: {attrname}, expected types ({tnames}), got {type(val).__name__}"
                )
        else:
            if type(val) != vtype:
                raise ConfigurationError(
                    f"Invalid type for attribute: {attrname}, "
                    f"expected type {vtype.__name__}, got {type(val).__name__}"
                )

        self.attr_checked[attrname] = True

    def check_config(self):
        mapping = self.create_mapping(self.default_config)
        for key in mapping.keys():
            if not self.attr_checked[key]:
                raise ConfigurationError(f"The attr '{key}' has not been checked")

    def update(self, **kw):
        """Update the attributes defined in kw with their new values."""
        for key, value in kw.items():
            if not hasattr(self, key):

                # check if the key is setting into a dictval entry.
                found_dictval = False
                for dictval in self.dictval_cases:
                    dictvalname = "__".join(dictval)
                    if dictvalname + "__" in key:
                        dictkey = key[len(dictvalname) + 2 :]
                        curdictval = getattr(self, dictvalname)
                        if curdictval is None:
                            setattr(self, dictvalname, dict(dictkey=value))
                        else:
                            curdictval[dictkey] = value

                        found_dictval = True
                        break

                if found_dictval:
                    continue
                raise ConfigurationError(f"unknown config parameter {key}.")
            try:
                if type(value) == tuple:
                    # convert tuple values to list values
                    value = list(value)
                setattr(self, key, value)
            except KeyError:
                raise ConfigurationError(f"Unable to set config parameter {key}.")

            self.attr_checked[key] = False

    def update_from_config(self, config, prefix):
        mapping = self.create_mapping(config)
        for attr, (key, value) in mapping.items():
            if not hasattr(self, attr):
                raise ConfigurationError(f"Unknown key from config file: {prefix}__{attr}")
            setattr(self, attr, value)

            self.attr_checked[attr] = False

    def changes_from_default(self):
        """Return all the attribute that are different from the default"""
        mapping = self.create_mapping(self.default_config)
        diff = []
        for attrname, (key, defval) in mapping.items():
            curval = getattr(self, attrname)
            if curval != defval:
                diff.append((attrname, curval, defval))
        return diff
