"""
Program 2, Step 9 — Operationalize the verifier epsilon-budget.

Turns the verifier-bottleneck findings into a reusable decision tool. Given a
verifier's measured exploitability eps (and base solve-rate p, discrimination d'),
it returns:
  * N*(eps)  -- the SAFE sampling budget: the N that maximises Best-of-N accuracy,
                beyond which Goodhart exploiters make MORE samples HURT.
  * the recommended SELECTION RULE: argmax Best-of-N vs verifier-free consensus
    (majority), per the finding that consensus overtakes argmax once eps is large.
  * the theoretical crossover eps* = p/(1-p) for reference.

Reuses the calibrated Monte-Carlo model in verifier_bottleneck.py (no new physics).

CAVEAT (exp 13, 2026-07-29): eps is POOL-RELATIVE — the same verifier's measured
exploitability moved 6.2x across upstream prefilters (13_eps_pool.py). Calibrate
the eps you pass here on the post-prefilter candidate distribution the verifier
will actually face in deployment, not on raw samples. Never place a prefilter
correlated with this verifier upstream of it (worse than no prefilter at all).

CAVEAT (exp 17, 2026-07-30): the N*(eps) table is a SAMPLED-exploit, RANKING-selector
result. If your exploits are CONSTRUCTED against the verifier (an optimizer that sees
the score, a public leaderboard, an adversary that can probe the harness), then at the
SAME eps the safe budget is smaller and the post-peak floor is 0, not a residual:
measured 512 -> 128 for N*, and 0.862 -> 0.000 for accuracy at N=8192. Use N* here as
an UPPER BOUND in that setting.
  Mitigation, and it is strong: construction attacks RANKING, not COUNTING. Switching
  from argmax to consensus-among-high-scorers recovered 0.000 -> 0.999 under constructed
  exploits (vs 0.193 -> 0.999 under sampled ones) — the defence is worth MORE, not less,
  against an adaptive adversary.

CORRECTION (exp 17b): eps* = p/(1-p) is the exploit-vs-correct crossover; it is NOT the
consensus-failure boundary. Voting can also lose to the DOMINANT DISTRACTOR, which eps*
does not model. Consensus holds only if additionally
    p*Phi(d' - tau) > m_d*(1 - p)*(1 - eps)*Phi(-tau)
(m_d = distractor mass, tau = filter threshold). At d'=2, tau=1, m_d=0.5, eps=0.05 this
gives p* ~ 0.082 — so at low base rates, voting fails BELOW eps* for a reason that has no
eps in it at all.

Run: cd verifier-bottleneck && ../.venv/bin/python verifier_budget.py
"""

from __future__ import annotations

import argparse

import numpy as np

from verifier_bottleneck import sweep, N_GRID, SEED

P = 0.20            # base per-sample solve rate
DPRIME = 1.5        # verifier discrimination (fixed; eps is the axis of interest)
EPS_GRID = (0.0, 0.01, 0.03, 0.1, 0.2, 0.4)


def safe_budget(p, dprime, eps):
    """Return (N*, peak_bon, bon_at_max_N, best_rule, majority_peak)."""
    rng = np.random.default_rng(SEED)
    s = sweep(p, dprime, eps, rng=rng)
    bon, maj, Ns = s["bon"], s["majority"], s["N"]
    istar = int(np.argmax(bon))
    n_star = int(Ns[istar])
    peak_bon = float(bon[istar])
    bon_max = float(bon[-1])                       # accuracy if you keep sampling to max N
    maj_peak = float(np.max(maj))
    # Rule: consensus wins if its peak beats Best-of-N's peak by a margin.
    best_rule = "consensus" if maj_peak > peak_bon + 0.01 else "best-of-N"
    return n_star, peak_bon, bon_max, best_rule, maj_peak


def run() -> dict:
    rows = []
    for eps in EPS_GRID:
        n_star, peak, bon_max, rule, maj = safe_budget(P, DPRIME, eps)
        decline = peak - bon_max                   # how much accuracy you LOSE past N*
        rows.append({"eps": eps, "n_star": n_star, "peak": peak, "bon_max": bon_max,
                     "decline": decline, "rule": rule, "maj": maj})
    return {"rows": rows, "eps_star": P / (1 - P)}


def _print(res) -> None:
    print("=" * 74)
    print("STEP 9 — Verifier eps-budget tool")
    print("=" * 74)
    print(f"\np={P}  d'={DPRIME}  (consensus crossover eps* = p/(1-p) = {res['eps_star']:.3f})\n")
    print(f"{'eps':>6} | {'N* (safe budget)':>16} | {'peak acc':>9} | "
          f"{'acc@maxN':>9} | {'decline':>8} | {'rule':>10}")
    for r in res["rows"]:
        print(f"{r['eps']:>6g} | {r['n_star']:>16} | {r['peak']:>9.3f} | "
              f"{r['bon_max']:>9.3f} | {r['decline']:>+8.3f} | {r['rule']:>10}")
    print("\nHOW TO USE:")
    print("  1. Measure your verifier's eps (fraction of wrong answers it scores high) "
          "and p.")
    print("  2. Read off N*: sample that many, NO MORE -- past N* exploiters make")
    print("     accuracy DECLINE (see the 'decline' column grow with eps).")
    print("  3. If eps exceeds ~p/(1-p), switch from argmax Best-of-N to CONSENSUS")
    print("     (verifier-free majority) -- the 'rule' column flips to 'consensus'.")
    # Headline check: does decline grow with eps and does the rule flip?
    flips = any(r["rule"] == "consensus" for r in res["rows"])
    grows = res["rows"][-1]["decline"] > res["rows"][0]["decline"]
    print(f"\nVERDICT: eps-driven decline grows with eps {'(CONFIRMED)' if grows else ''}; "
          f"selection rule flips to consensus at high eps {'(CONFIRMED)' if flips else '(not in grid)'}.")
    print("  The tool makes the finding ACTIONABLE: eps -> (safe budget, selection rule).")


def _query(p: float, dprime: float, eps: float) -> None:
    """Single-shot answer for a user's measured (p, d', eps)."""
    n_star, peak, bon_max, rule, maj = safe_budget(p, dprime, eps)
    eps_star = p / (1 - p)
    print(f"Verifier eps-budget  (p={p}, d'={dprime}, eps={eps})")
    print(f"  safe sampling budget  N* = {n_star}   (peak accuracy {peak:.3f})")
    print(f"  accuracy if you over-sample to N={N_GRID[-1]}: {bon_max:.3f} "
          f"(decline {peak - bon_max:+.3f} past N*)")
    print(f"  consensus crossover   eps* = p/(1-p) = {eps_star:.3f}  "
          f"-> eps {'>' if eps > eps_star else '<='} eps*")
    print(f"  RECOMMENDED RULE: {rule.upper()}"
          + ("  (argmax Best-of-N is safe here)" if rule == "best-of-N"
             else "  (switch to verifier-free majority; argmax is exploited)"))
    if n_star < N_GRID[-1]:
        print(f"  ACTION: sample ~{n_star}, not more. Past N* exploiters lower accuracy.")
    else:
        print("  ACTION: verifier looks honest (monotone) — more samples keep helping.")
    print("  CAVEAT (exp 13): eps is POOL-RELATIVE (moved 6.2x across prefilters).")
    print("    - Calibrate eps on the post-prefilter pool this verifier will actually see.")
    print("    - Never prefilter with a scorer correlated with this verifier (worse than")
    print("      no prefilter). A decorrelated prefilter starves exploits, but at tight")
    print("      budgets it can still starve RIGHT answers - watch coverage too.")
    print("  CAVEAT (exp 17): if exploits are CONSTRUCTED against this verifier (an")
    print("    optimizer that sees the score, a public leaderboard), N* above is an UPPER")
    print("    BOUND and the post-peak floor is 0, not a residual. Construction attacks")
    print("    RANKING, not COUNTING - consensus recovered 0.000 -> 0.999 there, so the")
    print("    switch below matters MORE against an adaptive adversary, not less.")
    # exp 17b: eps* ignores the dominant distractor, which can win the vote on its own.
    from math import erf as _erf
    _phi = lambda z: 0.5 * (1.0 + _erf(z / 2 ** 0.5))
    tau, m_d = dprime / 2.0, 0.5
    lhs, rhs = p * _phi(dprime - tau), m_d * (1 - p) * (1 - eps) * _phi(-tau)
    if lhs <= rhs:
        print(f"  WARNING (exp 17b): at p={p:.3f} the majority vote is lost to the DOMINANT")
        print(f"    DISTRACTOR, not the exploit ({lhs:.3f} <= {rhs:.3f}). eps* = p/(1-p) does")
        print("    not model this. Consensus needs p*Phi(d'-tau) > m_d*(1-p)(1-eps)*Phi(-tau);")
        print("    here it FAILS - so consensus is NOT an available fallback at this base")
        print("    rate, whatever eps* says. Raise p (easier problems) or d' (better scorer).")


def cli(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(
        prog="verifier-budget",
        description="Given a verifier's measured exploitability, return the safe Best-of-N "
                    "budget N*(eps) and the recommended selection rule. "
                    "Based on PAPER-verifier-exploitability.md.")
    ap.add_argument("--p", type=float, help="base per-sample solve rate (e.g. 0.10)")
    ap.add_argument("--dprime", type=float, default=2.0,
                    help="verifier discrimination d' (AUC=Phi(d'/sqrt2)); default 2.0")
    ap.add_argument("--eps", type=float,
                    help="verifier exploitability: fraction of WRONG answers it scores high. "
                         "POOL-RELATIVE (exp 13): measure it on the post-prefilter candidate "
                         "distribution the verifier will face in deployment, not on raw samples")
    ap.add_argument("--table", action="store_true",
                    help="print the full eps-sweep reference table instead of a single query")
    args = ap.parse_args(argv)

    if args.table or args.p is None or args.eps is None:
        if not args.table and (args.p is None or args.eps is None):
            print("(no --p/--eps given; showing the reference table. "
                  "Use --p P --eps E for your verifier.)\n")
        _print(run())
        return 0
    _query(args.p, args.dprime, args.eps)
    return 0


if __name__ == "__main__":
    raise SystemExit(cli())
