"""Diagnostics: system health check for an LLM project.

Deterministic — no LLM required. Reuses review aggregator and pattern detectors
to produce a structured health report.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

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


@dataclass
class AttentionItem:
    """Something that needs the user's attention."""
    status: str  # "ok", "warning", "critical"
    label: str
    detail: str
    evidence: list[str] = field(default_factory=list)


@dataclass
class DiagnosticsResult:
    """Structured diagnostics output."""
    project: str
    trace_count: int
    span_count: int

    # Reliability
    success_rate: float  # 1.0 - (error traces / total traces)
    error_count: int
    judge_pass_rate: float | None  # None if no judge scores
    human_pass_rate: float | None  # None if no human scores

    # Performance
    latency_p50: float | None
    latency_p95: float | None
    latency_p99: float | None
    latency_avg: float | None

    # Cost
    cost_avg: float | None
    cost_total: float | None
    cost_p95: float | None

    # Eval coverage
    human_eval_count: int
    judge_eval_count: int
    eval_coverage_pct: float  # % of traces with any eval

    # Breakdowns
    stats: AggregateStats

    # Segments
    segments: list[Segment] = field(default_factory=list)

    # What needs attention
    attention: list[AttentionItem] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        d = {
            "project": self.project,
            "trace_count": self.trace_count,
            "span_count": self.span_count,
            "reliability": {
                "success_rate": round(self.success_rate, 4),
                "error_count": self.error_count,
                "judge_pass_rate": round(self.judge_pass_rate, 4) if self.judge_pass_rate is not None else None,
                "human_pass_rate": round(self.human_pass_rate, 4) if self.human_pass_rate is not None else None,
            },
            "performance": {
                "latency_avg": round(self.latency_avg, 3) if self.latency_avg is not None else None,
                "latency_p50": round(self.latency_p50, 3) if self.latency_p50 is not None else None,
                "latency_p95": round(self.latency_p95, 3) if self.latency_p95 is not None else None,
                "latency_p99": round(self.latency_p99, 3) if self.latency_p99 is not None else None,
            },
            "cost": {
                "avg_per_trace": round(self.cost_avg, 6) if self.cost_avg is not None else None,
                "total": round(self.cost_total, 4) if self.cost_total is not None else None,
                "p95": round(self.cost_p95, 6) if self.cost_p95 is not None else None,
            },
            "eval_coverage": {
                "human_count": self.human_eval_count,
                "judge_count": self.judge_eval_count,
                "coverage_pct": round(self.eval_coverage_pct, 2),
            },
            "models": [
                {
                    "model": m.model,
                    "provider": m.provider,
                    "count": m.count,
                    "total_cost": round(m.total_cost, 6),
                    "avg_cost": round(m.avg_cost, 6),
                    "avg_latency": round(m.avg_latency, 3),
                    "total_tokens": m.total_tokens,
                }
                for m in self.stats.models
            ],
            "span_kinds": [
                {
                    "kind": sk.kind,
                    "count": sk.count,
                    "avg_latency": round(sk.avg_latency, 3),
                    "pct_of_latency": round(sk.pct_of_total, 1),
                    "error_count": sk.error_count,
                }
                for sk in self.stats.span_kinds
            ],
            "segments": [
                {
                    "key": seg.key,
                    "dimension": seg.dimension,
                    "trace_count": seg.trace_count,
                    "error_rate": round(seg.stats.error_count / max(seg.stats.span_count, 1), 4),
                    "latency_p95": round(seg.stats.latency.p95, 3) if seg.stats.latency.count > 0 else None,
                    "cost_mean": round(seg.stats.cost.mean, 6) if seg.stats.cost.count > 0 else None,
                    "judge_pass_rate": round(seg.stats.judge_avg_grade, 4) if seg.stats.judge_avg_grade is not None else None,
                }
                for seg in self.segments
            ],
            "attention": [
                {
                    "status": a.status,
                    "label": a.label,
                    "detail": a.detail,
                    "evidence": a.evidence,
                }
                for a in self.attention
            ],
        }
        return d


def run_diagnostics(
    project: str,
    traces: list[dict[str, Any]],
    evaluations: dict[str, dict[str, Any]] | None = None,
) -> DiagnosticsResult:
    """Run diagnostics on a project's traces and evaluations."""
    evaluations = evaluations or {}

    stats = aggregate_summaries(traces, evaluations)
    patterns = run_all(traces, stats, evaluations)

    # Reliability
    error_traces = sum(
        1 for t in traces if t.get("status") == "error"
    )
    success_rate = 1.0 - (error_traces / len(traces)) if traces else 0.0

    # Latency
    lat = stats.latency
    has_latency = lat.count > 0

    # Cost
    cost = stats.cost
    has_cost = cost.count > 0

    # Eval coverage
    eval_coverage_pct = (stats.eval_count / len(traces) * 100) if traces else 0.0

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

    # Attention items
    attention = _build_attention(stats, patterns, traces, evaluations)
    attention.extend(_build_segment_attention(stats, segments, traces))

    result = DiagnosticsResult(
        project=project,
        trace_count=stats.trace_count,
        span_count=stats.span_count,
        success_rate=success_rate,
        error_count=stats.error_count,
        judge_pass_rate=stats.judge_avg_grade,
        human_pass_rate=stats.human_avg_grade,
        latency_p50=lat.median if has_latency else None,
        latency_p95=lat.p95 if has_latency else None,
        latency_p99=lat.p99 if has_latency else None,
        latency_avg=lat.mean if has_latency else None,
        cost_avg=cost.mean if has_cost else None,
        cost_total=cost.total if has_cost else None,
        cost_p95=cost.p95 if has_cost else None,
        human_eval_count=stats.human_eval_count,
        judge_eval_count=stats.judge_eval_count,
        eval_coverage_pct=eval_coverage_pct,
        stats=stats,
        segments=segments,
        attention=attention,
    )
    return result


def _build_attention(
    stats: AggregateStats,
    patterns: list[Pattern],
    traces: list[dict[str, Any]],
    evaluations: dict[str, dict[str, Any]],
) -> list[AttentionItem]:
    """Convert patterns + threshold checks into attention items."""
    items: list[AttentionItem] = []

    # Error rate check
    if stats.trace_count > 0:
        error_traces = sum(1 for t in traces if t.get("status") == "error")
        error_rate = error_traces / stats.trace_count
        if error_rate > 0.05:
            items.append(AttentionItem(
                status="critical",
                label="Error rate elevated",
                detail=f"{error_rate:.0%} of traces have errors ({error_traces}/{stats.trace_count})",
            ))
        elif error_rate > 0.01:
            items.append(AttentionItem(
                status="warning",
                label="Error rate above 1%",
                detail=f"{error_rate:.1%} of traces have errors ({error_traces}/{stats.trace_count})",
            ))
        else:
            items.append(AttentionItem(
                status="ok",
                label="Error rate",
                detail=f"{error_rate:.1%}" if error_rate > 0 else "No errors",
            ))

    # Judge pass rate check
    if stats.judge_eval_count > 0 and stats.judge_avg_grade is not None:
        rate = stats.judge_avg_grade
        if rate < 0.7:
            items.append(AttentionItem(
                status="critical",
                label="Judge pass rate low",
                detail=f"{rate:.0%} pass rate ({stats.judge_eval_count} scored)",
            ))
        elif rate < 0.85:
            items.append(AttentionItem(
                status="warning",
                label="Judge pass rate below 85%",
                detail=f"{rate:.0%} pass rate ({stats.judge_eval_count} scored)",
            ))
        else:
            items.append(AttentionItem(
                status="ok",
                label="Judge pass rate",
                detail=f"{rate:.0%} ({stats.judge_eval_count} scored)",
            ))

    # Latency check
    if stats.latency.count > 0:
        p95 = stats.latency.p95
        if p95 > 10.0:
            items.append(AttentionItem(
                status="critical",
                label="p95 latency above 10s",
                detail=f"p95: {p95:.1f}s, p99: {stats.latency.p99:.1f}s",
            ))
        elif p95 > 5.0:
            items.append(AttentionItem(
                status="warning",
                label="p95 latency above 5s",
                detail=f"p95: {p95:.1f}s",
            ))
        else:
            items.append(AttentionItem(
                status="ok",
                label="Latency",
                detail=f"p95: {p95:.2f}s",
            ))

    # Eval coverage check
    if stats.trace_count > 0:
        coverage = (stats.eval_count / stats.trace_count) * 100
        if coverage < 5:
            items.append(AttentionItem(
                status="warning",
                label="Low eval coverage",
                detail=f"{coverage:.0f}% of traces have evaluations",
            ))
        elif coverage < 25:
            items.append(AttentionItem(
                status="ok",
                label="Eval coverage",
                detail=f"{coverage:.0f}% — grade more for better signal",
            ))
        else:
            items.append(AttentionItem(
                status="ok",
                label="Eval coverage",
                detail=f"{coverage:.0f}%",
            ))

    # Convert pattern detections to attention items
    for p in patterns:
        items.append(AttentionItem(
            status=p.severity,
            label=p.name.replace("_", " ").title(),
            detail=p.description,
            evidence=p.evidence[:3],
        ))

    return items


def _build_segment_attention(
    overall: AggregateStats,
    segments: list[Segment],
    traces: list[dict[str, Any]],
) -> list[AttentionItem]:
    """Generate attention items for segments that deviate from overall."""
    items: list[AttentionItem] = []
    if not segments or overall.trace_count == 0:
        return items

    # Use trace-level error rate for consistency with overall attention checks
    overall_error_traces = sum(1 for t in traces if t.get("status") == "error")
    overall_error_rate = overall_error_traces / overall.trace_count

    # Build trace lookup for segment-level error rate
    trace_status: dict[str, str] = {
        t.get("trace_id", ""): t.get("status", "ok") for t in traces
    }

    for seg in segments:
        deviations: list[str] = []

        # Error rate: segment > 2x overall AND segment > 5% (trace-level)
        if seg.trace_ids:
            seg_error_traces = sum(1 for tid in seg.trace_ids if trace_status.get(tid) == "error")
            seg_error_rate = seg_error_traces / len(seg.trace_ids)
            if (
                overall_error_rate > 0
                and seg_error_rate > overall_error_rate * 2
                and seg_error_rate > 0.05
            ):
                deviations.append(
                    f"error rate {seg_error_rate:.0%} vs {overall_error_rate:.0%} overall"
                )

        # Latency p95: segment > 1.5x overall
        if seg.stats.latency.count > 0 and overall.latency.count > 0:
            if overall.latency.p95 > 0 and seg.stats.latency.p95 > overall.latency.p95 * 1.5:
                ratio = seg.stats.latency.p95 / overall.latency.p95
                deviations.append(
                    f"p95 latency {seg.stats.latency.p95:.1f}s vs {overall.latency.p95:.1f}s overall ({ratio:.1f}x)"
                )

        # Cost mean: segment > 1.5x or < 0.5x overall
        if seg.stats.cost.count > 0 and overall.cost.count > 0 and overall.cost.mean > 0:
            ratio = seg.stats.cost.mean / overall.cost.mean
            if ratio > 1.5:
                deviations.append(
                    f"avg cost ${seg.stats.cost.mean:.4f} vs ${overall.cost.mean:.4f} overall ({ratio:.1f}x)"
                )
            elif ratio < 0.5:
                deviations.append(
                    f"avg cost ${seg.stats.cost.mean:.4f} vs ${overall.cost.mean:.4f} overall ({ratio:.1f}x cheaper)"
                )

        # Judge pass rate: segment differs by > 15pp
        if (
            seg.stats.judge_avg_grade is not None
            and overall.judge_avg_grade is not None
            and seg.stats.judge_eval_count >= 3
        ):
            diff = seg.stats.judge_avg_grade - overall.judge_avg_grade
            if abs(diff) > 0.15:
                direction = "higher" if diff > 0 else "lower"
                deviations.append(
                    f"judge pass rate {seg.stats.judge_avg_grade:.0%} vs {overall.judge_avg_grade:.0%} overall ({direction})"
                )

        if deviations:
            severity = "critical" if len(deviations) > 1 else "warning"
            items.append(AttentionItem(
                status=severity,
                label=f"Segment deviation: {seg.key}",
                detail=f"{seg.trace_count} traces — " + "; ".join(deviations),
            ))

    return items
