import dataclasses
from typing import (
    TypeVar,
    Optional,
    TYPE_CHECKING,
)
from collections.abc import Callable, Sequence, Mapping

from debputy.manifest_parser.exceptions import ManifestTypeException
from debputy.packages import BinaryPackage
from debputy.util import assume_not_none

if TYPE_CHECKING:
    from debputy.manifest_parser.util import AttributePath
    from debputy.manifest_parser.parser_data import ParserContextData

S = TypeVar("S")
T = TypeVar("T")


@dataclasses.dataclass(slots=True, frozen=True)
class PackageSelectorRule:
    valid_values: frozenset[str]
    impl: Callable[[BinaryPackage, str], bool]


def _pkg_selector_arch(package: BinaryPackage, selection_value: str) -> bool:
    if selection_value == "all":
        return package.is_arch_all
    return not package.is_arch_all


PACKAGE_SELECTORS: Mapping[str, PackageSelectorRule] = {
    "arch": PackageSelectorRule(
        frozenset(["all", "any"]),
        _pkg_selector_arch,
    ),
    "package-type": PackageSelectorRule(
        frozenset(["deb", "udeb"]),
        lambda p, v: p.package_type == v,
    ),
}


def type_mapper_str2package(
    raw_package_name: str,
    ap: "AttributePath",
    opc: Optional["ParserContextData"],
) -> BinaryPackage:
    pc = assume_not_none(opc)
    if "{{" in raw_package_name:
        resolved_package_name = pc.substitution.substitute(raw_package_name, ap.path)
    else:
        resolved_package_name = raw_package_name

    package_name_in_message = raw_package_name
    if resolved_package_name != raw_package_name:
        package_name_in_message = f'"{resolved_package_name}" ["{raw_package_name}"]'

    if not pc.is_known_package(resolved_package_name):
        package_names = ", ".join(pc.binary_packages)
        raise ManifestTypeException(
            f'The value {package_name_in_message} (from "{ap.path}") does not reference a package declared in'
            f" debian/control. Valid package names are: {package_names}"
        )
    package_data = pc.binary_package_data(resolved_package_name)
    if package_data.is_auto_generated_package:
        package_names = ", ".join(pc.binary_packages)
        raise ManifestTypeException(
            f'The package name {package_name_in_message} (from "{ap.path}") references an auto-generated package.'
            " However, auto-generated packages are now permitted here. Valid package names are:"
            f" {package_names}"
        )
    return package_data.binary_package


@dataclasses.dataclass(slots=True, frozen=True)
class PackageSelector:
    matched_binary_packages: Sequence[BinaryPackage]

    @classmethod
    def parse(
        cls,
        raw_value: str,
        attribute_path: "AttributePath",
        parser_context: "ParserContextData | None",
    ) -> "PackageSelector":
        pc = assume_not_none(parser_context)
        if ":" in raw_value:
            key, value = raw_value.split(":", maxsplit=1)
            selector = PACKAGE_SELECTORS.get(key)
            if selector is None:
                package_names = ", ".join(pc.binary_packages)
                valid_selectors = ", ".join(
                    f"{k}:<value>" for k in sorted(PACKAGE_SELECTORS)
                )
                raise ManifestTypeException(
                    f'Unknown package selector "{raw_value}" (from "{attribute_path.path}").'
                    f" Valid values here are: {package_names}, OR one of {valid_selectors}"
                )

            if value not in selector.valid_values:
                valid_values = ", ".join(sorted(selector.valid_values))
                raise ManifestTypeException(
                    f'The selection criteria "{key}" does not accept "{value}" (from "{attribute_path.path}").'
                    f" Valid values here are: {valid_values}"
                )

            matches = tuple(
                p for p in pc.binary_packages.values() if selector.impl(p, value)
            )
            return PackageSelector(matches)
        package = type_mapper_str2package(raw_value, attribute_path, parser_context)
        return PackageSelector((package,))


def wrap_into_list(
    x: T,
    _ap: "AttributePath",
    _pc: Optional["ParserContextData"],
) -> list[T]:
    return [x]


def normalize_into_list(
    x: T | list[T],
    _ap: "AttributePath",
    _pc: Optional["ParserContextData"],
) -> list[T]:
    return x if isinstance(x, list) else [x]


def map_each_element(
    mapper: Callable[[S, "AttributePath", Optional["ParserContextData"]], T],
) -> Callable[[list[S], "AttributePath", Optional["ParserContextData"]], list[T]]:
    def _generated_mapper(
        xs: list[S],
        ap: "AttributePath",
        pc: Optional["ParserContextData"],
    ) -> list[T]:
        return [mapper(s, ap[i], pc) for i, s in enumerate(xs)]

    return _generated_mapper
