"""
G-3/5/7 // DOES G-2's AUTO-HALT RULE WORK? Control-chart properties of the decay
detector that Task D inherits.
Mission 25-001. Ref: t013 c.0029 s3 (G-2), t012 c.0027 s7 TR-06.

G-2 specified, in one sentence: "rolling 200-bet CLV chart with pre-registered
CUSUM/EWMA limits and an auto-halt on that market family when the 90% CI of mean
CLV goes below zero."

Those are TWO DIFFERENT INSTRUMENTS with incompatible error properties. A
CUSUM/EWMA with published limits is a sequential test whose in-control average run
length you can set before deploying. A 90% CI recomputed on an OVERLAPPING rolling
window and inspected after every bet is not a sequential test: its false-halt rate
is a boundary-crossing probability nobody has bounded. I implement the halt, so I
need to know which rule I am audited against. Measured, not argued.

CONVENTIONS (stated once, applied everywhere):
  - One replicate = one market family, N_BETS observations of per-bet CLV.
  - Monitoring starts at WARMUP = 200 bets (the rolling window must fill first).
  - Detection delay is measured FROM THE TRUE CHANGE POINT, never from monitoring
    start, and is negative-censored at the change point (a rule cannot halt before
    the decay it is detecting).
  - "halted" is the fraction of replicates that halted within the horizon.
Synthetic. Pure numpy. No network, no writes, no corpus.
"""

import math
import numpy as np

SIGMA = 0.025        # sigma_CLV -- G-2's UNVERIFIED 2-2.5% of stake
N_BETS = 4000        # horizon per replicate
N_REPS = 800         # replicates; SE on a 10% rate is ~1.1pp
W = 200              # rolling window in G-2's literal rule
CHANGE = 1500        # bet index at which the edge changes
SEED = 7

Z90 = 1.6448536269514722
HALF = Z90 * SIGMA / math.sqrt(W)     # 90% CI excludes zero once mean < -HALF

KINDS = ["ci", "cusum_0.5_4.0", "cusum_0.5_5.0", "cusum_0.5_6.0",
         "ewma_0.05_2.7", "ewma_0.05_3.1", "ewma_0.02_2.7"]


def roll_mean(X, w):
    """Rolling mean along axis 0. Returns (matrix valid from index w-1, offset)."""
    c = np.cumsum(X, axis=0)
    zeros = np.zeros((1, X.shape[1]), dtype=X.dtype)
    prev = np.concatenate([zeros, c[:-w]], axis=0)      # shape (n-w+1, reps)
    return (c[w - 1:] - prev) / w, w - 1


def first_halt(X, kind):
    """First-halt index in raw bet units (inf if never). Monitors from WARMUP."""
    steps, reps = X.shape
    z = X / SIGMA
    halted = np.full(reps, np.inf)
    active = np.ones(reps, dtype=bool)

    if kind == "ci":
        m, off = roll_mean(X, W)
        for j in range(m.shape[0]):
            hit = (m[j] < -HALF) & active
            halted[hit] = j + off
            active &= ~hit
            if not active.any():
                break
        return halted, off
    if kind.startswith("cusum"):
        k, h = (float(v) for v in kind.split("_")[1:])
        S = np.zeros(reps)
        for t in range(steps):
            S = np.maximum(0.0, S - z[t] - k)          # detects DOWNWARD drift
            hit = (S > h) & active
            halted[hit] = t
            active &= ~hit
            if not active.any():
                break
        return halted, W
    if kind.startswith("ewma"):
        lam, L = (float(v) for v in kind.split("_")[1:])
        E = np.zeros(reps)
        sd = math.sqrt(lam / (2 - lam))
        for t in range(steps):
            E = lam * z[t] + (1 - lam) * E
            hit = (E < -L * sd) & active
            halted[hit] = t
            active &= ~hit
            if not active.any():
                break
        return halted, W
    raise ValueError(kind)


def scenario(title, means, change_at=None):
    """means: callable(i)->mu at bet i, or a constant float."""
    rng = np.random.default_rng(SEED if change_at is None else SEED + change_at)
    X = np.empty((N_BETS, N_REPS))
    if change_at is None:
        X[:] = rng.normal(means, SIGMA, size=(N_BETS, N_REPS))
    else:
        X[:change_at] = rng.normal(0.005, SIGMA, size=(change_at, N_REPS))
        X[change_at:] = rng.normal(means, SIGMA, size=(N_BETS - change_at, N_REPS))

    print(f"\n{title}")
    print("  " + "-" * 62)
    print(f"  {'instrument':<16}{'halted':>9}{'median delay':>14}{'p90 delay':>11}")
    print("  " + "-" * 62)
    out = {}
    for k in KINDS:
        H, warm = first_halt(X, k)
        if change_at is None:
            shown = H - warm                    # bets of monitoring before a false alarm
        else:
            shown = np.where(np.isfinite(H), H - change_at, np.inf)
            shown = np.maximum(shown, 0)        # censor: cannot halt before the change
        rate = float(np.mean(np.isfinite(H)))
        det = shown[np.isfinite(H)]
        med = float(np.median(det)) if len(det) else float("nan")
        p90 = float(np.percentile(det, 90)) if len(det) else float("nan")
        print(f"  {k:<16}{rate*100:>8.1f}%{med:>14.0f}{p90:>11.0f}")
        out[k] = (rate, med, p90)
    if change_at is None:
        print("  (delay column here = bets of monitoring before a FALSE alarm; higher is better)")
    else:
        print(f"  (delay = bets after bet {change_at} at which the edge truly went away; lower is better)")
    return out


print("=" * 78)
print("H0 -- A COMPLETELY HEALTHY FAMILY (true mean CLV = 0.00%/bet).")
print("A halt here is a FALSE ALARM. A high false-alarm rate is self-defeating:")
print("the rule gets switched off by a human within a week, which is worse than")
print("having no rule at all.")
print("=" * 78)
scenario("H0 false-alarm behaviour", 0.0)

print()
print("=" * 78)
print("H1 -- SOFT DECAY. Family priced at +0.50%/bet through bet 1500, then 0.00%.")
print("The vig has caught up with us: we are now running to stand still.")
print("=" * 78)
scenario("H1 decay +0.50% -> 0.00%", 0.0, change_at=CHANGE)

print()
print("=" * 78)
print("H2 -- HARD DECAY. +0.50%/bet through bet 1500, then -0.50%/bet.")
print("This is the case that actually loses money, and the only reason an")
print("auto-halt exists at all.")
print("=" * 78)
scenario("H2 decay +0.50% -> -0.50%", -0.005, change_at=CHANGE)

print()
print("=" * 78)
print("READ-OUT FOR G-2 AND VCOS")
print("=" * 78)
print("""
  1. Compare the 'ci' row against the CUSUM/EWMA rows in every panel. G-2's
     literal rule -- 90% CI of an overlapping rolling-200 mean, inspected after
     every bet -- has NO bounded in-control run length, so its H0 column is the
     number that decides whether the halt rule survives contact with a operator.
     If it false-alarms materially more often than a designed sequential test,
     then t013 c.0029 s3 must name ONE instrument, not two, and it should name
     the one whose ARL we can state BEFORE deployment.
  2. Proposed replacement spec, auditable by PROPERTY not mechanism:
        "in-control ARL >= 500 bets, AND >=90% detection within 300 bets of a
         -0.50%/bet shift."
     Those are G-2's own numbers, made testable. "90% CI below zero" is a rule
     whose error rate is unknown until simulated -- the only thing this file adds.
  3. Three properties the one-sentence version omits, regardless of instrument:
       (a) halt scoped to ONE MARKET FAMILY, never global;
       (b) re-arm HUMAN-ONLY. A halt that auto-clears once the bad bets roll out
           of the trailing window is a halt that fires on a schedule;
       (c) do not use the same trailing window as both the estimate and the test
           statistic -- an overlapping-window CI overstates its own evidence,
           which is precisely why its nominal 90% is not 90%.
  4. Detection delay scales ~linearly in sigma_CLV (UNVERIFIED) and assumes
     independence, which match-level correlation breaks. So G-4's per-family
     sigma_CLV is a DESIGN INPUT to the halt rule, not a nice-to-have. Until it
     is measured, any ARL we publish is a placeholder with a chart attached.
  5. Honest scope limit: this compares instruments on ONE statistic at ONE shift
     size. It does not establish that any halt is worth deploying in a family with
     fewer than ~500 bets of history, and per the companion sample-size file, most
     of our candidate families will start below that. The correct Phase-3 statement
     may be "we cannot monitor this family yet", which is a finding, not a gap.
""")
print("COMPLETE.")
