from __future__ import annotations

from abc import abstractmethod
from argparse import ArgumentTypeError
from concurrent.futures import Future
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any, Generator, List, Mapping, TypeVar

from tox.plugin import impl

from .convert import Convert, Factory
from .section import Section
from .str_convert import StrConvert

if TYPE_CHECKING:
    from tox.config.cli.parser import ToxParser
    from tox.config.main import Config


class Override:
    """
    An override for config definitions.
    """

    def __init__(self, value: str) -> None:
        key, equal, self.value = value.partition("=")
        if not equal:
            raise ArgumentTypeError(f"override {value} has no = sign in it")
        self.namespace, _, self.key = key.rpartition(".")

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}('{self}')"

    def __str__(self) -> str:
        return f"{self.namespace}{'.' if self.namespace else ''}{self.key}={self.value}"

    def __eq__(self, other: Any) -> bool:
        if type(self) != type(other):
            return False
        return (self.namespace, self.key, self.value) == (other.namespace, other.key, other.value)

    def __ne__(self, other: Any) -> bool:
        return not (self == other)


class ConfigLoadArgs:
    """Arguments that help loading a configuration value."""

    def __init__(self, chain: list[str] | None, name: str | None, env_name: str | None):
        """
        :param chain: the configuration chain (useful to detect circular references)
        :param name: the name of the configuration
        :param env_name: the tox environment this load is for
        """
        self.chain: list[str] = chain or []
        self.name = name
        self.env_name = env_name

    def copy(self) -> ConfigLoadArgs:
        """:return: create a copy of the object"""
        return ConfigLoadArgs(self.chain.copy(), self.name, self.env_name)


OverrideMap = Mapping[str, List[Override]]

T = TypeVar("T")
V = TypeVar("V")


class Loader(Convert[T]):
    """Loader loads a configuration value and converts it."""

    def __init__(self, section: Section, overrides: list[Override]) -> None:
        self._section = section
        self.overrides = {o.key: o for o in overrides}
        self.parent: Loader[Any] | None = None

    @property
    def section(self) -> Section:
        return self._section

    @abstractmethod
    def load_raw(self, key: str, conf: Config | None, env_name: str | None) -> T:
        """
        Load the raw object from the config store.

        :param key: the key under what we want the configuration
        :param env_name: load for env name
        :param conf: the global config object
        """
        raise NotImplementedError

    @abstractmethod
    def found_keys(self) -> set[str]:
        """A list of configuration keys found within the configuration."""
        raise NotImplementedError

    def __repr__(self) -> str:
        return f"{type(self).__name__}"

    def __contains__(self, item: str) -> bool:
        return item in self.found_keys()

    def load(
        self,
        key: str,
        of_type: type[V],
        factory: Factory[V],
        conf: Config | None,
        args: ConfigLoadArgs,
    ) -> V:
        """
        Load a value (raw and then convert).

        :param key: the key under it lives
        :param of_type: the type to convert to
        :param factory: factory method to build the object
        :param conf: the configuration object of this tox session (needed to manifest the value)
        :param args: the config load arguments
        :return: the converted type
        """
        if key in self.overrides:
            return _STR_CONVERT.to(self.overrides[key].value, of_type, factory)
        raw = self.load_raw(key, conf, args.env_name)
        future: Future[V] = Future()
        with self.build(future, key, of_type, conf, raw, args) as prepared:
            converted = self.to(prepared, of_type, factory)
            future.set_result(converted)
        return converted

    @contextmanager
    def build(
        self,
        future: Future[V],  # noqa: U100
        key: str,  # noqa: U100
        of_type: type[V],  # noqa: U100
        conf: Config | None,  # noqa: U100
        raw: T,
        args: ConfigLoadArgs,  # noqa: U100
    ) -> Generator[T, None, None]:
        """
        Materialize the raw configuration value from the loader.

        :param future: a future which when called will provide the converted config value
        :param key: the config key
        :param of_type: the config type
        :param conf: the global config
        :param raw: the raw value
        :param args: env args
        """
        yield raw


@impl
def tox_add_option(parser: ToxParser) -> None:
    parser.add_argument(
        "-x",
        "--override",
        action="append",
        type=Override,
        default=[],
        dest="override",
        help="configuration override(s)",
    )


_STR_CONVERT = StrConvert()
