"""
Eval result models for releaseops.

Captures the output of running an eval suite against a bundle,
including per-case results, assertion results, and the overall gate decision.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional


class GateDecision(str, Enum):
    """Outcome of the quality gate evaluation."""

    PASS = "PASS"
    FAIL = "FAIL"


@dataclass(frozen=True)
class AssertionResult:
    """Result of evaluating a single assertion."""

    judge_type: str  # Matches JudgeType value
    passed: bool
    confidence: float = 1.0  # 1.0 for deterministic judges, variable for LLM
    reasoning: str = ""  # LLM judge explanation
    details: Dict[str, Any] = field(default_factory=dict)  # Judge-specific data
    weight: float = 1.0

    @property
    def weighted_score(self) -> float:
        """Score contribution: weight * (1 if passed else 0)."""
        return self.weight if self.passed else 0.0

    def to_dict(self) -> dict:
        d: dict = {
            "type": self.judge_type,
            "passed": self.passed,
        }
        if self.confidence != 1.0:
            d["confidence"] = self.confidence
        if self.reasoning:
            d["reasoning"] = self.reasoning
        if self.details:
            d["details"] = dict(self.details)
        return d

    @classmethod
    def from_dict(cls, data: dict) -> AssertionResult:
        for key in ("type", "passed"):
            if key not in data:
                raise ValueError(f"AssertionResult missing required field: '{key}'")
        return cls(
            judge_type=data["type"],
            passed=data["passed"],
            confidence=data.get("confidence", 1.0),
            reasoning=data.get("reasoning", ""),
            details=data.get("details", {}),
            weight=data.get("weight", 1.0),
        )


@dataclass(frozen=True)
class CaseResult:
    """Result of evaluating a single test case."""

    case_id: str
    passed: bool
    assertions: List[AssertionResult] = field(default_factory=list)
    output: Optional[str] = None  # The actual LLM output
    latency_ms: Optional[float] = None  # Time to generate output
    error: Optional[str] = None  # Error message if case failed to execute

    @property
    def total_weight(self) -> float:
        return sum(a.weight for a in self.assertions)

    @property
    def weighted_score(self) -> float:
        return sum(a.weighted_score for a in self.assertions)

    @property
    def score_ratio(self) -> float:
        """Score as a ratio of weighted_score / total_weight."""
        if self.total_weight == 0:
            return 0.0
        return self.weighted_score / self.total_weight

    def to_dict(self) -> dict:
        d: dict = {
            "case_id": self.case_id,
            "passed": self.passed,
            "assertions": [a.to_dict() for a in self.assertions],
        }
        if self.output is not None:
            d["output"] = self.output
        if self.latency_ms is not None:
            d["latency_ms"] = self.latency_ms
        if self.error:
            d["error"] = self.error
        return d

    @classmethod
    def from_dict(cls, data: dict) -> CaseResult:
        for key in ("case_id", "passed"):
            if key not in data:
                raise ValueError(f"CaseResult missing required field: '{key}'")
        assertions = [
            AssertionResult.from_dict(a) for a in data.get("assertions", [])
        ]
        return cls(
            case_id=data["case_id"],
            passed=data["passed"],
            assertions=assertions,
            output=data.get("output"),
            latency_ms=data.get("latency_ms"),
            error=data.get("error"),
        )


@dataclass(frozen=True)
class ScoreCard:
    """Aggregated scoring summary for an eval run."""

    total_cases: int
    passed: int
    failed: int
    pass_rate: float
    gate_decision: GateDecision

    @property
    def errored(self) -> int:
        return self.total_cases - self.passed - self.failed

    def to_dict(self) -> dict:
        return {
            "total_cases": self.total_cases,
            "passed": self.passed,
            "failed": self.failed,
            "pass_rate": self.pass_rate,
            "gate_decision": self.gate_decision.value,
        }

    @classmethod
    def from_dict(cls, data: dict) -> ScoreCard:
        for key in ("total_cases", "passed", "failed", "pass_rate", "gate_decision"):
            if key not in data:
                raise ValueError(f"ScoreCard missing required field: '{key}'")
        return cls(
            total_cases=data["total_cases"],
            passed=data["passed"],
            failed=data["failed"],
            pass_rate=data["pass_rate"],
            gate_decision=GateDecision(data["gate_decision"]),
        )

    @classmethod
    def compute(
        cls, results: List[CaseResult], pass_threshold: float = 0.95
    ) -> ScoreCard:
        """Compute a ScoreCard from a list of case results."""
        total = len(results)
        passed = sum(1 for r in results if r.passed)
        failed = total - passed
        pass_rate = passed / total if total > 0 else 0.0
        gate_decision = (
            GateDecision.PASS if pass_rate >= pass_threshold else GateDecision.FAIL
        )
        return cls(
            total_cases=total,
            passed=passed,
            failed=failed,
            pass_rate=pass_rate,
            gate_decision=gate_decision,
        )


@dataclass(frozen=True)
class EvalReport:
    """
    Complete evaluation report for a bundle.

    Stored as YAML in .releaseops/eval_reports/{bundle-hash}.yaml.
    Contains the full results of running an eval suite against a bundle version.
    """

    bundle_id: str
    bundle_version: str
    suite_id: str
    timestamp: str  # ISO 8601
    summary: ScoreCard
    results: List[CaseResult] = field(default_factory=list)
    bundle_hash: Optional[str] = None

    @property
    def passed(self) -> bool:
        return self.summary.gate_decision == GateDecision.PASS

    def get_result(self, case_id: str) -> Optional[CaseResult]:
        """Look up a case result by ID."""
        for result in self.results:
            if result.case_id == case_id:
                return result
        return None

    @property
    def failed_cases(self) -> List[CaseResult]:
        return [r for r in self.results if not r.passed]

    def to_dict(self) -> dict:
        d: dict = {
            "bundle_id": self.bundle_id,
            "bundle_version": self.bundle_version,
            "suite_id": self.suite_id,
            "timestamp": self.timestamp,
            "summary": self.summary.to_dict(),
            "results": [r.to_dict() for r in self.results],
        }
        if self.bundle_hash:
            d["bundle_hash"] = self.bundle_hash
        return d

    @classmethod
    def from_dict(cls, data: dict) -> EvalReport:
        for key in ("bundle_id", "bundle_version", "suite_id", "summary"):
            if key not in data:
                raise ValueError(f"EvalReport missing required field: '{key}'")
        summary = ScoreCard.from_dict(data["summary"])
        results = [CaseResult.from_dict(r) for r in data.get("results", [])]
        return cls(
            bundle_id=data["bundle_id"],
            bundle_version=data["bundle_version"],
            suite_id=data["suite_id"],
            timestamp=data.get("timestamp", ""),
            summary=summary,
            results=results,
            bundle_hash=data.get("bundle_hash"),
        )
