"""Prometheus-Σ §12 trigger-probe — ticket #000037 Phase 0 evidence-gather.

Phase 1 of #000037 (the recursive falsification controller) gates on
one of four §12 triggers firing. Triggers 1-3 are measurable from
already-committed state; trigger 4 is operator decision. This script
walks every shard's ``audit_events``, ``capital_ledger``, and
``fork_score_branches`` (#000012 Phase 1c) tables, computes the §12
quantities, and emits a markdown report.

Pure measurement. No mutation. No LLM call. No schema change. The
report is the deliverable; fox uses it to decide go/no-go on Phase 1.

The probe is defensive about data-availability: it explicitly reports
"no data" when a trigger structurally cannot fire yet (e.g. trigger 1
when no branch sets have been persisted to ``fork_score_branches``)
rather than synthesising a false signal.
"""

from __future__ import annotations

import argparse
import json
import math
import sqlite3
import sys
from datetime import datetime, timezone
from pathlib import Path

# §12 thresholds, transcribed verbatim from the ticket.
DIVERGENCE_N_MIN = 30
DIVERGENCE_VARIANCE_RATIO_FLOOR = 0.5
DIVERGENCE_ABS_STDDEV_FLOOR = 0.10
WITNESS_COST_SHARE_FLOOR = 0.30
DIVERGENCE_EPSILON = 1e-6  # max(mean, ε) guard against div-by-zero
BRANCH_DENSITY_FLOOR = 4  # §12 trigger 1: ≥4 candidate branches per checkpoint


def _read_witness_events(shards: list[Path]) -> list[dict]:
    """Pull every ``providence_canonical_witness`` body across shards."""
    bodies: list[dict] = []
    for sp in shards:
        try:
            conn = sqlite3.connect(sp)
        except sqlite3.OperationalError:
            continue
        try:
            cur = conn.execute(
                "SELECT body, ts FROM audit_events "
                "WHERE event_type = 'providence_canonical_witness' "
                "ORDER BY seq"
            )
            for row in cur:
                try:
                    body = json.loads(row[0])
                except (TypeError, json.JSONDecodeError):
                    continue
                body["__ts"] = row[1]
                body["__shard"] = sp.name
                bodies.append(body)
        except sqlite3.OperationalError:
            # Pre-#000028 shard with no audit_events table or schema gap;
            # not a probe-failure mode.
            pass
        finally:
            conn.close()
    return bodies


def _read_capital_ledger(shards: list[Path]) -> list[dict]:
    """Aggregate every ``capital_ledger`` row across shards."""
    rows: list[dict] = []
    for sp in shards:
        try:
            conn = sqlite3.connect(sp)
            conn.row_factory = sqlite3.Row
        except sqlite3.OperationalError:
            continue
        try:
            cur = conn.execute(
                "SELECT op_type, living, material, financial, "
                "       intellectual, experiential, social, cultural, "
                "       spiritual, recorded_at "
                "FROM capital_ledger"
            )
            for r in cur:
                d = dict(r)
                d["__shard"] = sp.name
                rows.append(d)
        except sqlite3.OperationalError:
            pass
        finally:
            conn.close()
    return rows


# -------------------------------------------------------------- triggers


def trigger_1_branch_density(shards: list[Path]) -> dict:
    """Trigger 1 — branch density: ≥4 candidate branches per checkpoint.

    Reads the ``fork_score_branches`` sibling table (#000012 Phase 1c)
    via :func:`arborist.substrate.fork_score.branch_set_density`. Each
    distinct ``branch_set_id`` is a checkpoint; the trigger fires when
    the most-recently-recorded checkpoint carries ≥4 distinct branches.
    The report also surfaces how many of *all* recorded checkpoints
    clear the floor so §12's "regularly" qualifier stays visible.

    Density is summed across shards per ``branch_set_id`` (each
    ``arborist substrate score --persist-shard`` invocation writes one
    shard; a checkpoint normally lives in exactly one). When the table
    is absent across every shard — the common case, since ForkScore
    Phase 1a is single-validator and ``arborist substrate score`` only
    persists branch sets when ``--branch-set`` is passed — the trigger
    reports "no data" rather than synthesising a false signal.
    """
    from arborist.substrate.fork_score import branch_set_density

    density: dict[str, int] = {}
    last_ts: dict[str, int] = {}
    table_present = False
    for sp in shards:
        try:
            conn = sqlite3.connect(sp)
        except sqlite3.OperationalError:
            continue
        try:
            checkpoints = conn.execute(
                "SELECT branch_set_id, MAX(recorded_at) "
                "FROM fork_score_branches GROUP BY branch_set_id"
            ).fetchall()
        except sqlite3.OperationalError:
            # Pre-#000012-Phase-1c shard, or one that has never had a
            # branch set persisted. Not a probe-failure mode.
            conn.close()
            continue
        table_present = True
        for sid, ts in checkpoints:
            density[sid] = density.get(sid, 0) + branch_set_density(conn, sid)
            last_ts[sid] = max(last_ts.get(sid, 0), ts or 0)
        conn.close()

    if not table_present:
        return {
            "trigger": 1,
            "name": "branch density (≥4 branches/checkpoint)",
            "fires": False,
            "reason": (
                "no fork_score_branches table across shards; ForkScore is "
                "single-validator (Phase 1a) and `arborist substrate score` "
                "only persists branch sets when `--branch-set` is passed. "
                "Multi-branch fork-score persistence is the prerequisite — "
                "see #000012 Phase 1c."
            ),
            "data_available": False,
            "n_checkpoints": 0,
        }
    if not density:
        return {
            "trigger": 1,
            "name": "branch density (≥4 branches/checkpoint)",
            "fires": False,
            "reason": (
                "fork_score_branches table present but empty across shards; "
                "no branch sets persisted yet"
            ),
            "data_available": True,
            "n_checkpoints": 0,
        }

    latest_sid = max(density, key=lambda s: last_ts[s])
    latest_density = density[latest_sid]
    max_density = max(density.values())
    n_clear = sum(1 for d in density.values() if d >= BRANCH_DENSITY_FLOOR)
    fires = latest_density >= BRANCH_DENSITY_FLOOR
    if fires:
        reason = (
            f"latest checkpoint `{latest_sid}` carries {latest_density} "
            f"branches ≥ {BRANCH_DENSITY_FLOOR}; "
            f"{n_clear}/{len(density)} recorded checkpoints clear the floor"
        )
    else:
        reason = (
            f"latest checkpoint `{latest_sid}` carries {latest_density} "
            f"branch(es) < {BRANCH_DENSITY_FLOOR}; "
            f"{n_clear}/{len(density)} recorded checkpoints clear the floor "
            f"(max density seen: {max_density})"
        )
    return {
        "trigger": 1,
        "name": "branch density (≥4 branches/checkpoint)",
        "fires": fires,
        "reason": reason,
        "data_available": True,
        "n_checkpoints": len(density),
        "latest_branch_set_id": latest_sid,
        "latest_density": latest_density,
        "max_density": max_density,
        "n_checkpoints_clearing_floor": n_clear,
    }


def _divergence_score(agreement_label: str | None) -> float | None:
    """Map a #000028 §1.2 agreement label to a binary LLM-divergence
    score for trigger 2.

    Returns:
        - 0.0 when the LLM converged with the kernel
        - 1.0 when the LLM diverged from the kernel
        - None when the LLM modality was absent / unparseable, so the
          event tells us nothing about LLM divergence.

    The N_min=30 sample population is the set of events that produced
    a 0/1 score. KERNEL-CACHE-AGREE excludes LLM (unparseable) so it
    contributes no signal to trigger 2; CACHE-DRIFT means K=L (LLM
    agreed with kernel) so it counts as 0.0.
    """
    if not agreement_label:
        return None
    label = agreement_label.upper()
    LLM_AGREE = {
        "STRICT-WITNESSED",   # K=C=L
        "KERNEL-LLM-AGREE",   # no cache; K=L
        "CACHE-DRIFT",        # K=L; cache stale (counts as agree for LLM)
    }
    LLM_DIVERGED = {
        "LLM-DIVERGED",            # K=C; L≠K
        "KERNEL-LLM-DIVERGED",     # no cache; L≠K
        "LLM-AND-CACHE-DIVERGED",  # only K matches itself
    }
    LLM_ABSENT = {
        "KERNEL-CACHE-AGREE",      # LLM unparseable; no signal
    }
    if label in LLM_AGREE:
        return 0.0
    if label in LLM_DIVERGED:
        return 1.0
    if label in LLM_ABSENT:
        return None
    # Unknown label — substring fallback so renames don't silently
    # exclude data. Errs on the side of inclusion.
    if "DIVERGED" in label or "DRIFT_LLM" in label:
        return 1.0
    if "AGREE" in label or "WITNESSED" in label:
        return 0.0
    return None


def _stddev(samples: list[float]) -> float:
    n = len(samples)
    if n < 2:
        return 0.0
    mean = sum(samples) / n
    sq = sum((s - mean) ** 2 for s in samples)
    return math.sqrt(sq / (n - 1))


def trigger_2_divergence_variance(events: list[dict]) -> dict:
    """Trigger 2 — divergence variance: N≥30, ratio>0.5 or abs>0.10.

    Concrete signal from §12:
        sample_count >= N_min
        AND (stddev/max(mean, ε) > 0.5  OR  absolute stddev > 0.10)
    """
    scores = [
        s for s in (_divergence_score(e.get("agreement_label")) for e in events)
        if s is not None
    ]
    n = len(scores)
    if n == 0:
        return {
            "trigger": 2,
            "name": "divergence variance",
            "fires": False,
            "reason": (
                "no providence_canonical_witness events with kernel-vs-LLM "
                "outcomes found; #000028 not exercised on this corpus yet"
            ),
            "data_available": False,
            "n": 0,
        }
    mean = sum(scores) / n
    sd = _stddev(scores)
    ratio = sd / max(mean, DIVERGENCE_EPSILON)
    abs_fired = sd > DIVERGENCE_ABS_STDDEV_FLOOR
    ratio_fired = ratio > DIVERGENCE_VARIANCE_RATIO_FLOOR
    n_ok = n >= DIVERGENCE_N_MIN
    fires = n_ok and (abs_fired or ratio_fired)
    if not n_ok:
        reason = f"only {n} samples; N_min = {DIVERGENCE_N_MIN}"
    elif fires:
        which = []
        if ratio_fired:
            which.append(f"ratio {ratio:.3f} > {DIVERGENCE_VARIANCE_RATIO_FLOOR}")
        if abs_fired:
            which.append(f"abs σ {sd:.3f} > {DIVERGENCE_ABS_STDDEV_FLOOR}")
        reason = "; ".join(which)
    else:
        reason = (
            f"N={n} ≥ {DIVERGENCE_N_MIN}, but "
            f"ratio {ratio:.3f} ≤ {DIVERGENCE_VARIANCE_RATIO_FLOOR} "
            f"and abs σ {sd:.3f} ≤ {DIVERGENCE_ABS_STDDEV_FLOOR}"
        )
    return {
        "trigger": 2,
        "name": "divergence variance",
        "fires": fires,
        "reason": reason,
        "data_available": True,
        "n": n,
        "mean": round(mean, 4),
        "stddev": round(sd, 4),
        "ratio": round(ratio, 4),
    }


def trigger_3_witness_cost_share(rows: list[dict]) -> dict:
    """Trigger 3 — witness cost share: witness_compute / total_compute > 0.30.

    "Compute" is read as the ``material`` column (kWh proxy), which is
    the closest analogue to "compute" in the 8-form ledger. The ticket
    leaves the form vague; surfacing both ``material`` and ``financial``
    in the report so fox can choose if the threshold should be against
    a different form.

    Per §12 the trigger requires #000026 baseline to exist (it does;
    bench/results/real-shard-baseline.md, 2026-05-08). The ledger
    here is whatever has been recorded to date — not a controlled
    workload — so the ratio is informational pending a controlled
    sweep.
    """
    if not rows:
        return {
            "trigger": 3,
            "name": "witness cost share (material > 0.30)",
            "fires": False,
            "reason": "capital_ledger empty across all shards",
            "data_available": False,
            "witness_material": 0.0,
            "total_material": 0.0,
        }
    witness_material = sum(
        r["material"] or 0.0 for r in rows if r["op_type"] == "canonical_witness"
    )
    total_material = sum(r["material"] or 0.0 for r in rows)
    witness_financial = sum(
        r["financial"] or 0.0 for r in rows if r["op_type"] == "canonical_witness"
    )
    total_financial = sum(r["financial"] or 0.0 for r in rows)
    if total_material == 0.0:
        return {
            "trigger": 3,
            "name": "witness cost share (material > 0.30)",
            "fires": False,
            "reason": "total compute denominator is zero (per §12: do not divide)",
            "data_available": False,
            "witness_material": witness_material,
            "total_material": 0.0,
        }
    ratio = witness_material / total_material
    fires = ratio > WITNESS_COST_SHARE_FLOOR
    return {
        "trigger": 3,
        "name": "witness cost share (material > 0.30)",
        "fires": fires,
        "reason": (
            f"witness material / total material = "
            f"{witness_material:.6f} / {total_material:.6f} = {ratio:.3f}"
        ),
        "data_available": True,
        "witness_material": round(witness_material, 6),
        "total_material": round(total_material, 6),
        "witness_financial": round(witness_financial, 6),
        "total_financial": round(total_financial, 6),
        "ratio": round(ratio, 4),
        "n_witness_rows": sum(
            1 for r in rows if r["op_type"] == "canonical_witness"
        ),
        "n_total_rows": len(rows),
        "caveat": (
            "ledger reflects ad-hoc activity, not a controlled #000026 sweep — "
            "ratio is informational. Re-run after a #000026 baseline sweep "
            "for a workload-anchored answer."
        ),
    }


def trigger_4_operator_need() -> dict:
    return {
        "trigger": 4,
        "name": "operator mission need",
        "fires": None,
        "reason": "operator decision; not measurable from committed state",
        "data_available": False,
    }


# -------------------------------------------------------------- report


def render_markdown(
    shards_dir: Path,
    shards: list[Path],
    triggers: list[dict],
    events: list[dict],
    rows: list[dict],
) -> str:
    now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    out = []
    out.append("# Prometheus-Σ §12 trigger probe")
    out.append("")
    out.append(f"**Date:** {now}")
    out.append(f"**Shards directory:** `{shards_dir}`")
    out.append(f"**Shards walked:** {len(shards)} "
               f"({', '.join(s.name for s in shards)})")
    out.append("")
    out.append(
        "Phase 1 of ticket #000037 gates on one §12 trigger firing. "
        "This probe walks `fork_score_branches` + `audit_events` + "
        "`capital_ledger` and reports the empirical state of triggers "
        "1–3. Trigger 4 is operator-stated."
    )
    out.append("")
    out.append("## Verdict")
    out.append("")
    out.append("| # | Trigger | Fires? | Reason |")
    out.append("|---|---------|--------|--------|")
    for t in triggers:
        fires = t["fires"]
        if fires is True:
            mark = "**YES**"
        elif fires is False:
            mark = "no"
        else:
            mark = "n/a"
        out.append(f"| {t['trigger']} | {t['name']} | {mark} | {t['reason']} |")
    out.append("")
    any_fires = any(t["fires"] is True for t in triggers)
    if any_fires:
        out.append("**Phase 1 trigger has fired.** Prometheus-Σ Phase 1 may proceed.")
    else:
        out.append(
            "**No measurable §12 trigger has fired yet.** Phase 1 of #000037 "
            "remains paper-only; trigger 4 (operator-stated need) bypasses the "
            "measured-pressure gates if fox has explicit mission need."
        )
    out.append("")
    out.append("## Trigger 1 — branch density")
    out.append("")
    t1 = triggers[0]
    out.append(f"- Data available: {t1['data_available']}")
    if t1.get("n_checkpoints"):
        out.append(f"- Recorded checkpoints: {t1['n_checkpoints']}")
        out.append(
            f"- Latest checkpoint: `{t1['latest_branch_set_id']}` "
            f"→ {t1['latest_density']} branches "
            f"(floor {BRANCH_DENSITY_FLOOR})"
        )
        out.append(f"- Max density across checkpoints: {t1['max_density']}")
        out.append(
            f"- Checkpoints clearing the floor: "
            f"{t1['n_checkpoints_clearing_floor']} / {t1['n_checkpoints']}"
        )
    out.append(f"- {t1['reason']}")
    out.append("")
    out.append("## Trigger 2 — divergence variance")
    out.append("")
    t2 = triggers[1]
    if t2.get("data_available"):
        out.append(f"- Sample count (N): {t2['n']} "
                   f"(N_min = {DIVERGENCE_N_MIN})")
        out.append(f"- Mean divergence rate: {t2['mean']}")
        out.append(f"- Stddev: {t2['stddev']}")
        out.append(f"- Ratio (σ/mean): {t2['ratio']}")
        out.append(f"- Threshold ratio: > {DIVERGENCE_VARIANCE_RATIO_FLOOR} "
                   f"OR absolute σ > {DIVERGENCE_ABS_STDDEV_FLOOR}")
    out.append(f"- {t2['reason']}")
    if events:
        from collections import Counter
        agree_counts = Counter(e.get("agreement_label", "?") for e in events)
        out.append("")
        out.append("Agreement-label distribution across all shards:")
        out.append("")
        out.append("| label | count |")
        out.append("|-------|-------|")
        for k, v in sorted(agree_counts.items(), key=lambda kv: (-kv[1], kv[0])):
            out.append(f"| `{k}` | {v} |")
    out.append("")
    out.append("## Trigger 3 — witness cost share")
    out.append("")
    t3 = triggers[2]
    if t3.get("data_available"):
        out.append(f"- Witness material (kWh proxy): "
                   f"{t3['witness_material']}")
        out.append(f"- Total material: {t3['total_material']}")
        out.append(f"- Ratio: {t3['ratio']} "
                   f"(threshold > {WITNESS_COST_SHARE_FLOOR})")
        out.append(f"- Witness financial: {t3['witness_financial']}")
        out.append(f"- Total financial: {t3['total_financial']}")
        out.append(f"- Witness rows: {t3['n_witness_rows']}")
        out.append(f"- Total ledger rows: {t3['n_total_rows']}")
    out.append(f"- {t3['reason']}")
    if t3.get("caveat"):
        out.append("")
        out.append(f"**Caveat:** {t3['caveat']}")
    out.append("")
    out.append("## Trigger 4 — operator mission need")
    out.append("")
    out.append("- Operator-stated; not measurable from committed state.")
    out.append("- Bypasses §12 measured-pressure gates per ticket §12.")
    out.append("")
    return "\n".join(out)


# -------------------------------------------------------------- main


def main(argv: list[str] | None = None) -> int:
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument(
        "--shards-dir",
        type=Path,
        default=Path.home() / ".arborist" / "shards",
        help="Directory of *.db shard files (default: ~/.arborist/shards)",
    )
    p.add_argument(
        "--out",
        type=Path,
        default=None,
        help="Write markdown report to this path. Default: stdout.",
    )
    args = p.parse_args(argv)

    if not args.shards_dir.exists():
        print(f"shards-dir not found: {args.shards_dir}", file=sys.stderr)
        return 2

    shards = sorted(args.shards_dir.glob("*.db"))
    if not shards:
        print(f"no .db files in {args.shards_dir}", file=sys.stderr)
        return 2

    events = _read_witness_events(shards)
    rows = _read_capital_ledger(shards)

    triggers = [
        trigger_1_branch_density(shards),
        trigger_2_divergence_variance(events),
        trigger_3_witness_cost_share(rows),
        trigger_4_operator_need(),
    ]

    md = render_markdown(args.shards_dir, shards, triggers, events, rows)
    if args.out:
        args.out.parent.mkdir(parents=True, exist_ok=True)
        args.out.write_text(md, encoding="utf-8")
        print(f"wrote {args.out}")
    else:
        print(md)
    return 0


if __name__ == "__main__":
    sys.exit(main())
