# -*- coding: utf-8 -*-
"""全動画を濃度モデル経路で判定し、人間ラベルと突き合わせて精度を出す（LLM不使用）。

各動画について density_resolver を「濃度なし」で呼び、可読性・湯気・モデル推定濃度を得る。
結果は1件ごとにCSVへ追記（長時間実行でも途中結果が残る）。
最後に readable/steam 状態別の精度サマリを出力する。
"""

from __future__ import annotations

import argparse
import csv
import glob
import sys
from pathlib import Path

import numpy as np

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "local_batch_api"))
from density_resolver import DensityResolver, normalize_density  # noqa: E402

VIDEO_EXTS = [".MOV", ".mov", ".mp4", ".MP4", ".m4v", ".avi", ".mkv"]


def find_video(video_dir: Path, vid: str) -> Path | None:
    for e in VIDEO_EXTS:
        c = video_dir / f"{vid}{e}"
        if c.exists():
            return c
    return None


def load_human(label_csv: str) -> dict[str, str]:
    out: dict[str, str] = {}
    fs = glob.glob(label_csv)
    if fs:
        for r in csv.DictReader(open(fs[0], encoding="utf-8-sig")):
            vid = (r.get("ID") or "").strip()
            if vid:
                out[vid] = normalize_density(r.get("スープ濃度"))
    return out


def cleanup_stem(crops_dir: Path, stem: str) -> None:
    for p in crops_dir.glob(f"{stem}_*"):
        try:
            p.unlink()
        except OSError:
            pass


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--video-dir", default=str(ROOT / "学習データ"))
    ap.add_argument("--label-csv", default=str(ROOT / "学習データ" / "*グリッド 10.csv"))
    ap.add_argument("--frames", type=int, default=16)
    ap.add_argument("--steam-skip-threshold", type=float, default=0.80)
    ap.add_argument("--out", default=str(ROOT / "local_batch_api" / "full_density_eval.csv"))
    ap.add_argument("--limit", type=int, default=0)
    ap.add_argument("--keep-crops", action="store_true")
    ap.add_argument("--resume", action="store_true", help="既存CSVにあるIDをスキップして続きから実行")
    args = ap.parse_args()

    video_dir = Path(args.video_dir)
    human = load_human(args.label_csv)
    ids = sorted(human.keys(), key=lambda x: int(x) if x.isdigit() else 0)
    if args.limit > 0:
        ids = ids[: args.limit]

    out_path = Path(args.out)
    fields = ["id", "human", "model_value", "confidence", "readable", "assessable",
              "steam_mean", "steam_min", "status", "frames_used", "abs_err", "note"]
    done_ids: set[str] = set()
    if args.resume and out_path.exists():
        for r in csv.DictReader(open(out_path, encoding="utf-8-sig")):
            if r.get("id"):
                done_ids.add(r["id"].strip())
        print(f"resume: 既存 {len(done_ids)} 件をスキップ", flush=True)

    crops_dir = ROOT / "local_batch_api" / "full_eval_crops"
    crops_dir.mkdir(parents=True, exist_ok=True)
    resolver = DensityResolver(frames=args.frames, crops_dir=crops_dir)

    if args.resume and out_path.exists():
        fh = out_path.open("a", encoding="utf-8-sig", newline="")
        writer = csv.DictWriter(fh, fieldnames=fields)
    else:
        fh = out_path.open("w", encoding="utf-8-sig", newline="")
        writer = csv.DictWriter(fh, fieldnames=fields)
        writer.writeheader()

    total = len(ids)
    done = 0
    for vid in ids:
        if vid in done_ids:
            continue
        vpath = find_video(video_dir, vid)
        if vpath is None:
            continue
        res = resolver.resolve(vpath, {})  # 濃度なし=モデル経路
        h = human.get(vid, "")
        mv = res.get("value")
        abs_err = ""
        if res["status"] == "ok" and mv and h:
            abs_err = abs(int(mv) - int(h))
        writer.writerow({
            "id": vid, "human": h, "model_value": mv if mv is not None else "",
            "confidence": res.get("confidence"), "readable": int(bool(res.get("readable"))),
            "assessable": int(bool(res.get("assessable"))), "steam_mean": res.get("steam_mean"),
            "steam_min": res.get("steam_min"), "status": res.get("status"),
            "frames_used": res.get("frames_used"), "abs_err": abs_err, "note": res.get("note"),
        })
        fh.flush()
        if not args.keep_crops:
            cleanup_stem(crops_dir, vpath.stem)
        done += 1
        if done % 10 == 0 or done == total:
            print(f"progress {done}/{total} (last id={vid} human={h} -> {mv} {res.get('confidence')})", flush=True)

    fh.close()
    summarize(out_path, args.steam_skip_threshold)
    print(f"\noutput -> {out_path}", flush=True)
    return 0


def summarize(out_path: Path, steam_thr: float) -> None:
    rows = list(csv.DictReader(open(out_path, encoding="utf-8-sig")))

    def acc(subset, label):
        ev = [r for r in subset if r["abs_err"] != ""]
        if not ev:
            print(f"  {label}: 評価対象0件")
            return
        diffs = [int(r["abs_err"]) for r in ev]
        exact = sum(1 for d in diffs if d == 0)
        w1 = sum(1 for d in diffs if d <= 1)
        print(f"  {label}: n={len(ev)} 完全一致 {exact}/{len(ev)} ({exact/len(ev):.0%}) "
              f"/ ±1以内 {w1}/{len(ev)} ({w1/len(ev):.0%}) / MAE {np.mean(diffs):.2f}")

    n = len(rows)
    readable = [r for r in rows if r["readable"] == "1"]
    unreadable = [r for r in rows if r["readable"] == "0"]
    high = [r for r in rows if r["confidence"] == "high"]
    committed = high  # LLM/最終に数値を渡すのは high のみ

    print("\n================ 全件 濃度モデル精度サマリ ================")
    print(f"総件数 {n} / 可読 {len(readable)} / 不可読 {len(unreadable)}")
    print("\n[A] スープ可読フレームありの動画（モデルが数値を出した範囲）")
    acc(readable, "可読すべて")
    acc(high, "うち高信頼(high=最終採用)")
    print("\n[B] 信頼度別")
    for c in ("high", "medium", "none"):
        acc([r for r in rows if r["confidence"] == c], f"confidence={c}")
    print("\n[C] 湯気状態別（不可読動画の内訳）")
    steamy = [r for r in unreadable if r["steam_mean"] not in ("", None) and float(r["steam_mean"]) >= steam_thr]
    light = [r for r in unreadable if r["steam_mean"] not in ("", None) and float(r["steam_mean"]) < steam_thr]
    print(f"  不可読かつ湯気多(>= {steam_thr}) = {len(steamy)} 件 → LLMスキップ(判定不可)")
    print(f"  不可読だが湯気薄(<  {steam_thr}) = {len(light)} 件 → OOD疑い・LLM送信対象")
    # confusion (high のみ)
    print("\n[D] 混同（高信頼のみ, 人間→推定）")
    conf = {}
    for r in high:
        if r["human"] and r["model_value"]:
            conf[(r["human"], r["model_value"])] = conf.get((r["human"], r["model_value"]), 0) + 1
    for (hh, pp), cnt in sorted(conf.items()):
        print(f"    人間{hh} -> 推定{pp}: {cnt}")


if __name__ == "__main__":
    raise SystemExit(main())
