"""Structural outlier detection for traces.

Identifies traces that are structurally unusual compared to the overall population.
Uses z-score normalization (subtract mean, divide by std) per feature, then flags
traces with high euclidean distance from centroid.
"""

from __future__ import annotations

import math
from dataclasses import dataclass
from typing import Any

from .features import extract_features


@dataclass
class OutlierReport:
    structural_outlier_ids: list[str]
    total_traces: int
    outlier_pct: float


def detect_structural_outliers(
    traces: list[dict[str, Any]],
    *,
    percentile_threshold: float = 95.0,
    min_traces: int = 10,
) -> OutlierReport:
    """Detect structural outliers across all traces.

    A trace is a structural outlier if its z-score normalized distance from
    the global centroid is above the given percentile threshold.

    Args:
        traces: Full trace list
        percentile_threshold: Percentile of distances to use as threshold (default 95)
        min_traces: Minimum traces needed for outlier detection (default 10)

    Returns:
        OutlierReport with list of outlier trace_ids
    """
    if len(traces) < min_traces:
        return OutlierReport(
            structural_outlier_ids=[],
            total_traces=len(traces),
            outlier_pct=0.0,
        )

    trace_by_id = {t.get("trace_id"): t for t in traces if t.get("trace_id")}
    if not trace_by_id:
        return OutlierReport(structural_outlier_ids=[], total_traces=0, outlier_pct=0.0)

    items: list[tuple[str, list[float]]] = []
    raw_features: list[list[float]] = []

    for tid, trace in trace_by_id.items():
        features = extract_features(trace)
        vector = _features_to_continuous_vector(trace, features)
        items.append((tid, vector))
        raw_features.append(vector)

    if len(items) < min_traces:
        return OutlierReport(structural_outlier_ids=[], total_traces=len(traces), outlier_pct=0.0)

    normalized_vectors = _z_normalize(raw_features)

    centroid = _compute_centroid(normalized_vectors)

    distances: list[tuple[float, str]] = []
    for (tid, _), norm_vec in zip(items, normalized_vectors):
        dist = _euclidean_distance(norm_vec, centroid)
        distances.append((dist, tid))

    distances.sort(reverse=True)

    max_dist = distances[0][0] if distances else 0

    if max_dist < 1e-10:
        return OutlierReport(structural_outlier_ids=[], total_traces=len(traces), outlier_pct=0.0)

    threshold_idx = min(int(len(distances) * percentile_threshold / 100), len(distances) - 1)
    threshold = distances[threshold_idx][0] if distances else 0

    outlier_ids = [tid for dist, tid in distances if dist >= threshold]

    return OutlierReport(
        structural_outlier_ids=outlier_ids,
        total_traces=len(traces),
        outlier_pct=len(outlier_ids) / len(traces) * 100 if traces else 0.0,
    )


def _features_to_continuous_vector(trace: dict[str, Any], features: Any) -> list[float]:
    spans = trace.get("spans", [])
    total_tokens = sum((s.get("input_tokens", 0) or 0) + (s.get("output_tokens", 0) or 0) for s in spans)

    span_count = len(spans)

    composition_values = {
        "llm_only": 0.0,
        "retrieval_then_llm": 1.0,
        "agent_loop": 2.0,
        "embedding_heavy": 3.0,
    }
    composition_val = composition_values.get(features.span_composition, 0.5)

    error_val = 1.0 if features.has_error else 0.0

    return [float(total_tokens), float(span_count), composition_val, error_val]


def _z_normalize(vectors: list[list[float]]) -> list[list[float]]:
    if not vectors or not vectors[0]:
        return vectors

    n_features = len(vectors[0])
    n_vectors = len(vectors)

    means = [0.0] * n_features
    for vec in vectors:
        for i, v in enumerate(vec):
            means[i] += v
    for i in range(n_features):
        means[i] /= n_vectors

    variances = [0.0] * n_features
    for vec in vectors:
        for i, v in enumerate(vec):
            diff = v - means[i]
            variances[i] += diff * diff
    for i in range(n_features):
        variances[i] /= n_vectors

    stds = [math.sqrt(v) if v > 1e-10 else 1.0 for v in variances]

    normalized = []
    for vec in vectors:
        norm_vec = [(vec[i] - means[i]) / stds[i] for i in range(n_features)]
        normalized.append(norm_vec)

    return normalized


def _compute_centroid(vectors: list[list[float]]) -> list[float]:
    if not vectors:
        return [0.0, 0.0, 0.0, 0.0]

    n = len(vectors[0])
    centroid = [0.0] * n
    for vec in vectors:
        for i, v in enumerate(vec):
            centroid[i] += v
    for i in range(n):
        centroid[i] /= len(vectors)
    return centroid


def _euclidean_distance(a: list[float], b: list[float]) -> float:
    return math.sqrt(sum((x - y) ** 2 for x, y in zip(a, b)))
