"""Generate figures for the sound-by-verification theory paper.

All numbers are read verbatim from the module artifacts (nothing hard-coded):
    cartographer/certified_heuristic/results/control_ch_mg.json   (slack_law: the weight)
    cartographer/expressivity_bound/results/bias_frontier.json     (the bias frontier)
    cartographer/expressivity_bound/results/crossing_generalization.json  (and it scales)
    cartographer/expressivity_bound/results/predicate_axis.json    (match the predicate)

Run::

    .venv/bin/python cartographer/docs/paper_sound_by_verification/make_figures.py
"""
from __future__ import annotations

import json
from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt                    # noqa: E402

IMGS = Path(__file__).parent / "imgs"
IMGS.mkdir(parents=True, exist_ok=True)
ROOT = Path(__file__).resolve().parents[2]         # .../cartographer
CH = ROOT / "certified_heuristic" / "results"
EB = ROOT / "expressivity_bound" / "results"
HW = ROOT / "env_substrates" / "highway" / "results"

plt.rcParams.update({
    "font.size": 9, "axes.titlesize": 10, "axes.labelsize": 9,
    "legend.fontsize": 8, "figure.dpi": 200, "savefig.bbox": "tight",
    "axes.spines.top": False, "axes.spines.right": False,
})


def save(fig, name):
    fig.savefig(IMGS / name)
    plt.close(fig)
    print("wrote", name)


def _load(p):
    if not p.exists():
        print(f"  skip  {p.name} (artifact not present)")
        return None
    with open(p) as fh:
        return json.load(fh)


def fig1_weight_not_heuristic():
    """The certified win is the WEIGHT, not the learned heuristic (matched-weight control)."""
    a = _load(CH / "control_ch_mg.json")
    if a is None:
        return
    fr = a["slack_law"]["frontier"]
    regimes = [r["regime"] for r in fr]
    apparent = [r["ratio_vs_manhattan"] for r in fr]        # the "win" vs unweighted classical
    marginal = [r["marginal_over_classical_at_w"] for r in fr]  # the TRUE learned value
    x = range(len(regimes))
    fig, ax = plt.subplots(figsize=(6, 3))
    ax.bar([i - 0.2 for i in x], apparent, width=0.4, color="#1f77b4",
           label="apparent win vs unweighted classical")
    ax.bar([i + 0.2 for i in x], marginal, width=0.4, color="#bbbbbb",
           label="learned h's marginal at MATCHED weight")
    ax.axhline(1.0, color="k", lw=0.9, ls="--")
    ax.set_xticks(list(x))
    ax.set_xticklabels(regimes)
    ax.set_ylabel("nodes (ratio)")
    ax.legend(frameon=False, fontsize=7)
    ax.set_title("The speedup was the weight: at matched weight the learned h adds ~1.0x")
    save(fig, "fig1_weight_not_heuristic.png")


def fig2_bias_frontier():
    """The wall is inductive bias, not learnability: only the relaxation crosses ALT."""
    a = _load(EB / "bias_frontier.json")
    if a is None:
        return
    rows = a["rows"]
    order = ["patch", "depth_mpnn", "bellman_ford"]
    labels = {"patch": "local MLP\n(no bias)", "depth_mpnn": "message passing\n(locality)",
              "bellman_ford": "relaxation\n(the algorithm is the layer)"}
    vals = [rows[k]["ratio_vs_alt"] for k in order]
    cols = ["#d62728" if v > 1.0 else "#2ca02c" for v in vals]
    fig, ax = plt.subplots(figsize=(5, 3))
    ax.bar(range(len(order)), vals, color=cols)
    ax.axhline(1.0, color="k", lw=0.9, ls="--", label="tight classical (ALT) = 1.0x")
    ax.set_xticks(range(len(order)))
    ax.set_xticklabels([labels[k] for k in order], fontsize=8)
    ax.set_ylabel("held-out search nodes vs ALT")
    ax.legend(frameon=False)
    ax.set_title("The wall is inductive bias: only the relaxation crosses")
    save(fig, "fig2_bias_frontier.png")


def fig3_it_scales():
    """Trained at size 9, the relaxation crosses at EVERY size; the others degrade."""
    a = _load(EB / "crossing_generalization.json")
    if a is None:
        return
    rows = a["rows"]
    sizes = sorted(int(s) for s in rows)
    fig, ax = plt.subplots(figsize=(5, 3))
    for arm, col, lab in (("patch", "#1f77b4", "local MLP"),
                          ("depth_mpnn", "#ff7f0e", "message passing"),
                          ("bellman_ford", "#2ca02c", "relaxation")):
        ax.plot(sizes, [rows[str(s)][f"ratio_vs_alt_{arm}"] for s in sizes],
                "o-", color=col, label=lab)
    ax.axhline(1.0, color="k", lw=0.9, ls="--", label="ALT = 1.0x")
    ax.set_xlabel("test size (trained at 9)")
    ax.set_ylabel("search nodes vs ALT")
    ax.set_ylim(0.7, 2.6)
    ax.legend(frameon=False, fontsize=7)
    ax.set_title("And it scales: the crossing is stable 0.90x from 9 to 21")
    save(fig, "fig3_it_scales.png")


def fig4_predicate_matching():
    """The bias must MATCH the predicate: plain fails the constrained predicate."""
    a = _load(EB / "predicate_axis.json")
    if a is None:
        return
    rows = a["rows"]
    names = ["bellman_ford", "constrained_bf"]
    labels = ["plain relaxation\n(distance-to-go)", "predicate-matching\n(via the waypoint)"]
    vals = [rows[k]["ratio_vs_alt"] for k in names]
    cols = ["#d62728", "#2ca02c"]
    fig, ax = plt.subplots(figsize=(5, 3))
    ax.bar(range(2), vals, color=cols)
    ax.axhline(1.0, color="k", lw=0.9, ls="--", label="constrained ALT = 1.0x")
    ax.set_xticks(range(2))
    ax.set_xticklabels(labels, fontsize=8)
    ax.set_ylabel("search nodes vs constrained ALT")
    ax.legend(frameon=False)
    ax.set_title("Choose the bias for the predicate (PI1)")
    save(fig, "fig4_predicate_matching.png")


def fig5_regime():
    """Part (iii) measured: the classical-matching gap tracks 1 - FASTER-admissibility."""
    a = _load(HW / "bias_regime.json")
    if a is None:
        return
    cells = a.get("cells", [])
    if not cells:
        return
    x = [100 * c["faster_admissibility"] for c in cells]
    y = [c["gap_classical_matching"] for c in cells]
    rho = a["rho_cell"]
    fig, ax = plt.subplots(figsize=(5.5, 3.2))
    ax.scatter(x, y, color="#1f77b4", zorder=3)
    for c in cells:
        ax.annotate(c["scenario"], (100 * c["faster_admissibility"],
                                    c["gap_classical_matching"]),
                    textcoords="offset points", xytext=(4, 3), fontsize=6)
    ax.axhline(0, color="k", lw=0.8, ls="--")
    ax.set_xlabel("FASTER-admissibility of the regime (%)")
    ax.set_ylabel("expansions: classical - matching")
    ax.set_title(f"Part (iii): the regime carries the law (rho = {rho:+.2f})")
    save(fig, "fig5_regime.png")


def main() -> int:
    print(f"reading artifacts from {CH}, {EB} and {HW}")
    fig1_weight_not_heuristic()
    fig2_bias_frontier()
    fig3_it_scales()
    fig4_predicate_matching()
    fig5_regime()
    return 0


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