"""Pipeline-specific fragment models for the ACMS context assembly pipeline.

Provides ``ScoredFragment``, a frozen Pydantic value object that wraps a
``ContextFragment`` with composite scoring information produced by the
``FragmentScorer`` pipeline stage.

Equality and hashing are based on ``uko_node`` + ``detail_depth`` to support
deduplication in the pipeline without relying on object identity.

Based on ``docs/specification.md`` ~line 42826 (ScoredFragment).
"""

from __future__ import annotations

from pydantic import BaseModel, Field, field_validator, model_validator

from cleveragents.domain.models.core.context_fragment import ContextFragment


class ScoredFragment(BaseModel, frozen=True):
    """A ``ContextFragment`` annotated with a composite score.

    Produced by the ``FragmentScorer`` pipeline stage and consumed by
    ``BudgetPacker`` for ranking during budget packing.  The model is
    frozen so that scored fragments are safe to share across pipeline
    stages without defensive copies.

    Equality and hashing are based on ``fragment.uko_node`` and
    ``fragment.detail_depth`` so that two ``ScoredFragment`` instances
    wrapping the same logical content are considered duplicates
    regardless of scoring differences.

    Based on ``docs/specification.md`` ~line 42826.
    """

    fragment: ContextFragment = Field(
        ...,
        description="The underlying context fragment being scored.",
    )

    composite_score: float = Field(
        ...,
        ge=0.0,
        le=1.0,
        description="Overall composite ranking score (0.0-1.0).",
    )

    score_breakdown: dict[str, float] = Field(
        default_factory=dict,
        description=(
            "Component-level score breakdown.  Keys are component names "
            "(e.g. 'relevance', 'hierarchy', 'recency') and values are "
            "individual scores in 0.0-1.0."
        ),
    )

    rank: int = Field(
        default=0,
        ge=0,
        description=(
            "Ordinal rank assigned after scoring (0 = unranked / not yet "
            "assigned).  Lower values indicate higher priority."
        ),
    )

    @field_validator("score_breakdown")
    @classmethod
    def _validate_score_breakdown(cls, v: dict[str, float]) -> dict[str, float]:
        """Validate all component scores are in [0.0, 1.0] and deep-copy."""
        for name, score in v.items():
            if score < 0.0 or score > 1.0:
                msg = (
                    f"score_breakdown['{name}'] must be between 0.0 and 1.0, "
                    f"got {score}"
                )
                raise ValueError(msg)
        return dict(v)

    @model_validator(mode="after")
    def _check_composite_within_components(self) -> ScoredFragment:
        """Warn-free validation: composite_score is present and valid.

        The composite score does not need to equal the average of
        components — strategies may apply custom weighting — but it
        must be in range, which is already enforced by the field
        constraint.
        """
        return self

    # ------------------------------------------------------------------
    # Equality / hashing based on uko_node + detail_depth
    # ------------------------------------------------------------------

    def __eq__(self, other: object) -> bool:
        """Two scored fragments are equal when their underlying fragments
        share the same ``uko_node`` and ``detail_depth``."""
        if not isinstance(other, ScoredFragment):
            return NotImplemented
        return (
            self.fragment.uko_node == other.fragment.uko_node
            and self.fragment.detail_depth == other.fragment.detail_depth
        )

    def __hash__(self) -> int:
        """Hash based on ``uko_node`` + ``detail_depth`` for set/dict use."""
        return hash((self.fragment.uko_node, self.fragment.detail_depth))

    # ------------------------------------------------------------------
    # Factory helpers
    # ------------------------------------------------------------------

    @staticmethod
    def from_fragment(
        fragment: ContextFragment,
        composite_score: float,
        score_breakdown: dict[str, float] | None = None,
        rank: int = 0,
    ) -> ScoredFragment:
        """Create a ``ScoredFragment`` from an existing ``ContextFragment``.

        This is the preferred construction method for pipeline stages.

        Args:
            fragment: The context fragment to wrap.
            composite_score: The overall composite score (0.0-1.0).
            score_breakdown: Optional component-level breakdown.
            rank: Optional ordinal rank (0 = unranked).

        Returns:
            A new frozen ``ScoredFragment`` instance.
        """
        return ScoredFragment(
            fragment=fragment,
            composite_score=composite_score,
            score_breakdown=score_breakdown if score_breakdown is not None else {},
            rank=rank,
        )

    @staticmethod
    def from_relevance(
        fragment: ContextFragment,
        rank: int = 0,
    ) -> ScoredFragment:
        """Create a ``ScoredFragment`` using only the fragment's relevance_score.

        Convenience factory for simple scoring where the composite score
        equals the fragment's built-in ``relevance_score``.

        Args:
            fragment: The context fragment to wrap.
            rank: Optional ordinal rank (0 = unranked).

        Returns:
            A new frozen ``ScoredFragment`` with relevance as sole component.
        """
        return ScoredFragment(
            fragment=fragment,
            composite_score=fragment.relevance_score,
            score_breakdown={"relevance": fragment.relevance_score},
            rank=rank,
        )

    # ------------------------------------------------------------------
    # Convenience properties
    # ------------------------------------------------------------------

    @property
    def uko_node(self) -> str:
        """Shortcut to the underlying fragment's ``uko_node``."""
        return self.fragment.uko_node

    @property
    def detail_depth(self) -> int:
        """Shortcut to the underlying fragment's ``detail_depth``."""
        return self.fragment.detail_depth

    @property
    def token_count(self) -> int:
        """Shortcut to the underlying fragment's ``token_count``."""
        return self.fragment.token_count
