import collections
import logging
from typing import Any, Sequence, Type

from .adapters import BaseAdapter, DotEnvAdapter, OsEnvAdapter, YavAdapter
from .exceptions import InitializationError, OptionNotFound
from .option import MISSING, Option

default_adapters = (OsEnvAdapter, DotEnvAdapter, YavAdapter)

logger = logging.getLogger(__name__)


class ConfigLoader(collections.abc.Mapping):
    __slots__ = "_adapters", "_adapters_cls", "_options", "_state"

    _adapters: Sequence[BaseAdapter]
    _adapters_cls: Sequence[Type[BaseAdapter]]
    _options: Sequence[Option]
    _state: dict

    def __init__(
        self, *options: Option, adapters: Sequence[Type[BaseAdapter]] = default_adapters
    ):
        self._options = options
        self._adapters_cls = adapters
        self._state = {}

    def _init_adapters(self):
        self._adapters = ()

        for adapter in self._adapters_cls:
            try:
                _args = (self._load_option(option) for option in adapter.dependencies)
                self._adapters += (adapter(*_args),)
            except OptionNotFound as exc:
                logger.warning(
                    "Adapter %s omitted because dependency %s not found",
                    adapter.__name__,
                    exc.args[0],
                )
            except InitializationError as exc:
                logger.warning(
                    "Adapter %s omitted because initialization errored with "
                    'message "%s"',
                    adapter.__name__,
                    exc.args[0],
                )

    def _load_option(self, option: Option) -> Any:
        for adapter in self._adapters:
            try:
                raw_value = adapter.load(option.load_from)
            except OptionNotFound:
                continue
            else:
                value = option.converter(raw_value)
                break
        else:
            if option.default != MISSING:
                value = option.default
            else:
                raise OptionNotFound(option.name)

        return value

    def init(self) -> None:
        self._init_adapters()
        self._state = {
            option.name: self._load_option(option) for option in self._options
        }

    def __getitem__(self, key: str) -> Any:
        return self._state[key]

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

    def __len__(self):
        return len(self._state)
