#!/usr/bin/env python3
"""Build a frame-review index for non-map CNS image assets."""
from __future__ import annotations

import argparse
import html
import json
from collections import Counter
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"


def load_json(path: Path, fallback):
    if not path.exists():
        return fallback
    return json.loads(path.read_text(encoding="utf-8"))


def category_for(stem: str) -> str:
    if stem.startswith(("boss", "z")):
        return "enemy-object"
    if stem.startswith("cara_"):
        return "character"
    if stem.startswith("btl_"):
        return "battle"
    if stem.startswith("face_"):
        return "face"
    return "ui-misc"


def category_label(category: str) -> str:
    return {
        "battle": "전투/효과",
        "character": "캐릭터",
        "enemy-object": "몬스터/오브젝트",
        "face": "얼굴",
        "ui-misc": "아이템/UI/문구",
    }.get(category, category)


def review_href_for_asset(asset_key: str, category: str) -> str:
    encoded = html.escape(asset_key, quote=True)
    if category == "enemy-object":
        return f"../web/monster_review.html?asset={encoded}"
    if category == "character":
        return f"../web/field_character_review.html?asset={encoded}"
    if category in {"ui-misc", "face"}:
        return f"../web/ui_window_review.html?asset={encoded}"
    if category == "battle" and asset_key == "btl_etc":
        return f"../web/ui_window_review.html?asset={encoded}"
    if category == "battle":
        return f"../web/battle_effect_visual_review.html?asset={encoded}"
    return f"../web/cns_rect_review.html?asset={encoded}"


def default_frame_hint(stem: str, row: dict) -> dict:
    width = int(row.get("width") or 0)
    height = int(row.get("height") or 0)
    if stem in {"cara_at1", "cara_rs1", "cara_sm1"}:
        return {
            "sourceX": 0,
            "sourceY": 0,
            "sourceWidth": 48,
            "sourceHeight": 64,
            "columns": width // 48 if width else 0,
            "rows": height // 64 if height else 0,
            "frameCount": (width // 48) * (height // 64) if width and height else 0,
            "inference": "confirmed-character-walk-grid",
            "confidence": "high",
        }
    if stem == "face_01":
        return {
            "sourceX": 0,
            "sourceY": 0,
            "sourceWidth": 80,
            "sourceHeight": 80,
            "columns": width // 80 if width else 0,
            "rows": height // 80 if height else 0,
            "frameCount": (width // 80) * (height // 80) if width and height else 0,
            "inference": "review-initial-grid-face-80",
            "confidence": "medium",
        }
    if stem in {"item", "icon"}:
        return {
            "sourceX": 0,
            "sourceY": 0,
            "sourceWidth": 32,
            "sourceHeight": 32,
            "columns": width // 32 if width else 0,
            "rows": height // 32 if height else 0,
            "frameCount": (width // 32) * (height // 32) if width and height else 0,
            "inference": "review-initial-grid-32",
            "confidence": "medium",
        }
    if stem == "num":
        return {
            "sourceX": 0,
            "sourceY": 0,
            "sourceWidth": 16,
            "sourceHeight": 16,
            "columns": width // 16 if width else 0,
            "rows": height // 16 if height else 0,
            "frameCount": (width // 16) * (height // 16) if width and height else 0,
            "inference": "review-initial-grid-16",
            "confidence": "low",
        }
    if stem in {"btl_etc", "cara_efc"}:
        return {
            "sourceX": 0,
            "sourceY": 0,
            "sourceWidth": 64,
            "sourceHeight": 64,
            "columns": width // 64 if width else 0,
            "rows": height // 64 if height else 0,
            "frameCount": (width // 64) * (height // 64) if width and height else 0,
            "inference": "review-initial-grid-effect-64",
            "confidence": "low",
        }
    return {
        "sourceX": 0,
        "sourceY": 0,
        "sourceWidth": min(64, width) if width else 64,
        "sourceHeight": min(64, height) if height else 64,
        "columns": max(1, width // min(64, width)) if width else 0,
        "rows": max(1, height // min(64, height)) if height else 0,
        "frameCount": 0,
        "inference": "review-starting-grid-only",
        "confidence": "low",
    }


def character_frame_index(records: list[dict]) -> dict[str, dict]:
    index: dict[str, dict] = {}
    for record in records:
        file_name = record.get("file")
        if not file_name:
            continue
        rects = []
        labels = []
        for direction in record.get("directions") or []:
            for frame in direction.get("frames") or []:
                rects.append({
                    "x": frame.get("sourceX", 0),
                    "y": frame.get("sourceY", 0),
                    "w": frame.get("width", record.get("frameWidth", 48)),
                    "h": frame.get("height", record.get("frameHeight", 64)),
                    "label": f"{direction.get('name', '?')}:{frame.get('index', 0)}",
                    "bbox": frame.get("bbox"),
                    "opaquePixels": frame.get("opaquePixels"),
                })
                labels.append(f"{direction.get('name', '?')}:{frame.get('index', 0)}")
        for frame in record.get("specialFrames") or []:
            rects.append({
                "x": frame.get("sourceX", 0),
                "y": frame.get("sourceY", 0),
                "w": record.get("frameWidth", 48),
                "h": record.get("frameHeight", 64),
                "label": f"special:r{frame.get('row')}c{frame.get('col')}",
                "opaquePixels": frame.get("opaquePixels"),
            })
        index[file_name] = {
            "source": "out/character_sprite_frames.json",
            "status": "confirmed-character-walk-grid",
            "frameWidth": record.get("frameWidth", 48),
            "frameHeight": record.get("frameHeight", 64),
            "walkFramesPerDirection": record.get("walkFramesPerDirection"),
            "idleFrame": record.get("idleFrame"),
            "movingFrames": record.get("movingFrames") or [],
            "backgroundRgb": record.get("backgroundRgb") or [],
            "rects": rects,
        }
    return index


def battle_enemy_index(summary: dict) -> dict[str, dict]:
    return {
        row.get("enemyCns"): row
        for row in summary.get("spriteAssets") or []
        if row.get("enemyCns")
    }


def descriptor_index(summary: dict) -> dict[str, list[dict]]:
    index: dict[str, list[dict]] = {}
    for row in summary.get("linkedAssetRows") or []:
        name = row.get("name")
        if name:
            index.setdefault(name, []).append(row)
    return index


def build_rows(
    cns_payloads: list[dict],
    character_records: list[dict],
    battle_enemy_summary: dict,
    descriptor_summary: dict,
    out_dir: Path,
) -> list[dict]:
    character_frames = character_frame_index(character_records)
    battle_enemy = battle_enemy_index(battle_enemy_summary)
    descriptors = descriptor_index(descriptor_summary)
    rows = []
    for row in cns_payloads:
        if row.get("kind") != "image":
            continue
        name = str(row.get("name") or "")
        stem = Path(name).stem
        if not name or stem.startswith("map_"):
            continue
        category = category_for(stem)
        battle_row = battle_enemy.get(name) or {}
        descriptor_rows = descriptors.get(name) or []
        character = character_frames.get(name)
        frame_hint = dict(battle_row.get("frameHint") or default_frame_hint(stem, row))
        if character:
            frame_hint.update({
                "sourceX": 0,
                "sourceY": 0,
                "sourceWidth": character.get("frameWidth", 48),
                "sourceHeight": character.get("frameHeight", 64),
                "columns": int(row.get("width") or 0) // int(character.get("frameWidth", 48)),
                "rows": int(row.get("height") or 0) // int(character.get("frameHeight", 64)),
                "frameCount": len(character.get("rects") or []),
                "inference": "confirmed-character-walk-grid",
                "confidence": "high",
            })
        rows.append({
            "assetKey": stem,
            "cns": name,
            "path": f"../extract_fld/{stem}.cns",
            "category": category,
            "categoryLabel": category_label(category),
            "width": row.get("width"),
            "height": row.get("height"),
            "bpp": row.get("bpp"),
            "paletteColors": row.get("paletteColors"),
            "decodedSize": row.get("decodedSize"),
            "pixelBytes": row.get("pixelBytes"),
            "frameHint": frame_hint,
            "confirmedFrames": character,
            "selectionKind": battle_row.get("selectionKind") or category,
            "enemyObjectSprite": bool(battle_row),
            "descriptorRows": descriptor_rows,
            "descriptorRefCount": sum(item.get("resourceDescriptorRefCount", 0) for item in descriptor_rows),
            "sceneEventAnchorCount": sum(item.get("sceneEventAnchorCount", 0) for item in descriptor_rows),
            "runtimeSurfaces": sorted({item.get("runtimeSurface") for item in descriptor_rows if item.get("runtimeSurface")}),
        })
    return sorted(rows, key=lambda item: (item["category"], item["assetKey"]))


def build_summary(rows: list[dict]) -> dict:
    counts = Counter(row["category"] for row in rows)
    return {
        "scope": "Non-map CNS image assets available for frame review.",
        "source": [
            "out/cns_payloads.json",
            "out/character_sprite_frames.json",
            "out/battle_enemy_candidates.json",
            "out/non_party_descriptor_asset_links.json",
        ],
        "status": "non-map-cns-frame-index",
        "assetCount": len(rows),
        "categoryCounts": dict(sorted(counts.items())),
        "rows": rows,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# CNS Frame Assets",
        "",
        "Non-map CNS image assets exposed to the browser frame review workbench.",
        "",
        f"- assets: {summary['assetCount']}",
        f"- category counts: `{json.dumps(summary['categoryCounts'], ensure_ascii=False)}`",
        "",
        "| asset | cns | category | size | frame hint | confirmed |",
        "| --- | --- | --- | ---: | --- | --- |",
    ]
    for row in summary["rows"]:
        hint = row.get("frameHint") or {}
        hint_text = f"{hint.get('sourceWidth')}x{hint.get('sourceHeight')} {hint.get('inference')}"
        confirmed = row.get("confirmedFrames") or {}
        lines.append(
            f"| `{row['assetKey']}` | `{row['cns']}` | {row['category']} | "
            f"{row.get('width')}x{row.get('height')} | {hint_text} | {confirmed.get('status', '-')} |"
        )
    return "\n".join(lines) + "\n"


def html_page(summary: dict) -> str:
    rows = []
    for row in summary["rows"]:
        hint = row.get("frameHint") or {}
        href = review_href_for_asset(row["assetKey"], row["category"])
        rows.append(
            "<tr>"
            f"<td><a href=\"{href}\"><code>{html.escape(row['assetKey'])}</code></a></td>"
            f"<td><code>{html.escape(row['cns'])}</code></td>"
            f"<td>{html.escape(row['categoryLabel'])}</td>"
            f"<td>{row.get('width')}x{row.get('height')}</td>"
            f"<td>{html.escape(str(hint.get('sourceWidth')))}x{html.escape(str(hint.get('sourceHeight')))} · {html.escape(str(hint.get('inference')))}</td>"
            f"<td>{html.escape(str((row.get('confirmedFrames') or {}).get('status') or '-'))}</td>"
            "</tr>"
        )
    cards = "".join(
        f"<div class=\"card\"><strong>{count}</strong><span>{html.escape(category_label(category))}</span></div>"
        for category, count in sorted(summary["categoryCounts"].items())
    )
    return "\n".join([
        "<!doctype html>",
        '<html lang="ko">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>CNS Frame Assets</title>",
        "  <style>",
        "    body{font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#101214;color:#edf2f4;margin:24px;line-height:1.45}",
        "    a{color:#9bd4ff} code{color:#f5eec8}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin:16px 0}",
        "    .card{border:1px solid #303940;background:#151a1f;padding:10px}.card strong{display:block;font-size:22px}.card span{color:#aeb8bd}",
        "    table{border-collapse:collapse;width:100%;font-size:13px}th,td{border:1px solid #303940;padding:7px 9px;text-align:left;vertical-align:top}th{background:#151a1f;position:sticky;top:0}",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>CNS Frame Assets</h1>",
        f"  <p>비맵 CNS 이미지 {summary['assetCount']}개를 프레임 검토 워크벤치에 연결합니다.</p>",
        f"  <div class=\"cards\">{cards}</div>",
        "  <table><thead><tr><th>asset</th><th>cns</th><th>category</th><th>size</th><th>initial frame hint</th><th>confirmed</th></tr></thead>",
        f"  <tbody>{''.join(rows)}</tbody></table>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "cns_frame_assets.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "cns_frame_assets.js").write_text(
        "window.HWANSE_CNS_FRAME_ASSETS = "
        + json.dumps(summary, ensure_ascii=False, separators=(",", ":"))
        + ";\n",
        encoding="utf-8",
    )


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--cns-payloads", type=Path, default=OUT / "cns_payloads.json")
    parser.add_argument("--character-frames", type=Path, default=OUT / "character_sprite_frames.json")
    parser.add_argument("--battle-enemy-candidates", type=Path, default=OUT / "battle_enemy_candidates.json")
    parser.add_argument("--non-party-descriptor-links", type=Path, default=OUT / "non_party_descriptor_asset_links.json")
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    rows = build_rows(
        load_json(args.cns_payloads, []),
        load_json(args.character_frames, []),
        load_json(args.battle_enemy_candidates, {}),
        load_json(args.non_party_descriptor_links, {}),
        args.out_dir,
    )
    summary = build_summary(rows)
    write_outputs(summary, args.out_dir)
    print(f"wrote {summary['assetCount']} non-map CNS frame assets -> {args.out_dir / 'cns_frame_assets.json'}")
    return 0


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