#!/usr/bin/env python3
"""Make a browsable, self-contained results tree without moving original outputs."""
from __future__ import annotations

import csv
import html
import json
import os
import re
import shutil
from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np

PROJECT = Path(__file__).resolve().parents[1]
RESULTS = PROJECT / "results"
COMBINED = RESULTS / "ME1a_DT_comparison"
IND = RESULTS / "independent"
TOGETHER = RESULTS / "together"
ME_ROOT = PROJECT / "experiments/ME1a_DT"
RUNS = (("AC28_16h", "AC28", PROJECT, "16h"),
        ("AC28_18h01", "AC28", PROJECT, "18h01"),
        ("ME1a_14h06", "ME1a_DT", ME_ROOT, "14h06"))


def read_json(path: Path) -> dict:
    return json.loads(path.read_text(encoding="utf-8"))


def copy_dir(src: Path, dst: Path) -> None:
    if not src.is_dir():
        raise FileNotFoundError(src)
    shutil.copytree(src, dst, dirs_exist_ok=True,
                    ignore=shutil.ignore_patterns("__pycache__", ".DS_Store"))


def copy_file(src: Path, dst: Path) -> None:
    dst.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(src, dst)


def href(page: Path, target: Path) -> str:
    return os.path.relpath(target, page.parent).replace(os.sep, "/")


def a(page: Path, target: Path, label: str) -> str:
    return f'<a href="{html.escape(href(page, target), quote=True)}">{html.escape(label)}</a>'


def page(path: Path, title: str, intro: str, body: str, visual: Path | None = None) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    nav = a(path, RESULTS / "index.html", "All results")
    image = f'<img class="overview" src="{html.escape(href(path, visual), quote=True)}" alt="{html.escape(title)} overview">' if visual else ""
    document = f'''<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{html.escape(title)}</title>
<style>body{{font:16px/1.5 system-ui,sans-serif;background:#f4f7fb;color:#1c2a39;max-width:1420px;margin:auto;padding:26px}}a{{color:#145e93}}h1,h2{{line-height:1.2}}.lead{{font-size:18px}}.warning{{background:#fff0cc;border-left:5px solid #bf8a1d;padding:14px}}section{{background:white;border:1px solid #dbe4eb;border-radius:12px;margin:18px 0;padding:20px}}.cards{{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px}}.card{{background:#edf3f8;border-radius:9px;padding:15px}}.card strong{{font-size:19px}}.score{{font-size:30px;font-weight:750;color:#164e73}}.scroll{{overflow:auto}}table{{border-collapse:collapse;width:100%;min-width:700px}}th,td{{border-bottom:1px solid #dce4eb;padding:9px;text-align:right;white-space:nowrap}}th:first-child,td:first-child{{text-align:left}}img.overview{{width:100%;height:auto;border:1px solid #dae3ec;border-radius:8px}}.links a{{display:inline-block;margin:4px 13px 4px 0}}small{{color:#536576}}@media(max-width:700px){{body{{padding:12px}}}}</style>
<p>{nav}</p><h1>{html.escape(title)}</h1><p class="lead">{html.escape(intro)}</p>{body}{image}</html>'''
    path.write_text(document, encoding="utf-8")


def score_rows(report: dict, run: str, setup: str) -> dict:
    return next(row for row in report["rows"] if row["budget"] == 12000
                and row["kind"] == "residual_geo" and row["eval_run"] == run
                and row["setup"] == setup)


def plot_field(path: Path, pos: np.ndarray, truth: np.ndarray, field: np.ndarray,
               title: str, ap: float, iou: float) -> None:
    fig, axes = plt.subplots(1, 2, figsize=(12.5, 4.2), constrained_layout=True)
    for ax in axes:
        ax.set_aspect("equal"); ax.set_xlabel("CAD X (mm)"); ax.set_ylabel("CAD Z (mm)")
        ax.grid(alpha=.13)
    axes[0].scatter(pos[:, 0], pos[:, 2], c="#d4dae1", s=2, rasterized=True)
    axes[0].scatter(pos[truth, 0], pos[truth, 2], c="#d21d3b", s=22, rasterized=True)
    axes[0].set_title(f"Provisional weak target: {int(truth.sum())} nodes")
    axes[1].scatter(pos[:, 0], pos[:, 2], c=field, cmap="magma", vmin=0, vmax=1,
                    s=3, rasterized=True)
    axes[1].scatter(pos[truth, 0], pos[truth, 2], facecolors="none", edgecolors="#2bcae0",
                    linewidths=.55, s=19, rasterized=True)
    axes[1].set_title(f"GNN risk probability: AP {ap:.3f}, IoU {iou:.3f}")
    fig.suptitle(title + " · training-mask diagnostic", fontsize=15)
    fig.savefig(path, dpi=170); plt.close(fig)


def part_model_overview(path: Path, baseline: dict, residual: dict, part: str) -> None:
    rows = [(name, v["average_precision"], v["threshold_iou"]) for name, v in baseline["models"].items()]
    rows.append(("Residual Geo GNN (individual)", residual["mapped_ap"], residual["mapped_iou"]))
    rows.sort(key=lambda row: row[1])
    fig, axes = plt.subplots(1, 2, figsize=(13, 5.5), constrained_layout=True)
    for ax, index, title in zip(axes, (1, 2), ("Average precision", "IoU at 0.5")):
        ordered = sorted(rows, key=lambda row: row[index])
        colors = ["#196f9b" if "Residual" in r[0] else "#cb6b2d" if r[0] == "Risk GNN" else "#96a6b5" for r in ordered]
        ax.barh(range(len(ordered)), [r[index] for r in ordered], color=colors)
        ax.set_yticks(range(len(ordered)), [r[0] for r in ordered], fontsize=8)
        ax.set_title(title); ax.grid(axis="x", alpha=.2)
    fig.suptitle(f"{part}: same-run weak-label risk scores; not held-out accuracy", fontsize=14)
    fig.savefig(path, dpi=165); plt.close(fig)


def write_result_folder(dst: Path, run_name: str, root: Path, run: str, row: dict,
                        field: np.ndarray, scope: str) -> None:
    dst.mkdir(parents=True, exist_ok=True)
    with np.load(root / f"data_processed/paired/{run}_cad_ply_targets.npz", allow_pickle=False) as raw:
        pos = np.asarray(raw["cad_pos_mm"], np.float32)
        truth = np.asarray(raw["weak_defect_mask"][:, 1], bool)
    if len(field) != len(pos): raise ValueError(f"{run_name}: prediction length mismatch")
    np.savez_compressed(dst / "probability.npz", cad_pos_mm=pos,
                        weak_target=truth, risk_probability=field.astype(np.float32))
    copy_file(PROJECT / row["checkpoint"], dst / "checkpoint.pt")
    metrics = {"run": run_name, "scope": scope, "model": "Residual Geo GNN",
               "requested_graph_nodes": 12000, "actual_graph_nodes": row["actual_nodes"],
               "epochs": 180, "training_runs": row["train_runs"].split("+"),
               "weak_label_only": True, "production_valid": False,
               "average_precision": row["mapped_ap"], "iou_at_0_5": row["mapped_iou"],
               "dice_at_0_5": row["mapped_dice"],
               "weak_positive_nodes": int(truth.sum()),
               "source_checkpoint": row["checkpoint"]}
    (dst / "report.json").write_text(json.dumps(metrics, indent=2), encoding="utf-8")
    plot_field(dst / "prediction.png", pos, truth, field, f"{run_name} · {scope}",
               row["mapped_ap"], row["mapped_iou"])
    index = dst / "index.html"
    warning = '<p class="warning">This is fit to a provisional CSV-seeded mask on the same physical run. Registration and defect extent are unvalidated. These numbers are not held-out production accuracy.</p>'
    body = (f'<section><div class="cards"><div class="card"><strong>Average precision</strong><div class="score">{row["mapped_ap"]:.3f}</div></div>'
            f'<div class="card"><strong>IoU at 0.5</strong><div class="score">{row["mapped_iou"]:.3f}</div></div>'
            f'<div class="card"><strong>Dice at 0.5</strong><div class="score">{row["mapped_dice"]:.3f}</div></div></div></section>'
            + warning + f'<section><h2>Saved files</h2><p class="links">{a(index,dst/"prediction.png","Prediction PNG")} '
            + f'{a(index,dst/"probability.npz","Per-node probabilities NPZ")} '
            + f'{a(index,dst/"checkpoint.pt","Trained checkpoint PT")} '
            + f'{a(index,dst/"report.json","Metrics JSON")}</p></section>')
    page(index, f"{run_name}: individual residual GNN" if scope == "individual" else
         f"{run_name}: jointly trained residual GNN",
         f"12,000 requested graph nodes; {row['actual_nodes']:,} actual nodes; {scope} weak-label fit.",
         body, dst / "prediction.png")


def patch_copied_html(path: Path, replacements: dict[str, str]) -> None:
    if not path.exists(): return
    content = path.read_text(encoding="utf-8")
    for before, after in replacements.items(): content = content.replace(before, after)
    path.write_text(content, encoding="utf-8")


def package_originals() -> None:
    for part, root in (("AC28", PROJECT), ("ME1a_DT", ME_ROOT)):
        base = IND / part
        copy_dir(root / "results/models", base / "models/original")
        copy_dir(root / "data_processed", base / "preprocessing/data_processed")
        for name in (("graphs", "brep", "paired", "ply_audit", "representations") if part == "AC28"
                     else ("graphs", "brep", "inspection", "representations")):
            source = root / "results" / name
            if source.is_dir(): copy_dir(source, base / "preprocessing/results" / name)
        for name in ("outputs", "processed_inputs", "feature_attributions"):
            source = root / "gen_models" / name
            if source.is_dir(): copy_dir(source, base / "diffusion/original" / name)
        if part == "AC28":
            for p in (base / "models/original").rglob("*.html"):
                patch_copied_html(p, {"../../../data_processed": "../../../preprocessing/data_processed"})
            dashboard = base / "models/original/dashboard/index.html"
            patch_copied_html(dashboard, {
                "../../../results/representations": href(dashboard, base / "preprocessing/results/representations"),
                "../../../results/graphs": href(dashboard, base / "preprocessing/results/graphs"),
                "../../../results/models/weak_gnn": href(dashboard, base / "models/original/weak_gnn"),
            })
        else:
            patch_copied_html(base / "models/original/cad_predictions/index.html",
                              {"../../../../../results/ME1a_DT_comparison/index.html": "../../../index.html"})
        viewer = base / "preprocessing/results/graphs/viewer.html"
        if viewer.exists():
            relative = os.path.relpath(PROJECT / "datas_2/viewer/vendor", viewer.parent)
            content = viewer.read_text(encoding="utf-8")
            content = re.sub(r"(?:\.\./)+datas_2/viewer/vendor", relative, content)
            viewer.write_text(content, encoding="utf-8")


def build_individual(hard: dict, fields: dict) -> None:
    for part, root, baseline_path in (("AC28", PROJECT, PROJECT / "results/models/cad_predictions/report.json"),
                                      ("ME1a_DT", ME_ROOT, ME_ROOT / "results/models/cad_predictions/report.json")):
        base = IND / part
        baseline = read_json(baseline_path)
        model_index = base / "models/index.html"
        residual_folder = base / "models/residual_gnn_12000"
        run_rows = []
        for name, p, item_root, run in RUNS:
            if p != part: continue
            row = score_rows(hard, name, f"single_{name}")
            dst = residual_folder / run
            write_result_folder(dst, name, item_root, run, row,
                                np.asarray(fields[f"{name}_single_{name}"], np.float32), "individual")
            run_rows.append((run, row, dst / "index.html"))
        residual_index = residual_folder / "index.html"
        rtable = "".join(f'<tr><th>{a(residual_index,p,f"{part} {run}")}</th><td>{row["mapped_ap"]:.3f}</td>'
                         f'<td>{row["mapped_iou"]:.3f}</td><td>{row["actual_nodes"]:,}</td></tr>'
                         for run,row,p in run_rows)
        page(residual_index, f"{part}: individual residual GNN runs",
             "Each run has its own prediction PNG, per-node probabilities, JSON metrics and trained checkpoint.",
             f'<section><div class="scroll"><table><thead><tr><th>Run</th><th>AP</th><th>IoU</th><th>Actual nodes</th></tr></thead><tbody>{rtable}</tbody></table></div></section>',
             run_rows[-1][2].parent / "prediction.png")
        top_run = next((x for x in run_rows if x[0] == "14h06"), run_rows[-1])
        part_model_overview(base / "models/overview.png", baseline, top_run[1], part)
        rows = []
        for name, metrics in baseline["models"].items():
            image = base / "models/original/cad_predictions" / metrics["image"]
            rows.append(f'<tr><th>{html.escape(name)}</th><td>{metrics["average_precision"]:.3f}</td>'
                        f'<td>{metrics["threshold_iou"]:.3f}</td><td>{a(model_index,image,"Prediction PNG")}</td></tr>')
        rows.append(f'<tr><th>Residual Geo GNN (individual, {top_run[0]})</th><td>{top_run[1]["mapped_ap"]:.3f}</td>'
                    f'<td>{top_run[1]["mapped_iou"]:.3f}</td><td>{a(model_index,top_run[2],"Full result")}</td></tr>')
        body = (f'<section><p class="links">{a(model_index,base/"models/original/cad_predictions/index.html","Original ten-model gallery")} '
                f'{a(model_index,base/"models/original/weak_gnn/index.html","Risk GNN gallery")} '
                f'{a(model_index,residual_index,"Individual residual GNN runs")} '
                f'{a(model_index,base/"models/original/report.json","Architecture checks JSON")}</p></section>'
                f'<section><h2>Saved model scores for {part}</h2><p>All numbers fit provisional labels on their training runs. '
                f'The residual GNN result has its own checkpoint and prediction file.</p><div class="scroll"><table>'
                f'<thead><tr><th>Model</th><th>AP</th><th>IoU</th><th>Open</th></tr></thead><tbody>{"".join(rows)}</tbody></table></div></section>')
        page(model_index, f"{part}: independent model results",
             "Original model galleries and every separately trained residual GNN result are collected here.",
             body, base / "models/overview.png")
        diffusion_index = base / "diffusion/index.html"
        diffusion_outputs = base / "diffusion/original/outputs"
        preview = diffusion_outputs / ("18h01_four_variant_comparison.png" if part == "AC28"
                                       else "14h06_four_variant_comparison.png")
        page(diffusion_index, f"{part}: conditional diffusion results",
             "Four representation-specific diffusion variants and the TAG Graph U-Net; weak-label diagnostics.",
             f'<section><p class="links">{a(diffusion_index,diffusion_outputs/"index.html","Full diffusion gallery")} '
             f'{a(diffusion_index,diffusion_outputs/"report.json","Five-variant JSON report")} '
             f'{a(diffusion_index,base/"diffusion/original/processed_inputs","Processed inputs")}</p></section>',
             preview)
        prep_index = base / "preprocessing/index.html"
        scan_dir = base / "preprocessing/results/inspection"
        preview = (scan_dir / "csv_defects_on_merged_image.png" if part == "ME1a_DT"
                   else base / "preprocessing/results/graphs/comparison.png")
        graph_viewer = base / "preprocessing/results/graphs/viewer.html"
        prep_body = (f'<section><p class="links">{a(prep_index,base/"preprocessing/data_processed/index.html","All prepared representations")} '
                     f'{a(prep_index,graph_viewer,"Interactive graph viewer")} '
                     f'{a(prep_index,base/"preprocessing/results/graphs/comparison.png","Graph resolution PNG")}</p></section>')
        if part == "ME1a_DT":
            prep_body += (f'<section><h2>Scan and annotation inspection</h2><p class="links">'
                          f'{a(prep_index,scan_dir/"csv_defects_on_merged_image.png","CSV marks PNG")} '
                          f'{a(prep_index,scan_dir/"ply_components.png","PLY components PNG")} '
                          f'{a(prep_index,scan_dir/"alignment_targets.png","Registration PNG")}</p></section>')
        page(prep_index, f"{part}: preprocessing and inspection",
             "Prepared CAD representations, graph nodes and source inspection images are here.",
             prep_body, preview)
        part_index = base / "index.html"
        body = (f'<section class="cards"><div class="card"><strong>Models</strong><p>Original ten families, Risk GNN and individual residual GNN checkpoints.</p>{a(part_index,model_index,"Open models")}</div>'
                f'<div class="card"><strong>Preprocessing</strong><p>CAD graph, image, point, voxel and visual inspection results.</p>{a(part_index,prep_index,"Open preprocessing")}</div>'
                f'<div class="card"><strong>Diffusion</strong><p>Variant metrics, generated fields and PNG comparisons.</p>{a(part_index,diffusion_index,"Open diffusion")}</div></section>')
        if part == "ME1a_DT":
            item = top_run
            body += (f'<section><h2>Requested result: ME1a individual residual GNN</h2>'
                     f'<div class="score">AP {item[1]["mapped_ap"]:.3f} · IoU {item[1]["mapped_iou"]:.3f}</div>'
                     f'<p>{a(part_index,item[2],"Prediction PNG, checkpoint and metrics")}</p></section>')
        page(part_index, f"{part}: independent results", "One part's saved preprocessing, model and diffusion outputs.",
             body, base / "models/overview.png")
    independent_index = IND / "index.html"
    body = '<section class="cards">' + ''.join(
        f'<div class="card"><strong>{part}</strong><p>Independent preprocessing, model training and diffusion results.</p>'
        f'{a(independent_index,IND/part/"index.html","Open part results")}</div>'
        for part in ("AC28", "ME1a_DT")) + '</section>'
    page(independent_index, "Independent part results",
         "AC28 and ME1a outputs are separated by physical part and training run.", body)


def build_together(hard: dict, fields: dict) -> None:
    models = TOGETHER / "models"
    copy_dir(COMBINED / "gnn_joint", models / "graph_search")
    copy_dir(COMBINED / "gnn_joint_hard", models / "optimized_experiment")
    copy_dir(COMBINED / "model_ranking", models / "comparison")
    # The copied comparison page keeps its image and tables, but points to the
    # organized galleries rather than paths based on its former location.
    comparison = models / "comparison/index.html"
    patch_copied_html(comparison, {
        "../gnn_joint_hard/index.html": "../optimized_experiment/index.html",
        "../../models/cad_predictions/index.html": href(comparison, IND / "AC28/models/original/cad_predictions/index.html"),
        "../../../experiments/ME1a_DT/results/models/cad_predictions/index.html":
            href(comparison, IND / "ME1a_DT/models/original/cad_predictions/index.html")})
    residual = models / "residual_gnn_12000"
    residual.mkdir(parents=True, exist_ok=True)
    joint_checkpoint = score_rows(hard, "ME1a_14h06", "joint")["checkpoint"]
    copy_file(PROJECT / joint_checkpoint, residual / "checkpoint.pt")
    copy_file(COMBINED / "gnn_joint_hard/prediction_maps.png", residual / "prediction.png")
    np.savez_compressed(residual / "probabilities.npz",
                        **{f"{name}_joint": np.asarray(fields[f"{name}_joint"], np.float32)
                           for name, _, _, _ in RUNS})
    joint_rows=[]
    for name, part, root, run in RUNS:
        row=score_rows(hard,name,"joint")
        folder=residual/name
        write_result_folder(folder,name,root,run,row,
                            np.asarray(fields[f"{name}_joint"],np.float32),"joint")
        joint_rows.append((name,row,folder/"index.html"))
    with (residual / "report.csv").open("w",newline="",encoding="utf-8") as fh:
        writer=csv.writer(fh); writer.writerow(("run","train_runs","AP","IoU","Dice","actual_nodes"))
        writer.writerows((name,row["train_runs"],row["mapped_ap"],row["mapped_iou"],
                          row["mapped_dice"],row["actual_nodes"]) for name,row,_ in joint_rows)
    (residual / "report.json").write_text(json.dumps({"status":"joint_weak_label_fit",
        "training_runs":[item[0] for item in RUNS],"epochs":180,"production_valid":False,
        "source_report":"results/ME1a_DT_comparison/gnn_joint_hard/report.json",
        "rows":[dict(row) for _,row,_ in joint_rows]},indent=2),encoding="utf-8")
    residual_index=residual/"index.html"
    table="".join(f'<tr><th>{a(residual_index,target,name)}</th><td>{row["mapped_ap"]:.3f}</td>'
                  f'<td>{row["mapped_iou"]:.3f}</td><td>{row["actual_nodes"]:,}</td></tr>'
                  for name,row,target in joint_rows)
    page(residual_index,"AC28 + ME1a: jointly trained residual GNN",
         "One GNN checkpoint trained on two AC28 runs and one ME1a run. Scores are on those same weak-label runs.",
         f'<section><div class="scroll"><table><thead><tr><th>Run</th><th>AP</th><th>IoU</th><th>Nodes</th></tr></thead><tbody>{table}</tbody></table></div>'
         f'<p class="links">{a(residual_index,residual/"checkpoint.pt","Shared checkpoint PT")} '
         f'{a(residual_index,residual/"probabilities.npz","Three prediction arrays NPZ")} '
         f'{a(residual_index,residual/"report.csv","Metrics CSV")} '
         f'{a(residual_index,residual/"prediction.png","All prediction maps PNG")}</p></section>',
         residual/"prediction.png")
    held = models / "heldout"
    held.mkdir(parents=True,exist_ok=True)
    copy_file(COMBINED/"gnn_joint_hard/prediction_maps.png",held/"prediction.png")
    held_rows=[]
    for name,part,root,run in RUNS:
        row=score_rows(hard,name,f"holdout_{name}")
        copy_file(PROJECT/row["checkpoint"],held/f"{name}_checkpoint.pt")
        held_rows.append((name,row))
    (held/"report.json").write_text(json.dumps({"production_valid":False,"rows":[r for _,r in held_rows]},indent=2),encoding="utf-8")
    held_index=held/"index.html"
    table="".join(f'<tr><th>{name}</th><td>{r["mapped_ap"]:.4f}</td><td>{r["mapped_iou"]:.3f}</td>'
                  f'<td>{a(held_index,held/f"{name}_checkpoint.pt","Checkpoint")}</td></tr>' for name,r in held_rows)
    page(held_index,"Held-out run GNN results",
         "One run excluded from each fit. All held-out IoU values were zero; labels remain unvalidated.",
         f'<section><div class="scroll"><table><thead><tr><th>Held-out run</th><th>AP</th><th>IoU</th><th>Model</th></tr></thead><tbody>{table}</tbody></table></div>'
         f'<p>{a(held_index,held/"report.json","Metrics JSON")}</p></section>',held/"prediction.png")
    model_index=models/"index.html"
    cards = (
        ("Joint residual GNN", residual_index, "Shared checkpoint and a prediction PNG for each run."),
        ("Held-out runs", held_index, "Cross-run AP, IoU and checkpoints."),
        ("Graph and node search", models/"graph_search/index.html", "3k, 5k, 6k, 12k; Mesh GNN, residual GNN and MLP control."),
        ("Optimized experiment", models/"optimized_experiment/index.html", "All 12k individual, joint and held-out fits."),
        ("All-model comparison", comparison, "Risk GNN versus CNN, FNO, point, voxel and residual models."))
    body='<section class="cards">'+''.join(f'<div class="card"><strong>{title}</strong><p>{description}</p>{a(model_index,target,"Open results")}</div>'
                                         for title,target,description in cards)+'</section>'
    body += (f'<section><h2>Joint-fit ME1a result</h2><div class="score">AP {score_rows(hard,"ME1a_14h06","joint")["mapped_ap"]:.3f}'
             f' · IoU {score_rows(hard,"ME1a_14h06","joint")["mapped_iou"]:.3f}</div>'
             f'<p>The individual ME1a AP 0.629 result is in {a(model_index,IND/"ME1a_DT/models/residual_gnn_12000/14h06/index.html","Independent ME1a results")}.</p></section>')
    page(model_index,"Together: AC28 + ME1a model results",
         "Joint training, individual-versus-joint comparison, graph-size search and held-out diagnostics.",
         body, residual/"prediction.png")
    graph = TOGETHER / "graph_node_study"
    copy_dir(COMBINED/"graph_fit",graph/"graph_fit")
    for filename in ("representation_sensitivity.csv","representation_sensitivity.json",
                     "resolution_views.png","sensitivity_curves.png"):
        copy_file(COMBINED/filename,graph/filename)
    graph_index=graph/"index.html"
    page(graph_index,"Together: graph node and representation study",
         "AC28 and ME1a representation fidelity across eight graph budgets, with 16 weak training fits.",
         f'<section><p class="links">{a(graph_index,graph/"representation_sensitivity.csv","Representation CSV")} '
         f'{a(graph_index,graph/"graph_fit/report.csv","Per-size GNN fit CSV")} '
         f'{a(graph_index,graph/"resolution_views.png","Resolution views PNG")} '
         f'{a(graph_index,graph/"sensitivity_curves.png","Sensitivity curves PNG")}</p></section>',
         graph/"sensitivity_curves.png")
    together_index=TOGETHER/"index.html"
    body='<section class="cards">'+''.join(
        f'<div class="card"><strong>{title}</strong><p>{description}</p>{a(together_index,target,"Open results")}</div>'
        for title,target,description in (
            ("Models",model_index,"Joint residual GNN, held-out runs and all-model ranking."),
            ("Graph node study",graph_index,"Node sizes, shape fidelity and label transfer."),
            ("Full AC28 + ME1a report",COMBINED/"index.html","All preprocessing, models and visual inspections.")))+'</section>'
    page(together_index,"Together: AC28 + ME1a results",
         "Experiments that compare or train with both parts are collected here.",body,
         graph/"sensitivity_curves.png")


def build_top() -> None:
    index=RESULTS/"index.html"
    cards=(("Independent AC28",IND/"AC28/index.html","All AC28 models, preprocessing and diffusion."),
           ("Independent ME1a_DT",IND/"ME1a_DT/index.html","ME1a baseline models and the individual residual GNN AP 0.629 result."),
           ("Together",TOGETHER/"index.html","Joint model training, held-out runs, graph sizes and cross-model comparison."))
    body='<section class="cards">'+''.join(
        f'<div class="card"><strong>{html.escape(title)}</strong><p>{html.escape(description)}</p>{a(index,target,"Open folder")}</div>'
        for title,target,description in cards)+'</section>'
    body += (f'<section><h2>Find the 0.629 ME1a result</h2><div class="score">AP 0.629 · IoU 0.476</div>'
             f'<p>{a(index,IND/"ME1a_DT/models/residual_gnn_12000/14h06/index.html","Individual ME1a residual GNN: prediction PNG, checkpoint and metrics")}</p></section>'
             f'<section><h2>Experiment folders</h2><p>{a(index,PROJECT/"experiments/index.html","AC28, ME1a and together results inside experiments")} · '
             f'{a(index,PROJECT/"experiments/preprocess_size_desicisions/index.html","Preprocessing size decisions")}</p></section>'
             f'<section><h2>Existing original outputs</h2><p>The source folders remain in place for compatibility. '
             f'{a(index,COMBINED/"index.html","Previous combined visual report")} · '
             f'{a(index,RESULTS/"models/cad_predictions/index.html","Original AC28 model gallery")} · '
             f'{a(index,ME_ROOT/"results/models/cad_predictions/index.html","Original ME1a model gallery")}</p></section>')
    page(index,"All AC28 and ME1a results",
         "Open independent part results or the experiments that use both parts. Every result page also has a PNG export.",body)
    (RESULTS/"INDEX.md").write_text("""# Organized results

- [Open all results](index.html) or [PNG of the index](page.png).
- [AC28 independent results](independent/AC28/index.html): preprocessing, all ten original models, Risk GNN, individual residual GNN, and diffusion.
- [ME1a independent results](independent/ME1a_DT/index.html): preprocessing, all ten original models, Risk GNN, individual residual GNN, and diffusion.
- [ME1a individual residual GNN AP 0.629](independent/ME1a_DT/models/residual_gnn_12000/14h06/index.html): `prediction.png`, `probability.npz`, `checkpoint.pt`, and `report.json`.
- [Together results](together/index.html): joint GNN checkpoint, per-run predictions, held-out tests, graph-size search, and all-model ranking.
- [Experiment folders](../experiments/index.html): mirrored individual and together models, with reports and PNGs.
- [Preprocessing size decisions](../experiments/preprocess_size_desicisions/index.html): image, graph, voxel and point comparisons, fixed input contract, scored selection and PNGs.

The original output paths remain available. All AP and IoU values are diagnostics on provisional weak labels, not validated production accuracy.
""",encoding="utf-8")


def main() -> None:
    hard=read_json(COMBINED/"gnn_joint_hard/report.json")
    with np.load(COMBINED/"gnn_joint_hard/prediction_maps.npz",allow_pickle=False) as d:
        fields={key:np.asarray(d[key],np.float32) for key in d.files}
    package_originals()
    build_individual(hard,fields)
    build_together(hard,fields)
    build_top()
    print(RESULTS/"index.html")


if __name__=="__main__":main()
