"""Model regression testing — compare DAG checkpoints and detect drift.

Compares two WorldDAG snapshots for structural changes, metric drift,
and causal graph differences. Supports A/B comparison and historical
performance tracking. Stdlib-only (ADR-007 Tier-0 compliance).
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

from orxaq.kernel.checkpoint import Checkpoint, restore_from_checkpoint
from orxaq.kernel.world_dag import WorldDAG


@dataclass
class StructuralDiff:
    """Structural difference between two DAGs."""

    added_entities: list[str] = field(default_factory=list)
    removed_entities: list[str] = field(default_factory=list)
    added_edges: list[tuple[str, str]] = field(default_factory=list)
    removed_edges: list[tuple[str, str]] = field(default_factory=list)
    modified_edges: list[dict] = field(default_factory=list)

    @property
    def has_changes(self) -> bool:
        return bool(
            self.added_entities
            or self.removed_entities
            or self.added_edges
            or self.removed_edges
            or self.modified_edges
        )

    @property
    def change_count(self) -> int:
        return (
            len(self.added_entities)
            + len(self.removed_entities)
            + len(self.added_edges)
            + len(self.removed_edges)
            + len(self.modified_edges)
        )

    def to_dict(self) -> dict:
        return {
            "has_changes": self.has_changes,
            "change_count": self.change_count,
            "added_entities": self.added_entities,
            "removed_entities": self.removed_entities,
            "added_edges": [{"source": s, "target": t} for s, t in self.added_edges],
            "removed_edges": [{"source": s, "target": t} for s, t in self.removed_edges],
            "modified_edges": self.modified_edges,
        }


@dataclass
class MetricDiff:
    """Metric difference between two DAG states."""

    name: str
    baseline_value: float
    candidate_value: float
    threshold: float = 0.0
    direction: str = "higher_is_better"

    @property
    def delta(self) -> float:
        return self.candidate_value - self.baseline_value

    @property
    def pct_change(self) -> float:
        if self.baseline_value == 0:
            return 0.0 if self.candidate_value == 0 else 100.0
        return (self.delta / abs(self.baseline_value)) * 100

    @property
    def is_regression(self) -> bool:
        """Check if change exceeds threshold in the wrong direction."""
        if self.direction == "higher_is_better":
            return self.delta < -self.threshold
        return self.delta > self.threshold

    def to_dict(self) -> dict:
        return {
            "name": self.name,
            "baseline": round(self.baseline_value, 6),
            "candidate": round(self.candidate_value, 6),
            "delta": round(self.delta, 6),
            "pct_change": round(self.pct_change, 2),
            "is_regression": self.is_regression,
            "direction": self.direction,
        }


@dataclass
class RegressionReport:
    """Full regression test report comparing two model versions."""

    baseline_name: str
    candidate_name: str
    structural: StructuralDiff
    metrics: list[MetricDiff] = field(default_factory=list)
    passed: bool = True
    notes: list[str] = field(default_factory=list)

    @property
    def regression_count(self) -> int:
        return sum(1 for m in self.metrics if m.is_regression)

    def to_dict(self) -> dict:
        return {
            "baseline": self.baseline_name,
            "candidate": self.candidate_name,
            "passed": self.passed,
            "regression_count": self.regression_count,
            "structural": self.structural.to_dict(),
            "metrics": [m.to_dict() for m in self.metrics],
            "notes": self.notes,
        }


def compare_structure(baseline: WorldDAG, candidate: WorldDAG) -> StructuralDiff:
    """Compare the structural topology of two DAGs."""
    diff = StructuralDiff()

    base_entities = set(baseline.entities.keys())
    cand_entities = set(candidate.entities.keys())
    diff.added_entities = sorted(cand_entities - base_entities)
    diff.removed_entities = sorted(base_entities - cand_entities)

    base_edges = set(baseline.edges.keys())
    cand_edges = set(candidate.edges.keys())
    diff.added_edges = sorted(
        (candidate.edges[eid].source, candidate.edges[eid].target)
        for eid in (cand_edges - base_edges)
    )
    diff.removed_edges = sorted(
        (baseline.edges[eid].source, baseline.edges[eid].target)
        for eid in (base_edges - cand_edges)
    )

    # Check for modified edges (same endpoints, different attributes)
    common = base_edges & cand_edges
    for edge_key in sorted(common):
        b_edge = baseline.edges[edge_key]
        c_edge = candidate.edges[edge_key]
        changes: dict[str, Any] = {}
        if abs(b_edge.confidence - c_edge.confidence) > 1e-6:
            changes["confidence"] = {
                "from": b_edge.confidence,
                "to": c_edge.confidence,
            }
        if abs(b_edge.soliton_score - c_edge.soliton_score) > 1e-6:
            changes["soliton_score"] = {
                "from": b_edge.soliton_score,
                "to": c_edge.soliton_score,
            }
        if b_edge.mechanism != c_edge.mechanism:
            changes["mechanism"] = {
                "from": b_edge.mechanism,
                "to": c_edge.mechanism,
            }
        if changes:
            diff.modified_edges.append(
                {
                    "source": edge_key[0],
                    "target": edge_key[1],
                    "changes": changes,
                }
            )

    return diff


def compare_checkpoints(
    baseline: Checkpoint,
    candidate: Checkpoint,
) -> StructuralDiff:
    """Compare two checkpoints structurally."""
    base_dag = restore_from_checkpoint(baseline)
    cand_dag = restore_from_checkpoint(candidate)
    return compare_structure(base_dag, cand_dag)


def dag_metrics(dag: WorldDAG) -> dict[str, float]:
    """Extract numeric metrics from a DAG for comparison."""
    entities = dag.entities
    edges = dag.edges

    entity_count = len(entities)
    edge_count = len(edges)
    density = edge_count / (entity_count * (entity_count - 1)) if entity_count > 1 else 0.0

    confidences = [e.confidence for e in edges.values()]
    avg_confidence = sum(confidences) / len(confidences) if confidences else 0.0

    soliton_scores = [e.soliton_score for e in edges.values()]
    avg_soliton = sum(soliton_scores) / len(soliton_scores) if soliton_scores else 0.0

    return {
        "entity_count": float(entity_count),
        "edge_count": float(edge_count),
        "density": density,
        "avg_confidence": avg_confidence,
        "avg_soliton_score": avg_soliton,
    }


def run_regression_test(
    baseline: Checkpoint,
    candidate: Checkpoint,
    metric_thresholds: dict[str, float] | None = None,
) -> RegressionReport:
    """Run a full regression test between two checkpoints.

    metric_thresholds: maps metric name to max allowed degradation.
    Default threshold is 0.05 for rate metrics, 0 for counts.
    """
    thresholds = metric_thresholds or {}

    base_dag = restore_from_checkpoint(baseline)
    cand_dag = restore_from_checkpoint(candidate)

    structural = compare_structure(base_dag, cand_dag)

    base_metrics = dag_metrics(base_dag)
    cand_metrics = dag_metrics(cand_dag)

    metric_diffs: list[MetricDiff] = []
    default_thresholds = {
        "density": 0.05,
        "avg_confidence": 0.05,
        "avg_soliton_score": 0.05,
        "entity_count": 0.0,
        "edge_count": 0.0,
    }

    for name in sorted(base_metrics.keys()):
        threshold = thresholds.get(name, default_thresholds.get(name, 0.0))
        md = MetricDiff(
            name=name,
            baseline_value=base_metrics[name],
            candidate_value=cand_metrics.get(name, 0.0),
            threshold=threshold,
            direction="higher_is_better",
        )
        metric_diffs.append(md)

    regressions = [m for m in metric_diffs if m.is_regression]
    passed = len(regressions) == 0

    notes: list[str] = []
    if structural.has_changes:
        notes.append(f"Structural changes: {structural.change_count} total")
    if regressions:
        names = ", ".join(m.name for m in regressions)
        notes.append(f"Metric regressions: {names}")

    return RegressionReport(
        baseline_name=f"{baseline.name}@{baseline.version}",
        candidate_name=f"{candidate.name}@{candidate.version}",
        structural=structural,
        metrics=metric_diffs,
        passed=passed,
        notes=notes,
    )


class PerformanceTracker:
    """Track model performance metrics over multiple versions.

    Stores metrics for each version and detects trends::

        tracker = PerformanceTracker()
        tracker.record("v1.0", {"accuracy": 0.95, "coverage": 0.88})
        tracker.record("v1.1", {"accuracy": 0.94, "coverage": 0.90})
        drift = tracker.detect_drift("accuracy", window=2)
    """

    def __init__(self) -> None:
        self._history: list[dict] = []

    def record(self, version: str, metrics: dict[str, float]) -> None:
        """Record metrics for a version."""
        self._history.append(
            {
                "version": version,
                "metrics": dict(metrics),
            }
        )

    def history(self) -> list[dict]:
        """Return full version history."""
        return list(self._history)

    def latest(self) -> dict | None:
        """Return the most recent record."""
        return self._history[-1] if self._history else None

    def metric_series(self, metric_name: str) -> list[float]:
        """Extract time-series for a single metric."""
        return [entry["metrics"].get(metric_name, 0.0) for entry in self._history]

    def detect_drift(
        self,
        metric_name: str,
        window: int = 3,
        threshold: float = 0.05,
    ) -> dict:
        """Detect metric drift over a rolling window.

        Returns drift analysis: trend direction, magnitude, alert flag.
        """
        series = self.metric_series(metric_name)
        if len(series) < 2:
            return {
                "metric": metric_name,
                "trend": "insufficient_data",
                "magnitude": 0.0,
                "alert": False,
            }

        recent = series[-min(window, len(series)) :]
        first = recent[0]
        last = recent[-1]

        if first == 0:
            magnitude = 0.0 if last == 0 else 1.0
        else:
            magnitude = (last - first) / abs(first)

        if magnitude > threshold:
            trend = "improving"
        elif magnitude < -threshold:
            trend = "degrading"
        else:
            trend = "stable"

        return {
            "metric": metric_name,
            "trend": trend,
            "magnitude": round(magnitude, 4),
            "alert": trend == "degrading",
            "window": len(recent),
            "values": [round(v, 6) for v in recent],
        }

    def summary(self) -> dict:
        """Summary of tracked performance."""
        if not self._history:
            return {"versions": 0, "metrics": []}
        latest = self._history[-1]
        return {
            "versions": len(self._history),
            "latest_version": latest["version"],
            "metrics": sorted(latest["metrics"].keys()),
        }


__all__ = [
    "MetricDiff",
    "PerformanceTracker",
    "RegressionReport",
    "StructuralDiff",
    "compare_checkpoints",
    "compare_structure",
    "dag_metrics",
    "run_regression_test",
]
