"""SmTIAS 定量モード DNG の照明均一性評価スクリプト（PNG 版 run_uniformity.py の DNG 対応版）.

PNG 版と同じ出力一式（輝度マップ / ヒストグラム / 動径プロファイル / ゾーンマップ /
均一性 CSV / コンソール指標）を，DNG に対して同じ ROI・同じ向き（portrait）で出力する．

PNG 版との違い:
- 読み込みを load_image_dng（linear, output_color=raw）に置換
- DNG は RAW_SENSOR で未補正のため，meta.json の lscMap でレンズシェーディングを順適用（--no-lsc で無効化可）
- DNG はセンサ native が landscape のため，PNG と同じ portrait へ rot90(-1) で向き合わせ

使い方:
    python scripts/run_uniformity_dng.py --image data/smtias/quantitative/xxx.dng
"""

import argparse
import json
import sys
from pathlib import Path

import numpy as np

PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))

from src.analysis.spatial import calc_spatial_uniformity  # noqa: E402
from src.analysis.uniformity import (  # noqa: E402
    REC709_COEFF_B,
    REC709_COEFF_G,
    REC709_COEFF_R,
    calc_uniformity,
)
from src.export.exporter import ensure_output_dirs, export_results  # noqa: E402
from src.io.dng_loader import load_image_dng  # noqa: E402
from src.io.loader import extract_roi  # noqa: E402
from src.visualization.plotter import (  # noqa: E402
    plot_histogram,
    plot_luminance_map,
    plot_radial_profile,
    plot_zone_map,
)


def load_roi_config(config_path: str, roi_key: str) -> dict | None:
    """ROI 設定ファイルを読み込んで指定キーの ROI を返す．

    Args:
        config_path: JSON 設定ファイルのパス．
        roi_key: ドット区切りのキー（例: "smtias.whiteboard"）．

    Returns:
        ROI の辞書（x, y, width, height）．キーが存在しない場合は None．

    Raises:
        FileNotFoundError: 設定ファイルが存在しない場合．
    """
    config_file = Path(config_path)
    if not config_file.exists():
        raise FileNotFoundError(f"設定ファイルが見つかりません: {config_path}")

    with config_file.open(encoding="utf-8") as f:
        config = json.load(f)

    # ドット区切りでネストされたキーを辿る
    node = config
    for key in roi_key.split("."):
        if not isinstance(node, dict) or key not in node:
            return None
        node = node[key]

    return node if isinstance(node, dict) else None


def _upsample(grid: np.ndarray, shape: tuple[int, int]) -> np.ndarray:
    """(rows, cols) グリッドを画像サイズ shape へバイリニア補間する．

    Args:
        grid: 補間元グリッドの NumPy 配列（rows x cols, float64）．
        shape: 補間先の画像サイズ (height, width)．

    Returns:
        補間後の NumPy 配列（height x width, float64）．
    """
    rows, cols = grid.shape
    h, w = shape
    ys = np.linspace(0, rows - 1, h)
    xs = np.linspace(0, cols - 1, w)
    y0 = np.floor(ys).astype(int)
    y1 = np.minimum(y0 + 1, rows - 1)
    x0 = np.floor(xs).astype(int)
    x1 = np.minimum(x0 + 1, cols - 1)
    wy = (ys - y0)[:, None]
    wx = (xs - x0)[None, :]
    top = grid[y0][:, x0] * (1 - wx) + grid[y0][:, x1] * wx
    bot = grid[y1][:, x0] * (1 - wx) + grid[y1][:, x1] * wx
    return top * (1 - wy) + bot * wy


def lsc_gain_rgb(meta_path: Path, shape: tuple[int, int]) -> np.ndarray:
    """meta.json の lscMap (4, M, N) を (H, W, 3) の RGB ゲインへ補間する．

    4ch = [R, G_even, G_odd, B]．G は G_even/G_odd の平均を用いる．

    Args:
        meta_path: meta.json ファイルのパス．
        shape: 補間先の画像サイズ (height, width)．

    Returns:
        RGB ゲインの NumPy 配列（H x W x 3, float64）．
    """
    meta = json.loads(meta_path.read_text(encoding="utf-8"))
    rows, cols = meta["lscMapRowCount"], meta["lscMapColumnCount"]
    m = np.array(meta["lscMap"], dtype=np.float64).reshape(rows, cols, 4)
    gr = _upsample(m[:, :, 0], shape)
    gg = _upsample((m[:, :, 1] + m[:, :, 2]) / 2.0, shape)
    gb = _upsample(m[:, :, 3], shape)
    return np.stack([gr, gg, gb], axis=2)


def dng_luminance(image_path: str, apply_lsc: bool) -> np.ndarray:
    """DNG から portrait・linear・（任意で LSC 補正済）の輝度画像を返す．

    - output_format="rgb_float32"（output_color=raw, linear）で読み込み
    - apply_lsc=True なら lscMap を順適用してレンズビネットを除去
    - Rec.709 で輝度化（uniformity.py と同じ係数）
    - PNG と同じ portrait へ rot90(-1) で向き合わせ

    Args:
        image_path: DNG ファイルのパス．
        apply_lsc: True の場合，同名 .meta.json から LSC ゲインを読み込んで補正する．

    Returns:
        portrait 向きの輝度画像（H x W, float64）．値域は 0〜255 の linear スケール．
    """
    rgb = load_image_dng(image_path, output_format="rgb_float32")  # (H,W,3) linear landscape

    if apply_lsc:
        meta_path = Path(image_path).with_suffix("").with_suffix(".meta.json")
        if not meta_path.exists():
            # ".meta.json" は二重拡張子なので with_suffix では拾えない場合がある
            meta_path = Path(str(Path(image_path).with_suffix("")) + ".meta.json")
        if not meta_path.exists():
            raise FileNotFoundError(f"meta.json が見つかりません（LSC 補正に必要）: {meta_path}")
        rgb = rgb * lsc_gain_rgb(meta_path, rgb.shape[:2])

    luma = (
        REC709_COEFF_R * rgb[:, :, 0]
        + REC709_COEFF_G * rgb[:, :, 1]
        + REC709_COEFF_B * rgb[:, :, 2]
    )
    # linear のまま 0〜255 スケールで表現する（PNG 図と軸を揃えて比較するため）．
    # CoV・max/min・中心周辺比はスケール不変なので不変，mean/std のみ ×255 となる．
    # 既存 plot_histogram が range=(0,255) 固定であることにも整合する．
    luma = luma * 255.0
    return np.rot90(luma, -1)  # portrait（PNG と同じ向き）


def analyze_single_dng(
    image_path: str,
    roi: dict | None,
    output_base: Path,
    apply_lsc: bool,
) -> None:
    """DNG 1 枚を解析し，PNG 版と同じ出力一式を生成する．

    Args:
        image_path: 解析対象 DNG ファイルのパス．
        roi: ROI の辞書（x, y, width, height）．None の場合は画像全体を解析．
        output_base: 出力ルートディレクトリのパス．
        apply_lsc: True の場合，LSC（レンズシェーディング）補正を適用する．
    """
    print(f"解析開始: {image_path}")
    luma_full = dng_luminance(image_path, apply_lsc)
    print(f"画像サイズ: {luma_full.shape[1]} x {luma_full.shape[0]} px（portrait）")
    print(f"LSC 補正: {'あり' if apply_lsc else 'なし'} / ドメイン: linear（output_color=raw）")

    if roi is not None:
        print(
            f"ROI を適用します: x={roi['x']}, y={roi['y']}, "
            f"width={roi['width']}, height={roi['height']}"
        )
        luminance = extract_roi(luma_full, roi["x"], roi["y"], roi["width"], roi["height"])
    else:
        print("ROI 未指定: 画像全体を解析します")
        luminance = luma_full

    results = calc_uniformity(luminance)
    print("--- 均一性指標（輝度は 0〜255 linear スケール）---")
    print(f"  平均輝度     : {results['mean']:.2f}")
    print(f"  標準偏差     : {results['std']:.2f}")
    print(f"  CoV          : {results['cov']:.4f}")
    print(f"  最大/最小比  : {results['max_min_ratio']:.4f}")
    print(f"  最大輝度     : {results['max']:.2f}")
    print(f"  最小輝度     : {results['min']:.2f}")

    spatial = calc_spatial_uniformity(luminance)
    zone_stats = spatial["zone_stats"]
    print("--- 空間解析指標 ---")
    print(f"  中心平均輝度   : {zone_stats['center_mean']:.2f}")
    print(f"  中間平均輝度   : {zone_stats['middle_mean']:.2f}")
    print(f"  周辺平均輝度   : {zone_stats['periphery_mean']:.2f}")
    print(f"  中心/周辺比    : {zone_stats['center_periphery_ratio']:.4f}")
    print(f"  勾配量 (%)     : {zone_stats['gradient_magnitude']:.2f}")
    print(f"  動径min/max比  : {spatial['radial_min_max_ratio']:.4f}")

    stem = Path(image_path).stem  # 例: SmTIAS_QM_20260601_102753
    figures = output_base / "figures"
    plot_luminance_map(luminance, str(figures / f"{stem}_luminance_map.png"))
    plot_histogram(luminance, str(figures / f"{stem}_histogram.png"))
    plot_radial_profile(spatial["radial_profile"], str(figures / f"{stem}_radial_profile.png"))
    plot_zone_map(luminance, spatial["zone_map"], str(figures / f"{stem}_zone_map.png"))
    export_results(results, str(output_base / "results" / f"{stem}_uniformity.csv"))

    print(f"解析完了: {image_path}")


def main() -> None:
    """エントリーポイント."""
    parser = argparse.ArgumentParser(
        description="SmTIAS 定量モード DNG の照明均一性を評価する（PNG 版と同じ出力一式）."
    )
    parser.add_argument("--image", required=True, help="解析対象の DNG ファイルパス")
    parser.add_argument(
        "--config",
        default=str(PROJECT_ROOT / "config" / "roi_config.json"),
        help="ROI 設定ファイルのパス（デフォルト: config/roi_config.json）",
    )
    parser.add_argument(
        "--roi",
        default="smtias.whiteboard",
        help="ROI のキー（ドット区切り，デフォルト: smtias.whiteboard）",
    )
    parser.add_argument(
        "--output",
        default=str(PROJECT_ROOT / "output"),
        help="出力ディレクトリのパス（デフォルト: output/）",
    )
    parser.add_argument(
        "--no-lsc",
        action="store_true",
        help="LSC（レンズシェーディング）補正を無効化する（既定は補正あり）",
    )
    args = parser.parse_args()

    output_base = Path(args.output)
    ensure_output_dirs(args.output)

    roi = None
    if args.config:
        try:
            roi = load_roi_config(args.config, args.roi)
        except FileNotFoundError as e:
            print(f"警告: {e}  → 画像全体を解析します")

    analyze_single_dng(args.image, roi, output_base, apply_lsc=not args.no_lsc)


if __name__ == "__main__":
    main()
