#!/usr/bin/env python3
"""Analysis layer for the combined-pool combat sim (spec §12).

Consumes the CSV/JSON that ``combat-sim`` exports and renders the headline views
that make "feel" measurable instead of eyeballed:

  1. Difficulty curve  — player win-rate vs. con tier, one line per policy.
  2. Duration tent     — time-to-resolution vs. con tier (trivial → contested → lethal).
  3. Auto-attack guardrail — auto-only win-rate vs. the full kit (§12 anti-turtle).
  4. Stagger frequency — staggers inflicted vs. con tier.
  5. One fight's ceiling-decay — the combined pool (fill) and ceiling (cap) over time.

Run after a sweep:

    python analyze.py --data ../out --plots plots

The file is also organized into ``# %%`` cells so it doubles as a notebook in
Jupyter / VS Code interactive.
"""
# %%
from __future__ import annotations

import argparse
import json
from pathlib import Path

import matplotlib

matplotlib.use("Agg")  # headless: write PNGs, never block on a window
import matplotlib.pyplot as plt
import pandas as pd


# %%
def load(data_dir: Path):
    """Load the three sim artifacts; tolerate a missing sample fight."""
    fights = pd.read_csv(data_dir / "fights.csv")
    summary = pd.read_csv(data_dir / "summary.csv")
    sample_path = data_dir / "sample_fight.json"
    sample = json.loads(sample_path.read_text()) if sample_path.exists() else None
    return fights, summary, sample


# %%
def plot_difficulty_curve(summary: pd.DataFrame, out: Path):
    """Win-rate vs. con tier, one line per player policy — the difficulty curve
    and the skill gradient in one panel."""
    con = summary[summary["sweep"] == "con"]
    fig, ax = plt.subplots(figsize=(8, 5))
    for ctrl, grp in sorted(con.groupby("player_ctrl")):
        grp = grp.sort_values("con_delta")
        ax.plot(grp["con_delta"], grp["player_win_rate"] * 100, marker="o", label=ctrl)
    ax.axvline(0, color="grey", lw=0.8, ls=":")
    ax.set_xlabel("con tier  (enemy_level − player_level)")
    ax.set_ylabel("player win rate (%)")
    ax.set_title("Difficulty curve — win rate by con tier and policy")
    ax.set_ylim(-3, 103)
    ax.grid(True, alpha=0.3)
    ax.legend(title="player policy")
    fig.tight_layout()
    fig.savefig(out / "difficulty_curve.png", dpi=120)
    plt.close(fig)


# %%
def plot_duration_tent(summary: pd.DataFrame, out: Path):
    """Mean fight duration vs. con tier — the 'trivial → contested → lethal' tent."""
    con = summary[(summary["sweep"] == "con") & (summary["player_ctrl"] == "skill80")]
    con = con.sort_values("con_delta")
    fig, ax = plt.subplots(figsize=(8, 5))
    ax.plot(con["con_delta"], con["mean_seconds"], marker="o", color="darkorange")
    ax.axvline(0, color="grey", lw=0.8, ls=":")
    ax.set_xlabel("con tier  (enemy_level − player_level)")
    ax.set_ylabel("mean time-to-resolution (s)")
    ax.set_title("Duration tent — fights are longest (most contested) at even con")
    ax.grid(True, alpha=0.3)
    fig.tight_layout()
    fig.savefig(out / "duration_tent.png", dpi=120)
    plt.close(fig)


# %%
def plot_guardrail(summary: pd.DataFrame, out: Path):
    """The §12 anti-turtle guardrail: auto-attack-only win-rate against the best
    full-kit policy. If auto-only ever matches the kit, the interesting verbs are
    a tax and the thesis is broken."""
    con = summary[summary["sweep"] == "con"]
    auto = con[con["player_ctrl"] == "auto_only"].sort_values("con_delta")
    best = (
        con[con["player_ctrl"] != "auto_only"]
        .groupby("con_delta")["player_win_rate"]
        .max()
        .reset_index()
        .sort_values("con_delta")
    )
    fig, ax = plt.subplots(figsize=(8, 5))
    ax.plot(auto["con_delta"], auto["player_win_rate"] * 100, marker="o", label="auto-attack only")
    ax.plot(best["con_delta"], best["player_win_rate"] * 100, marker="s", label="best full kit")
    ax.fill_between(
        best["con_delta"],
        auto.set_index("con_delta").reindex(best["con_delta"])["player_win_rate"].values * 100,
        best["player_win_rate"] * 100,
        alpha=0.15,
        color="green",
        label="value of abilities",
    )
    ax.set_xlabel("con tier  (enemy_level − player_level)")
    ax.set_ylabel("player win rate (%)")
    ax.set_title("Anti-turtle guardrail — abilities must beat auto-attack")
    ax.grid(True, alpha=0.3)
    ax.legend()
    fig.tight_layout()
    fig.savefig(out / "guardrail.png", dpi=120)
    plt.close(fig)


# %%
def plot_stagger_frequency(summary: pd.DataFrame, out: Path):
    """Staggers the player lands vs. con tier (a skilled player)."""
    con = summary[(summary["sweep"] == "con") & (summary["player_ctrl"] == "skill80")]
    con = con.sort_values("con_delta")
    fig, ax = plt.subplots(figsize=(8, 5))
    ax.plot(con["con_delta"], con["mean_player_staggers_inflicted"], marker="o", color="purple")
    ax.set_xlabel("con tier  (enemy_level − player_level)")
    ax.set_ylabel("mean staggers inflicted / fight")
    ax.set_title("Stagger frequency by con tier (skilled player)")
    ax.grid(True, alpha=0.3)
    fig.tight_layout()
    fig.savefig(out / "stagger_frequency.png", dpi=120)
    plt.close(fig)


# %%
def plot_sample_fight(sample: dict, out: Path):
    """One fight's combined pool over time: fill (the visible bar) and cap (the
    ceiling that attrition grinds down). The squeeze made legible."""
    if sample is None:
        print("  (no sample_fight.json — skipping time-series plot)")
        return
    metrics = sample["metrics"]
    tick_rate = metrics.get("tick_rate", 10)
    fig, ax = plt.subplots(figsize=(9, 5))
    for actor in metrics["actors"]:
        fill = [v / 1000.0 for v in actor.get("fill_series", [])]
        cap = [v / 1000.0 for v in actor.get("cap_series", [])]
        if not fill:
            continue
        t = [i / tick_rate for i in range(len(fill))]
        (line,) = ax.plot(t, fill, label=f'{actor["name"]} — vigor (fill)')
        ax.plot(t, cap, ls="--", color=line.get_color(), alpha=0.7, label=f'{actor["name"]} — cap (ceiling)')
    ax.set_xlabel("time (s)")
    ax.set_ylabel("vigor")
    ax.set_title("One fight — fill spent toward zero, ceiling ground down by attrition")
    ax.grid(True, alpha=0.3)
    ax.legend(fontsize=8)
    fig.tight_layout()
    fig.savefig(out / "sample_fight.png", dpi=120)
    plt.close(fig)


# %%
def print_findings(summary: pd.DataFrame):
    """A terse text read-out of the headline numbers."""
    con = summary[summary["sweep"] == "con"]
    print("\n=== headline findings ===")

    even = con[con["con_delta"] == 0]
    if not even.empty:
        print("Even-con win rate by policy (skill should climb monotonically):")
        for _, r in even.sort_values("player_ctrl").iterrows():
            print(f"  {r['player_ctrl']:>10}: {r['player_win_rate'] * 100:5.1f}%  "
                  f"(mean {r['mean_seconds']:.1f}s)")

    # The guardrail only means something in the CONTESTED band: at trivial
    # (under-con) and lethal (over-con) tiers every policy wins 100% / 0%, so the
    # kit can't add value to an already-decided fight. Averaging those in hides
    # the signal. Restrict to tiers where the full kit is genuinely in play.
    kit_by_delta = (
        con[con["player_ctrl"] == "skill80"].set_index("con_delta")["player_win_rate"]
    )
    auto_by_delta = (
        con[con["player_ctrl"] == "auto_only"].set_index("con_delta")["player_win_rate"]
    )
    contested = [d for d, w in kit_by_delta.items() if 0.05 < w < 0.95]
    if contested:
        auto_c = auto_by_delta.reindex(contested).mean()
        kit_c = kit_by_delta.reindex(contested).mean()
        print(f"\nContested tiers (kit win-rate in 5–95%): {sorted(contested)}")
        print(f"  auto-attack-only mean win rate: {auto_c * 100:5.1f}%")
        print(f"  full-kit (skill80) mean win rate: {kit_c * 100:5.1f}%")
        verdict = (
            "GREEN — abilities beat turtling where it matters"
            if kit_c > auto_c + 0.05
            else "RED — auto-attack dominates even contested fights"
        )
        print(f"  guardrail: {verdict}")
    else:
        print("\nNo contested tiers found (curve is a step) — widen the con curve.")

    arch = summary[summary["sweep"] == "archetypes"]
    if not arch.empty:
        print("\nArchetype matchups (skill80 player win rate):")
        for _, r in arch.sort_values("matchup").iterrows():
            print(f"  vs {r['matchup']:>10} (con {r['con_delta']:+d}): "
                  f"{r['player_win_rate'] * 100:5.1f}%  mean {r['mean_seconds']:.1f}s")


# %%
def main():
    ap = argparse.ArgumentParser(description="Analyze combat-sim output.")
    ap.add_argument("--data", default="../out", type=Path, help="dir with fights.csv/summary.csv")
    ap.add_argument("--plots", default="plots", type=Path, help="dir to write PNGs")
    args = ap.parse_args()

    fights, summary, sample = load(args.data)
    args.plots.mkdir(parents=True, exist_ok=True)

    print(f"loaded {len(fights):,} fights across {len(summary)} cells from {args.data}")
    plot_difficulty_curve(summary, args.plots)
    plot_duration_tent(summary, args.plots)
    plot_guardrail(summary, args.plots)
    plot_stagger_frequency(summary, args.plots)
    plot_sample_fight(sample, args.plots)
    print_findings(summary)
    print(f"\nplots written to {args.plots}/")


if __name__ == "__main__":
    main()
