"""Capability-level dependency declaration and resolution.

Distinct from ``PackDependencyRef`` (``src/domain/feature_packs/contract.py``), which
declares one Feature Pack's dependency on another pack or a Cloud primitive.
``CapabilityDependency`` operates one layer up: which platform capability (by
``capability_key``, e.g. ``"auth"``, ``"forms"``) another capability requires, and at
what minimum or exact version. Feature Packs are the intended first concrete
consumer -- wiring an actual pack's ``requires`` list through this resolver, so a
pack that needs ``auth`` at version >= 2 gets a structured conflict instead of a
silent mismatch, is left to the following Codex tranche.

Only the version semantics actually needed are supported: ``minimum_version`` (>=)
and ``exact_version`` (==). No semver ranges -- every capability version in this
codebase is a plain int (see ``CapabilityProvisioner.version`` /
``AuthV2Provisioner.version = 2``).
"""
from __future__ import annotations

from collections.abc import Sequence

from pydantic import BaseModel, ConfigDict, Field, model_validator


class CapabilityDependency(BaseModel):
    """``key`` requires the dependency-holding capability to exist (and if given, to
    be at/above ``minimum_version`` or exactly at ``exact_version``). ``required=False``
    marks an optional dependency: resolvable and reported, but its absence is not a
    resolution failure."""

    model_config = ConfigDict(extra="forbid", frozen=True)

    key: str
    required: bool = True
    minimum_version: int | None = None
    exact_version: int | None = None

    @model_validator(mode="after")
    def _not_both_minimum_and_exact(self) -> CapabilityDependency:
        if self.minimum_version is not None and self.exact_version is not None:
            raise ValueError(
                "CapabilityDependency cannot set both minimum_version and exact_version"
            )
        return self


class ResolvedCapability(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)

    key: str
    version: int | None
    required: bool
    required_by: tuple[str, ...] = Field(default_factory=tuple)


class DependencyCycle(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)

    cycle: tuple[str, ...]


class DependencyVersionConflict(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)

    key: str
    requirements: tuple[CapabilityDependency, ...]
    requirers: tuple[str, ...]


class DependencyResolution(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)

    order: tuple[ResolvedCapability, ...] = Field(default_factory=tuple)
    cycles: tuple[DependencyCycle, ...] = Field(default_factory=tuple)
    version_conflicts: tuple[DependencyVersionConflict, ...] = Field(default_factory=tuple)

    @property
    def ok(self) -> bool:
        return not self.cycles and not self.version_conflicts


def resolve_dependencies(
    roots: dict[str, int],
    dependencies: dict[str, Sequence[CapabilityDependency]],
) -> DependencyResolution:
    """Resolve ``roots`` (capability_key -> declared version) plus their transitive
    dependencies into one deterministic, dependency-before-dependent order.

    ``dependencies[key]`` lists what ``key`` requires; a key absent from
    ``dependencies`` is a leaf. Order is deterministic by construction: roots are
    visited in the order given, and each key's own dependencies are visited in the
    order they're declared, so the same inputs always produce the same output -- no
    sorting is applied beyond that.
    """
    order: list[str] = []
    seen: set[str] = set()
    visiting: list[str] = []
    cycles: list[DependencyCycle] = []
    requirements_by_key: dict[str, list[CapabilityDependency]] = {}
    requirers_by_key: dict[str, list[str]] = {}

    def record_requirement(key: str, dep: CapabilityDependency, requirer: str) -> None:
        requirements_by_key.setdefault(key, []).append(dep)
        requirers = requirers_by_key.setdefault(key, [])
        if requirer not in requirers:
            requirers.append(requirer)

    def visit(key: str) -> None:
        if key in visiting:
            cycle_start = visiting.index(key)
            cycles.append(DependencyCycle(cycle=tuple(visiting[cycle_start:]) + (key,)))
            return
        if key in seen:
            return
        visiting.append(key)
        for dep in dependencies.get(key, ()):
            record_requirement(dep.key, dep, key)
            visit(dep.key)
        visiting.pop()
        seen.add(key)
        order.append(key)

    for root_key in roots:
        visit(root_key)

    version_conflicts: list[DependencyVersionConflict] = []
    resolved_versions: dict[str, int | None] = {}
    for key in order:
        reqs = requirements_by_key.get(key, [])
        root_version = roots.get(key)
        exact_values = {d.exact_version for d in reqs if d.exact_version is not None}
        minimum_values = {d.minimum_version for d in reqs if d.minimum_version is not None}
        conflict = False

        if len(exact_values) > 1:
            conflict = True
        if exact_values and root_version is not None and root_version not in exact_values:
            conflict = True
        if exact_values and minimum_values:
            exact = next(iter(exact_values)) if len(exact_values) == 1 else None
            if exact is not None and any(m > exact for m in minimum_values):
                conflict = True

        if exact_values and len(exact_values) == 1:
            resolved_versions[key] = next(iter(exact_values))
        elif minimum_values or root_version is not None:
            floor = {root_version} if root_version is not None else set()
            candidates = floor | minimum_values
            resolved_versions[key] = max(candidates) if candidates else None
        else:
            resolved_versions[key] = None

        if conflict:
            version_conflicts.append(
                DependencyVersionConflict(
                    key=key,
                    requirements=tuple(reqs),
                    requirers=tuple(requirers_by_key.get(key, ())),
                )
            )

    resolved_order = tuple(
        ResolvedCapability(
            key=key,
            version=resolved_versions.get(key),
            required=(key in roots) or any(d.required for d in requirements_by_key.get(key, ())),
            required_by=tuple(requirers_by_key.get(key, ())),
        )
        for key in order
    )
    return DependencyResolution(
        order=resolved_order,
        cycles=tuple(cycles),
        version_conflicts=tuple(version_conflicts),
    )
