from __future__ import annotations

import inspect
import logging
from pathlib import Path
from types import GenericAlias
from typing import TYPE_CHECKING, Any, TypeVar, cast

from tox.config.loader.api import ConfigLoadArgs, Loader, Override
from tox.config.loader.replacer import MatchError, replace
from tox.config.set_env import SetEnv
from tox.config.types import Command, EnvList
from tox.report import HandledError

from ._api import TomlTypes
from ._replace import TomlReplaceLoader, Unroll
from ._validate import validate

if TYPE_CHECKING:
    from collections.abc import Iterator, Mapping
    from types import UnionType

    from tox.config.loader.convert import Factory
    from tox.config.loader.section import Section
    from tox.config.main import Config

_T = TypeVar("_T")
_V = TypeVar("_V")


class TomlLoader(Loader[TomlTypes]):
    """Load configuration from a pyproject.toml file."""

    def __init__(
        self,
        section: Section,
        overrides: list[Override],
        content: Mapping[str, TomlTypes],
        root_content: Mapping[str, TomlTypes],
        unused_exclude: set[str],
    ) -> None:
        self.content = content
        self._root_content = root_content
        self._unused_exclude = unused_exclude
        super().__init__(section, overrides)

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}({self.section.name}, {self.content!r})"

    def load_raw(self, key: str, conf: Config | None, env_name: str | None) -> TomlTypes:  # ruff:ignore[unused-method-argument]
        return self.content[key]

    def load_raw_from_root(self, path: str) -> TomlTypes:
        current = cast("TomlTypes", self._root_content)
        for key in path.split(self.section.SEP):
            if isinstance(current, dict):
                current = current[key]
            else:
                msg = f"Failed to load key {key} as not dictionary {current!r}"
                logging.warning(msg)
                raise KeyError(msg)
        return current

    def build(  # ruff:ignore[too-many-arguments]
        self,
        key: str,
        of_type: type[_T] | UnionType,
        factory: Factory[_T],
        conf: Config | None,
        raw: TomlTypes,
        args: ConfigLoadArgs,
    ) -> _T:
        delay_replace = inspect.isclass(of_type) and issubclass(of_type, SetEnv)
        try:
            unroll = Unroll(conf=conf, loader=self, args=args)
            exploded = unroll(raw, skip_str=True) if delay_replace else unroll(raw)
            result = self.to(exploded, of_type, factory)
        except (HandledError, MatchError):
            raise
        except Exception as exception:
            name = "core" if args.env_name is None else args.env_name
            msg = f"failed to load {name}.{key}: {exception}"
            raise HandledError(msg) from exception
        if delay_replace:
            loader = self

            def _toml_replacer(value: str, args_: ConfigLoadArgs) -> str:
                if conf is None:
                    return value
                return replace(conf, TomlReplaceLoader(conf, loader), value, args_)

            cast("SetEnv", result).use_replacer(_toml_replacer, args=args)  # delay_replace means of_type is SetEnv
        return result

    def substitute(self, value: str, conf: Config, args: ConfigLoadArgs) -> str:
        return replace(conf, TomlReplaceLoader(conf, self), value, args)

    def found_keys(self) -> set[str]:
        return set(self.content.keys()) - self._unused_exclude

    @staticmethod
    def to_str(value: TomlTypes) -> str:
        return validate(value, str)

    @staticmethod
    def to_bool(value: TomlTypes) -> bool:
        return validate(value, bool)

    @staticmethod
    def to_list(value: TomlTypes, of_type: type[_T]) -> Iterator[_T]:
        result = cast("list[_T]", validate(value, cast("type[list[Any]]", GenericAlias(list, (of_type,)))))
        if inspect.isclass(of_type) and issubclass(of_type, Command):
            # a generated command list leaves gaps rather than compacting itself, so drop them here (see #3388)
            return iter([i for i in result if i])
        return iter(result)

    @staticmethod
    def to_set(value: TomlTypes, of_type: type[_T]) -> Iterator[_T]:
        return TomlLoader.to_list(value, of_type)

    @staticmethod
    def to_dict(value: TomlTypes, of_type: tuple[type[_T], type[_V]]) -> Iterator[tuple[_T, _V]]:
        result = validate(value, cast("type[dict[Any, Any]]", GenericAlias(dict, of_type)))
        return iter(cast("dict[_T, _V]", result).items())

    @staticmethod
    def to_path(value: TomlTypes) -> Path:
        return Path(TomlLoader.to_str(value))

    @staticmethod
    def to_command(value: TomlTypes) -> Command:
        if not value:
            msg = f"attempting to parse {value!r} into a command failed"
            raise ValueError(msg)
        return Command(args=cast("list[str]", value))  # validated during load in _ensure_type_correct

    @staticmethod
    def to_env_list(value: TomlTypes) -> EnvList:
        from ._product import expand_factor_group, expand_product  # ruff:ignore[import-outside-top-level]

        if not isinstance(value, list):
            msg = f"env_list must be a list, got {type(value).__name__}"
            raise TypeError(msg)
        envs: list[str] = []
        for item in value:
            if isinstance(item, str):
                envs.append(item)
            elif isinstance(item, dict):
                if "product" in item:
                    if "prefix" in item:
                        msg = "env_list dict items cannot combine 'product' with 'prefix'"
                        raise TypeError(msg)
                    envs.extend(expand_product(item))
                else:
                    envs.extend(expand_factor_group(item))
            else:
                msg = (
                    f"env_list items must be strings, product dicts, range dicts, or labeled dicts, "
                    f"got {type(item).__name__}"
                )
                raise TypeError(msg)
        return EnvList(envs=envs)


__all__ = [
    "HandledError",
    "TomlLoader",
]
