"""
vbcore — shared, validated core for the verifier-bottleneck track.

Extracted verbatim (behaviour-preserving) from verifier_bottleneck.py and
verifier_bottleneck_followups.py so the 10 next-step experiments all build on one sampler.

Model recap: N candidate answers per problem; each correct w.p. p. A verifier scores each:
  correct        ~ N(d', 1)
  honest-wrong   ~ N(0, 1)
  exploiter      ~ N(d' + hack_margin, 1)   (a Goodhart blind spot, scored high)
Two structural knobs on exploiters:
  f_exploitable  : fraction of PROBLEMS that admit exploiters (per-problem correlation),
                   with within-problem rate q = eps / f_exploitable (matched marginal eps).
  exploit_cluster: fraction of exploiters that share ONE gamed answer (bucket 2) vs scatter.
Answer buckets for consensus voting: 0=correct, 1=dominant distractor, 2=shared exploit, 3=unique wrong.
"""
import numpy as np
from math import erf

SEED = 0
N_GRID = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192]


def auc(dprime):
    return 0.5 * (1 + erf(dprime / 2.0))


def bar(x, w=20):
    n = int(round(max(0.0, min(1.0, x)) * w))
    return "#" * n + "-" * (w - n)


def n_trials_for(N, budget=2_000_000, lo=1500, hi=12000):
    return int(min(hi, max(lo, budget // N)))


def draw(N, p, dprime, eps, hack_margin=1.0, distractor_mass=0.5,
         f_exploitable=1.0, exploit_cluster=1.0, n_trials=4000, rng=None):
    """Return (correct, scores, buckets), each shape (n_trials, N)."""
    if rng is None:
        rng = np.random.default_rng(SEED)
    correct = rng.random((n_trials, N), dtype=np.float32) < p
    wrong = ~correct

    q_within = min(1.0, eps / f_exploitable) if f_exploitable > 0 else 0.0
    prob_expl = (rng.random((n_trials, 1), dtype=np.float32) < f_exploitable)
    expl_hit = (rng.random((n_trials, N), dtype=np.float32) < q_within)
    exploiter = wrong & expl_hit & prob_expl

    scores = rng.standard_normal((n_trials, N)).astype(np.float32)
    scores += correct * np.float32(dprime)
    scores += exploiter * np.float32(dprime + hack_margin)

    buckets = np.zeros((n_trials, N), dtype=np.int8)
    wne = wrong & ~exploiter
    is_distractor = wne & (rng.random((n_trials, N), dtype=np.float32) < distractor_mass)
    buckets[wne] = 3
    buckets[is_distractor] = 1
    is_clustered = exploiter & (rng.random((n_trials, N), dtype=np.float32) < exploit_cluster)
    buckets[exploiter] = 3
    buckets[is_clustered] = 2
    return correct, scores, buckets


def select(correct, scores, buckets, tau, method):
    """Accuracy of a selection rule: argmax | earlystop | filtervote."""
    T = correct.shape[0]
    rows = np.arange(T)
    gmax = scores.argmax(axis=1)
    if method == "argmax":
        return float(correct[rows, gmax].mean())
    if method == "earlystop":
        passed = scores > tau
        has = passed.any(axis=1)
        first = passed.argmax(axis=1)
        sel = np.where(has, first, gmax)
        return float(correct[rows, sel].mean())
    if method == "filtervote":
        surv = scores > tau
        c0 = ((buckets == 0) & surv).sum(1)
        c1 = ((buckets == 1) & surv).sum(1)
        c2 = ((buckets == 2) & surv).sum(1)
        has_uniq = ((buckets == 3) & surv).any(1)
        win0 = (c0 > c1) & (c0 > c2) & (c0 >= 1) & ((c0 >= 2) | (~has_uniq))
        no_surv = ~surv.any(1)
        fallback = correct[rows, gmax]
        return float(np.where(no_surv, fallback, win0).mean())
    raise ValueError(method)


def curve(p, dprime, eps, hack_margin=1.0, distractor_mass=0.5,
          f_exploitable=1.0, exploit_cluster=1.0, method="argmax",
          tau=None, rng=None, n_grid=None):
    if rng is None:
        rng = np.random.default_rng(SEED)
    if tau is None:
        tau = dprime / 2.0
    if n_grid is None:
        n_grid = N_GRID
    acc = []
    for N in n_grid:
        c, s, b = draw(N, p, dprime, eps, hack_margin, distractor_mass,
                       f_exploitable, exploit_cluster, n_trials_for(N), rng)
        acc.append(select(c, s, b, tau, method))
    return np.array(acc)


def shape_of(acc):
    istar = int(np.argmax(acc))
    return istar, float(acc[istar]), float(acc[-1])
