"""Coverage analysis: eval coverage gaps and blind spot detection.

Deterministic — no LLM required. Identifies segments with no evaluations
and reports calibration freshness.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any

from ..review.aggregator import aggregate_summaries
from ..review.models import AggregateStats, Segment
from ..review.segmenter import segment


@dataclass
class BlindSpot:
    """A segment with zero evaluation coverage."""
    segment_key: str
    dimension: str
    trace_count: int
    pct_of_traces: float  # percentage of total traces
    human_count: int = 0
    judge_count: int = 0
    coverage_pct: float = 0.0


@dataclass
class StaleSegment:
    """A segment with old evaluations (may be stale)."""
    segment_key: str
    dimension: str
    trace_count: int
    last_graded_days_ago: int
    human_count: int
    judge_count: int


@dataclass
class CalibrationStatus:
    """Judge calibration status."""
    calibrated: bool
    calibrated_at: str | None = None
    days_since_calibration: int | None = None
    is_stale: bool = False
    tpr: float | None = None
    tnr: float | None = None
    grades_since_calibration: int = 0


@dataclass
class CoverageResult:
    """Full coverage analysis output."""
    project: str
    trace_count: int
    total_evals: int
    coverage_pct: float  # overall eval coverage
    human_eval_count: int
    judge_eval_count: int
    blind_spots: list[BlindSpot] = field(default_factory=list)
    stale_segments: list[StaleSegment] = field(default_factory=list)
    calibration: CalibrationStatus | None = None
    segments: list[Segment] = field(default_factory=list)

    # Recommendations
    recommendation: str = ""
    grade_count: int = 0  # suggested number of traces to grade

    def to_dict(self) -> dict[str, Any]:
        return {
            "project": self.project,
            "trace_count": self.trace_count,
            "total_evals": self.total_evals,
            "coverage_pct": round(self.coverage_pct, 2),
            "human_eval_count": self.human_eval_count,
            "judge_eval_count": self.judge_eval_count,
            "blind_spots": [
                {
                    "segment_key": bs.segment_key,
                    "dimension": bs.dimension,
                    "trace_count": bs.trace_count,
                    "pct_of_traces": round(bs.pct_of_traces, 2),
                    "coverage_pct": round(bs.coverage_pct, 2),
                }
                for bs in self.blind_spots
            ],
            "stale_segments": [
                {
                    "segment_key": ss.segment_key,
                    "dimension": ss.dimension,
                    "trace_count": ss.trace_count,
                    "last_graded_days_ago": ss.last_graded_days_ago,
                }
                for ss in self.stale_segments
            ],
            "calibration": {
                "calibrated": self.calibration.calibrated,
                "calibrated_at": self.calibration.calibrated_at,
                "days_since_calibration": self.calibration.days_since_calibration,
                "is_stale": self.calibration.is_stale,
                "tpr": self.calibration.tpr,
                "tnr": self.calibration.tnr,
                "grades_since_calibration": self.calibration.grades_since_calibration,
            } if self.calibration else None,
            "recommendation": self.recommendation,
            "grade_count": self.grade_count,
        }


def run_coverage(
    project: str,
    traces: list[dict[str, Any]],
    evaluations: dict[str, dict[str, Any]] | None = None,
    calibration_status: dict[str, Any] | None = None,
    *,
    blind_spot_threshold: float = 5.0,  # segments with <5% coverage
    stale_days: int = 14,  # segments not graded in N days
) -> CoverageResult:
    """Run coverage analysis on traces and evaluations.

    Args:
        project: Project name
        traces: List of trace dicts
        evaluations: Dict of trace_id -> eval dict
        calibration_status: Optional calibration status from backend
        blind_spot_threshold: Segments with less than this % coverage are blind spots
        stale_days: Segments not graded in this many days are stale

    Returns:
        CoverageResult with blind spots and recommendations
    """
    evaluations = evaluations or {}
    trace_count = len(traces)

    if trace_count == 0:
        return CoverageResult(
            project=project,
            trace_count=0,
            total_evals=0,
            coverage_pct=0.0,
            human_eval_count=0,
            judge_eval_count=0,
            recommendation="No traces to analyze.",
        )

    # Aggregate overall stats
    stats = aggregate_summaries(traces, evaluations)

    # Segment traces
    segments = segment(traces, evaluations, from_summaries=True)

    # Find blind spots (segments with 0 or very low coverage)
    blind_spots = _find_blind_spots(segments, trace_count, blind_spot_threshold)

    # Check calibration status
    calibration = _parse_calibration(calibration_status)

    # Build recommendations
    recommendation, grade_count = _build_recommendations(
        blind_spots, calibration, trace_count, stats.eval_count
    )

    return CoverageResult(
        project=project,
        trace_count=trace_count,
        total_evals=stats.eval_count,
        coverage_pct=(stats.eval_count / trace_count * 100) if trace_count > 0 else 0.0,
        human_eval_count=stats.human_eval_count,
        judge_eval_count=stats.judge_eval_count,
        blind_spots=blind_spots,
        stale_segments=[],  # TODO: implement if we store eval timestamps
        calibration=calibration,
        segments=segments,
        recommendation=recommendation,
        grade_count=grade_count,
    )


def _find_blind_spots(
    segments: list[Segment],
    total_traces: int,
    threshold: float,
) -> list[BlindSpot]:
    """Find segments with low or zero evaluation coverage."""
    blind_spots = []

    for seg in segments:
        seg_coverage = 0.0
        if seg.trace_count > 0:
            eval_count = seg.stats.human_eval_count + seg.stats.judge_eval_count
            seg_coverage = (eval_count / seg.trace_count) * 100

        if seg_coverage < threshold:
            pct_of_traces = (seg.trace_count / total_traces * 100) if total_traces > 0 else 0.0
            blind_spots.append(BlindSpot(
                segment_key=seg.key,
                dimension=seg.dimension,
                trace_count=seg.trace_count,
                pct_of_traces=pct_of_traces,
                human_count=seg.stats.human_eval_count,
                judge_count=seg.stats.judge_eval_count,
                coverage_pct=seg_coverage,
            ))

    blind_spots.sort(key=lambda b: b.pct_of_traces, reverse=True)
    return blind_spots


def _parse_calibration(cal_status: dict[str, Any] | None) -> CalibrationStatus | None:
    """Parse calibration status from backend response."""
    if cal_status is None:
        return None

    cal = cal_status.get("calibration")
    if cal is None:
        return CalibrationStatus(calibrated=False)

    calibrated_at = cal.get("calibrated_at")
    days_since = None
    if calibrated_at:
        try:
            cal_dt = datetime.fromisoformat(calibrated_at.replace("Z", "+00:00"))
            days_since = (datetime.now(timezone.utc) - cal_dt).days
        except (ValueError, TypeError):
            pass

    is_stale = cal_status.get("stale", False) or (days_since is not None and days_since > 7)

    return CalibrationStatus(
        calibrated=True,
        calibrated_at=calibrated_at,
        days_since_calibration=days_since,
        is_stale=is_stale,
        tpr=cal.get("tpr"),
        tnr=cal.get("tnr"),
        grades_since_calibration=cal_status.get("grades_since_calibration", 0),
    )


def _build_recommendations(
    blind_spots: list[BlindSpot],
    calibration: CalibrationStatus | None,
    total_traces: int,
    total_evals: int,
) -> tuple[str, int]:
    """Build actionable recommendation based on coverage gaps."""
    recommendations = []
    grade_count = 0

    # Blind spot recommendation
    if blind_spots:
        # Focus on largest blind spots
        top_blind = blind_spots[:3]
        total_blind_traces = sum(b.trace_count for b in top_blind)

        if total_evals == 0:
            recommendations.append(
                f"Grade at least 20 traces across {len(top_blind)} blind spots "
                f"(tags, models, or span types with no evaluations)."
            )
            grade_count = max(20, min(50, total_traces // 10))
        else:
            # Suggest grading proportional to blind spot size
            suggested = min(30, max(10, total_blind_traces // 5))
            recommendations.append(
                f"Grade {suggested} traces from blind spots: "
                f"{', '.join(b.segment_key for b in top_blind[:2])}."
            )
            grade_count = suggested

    # Calibration recommendation
    if calibration:
        if not calibration.calibrated:
            recommendations.append(
                "Calibrate the judge to establish quality baselines."
            )
        elif calibration.is_stale:
            recommendations.append(
                f"Judge calibration is {calibration.days_since_calibration} days old "
                f"and may be stale. Re-calibrate after grading."
            )
        elif calibration.grades_since_calibration > 50:
            recommendations.append(
                f"{calibration.grades_since_calibration} new grades since last calibration. "
                "Re-calibrate for accurate scoring."
            )

    # Overall coverage recommendation
    coverage_pct = (total_evals / total_traces * 100) if total_traces > 0 else 0
    if coverage_pct < 10 and total_evals > 0:
        recommendations.append(
            f"Current coverage is {coverage_pct:.0f}%. "
            "Aim for 25%+ for confident decision-making."
        )

    if not recommendations:
        recommendations.append("Coverage looks healthy. Keep grading to maintain signal.")

    return " ".join(recommendations), grade_count
