"""Reliability scoring — score a trace corpus for CI gating."""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from pathlib import Path

from litmus.models import Trace, LLMCall, ToolCall
from litmus.faults import Fault, FaultType, apply_faults


@dataclass
class TraceScore:
    """Score for a single trace."""

    trace_id: str
    file_path: str
    steps: int = 0
    llm_calls: int = 0
    tool_calls: int = 0

    # Dimensions (0-100)
    correctness: float = 0  # Did the agent complete without errors?
    resilience: float = 0  # How many faults did it survive?
    efficiency: float = 0  # Reasonable token/call count?

    # Details
    errors: list[str] = field(default_factory=list)
    fault_results: dict[str, str] = field(default_factory=dict)  # fault_name -> "survived"|"crashed"

    @property
    def overall(self) -> float:
        """Weighted overall score."""
        return (
            self.correctness * 0.5
            + self.resilience * 0.3
            + self.efficiency * 0.2
        )


@dataclass
class CIReport:
    """Aggregate report for a CI run."""

    trace_count: int = 0
    scores: list[TraceScore] = field(default_factory=list)
    threshold: float = 80.0

    @property
    def average_score(self) -> float:
        if not self.scores:
            return 0.0
        return sum(s.overall for s in self.scores) / len(self.scores)

    @property
    def passed(self) -> bool:
        return self.average_score >= self.threshold

    @property
    def min_score(self) -> float:
        return min((s.overall for s in self.scores), default=0.0)

    @property
    def max_score(self) -> float:
        return max((s.overall for s in self.scores), default=0.0)

    def to_dict(self) -> dict:
        return {
            "passed": self.passed,
            "threshold": self.threshold,
            "average_score": round(self.average_score, 1),
            "min_score": round(self.min_score, 1),
            "max_score": round(self.max_score, 1),
            "trace_count": self.trace_count,
            "traces": [
                {
                    "trace_id": s.trace_id,
                    "file": s.file_path,
                    "overall": round(s.overall, 1),
                    "correctness": round(s.correctness, 1),
                    "resilience": round(s.resilience, 1),
                    "efficiency": round(s.efficiency, 1),
                    "errors": s.errors,
                }
                for s in self.scores
            ],
        }


def _score_correctness(trace: Trace) -> tuple[float, list[str]]:
    """Score: did the agent complete without errors?"""
    errors = []
    score = 100.0

    if not trace.steps:
        return 0.0, ["Empty trace — no steps recorded"]

    # Check for failed status
    if trace.status.value == "failed":
        score -= 50
        errors.append("Trace status is 'failed'")

    # Check each step for errors
    error_count = 0
    for i, step in enumerate(trace.steps):
        if isinstance(step, LLMCall):
            if step.error:
                error_count += 1
                errors.append(f"Step {i}: LLM error — {step.error}")
            elif step.response and step.response.status_code >= 400:
                error_count += 1
                errors.append(f"Step {i}: HTTP {step.response.status_code}")

    # Deduct per error
    if error_count > 0:
        score -= min(50, error_count * 15)

    # Check the last step has a successful response (agent completed)
    last = trace.steps[-1]
    if isinstance(last, LLMCall):
        if last.response and last.response.status_code == 200:
            body = last.response.body or {}
            stop = body.get("stop_reason") or ""
            # Anthropic: end_turn means the agent finished naturally
            # OpenAI: check finish_reason in choices
            choices = body.get("choices", [])
            if stop == "end_turn" or any(c.get("finish_reason") == "stop" for c in choices):
                pass  # Good — agent completed
            elif stop == "tool_use" or any(c.get("finish_reason") == "tool_calls" for c in choices):
                score -= 10
                errors.append("Trace ends with pending tool call — agent may not have finished")
        elif last.response is None:
            score -= 30
            errors.append("Last step has no response")

    return max(0, score), errors


def _score_resilience(trace: Trace) -> tuple[float, dict[str, str]]:
    """Score: how does the trace hold up under fault injection?

    We apply each fault type to each LLM step and check if the
    agent's remaining steps still look reasonable.
    """
    if not trace.llm_calls():
        return 100.0, {}

    fault_types = [FaultType.LLM_ERROR, FaultType.LLM_REFUSE, FaultType.LLM_TIMEOUT]
    results: dict[str, str] = {}
    survived = 0
    total = 0

    for i, step in enumerate(trace.steps):
        if not isinstance(step, LLMCall):
            continue
        for ft in fault_types:
            total += 1
            fault = Fault(fault_type=ft, step=i)
            faulted_trace = apply_faults(trace, [fault])

            # Check: does the faulted trace still have valid remaining steps?
            faulted_step = faulted_trace.steps[i]
            key = f"{ft.value}@step{i}"

            if isinstance(faulted_step, LLMCall) and faulted_step.response:
                if faulted_step.response.status_code < 400:
                    # Fault was applied but response still looks OK (refuse/hallucinate)
                    survived += 1
                    results[key] = "survived"
                else:
                    # Error response — check if there are recovery steps after
                    remaining = trace.steps[i + 1:]
                    if remaining:
                        survived += 1
                        results[key] = "survived (has recovery steps)"
                    else:
                        results[key] = "crashed (no recovery)"
            else:
                results[key] = "crashed (no response)"

    score = (survived / total * 100) if total > 0 else 100.0
    return score, results


def _score_efficiency(trace: Trace) -> float:
    """Score: reasonable resource usage?"""
    score = 100.0

    steps = len(trace.steps)
    llm_calls = len(trace.llm_calls())

    # Penalize excessive calls
    if llm_calls > 10:
        score -= (llm_calls - 10) * 5

    # Penalize very high latency (possible infinite loops)
    total_latency = sum(s.latency_ms for s in trace.steps)
    if total_latency > 60000:  # > 60 seconds total
        score -= 20
    elif total_latency > 30000:
        score -= 10

    # Check for duplicate calls (same request body)
    seen_bodies = set()
    duplicates = 0
    for step in trace.steps:
        if isinstance(step, LLMCall) and step.request.body:
            body_key = json.dumps(step.request.body, sort_keys=True)
            if body_key in seen_bodies:
                duplicates += 1
            seen_bodies.add(body_key)

    if duplicates > 0:
        score -= duplicates * 10

    return max(0, score)


def score_trace(trace: Trace, file_path: str = "") -> TraceScore:
    """Score a single trace across all dimensions."""
    correctness, errors = _score_correctness(trace)
    resilience, fault_results = _score_resilience(trace)
    efficiency = _score_efficiency(trace)

    return TraceScore(
        trace_id=trace.trace_id,
        file_path=file_path,
        steps=len(trace.steps),
        llm_calls=len(trace.llm_calls()),
        tool_calls=len(trace.tool_calls()),
        correctness=correctness,
        resilience=resilience,
        efficiency=efficiency,
        errors=errors,
        fault_results=fault_results,
    )


def run_ci(traces_dir: str, threshold: float = 80.0) -> CIReport:
    """Run CI scoring on all traces in a directory."""
    trace_dir = Path(traces_dir)
    trace_files = sorted(trace_dir.glob("*.trace.json"))

    report = CIReport(threshold=threshold)

    for tf in trace_files:
        try:
            trace = Trace.load(str(tf))
        except Exception as e:
            report.scores.append(TraceScore(
                trace_id="?",
                file_path=str(tf),
                errors=[f"Failed to load: {e}"],
            ))
            continue

        score = score_trace(trace, str(tf))
        report.scores.append(score)

    report.trace_count = len(report.scores)
    return report
