"""DriftDetector — Phase 5: catch active reflexes that regress.

Runs daily via the WorkQueue handler ``evolution_drift_scan``. For every
``status='active'`` reflex, walks the rolling window of recent runs and
applies three drift triggers:

  1. Rising zero-effect rate (≥ configured threshold)
  2. Utility EMA drop (≥ configured absolute drop)
  3. Safety regression (recent safety EMA < configured floor)

Any triggered reflex transitions to ``status='quarantined'`` and emits
a high-importance ``reflex_quarantined`` activity event. A quarantined
reflex that stays there for ``quarantine_after_days`` (default 7) gets
auto-retired on the next drift pass.

Every drift pass writes a readable ``reports/drift-{YYYY-MM-DD}.md``
artifact listing every active reflex's current window + verdict.

Phase 5 is deliberately simple — no p-values, no sequential testing, no
Bayesian change-point detection. The rolling-window heuristic catches
the failure modes we care about (zero-effect-storm, utility collapse,
safety regression) cheaply + deterministically. Fancy statistics can
come later if the heuristic produces too many false positives or misses
real drift.
"""

from __future__ import annotations

import logging
import time
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING, Any

from agntspace.evolution.storage import drift_report_path

if TYPE_CHECKING:
    from agntspace.evolution.ledger import EvolutionLedger

log = logging.getLogger(__name__)


class DriftDetector:
    """Audit active reflexes for regression; quarantine + retire on drift."""

    def __init__(
        self,
        *,
        ledger: EvolutionLedger,
        config: dict[str, Any],
        writer: Any | None = None,
        activity_logger: Any | None = None,
    ) -> None:
        self._ledger = ledger
        self._config = config or {}
        self._writer = writer
        self._activity = activity_logger

    async def audit(self) -> dict[str, Any]:
        """Scan every active reflex for drift. Returns audit summary."""
        drift_cfg = self._config.get("drift", {}) if isinstance(self._config, dict) else {}
        zero_threshold = float(drift_cfg.get("zero_effect_rate_threshold", 0.20))
        window = int(drift_cfg.get("utility_window", 20))
        drop_min = float(drift_cfg.get("utility_drop_min", 0.08))
        safety_floor = float(
            self._config.get("promotion", {}).get("safety_ema_floor", 0.95),
        )
        # Evolution v2 #1 — use the mining success threshold as the "what
        # counts as a healthy utility level" baseline. Same value the
        # posterior update uses for Bernoulli classification — keeps ONE
        # notion of success across miner + posterior + drift detector.
        success_threshold = float(
            self._config.get("mining", {}).get("success_utility_threshold", 0.75),
        )
        quarantine_days = int(drift_cfg.get("quarantine_after_days", 7))

        active = await self._ledger.list_reflexes(kind="reflex", status="active")
        quarantined = await self._ledger.list_reflexes(kind="reflex", status="quarantined")

        findings: list[dict[str, Any]] = []
        quarantined_count = 0
        retired_count = 0

        # ── Audit active reflexes ──
        for r in active:
            posterior = await self._ledger.get_posterior(r.reflex_id)
            ze_rate = await self._ledger.recent_zero_effect_rate(
                r.reflex_id, window=window,
            )
            safety_ema = await self._ledger.recent_safety_ema(
                r.reflex_id, window=window,
            )

            # Utility drop: how far has the running utility fallen below
            # the success bar? After Evolution v2 #1 (conjugate Beta-
            # Bernoulli update, 2026-04-24), posterior.alpha/(α+β) is the
            # success RATE under the Bernoulli model — NOT the utility
            # magnitude. Comparing utility_ema against posterior_mean mixed
            # two quantities that happened to track each other under the
            # old linearized update; that coincidence is gone now.
            #
            # New honest signal: compare utility_ema (continuous magnitude)
            # against the SUCCESS THRESHOLD — the same cutoff the posterior
            # update uses to classify runs as success vs. failure. When
            # utility_ema falls well below the success bar, recent runs
            # have dragged the continuous signal down even if the Beta
            # posterior still reports a healthy success rate from older
            # successes. That's a reflex drifting toward worse outcomes.
            posterior_mean_val = (
                posterior.alpha / (posterior.alpha + posterior.beta_param)
                if (posterior.alpha + posterior.beta_param) > 0 else 0.5
            )
            utility_drop = max(0.0, success_threshold - posterior.utility_ema)

            triggers: list[str] = []
            if ze_rate >= zero_threshold:
                triggers.append(
                    f"zero_effect_rate={ze_rate:.3f} >= {zero_threshold}",
                )
            if utility_drop >= drop_min:
                triggers.append(
                    f"utility_drop={utility_drop:.3f} >= {drop_min} "
                    f"(posterior_mean={posterior_mean_val:.3f}, ema={posterior.utility_ema:.3f})",
                )
            if safety_ema < safety_floor:
                triggers.append(
                    f"safety_regression: ema={safety_ema:.3f} < {safety_floor}",
                )

            verdict = "ok" if not triggers else "drift"
            findings.append({
                "reflex_id": r.reflex_id,
                "name": r.name,
                "status": r.status,
                "verdict": verdict,
                "triggers": triggers,
                "window_size": window,
                "zero_effect_rate": round(ze_rate, 4),
                "safety_ema": round(safety_ema, 4),
                "posterior_mean": round(posterior_mean_val, 4),
                "utility_ema": round(posterior.utility_ema, 4),
                "n_runs": posterior.n_runs,
            })

            if verdict == "drift":
                await self._ledger.update_reflex_status(
                    r.reflex_id, "quarantined",
                    last_validated_at=datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
                )
                quarantined_count += 1
                self._emit_activity(
                    "reflex_quarantined",
                    f"Reflex {r.name} quarantined due to drift: {', '.join(triggers)}",
                    {
                        "reflex_id": r.reflex_id,
                        "triggers": triggers,
                        "zero_effect_rate": ze_rate,
                        "utility_drop": utility_drop,
                        "safety_ema": safety_ema,
                    },
                    importance="high",
                )

        # ── Retire quarantined reflexes that have stayed quarantined too long ──
        retire_cutoff = datetime.now(UTC) - timedelta(days=quarantine_days)
        retire_cutoff_iso = retire_cutoff.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
        for r in quarantined:
            if r.last_validated_at and r.last_validated_at < retire_cutoff_iso:
                await self._ledger.update_reflex_status(r.reflex_id, "retired")
                retired_count += 1
                self._emit_activity(
                    "reflex_retired",
                    f"Reflex {r.name} retired after {quarantine_days}d in quarantine",
                    {
                        "reflex_id": r.reflex_id,
                        "quarantined_since": r.last_validated_at,
                    },
                    importance="medium",
                )

        # ── Write the readable drift report ──
        today_iso = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
        report_path = drift_report_path(today_iso)
        if self._writer is not None:
            try:
                body = _render_drift_report(
                    findings, quarantined_count=quarantined_count,
                    retired_count=retired_count,
                    active_count=len(active),
                    window=window,
                    cfg=drift_cfg,
                )
                try:
                    await self._writer.write(
                        report_path, body,
                        frontmatter={
                            "title": f"Drift audit — {today_iso[:10]}",
                            "description": (
                                f"Drift audit result: {quarantined_count} "
                                f"quarantined, {retired_count} retired "
                                f"(out of {len(active)} active)"
                            ),
                            "date": time.strftime("%Y-%m-%d"),
                            "author": "agent",
                            "agent": "evolution",
                        },
                        trust_reason="evolution_persist",
                    )
                except TypeError:
                    await self._writer.write(report_path, body)
            except Exception:
                log.debug("drift report write failed", exc_info=True)

        return {
            "status": "done",
            "active_scanned": len(active),
            "quarantined_scanned": len(quarantined),
            "quarantined_new": quarantined_count,
            "retired": retired_count,
            "findings": findings,
            "report_path": report_path,
        }

    def _emit_activity(
        self, action: str, summary: str, details: dict, importance: str = "medium",
    ) -> None:
        if self._activity is None:
            return
        try:
            self._activity.log(
                "evolution", action, summary, details, importance=importance,
            )
        except Exception:
            log.debug("drift activity log failed", exc_info=True)


def _render_drift_report(
    findings: list[dict[str, Any]],
    *,
    quarantined_count: int,
    retired_count: int,
    active_count: int,
    window: int,
    cfg: dict[str, Any],
) -> str:
    """Render the daily drift audit artifact."""
    drift_items = [f for f in findings if f["verdict"] == "drift"]
    ok_items = [f for f in findings if f["verdict"] == "ok"]

    def row(f):
        triggers = "; ".join(f["triggers"]) if f["triggers"] else "(none)"
        return (
            f"| `{f['reflex_id']}` | `{f['name']}` | {f['verdict']} | "
            f"ze={f['zero_effect_rate']} | safety={f['safety_ema']} | "
            f"ema={f['utility_ema']} | n={f['n_runs']} | {triggers} |"
        )

    lines = [
        "# Drift audit report",
        "",
        f"- **Active reflexes scanned**: {active_count}",
        f"- **Quarantined this pass**: {quarantined_count}",
        f"- **Retired this pass**: {retired_count}",
        f"- **Window size**: {window} runs",
        "",
        "## Config",
        "",
        "```yaml",
        f"zero_effect_rate_threshold: {cfg.get('zero_effect_rate_threshold', 0.20)}",
        f"utility_drop_min: {cfg.get('utility_drop_min', 0.08)}",
        f"quarantine_after_days: {cfg.get('quarantine_after_days', 7)}",
        "```",
        "",
        "## Reflexes in drift",
        "",
    ]
    if drift_items:
        lines.extend([
            "| reflex_id | name | verdict | ze_rate | safety | utility_ema | n | triggers |",
            "| --- | --- | --- | --- | --- | --- | --- | --- |",
            *[row(f) for f in drift_items],
            "",
        ])
    else:
        lines.extend(["_None — all active reflexes within bounds._", ""])

    lines.extend([
        "## Healthy reflexes",
        "",
    ])
    if ok_items:
        lines.extend([
            "| reflex_id | name | ze_rate | safety | utility_ema | n |",
            "| --- | --- | --- | --- | --- | --- |",
            *[
                f"| `{f['reflex_id']}` | `{f['name']}` | "
                f"{f['zero_effect_rate']} | {f['safety_ema']} | "
                f"{f['utility_ema']} | {f['n_runs']} |"
                for f in ok_items
            ],
            "",
        ])
    else:
        lines.extend(["_No active reflexes in scope._", ""])

    lines.extend([
        "---",
        "_Auto-generated by the drift detector. "
        "Quarantined reflexes auto-retire after the quarantine window "
        "if they don't recover._",
    ])
    return "\n".join(lines) + "\n"
