import dataclasses
from enum import Enum
from typing import List, Optional, Any, Self
from collections.abc import Callable, Sequence, Mapping

from debian.debian_support import DpkgArchTable

from debputy._deb_options_profiles import DebBuildOptionsAndProfiles
from debputy.architecture_support import DpkgArchitectureBuildProcessValuesTable
from debputy.manifest_parser.tagging_types import DebputyDispatchableType
from debputy.packages import BinaryPackage
from debputy.substitution import Substitution
from debputy.util import active_profiles_match


@dataclasses.dataclass(slots=True, frozen=True)
class ConditionContext:
    binary_package: BinaryPackage | None
    deb_options_and_profiles: DebBuildOptionsAndProfiles
    substitution: Substitution
    dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable
    dpkg_arch_query_table: DpkgArchTable

    def replace(self, /, **changes: Any) -> "Self":
        return dataclasses.replace(self, **changes)


class ManifestCondition(DebputyDispatchableType):
    __slots__ = ()

    def describe(self) -> str:
        raise NotImplementedError

    def negated(self) -> "ManifestCondition":
        return NegatedManifestCondition(self)

    def evaluate(self, context: ConditionContext) -> bool:
        raise NotImplementedError

    @classmethod
    def _manifest_group(
        cls,
        match_type: "_ConditionGroupMatchType",
        conditions: "Sequence[ManifestCondition]",
    ) -> "ManifestCondition":
        condition = conditions[0]
        if (
            isinstance(condition, ManifestConditionGroup)
            and condition.match_type == match_type
        ):
            return condition.extend(conditions[1:])
        return ManifestConditionGroup(match_type, conditions)

    @classmethod
    def any_of(cls, conditions: "Sequence[ManifestCondition]") -> "ManifestCondition":
        return cls._manifest_group(_ConditionGroupMatchType.ANY_OF, conditions)

    @classmethod
    def all_of(cls, conditions: "Sequence[ManifestCondition]") -> "ManifestCondition":
        return cls._manifest_group(_ConditionGroupMatchType.ALL_OF, conditions)

    @classmethod
    def is_cross_building(cls) -> "ManifestCondition":
        return _IS_CROSS_BUILDING

    @classmethod
    def can_execute_compiled_binaries(cls) -> "ManifestCondition":
        return _CAN_EXECUTE_COMPILED_BINARIES

    @classmethod
    def run_build_time_tests(cls) -> "ManifestCondition":
        return _RUN_BUILD_TIME_TESTS

    @classmethod
    def literal_bool(cls, value: bool) -> "ManifestCondition":
        return MANIFEST_CONDITION_TRUE if value else MANIFEST_CONDITION_FALSE


@dataclasses.dataclass(slots=True, frozen=True)
class LiteralManifestCondition(ManifestCondition):
    value: bool

    def negated(self) -> "ManifestCondition":
        return ManifestCondition.literal_bool(not self.value)

    def describe(self) -> str:
        return "true" if self.value else "false"

    def evaluate(self, context: ConditionContext) -> bool:
        return self.value


MANIFEST_CONDITION_TRUE = LiteralManifestCondition(True)
MANIFEST_CONDITION_FALSE = LiteralManifestCondition(False)

del LiteralManifestCondition


class NegatedManifestCondition(ManifestCondition):
    __slots__ = ("_condition",)

    def __init__(self, condition: ManifestCondition) -> None:
        super().__init__()
        self._condition = condition

    def negated(self) -> "ManifestCondition":
        return self._condition

    def describe(self) -> str:
        return f"not ({self._condition.describe()})"

    def evaluate(self, context: ConditionContext) -> bool:
        return not self._condition.evaluate(context)


class _ConditionGroupMatchType(Enum):
    ANY_OF = (any, "At least one of: [{conditions}]")
    ALL_OF = (all, "All of: [{conditions}]")

    def describe(self, conditions: Sequence[ManifestCondition]) -> str:
        return self.value[1].format(
            conditions=", ".join(x.describe() for x in conditions)
        )

    def evaluate(
        self, conditions: Sequence[ManifestCondition], context: ConditionContext
    ) -> bool:
        return self.value[0](c.evaluate(context) for c in conditions)


class ManifestConditionGroup(ManifestCondition):
    __slots__ = ("match_type", "_conditions")

    def __init__(
        self,
        match_type: _ConditionGroupMatchType,
        conditions: Sequence[ManifestCondition],
    ) -> None:
        super().__init__()
        self.match_type = match_type
        self._conditions = conditions

    def describe(self) -> str:
        return self.match_type.describe(self._conditions)

    def evaluate(self, context: ConditionContext) -> bool:
        return self.match_type.evaluate(self._conditions, context)

    def extend(
        self,
        conditions: Sequence[ManifestCondition],
    ) -> "ManifestConditionGroup":
        combined = list(self._conditions)
        combined.extend(conditions)
        return ManifestConditionGroup(
            self.match_type,
            combined,
        )


class ArchMatchManifestConditionBase(ManifestCondition):
    __slots__ = ("_arch_spec", "_is_negated")

    def __init__(self, arch_spec: list[str], *, is_negated: bool = False) -> None:
        super().__init__()
        self._arch_spec = arch_spec
        self._is_negated = is_negated

    def negated(self) -> "ManifestCondition":
        return self.__class__(self._arch_spec, is_negated=not self._is_negated)


class SourceContextArchMatchManifestCondition(ArchMatchManifestConditionBase):
    def describe(self) -> str:
        qual = "*none*" if self._is_negated else "any"
        archs = ", ".join(self._arch_spec)
        return f"architecture (for source package) matches {qual} of [{archs}]"

    def evaluate(self, context: ConditionContext) -> bool:
        arch = context.dpkg_architecture_variables.current_host_arch
        match = context.dpkg_arch_query_table.architecture_is_concerned(
            arch, self._arch_spec
        )
        return match != self._is_negated  # xor


class BinaryPackageContextArchMatchManifestCondition(ArchMatchManifestConditionBase):
    def describe(self) -> str:
        qual = "*none*" if self._is_negated else "any"
        archs = ", ".join(self._arch_spec)
        return f"architecture (for binary package) matches {qual} of [{archs}]"

    def evaluate(self, context: ConditionContext) -> bool:
        binary_package = context.binary_package
        if binary_package is None:
            raise RuntimeError(
                "Condition only applies in the context of a BinaryPackage, but was evaluated"
                " without one"
            )
        arch = binary_package.resolved_architecture
        match = context.dpkg_arch_query_table.architecture_is_concerned(
            arch, self._arch_spec
        )
        return not match if self._is_negated else match


class BuildProfileMatch(ManifestCondition):
    __slots__ = ("_profile_spec", "_is_negated")

    def __init__(self, profile_spec: str, *, is_negated: bool = False) -> None:
        super().__init__()
        self._profile_spec = profile_spec
        self._is_negated = is_negated

    def negated(self) -> "ManifestCondition":
        return self.__class__(self._profile_spec, is_negated=not self._is_negated)

    def describe(self) -> str:
        qual = "*none*" if self._is_negated else "any"
        return f"DEB_BUILD_PROFILES matches {qual} of [{self._profile_spec}]"

    def evaluate(self, context: ConditionContext) -> bool:
        match = active_profiles_match(
            self._profile_spec, context.deb_options_and_profiles.deb_build_profiles
        )
        return match != self._is_negated  # xor


@dataclasses.dataclass(frozen=True, slots=True)
class _SingletonCondition(ManifestCondition):
    description: str
    implementation: Callable[[ConditionContext], bool]

    def describe(self) -> str:
        return self.description

    def evaluate(self, context: ConditionContext) -> bool:
        return self.implementation(context)


def _can_run_built_binaries(context: ConditionContext) -> bool:
    if not context.dpkg_architecture_variables.is_cross_compiling:
        return True
    # User / Builder asserted that we could even though we are cross-compiling, so we have to assume it is true
    return (
        "crossbuildcanrunhostbinaries"
        in context.deb_options_and_profiles.deb_build_options
    )


def _run_build_time_tests(deb_build_options: Mapping[str, str | None]) -> bool:
    return "nocheck" not in deb_build_options


_IS_CROSS_BUILDING = _SingletonCondition(
    "Cross Compiling (i.e., DEB_HOST_GNU_TYPE != DEB_BUILD_GNU_TYPE)",
    lambda c: c.dpkg_architecture_variables.is_cross_compiling,
)

_CAN_EXECUTE_COMPILED_BINARIES = _SingletonCondition(
    "Can run built binaries (natively or via transparent emulation)",
    _can_run_built_binaries,
)

_RUN_BUILD_TIME_TESTS = _SingletonCondition(
    "Run build time tests",
    lambda c: _run_build_time_tests(c.deb_options_and_profiles.deb_build_options),
)

_BUILD_DOCS_BDO = _SingletonCondition(
    "Build docs (nodocs not in DEB_BUILD_OPTIONS)",
    lambda c: "nodocs" not in c.deb_options_and_profiles.deb_build_options,
)


del _SingletonCondition
del _can_run_built_binaries
