"""Data models for evidence verification results and rejected candidates.

Provides structured types for offset verification, entity-evidence association,
numeric consistency checks, rejected-candidate storage, and verification reports.
"""

from __future__ import annotations

from datetime import datetime, timezone
from enum import Enum

from pydantic import BaseModel, Field


class RejectionReason(str, Enum):
    """Structured reason codes for candidate rejection during verification."""

    INVALID_OFFSET = "invalid_offset"
    TEXT_MISMATCH = "text_mismatch"
    ENTITY_NOT_IN_EVIDENCE = "entity_not_in_evidence"
    NUMERIC_INCONSISTENCY = "numeric_inconsistency"
    UNSUPPORTED_CLAIM = "unsupported_claim"
    SCHEMA_VIOLATION = "schema_violation"
    CONFIDENCE_BELOW_THRESHOLD = "confidence_below_threshold"


class OffsetVerification(BaseModel):
    """Result of verifying a single evidence span's offsets against source text."""

    span_id: str = Field(description="ID of the evidence span being verified.")
    valid: bool = Field(description="Whether the span text matches source at offsets.")
    reason: str | None = Field(
        default=None,
        description="Explanation when invalid (e.g., 'text mismatch at offset 42').",
    )


class AssociationVerification(BaseModel):
    """Result of verifying that an entity appears in at least one linked evidence span."""

    entity_id: str = Field(description="ID of the entity being verified.")
    valid: bool = Field(description="Whether entity text was found in any linked span.")
    reason: str | None = Field(
        default=None,
        description="Explanation when invalid (e.g., 'entity text not in any linked span').",
    )


class NumericVerification(BaseModel):
    """Result of verifying a numeric fact against its evidence spans."""

    fact_id: str = Field(description="ID of the numeric fact being verified.")
    valid: bool = Field(description="Whether the numeric value was found in evidence text.")
    found_value: str | None = Field(
        default=None,
        description="The value found in the evidence text, if any.",
    )
    expected_value: str = Field(description="The expected normalized value from the fact.")
    reason: str | None = Field(
        default=None,
        description="Explanation when invalid (e.g., 'value 3.14 not found in evidence').",
    )


class RejectedCandidate(BaseModel):
    """A candidate that was rejected during verification, stored for audit and learning."""

    candidate_type: str = Field(
        description="Type of candidate: entity, fact, event, relation, sentiment."
    )
    candidate_data: dict = Field(
        description="Serialized candidate data for audit trail."
    )
    rejection_reason: RejectionReason = Field(
        description="Structured reason code for rejection."
    )
    stage: str = Field(
        description="Pipeline stage where rejection occurred (e.g., 'offset_verification')."
    )
    timestamp: datetime = Field(
        default_factory=lambda: datetime.now(tz=timezone.utc),
        description="When the rejection was recorded.",
    )


class VerificationReport(BaseModel):
    """Summary report from a full verification pass over extraction candidates."""

    total_candidates: int = Field(ge=0, description="Total candidates evaluated.")
    verified: int = Field(ge=0, description="Candidates that passed verification.")
    rejected: int = Field(ge=0, description="Candidates that failed verification.")
    coverage_rate: float = Field(
        ge=0.0,
        le=1.0,
        description="Proportion of candidates with valid evidence support.",
    )
    rejection_breakdown: dict[str, int] = Field(
        default_factory=dict,
        description="Count of rejections per RejectionReason code.",
    )
