"""Main module."""
import asyncio
import errno
import json
import logging
import os
from threading import Lock
from typing import Any, KeysView, List, Tuple, MutableMapping, Dict

from configalchemy.field import Field
from configalchemy.meta import ConfigMeta, ConfigMetaJSONEncoder

ConfigType = MutableMapping[str, Any]


class BaseConfig(ConfigType):
    """Initialize the :any:`Config` with the Priority::

        ** config from env > config from local file > config from function > default config **

    Example of class-based configuration::

        class DefaultConfig(BaseConfig):
            TEST = "test"

        config = DefaultConfig()
    """

    # The prefix to construct the full environment variable key to access overrode config.
    CONFIGALCHEMY_ENV_PREFIX = ""
    CONFIGALCHEMY_ENVIRONMENT_VALUE_PRIORITY = 20

    # The the filename of the JSON file. This can either be
    # an absolute filename or a filename relative to the
    # `CONFIGALCHEMY_ROOT_PATH`.
    CONFIGALCHEMY_ROOT_PATH = ""
    CONFIGALCHEMY_CONFIG_FILE = ""
    CONFIGALCHEMY_CONFIG_FILE_VALUE_PRIORITY = 10
    # set to ``True`` if you want silent failure for missing files.
    CONFIGALCHEMY_LOAD_FILE_SILENT = False

    # set to ``True`` if you want to override config from function return value.
    CONFIGALCHEMY_ENABLE_FUNCTION = False
    CONFIGALCHEMY_FUNCTION_VALUE_PRIORITY = 0

    # The priority of config['TEST'] = value,
    # config.TEST = value and
    # config.update(TEST=value)
    CONFIGALCHEMY_SETITEM_PRIORITY = 99

    def __init__(self):
        self.lock = Lock()
        self.meta: Dict[str, ConfigMeta] = {}

        self._setup()

        #: env
        if self.CONFIGALCHEMY_ENV_PREFIX:
            self._from_env()

        #: config file
        if self.CONFIGALCHEMY_CONFIG_FILE:
            self._from_file()

        #: function
        if self.CONFIGALCHEMY_ENABLE_FUNCTION:
            #: Sync
            self.access_config_from_function(
                priority=self.CONFIGALCHEMY_FUNCTION_VALUE_PRIORITY
            )

            # Async
            loop = asyncio.get_event_loop()
            loop.run_until_complete(
                self.access_config_from_coroutine(
                    priority=self.CONFIGALCHEMY_FUNCTION_VALUE_PRIORITY
                )
            )
        super().__init__()

    def _setup(self):
        """Setup the default values and field of value from self.
        """
        for key in dir(self):
            if key.isupper():
                self[key] = getattr(self, key)
        return True

    def _from_file(self) -> bool:
        """Updates the values in the config from a JSON file. This function
        behaves as if the JSON object was a dictionary and passed to the
        :meth:`from_mapping` function.
        """
        filename = os.path.join(
            self.CONFIGALCHEMY_ROOT_PATH, self.CONFIGALCHEMY_CONFIG_FILE
        )
        try:
            with open(filename) as f:
                obj = json.load(f)
        except IOError as e:
            if self.CONFIGALCHEMY_LOAD_FILE_SILENT and e.errno in (
                errno.ENOENT,
                errno.EISDIR,
            ):
                return False
            e.strerror = "Unable to load configuration file (%s)" % e.strerror
            raise
        else:
            logging.info(f"Loaded configuration file: {filename}")
            return self.from_mapping(
                obj, priority=self.CONFIGALCHEMY_CONFIG_FILE_VALUE_PRIORITY
            )

    def from_mapping(self, *mappings: ConfigType, priority: int) -> bool:
        """Updates the config like :meth:`update` ignoring items with non-upper
        keys.
        """
        for mapping in mappings:
            for key, value in mapping.items():
                if key.isupper():
                    self._set_value(key, value, priority=priority)
        return True

    def _from_env(self) -> bool:
        """Updates the values in the config from the environment variable.
        """
        for key, value in self.items():
            env_value = os.getenv(f"{self.CONFIGALCHEMY_ENV_PREFIX}{key}")
            if env_value is not None:
                self._set_value(
                    key,
                    env_value,
                    priority=self.CONFIGALCHEMY_ENVIRONMENT_VALUE_PRIORITY,
                )
        return True

    def sync_function(self) -> ConfigType:
        return {}

    async def async_function(self) -> ConfigType:
        return {}

    def access_config_from_function(self, priority: int) -> bool:
        """Updates the values in the config from the sync_access_config_list.
        """
        self.from_mapping(self.sync_function(), priority=priority)
        return True

    async def access_config_from_coroutine(self, priority: int) -> bool:
        """Async updates the values in the config from the async_access_config_list.
        """
        self.from_mapping(await self.async_function(), priority=priority)
        return True

    def _set_value(self, key: str, value: Any, priority: int):
        with self.lock:
            if key not in self.meta:
                self.meta[key] = ConfigMeta(
                    default_value=value,
                    field=Field(
                        name=key,
                        default_value=value,
                        value_type=getattr(self, "__annotations__", {}).get(key),
                    ),
                )
                setattr(self.__class__, key, _ConfigAttribute(key, value))
            else:
                self.meta[key].set(priority=priority, value=value)

    def __getitem__(self, key: str) -> Any:
        """ x.__getitem__(y) <==> x[y] """
        with self.lock:
            return self.meta[key].value

    def items(self) -> List[Tuple[str, Any]]:  # type: ignore
        return [(key, config_meta.value) for key, config_meta in self.meta.items()]

    def keys(self) -> KeysView[str]:
        return self.meta.keys()

    def __contains__(self, key: object) -> bool:
        return key in self.meta

    def __iter__(self):
        return iter(self.meta)

    def __len__(self) -> int:
        return len(self.meta)

    def __setitem__(self, k, v) -> None:
        self._set_value(k, v, priority=self.CONFIGALCHEMY_SETITEM_PRIORITY)

    def __delitem__(self, key) -> None:
        del self.meta[key]

    def update(self, __m, **kwargs):
        self.from_mapping(__m, kwargs, priority=self.CONFIGALCHEMY_SETITEM_PRIORITY)

    def get(self, key: str, default=None):
        with self.lock:
            if key in self.meta:
                return self.meta[key].value
            else:
                return default

    def __bool__(self) -> bool:
        return bool(self.meta)

    def __repr__(self) -> str:
        return repr(self.meta)

    def __str__(self) -> str:
        return repr(self)

    def json(
        self,
        skipkeys=False,
        ensure_ascii=True,
        check_circular=True,
        allow_nan=True,
        sort_keys=False,
        indent=None,
        separators=None,
    ) -> str:
        return json.dumps(
            self.meta,
            cls=ConfigMetaJSONEncoder,
            skipkeys=skipkeys,
            ensure_ascii=ensure_ascii,
            check_circular=check_circular,
            allow_nan=allow_nan,
            indent=indent,
            separators=separators,
            sort_keys=sort_keys,
        )


class _ConfigAttribute:
    def __init__(self, name: str, default_value: Any):
        self._name = name
        self._default_value = default_value

    def __get__(self, obj: BaseConfig, type=None) -> Any:
        if obj is None:
            return self
        if self._name not in obj:
            return self._default_value
        else:
            return obj[self._name]

    def __set__(self, instance: BaseConfig, value: Any) -> None:
        instance[self._name] = value
