"""Generate local, sample-level input-response diagnostics for prepared models.

The artifacts in this module are deliberately *not* feature importance claims.
They are absolute input-times-gradient responses of an unvalidated model's mean
raw output at one prepared sample (run ``18h01`` where a run is applicable).
They answer which inputs the executed network is locally sensitive to, not what
caused a forming defect or what is predictive in a new production run.
"""
from __future__ import annotations

import argparse
import html
import json
from pathlib import Path
from typing import Any, Callable

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib import font_manager
import numpy as np
import torch
from torch import Tensor, nn

from .architectures import (CNN2DUNet, DGCNNSeg, FNO2d, MeshGraphProcessor,
                            PhysicsConstrainedRiskGNN, PointNetSeg,
                            SparseUNetOptional, TabularSurrogate, UNet3D, UVNetFaceModel)
from .data import (ROOT, brep_input, graph_inputs, image_input, point_inputs,
                   process_conditions, sparse_batch_collate, sparse_voxel_input,
                   tabular_input, voxel_input)
from .train_weak_gnn import load_training_data

OUT = ROOT / "results/models/attributions"
RUN = "18h01"
METHOD = "absolute input-times-gradient of mean raw model output"
WARNING = ("Local response diagnostic only. production_valid=false; no causal, "
           "validated-prediction, or cross-sample feature-importance claim.")

# Preserve prepared Korean XLS labels in local PNGs when the macOS font exists;
# fall back to Matplotlib's normal platform font selection elsewhere.
_KOREAN_FONT = Path("/System/Library/Fonts/Supplemental/AppleGothic.ttf")
if _KOREAN_FONT.exists():
    font_manager.fontManager.addfont(str(_KOREAN_FONT))
    plt.rcParams["font.family"] = "AppleGothic"
    plt.rcParams["axes.unicode_minus"] = False


def _tensor(value: np.ndarray, dtype: torch.dtype = torch.float32) -> Tensor:
    return torch.as_tensor(value, dtype=dtype)


def _load(model: nn.Module, checkpoint: str | None) -> str:
    """Load only an existing diagnostic checkpoint, retaining its provenance."""
    if checkpoint is None:
        return "random_initialization"
    path = ROOT / checkpoint
    if not path.exists():
        return f"missing_checkpoint:{checkpoint}"
    payload = torch.load(path, map_location="cpu", weights_only=False)
    model.load_state_dict(payload["state_dict"])
    return checkpoint


def _preferred_checkpoint(slug: str, fallback: str) -> str:
    """Use the exact state behind the displayed 18h01 CAD diagnostic when present."""
    candidate = f"results/models/cad_predictions/checkpoints/18h01-{slug}.pt"
    return candidate if (ROOT / candidate).exists() else fallback


def _grad_x_input(model: nn.Module, forward: Callable[..., Tensor], *inputs: Tensor) -> list[np.ndarray]:
    model.eval()
    leaves = [value.detach().clone().requires_grad_(True) for value in inputs]
    model.zero_grad(set_to_none=True)
    value = forward(*leaves)
    if value.numel() == 0 or not torch.isfinite(value).all():
        raise RuntimeError("attribution output is empty or non-finite")
    value.mean().backward()
    result = []
    for leaf in leaves:
        if leaf.grad is None or not torch.isfinite(leaf.grad).all():
            raise RuntimeError("attribution gradient is missing or non-finite")
        result.append((leaf * leaf.grad).abs().detach().cpu().numpy().astype(np.float32))
    return result


def _names_with_masks(names: list[str] | np.ndarray) -> list[str]:
    names = [str(name) for name in names]
    return names + ["valid_mask: " + name for name in names]


def _top(values: np.ndarray, names: list[str], limit: int = 18) -> list[dict[str, Any]]:
    values = np.asarray(values, float).reshape(-1)
    index = np.argsort(values)[::-1][:min(limit, len(values))]
    return [{"feature": names[int(i)], "response": float(values[int(i)])} for i in index]


def _plot_bar(path: Path, values: np.ndarray, names: list[str], title: str, limit: int = 18) -> None:
    picks = _top(values, names, limit)
    labels = [row["feature"] for row in picks][::-1]
    score = [row["response"] for row in picks][::-1]
    fig, ax = plt.subplots(figsize=(9, max(3.6, .34 * len(labels) + 1.2)), constrained_layout=True)
    ax.barh(np.arange(len(labels)), score, color="#276fbf")
    ax.set_yticks(np.arange(len(labels)), labels, fontsize=8)
    ax.set_xlabel("absolute input × gradient")
    ax.set_title(title)
    ax.grid(axis="x", alpha=.25)
    fig.savefig(path, dpi=150)
    plt.close(fig)


def _plot_map(path: Path, spatial: np.ndarray, title: str) -> None:
    fig, ax = plt.subplots(figsize=(7, 4.5), constrained_layout=True)
    image = ax.imshow(spatial, cmap="magma")
    ax.set_title(title); ax.set_axis_off()
    fig.colorbar(image, ax=ax, label="mean absolute input × gradient")
    fig.savefig(path, dpi=150)
    plt.close(fig)


def _plot_volume(path: Path, values: np.ndarray, title: str) -> None:
    # Max projections preserve a visible response for the shallow 32×4×16 grid.
    projections = [values.max(axis=i) for i in range(3)]
    fig, axes = plt.subplots(1, 3, figsize=(11, 3.5), constrained_layout=True)
    for axis, value, label in zip(axes, projections, ("max over X", "max over Y", "max over Z")):
        image = axis.imshow(value, cmap="magma", aspect="auto")
        axis.set_title(label); axis.set_axis_off(); fig.colorbar(image, ax=axis, fraction=.05)
    fig.suptitle(title)
    fig.savefig(path, dpi=150)
    plt.close(fig)


def _plot_surface(path: Path, pos: np.ndarray, value: np.ndarray, title: str) -> None:
    # A fixed decimation makes the diagnostic quick without changing saved arrays.
    step = max(1, len(pos) // 12000)
    fig = plt.figure(figsize=(7, 5), constrained_layout=True)
    ax = fig.add_subplot(projection="3d")
    points = ax.scatter(pos[::step, 0], pos[::step, 1], pos[::step, 2], c=value[::step],
                        s=1.5, cmap="magma")
    ax.set_title(title); ax.set_axis_off(); fig.colorbar(points, ax=ax, shrink=.65,
                                                          label="mean absolute input × gradient")
    fig.savefig(path, dpi=150)
    plt.close(fig)


def _record(name: str, checkpoint: str, input_source: str, output_selector: str,
            feature_names: list[str], feature_values: np.ndarray, *, spatial_npz: str | None = None,
            map_file: str | None = None, process: dict[str, Any] | None = None,
            note: str = "") -> dict[str, Any]:
    displayed = "cad_predictions/checkpoints/18h01-" in checkpoint
    return {
        "model": name, "status": "completed", "method": METHOD,
        "checkpoint": checkpoint, "checkpoint_scope": ("18h01 weak-label diagnostic fit" if displayed
            else "weak-label diagnostic" if "weak_gnn" in checkpoint
            else "two-step synthetic zero-output learnability diagnostic"),
        "matches_displayed_18h01_prediction": displayed,
        "input_source": input_source, "sample": RUN, "output_selector": output_selector,
        "production_valid": False, "generalization_measured": False, "warning": WARNING,
        "feature_response_top": _top(feature_values, feature_names),
        "spatial_npz": spatial_npz, "map_file": map_file, "process_response": process,
        "note": note,
    }


def _process_summary(response: np.ndarray, names: list[str]) -> dict[str, Any]:
    return {"method": METHOD, "feature_response_top": _top(response, names),
            "feature_count": int(len(response))}


def image_models() -> tuple[list[dict[str, Any]], dict[str, np.ndarray]]:
    data = image_input(); process_x, process_mask, run_ids, process_source = process_conditions()
    run = run_ids.index(RUN)
    x_np = np.concatenate([data["x"], data["mask"].astype(np.float32)], 1)
    p_np = np.concatenate([process_x, process_mask.astype(np.float32)], 1)[run:run + 1]
    x = _tensor(x_np); p = _tensor(p_np.repeat(len(x_np), axis=0))
    feature_names = _names_with_masks(np.load(ROOT / data["source"], allow_pickle=False)["feature_names"])
    tabular = np.load(ROOT / process_source, allow_pickle=False)
    process_names = _names_with_masks(list(tabular["process_feature_names"].astype(str)) + list(tabular["geometry_feature_names"].astype(str)))
    out: list[dict[str, Any]] = []; arrays: dict[str, np.ndarray] = {}
    for label, cls, slug, ckpt in (
        ("2D CNN/U-Net", CNN2DUNet, "2d-cnn-u-net", "results/models/checkpoints/synthetic_2d_cnn_unet.pt"),
        ("FNO2d", FNO2d, "fno2d", "results/models/checkpoints/synthetic_fno2d.pt"),
    ):
        model = cls(x.shape[1], global_dim=p.shape[1]); loaded = _load(model, _preferred_checkpoint(slug, ckpt))
        saliency, process_saliency = _grad_x_input(model, lambda a, b: torch.sigmoid(model(a, b)), x, p)
        channel = saliency.mean(axis=(0, 2, 3)); spatial = saliency.mean(axis=(0, 1))
        stem = label.lower().replace("/", "-").replace(" ", "-")
        _plot_bar(OUT / f"{stem}-channels.png", channel, feature_names, f"{label}: CAD image channel response")
        _plot_map(OUT / f"{stem}-map.png", spatial, f"{label}: six-view mean response")
        _plot_bar(OUT / f"{stem}-process.png", process_saliency.mean(0), process_names, f"{label}: XLS/CAD-summary response")
        arrays[stem] = spatial
        np.savez_compressed(OUT / f"{stem}.npz", channel_response=channel, spatial_response=spatial,
                            process_response=process_saliency.mean(0), feature_names=np.asarray(feature_names),
                            process_feature_names=np.asarray(process_names))
        out.append(_record(label, loaded, data["source"], "mean weak-fit predicted probability across six CAD views",
                           feature_names, channel, spatial_npz=f"{stem}.npz", map_file=f"{stem}-map.png",
                           process=_process_summary(process_saliency.mean(0), process_names)))
    return out, arrays


def voxel_model() -> tuple[dict[str, Any], np.ndarray]:
    data = voxel_input(); process_x, process_mask, run_ids, process_source = process_conditions(); run = run_ids.index(RUN)
    x_np = np.concatenate([data["x"], data["mask"].astype(np.float32)], 0)[None]
    p_np = np.concatenate([process_x, process_mask.astype(np.float32)], 1)[run:run + 1]
    x, p = _tensor(x_np), _tensor(p_np)
    model = UNet3D(x.shape[1], global_dim=p.shape[1]); loaded = _load(model, _preferred_checkpoint("dense-3d-u-net", "results/models/checkpoints/synthetic_3d_unet.pt"))
    saliency, process_saliency = _grad_x_input(model, lambda a, b: torch.sigmoid(model(a, b)), x, p)
    channel = saliency.mean(axis=(0, 2, 3, 4)); spatial = saliency.mean(axis=(0, 1))
    names = _names_with_masks(np.load(ROOT / data["source"], allow_pickle=False)["feature_names"])
    raw = np.load(ROOT / process_source, allow_pickle=False)
    process_names = _names_with_masks(list(raw["process_feature_names"].astype(str)) + list(raw["geometry_feature_names"].astype(str)))
    _plot_bar(OUT / "dense-3d-u-net-channels.png", channel, names, "Dense 3D U-Net: voxel-channel response")
    _plot_volume(OUT / "dense-3d-u-net-map.png", spatial, "Dense 3D U-Net: surface-voxel response")
    _plot_bar(OUT / "dense-3d-u-net-process.png", process_saliency.mean(0), process_names, "Dense 3D U-Net: XLS/CAD-summary response")
    np.savez_compressed(OUT / "dense-3d-u-net.npz", channel_response=channel, spatial_response=spatial,
                        process_response=process_saliency.mean(0), feature_names=np.asarray(names), process_feature_names=np.asarray(process_names))
    return _record("Dense 3D U-Net", loaded, data["source"], "mean weak-fit predicted voxel probability", names, channel,
                   spatial_npz="dense-3d-u-net.npz", map_file="dense-3d-u-net-map.png",
                   process=_process_summary(process_saliency.mean(0), process_names),
                   note="Surface-only voxel representation; the map is not a solid material-volume field."), spatial


def sparse_model() -> dict[str, Any]:
    """Run a true sparse attribution only when the optional backend is usable."""
    try:
        import MinkowskiEngine as ME  # type: ignore
    except ImportError as exc:
        return {"model": "Sparse U-Net", "status": "unavailable_optional_backend", "backend": "MinkowskiEngine",
                "production_valid": False, "warning": WARNING, "reason": f"backend import unavailable: {exc}"}
    data = sparse_voxel_input()
    coords_np, features_np = sparse_batch_collate(data["coords"], data["features"])
    coords = _tensor(coords_np, torch.int32); features = _tensor(features_np)
    model = SparseUNetOptional(features.shape[1])
    checkpoint = _preferred_checkpoint("sparse-3d-u-net", "results/models/checkpoints/synthetic_sparse_unet.pt")
    if (ROOT / checkpoint).exists():
        loaded = _load(model, checkpoint)
        scope = "two-step synthetic zero-output learnability diagnostic"
    else:
        # A fixed seed makes this backend smoke reproducible if the standard
        # smoke suite has not yet written its sparse diagnostic checkpoint.
        torch.manual_seed(17)
        model = SparseUNetOptional(features.shape[1])
        loaded = "random_initialization_seed17_no_checkpoint"
        scope = "fixed-seed random initialization; no checkpoint available"
    saliency = _grad_x_input(model, lambda a: torch.sigmoid(model(ME.SparseTensor(features=a, coordinates=coords)).F[:, 0]), features)[0]
    feature, spatial = saliency.mean(0), saliency.mean(1)
    raw = np.load(ROOT / data["source"], allow_pickle=False)
    names = _names_with_masks(raw["feature_names"])
    origin, pitch = np.asarray(raw["grid_origin_mm"], np.float32), float(raw["voxel_pitch_mm"])
    pos = origin + (np.asarray(data["coords"], np.float32) + .5) * pitch
    _plot_bar(OUT / "sparse-3d-u-net-features.png", feature, names, "Sparse U-Net: occupied-voxel feature response")
    _plot_surface(OUT / "sparse-3d-u-net-map.png", pos, spatial, "Sparse U-Net: occupied surface-voxel response")
    np.savez_compressed(OUT / "sparse-3d-u-net.npz", feature_response=feature, voxel_response=spatial,
                        voxel_center_mm=pos, voxel_coords=np.asarray(data["coords"], np.int32), feature_names=np.asarray(names))
    row = _record("Sparse U-Net", loaded, data["source"],
                  "mean weak-fit predicted sparse-voxel probability", names, feature, spatial_npz="sparse-3d-u-net.npz",
                  map_file="sparse-3d-u-net-map.png", note="Backend executed in this environment, but no fitted sparse checkpoint exists.")
    row["status"] = "completed" if "cad_predictions/checkpoints" in loaded else "completed_untrained_backend_smoke"
    row["checkpoint_scope"] = "18h01 weak-label diagnostic fit" if "cad_predictions/checkpoints" in loaded else scope
    if "cad_predictions/checkpoints" in loaded:
        row["note"] = "This is the exact weak-label diagnostic checkpoint used for the displayed sparse CAD field."
    elif loaded != "random_initialization_seed17_no_checkpoint":
        row["note"] = "Backend executed with the two-step synthetic diagnostic checkpoint; it is not a fitted production model."
    return row


def point_models() -> list[dict[str, Any]]:
    item = point_inputs()[0]
    x_np = np.concatenate([item["x"], item["mask"].astype(np.float32)], 1)[None]
    x = _tensor(x_np)
    raw = np.load(ROOT / item["source"], allow_pickle=False)
    names = _names_with_masks(raw["feature_names"])
    pos = np.asarray(raw["pos_mm"], np.float32)
    rows = []
    for label, cls, slug, ckpt, cap in (
        ("PointNet", PointNetSeg, "pointnet", "results/models/checkpoints/synthetic_pointnet_cad_surface_points.pt", len(pos)),
        ("DGCNN", DGCNNSeg, "dgcnn", "results/models/checkpoints/synthetic_dgcnn_cad_surface_points.pt", 512),
    ):
        z = x[:, :cap]; model = cls(z.shape[-1], **({"k": 12} if cls is DGCNNSeg else {})); loaded = _load(model, _preferred_checkpoint(slug, ckpt))
        saliency = _grad_x_input(model, lambda a: torch.sigmoid(model(a)), z)[0]
        feature = saliency.mean(axis=(0, 1)); spatial = saliency.mean(axis=(0, 2))
        stem = label.lower()
        _plot_bar(OUT / f"{stem}-features.png", feature, names, f"{label}: point-feature response")
        _plot_surface(OUT / f"{stem}-map.png", pos[:cap], spatial, f"{label}: CAD point response")
        np.savez_compressed(OUT / f"{stem}.npz", feature_response=feature, point_response=spatial,
                            point_pos_mm=pos[:cap], feature_names=np.asarray(names))
        rows.append(_record(label, loaded, item["source"], "mean weak-fit predicted point probability", names, feature,
                            spatial_npz=f"{stem}.npz", map_file=f"{stem}-map.png",
                            note=("DGCNN uses the executed deterministic 512-point prefix." if cap == 512 else "")))
    return rows


def mesh_model() -> dict[str, Any]:
    graph = max(graph_inputs(), key=lambda row: row["nodes"])
    process_x, process_mask, run_ids, process_source = process_conditions(); run = run_ids.index(RUN)
    x_np = np.concatenate([graph["x"], graph["mask"].astype(np.float32)], 1)
    edge_np = np.asarray(graph["edge_attr"], np.float32).copy(); edge_np[:, 0] /= 1000.0
    process_np = np.concatenate([process_x, process_mask.astype(np.float32)], 1)[run:run + 1]
    x, edge, p = _tensor(x_np), _tensor(edge_np), _tensor(process_np)
    edge_index = _tensor(graph["edge_index"], torch.long); batch = torch.zeros(len(x), dtype=torch.long)
    model = MeshGraphProcessor(x.shape[1], edge.shape[1], p.shape[1]); loaded = _load(model, _preferred_checkpoint("mesh-gnn", "results/models/checkpoints/synthetic_meshgraphnet.pt"))
    saliency, edge_saliency, process_saliency = _grad_x_input(model, lambda a, b, c: torch.sigmoid(model(a, edge_index, b, c, batch)), x, edge, p)
    feature = saliency.mean(0); spatial = saliency.mean(1); names = _names_with_masks(np.load(ROOT / graph["source"], allow_pickle=False)["feature_names"])
    raw = np.load(ROOT / process_source, allow_pickle=False)
    process_names = _names_with_masks(list(raw["process_feature_names"].astype(str)) + list(raw["geometry_feature_names"].astype(str)))
    _plot_bar(OUT / "mesh-gnn-features.png", feature, names, "Mesh GNN: node-feature response")
    _plot_surface(OUT / "mesh-gnn-map.png", np.load(ROOT / graph["source"], allow_pickle=False)["pos_mm"], spatial, "Mesh GNN: CAD-node response")
    _plot_bar(OUT / "mesh-gnn-process.png", process_saliency.mean(0), process_names, "Mesh GNN: XLS/CAD-summary response")
    np.savez_compressed(OUT / "mesh-gnn.npz", node_feature_response=feature, node_response=spatial,
                        edge_feature_response=edge_saliency.mean(0), process_response=process_saliency.mean(0),
                        pos_mm=np.load(ROOT / graph["source"], allow_pickle=False)["pos_mm"], feature_names=np.asarray(names),
                        process_feature_names=np.asarray(process_names))
    return _record("Mesh GNN", loaded, graph["source"], "mean weak-fit predicted CAD-node probability", names, feature,
                   spatial_npz="mesh-gnn.npz", map_file="mesh-gnn-map.png",
                   process=_process_summary(process_saliency.mean(0), process_names))


def risk_gnn() -> dict[str, Any]:
    data = load_training_data(); run = data["runs"].index(RUN); n = data["nodes_per_run"]; sl = slice(run * n, (run + 1) * n)
    # Build a one-run graph from exactly the same transformed values as diagnostic training.
    x = _tensor(data["node"][sl]); edge = _tensor(data["edge"][:, :data["edge"].shape[1] // len(data["runs"])], torch.long)
    edge_attr = _tensor(data["edge_attr"][:edge.shape[1]]); process = _tensor(data["process"][run:run + 1]); batch = torch.zeros(n, dtype=torch.long)
    model = PhysicsConstrainedRiskGNN(x.shape[1], edge_attr.shape[1], process.shape[1], max(1, len(data["class_names"])))
    loaded = _load(model, _preferred_checkpoint("risk-gnn", "results/models/weak_gnn/diagnostic_weak_gnn.pt"))
    saliency, edge_saliency, process_saliency = _grad_x_input(model, lambda a, b, c: torch.sigmoid(model(a, edge, b, c, batch)["risk_logit"]), x, edge_attr, process)
    feature = saliency.mean(0); spatial = saliency.mean(1)
    graph = max(graph_inputs(), key=lambda row: row["nodes"]); names = _names_with_masks(np.load(ROOT / graph["source"], allow_pickle=False)["feature_names"])
    raw_names = np.load(ROOT / "data_processed/paired/18h01_cad_ply_targets.npz", allow_pickle=False)["process_feature_names"]
    process_names = [str(name) for name in raw_names] + ["observed_mask: " + str(name) for name in raw_names] + ["imputed_mask: " + str(name) for name in raw_names]
    _plot_bar(OUT / "risk-gnn-features.png", feature, names, "Constrained risk GNN: CAD-node response")
    _plot_surface(OUT / "risk-gnn-map.png", data["cad_pos_mm"], spatial, "Constrained risk GNN: risk-logit response")
    _plot_bar(OUT / "risk-gnn-process.png", process_saliency.mean(0), process_names, "Constrained risk GNN: XLS response")
    np.savez_compressed(OUT / "risk-gnn.npz", node_feature_response=feature, node_response=spatial,
                        edge_feature_response=edge_saliency.mean(0), process_response=process_saliency.mean(0),
                        pos_mm=data["cad_pos_mm"], feature_names=np.asarray(names), process_feature_names=np.asarray(process_names))
    return _record("Constrained risk GNN", loaded, "data_processed/paired/18h01_cad_ply_targets.npz",
                   "mean diagnostic weak-fit risk probability on 18h01 CAD nodes", names, feature,
                   spatial_npz="risk-gnn.npz", map_file="risk-gnn-map.png",
                   process=_process_summary(process_saliency.mean(0), process_names),
                   note="This checkpoint produces the displayed 18h01 condition; its underlying risk-GNN fit is joint across the two weak-label runs and remains non-production.")


def uv_model() -> dict[str, Any]:
    data = brep_input(); x, adjacency = _tensor(data["grids"]), _tensor(data["adjacency"], torch.long)
    model = UVNetFaceModel(x.shape[1]); loaded = _load(model, _preferred_checkpoint("uv-net", "results/models/checkpoints/synthetic_uvnet.pt"))
    saliency = _grad_x_input(model, lambda a: torch.sigmoid(model(a, adjacency)), x)[0]
    feature, spatial = saliency.mean(axis=(0, 2, 3)), saliency.mean(axis=(1, 2, 3))
    names = ["x_mm", "y_mm", "z_mm", "normal_x", "normal_y", "normal_z", "kmax_1_per_mm", "kmin_1_per_mm", "gaussian_1_per_mm2", "trim_valid", "normal_valid", "curvature_valid"]
    pos = np.load(ROOT / data["source"], allow_pickle=False)["pos_mm"].mean(axis=(1, 2))
    _plot_bar(OUT / "uv-net-features.png", feature, names, "UV-Net: face-grid channel response")
    _plot_surface(OUT / "uv-net-map.png", pos, spatial, "UV-Net: B-Rep face response")
    np.savez_compressed(OUT / "uv-net.npz", feature_response=feature, face_response=spatial, face_centers_mm=pos, feature_names=np.asarray(names))
    return _record("UV-Net", loaded, data["source"], "mean weak-fit predicted B-Rep-face probability", names, feature,
                   spatial_npz="uv-net.npz", map_file="uv-net-map.png")


def tabular_model() -> dict[str, Any]:
    data = tabular_input(); run = data["runs"].index(RUN)
    x = _tensor(np.concatenate([data["x"], data["mask"].astype(np.float32)], 1)[run:run + 1])
    raw = np.load(ROOT / data["source"], allow_pickle=False)
    names = _names_with_masks(list(raw["process_feature_names"].astype(str)) + list(raw["geometry_feature_names"].astype(str)))
    model = TabularSurrogate(x.shape[1]); loaded = _load(model, _preferred_checkpoint("tabular-mlp", "results/models/checkpoints/synthetic_tabular_surrogate.pt"))
    saliency = _grad_x_input(model, lambda a: torch.sigmoid(model(a)), x)[0].mean(0)
    _plot_bar(OUT / "tabular-mlp-features.png", saliency, names, "Tabular MLP: run-level response")
    np.savez_compressed(OUT / "tabular-mlp.npz", feature_response=saliency, feature_names=np.asarray(names))
    return _record("Tabular MLP", loaded, data["source"], "weak-fit predicted run-level probability", names, saliency,
                   spatial_npz="tabular-mlp.npz", map_file="tabular-mlp-features.png")


def _page(rows: list[dict[str, Any]]) -> None:
    cards = []
    for row in rows:
        top = "".join(f"<li>{html.escape(item['feature'])}: {item['response']:.3g}</li>" for item in row.get("feature_response_top", [])[:6])
        process = row.get("process_response") or {}
        process_top = "".join(f"<li>{html.escape(item['feature'])}: {item['response']:.3g}</li>" for item in process.get("feature_response_top", [])[:6])
        map_tag = f'<img src="{html.escape(row["map_file"])}" alt="{html.escape(row["model"])} local response map">' if row.get("map_file") else ""
        cards.append(f"<article><h2>{html.escape(row['model'])}</h2><p><code>{html.escape(row['output_selector'])}</code></p>{map_tag}<p><a href=\"{html.escape(row['spatial_npz'])}\">machine-readable NPZ</a></p><h3>Geometry/topology channels</h3><ol>{top}</ol><h3>XLS/process branch</h3><ol>{process_top or '<li>Not wired for this model.</li>'}</ol></article>")
    (OUT / "index.html").write_text(f"""<!doctype html><meta charset=\"utf-8\"><title>Local model response diagnostics</title>
<style>body{{font:16px/1.5 system-ui;max-width:1180px;margin:2rem auto;padding:0 1rem;color:#172033;background:#f5f7fb}}article{{background:#fff;border:1px solid #d7dee9;border-radius:10px;padding:1rem;margin:1rem 0}}img{{max-width:620px;width:100%;border:1px solid #d7dee9}}code{{background:#eef2f8;padding:.2rem .3rem}}.warning{{background:#fff1d6;border-left:4px solid #a66a00;padding:1rem}}</style>
<p><a href=\"../dashboard/index.html\">Model dashboard</a> · <a href=\"report.json\">machine-readable report</a></p><h1>Local input-response diagnostics</h1>
<p class=\"warning\">{html.escape(WARNING)} Values use {html.escape(METHOD)} for one prepared 18h01 sample and the named checkpoint. Where available, each checkpoint is the exact weak-label diagnostic state used for the displayed CAD prediction; a synthetic fallback is named explicitly in the JSON.</p>{''.join(cards)}""", encoding="utf-8")


def build() -> Path:
    OUT.mkdir(parents=True, exist_ok=True)
    torch.set_num_threads(max(1, min(4, torch.get_num_threads())))
    rows: list[dict[str, Any]] = []
    image_rows, _ = image_models(); rows.extend(image_rows)
    voxel_row, _ = voxel_model(); rows.append(voxel_row)
    rows.append(sparse_model()); rows.extend(point_models()); rows.append(mesh_model()); rows.append(risk_gnn()); rows.append(uv_model()); rows.append(tabular_model())
    report = {"method": METHOD, "sample": RUN, "production_valid": False, "warning": WARNING, "models": rows}
    (OUT / "report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
    _page([row for row in rows if row["status"].startswith("completed")])
    return OUT / "report.json"


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--build", action="store_true", help="generate prepared-sample local response artifacts")
    args = parser.parse_args()
    if not args.build:
        parser.error("choose --build")
    print(build())


if __name__ == "__main__":
    main()
