"""Context Request Protocol (CRP) domain models for CleverAgents.

The CRP is the structured vocabulary through which actors (via skills)
declare what information they need, at what level of detail, and with
what scope, while they are reasoning.  These models are consumed by the
Context Assembly Pipeline and produced by context strategies.

## Core Types

| Type                | Role                                                |
|---------------------|-----------------------------------------------------|
| ``DetailLevelMap``  | Named-level-to-integer resolution with inheritance  |
| ``ContextRequest``  | Actor-issued request for context                    |
| ``ContextFragment`` | Atomic unit of context returned by strategies       |
| ``FragmentProvenance`` | Trace-back to resource + location               |
| ``ContextBudget``   | Token budget management with reservation            |
| ``AssembledContext`` | Fused, budget-respecting context payload            |

``DetailLevelMap``, ``DetailDepth``, ``DetailLevelError``, and
``DetailLevelCycleError`` are defined in the ``detail_level`` sibling
module and re-exported here for backward compatibility.

Based on ``docs/specification.md`` ACMS / CRP sections and ADR-014.
"""

from __future__ import annotations

from typing import Annotated, Any

from pydantic import (
    BaseModel,
    ConfigDict,
    Field,
    StringConstraints,
    field_validator,
    model_validator,
)

from cleveragents.domain.models.acms.detail_level import (  # re-exported
    DetailDepth,
    DetailLevelCycleError,
    DetailLevelError,
    DetailLevelMap,
)
from cleveragents.domain.models.core.project import TemporalScope

__all__: list[str] = [
    "AssembledContext",
    "ContextBudget",
    "ContextFragment",
    "ContextRequest",
    "DetailDepth",
    "DetailLevelCycleError",
    "DetailLevelError",
    "DetailLevelMap",
    "FragmentProvenance",
]


# ---------------------------------------------------------------------------
# FragmentProvenance
# ---------------------------------------------------------------------------


class FragmentProvenance(BaseModel, frozen=True):
    """Provenance trace for a context fragment.

    Links a fragment back to the originating resource and location
    within that resource.
    """

    resource_uri: str = Field(
        ...,
        min_length=1,
        description="URI of the originating resource",
    )
    location: str = Field(
        default="",
        description="Location within the resource (e.g., line range, section)",
    )
    strategy: str = Field(
        default="",
        description="Name of the strategy that produced this fragment",
    )

    model_config = ConfigDict(
        str_strip_whitespace=True,
    )


# ---------------------------------------------------------------------------
# ContextBudget
# ---------------------------------------------------------------------------


class ContextBudget(BaseModel, frozen=True):
    """Token budget for context assembly.

    Tracks the maximum token budget and any reserved portion, providing
    a computed ``available_tokens`` property for the remaining budget.

    Example::

        budget = ContextBudget(max_tokens=8000, reserved_tokens=1000)
        assert budget.available_tokens == 7000
    """

    max_tokens: int = Field(
        ...,
        ge=0,
        description="Maximum total token budget",
    )
    reserved_tokens: int = Field(
        default=0,
        ge=0,
        description="Tokens reserved (e.g., for skeleton context)",
    )

    @model_validator(mode="after")
    def _validate_reservation(self) -> ContextBudget:
        """Ensure reserved_tokens does not exceed max_tokens."""
        if self.reserved_tokens > self.max_tokens:
            raise ValueError("reserved_tokens cannot exceed max_tokens")
        return self

    @property
    def available_tokens(self) -> int:
        """Return the number of tokens available after reservations."""
        return self.max_tokens - self.reserved_tokens

    model_config = ConfigDict(
        str_strip_whitespace=True,
    )


# ---------------------------------------------------------------------------
# ContextFragment
# ---------------------------------------------------------------------------


class ContextFragment(BaseModel, frozen=True):
    """A single piece of context assembled by a strategy.

    The atomic unit of context returned by strategies and consumed by
    the Context Assembly Pipeline's fusion phase
    (FragmentDeduplicator, DetailDepthResolver, FragmentScorer,
    BudgetPacker, FragmentOrderer).
    """

    uko_node: str = Field(
        ...,
        min_length=1,
        description="UKO URI of the source node",
    )
    content: str = Field(
        ...,
        description="Rendered text content",
    )
    detail_depth: int = Field(
        ...,
        ge=0,
        description="Resolved integer depth of this fragment",
    )
    token_count: int = Field(
        ...,
        ge=0,
        description="Token count of content",
    )
    relevance_score: float = Field(
        ...,
        ge=0.0,
        le=1.0,
        description="0.0-1.0 relevance to the request",
    )
    provenance: FragmentProvenance = Field(
        ...,
        description="Trace back to resource + location",
    )
    metadata: dict[str, Any] = Field(
        default_factory=dict,
        description="Arbitrary strategy-specific metadata",
    )

    model_config = ConfigDict(
        str_strip_whitespace=True,
    )


# ---------------------------------------------------------------------------
# ContextRequest
# ---------------------------------------------------------------------------

#: Upper bound for list fields on ``ContextRequest`` to prevent
#: memory exhaustion from excessively large payloads.
_MAX_LIST_ITEMS: int = 500

#: Upper bound for the natural-language ``query`` string.
_MAX_QUERY_LENGTH: int = 10_000

#: Upper bound for individual string items inside list fields.
_MAX_ITEM_LENGTH: int = 2_000

#: Constrained string type for list items in ``ContextRequest``.
_BoundedStr = Annotated[str, StringConstraints(max_length=_MAX_ITEM_LENGTH)]


class ContextRequest(BaseModel):
    """A structured request for context, issued by an actor or skill.

    Actors issue context requests through the ``builtin/context`` skill
    which is automatically injected into every actor's skill set.  Each
    request triggers a context assembly cycle in the ACMS pipeline.

    Fields follow the spec (``docs/specification.md`` > ACMS > CRP >
    ContextRequest).
    """

    # === What to find ===
    query: str | None = Field(
        default=None,
        max_length=_MAX_QUERY_LENGTH,
        description="Natural language query",
    )
    entities: list[_BoundedStr] = Field(
        default_factory=list,
        max_length=_MAX_LIST_ITEMS,
        description="Named entities to focus on",
    )
    uko_types: list[_BoundedStr] = Field(
        default_factory=list,
        max_length=_MAX_LIST_ITEMS,
        description="UKO types to filter",
    )

    # === Scope control ===
    focus: list[_BoundedStr] = Field(
        default_factory=list,
        max_length=_MAX_LIST_ITEMS,
        description="URIs or identifiers of specific items to focus on",
    )
    breadth: int = Field(
        default=2,
        ge=0,
        description="How many hops outward in the dependency/reference graph",
    )
    depth: DetailDepth = Field(
        default=3,
        description=(
            "How much detail to include for each item found. "
            "May be a raw integer (0-N) or a named level string "
            "(e.g., 'SIGNATURES') resolved via the active DetailLevelMap."
        ),
    )

    # === Focus depth gradient ===
    depth_gradient: bool = Field(
        default=True,
        description="When True, items closer to the focus get more detail",
    )

    # === Temporal scope ===
    temporal: TemporalScope = Field(
        default=TemporalScope.CURRENT,
        description="Temporal scope for context retrieval",
    )

    # === Budget ===
    max_tokens: int | None = Field(
        default=None,
        ge=0,
        description="Maximum token budget for this request",
    )

    # === Strategy hints ===
    preferred_strategies: list[_BoundedStr] = Field(
        default_factory=list,
        max_length=_MAX_LIST_ITEMS,
        description="Preferred context strategies to use",
    )
    required_backends: list[_BoundedStr] = Field(
        default_factory=list,
        max_length=_MAX_LIST_ITEMS,
        description="Required data backends (text, vector, graph)",
    )

    # === Priority and purpose ===
    priority: float = Field(
        default=0.5,
        ge=0.0,
        le=1.0,
        description="0.0 = background, 1.0 = critical",
    )
    purpose: str = Field(
        default="",
        max_length=_MAX_ITEM_LENGTH,
        description="Why is this context needed?",
    )

    @field_validator("depth")
    @classmethod
    def validate_depth(
        cls: type[ContextRequest],
        v: DetailDepth,
    ) -> DetailDepth:
        """Ensure integer depths are non-negative."""
        if isinstance(v, int) and v < 0:
            raise ValueError("depth must be non-negative when specified as integer")
        return v

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
        use_enum_values=False,
    )


# ---------------------------------------------------------------------------
# AssembledContext
# ---------------------------------------------------------------------------


class AssembledContext(BaseModel, frozen=True):
    """The fused, budget-respecting context payload.

    Output of a context assembly cycle -- the final payload delivered
    to an actor after strategy execution, fragment deduplication,
    scoring, budget packing, and ordering.
    """

    fragments: tuple[ContextFragment, ...] = Field(
        default=(),
        description="Ordered context fragments",
    )
    total_tokens: int = Field(
        ...,
        ge=0,
        description="Total token count",
    )
    budget_used: float = Field(
        ...,
        ge=0.0,
        le=1.0,
        description="Fraction of budget consumed (0.0-1.0)",
    )
    strategies_used: tuple[str, ...] = Field(
        default=(),
        description="Which strategies contributed",
    )
    context_hash: str = Field(
        ...,
        min_length=1,
        description="Cryptographic hash for snapshot",
    )
    preamble: str | None = Field(
        default=None,
        description="Optional structure summary",
    )
    provenance_map: dict[str, Any] = Field(
        default_factory=dict,
        description="Fragment -> resource/location mapping",
    )

    model_config = ConfigDict(
        str_strip_whitespace=True,
    )
