"""
Judge V2 data models — structured types for SmartSelector, RubricJudge, and DiagnosisRouter.

All handoffs between V2 components use these typed dataclasses instead of dicts/strings.
"""

from dataclasses import dataclass, field
from enum import Enum
from typing import Any


class RootCause(str, Enum):
    """Valid root causes for structured diagnosis. LLM output validated against these."""

    # Hallucination
    UNGROUNDED_CLAIM = "ungrounded_claim"
    TOOL_RESULT_MISMATCH = "tool_result_mismatch"
    FABRICATED_REFERENCE = "fabricated_reference"
    OVERCONFIDENT_ASSERTION = "overconfident_assertion"

    # Drift
    TASK_DEVIATION = "task_deviation"
    TOPIC_SHIFT = "topic_shift"
    ROLE_DEVIATION = "role_deviation"

    # Safety
    HARMFUL_CONTENT = "harmful_content"
    PII_LEAK = "pii_leak"
    INJECTION_SUCCESS = "injection_success"
    POLICY_VIOLATION = "policy_violation"

    # Quality
    INCOMPLETE_RESPONSE = "incomplete_response"
    INCOHERENT_LOGIC = "incoherent_logic"
    IRRELEVANT_CONTENT = "irrelevant_content"
    VAGUE_RESPONSE = "vague_response"

    # Tool usage
    WRONG_TOOL = "wrong_tool"
    BAD_PARAMETERS = "bad_parameters"
    MISINTERPRETED_RESULT = "misinterpreted_result"
    UNHANDLED_TOOL_ERROR = "unhandled_tool_error"

    # Loop
    STUCK_LOOP = "stuck_loop"
    CIRCULAR_REASONING = "circular_reasoning"
    TOOL_RETRY_LOOP = "tool_retry_loop"

    # Fallback
    UNKNOWN = "unknown"

    @classmethod
    def parse(cls, value: str) -> "RootCause":
        """Parse a string to RootCause, falling back to UNKNOWN."""
        try:
            return cls(value)
        except ValueError:
            return cls.UNKNOWN


@dataclass
class StructuredDiagnosis:
    """Structured output from a rubric judge. Bridges judges and remediation."""

    # What
    issue_domain: str
    issue_type: str
    severity: str  # critical, high, medium, low, info

    # Where
    span_id: str
    affected_region: str | None = None

    # Why
    root_cause: RootCause = RootCause.UNKNOWN
    evidence: list[str] = field(default_factory=list)

    # How to fix
    recommended_strategy: str = ""
    fix_instruction: str = ""
    fix_context: dict[str, Any] = field(default_factory=dict)

    # Confidence
    confidence: float = 0.0
    reasoning: str = ""

    # Metadata
    judge_id: str = ""
    judge_model: str = ""
    evaluation_time_ms: float = 0.0

    @property
    def is_passing(self) -> bool:
        """True if no actionable issue was found."""
        return self.severity in ("info", "") or (
            self.root_cause == RootCause.UNKNOWN and not self.evidence
        )


@dataclass
class EnrichedContext:
    """All context available for judge selection. Built once per step."""

    # From FastDetector
    signal_hints: list[str] = field(default_factory=list)

    # Span metadata
    span_type: str = "llm"
    span_name: str = ""
    model: str | None = None
    tool_name: str | None = None

    # Step position
    step_index: int = 0
    total_steps_so_far: int = 0

    # Previous step outcomes
    previous_step_had_issue: bool = False
    previous_step_issue_type: str | None = None
    consecutive_issues: int = 0

    # Workflow context (from PatternCache)
    workflow_id: str | None = None
    workflow_name: str | None = None
    workflow_known_failure_points: list[str] = field(default_factory=list)
    workflow_common_issues: list[str] = field(default_factory=list)

    # Customer patterns (from PatternCache)
    agent_name: str | None = None
    agent_framework: str | None = None
    customer_top_issues: list[str] = field(default_factory=list)

    # Phase 2: historical accuracy
    judge_accuracy_scores: dict[str, float] = field(default_factory=dict)

    @property
    def is_first_step(self) -> bool:
        return self.step_index == 0

    @property
    def is_deep_in_trace(self) -> bool:
        return self.step_index > 5

    def has_signal(self, signal: str) -> bool:
        return signal in self.signal_hints


@dataclass
class SelectedJudge:
    """A judge selected by SmartSelector with its relevance score."""

    judge_id: str
    name: str
    domain: str
    relevance_score: float
    template_id: str | None = None


@dataclass
class RubricCriterion:
    """A single evaluation criterion within a rubric."""

    name: str
    question: str
    weight: float
    scoring: str  # "binary" or "scale"
    failure_root_cause: str


@dataclass
class JudgeRubric:
    """Evaluation rubric for a specific issue domain."""

    domain: str
    name: str
    criteria: list[RubricCriterion] = field(default_factory=list)
    pass_threshold: float = 0.7
    retry_threshold: float = 0.3
    stop_threshold: float = 0.1
    root_cause_options: list[str] = field(default_factory=list)
    root_cause_to_strategy: dict[str, str] = field(default_factory=dict)
    output_schema: dict[str, Any] = field(default_factory=dict)


@dataclass
class FixTemplate:
    """Template for generating a specific remediation fix."""

    strategy: str
    fix_type: str
    template: str
    placement: str  # "prepend_system", "append_user", "replace_last"
    max_retries: int = 1
    fallback: list[str] = field(default_factory=list)

    def render(self, context: dict[str, Any]) -> str:
        """Render template with context values. Graceful on missing keys."""
        try:
            return self.template.format(**context)
        except KeyError:
            return self.template.format_map(_SafeDict(context))


@dataclass
class RemediationPlan:
    """Complete plan for fixing a detected issue."""

    diagnosis: StructuredDiagnosis
    strategy: str
    fix_type: str
    fix_content: str
    fix_placement: str
    max_retries: int = 1
    backoff_ms: float = 500.0
    fallback_strategies: list[str] = field(default_factory=list)


class _SafeDict(dict):
    """Dict that returns {key} for missing keys instead of raising KeyError."""

    def __missing__(self, key: str) -> str:
        return "{" + key + "}"
