"""Temporal drift metrics — velocity, acceleration, and trend analysis.

Computes how drift is changing over time, not just the current score.
These metrics are metadata attached to DriftResult, not scoring inputs.
"""

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime


@dataclass
class TemporalMetrics:
    """Temporal analysis of drift score history."""
    velocity: float           # score change per day (positive = drifting faster)
    acceleration: float       # velocity change per day
    trend_direction: str      # "increasing" | "decreasing" | "stable"
    trend_strength: float     # 0-1, R² of linear fit over recent history
    lookback_tests: int       # how many tests used in computation


def compute_temporal_metrics(
    current_score: float,
    history: list,  # list of DriftResult, most recent first
    min_tests: int = 3,
) -> TemporalMetrics | None:
    """Compute velocity/acceleration from score history.

    # WHY temporal metrics as metadata, not scoring input: Temporal data tells
    # you HOW drift is changing (accelerating, decelerating, stable) but doesn't
    # change what the current drift IS. Mixing velocity into the score would
    # make identical snapshots score differently based on history, which breaks
    # the "same inputs → same score" guarantee needed for attestation chains.

    Args:
        current_score: The score from the just-completed test.
        history: Previous DriftResults, most recent first.
        min_tests: Minimum historical tests needed to compute metrics.

    Returns:
        TemporalMetrics, or None if insufficient history.
    """
    if len(history) < min_tests:
        return None

    # Reverse to chronological order and limit to recent history
    recent = list(reversed(history[:10]))

    # Build (days_from_first, score) pairs
    first_time = recent[0].created_at
    points: list[tuple[float, float]] = []
    for r in recent:
        days = _days_between(first_time, r.created_at)
        points.append((days, r.score))

    # Add current test as the latest point
    if points:
        last_days = points[-1][0]
        current_days = last_days + max(1.0, _avg_gap(points))
    else:
        current_days = 0.0
    points.append((current_days, current_score))

    # Velocity: average score change per day over last few tests
    velocities: list[float] = []
    for i in range(1, len(points)):
        dt = points[i][0] - points[i - 1][0]
        if dt > 0:
            v = (points[i][1] - points[i - 1][1]) / dt
            velocities.append(v)

    velocity = sum(velocities) / len(velocities) if velocities else 0.0

    # Acceleration: change in velocity
    if len(velocities) >= 2:
        recent_v = sum(velocities[-2:]) / 2
        older_v = sum(velocities[:-2]) / max(1, len(velocities) - 2) if len(velocities) > 2 else velocities[0]
        total_days = points[-1][0] - points[0][0]
        acceleration = (recent_v - older_v) / max(total_days, 1.0)
    else:
        acceleration = 0.0

    # WHY dead zone of ±0.5/day: Embedding similarity naturally fluctuates
    # due to LLM sampling temperature. A velocity under 0.5 drift-points/day
    # is noise, not signal. Without the dead zone, every test would show
    # "increasing" or "decreasing" even when the agent is genuinely stable.
    # Trend direction (with dead zone)
    if abs(velocity) < 0.5:
        trend_direction = "stable"
    elif velocity > 0:
        trend_direction = "increasing"
    else:
        trend_direction = "decreasing"

    # Trend strength: R² of linear fit
    trend_strength = _linear_r_squared(points)

    return TemporalMetrics(
        velocity=round(velocity, 4),
        acceleration=round(acceleration, 6),
        trend_direction=trend_direction,
        trend_strength=round(trend_strength, 4),
        lookback_tests=len(points) - 1,  # exclude current
    )


def compute_dimension_temporal(
    current_dim_scores: list,  # list of DimensionScore
    history: list,  # list of DriftResult
) -> dict[str, dict]:
    """Per-dimension temporal metrics.

    Args:
        current_dim_scores: Current test's dimension scores.
        history: Previous DriftResults.

    Returns:
        Dict mapping dimension name to temporal metrics dict.
    """
    result: dict[str, dict] = {}

    for ds in current_dim_scores:
        dim_name = ds.dimension.value

        # Build per-dimension history
        dim_history_scores: list[tuple[datetime, float]] = []
        for h in reversed(history[:10]):  # chronological
            for hds in h.dimension_scores:
                if hds.dimension.value == dim_name:
                    dim_history_scores.append((h.created_at, hds.score))
                    break

        if len(dim_history_scores) < 2:
            continue

        # Build points
        first_time = dim_history_scores[0][0]
        points: list[tuple[float, float]] = [
            (_days_between(first_time, t), s) for t, s in dim_history_scores
        ]
        # Add current
        if points:
            last_days = points[-1][0]
            current_days = last_days + max(1.0, _avg_gap(points))
        else:
            current_days = 0.0
        points.append((current_days, ds.score))

        # Velocity
        velocities: list[float] = []
        for i in range(1, len(points)):
            dt = points[i][0] - points[i - 1][0]
            if dt > 0:
                v = (points[i][1] - points[i - 1][1]) / dt
                velocities.append(v)

        velocity = sum(velocities) / len(velocities) if velocities else 0.0

        if abs(velocity) < 0.5:
            direction = "stable"
        elif velocity > 0:
            direction = "increasing"
        else:
            direction = "decreasing"

        result[dim_name] = {
            "velocity": round(velocity, 4),
            "trend_direction": direction,
            "trend_strength": round(_linear_r_squared(points), 4),
        }

    return result


def _days_between(a: datetime, b: datetime) -> float:
    """Days between two datetimes (always positive)."""
    delta = (b - a).total_seconds()
    return max(0.0, delta / 86400.0)


def _avg_gap(points: list[tuple[float, float]]) -> float:
    """Average time gap between consecutive points."""
    if len(points) < 2:
        return 1.0
    gaps = [points[i][0] - points[i - 1][0] for i in range(1, len(points))]
    return sum(gaps) / len(gaps) if gaps else 1.0


def _linear_r_squared(points: list[tuple[float, float]]) -> float:
    """R² of linear regression on (x, y) points.

    # WHY R² for trend strength: R² tells you how well a linear model fits
    # the score history. High R² (>0.8) means drift is moving in a consistent
    # direction — real drift. Low R² means scores are bouncing around —
    # noise or intermittent influence. This helps distinguish "agent is
    # steadily drifting" from "agent had one bad test."

    Returns 0.0 for degenerate cases (< 2 points, no variance).
    """
    n = len(points)
    if n < 2:
        return 0.0

    xs = [p[0] for p in points]
    ys = [p[1] for p in points]

    x_mean = sum(xs) / n
    y_mean = sum(ys) / n

    ss_tot = sum((y - y_mean) ** 2 for y in ys)
    if ss_tot < 1e-10:
        return 1.0  # All scores identical — perfectly "predicted"

    ss_xy = sum((x - x_mean) * (y - y_mean) for x, y in zip(xs, ys))
    ss_xx = sum((x - x_mean) ** 2 for x in xs)

    if ss_xx < 1e-10:
        return 0.0  # All tests at same time — can't fit

    slope = ss_xy / ss_xx
    intercept = y_mean - slope * x_mean

    ss_res = sum((y - (slope * x + intercept)) ** 2 for x, y in zip(xs, ys))

    r_squared = 1.0 - (ss_res / ss_tot)
    return max(0.0, min(1.0, r_squared))
