#!/usr/bin/env python3
"""Render the compact, audit-bound Phase 4 presentation (CPU only)."""
from __future__ import annotations

import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import sys

os.environ.setdefault("CUDA_VISIBLE_DEVICES", "")
os.environ.setdefault("MPLBACKEND", "Agg")

import matplotlib.pyplot as plt
import numpy as np


ROOT = Path(__file__).resolve().parents[3]
RUN_ROOT = ROOT / "src/OID_analysis/runs/active-two-scene-v2"
AUDIT_NAME = "OID_PHASE4_AUDIT_20260723T1510Z.json"
JOBS = (
    ("karman_re100", "delta_forces"),
    ("karman_re100", "delta_sensors"),
    ("illusion_1.0L", "delta_forces"),
    ("illusion_1.0L", "delta_sensors"),
)
SHORT_LABELS = ("Kármán · force", "Kármán · sensors", "Illusion · force", "Illusion · sensors")
FIGURES = (
    "01_primary_vs_deranged",
    "02_selection_adjusted_periodic_excluded_p_values",
    "03_force_state_reconstruction",
)


def _sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        for block in iter(lambda: stream.read(1024 * 1024), b""):
            digest.update(block)
    return digest.hexdigest()


def _load(path: Path) -> dict:
    with path.open("r", encoding="utf-8") as stream:
        return json.load(stream)


def _sources() -> tuple[Path, list[Path]]:
    audit = RUN_ROOT / AUDIT_NAME
    reports = [RUN_ROOT / "analysis" / scene / observable / "phase_3_5_report.json"
               for scene, observable in JOBS]
    return audit, reports


def _verify(audit_path: Path, report_paths: list[Path]) -> tuple[dict, list[dict], dict[str, str]]:
    audit = _load(audit_path)
    if audit.get("schema_id") != "oid-phase4-audit/v3":
        raise RuntimeError("unexpected Phase 4 audit schema")
    reports = [_load(path) for path in report_paths]
    hashes = {str(audit_path.relative_to(ROOT)): _sha256(audit_path)}
    jobs = audit["gates"]["formal_analyses"]["evidence"]["jobs"]
    for (scene, observable), path, report in zip(JOBS, report_paths, reports):
        if report.get("schema_id") != "oid-phase3-5-loro/v2":
            raise RuntimeError(f"unexpected report schema: {path}")
        actual = _sha256(path)
        expected = jobs[f"{scene}/{observable}"]["evidence"]["report_sha256"]
        if actual != expected:
            raise RuntimeError(f"audit/report hash mismatch: {path}")
        hashes[str(path.relative_to(ROOT))] = actual
    if audit.get("overall") != "conditional":
        raise RuntimeError("this presentation is restricted to the audited conditional result")
    return audit, reports, hashes


def _style() -> None:
    plt.rcParams.update({
        "figure.dpi": 140,
        "savefig.dpi": 180,
        "font.size": 8,
        "axes.titlesize": 9,
        "axes.labelsize": 8,
        "legend.fontsize": 7,
        "pdf.fonttype": 42,
    })


def _save(fig: plt.Figure, destination: Path, stem: str, binding: str) -> None:
    common = {"bbox_inches": "tight", "facecolor": "white"}
    fig.savefig(destination / f"{stem}.png", metadata={"Evidence": binding}, **common)
    fig.savefig(
        destination / f"{stem}.pdf",
        metadata={
            "Title": stem,
            "Subject": "Audited OID Phase 4 compact summary",
            "Keywords": binding,
            "Creator": "plot_phase4_summary.py (CPU-only)",
        },
        **common,
    )
    plt.close(fig)


def _plot_primary(reports: list[dict]) -> plt.Figure:
    fig, ax = plt.subplots(figsize=(7.2, 3.6))
    offsets = np.array((-0.18, 0.0, 0.18))
    for job_index, report in enumerate(reports):
        for fold_index, fold in enumerate(report["folds"]):
            y = job_index + offsets[fold_index]
            observed = fold["metric_weighted_r2"]
            control = fold["realization_derangement_negative_control_metric_weighted_r2"]
            passed = fold["negative_control_pass"]
            color = "#16835b" if passed else "#c43c39"
            ax.plot([control, observed], [y, y], color=color, alpha=0.65, linewidth=1.2)
            ax.scatter(control, y, marker="x", color="#555555", s=25, zorder=3)
            ax.scatter(control + 0.01, y, marker="|", color="#111111", s=65, zorder=3)
            ax.scatter(observed, y, marker="o", color=color, edgecolor="white", linewidth=0.4, s=28, zorder=4)
    ax.axvline(0, color="#aaaaaa", linewidth=0.7)
    ax.set_yticks(range(len(JOBS)), SHORT_LABELS)
    ax.invert_yaxis()
    ax.set_xlabel(r"H-weighted held-out $R^2$ (H = I)")
    ax.set_title("Primary fold score vs realization-deranged negative control")
    ax.grid(axis="x", color="#dddddd", linewidth=0.6)
    ax.scatter([], [], marker="o", color="#16835b", label="observed: gate pass")
    ax.scatter([], [], marker="o", color="#c43c39", label="observed: gate fail")
    ax.scatter([], [], marker="x", color="#555555", label="deranged control")
    ax.scatter([], [], marker="|", color="#111111", label="control + 0.01 gate")
    ax.legend(loc="best", frameon=False, ncol=2)
    fig.tight_layout()
    return fig


def _plot_selection(reports: list[dict]) -> plt.Figure:
    fig, axes = plt.subplots(1, 3, figsize=(8.6, 3.4), sharex=True)
    x = np.arange(len(JOBS))
    offsets = np.array((-0.18, 0.0, 0.18))
    for job_index, report in enumerate(reports):
        for fold_index, fold in enumerate(report["folds"]):
            position = job_index + offsets[fold_index]
            color = "#16835b" if fold["negative_control_pass"] else "#c43c39"
            axes[0].scatter(
                position,
                fold["selection_adjusted_periodic_excluded_empirical_p_value"],
                color=color,
                s=24,
            )
            axes[1].scatter(position, fold["rank"], color=color, s=24)
            axes[2].scatter(position, fold["sample_lag"], color=color, s=24)
    axes[0].axhline(0.05, color="#555555", linestyle="--", linewidth=0.8)
    axes[0].set_yscale("log")
    axes[0].set_ylabel("selection-adjusted periodic-excluded p-value")
    axes[0].set_title("Periodic-excluded circular-shift null")
    axes[1].set_ylabel("selected state rank")
    axes[1].set_title("Training-only rank")
    axes[2].set_ylabel("selected sample lag")
    axes[2].set_title("Training-only lag")
    for ax in axes:
        ax.set_xticks(x, ("K·F", "K·S", "I·F", "I·S"))
        ax.grid(axis="y", color="#dddddd", linewidth=0.6)
    fig.suptitle("Periodic-excluded null p-values and selected model", y=1.02)
    fig.tight_layout()
    return fig


def _plot_reconstruction(reports: list[dict]) -> plt.Figure:
    fig, ax = plt.subplots(figsize=(6.4, 3.1))
    force_reports = (reports[0], reports[2])
    labels = ("Kármán · force", "Illusion · force")
    offsets = np.array((-0.15, 0.0, 0.15))
    for job_index, report in enumerate(force_reports):
        for fold_index, fold in enumerate(report["folds"]):
            value = fold["lr_state_coefficient_r2"]
            ax.scatter(job_index + offsets[fold_index], value, color="#c43c39", s=30)
            ax.annotate(fold["held_out_realization"], (job_index + offsets[fold_index], value),
                        xytext=(0, 4), textcoords="offset points", ha="center", fontsize=7)
    ax.axhline(0, color="#111111", linewidth=0.8)
    ax.set_xticks(range(2), labels)
    ax.set_ylabel(r"held-out LR state-coefficient $R^2$")
    ax.set_title("Force OID does not demonstrate state reconstruction skill")
    ax.grid(axis="y", color="#dddddd", linewidth=0.6)
    fig.tight_layout()
    return fig


def _summary(audit: dict, reports: list[dict], sources: dict[str, str]) -> dict:
    jobs = {}
    audited_jobs = audit["gates"]["formal_analyses"]["evidence"]["jobs"]
    for (scene, observable), report in zip(JOBS, reports):
        key = f"{scene}/{observable}"
        jobs[key] = {
            "audit_status": audited_jobs[key]["status"],
            "scientific_claim_status": audited_jobs[key]["scientific_claim_status"],
            "failed_negative_control_folds": audited_jobs[key]["failed_folds"],
            "folds": [{
                "held_out_realization": fold["held_out_realization"],
                "metric_weighted_r2": fold["metric_weighted_r2"],
                "deranged_control_metric_weighted_r2":
                    fold["realization_derangement_negative_control_metric_weighted_r2"],
                "negative_control_margin": fold["negative_control_margin"],
                "negative_control_pass": fold["negative_control_pass"],
                "selection_adjusted_periodic_excluded_empirical_p_value":
                    fold["selection_adjusted_periodic_excluded_empirical_p_value"],
                "rank": fold["rank"],
                "sample_lag": fold["sample_lag"],
                "lr_state_coefficient_r2": fold["lr_state_coefficient_r2"],
            } for fold in report["folds"]],
        }
    return {
        "schema_id": "oid-phase4-compact-summary/v2",
        "scope": "descriptive audit-bound presentation; no new scientific inference",
        "overall": audit["overall"],
        "formal_complete": audit["gates"]["formal_complete"],
        "negative_control_rule": "observed metric_weighted_r2 > deranged control + 0.01 in every fold",
        "promoted_jobs": audit["gates"]["formal_analyses"]["evidence"]["promoted_jobs"],
        "not_promoted_jobs": audit["gates"]["formal_analyses"]["evidence"]["not_promoted_jobs"],
        "field_mode_plots": {
            "included": False,
            "reason": "audit does not promote causal, universal, or mode-unique interpretation",
        },
        "source_sha256": sources,
        "jobs": jobs,
    }


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--output", type=Path, default=RUN_ROOT / "summary")
    args = parser.parse_args()
    output = args.output.resolve()
    if output.exists():
        raise FileExistsError(f"refusing to clobber existing summary: {output}")
    output.parent.mkdir(parents=True, exist_ok=True)
    temporary = output.with_name(f".{output.name}.tmp-{os.getpid()}")
    temporary.mkdir()
    try:
        audit_path, report_paths = _sources()
        audit, reports, sources = _verify(audit_path, report_paths)
        binding = " | ".join(f"sha256:{digest} {path}" for path, digest in sources.items())
        _style()
        figures = (_plot_primary(reports), _plot_selection(reports), _plot_reconstruction(reports))
        for figure, stem in zip(figures, FIGURES):
            _save(figure, temporary, stem, binding)
        summary = _summary(audit, reports, sources)
        summary["figure_artifacts"] = {
            path.name: {"sha256": _sha256(path), "evidence_binding": sources}
            for path in sorted(temporary.iterdir())
        }
        (temporary / "phase4_summary.json").write_text(
            json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8"
        )
        if output.exists():
            raise FileExistsError(f"refusing to clobber existing summary: {output}")
        temporary.rename(output)
    except Exception:
        shutil.rmtree(temporary, ignore_errors=True)
        raise
    print(json.dumps({"output": str(output), "files": sorted(path.name for path in output.iterdir())}, indent=2))
    return 0


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