"""立方体キャビティのサイズスイープ（球との形状比較・TEST_16）.

TEST_15 の結論「効き目の序列は 板直下の喉 > 接合の相似・G2 化 > 球半径そのもの」を受け，
ロフトも接合も構造的に存在しない立方体キャビティで同じスイープを行う．立方体の上面は
板の縁から外へ広がる水平な天井そのもので，TEST_15 最良の similar_radial（リム水平出し）が
目指した形と一致する．「球のほうが理想積分球だから有利」が成り立つかを実測で確かめる．

幾何のパラメータ化:
    キャビティ内寸 L の立方体．天井 z=0（リムと同一平面），底面 z=-L，内壁 x, y = ±L/2．
    評価板・リム・材質・レンダ条件は球スイープ（sweep_sphere_radius.py）と完全に同一．

        led_z    = -L + LED_WALL_STANDOFF   （底面中心に密着・評価板向き +Z）
        バッフル = led_z + BAFFLE_GAP        （球版と同じ閉形式 baffle_radius）

固定条件（球スイープと完全に同一に保つ）:
    材質  : blendbsdf rho=0.85 / omega=0.05 / alpha=0.1
    光源  : 円板 radius=1.7662mm（面積 9.8mm^2 保存）．board_baffle 構成のみ
    レンダ: spp=1024 / max_depth=16 / 512^2 / llvm_ad_rgb / path / orthographic (50x52)
    評価  : configs/eval_baseline_dng_target_shape.yaml（shape_metrics 込み）

球との対応（内面積を揃える解釈）:
    球 r=88/100/135/170/250 の内面積は L=126/142/188/234/341 の立方体に相当する．
    スイープはキリの良い L で振り，比較は内面積を横軸にして行う．

実行方法:
    uv run python scripts/sweep_cube_size.py --prepare-only            # メッシュ + YAML のみ
    uv run python scripts/sweep_cube_size.py --sizes 120 160 200 280 360
"""

from __future__ import annotations

import argparse
import copy
import csv
import subprocess
import sys
import time
from pathlib import Path

import numpy as np
import trimesh
import yaml

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

from generate_cube_housing import build_cube_housing  # noqa: E402
from sweep_sphere_radius import (  # noqa: E402
    BAFFLE_GAP,
    BAFFLE_MARGIN,
    BAFFLE_THICKNESS,
    BOARD_CORNER,
    BOARD_HALF,
    BOARD_SURFACE_Z,
    EVAL_CONFIG_YAML,
    LED_RADIUS,
    LED_WALL_STANDOFF,
    RIM_INSET,
    RIM_Z,
    TEMPLATE_YAML,
    THICKNESS,
    raw_stats,
    read_metrics_csv,
)

ROOT = Path(__file__).resolve().parents[1]

TMP_DIR = ROOT / "configs" / "_tmp_cube_size"
MESH_DIR = TMP_DIR / "meshes"
OUTPUT_BASE = ROOT / "output" / "cube_size_sweep"
RUN_DIR = "board_baffle"

# 評価板 99×102mm が上面に収まる下限は L=102．天井の余白（Y 側片 9mm）を考え 120 から振る
SIZES = [120.0, 140.0, 160.0, 180.0, 200.0, 240.0, 280.0, 320.0, 360.0]

# 板の実面積とリム開口（板輪郭から RIM_INSET 内側）の面積（mm^2）．内面積の集計に使う
BOARD_AREA = (2 * BOARD_HALF[0]) * (2 * BOARD_HALF[1])
RIM_OPENING_AREA = (2 * (BOARD_HALF[0] - RIM_INSET)) * (2 * (BOARD_HALF[1] - RIM_INSET))

SUMMARY_COLUMNS = [
    "size_mm",
    "led_z_mm",
    "baffle_radius_mm",
    "cavity_area_mm2",
    "board_area_ratio",
    "elapsed_s",
    "raw_mean_roi",
    "raw_mean_full",
    "raw_rel_std_roi",
    "cov",
    "center_periphery_ratio",
    "gradient_magnitude",
    "radial_min_max_ratio",
    "angular_ratio",
    "pca_eccentricity",
    "intensity_centroid_offset_norm",
    "max_min_ratio",
]


def led_z(size: float) -> float:
    """LED 円板の Z 座標（底面 z=-L から壁面密着分だけ手前）を返す."""
    return RIM_Z - size + LED_WALL_STANDOFF


def job_name(size: float) -> str:
    """一辺の長さからジョブ名（ディレクトリ名）を作る."""
    return f"L{size:g}"


def baffle_radius(size: float) -> float:
    """評価板の全点から LED を隠すのに必要なバッフル円板の半径（mm）を返す.

    導出は sweep_sphere_radius.baffle_radius と同一（LED と板の内分点による厳密上界）．
    LED_z と板の外接円だけで決まる式なので，キャビティが球でも立方体でも変わらない．
    """
    f = BAFFLE_GAP / (BOARD_SURFACE_Z - led_z(size))
    return BAFFLE_MARGIN * ((1.0 - f) * LED_RADIUS + f * BOARD_CORNER)


def build_baffle_mesh(size: float, dest: Path) -> trimesh.Trimesh:
    """LED 直前のバッフル円板（薄い円柱ソリッド）を生成して書き出す.

    半セクタ回転は球版と同じ理由（蓋の三角形ファンのエッジを XZ / YZ 対称面から外し，
    遮蔽検証レイの数値的なすり抜けを防ぐ）．
    """
    sections = 128
    mesh = trimesh.creation.cylinder(
        radius=baffle_radius(size), height=BAFFLE_THICKNESS, sections=sections
    )
    mesh.apply_transform(trimesh.transformations.rotation_matrix(np.pi / sections, [0, 0, 1]))
    mesh.apply_translation([0.0, 0.0, led_z(size) + BAFFLE_GAP])
    dest.parent.mkdir(parents=True, exist_ok=True)
    mesh.export(dest)
    return mesh


def build_mesh(size: float, dest: Path) -> trimesh.Trimesh:
    """指定サイズの立方体筐体 STL を生成して書き出す."""
    mesh = build_cube_housing(
        size=size,
        thickness=THICKNESS,
        board_half_x=BOARD_HALF[0],
        board_half_y=BOARD_HALF[1],
        rim_inset=RIM_INSET,
        rim_z=RIM_Z,
    )
    dest.parent.mkdir(parents=True, exist_ok=True)
    mesh.export(dest)
    return mesh


def cavity_geometry(size: float) -> dict[str, float]:
    """キャビティ内面積と，評価板がそれに占める割合（開口比）を返す.

    内面積 = 側壁 4L^2 + 底面 L^2 + 天井 (L^2 - リム開口) + 評価板．
    球版 cavity_geometry と同じ「板込みの総内面積」の定義で，比較の横軸に使う．
    """
    total = 6.0 * size**2 - RIM_OPENING_AREA + BOARD_AREA
    return {
        "cavity_area_mm2": round(total, 1),
        "board_area_ratio": round(BOARD_AREA / total, 5),
    }


def build_scene_yaml(size: float, mesh_path: Path, dest: Path, baffle_path: Path) -> None:
    """テンプレートから当該サイズのシーン YAML を書き出す.

    材質・センサ・レンダ設定・LED の半径/radiance は一切変更しない．変えるのは
    筐体メッシュ（球 → 立方体）・LED の Z・LED の向き・バッフルだけ．
    メッシュの相対パスは YAML 自身のディレクトリ基準で解決されるため絶対パス化する．
    """
    config = copy.deepcopy(yaml.safe_load(TEMPLATE_YAML.read_text(encoding="utf-8")))
    config["name"] = f"cube_size_{RUN_DIR}_{job_name(size)}"
    config["description"] = (
        f"立方体キャビティのサイズスイープ [board_baffle]: 内寸 {size:g}mm"
        f"（天井 z={RIM_Z:g}・底面 z={RIM_Z - size:g}）．"
        f"LED は底面中心に密着し評価板向き（+Z）．バッフルで直接光を遮断．"
    )
    for mesh in config["geometry"]["meshes"]:
        if mesh["id"] == "sphere_housing":
            mesh["id"] = "cube_housing"
            mesh["path"] = str(mesh_path.resolve())
        else:
            mesh["path"] = str((TEMPLATE_YAML.parent / mesh["path"]).resolve())
    config["geometry"]["meshes"].append(
        {
            "id": "baffle",
            "path": str(baffle_path.resolve()),
            "unit": "mm",
            "material": "printer_white",
        }
    )
    for light in config["lights"]:
        light["center"] = [0.0, 0.0, led_z(size)]
        light["direction"] = [0.0, 0.0, 1.0]
    dest.parent.mkdir(parents=True, exist_ok=True)
    dest.write_text(yaml.safe_dump(config, allow_unicode=True, sort_keys=False), encoding="utf-8")


def prepare(sizes: list[float], log) -> dict[float, Path]:
    """全サイズについて筐体・バッフルのメッシュとシーン YAML を生成し，watertight を検証する."""
    scenes: dict[float, Path] = {}
    log("=== 幾何生成（立方体 board_baffle）===")
    for size in sizes:
        mesh_path = MESH_DIR / f"立方体_{job_name(size)}.stl"
        mesh = build_mesh(size, mesh_path)
        if not mesh.is_watertight:
            raise ValueError(f"L={size}: watertight でないメッシュが生成されました")
        baffle_path = MESH_DIR / f"バッフル_{job_name(size)}.stl"
        baffle = build_baffle_mesh(size, baffle_path)
        if not baffle.is_watertight:
            raise ValueError(f"L={size}: バッフルが watertight になりませんでした")
        scene_yaml = TMP_DIR / f"scene_{RUN_DIR}_{job_name(size)}.yaml"
        build_scene_yaml(size, mesh_path, scene_yaml, baffle_path)
        scenes[size] = scene_yaml
        geo = cavity_geometry(size)
        log(
            f"  L={size:6.1f} LED_z={led_z(size):9.3f} "
            f"内面積={geo['cavity_area_mm2']:9.0f}mm2 "
            f"開口比={geo['board_area_ratio'] * 100:5.2f}% "
            f"バッフル R={baffle_radius(size):5.2f}mm z={led_z(size) + BAFFLE_GAP:8.2f}"
        )
    return scenes


def run_job(size: float, scene_yaml: Path, log) -> dict[str, float] | None:
    """1 サイズをレンダ→評価まで実行して集約行を返す（失敗時 None）.

    既に metrics.csv と luminance.npy が揃っているジョブは再レンダせず再利用する．
    """
    name = job_name(size)
    out_dir = OUTPUT_BASE / RUN_DIR / name
    row: dict[str, float] = {
        "size_mm": size,
        "led_z_mm": round(led_z(size), 3),
        "baffle_radius_mm": round(baffle_radius(size), 3),
        "elapsed_s": 0.0,
    }
    row.update(cavity_geometry(size))
    if (out_dir / "metrics.csv").exists() and (out_dir / "luminance.npy").exists():
        log(f"--- {name}: 既存結果を再利用")
    else:
        log(f"--- {name}: レンダ開始（LED_z={led_z(size):.2f}）")
        out_dir.mkdir(parents=True, exist_ok=True)
        start = time.time()
        for cmd in (
            ["uv", "run", "sm-render", "--scene", str(scene_yaml), "--output", str(out_dir)],
            [
                "uv",
                "run",
                "sm-evaluate",
                "--image",
                str(out_dir / "luminance.npy"),
                "--config",
                str(EVAL_CONFIG_YAML),
                "--output",
                str(out_dir),
            ],
        ):
            # encoding を明示する．既定では Windows の cp932 で復号され，
            # sm-render が出す日本語ログで UnicodeDecodeError になる（球スイープで実測）
            result = subprocess.run(
                cmd,
                cwd=ROOT,
                capture_output=True,
                text=True,
                encoding="utf-8",
                errors="replace",
            )
            if result.returncode != 0:
                log(f"  !! 失敗 ({cmd[2]}): {result.stderr[-800:]}")
                return None
        row["elapsed_s"] = round(time.time() - start, 1)
    row.update(read_metrics_csv(out_dir / "metrics.csv"))
    row.update(raw_stats(out_dir / "luminance.npy"))
    log(
        f"  -> {row['elapsed_s']:.0f}s CoV={row['cov']:.4f} "
        f"angular_ratio={row['angular_ratio']:.4f} C/P={row['center_periphery_ratio']:.4f} "
        f"勾配={row['gradient_magnitude']:.2f}% 生mean={row['raw_mean_roi']:.4f}"
    )
    return row


def write_summary(rows: list[dict[str, float]]) -> None:
    """集約 CSV をサイズ昇順で書き直す（途中終了でも最新状態が残る）."""
    summary_csv = OUTPUT_BASE / RUN_DIR / "summary.csv"
    summary_csv.parent.mkdir(parents=True, exist_ok=True)
    with summary_csv.open("w", newline="", encoding="utf-8") as fh:
        writer = csv.DictWriter(fh, fieldnames=SUMMARY_COLUMNS)
        writer.writeheader()
        for row in sorted(rows, key=lambda r: r["size_mm"]):
            writer.writerow({k: row.get(k, "") for k in SUMMARY_COLUMNS})


def main() -> int:
    """サイズスイープを実行するメインエントリポイント."""
    parser = argparse.ArgumentParser(description="立方体キャビティのサイズスイープ")
    parser.add_argument(
        "--prepare-only",
        action="store_true",
        help="メッシュとシーン YAML の生成だけ行う（3D 配置確認用）",
    )
    parser.add_argument(
        "--sizes",
        type=float,
        nargs="*",
        default=None,
        help="一辺の長さリスト（mm）．省略時は既定グリッド",
    )
    args = parser.parse_args()

    sizes = args.sizes if args.sizes else SIZES
    log_path = ROOT / "output" / "_sweep_cube_size.log"
    log_path.parent.mkdir(parents=True, exist_ok=True)
    log_file = log_path.open("a", encoding="utf-8")

    def log(message: str) -> None:
        print(message, flush=True)
        log_file.write(message + "\n")
        log_file.flush()

    log("=== 立方体キャビティ サイズスイープ（board_baffle）===")
    scenes = prepare(sizes, log)
    if args.prepare_only:
        log(f"--prepare-only のため終了．シーン YAML: {TMP_DIR}")
        log_file.close()
        return 0

    rows: list[dict[str, float]] = []
    for size in sizes:
        row = run_job(size, scenes[size], log)
        if row is not None:
            rows.append(row)
            write_summary(rows)

    log(f"\n=== 完了: {len(rows)} 条件 ===")
    log(f"  集約 CSV: {OUTPUT_BASE / RUN_DIR / 'summary.csv'}")
    log_file.close()
    return 0


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