#!/usr/bin/env python3
"""
Reproducible simulations for "The Attacker's Verifier" (blog.pdj.dev).

A shared vulnerability finder (a Mythos-class model) is available to both a
defender and an attacker. For each of N latent defects in a target the two
race: the defender's discovery time is Exp(beta_D); the attacker's is
tau + Exp(beta_A), where tau >= 0 is the attacker's ACCESS LAG (the head start
a gated release / defender-first deployment buys) and g = beta_D / beta_A >= 1
is the CAPABILITY GAP (the gated model handed to defenders vs the weaker public
model attackers hold). Defender-first => patched (permanent, monotone);
attacker-first => exploited (damage, consumed).

The attacker's captured-damage share has the closed form
    phi_A(tau, g) = exp(-beta_D * tau) / (g + 1),
so the ecosystem is DEFENSE-DOMINANT (phi_A <= eps) iff
    tau >= tau*(g) = ( ln(1/eps) - ln(g + 1) ) / beta_D.

Generates two figures (themed SVG, transparent background for the dark site):
  1. mythos-race.svg      - Monte-Carlo attacker share vs access lag tau for two
                            capability gaps, against the closed form, with the
                            critical lag tau* and the tolerance eps marked.
  2. mythos-frontier.svg  - the defense-dominance phase diagram: the frontier
                            tau*(g) in (capability gap, access lag) space, with
                            the open-weights and gated-release operating points.

Run:  python3 simulations.py
Deterministic (seeded). Requires numpy, matplotlib. No network, no data files.
"""

from __future__ import annotations
import pathlib
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

SEED = 11
RNG = np.random.default_rng(SEED)
OUT = pathlib.Path(__file__).resolve().parents[2] / "assets" / "figures"
OUT.mkdir(parents=True, exist_ok=True)

# ---- dark-site theme -------------------------------------------------------
ACCENT, C2, C3, C4 = "#4da3ff", "#ff7ab2", "#82d9ff", "#d2a8ff"
TEXT, MUTED, GRID = "#c9c9cf", "#8a8a90", "#2c2c2e"
plt.rcParams.update({
    "figure.facecolor": "none", "axes.facecolor": "none", "savefig.facecolor": "none",
    "font.family": "DejaVu Sans", "font.size": 11,
    "text.color": TEXT, "axes.labelcolor": TEXT,
    "xtick.color": MUTED, "ytick.color": MUTED,
    "axes.edgecolor": GRID, "grid.color": GRID,
    "axes.spines.top": False, "axes.spines.right": False,
    "legend.frameon": False, "figure.dpi": 110,
})

def style(ax):
    ax.grid(True, alpha=0.5, linewidth=0.7)
    ax.tick_params(length=3)
    for s in ("left", "bottom"):
        ax.spines[s].set_linewidth(0.8)

def save(fig, name):
    fig.tight_layout()
    fig.savefig(OUT / name, format="svg", transparent=True, bbox_inches="tight")
    plt.close(fig)
    print(f"wrote {OUT / name}")

# ---------------------------------------------------------------------------
# The race model.
#
# beta_D fixes the time unit (1 / beta_D = expected defender find-time per
# defect); we set beta_D = 1. The attacker's per-defect hazard is beta_A =
# beta_D / g. Each defect is won by the attacker iff  tau + E_A < E_D, with
# E_D ~ Exp(beta_D), E_A ~ Exp(beta_A), independent across defects. Damages
# d_i are drawn i.i.d. and are independent of the winner, so the expected
# captured-damage share equals the win probability phi_A exactly; the Monte
# Carlo band is finite-N sampling spread around that closed form.
# ---------------------------------------------------------------------------
BETA_D = 1.0

def phi_closed(tau, g):
    """Closed-form attacker captured-damage share, exp(-beta_D tau)/(g+1)."""
    return np.exp(-BETA_D * tau) / (g + 1.0)

def race_share(tau, g, n_defects, rng):
    """One target: draw an N-defect race, return the attacker's damage share."""
    beta_A = BETA_D / g
    e_d = rng.exponential(1.0 / BETA_D, size=n_defects)     # defender find times
    e_a = tau + rng.exponential(1.0 / beta_A, size=n_defects)  # attacker, lagged
    dmg = rng.exponential(1.0, size=n_defects)              # per-defect damage
    won = e_a < e_d                                         # attacker first
    return dmg[won].sum() / dmg.sum()

def tau_star(g, eps):
    """Critical access lag; clamp negatives to 0 (moat alone suffices)."""
    return np.maximum((np.log(1.0 / eps) - np.log(g + 1.0)) / BETA_D, 0.0)

# ---------------------------------------------------------------------------
# Figure 1: attacker share vs access lag tau, ungated (g=1) vs gated (g=4),
# Monte Carlo against the closed form, with tolerance eps and tau* marked.
# ---------------------------------------------------------------------------
def fig_race():
    eps = 0.10                              # tolerated attacker share
    taus = np.linspace(0.0, 3.5, 22)
    n_defects, trials = 40, 4000
    gaps = [(1.0, C2, "s", "ungated  (g = 1)"),
            (4.0, ACCENT, "o", "gated  (g = 4)")]

    fig, ax = plt.subplots(figsize=(7.0, 3.95))
    ax.axhline(eps, color=MUTED, lw=0.9, ls=":", alpha=0.9)
    ax.text(3.47, eps + 0.012, r"tolerance $\varepsilon$", color=MUTED,
            fontsize=9, ha="right")
    for g, color, mk, label in gaps:
        mean, lo, hi = [], [], []
        for tau in taus:
            rs = np.array([race_share(tau, g, n_defects, RNG) for _ in range(trials)])
            mean.append(rs.mean())
            lo.append(np.percentile(rs, 10)); hi.append(np.percentile(rs, 90))
        mean = np.array(mean)
        ax.fill_between(taus, lo, hi, color=color, alpha=0.13, linewidth=0)
        ax.plot(taus, mean, color=color, lw=0, marker=mk, ms=4.5, label=label)
        ax.plot(taus, phi_closed(taus, g), color=color, lw=1.6, alpha=0.9)
        ts = tau_star(g, eps)
        ax.plot([ts], [eps], color=color, marker="v", ms=7, clip_on=False)
        ax.annotate(rf"$\tau^\ast={ts:.2f}$", (ts, eps), color=color, fontsize=9,
                    xytext=(ts + 0.05, eps + 0.05), ha="left")
    ax.set_xlabel(r"attacker access lag,  $\tau$   (units of $1/\beta_D$)")
    ax.set_ylabel(r"attacker captured share,  $\varphi_A$")
    ax.set_ylim(0.0, 0.55)
    ax.set_xlim(0.0, taus.max())
    ax.legend(loc="upper right")
    style(ax)
    save(fig, "mythos-race.svg")

# ---------------------------------------------------------------------------
# Figure 2: the defense-dominance frontier tau*(g). Above the curve the
# ecosystem holds the attacker share below eps; below it, offense wins the
# window. Real operating points annotated.
# ---------------------------------------------------------------------------
def fig_frontier():
    eps = 0.10
    g = np.linspace(1.0, 20.0, 400)
    ts = tau_star(g, eps)                                 # frontier, clamped at 0
    g_star = 1.0 / eps - 1.0                              # tau* hits 0 here (=9)
    top = 1.9

    fig, ax = plt.subplots(figsize=(7.0, 3.95))
    ax.fill_between(g, ts, top, color=ACCENT, alpha=0.11, linewidth=0)
    ax.fill_between(g, 0, ts, color=C2, alpha=0.10, linewidth=0)
    ax.plot(g, ts, color=ACCENT, lw=1.9)
    ax.axvline(g_star, color=MUTED, lw=0.9, ls=":", alpha=0.8)
    ax.text(g_star + 0.3, top * 0.9, r"$g^\ast=1/\varepsilon-1$" + "\nmoat alone suffices",
            color=MUTED, fontsize=8.5, va="top")
    ax.text(11.5, 1.15, "DEFENSE-DOMINANT\n" r"$\varphi_A\leq\varepsilon$",
            color=ACCENT, fontsize=10, ha="center", va="center")
    ax.text(3.1, 0.16, "offense wins\nthe window", color=C2, fontsize=9,
            ha="center", va="center")
    # operating points
    ax.plot([1.0], [0.0], color=C2, marker="X", ms=9, clip_on=False)
    ax.annotate("open weights\n(g = 1,  τ = 0)", (1.0, 0.0), color=C2, fontsize=8.5,
                xytext=(1.7, 0.30), ha="left",
                arrowprops=dict(arrowstyle="-", color=C2, lw=0.8))
    ax.plot([4.0], [1.2], color=ACCENT, marker="*", ms=13)
    ax.annotate("Fable public · Mythos gated", (4.0, 1.2), color=ACCENT, fontsize=8.5,
                xytext=(4.6, 1.5), ha="left",
                arrowprops=dict(arrowstyle="-", color=ACCENT, lw=0.8))
    ax.set_xlabel(r"capability gap,  $g=\beta_D/\beta_A$")
    ax.set_ylabel(r"access lag,  $\tau$   (units of $1/\beta_D$)")
    ax.set_ylim(0.0, top)
    ax.set_xlim(1.0, 20.0)
    style(ax)
    save(fig, "mythos-frontier.svg")

if __name__ == "__main__":
    fig_race()
    fig_frontier()
    print("done.")
