"""Context strategy domain models for the ACMS.

Defines the ``ContextStrategy`` protocol, supporting value objects, and
the ``ContextStrategyResult`` output model used by strategy stubs and
the Context Assembly Pipeline.

Based on ``docs/specification.md`` §§ 25162-25233 (Context Strategy
Protocol, StrategyCapabilities, Built-in Strategies) and
§§ 28682-28708 (strategy configuration keys).

All frozen Pydantic models follow ADR-004 immutability rules.
"""

from __future__ import annotations

from types import MappingProxyType
from typing import Any, Protocol, runtime_checkable

from pydantic import BaseModel, ConfigDict, Field, field_validator

from cleveragents.domain.models.acms.backends import (
    GraphBackend,
    TextBackend,
    VectorBackend,
)
from cleveragents.domain.models.acms.crp import (
    ContextFragment,
    ContextRequest,
)
from cleveragents.domain.models.acms.temporal import TemporalBackend

__all__ = [
    "BackendSet",
    "ContextStrategy",
    "ContextStrategyResult",
    "PlanContext",
    "StrategyCapabilities",
    "StrategyConfig",
    "StrategyRegistryEntry",
]


def _deep_freeze_value(v: Any) -> Any:
    """Recursively freeze a value: dicts → MappingProxyType, lists → tuples."""
    if isinstance(v, (dict, MappingProxyType)):
        source = dict(v) if isinstance(v, MappingProxyType) else v
        return MappingProxyType(
            {k: _deep_freeze_value(val) for k, val in source.items()}
        )
    if isinstance(v, list):
        return tuple(_deep_freeze_value(item) for item in v)
    if isinstance(v, set):
        return frozenset(_deep_freeze_value(item) for item in v)
    return v


def _deep_freeze_mapping(m: Any) -> MappingProxyType[str, Any]:
    """Recursively freeze a mapping and all nested containers."""
    source = dict(m) if isinstance(m, MappingProxyType) else m
    return MappingProxyType(
        {k: _deep_freeze_value(v) for k, v in source.items()},
    )


# ---------------------------------------------------------------------------
# BackendSet — container for available data backends
# ---------------------------------------------------------------------------


class BackendSet(BaseModel, frozen=True):
    """Container holding the set of available data backends.

    Strategies inspect which backends are present (not ``None``) to
    determine their ``can_handle`` confidence.

    Based on spec §42851 — ``ContextAssemblyPipeline`` passes a
    ``BackendSet`` to strategies during assembly.
    """

    text: TextBackend | None = Field(
        default=None,
        description="Full-text search backend (e.g., Tantivy, SQLite FTS)",
    )
    vector: VectorBackend | None = Field(
        default=None,
        description="Vector similarity search backend (e.g., FAISS, Qdrant)",
    )
    graph: GraphBackend | None = Field(
        default=None,
        description="Knowledge graph backend (e.g., Blazegraph, Neo4j)",
    )
    temporal: TemporalBackend | None = Field(
        default=None,
        description=(
            "Temporal backend for historical pattern discovery and "
            "revision-aware RDF queries.  Presence (not ``None``) "
            "indicates temporal/cold-tier data is available."
        ),
    )

    model_config = ConfigDict(arbitrary_types_allowed=True)


# ---------------------------------------------------------------------------
# PlanContext — lightweight plan hierarchy reference
# ---------------------------------------------------------------------------


class PlanContext(BaseModel, frozen=True):
    """Lightweight reference to the current plan's hierarchy.

    Passed to ``ContextStrategy.assemble()`` so strategies like
    ``plan-decision-context`` can traverse parent/ancestor decisions.

    Based on spec §25182-25186 — ``assemble()`` receives
    ``plan_context: PlanContext``.
    """

    plan_id: str = Field(
        default="",
        description="ULID of the current plan",
    )
    parent_plan_id: str = Field(
        default="",
        description="ULID of the parent plan (empty if root)",
    )
    ancestor_plan_ids: tuple[str, ...] = Field(
        default=(),
        description="Ordered ancestor plan ULIDs (root first)",
    )


# ---------------------------------------------------------------------------
# StrategyCapabilities — declares what a strategy can do
# ---------------------------------------------------------------------------


class StrategyCapabilities(BaseModel, frozen=True):
    """Declares what a strategy is capable of.

    Based on spec §25192-25205.  Strategies expose this to the
    ``StrategySelector`` so it can match strategies to requests.
    """

    uses_text: bool = Field(
        default=False,
        description="Requires a TextBackend",
    )
    uses_vector: bool = Field(
        default=False,
        description="Requires a VectorBackend",
    )
    uses_graph: bool = Field(
        default=False,
        description="Requires a GraphBackend",
    )
    uses_temporal: bool = Field(
        default=False,
        description="Requires temporal/cold-tier data",
    )
    uko_levels: tuple[str, ...] = Field(
        default=(),
        description="UKO levels this strategy operates on",
    )
    resource_types: tuple[str, ...] = Field(
        default=(),
        description="Resource types this strategy supports (empty = all)",
    )
    supports_depth_breadth: bool = Field(
        default=False,
        description="Supports depth/breadth projection",
    )
    supports_plan_hierarchy: bool = Field(
        default=False,
        description="Supports plan hierarchy traversal",
    )
    supports_temporal: bool = Field(
        default=False,
        description="Supports temporal archaeology queries",
    )
    quality_score: float = Field(
        default=0.5,
        ge=0.0,
        le=1.0,
        description="Intrinsic quality score (0.0-1.0)",
    )


# ---------------------------------------------------------------------------
# ContextStrategy — the pluggable strategy protocol
# ---------------------------------------------------------------------------


@runtime_checkable
class ContextStrategy(Protocol):
    """A pluggable context assembly strategy.

    Spec ref: ``specification.md:25166-25189``.

    Strategies are registered via configuration (TOML ``[context.strategies]``
    section) and invoked by the ``StrategyExecutor`` pipeline component
    in parallel.

    ``can_handle`` returns 0.0-1.0 confidence that this strategy can
    usefully contribute to the given request.  ``assemble`` executes the
    strategy and **must** respect the ``budget`` parameter.
    """

    @property
    def name(self) -> str:
        """Strategy's unique name (e.g., 'simple-keyword')."""
        ...

    @property
    def capabilities(self) -> StrategyCapabilities:
        """Declares what backends and features this strategy uses."""
        ...

    def can_handle(
        self,
        request: ContextRequest,
        backends: BackendSet,
    ) -> float:
        """Return 0.0-1.0 confidence for this request.

        Returns 0.0 if required backends are missing or the request
        is outside this strategy's scope.
        """
        ...

    def assemble(
        self,
        request: ContextRequest,
        backends: BackendSet,
        budget: int,
        plan_context: PlanContext,
    ) -> list[ContextFragment]:
        """Execute the strategy.  Must respect the budget."""
        ...

    def explain(self) -> str:
        """Return a human-readable explanation of this strategy."""
        ...


# ---------------------------------------------------------------------------
# StrategyConfig — per-strategy configuration
# ---------------------------------------------------------------------------


class StrategyConfig(BaseModel, frozen=True):
    """Per-strategy configuration.

    Controls whether a strategy is enabled, its timeout, and fragment
    limits.  Based on spec §28706-28708 (pipeline executor config) and
    issue #191 acceptance criteria.
    """

    enabled: bool = Field(
        default=True,
        description="Whether this strategy is enabled",
    )
    timeout_seconds: int = Field(
        default=30,
        ge=1,
        description="Timeout for this strategy's assembly call (spec §28706)",
    )
    max_fragments: int = Field(
        default=100,
        ge=1,
        description="Maximum fragments this strategy may return per call",
    )
    max_workers: int = Field(
        default=4,
        ge=1,
        description="Max parallel workers (spec §28707)",
    )
    circuit_breaker_threshold: int = Field(
        default=3,
        ge=1,
        description="Consecutive failures before circuit opens (spec §28708)",
    )
    resource_types: tuple[str, ...] = Field(
        default=(),
        description="Resource types this strategy is limited to (empty = all)",
    )
    extra: MappingProxyType[str, Any] = Field(
        default_factory=lambda: MappingProxyType({}),
        description="Strategy-specific extra configuration (immutable)",
    )

    model_config = ConfigDict(arbitrary_types_allowed=True)

    @field_validator("extra", mode="before")
    @classmethod
    def _coerce_extra(
        cls: type[StrategyConfig],
        v: Any,
    ) -> MappingProxyType[str, Any]:
        """Wrap plain dicts in MappingProxyType for immutability (ADR-004)."""
        if isinstance(v, MappingProxyType):
            return _deep_freeze_mapping(v)
        if isinstance(v, dict):
            return _deep_freeze_mapping(v)
        msg = f"expected dict or MappingProxyType, got {type(v).__name__}"
        raise ValueError(msg)


# ---------------------------------------------------------------------------
# ContextStrategyResult — output of a strategy execution
# ---------------------------------------------------------------------------


class ContextStrategyResult(BaseModel, frozen=True):
    """Result of a single strategy's execution.

    Provides fragments, execution statistics, and any errors
    encountered.  Fragments are stored in deterministic order:
    ``(relevance_score DESC, uko_node ASC)``.

    Required by issue #191 acceptance criteria.
    """

    strategy_name: str = Field(
        ...,
        min_length=1,
        description="Name of the strategy that produced this result",
    )
    fragments: tuple[ContextFragment, ...] = Field(
        default=(),
        description="Fragments in deterministic order",
    )
    total_fragments: int = Field(
        default=0,
        ge=0,
        description="Total fragments produced (before any limit)",
    )
    tokens_used: int = Field(
        default=0,
        ge=0,
        description="Total tokens across all fragments",
    )
    execution_time_ms: float = Field(
        default=0.0,
        ge=0.0,
        description="Wall-clock execution time in milliseconds",
    )
    errors: tuple[str, ...] = Field(
        default=(),
        description="Error messages encountered during execution",
    )
    stats: MappingProxyType[str, Any] = Field(
        default_factory=lambda: MappingProxyType({}),
        description="Strategy-specific statistics (immutable)",
    )

    model_config = ConfigDict(arbitrary_types_allowed=True)

    @field_validator("fragments")
    @classmethod
    def _sort_fragments(
        cls: type[ContextStrategyResult],
        v: tuple[ContextFragment, ...],
    ) -> tuple[ContextFragment, ...]:
        """Ensure deterministic ordering: relevance DESC, uko_node ASC."""
        return tuple(
            sorted(v, key=lambda f: (-f.relevance_score, f.uko_node)),
        )

    @field_validator("stats", mode="before")
    @classmethod
    def _coerce_stats(
        cls: type[ContextStrategyResult],
        v: Any,
    ) -> MappingProxyType[str, Any]:
        """Wrap plain dicts in MappingProxyType for immutability (ADR-004)."""
        if isinstance(v, MappingProxyType):
            return _deep_freeze_mapping(v)
        if isinstance(v, dict):
            return _deep_freeze_mapping(v)
        msg = f"expected dict or MappingProxyType, got {type(v).__name__}"
        raise ValueError(msg)


# ---------------------------------------------------------------------------
# StrategyRegistryEntry — metadata wrapper for registered strategies
# ---------------------------------------------------------------------------


class StrategyRegistryEntry(BaseModel, frozen=True):
    """Metadata wrapper for a strategy in the registry.

    Combines a strategy name with its resolved configuration.
    The actual ``ContextStrategy`` instance is not stored here
    (it's a protocol, not a Pydantic model); the registry holds
    both this entry and the strategy instance separately.
    """

    name: str = Field(
        ...,
        min_length=1,
        description="Unique strategy name",
    )
    config: StrategyConfig = Field(
        default_factory=StrategyConfig,
        description="Per-strategy configuration",
    )
    module_path: str = Field(
        default="",
        description=(
            "Python module path for custom strategies "
            "(e.g., 'my_package.strategies:MyStrategy')"
        ),
    )
    is_builtin: bool = Field(
        default=False,
        description="Whether this is a built-in strategy",
    )
