from __future__ import annotations

import uuid
from collections import defaultdict
from dataclasses import dataclass, field
from numbers import Real
from typing import Any, Iterable, Literal

from models import AlertRecord, EvidenceRecord, HealthScoreRecord, LayerSummaryRecord, MetricRecord, RunContext
from storage import LocalSessionStore, utc_now

Severity = Literal["info", "warning", "error", "critical"]

_SEVERITY_ORDER: dict[Severity, int] = {
    "info": 0,
    "warning": 1,
    "error": 2,
    "critical": 3,
}
_SEVERITY_PENALTY: dict[Severity, float] = {
    "info": 5.0,
    "warning": 15.0,
    "error": 30.0,
    "critical": 45.0,
}
_HEALTH_SCORE_KEYS = ("optimization", "representation", "generalization", "runtime")
_TRAIN_LOSS_KEYS = ("train/loss", "training/loss")
_VAL_LOSS_KEYS = ("val/loss", "validation/loss")
_TRAIN_ACCURACY_KEYS = ("train/accuracy", "training/accuracy", "train/acc")
_VAL_ACCURACY_KEYS = ("val/accuracy", "validation/accuracy", "val/acc")


def _normalize_metric_keys(values: Iterable[str], *, label: str) -> tuple[str, ...]:
    normalized_values: list[str] = []
    for raw_value in values:
        if not isinstance(raw_value, str):
            raise TypeError(f"{label} values must be strings.")
        candidate = raw_value.strip()
        if not candidate:
            raise ValueError(f"{label} values must not be empty.")
        if candidate not in normalized_values:
            normalized_values.append(candidate)
    if not normalized_values:
        raise ValueError(f"{label} must include at least one metric key.")
    return tuple(normalized_values)


def _validate_positive(name: str, value: Real) -> float:
    if isinstance(value, bool) or not isinstance(value, Real):
        raise TypeError(f"{name} must be a real number.")
    normalized_value = float(value)
    if normalized_value <= 0.0:
        raise ValueError(f"{name} must be greater than zero.")
    return normalized_value


def _validate_non_negative(name: str, value: Real) -> float:
    if isinstance(value, bool) or not isinstance(value, Real):
        raise TypeError(f"{name} must be a real number.")
    normalized_value = float(value)
    if normalized_value < 0.0:
        raise ValueError(f"{name} must not be negative.")
    return normalized_value


def _validate_probability(name: str, value: Real) -> float:
    normalized_value = _validate_non_negative(name, value)
    if normalized_value > 1.0:
        raise ValueError(f"{name} must not exceed 1.0.")
    return normalized_value


def _validate_positive_int(name: str, value: Any) -> int:
    if isinstance(value, bool) or not isinstance(value, int):
        raise TypeError(f"{name} must be an integer.")
    if value <= 0:
        raise ValueError(f"{name} must be a positive integer.")
    return int(value)


def _validate_ratio_thresholds(name: str, warning: float, error: float, critical: float) -> None:
    if not (warning <= error <= critical):
        raise ValueError(
            f"{name} thresholds must be ordered warning <= error <= critical."
        )


def _validate_inverse_thresholds(name: str, warning: float, error: float, critical: float) -> None:
    if not (warning >= error >= critical):
        raise ValueError(
            f"{name} thresholds must be ordered warning >= error >= critical."
        )


@dataclass(frozen=True, slots=True)
class AnalyzerThresholds:
    exploding_gradient_warning_norm: float = 10.0
    exploding_gradient_error_norm: float = 25.0
    exploding_gradient_critical_norm: float = 50.0
    vanishing_gradient_norm: float = 1e-4
    vanishing_gradient_warning_ratio: float = 0.5
    vanishing_gradient_error_ratio: float = 0.75
    vanishing_gradient_critical_ratio: float = 0.9
    vanishing_gradient_min_samples: int = 4
    stagnant_layer_gradient_norm: float = 1e-4
    stagnant_layer_warning_probes: int = 2
    stagnant_layer_error_probes: int = 4
    stagnant_layer_critical_probes: int = 6
    activation_collapse_warning_dead_unit_ratio: float = 0.8
    activation_collapse_error_dead_unit_ratio: float = 0.92
    activation_collapse_critical_dead_unit_ratio: float = 0.98
    activation_collapse_warning_saturation_ratio: float = 0.9
    activation_collapse_error_saturation_ratio: float = 0.97
    activation_collapse_critical_saturation_ratio: float = 0.995
    activation_collapse_warning_std: float = 0.05
    activation_collapse_error_std: float = 0.01
    activation_collapse_critical_std: float = 0.001
    overfitting_warning_gap: float = 0.2
    overfitting_error_gap: float = 0.4
    overfitting_critical_gap: float = 0.7
    overfitting_min_points: int = 3
    runtime_warning_step_time_ms: float = 250.0
    runtime_error_step_time_ms: float = 500.0
    runtime_critical_step_time_ms: float = 1000.0
    runtime_warning_data_fraction: float = 0.4
    runtime_error_data_fraction: float = 0.6
    runtime_critical_data_fraction: float = 0.75
    runtime_warning_jitter_ratio: float = 0.25
    runtime_error_jitter_ratio: float = 0.5
    runtime_critical_jitter_ratio: float = 0.75

    def __post_init__(self) -> None:
        object.__setattr__(
            self,
            "exploding_gradient_warning_norm",
            _validate_positive(
                "exploding_gradient_warning_norm",
                self.exploding_gradient_warning_norm,
            ),
        )
        object.__setattr__(
            self,
            "exploding_gradient_error_norm",
            _validate_positive(
                "exploding_gradient_error_norm",
                self.exploding_gradient_error_norm,
            ),
        )
        object.__setattr__(
            self,
            "exploding_gradient_critical_norm",
            _validate_positive(
                "exploding_gradient_critical_norm",
                self.exploding_gradient_critical_norm,
            ),
        )
        if not (
            self.exploding_gradient_warning_norm
            <= self.exploding_gradient_error_norm
            <= self.exploding_gradient_critical_norm
        ):
            raise ValueError(
                "Exploding-gradient thresholds must be ordered warning <= error <= critical."
            )

        object.__setattr__(
            self,
            "vanishing_gradient_norm",
            _validate_non_negative("vanishing_gradient_norm", self.vanishing_gradient_norm),
        )
        object.__setattr__(
            self,
            "vanishing_gradient_warning_ratio",
            _validate_probability(
                "vanishing_gradient_warning_ratio",
                self.vanishing_gradient_warning_ratio,
            ),
        )
        object.__setattr__(
            self,
            "vanishing_gradient_error_ratio",
            _validate_probability(
                "vanishing_gradient_error_ratio",
                self.vanishing_gradient_error_ratio,
            ),
        )
        object.__setattr__(
            self,
            "vanishing_gradient_critical_ratio",
            _validate_probability(
                "vanishing_gradient_critical_ratio",
                self.vanishing_gradient_critical_ratio,
            ),
        )
        _validate_ratio_thresholds(
            "Vanishing-gradient ratio",
            self.vanishing_gradient_warning_ratio,
            self.vanishing_gradient_error_ratio,
            self.vanishing_gradient_critical_ratio,
        )
        object.__setattr__(
            self,
            "vanishing_gradient_min_samples",
            _validate_positive_int(
                "vanishing_gradient_min_samples",
                self.vanishing_gradient_min_samples,
            ),
        )

        object.__setattr__(
            self,
            "stagnant_layer_gradient_norm",
            _validate_non_negative(
                "stagnant_layer_gradient_norm",
                self.stagnant_layer_gradient_norm,
            ),
        )
        object.__setattr__(
            self,
            "stagnant_layer_warning_probes",
            _validate_positive_int(
                "stagnant_layer_warning_probes",
                self.stagnant_layer_warning_probes,
            ),
        )
        object.__setattr__(
            self,
            "stagnant_layer_error_probes",
            _validate_positive_int(
                "stagnant_layer_error_probes",
                self.stagnant_layer_error_probes,
            ),
        )
        object.__setattr__(
            self,
            "stagnant_layer_critical_probes",
            _validate_positive_int(
                "stagnant_layer_critical_probes",
                self.stagnant_layer_critical_probes,
            ),
        )
        if not (
            self.stagnant_layer_warning_probes
            <= self.stagnant_layer_error_probes
            <= self.stagnant_layer_critical_probes
        ):
            raise ValueError(
                "Stagnant-layer thresholds must be ordered warning <= error <= critical."
            )
        if self.stagnant_layer_warning_probes <= 0:
            raise ValueError("stagnant_layer_warning_probes must be a positive integer.")

        for field_name in (
            "activation_collapse_warning_dead_unit_ratio",
            "activation_collapse_error_dead_unit_ratio",
            "activation_collapse_critical_dead_unit_ratio",
            "activation_collapse_warning_saturation_ratio",
            "activation_collapse_error_saturation_ratio",
            "activation_collapse_critical_saturation_ratio",
            "runtime_warning_data_fraction",
            "runtime_error_data_fraction",
            "runtime_critical_data_fraction",
            "runtime_warning_jitter_ratio",
            "runtime_error_jitter_ratio",
            "runtime_critical_jitter_ratio",
        ):
            object.__setattr__(
                self,
                field_name,
                _validate_probability(field_name, getattr(self, field_name)),
            )

        _validate_ratio_thresholds(
            "Activation dead-unit ratio",
            self.activation_collapse_warning_dead_unit_ratio,
            self.activation_collapse_error_dead_unit_ratio,
            self.activation_collapse_critical_dead_unit_ratio,
        )
        _validate_ratio_thresholds(
            "Activation saturation ratio",
            self.activation_collapse_warning_saturation_ratio,
            self.activation_collapse_error_saturation_ratio,
            self.activation_collapse_critical_saturation_ratio,
        )

        object.__setattr__(
            self,
            "activation_collapse_warning_std",
            _validate_non_negative(
                "activation_collapse_warning_std",
                self.activation_collapse_warning_std,
            ),
        )
        object.__setattr__(
            self,
            "activation_collapse_error_std",
            _validate_non_negative(
                "activation_collapse_error_std",
                self.activation_collapse_error_std,
            ),
        )
        object.__setattr__(
            self,
            "activation_collapse_critical_std",
            _validate_non_negative(
                "activation_collapse_critical_std",
                self.activation_collapse_critical_std,
            ),
        )
        _validate_inverse_thresholds(
            "Activation std",
            self.activation_collapse_warning_std,
            self.activation_collapse_error_std,
            self.activation_collapse_critical_std,
        )

        object.__setattr__(
            self,
            "overfitting_warning_gap",
            _validate_non_negative("overfitting_warning_gap", self.overfitting_warning_gap),
        )
        object.__setattr__(
            self,
            "overfitting_error_gap",
            _validate_non_negative("overfitting_error_gap", self.overfitting_error_gap),
        )
        object.__setattr__(
            self,
            "overfitting_critical_gap",
            _validate_non_negative("overfitting_critical_gap", self.overfitting_critical_gap),
        )
        if not (
            self.overfitting_warning_gap
            <= self.overfitting_error_gap
            <= self.overfitting_critical_gap
        ):
            raise ValueError(
                "Overfitting thresholds must be ordered warning <= error <= critical."
            )
        object.__setattr__(
            self,
            "overfitting_min_points",
            _validate_positive_int("overfitting_min_points", self.overfitting_min_points),
        )
        if self.overfitting_min_points <= 1:
            raise ValueError("overfitting_min_points must be greater than one.")

        object.__setattr__(
            self,
            "runtime_warning_step_time_ms",
            _validate_positive("runtime_warning_step_time_ms", self.runtime_warning_step_time_ms),
        )
        object.__setattr__(
            self,
            "runtime_error_step_time_ms",
            _validate_positive("runtime_error_step_time_ms", self.runtime_error_step_time_ms),
        )
        object.__setattr__(
            self,
            "runtime_critical_step_time_ms",
            _validate_positive(
                "runtime_critical_step_time_ms",
                self.runtime_critical_step_time_ms,
            ),
        )
        if not (
            self.runtime_warning_step_time_ms
            <= self.runtime_error_step_time_ms
            <= self.runtime_critical_step_time_ms
        ):
            raise ValueError(
                "Runtime step-time thresholds must be ordered warning <= error <= critical."
            )
        _validate_ratio_thresholds(
            "Runtime data-fraction",
            self.runtime_warning_data_fraction,
            self.runtime_error_data_fraction,
            self.runtime_critical_data_fraction,
        )
        _validate_ratio_thresholds(
            "Runtime jitter ratio",
            self.runtime_warning_jitter_ratio,
            self.runtime_error_jitter_ratio,
            self.runtime_critical_jitter_ratio,
        )


@dataclass(frozen=True, slots=True)
class AnalyzerEngineConfig:
    thresholds: AnalyzerThresholds = field(default_factory=AnalyzerThresholds)
    replace_existing_outputs: bool = True
    step_time_metric_keys: tuple[str, ...] = field(
        default_factory=lambda: ("system/runtime/step_time_ms", "system/step_time_ms")
    )
    data_time_metric_keys: tuple[str, ...] = field(
        default_factory=lambda: ("system/runtime/data_time_ms", "system/data_time_ms")
    )
    compute_time_metric_keys: tuple[str, ...] = field(
        default_factory=lambda: ("system/runtime/compute_time_ms", "system/compute_time_ms")
    )

    def __post_init__(self) -> None:
        if not isinstance(self.thresholds, AnalyzerThresholds):
            raise TypeError("thresholds must be an AnalyzerThresholds instance.")
        if not isinstance(self.replace_existing_outputs, bool):
            raise TypeError("replace_existing_outputs must be a boolean.")
        object.__setattr__(
            self,
            "step_time_metric_keys",
            _normalize_metric_keys(self.step_time_metric_keys, label="step_time_metric_keys"),
        )
        object.__setattr__(
            self,
            "data_time_metric_keys",
            _normalize_metric_keys(self.data_time_metric_keys, label="data_time_metric_keys"),
        )
        object.__setattr__(
            self,
            "compute_time_metric_keys",
            _normalize_metric_keys(
                self.compute_time_metric_keys,
                label="compute_time_metric_keys",
            ),
        )


@dataclass(frozen=True, slots=True)
class AnalyzerResult:
    alerts: tuple[AlertRecord, ...]
    health_scores: tuple[HealthScoreRecord, ...]


class AnalyzerEngine:
    """Rule-based diagnostics derived from persisted telemetry for a single run."""

    def __init__(
        self,
        session: Any,
        *,
        config: AnalyzerEngineConfig | None = None,
    ) -> None:
        self._context, self._store = self._resolve_session_dependencies(session)
        self._config = config or AnalyzerEngineConfig()

    @property
    def config(self) -> AnalyzerEngineConfig:
        return self._config

    @property
    def run_id(self) -> str:
        return self._context.run_id

    def analyze(self, *, replace_existing: bool | None = None) -> AnalyzerResult:
        """Evaluate persisted telemetry, persist derived diagnostics, and return them."""

        metrics = self._store.read_scalar_metrics(self.run_id)
        layer_summaries = self._store.read_layer_summaries(self.run_id)

        alerts: list[AlertRecord] = []

        exploding_gradients = self._build_exploding_gradient_alert(
            layer_summaries,
            sequence_id=len(alerts),
        )
        if exploding_gradients is not None:
            alerts.append(exploding_gradients)

        vanishing_gradients = self._build_vanishing_gradient_alert(
            layer_summaries,
            sequence_id=len(alerts),
        )
        if vanishing_gradients is not None:
            alerts.append(vanishing_gradients)

        stagnant_layers = self._build_stagnant_layer_alert(
            layer_summaries,
            sequence_id=len(alerts),
        )
        if stagnant_layers is not None:
            alerts.append(stagnant_layers)

        activation_collapse = self._build_activation_collapse_alert(
            layer_summaries,
            sequence_id=len(alerts),
        )
        if activation_collapse is not None:
            alerts.append(activation_collapse)

        overfitting_onset = self._build_overfitting_alert(
            metrics,
            sequence_id=len(alerts),
        )
        if overfitting_onset is not None:
            alerts.append(overfitting_onset)

        runtime_bottleneck = self._build_runtime_bottleneck_alert(
            metrics,
            sequence_id=len(alerts),
        )
        if runtime_bottleneck is not None:
            alerts.append(runtime_bottleneck)

        health_scores = self._build_health_scores(tuple(alerts))
        persisted_alerts, persisted_scores = self._store.write_analysis_outputs(
            self.run_id,
            alerts=tuple(alerts),
            health_scores=health_scores,
            replace_existing=(
                self._config.replace_existing_outputs
                if replace_existing is None
                else replace_existing
            ),
        )
        return AnalyzerResult(
            alerts=persisted_alerts,
            health_scores=persisted_scores,
        )

    def read_alerts(
        self,
        *,
        rule_id: str | None = None,
        category: str | None = None,
        severity: str | None = None,
        step: int | None = None,
        step_min: int | None = None,
        step_max: int | None = None,
        limit: int | None = None,
    ) -> tuple[AlertRecord, ...]:
        return self._store.read_alerts(
            self.run_id,
            rule_id=rule_id,
            category=category,
            severity=severity,
            step=step,
            step_min=step_min,
            step_max=step_max,
            limit=limit,
        )

    def read_health_scores(
        self,
        *,
        score_key: str | None = None,
        category: str | None = None,
        limit: int | None = None,
    ) -> tuple[HealthScoreRecord, ...]:
        return self._store.read_health_scores(
            self.run_id,
            score_key=score_key,
            category=category,
            limit=limit,
        )

    def _build_exploding_gradient_alert(
        self,
        layer_summaries: tuple[LayerSummaryRecord, ...],
        *,
        sequence_id: int,
    ) -> AlertRecord | None:
        thresholds = self._config.thresholds
        gradients = [
            summary
            for summary in layer_summaries
            if summary.summary_type == "gradient" and summary.norm is not None
        ]
        if not gradients:
            return None

        peak_summary = max(gradients, key=lambda item: float(item.norm or 0.0))
        peak_norm = float(peak_summary.norm or 0.0)
        severity = self._severity_from_upper_bound(
            peak_norm,
            warning=thresholds.exploding_gradient_warning_norm,
            error=thresholds.exploding_gradient_error_norm,
            critical=thresholds.exploding_gradient_critical_norm,
        )
        if severity is None:
            return None

        triggered = [
            summary
            for summary in gradients
            if float(summary.norm or 0.0) >= thresholds.exploding_gradient_warning_norm
        ]
        step_start, step_end = self._layer_step_range(triggered)

        evidence = (
            EvidenceRecord(
                kind="layer_summary",
                source=peak_summary.module_name,
                summary="Peak gradient norm observed for a single instrumented module.",
                observed_value=peak_norm,
                threshold_value=self._threshold_for_upper_severity(
                    severity,
                    warning=thresholds.exploding_gradient_warning_norm,
                    error=thresholds.exploding_gradient_error_norm,
                    critical=thresholds.exploding_gradient_critical_norm,
                ),
                step=peak_summary.step,
                metadata={"module_type": peak_summary.module_type},
            ),
            EvidenceRecord(
                kind="aggregate",
                source="gradient_norm",
                summary="Number of gradient summaries above the warning threshold.",
                observed_value=len(triggered),
                threshold_value=1,
                step_start=step_start,
                step_end=step_end,
                metadata={
                    "affected_modules": sorted({summary.module_name for summary in triggered}),
                },
            ),
            EvidenceRecord(
                kind="threshold",
                source="config",
                summary="Configured exploding-gradient warning threshold.",
                threshold_value=thresholds.exploding_gradient_warning_norm,
            ),
        )

        return self._make_alert(
            sequence_id=sequence_id,
            rule_id="exploding_gradients",
            category="optimization",
            severity=severity,
            title="Exploding gradients detected",
            summary="Gradient norms exceeded the configured stability threshold.",
            fact=(
                f"Peak gradient norm reached {peak_norm:.4f} in '{peak_summary.module_name}'"
                f" at step {peak_summary.step}."
            ),
            inference=(
                "Optimization appears unstable and may diverge without reducing update magnitude."
            ),
            step=peak_summary.step,
            step_start=step_start,
            step_end=step_end,
            evidence=evidence,
            metadata={"peak_module": peak_summary.module_name},
        )

    def _build_vanishing_gradient_alert(
        self,
        layer_summaries: tuple[LayerSummaryRecord, ...],
        *,
        sequence_id: int,
    ) -> AlertRecord | None:
        thresholds = self._config.thresholds
        gradients = [
            summary
            for summary in layer_summaries
            if summary.summary_type == "gradient" and summary.norm is not None
        ]
        if len(gradients) < thresholds.vanishing_gradient_min_samples:
            return None

        weak_gradients = [
            summary
            for summary in gradients
            if float(summary.norm or 0.0) <= thresholds.vanishing_gradient_norm
        ]
        weak_ratio = len(weak_gradients) / len(gradients)
        severity = self._severity_from_upper_bound(
            weak_ratio,
            warning=thresholds.vanishing_gradient_warning_ratio,
            error=thresholds.vanishing_gradient_error_ratio,
            critical=thresholds.vanishing_gradient_critical_ratio,
        )
        if severity is None:
            return None

        weakest_summary = min(weak_gradients, key=lambda item: float(item.norm or 0.0))
        step_start, step_end = self._layer_step_range(weak_gradients)

        evidence = (
            EvidenceRecord(
                kind="aggregate",
                source="gradient_norm",
                summary="Fraction of gradient summaries below the vanishing threshold.",
                observed_value=weak_ratio,
                threshold_value=self._threshold_for_upper_severity(
                    severity,
                    warning=thresholds.vanishing_gradient_warning_ratio,
                    error=thresholds.vanishing_gradient_error_ratio,
                    critical=thresholds.vanishing_gradient_critical_ratio,
                ),
                step_start=step_start,
                step_end=step_end,
                metadata={
                    "weak_summary_count": len(weak_gradients),
                    "total_summary_count": len(gradients),
                },
            ),
            EvidenceRecord(
                kind="layer_summary",
                source=weakest_summary.module_name,
                summary="Smallest observed gradient norm within the affected window.",
                observed_value=float(weakest_summary.norm or 0.0),
                threshold_value=thresholds.vanishing_gradient_norm,
                step=weakest_summary.step,
                metadata={"module_type": weakest_summary.module_type},
            ),
        )

        return self._make_alert(
            sequence_id=sequence_id,
            rule_id="vanishing_gradients",
            category="optimization",
            severity=severity,
            title="Vanishing gradients detected",
            summary="A large share of gradient summaries fell below the minimum learning signal.",
            fact=(
                f"{len(weak_gradients)} of {len(gradients)} gradient summaries"
                f" stayed at or below {thresholds.vanishing_gradient_norm:.6f}."
            ),
            inference=(
                "Large parts of the model are receiving too little gradient signal to keep learning."
            ),
            step=weakest_summary.step,
            step_start=step_start,
            step_end=step_end,
            evidence=evidence,
            metadata={"weak_gradient_ratio": weak_ratio},
        )

    def _build_stagnant_layer_alert(
        self,
        layer_summaries: tuple[LayerSummaryRecord, ...],
        *,
        sequence_id: int,
    ) -> AlertRecord | None:
        thresholds = self._config.thresholds
        gradients = [
            summary
            for summary in layer_summaries
            if summary.summary_type == "gradient" and summary.norm is not None
        ]
        if not gradients:
            return None

        low_signal_steps_by_module: dict[str, set[int]] = defaultdict(set)
        module_types: dict[str, str] = {}
        for summary in gradients:
            module_types.setdefault(summary.module_name, summary.module_type)
            if float(summary.norm or 0.0) <= thresholds.stagnant_layer_gradient_norm:
                low_signal_steps_by_module[summary.module_name].add(summary.step)

        affected_modules = {
            module_name: sorted(steps)
            for module_name, steps in low_signal_steps_by_module.items()
            if len(steps) >= thresholds.stagnant_layer_warning_probes
        }
        if not affected_modules:
            return None

        longest_persistence = max(len(steps) for steps in affected_modules.values())
        severity = self._severity_from_upper_bound(
            float(longest_persistence),
            warning=float(thresholds.stagnant_layer_warning_probes),
            error=float(thresholds.stagnant_layer_error_probes),
            critical=float(thresholds.stagnant_layer_critical_probes),
        )
        if severity is None:
            return None

        top_modules = sorted(
            affected_modules.items(),
            key=lambda item: (-len(item[1]), item[0]),
        )[:3]
        step_start = min(steps[0] for steps in affected_modules.values())
        step_end = max(steps[-1] for steps in affected_modules.values())
        representative_module, representative_steps = top_modules[0]

        evidence = tuple(
            EvidenceRecord(
                kind="layer_summary",
                source=module_name,
                summary="Low-gradient probes persisted for this module.",
                observed_value=len(steps),
                threshold_value=thresholds.stagnant_layer_warning_probes,
                step_start=steps[0],
                step_end=steps[-1],
                metadata={"module_type": module_types.get(module_name)},
            )
            for module_name, steps in top_modules
        ) + (
            EvidenceRecord(
                kind="threshold",
                source="config",
                summary="Configured stagnant-layer gradient norm threshold.",
                threshold_value=thresholds.stagnant_layer_gradient_norm,
            ),
        )

        return self._make_alert(
            sequence_id=sequence_id,
            rule_id="stagnant_layers",
            category="optimization",
            severity=severity,
            title="Stagnant layers detected",
            summary="One or more layers showed persistently weak gradient activity.",
            fact=(
                f"{len(affected_modules)} modules stayed below"
                f" {thresholds.stagnant_layer_gradient_norm:.6f} for at least"
                f" {thresholds.stagnant_layer_warning_probes} probes."
            ),
            inference=(
                "These layers may not be updating enough to contribute meaningful learning progress."
            ),
            step=representative_steps[-1],
            step_start=step_start,
            step_end=step_end,
            evidence=evidence,
            metadata={
                "affected_modules": sorted(affected_modules),
                "representative_module": representative_module,
            },
        )

    def _build_activation_collapse_alert(
        self,
        layer_summaries: tuple[LayerSummaryRecord, ...],
        *,
        sequence_id: int,
    ) -> AlertRecord | None:
        thresholds = self._config.thresholds
        activations = [
            summary
            for summary in layer_summaries
            if summary.summary_type == "activation"
        ]
        if not activations:
            return None

        triggered: list[tuple[LayerSummaryRecord, Severity]] = []
        for summary in activations:
            local_severity: Severity | None = None
            if summary.dead_unit_ratio is not None:
                local_severity = self._max_severity(
                    local_severity,
                    self._severity_from_upper_bound(
                        float(summary.dead_unit_ratio),
                        warning=thresholds.activation_collapse_warning_dead_unit_ratio,
                        error=thresholds.activation_collapse_error_dead_unit_ratio,
                        critical=thresholds.activation_collapse_critical_dead_unit_ratio,
                    ),
                )
            if summary.saturation_ratio is not None:
                local_severity = self._max_severity(
                    local_severity,
                    self._severity_from_upper_bound(
                        float(summary.saturation_ratio),
                        warning=thresholds.activation_collapse_warning_saturation_ratio,
                        error=thresholds.activation_collapse_error_saturation_ratio,
                        critical=thresholds.activation_collapse_critical_saturation_ratio,
                    ),
                )
            if summary.std is not None:
                local_severity = self._max_severity(
                    local_severity,
                    self._severity_from_lower_bound(
                        float(summary.std),
                        warning=thresholds.activation_collapse_warning_std,
                        error=thresholds.activation_collapse_error_std,
                        critical=thresholds.activation_collapse_critical_std,
                    ),
                )
            if local_severity is not None:
                triggered.append((summary, local_severity))

        if not triggered:
            return None

        severity = max(
            (alert_severity for _summary, alert_severity in triggered),
            key=lambda item: _SEVERITY_ORDER[item],
        )
        triggered_summaries = [summary for summary, _alert_severity in triggered]
        step_start, step_end = self._layer_step_range(triggered_summaries)
        worst_dead = max(
            (summary for summary in triggered_summaries if summary.dead_unit_ratio is not None),
            key=lambda item: float(item.dead_unit_ratio or 0.0),
            default=None,
        )
        worst_saturation = max(
            (summary for summary in triggered_summaries if summary.saturation_ratio is not None),
            key=lambda item: float(item.saturation_ratio or 0.0),
            default=None,
        )
        lowest_std = min(
            (summary for summary in triggered_summaries if summary.std is not None),
            key=lambda item: float(item.std or 0.0),
            default=None,
        )

        evidence_items: list[EvidenceRecord] = [
            EvidenceRecord(
                kind="aggregate",
                source="activation_summary",
                summary="Modules with activation-collapse signals in the current window.",
                observed_value=len({summary.module_name for summary in triggered_summaries}),
                threshold_value=1,
                step_start=step_start,
                step_end=step_end,
                metadata={
                    "affected_modules": sorted(
                        {summary.module_name for summary in triggered_summaries}
                    ),
                },
            )
        ]
        if worst_dead is not None:
            evidence_items.append(
                EvidenceRecord(
                    kind="layer_summary",
                    source=worst_dead.module_name,
                    summary="Highest dead-unit ratio observed in activation summaries.",
                    observed_value=float(worst_dead.dead_unit_ratio or 0.0),
                    threshold_value=thresholds.activation_collapse_warning_dead_unit_ratio,
                    step=worst_dead.step,
                    metadata={"module_type": worst_dead.module_type},
                )
            )
        if worst_saturation is not None:
            evidence_items.append(
                EvidenceRecord(
                    kind="layer_summary",
                    source=worst_saturation.module_name,
                    summary="Highest activation saturation ratio observed in the window.",
                    observed_value=float(worst_saturation.saturation_ratio or 0.0),
                    threshold_value=thresholds.activation_collapse_warning_saturation_ratio,
                    step=worst_saturation.step,
                    metadata={"module_type": worst_saturation.module_type},
                )
            )
        if lowest_std is not None:
            evidence_items.append(
                EvidenceRecord(
                    kind="layer_summary",
                    source=lowest_std.module_name,
                    summary="Lowest activation standard deviation observed in the window.",
                    observed_value=float(lowest_std.std or 0.0),
                    threshold_value=thresholds.activation_collapse_warning_std,
                    step=lowest_std.step,
                    metadata={"module_type": lowest_std.module_type},
                )
            )

        representative = worst_dead or worst_saturation or lowest_std or triggered_summaries[0]
        return self._make_alert(
            sequence_id=sequence_id,
            rule_id="activation_collapse",
            category="representation",
            severity=severity,
            title="Activation collapse detected",
            summary="Activation statistics indicate dead, saturated, or near-constant representations.",
            fact=(
                f"Collapse signals appeared across {len({summary.module_name for summary in triggered_summaries})}"
                " modules in the probed window."
            ),
            inference=(
                "Representation quality is degrading, which can prevent downstream layers from learning useful features."
            ),
            step=representative.step,
            step_start=step_start,
            step_end=step_end,
            evidence=tuple(evidence_items),
            metadata={
                "affected_modules": sorted(
                    {summary.module_name for summary in triggered_summaries}
                )
            },
        )

    def _build_overfitting_alert(
        self,
        metrics: tuple[MetricRecord, ...],
        *,
        sequence_id: int,
    ) -> AlertRecord | None:
        thresholds = self._config.thresholds
        train_loss = self._select_metric_series(metrics, _TRAIN_LOSS_KEYS)
        val_loss = self._select_metric_series(metrics, _VAL_LOSS_KEYS)
        if min(len(train_loss), len(val_loss)) >= thresholds.overfitting_min_points:
            return self._build_overfitting_from_loss(
                train_loss,
                val_loss,
                sequence_id=sequence_id,
            )

        train_accuracy = self._select_metric_series(metrics, _TRAIN_ACCURACY_KEYS)
        val_accuracy = self._select_metric_series(metrics, _VAL_ACCURACY_KEYS)
        if min(len(train_accuracy), len(val_accuracy)) >= thresholds.overfitting_min_points:
            return self._build_overfitting_from_accuracy(
                train_accuracy,
                val_accuracy,
                sequence_id=sequence_id,
            )
        return None

    def _build_overfitting_from_loss(
        self,
        train_loss: tuple[MetricRecord, ...],
        val_loss: tuple[MetricRecord, ...],
        *,
        sequence_id: int,
    ) -> AlertRecord | None:
        thresholds = self._config.thresholds
        window_size = min(len(train_loss), len(val_loss), thresholds.overfitting_min_points)
        recent_train = train_loss[-window_size:]
        recent_val = val_loss[-window_size:]

        train_delta = recent_train[-1].value - recent_train[0].value
        val_delta = recent_val[-1].value - recent_val[0].value
        latest_gap = recent_val[-1].value - recent_train[-1].value
        if train_delta >= 0.0 or val_delta <= 0.0:
            return None

        severity = self._severity_from_upper_bound(
            latest_gap,
            warning=thresholds.overfitting_warning_gap,
            error=thresholds.overfitting_error_gap,
            critical=thresholds.overfitting_critical_gap,
        )
        if severity is None:
            return None

        step_start, step_end = self._metric_step_range(recent_train + recent_val)
        return self._make_alert(
            sequence_id=sequence_id,
            rule_id="overfitting_onset",
            category="generalization",
            severity=severity,
            title="Overfitting onset detected",
            summary="Validation loss is diverging upward while training loss continues to improve.",
            fact=(
                f"Training loss changed by {train_delta:.4f} while validation loss changed by {val_delta:.4f};"
                f" the latest gap reached {latest_gap:.4f}."
            ),
            inference=(
                "Generalization is degrading even though optimization on the training set still improves."
            ),
            step=recent_val[-1].step,
            step_start=step_start,
            step_end=step_end,
            evidence=(
                EvidenceRecord(
                    kind="metric",
                    source=recent_train[-1].metric_key,
                    summary="Recent training-loss trend over the analysis window.",
                    observed_value={
                        "start": recent_train[0].value,
                        "end": recent_train[-1].value,
                        "delta": train_delta,
                    },
                    step_start=step_start,
                    step_end=step_end,
                ),
                EvidenceRecord(
                    kind="metric",
                    source=recent_val[-1].metric_key,
                    summary="Recent validation-loss trend over the analysis window.",
                    observed_value={
                        "start": recent_val[0].value,
                        "end": recent_val[-1].value,
                        "delta": val_delta,
                    },
                    threshold_value=self._threshold_for_upper_severity(
                        severity,
                        warning=thresholds.overfitting_warning_gap,
                        error=thresholds.overfitting_error_gap,
                        critical=thresholds.overfitting_critical_gap,
                    ),
                    step_start=step_start,
                    step_end=step_end,
                ),
            ),
            metadata={"gap": latest_gap, "signal": "loss"},
        )

    def _build_overfitting_from_accuracy(
        self,
        train_accuracy: tuple[MetricRecord, ...],
        val_accuracy: tuple[MetricRecord, ...],
        *,
        sequence_id: int,
    ) -> AlertRecord | None:
        thresholds = self._config.thresholds
        window_size = min(
            len(train_accuracy),
            len(val_accuracy),
            thresholds.overfitting_min_points,
        )
        recent_train = train_accuracy[-window_size:]
        recent_val = val_accuracy[-window_size:]

        train_delta = recent_train[-1].value - recent_train[0].value
        val_delta = recent_val[-1].value - recent_val[0].value
        latest_gap = recent_train[-1].value - recent_val[-1].value
        if train_delta <= 0.0 or val_delta >= 0.0:
            return None

        severity = self._severity_from_upper_bound(
            latest_gap,
            warning=thresholds.overfitting_warning_gap,
            error=thresholds.overfitting_error_gap,
            critical=thresholds.overfitting_critical_gap,
        )
        if severity is None:
            return None

        step_start, step_end = self._metric_step_range(recent_train + recent_val)
        return self._make_alert(
            sequence_id=sequence_id,
            rule_id="overfitting_onset",
            category="generalization",
            severity=severity,
            title="Overfitting onset detected",
            summary="Training accuracy is improving while validation accuracy regresses.",
            fact=(
                f"Training accuracy changed by {train_delta:.4f} while validation accuracy changed by {val_delta:.4f};"
                f" the latest generalization gap reached {latest_gap:.4f}."
            ),
            inference=(
                "The model is fitting training examples more tightly without carrying those gains to validation data."
            ),
            step=recent_val[-1].step,
            step_start=step_start,
            step_end=step_end,
            evidence=(
                EvidenceRecord(
                    kind="metric",
                    source=recent_train[-1].metric_key,
                    summary="Recent training-accuracy trend over the analysis window.",
                    observed_value={
                        "start": recent_train[0].value,
                        "end": recent_train[-1].value,
                        "delta": train_delta,
                    },
                    step_start=step_start,
                    step_end=step_end,
                ),
                EvidenceRecord(
                    kind="metric",
                    source=recent_val[-1].metric_key,
                    summary="Recent validation-accuracy trend over the analysis window.",
                    observed_value={
                        "start": recent_val[0].value,
                        "end": recent_val[-1].value,
                        "delta": val_delta,
                    },
                    threshold_value=self._threshold_for_upper_severity(
                        severity,
                        warning=thresholds.overfitting_warning_gap,
                        error=thresholds.overfitting_error_gap,
                        critical=thresholds.overfitting_critical_gap,
                    ),
                    step_start=step_start,
                    step_end=step_end,
                ),
            ),
            metadata={"gap": latest_gap, "signal": "accuracy"},
        )

    def _build_runtime_bottleneck_alert(
        self,
        metrics: tuple[MetricRecord, ...],
        *,
        sequence_id: int,
    ) -> AlertRecord | None:
        thresholds = self._config.thresholds
        step_times = self._select_metric_series(metrics, self._config.step_time_metric_keys)
        if len(step_times) < 2:
            return None

        recent_step_times = step_times[-min(5, len(step_times)) :]
        average_step_time = self._mean(record.value for record in recent_step_times)
        jitter_ratio = self._coefficient_of_variation(record.value for record in recent_step_times)
        data_times = self._select_metric_series(metrics, self._config.data_time_metric_keys)
        recent_data_times = data_times[-len(recent_step_times) :] if data_times else ()
        data_fraction = None
        if recent_data_times:
            average_data_time = self._mean(record.value for record in recent_data_times)
            if average_step_time > 0.0:
                data_fraction = average_data_time / average_step_time

        severity = self._max_severity(
            self._severity_from_upper_bound(
                average_step_time,
                warning=thresholds.runtime_warning_step_time_ms,
                error=thresholds.runtime_error_step_time_ms,
                critical=thresholds.runtime_critical_step_time_ms,
            ),
            self._severity_from_upper_bound(
                jitter_ratio,
                warning=thresholds.runtime_warning_jitter_ratio,
                error=thresholds.runtime_error_jitter_ratio,
                critical=thresholds.runtime_critical_jitter_ratio,
            ),
        )
        if data_fraction is not None:
            severity = self._max_severity(
                severity,
                self._severity_from_upper_bound(
                    data_fraction,
                    warning=thresholds.runtime_warning_data_fraction,
                    error=thresholds.runtime_error_data_fraction,
                    critical=thresholds.runtime_critical_data_fraction,
                ),
            )
        if severity is None:
            return None

        step_start, step_end = self._metric_step_range(recent_step_times + recent_data_times)
        representative_step = recent_step_times[-1].step
        evidence_items = [
            EvidenceRecord(
                kind="metric",
                source=recent_step_times[-1].metric_key,
                summary="Rolling average step time across the most recent telemetry window.",
                observed_value=average_step_time,
                threshold_value=self._threshold_for_upper_severity(
                    severity,
                    warning=thresholds.runtime_warning_step_time_ms,
                    error=thresholds.runtime_error_step_time_ms,
                    critical=thresholds.runtime_critical_step_time_ms,
                ),
                step_start=step_start,
                step_end=step_end,
            ),
            EvidenceRecord(
                kind="aggregate",
                source="step_time_jitter",
                summary="Coefficient of variation for recent step times.",
                observed_value=jitter_ratio,
                threshold_value=thresholds.runtime_warning_jitter_ratio,
                step_start=step_start,
                step_end=step_end,
            ),
        ]
        if data_fraction is not None and recent_data_times:
            evidence_items.append(
                EvidenceRecord(
                    kind="metric",
                    source=recent_data_times[-1].metric_key,
                    summary="Average data-loading share of step time across the recent window.",
                    observed_value=data_fraction,
                    threshold_value=thresholds.runtime_warning_data_fraction,
                    step_start=step_start,
                    step_end=step_end,
                )
            )

        return self._make_alert(
            sequence_id=sequence_id,
            rule_id="runtime_bottleneck_risk",
            category="runtime",
            severity=severity,
            title="Runtime bottleneck risk detected",
            summary="Recent runtime telemetry indicates throughput pressure or unstable step latency.",
            fact=(
                f"Average step time reached {average_step_time:.2f} ms"
                f" with a jitter ratio of {jitter_ratio:.2f}."
            ),
            inference=(
                "Training throughput may be constrained by system overhead rather than model quality alone."
            ),
            step=representative_step,
            step_start=step_start,
            step_end=step_end,
            evidence=tuple(evidence_items),
            metadata={
                "average_step_time_ms": average_step_time,
                "data_fraction": data_fraction,
                "jitter_ratio": jitter_ratio,
            },
        )

    def _build_health_scores(
        self,
        alerts: tuple[AlertRecord, ...],
    ) -> tuple[HealthScoreRecord, ...]:
        alerts_by_category: dict[str, list[AlertRecord]] = defaultdict(list)
        for alert in alerts:
            alerts_by_category[alert.category].append(alert)

        scores: list[HealthScoreRecord] = []
        for category in _HEALTH_SCORE_KEYS:
            category_alerts = tuple(alerts_by_category.get(category, ()))
            penalty = sum(_SEVERITY_PENALTY[alert.severity] for alert in category_alerts)
            score_value = max(0.0, 100.0 - penalty)
            step_start, step_end = self._alert_step_range(category_alerts)
            scores.append(
                HealthScoreRecord(
                    id=uuid.uuid4().hex,
                    run_id=self.run_id,
                    sequence_id=len(scores),
                    score_key=category,
                    category=category,
                    score=round(score_value, 2),
                    status=self._score_status(score_value),
                    summary=self._score_summary(category, score_value, category_alerts),
                    timestamp=utc_now(),
                    step_start=step_start,
                    step_end=step_end,
                    evidence=(
                        EvidenceRecord(
                            kind="aggregate",
                            source=f"{category}_alerts",
                            summary="Alert count and severity penalties contributing to this health score.",
                            observed_value=len(category_alerts),
                            threshold_value=0,
                            step_start=step_start,
                            step_end=step_end,
                            metadata={
                                "penalty": penalty,
                                "severities": [alert.severity for alert in category_alerts],
                            },
                        ),
                    ),
                    contributing_alert_ids=tuple(alert.id for alert in category_alerts),
                    metadata={"penalty": penalty},
                )
            )

        overall_score = self._mean(score.score for score in scores) if scores else 100.0
        overall_step_start = min(
            (score.step_start for score in scores if score.step_start is not None),
            default=None,
        )
        overall_step_end = max(
            (score.step_end for score in scores if score.step_end is not None),
            default=None,
        )
        scores.append(
            HealthScoreRecord(
                id=uuid.uuid4().hex,
                run_id=self.run_id,
                sequence_id=len(scores),
                score_key="overall",
                category="overall",
                score=round(overall_score, 2),
                status=self._score_status(overall_score),
                summary=self._overall_score_summary(overall_score, tuple(scores)),
                timestamp=utc_now(),
                step_start=overall_step_start,
                step_end=overall_step_end,
                evidence=tuple(
                    EvidenceRecord(
                        kind="aggregate",
                        source=score.score_key,
                        summary="Category score contributing to the overall run health.",
                        observed_value=score.score,
                        step_start=score.step_start,
                        step_end=score.step_end,
                    )
                    for score in scores
                ),
                contributing_alert_ids=tuple(alert.id for alert in alerts),
                metadata={"category_count": len(_HEALTH_SCORE_KEYS)},
            )
        )
        return tuple(scores)

    def _make_alert(
        self,
        *,
        sequence_id: int,
        rule_id: str,
        category: Literal["optimization", "representation", "generalization", "runtime"],
        severity: Severity,
        title: str,
        summary: str,
        fact: str,
        inference: str,
        step: int | None,
        step_start: int | None,
        step_end: int | None,
        evidence: tuple[EvidenceRecord, ...],
        metadata: dict[str, Any] | None = None,
    ) -> AlertRecord:
        return AlertRecord(
            id=uuid.uuid4().hex,
            run_id=self.run_id,
            sequence_id=sequence_id,
            rule_id=rule_id,
            category=category,
            severity=severity,
            title=title,
            summary=summary,
            fact=fact,
            inference=inference,
            timestamp=utc_now(),
            step=step,
            step_start=step_start,
            step_end=step_end,
            evidence=evidence,
            metadata=metadata,
        )

    @staticmethod
    def _resolve_session_dependencies(session: Any) -> tuple[RunContext, LocalSessionStore]:
        if isinstance(session, RunContext):
            return session, LocalSessionStore(session.storage_dir)

        context = getattr(session, "context", None)
        store = getattr(session, "_store", None)
        if isinstance(context, RunContext) and isinstance(store, LocalSessionStore):
            return context, store

        raise TypeError(
            "AnalyzerEngine expects a TrainingSession or RunContext created by the Quark SDK."
        )

    @staticmethod
    def _severity_from_upper_bound(
        value: float,
        *,
        warning: float,
        error: float,
        critical: float,
    ) -> Severity | None:
        if value >= critical:
            return "critical"
        if value >= error:
            return "error"
        if value >= warning:
            return "warning"
        return None

    @staticmethod
    def _severity_from_lower_bound(
        value: float,
        *,
        warning: float,
        error: float,
        critical: float,
    ) -> Severity | None:
        if value <= critical:
            return "critical"
        if value <= error:
            return "error"
        if value <= warning:
            return "warning"
        return None

    @staticmethod
    def _threshold_for_upper_severity(
        severity: Severity,
        *,
        warning: float,
        error: float,
        critical: float,
    ) -> float:
        if severity == "critical":
            return critical
        if severity == "error":
            return error
        return warning

    @staticmethod
    def _max_severity(
        left: Severity | None,
        right: Severity | None,
    ) -> Severity | None:
        if left is None:
            return right
        if right is None:
            return left
        return left if _SEVERITY_ORDER[left] >= _SEVERITY_ORDER[right] else right

    @staticmethod
    def _select_metric_series(
        metrics: tuple[MetricRecord, ...],
        keys: tuple[str, ...],
    ) -> tuple[MetricRecord, ...]:
        for key in keys:
            series = tuple(metric for metric in metrics if metric.metric_key == key)
            if series:
                return series
        return ()

    @staticmethod
    def _mean(values: Iterable[float]) -> float:
        materialized = tuple(float(value) for value in values)
        if not materialized:
            return 0.0
        return sum(materialized) / len(materialized)

    @staticmethod
    def _coefficient_of_variation(values: Iterable[float]) -> float:
        materialized = tuple(float(value) for value in values)
        if len(materialized) < 2:
            return 0.0
        mean_value = AnalyzerEngine._mean(materialized)
        if mean_value <= 0.0:
            return 0.0
        variance = sum((value - mean_value) ** 2 for value in materialized) / len(materialized)
        return (variance ** 0.5) / mean_value

    @staticmethod
    def _layer_step_range(
        summaries: Iterable[LayerSummaryRecord],
    ) -> tuple[int | None, int | None]:
        steps = sorted(summary.step for summary in summaries)
        if not steps:
            return None, None
        return steps[0], steps[-1]

    @staticmethod
    def _metric_step_range(
        metrics: Iterable[MetricRecord],
    ) -> tuple[int | None, int | None]:
        steps = sorted(metric.step for metric in metrics if metric.step is not None)
        if steps:
            return steps[0], steps[-1]
        return None, None

    @staticmethod
    def _alert_step_range(
        alerts: Iterable[AlertRecord],
    ) -> tuple[int | None, int | None]:
        step_starts = [
            step
            for alert in alerts
            for step in (alert.step_start, alert.step)
            if step is not None
        ]
        step_ends = [
            step
            for alert in alerts
            for step in (alert.step_end, alert.step_start, alert.step)
            if step is not None
        ]
        if not step_starts or not step_ends:
            return None, None
        return min(step_starts), max(step_ends)

    @staticmethod
    def _score_status(score: float) -> str:
        if score >= 90.0:
            return "healthy"
        if score >= 75.0:
            return "watch"
        if score >= 50.0:
            return "degraded"
        return "critical"

    @staticmethod
    def _score_summary(
        category: str,
        score: float,
        alerts: tuple[AlertRecord, ...],
    ) -> str:
        if not alerts:
            return f"{category.capitalize()} health is stable with no active diagnostic alerts."
        return (
            f"{category.capitalize()} health scored {score:.1f}/100 from {len(alerts)} alert"
            f"{'' if len(alerts) == 1 else 's'}."
        )

    @staticmethod
    def _overall_score_summary(
        score: float,
        category_scores: tuple[HealthScoreRecord, ...],
    ) -> str:
        degraded_categories = [
            item.score_key for item in category_scores if item.status in {"degraded", "critical"}
        ]
        if not degraded_categories:
            return f"Overall run health scored {score:.1f}/100 with no degraded categories."
        return (
            f"Overall run health scored {score:.1f}/100; degraded categories:"
            f" {', '.join(degraded_categories)}."
        )
