"""半径スイープの結果を一覧図にする（輝度マップ一覧 + 指標の半径依存カーブ）.

数値だけで形を判断しないための図．TEST_14 では総合スコアと残差 RMS が別の最良条件を
指す食い違いが起き，輝度マップそのものを見て初めて解釈できた．本スクリプトは
summary.csv の指標カーブと，各半径の輝度マップを必ず並べて出す．

輝度マップは平滑化（median 5px → gaussian sigma=4）した上で **各条件の平均で正規化**して
表示する．生のままでは MC ノイズが支配し，絶対輝度が半径で 1 桁変わるため形が比較できない．

実行方法:
    uv run python scripts/build_radius_sweep_visual.py --variant board_baffle
"""

from __future__ import annotations

import argparse
import csv
import sys
from pathlib import Path

import matplotlib
import numpy as np
from scipy.ndimage import gaussian_filter, median_filter

matplotlib.use("Agg")
import matplotlib.pyplot as plt  # noqa: E402

sys.path.insert(0, str(Path(__file__).resolve().parent))
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))

from sweep_sphere_radius import OUTPUT_BASE, VARIANTS, job_name  # noqa: E402

from sm_tias_lightsim.evaluation.roi_presets import SIM_ROI, extract_roi_xywh  # noqa: E402

plt.rcParams["font.family"] = ["Yu Gothic", "MS Gothic", "DejaVu Sans"]

# 指標カーブに出す列と表示名（小さいほど均一 / 1 に近いほど均一を注記する）
METRIC_PANELS = [
    ("gradient_magnitude", "勾配 [%]（小さいほど均一）", False),
    ("center_periphery_ratio", "中心/周辺比（1 が理想）", True),
    ("radial_min_max_ratio", "動径 min/max（1 が理想）", True),
    ("angular_ratio", "angular_ratio（0=同心円）", False),
    ("raw_mean_roi", "生の ROI 平均輝度（絶対値・対数）", False),
]

# CoV 分解に使う平滑化強度．sigma=4 では MC ノイズが残るため構造の推定には 12 を使う
# （sigma を 4→20 と変えても構造値は 4% しか動かないことを実測して確認済み）
STRUCTURE_SIGMA = 12


def read_summary(path: Path) -> list[dict[str, float]]:
    """summary.csv を読み，半径昇順の辞書リストで返す."""
    with path.open(encoding="utf-8") as fh:
        rows = [{k: (float(v) if v else float("nan")) for k, v in row.items()}
                for row in csv.DictReader(fh)]
    return sorted(rows, key=lambda r: r["radius_mm"])


def smooth_map(luminance_npy: Path) -> np.ndarray:
    """ROI を切り出して平滑化した輝度マップを返す（MC ノイズ支配を避ける）."""
    arr = np.load(luminance_npy).astype(np.float64)
    if arr.ndim == 3:
        arr = 0.2126 * arr[..., 0] + 0.7152 * arr[..., 1] + 0.0722 * arr[..., 2]
    roi = extract_roi_xywh(arr, SIM_ROI)
    return gaussian_filter(median_filter(roi, size=5), sigma=4)


def decompose_cov(luminance_npy: Path, seed: int = 0) -> tuple[float, float, float]:
    """CoV を「構造」と「MC ノイズ」に分解して (生, 構造, ノイズ) を返す.

    生の CoV は絶対輝度が半径で 1 桁変わる本スイープでは MC ノイズに支配され，
    そのまま半径依存を読むと「大きい球ほど不均一」という誤った結論になる．

    構造は STRUCTURE_SIGMA で平滑化した像の CoV から，ノイズの漏れ込み分を
    二乗で差し引いて求める．漏れ込み量は残差（生 - 平滑）を空間シャッフルして
    同じ平滑化を掛けた std として実測する（構造を壊しノイズの統計だけ残す）．
    """
    arr = np.load(luminance_npy).astype(np.float64)
    if arr.ndim == 3:
        arr = 0.2126 * arr[..., 0] + 0.7152 * arr[..., 1] + 0.0722 * arr[..., 2]
    roi = extract_roi_xywh(arr, SIM_ROI)
    mean = roi.mean()
    residual = roi - gaussian_filter(median_filter(roi, size=5), sigma=4)
    smoothed = gaussian_filter(median_filter(roi, size=5), sigma=STRUCTURE_SIGMA)
    flat = residual.ravel().copy()
    np.random.default_rng(seed).shuffle(flat)
    leak = gaussian_filter(
        median_filter(flat.reshape(residual.shape), size=5), sigma=STRUCTURE_SIGMA
    ).std() / mean
    structure = float(np.sqrt(max((smoothed.std() / smoothed.mean()) ** 2 - leak**2, 0.0)))
    return float(roi.std() / mean), structure, float(residual.std() / mean)


def draw_cov_panel(ax, rows: list[dict[str, float]], variant: str) -> None:
    """CoV を生・構造・ノイズに分けて描く（生だけ見ると誤読するため）."""
    radii, raws, structures, noises = [], [], [], []
    for row in rows:
        path = OUTPUT_BASE / variant / job_name(row["radius_mm"]) / "luminance.npy"
        if not path.exists():
            continue
        raw, structure, noise = decompose_cov(path)
        radii.append(row["radius_mm"])
        raws.append(raw)
        structures.append(structure)
        noises.append(noise)
    ax.plot(radii, raws, marker="o", color="gray", label="生 CoV（= metrics.csv）")
    ax.plot(radii, noises, marker="^", color="tab:orange", label="MC ノイズ成分")
    ax.plot(radii, structures, marker="s", color="tab:red", lw=2.2, label="構造成分（真の不均一）")
    ax.set_xlabel("球内半径 (mm)")
    ax.set_title("CoV の分解（構造 vs MC ノイズ）", fontsize=11)
    ax.legend(fontsize=8)
    ax.grid(alpha=0.3)


def draw_maps(fig, gs, rows: list[dict[str, float]], variant: str) -> None:
    """各半径の輝度マップを平均正規化して並べる（共通カラースケール）."""
    maps: list[tuple[float, np.ndarray]] = []
    for row in rows:
        path = OUTPUT_BASE / variant / job_name(row["radius_mm"]) / "luminance.npy"
        if path.exists():
            smoothed = smooth_map(path)
            maps.append((row["radius_mm"], smoothed / smoothed.mean()))
    if not maps:
        return
    stacked = np.concatenate([m.ravel() for _, m in maps])
    vmin, vmax = np.percentile(stacked, [0.5, 99.5])
    for i, (radius, data) in enumerate(maps[:10]):
        ax = fig.add_subplot(gs[i // 5, i % 5])
        image = ax.imshow(data, cmap="hot", vmin=vmin, vmax=vmax)
        ax.set_title(f"r={radius:g}mm", fontsize=10)
        ax.axis("off")
        if i == len(maps[:10]) - 1:
            fig.colorbar(image, ax=ax, fraction=0.046, label="平均=1 で正規化")


def draw_metrics(fig, gs, rows: list[dict[str, float]]) -> None:
    """指標の半径依存カーブを描く（理想値がある指標には水平線を引く）."""
    radii = [row["radius_mm"] for row in rows]
    for i, (key, label, has_ideal) in enumerate(METRIC_PANELS, start=1):
        ax = fig.add_subplot(gs[2 + i // 3, (i % 3) * 2 : (i % 3) * 2 + 2])
        ax.plot(radii, [row[key] for row in rows], marker="o", color="tab:blue")
        if has_ideal:
            ax.axhline(1.0, color="gray", lw=0.8, ls="--")
        if key == "raw_mean_roi":
            ax.set_yscale("log")
        ax.set_xlabel("球内半径 (mm)")
        ax.set_title(label, fontsize=10)
        ax.grid(alpha=0.3)


def main() -> int:
    """半径スイープの一覧図を生成する."""
    parser = argparse.ArgumentParser(description="半径スイープの結果一覧図を生成")
    parser.add_argument("--variant", choices=VARIANTS, default="board_baffle")
    parser.add_argument("--output", type=Path, default=None, help="出力 PNG パス")
    args = parser.parse_args()

    summary = OUTPUT_BASE / args.variant / "summary.csv"
    if not summary.exists():
        print(f"エラー: {summary} がありません（先にスイープを実行してください）")
        return 1
    rows = read_summary(summary)
    output = args.output or OUTPUT_BASE / args.variant / "sweep_overview.png"

    fig = plt.figure(figsize=(20, 13))
    gs = fig.add_gridspec(4, 6, height_ratios=[1.1, 1.1, 1, 1])
    draw_maps(fig, gs, rows, args.variant)
    draw_cov_panel(fig.add_subplot(gs[2, 0:2]), rows, args.variant)
    draw_metrics(fig, gs, rows)
    fig.suptitle(f"球体筐体 半径スイープ 結果一覧 [{args.variant}]", fontsize=15)
    fig.tight_layout()
    output.parent.mkdir(parents=True, exist_ok=True)
    fig.savefig(output, dpi=100)
    print(f"出力: {output}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
