"""Plot only the saved corrected Case20 results; no scientific recalculation.
Usage:
  python case20-season-lag-associations-20261012-plot.py --zip florida_nino34_reproducibility_corrected_1986_2025.zip --output-dir new-figure
Requires existing numpy and matplotlib. No downloads or installation.
Refuses to overwrite outputs. Source ZIP must match the final saved snapshot.
"""
import argparse
import csv
import hashlib
import io
import json
from pathlib import Path
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from matplotlib.backends.backend_agg import FigureCanvasAgg

PREFIX = "case20-season-lag-associations-20261012"
EXPECTED_ZIP_SHA256 = "42943673d141b85caeac1f1d6d4268e771b4f6b3701a292a62706a65298f38ea"
SOURCE_NAME = "florida_nino34_reproducibility_corrected_1986_2025.zip"
STYLE = {
    "font.family": "DejaVu Sans", "font.size": 14,
    "axes.labelsize": 14, "axes.titlesize": 14,
    "xtick.labelsize": 10, "ytick.labelsize": 10, "legend.fontsize": 11,
    "figure.facecolor": "white", "axes.facecolor": "white",
    "savefig.facecolor": "white", "text.color": "#202020",
    "axes.labelcolor": "#202020", "xtick.color": "#202020",
    "ytick.color": "#202020", "axes.edgecolor": "#757575",
    "axes.grid": False, "text.usetex": False,
    "savefig.bbox": "tight", "savefig.pad_inches": 0.1,
    "axes.linewidth": 0.6, "xtick.major.width": 0.6, "ytick.major.width": 0.6,
    "figure.autolayout": False, "figure.constrained_layout.use": False,
    "font.weight": "normal", "axes.labelweight": "normal", "axes.titleweight": "normal",
    "lines.antialiased": True, "patch.antialiased": True,
    "svg.fonttype": "none", "svg.hashsalt": PREFIX
}

def create_figure(zip_path, output_dir):
    import zipfile
    zip_path, output_dir = Path(zip_path), Path(output_dir)
    before = hashlib.sha256(zip_path.read_bytes()).hexdigest()
    assert before == EXPECTED_ZIP_SHA256, "Not the verified corrected ZIP snapshot"
    with zipfile.ZipFile(zip_path) as archive:
        names = ["season_lag_estimates.csv", "contrasts.csv", "validation.json"]
        payloads = {name: archive.read(name) for name in names}
        manifest = json.loads(archive.read("manifest_sha256.json"))
        for name, content in payloads.items():
            assert hashlib.sha256(content).hexdigest() == manifest[name]
        rows = list(csv.DictReader(io.StringIO(payloads[names[0]].decode("utf-8"))))
        contrasts = list(csv.DictReader(io.StringIO(payloads[names[1]].decode("utf-8"))))
        validation = json.loads(payloads["validation.json"])
    assert (validation["outcome_start"],validation["outcome_end"]) == ("1986-01-01","2025-12-01")
    assert (validation["months"],validation["calendar_years"]) == (480,40)
    order = [(s,k) for s in ["DJF","MAM","JJA","SON"] for k in range(3)]
    assert [(r["season"],int(r["lag_months"])) for r in rows] == order
    assert all(int(r["n_months"])==120 and int(r["n_calendar_years"])==40 for r in rows)
    columns = ["slope_mm_per_C","pointwise_low","pointwise_high","simultaneous_low","simultaneous_high"]
    values = np.array([[float(r[c]) for c in columns] for r in rows])
    assert values.shape == (12,5) and np.isfinite(values).all()
    assert np.all(values[:,1]<=values[:,2]) and np.all(values[:,3]<=values[:,4])
    assert np.all((values[:,1]<=values[:,0]) & (values[:,0]<=values[:,2]))
    contrast = next(r for r in contrasts if r["contrast"]=="DJF_minus_JJA_lag0")
    clo,chi = float(contrast["simultaneous_low"]),float(contrast["simultaneous_high"])
    assert clo < 0 < chi
    output_dir.mkdir(parents=True,exist_ok=True)
    paths = {extension: output_dir/(PREFIX+extension) for extension in [".png",".svg","-source-check.csv"]}
    assert not any(p.exists() for p in paths.values()), "Refusing to overwrite existing outputs"

    with mpl.rc_context(STYLE):
        fig = plt.figure(figsize=(10,9.5),dpi=150,facecolor="white")
        FigureCanvasAgg(fig)
        ax = fig.add_axes([.22,.285,.73,.49])
        ax.set_facecolor("white")
        fig.text(.06,.965,"Florida precipitation and Niño 3.4: seasonal and lag associations",fontsize=14,va="top")
        fig.text(.06,.929,"1986–2025  |  120 monthly observations per season across 40 years",fontsize=11,va="top")
        fig.text(.06,.900,"Calendar-month-adjusted slopes; positive lag means Niño precedes precipitation.",fontsize=11,va="top")
        neutral, focal = "#595959","#0072B2"
        handles = [
            Line2D([0],[0],color=neutral,lw=4,solid_capstyle="butt",label="95% pointwise interval (thick)"),
            Line2D([0],[0],color=neutral,lw=1.2,marker="|",markersize=8,label="Approx. simultaneous 95% interval (thin, capped)"),
            Line2D([0],[0],color=neutral,lw=0,marker="o",markersize=5,label="Saved slope"),
            Line2D([0],[0],color=focal,lw=0,marker="D",markersize=6,label="Prespecified primary: DJF, lag 0")
        ]
        fig.legend(handles=handles,loc="upper left",bbox_to_anchor=(.06,.863),
                   ncol=2,frameon=False,columnspacing=2,handlelength=2.5,
                   borderaxespad=0,labelspacing=.75)
        positions = np.array([i+(i//3)*.7 for i in range(12)],dtype=float)
        ax.axvline(0,color="#909090",ls=(0,(3,3)),lw=.9,zorder=0)
        marks=[]
        for i,(r,y) in enumerate(zip(rows,positions)):
            s,pl,ph,sl,sh = values[i]
            color = focal if i==0 else neutral
            sim, = ax.plot([sl,sh],[y,y],color=color,lw=1.2,marker="|",markersize=8,
                           markeredgewidth=1.2,zorder=2,solid_capstyle="butt")
            point, = ax.plot([pl,ph],[y,y],color=color,lw=4,zorder=3,solid_capstyle="butt")
            dot, = ax.plot([s],[y],marker="D" if i==0 else "o",ms=6 if i==0 else 5,
                          color=color,mec="white",mew=.65,ls="none",zorder=4)
            sim.set_gid(f"{r['season']}-lag{r['lag_months']}-simultaneous")
            point.set_gid(f"{r['season']}-lag{r['lag_months']}-pointwise")
            dot.set_gid(f"{r['season']}-lag{r['lag_months']}-slope")
            # Exact coordinate checks against parsed saved decimal strings; no estimates recomputed.
            assert np.array_equal(sim.get_xdata(),[float(r["simultaneous_low"]),float(r["simultaneous_high"])])
            assert np.array_equal(point.get_xdata(),[float(r["pointwise_low"]),float(r["pointwise_high"])])
            assert np.array_equal(dot.get_xdata(),[float(r["slope_mm_per_C"])])
            marks.append((dot,point,sim))
        for i in [2,5,8]:
            ax.axhline((positions[i]+positions[i+1])/2,color="#eeeeee",lw=.8,zorder=0)
        ax.set_yticks(positions,[f"{s}  |  lag {k}" for s,k in order])
        ax.get_yticklabels()[0].set_color(focal)
        ax.get_yticklabels()[0].set_fontweight("bold")
        ax.set_ylim(positions[-1]+.8,-.8)
        ax.set_xlim(float(values[:,3].min())-4,float(values[:,4].max())+4)
        ax.set_xticks([-20,-10,0,10,20,30])
        ax.set_xlabel("Monthly precipitation slope (mm per °C of Niño 3.4)",labelpad=12)
        ax.tick_params(axis="y",length=0,pad=10)
        ax.tick_params(axis="x",direction="out",length=4)
        for side in ["top","right","left"]:
            ax.spines[side].set_visible(False)
        fig.text(.06,.205,"Lag is in months. DJF: Dec–Feb; MAM: Mar–May; JJA: Jun–Aug; SON: Sep–Nov.",fontsize=11,va="top")
        fig.text(.06,.170,"Saved intervals: 5-year block bootstrap; 4,000 replicates.",fontsize=11,va="top")
        fig.text(.06,.140,"Approximate simultaneous coverage is over 23 estimates (12 slopes + 11 contrasts).",fontsize=11,va="top")
        fig.text(.06,.100,f"DJF − JJA at lag 0: simultaneous interval [{clo:.2f}, {chi:.2f}] mm/°C includes zero.",fontsize=11,va="top")
        fig.text(.06,.060,"Historical association is neither causation nor forecast validation.",fontsize=11,va="top")
        fig.canvas.draw()
        renderer=fig.canvas.get_renderer()
        texts=[(t,t.get_window_extent(renderer)) for t in fig.findobj(mpl.text.Text)
               if t.get_visible() and t.get_text().strip()]
        outside=[t.get_text() for t,b in texts if not (b.x0>=0 and b.y0>=0 and b.x1<=fig.bbox.x1 and b.y1<=fig.bbox.y1)]
        overlaps=[(a.get_text(),b.get_text()) for i,(a,ba) in enumerate(texts)
                  for b,bb in texts[i+1:] if ba.overlaps(bb)]
        assert not outside,("Text clipping",outside)
        assert not overlaps,("Text overlap",overlaps)
        for dot,point,sim in marks:
            for artist in [dot,point,sim]:
                assert np.all(np.asarray(artist.get_xdata())>ax.get_xlim()[0])
                assert np.all(np.asarray(artist.get_xdata())<ax.get_xlim()[1])
        fig.savefig(paths[".png"],dpi=300,facecolor="white")
        fig.savefig(paths[".svg"],facecolor="white",metadata={"Date":None,"Description":"Saved corrected 1986–2025 Case20 results; visualization only."})
    checks=[]
    for i,r in enumerate(rows):
        item=dict(r)
        item.update({"record_type":"plotted_association","source_member":"season_lag_estimates.csv",
            "source_data_row_1based":i+1,"plotted_y":repr(float(positions[i])),
            **{f"plotted_{c}":r[c] for c in columns},
            "prespecified_primary":str(i==0).lower(),"artist_values_match_saved":"true"})
        checks.append(item)
    item=dict(contrast)
    item.update({"record_type":"caption_contrast_warning","source_member":"contrasts.csv",
        "source_data_row_1based":contrasts.index(contrast)+1,
        "caption_simultaneous_low":contrast["simultaneous_low"],
        "caption_simultaneous_high":contrast["simultaneous_high"],
        "simultaneous_interval_includes_zero":"true"})
    checks.append(item)
    for item in checks:
        item.update({"source_zip":SOURCE_NAME,"source_zip_sha256":before,
            "source_member_sha256":hashlib.sha256(payloads[item["source_member"]]).hexdigest(),
            "season_lag_estimates_sha256":hashlib.sha256(payloads["season_lag_estimates.csv"]).hexdigest(),
            "contrasts_sha256":hashlib.sha256(payloads["contrasts.csv"]).hexdigest(),
            "validation_sha256":hashlib.sha256(payloads["validation.json"]).hexdigest(),
            "raw_florida_source_sha256":validation["raw_snapshot_sha256"]["florida_precipitation.csv"],
            "raw_nino_source_sha256":validation["raw_snapshot_sha256"]["nino34_anomalies.txt"],
            "outcome_start":validation["outcome_start"],"outcome_end":validation["outcome_end"],
            "python_plot_only":"true","matplotlib_version":mpl.__version__,
            "numpy_version":np.__version__,"text_bbox_check":"pass","mark_bounds_check":"pass"})
    fields=list(dict.fromkeys(k for item in checks for k in item))
    with paths["-source-check.csv"].open("x",newline="",encoding="utf-8") as f:
        writer=csv.DictWriter(f,fieldnames=fields); writer.writeheader(); writer.writerows(checks)
    with paths["-source-check.csv"].open(newline="",encoding="utf-8") as f:
        saved_check=list(csv.DictReader(f))
    for i,r in enumerate(rows):
        for key,value in r.items():
            assert saved_check[i][key]==value
        for key in columns:
            assert saved_check[i]["plotted_"+key]==r[key]
    assert hashlib.sha256(zip_path.read_bytes()).hexdigest()==before
    print(json.dumps({"status":"PASS","associations":12,"saved_numeric_coordinates_checked":60,
        "source_check_rows":13,"source_unchanged":True,"text_clipping":False,"text_overlap":False,
        "scientific_analysis_rerun":False,"outputs":[p.name for p in paths.values()]}))
    return fig, paths

if __name__=="__main__":
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--zip",type=Path,default=Path(SOURCE_NAME))
    parser.add_argument("--output-dir",type=Path,default=Path("."))
    args=parser.parse_args()
    create_figure(args.zip,args.output_dir)
