#!/usr/bin/env python3
"""Build a compact review of promoted UI CNS sheet consumers.

This is not a raw grid page.  It records the EXE-backed places where the
runtime consumes status.cns, icon.cns, num.cns, and 01234567.cns in the
normal HUD/menu surfaces.
"""
from __future__ import annotations

import html
import json
from pathlib import Path
from typing import Any


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

MENU_JSON = OUT / "menu_right_panel_ui_review.json"
HUD_JSON = OUT / "hud_normal_static_hint_review.json"
STATUS_JSON = OUT / "status_menu_ui_expression_review.json"
HUD_PREVIEW_JSON = OUT / "hud_menu_preview.json"

OUT_JSON = OUT / "ui_cns_consumer_review.json"
OUT_HTML = WEB / "ui_cns_consumer_review.html"
WEB_HTML = WEB / "ui_cns_consumer_review.html"


def read_json(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8"))


def rect_label(rect: dict[str, Any] | None) -> str:
    if not rect:
        return "-"
    if "w" not in rect and "h" not in rect:
        return f"{int(rect.get('x', 0))},{int(rect.get('y', 0))}"
    return f"{int(rect.get('x', 0))},{int(rect.get('y', 0))} {int(rect.get('w', 0))}x{int(rect.get('h', 0))}"


def consumer(
    *,
    cns: str,
    label: str,
    role: str,
    source: dict[str, Any] | None,
    dest: dict[str, Any] | None = None,
    evidence: str = "",
    status: str = "exe-grounded",
    group: str = "",
    refs: list[str] | None = None,
) -> dict[str, Any]:
    return {
        "cns": cns,
        "group": group,
        "label": label,
        "role": role,
        "source": source or {},
        "dest": dest or {},
        "sourceLabel": rect_label(source),
        "destLabel": rect_label(dest),
        "evidence": evidence,
        "status": status,
        "refs": refs or [],
    }


def collect_status_consumers(menu: dict[str, Any], hud: dict[str, Any], preview: dict[str, Any]) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    cursor = menu.get("cursorModel") or {}

    for key, label in [
        ("topCursor", "상단 메뉴 커서"),
        ("pressedTopCursor", "상단 메뉴 pressed 커서"),
        ("subCursor", "하위 메뉴 커서"),
        ("pressedSubCursor", "하위 메뉴 pressed 커서"),
    ]:
        model = cursor.get(key) or {}
        rows.append(
            consumer(
                cns="status.cns",
                group="cursor",
                label=label,
                role=model.get("note") or "메뉴 선택 상태를 표시한다.",
                source=model.get("source") or {},
                dest=model.get("startDest") or {},
                evidence=model.get("evidenceLevel") or "object VM 0x87/status draw consumer",
                refs=[model.get("sourceRectIndex") is not None and f"source rect #{model.get('sourceRectIndex')}" or ""],
            )
        )

    page_arrows = cursor.get("pageArrows") or {}
    placement = page_arrows.get("placement") or {}
    for key, label, dest_key in [
        ("left", "하위 페이지 왼쪽 화살표", "leftDest"),
        ("leftAlt", "하위 페이지 왼쪽 화살표 alt", "leftDest"),
        ("right", "하위 페이지 오른쪽 화살표", "rightDest"),
        ("rightAlt", "하위 페이지 오른쪽 화살표 alt", "rightDest"),
    ]:
        model = page_arrows.get(key) or {}
        rows.append(
            consumer(
                cns="status.cns",
                group="page arrow",
                label=label,
                role=model.get("note") or page_arrows.get("note", ""),
                source=model.get("source") or {},
                dest=(placement.get(dest_key) or {}),
                evidence=f"{page_arrows.get('handlerVaHex', '')} direct blit {page_arrows.get('blitVaHex', '')}",
                refs=[model.get("encodedSourceIdHex", ""), model.get("evidenceVaHex", "")],
            )
        )

    stack = cursor.get("stackArrows") or {}
    stack_dest = stack.get("placement") or {}
    for pair_key, label, dest_key in [
        ("leftPair", "상태창 actor 위쪽 전환 화살표", "leftDest"),
        ("rightPair", "상태창 actor 아래쪽 전환 화살표", "rightDest"),
    ]:
        pair = stack.get(pair_key) or {}
        for idx, source in enumerate(pair.get("sourceRects") or []):
            rows.append(
                consumer(
                    cns="status.cns",
                    group="actor stack arrow",
                    label=f"{label} #{pair.get('sourceRectIndices', ['?'])[idx]}",
                    role=stack.get("note", ""),
                    source=source,
                    dest=stack_dest.get(dest_key) or {},
                    evidence=f"{stack.get('handlerVaHex', '')} direct blit {stack.get('blitVaHex', '')}",
                    refs=[(pair.get("encodedSourceIdsHex") or [""])[idx], pair.get("evidenceVaHex", "")],
                )
            )

    mp = (menu.get("rightMenuLayout") or {}).get("skillMpHeader") or {}
    rows.append(
        consumer(
            cns="status.cns",
            group="menu header",
            label="기술 목록 MP 타이틀",
            role="#4 하위 기술 목록 제목 우측의 MP 컬럼 라벨.",
            source=mp.get("sourceRect") or {},
            dest={"x": mp.get("x", 0), "y": mp.get("y", 0), "w": mp.get("w", 0), "h": mp.get("h", 0)},
            evidence=mp.get("source", ""),
            refs=[mp.get("sourceIdHex", "")],
        )
    )

    bar_sources = ((hud.get("exe_evidence") or {}).get("status_bar_dynamic_sources") or {}).get("rects") or []
    for row in bar_sources:
        rows.append(
            consumer(
                cns="status.cns",
                group="normal HUD gauge",
                label=row.get("name", "bar source"),
                role="평상시 #1 HP/MP/EXP 게이지의 배경, 현재값, 지연 감소분 source.",
                source=row.get("rect") or {},
                evidence="0x00420a6a..0x00420e51 gauge draw path",
                refs=row.get("text_refs") or [],
            )
        )

    record_table = preview.get("normalHudStatusRecordTable") or {}
    for row in (record_table.get("records") or [])[:9]:
        rows.append(
            consumer(
                cns="status.cns",
                group="normal HUD gauge placement",
                label=f"slot {row.get('actor_slot')} {row.get('stat')} 게이지",
                role="source는 위 bar source를 쓰고, destination은 EXE record table의 x/y에서 나온다.",
                source={"x": 0, "y": 16, "w": 88, "h": 8},
                dest=row.get("gauge_back_dest") or {},
                evidence=f"record {row.get('record_va_hex')} / draw_gauge {record_table.get('consumer_vas', {}).get('draw_gauge')}",
                refs=[row.get("record_va_hex", "")],
            )
        )
    return rows


def walk_menu_entries(entries: list[dict[str, Any]], path: str = "") -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    for entry in entries:
        label = str(entry.get("label") or entry.get("key") or "")
        new_path = f"{path} / {label}" if path else label
        rows.append({"path": new_path, "entry": entry})
        rows.extend(walk_menu_entries(entry.get("children") or [], new_path))
        rows.extend(walk_menu_entries(entry.get("pages") or [], new_path))
    return rows


def collect_icon_consumers(menu: dict[str, Any]) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    payload = menu.get("rightMenuPayload") or {}
    for row in payload.get("normalTopIconRows") or []:
        rows.append(
            consumer(
                cns="icon.cns",
                group="top menu",
                label=row.get("normalTopMenuLabel") or f"top #{row.get('normalTopMenuIndex')}",
                role=row.get("interpretation", ""),
                source={"x": int(row.get("iconId", 0)) % 20 * 32, "y": int(row.get("iconId", 0)) // 20 * 32, "w": 32, "h": 32},
                dest={"x": row.get("previewX", 0), "y": row.get("previewY", 0), "w": 32, "h": 32},
                evidence=row.get("vaHex", ""),
                refs=[f"icon #{row.get('iconId')}"],
            )
        )

    seen: set[tuple[str, int, str]] = set()
    for walked in walk_menu_entries(menu.get("menuModel") or []):
        entry = walked["entry"]
        icon = entry.get("icon") or {}
        if icon.get("cns") != "icon.cns":
            continue
        key = (walked["path"], int(icon.get("cellIndex", -1)), str(entry.get("sourceVaHex") or ""))
        if key in seen:
            continue
        seen.add(key)
        rows.append(
            consumer(
                cns="icon.cns",
                group="detail row",
                label=walked["path"],
                role=entry.get("summary") or entry.get("source") or "하위 메뉴 row icon.",
                source={"x": icon.get("x", 0), "y": icon.get("y", 0), "w": icon.get("w", 32), "h": icon.get("h", 32)},
                evidence=entry.get("sourceVaHex") or entry.get("descriptionEvidence") or "",
                status=entry.get("evidenceStatus") or "exe-grounded",
                refs=[f"icon #{icon.get('cellIndex')}"],
            )
        )
    return rows


def collect_number_consumers(preview: dict[str, Any]) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    info = preview.get("normalHudRegion2Info") or {}
    gold = info.get("goldValue") or {}
    rows.append(
        consumer(
            cns="num.cns",
            group="gold",
            label="#2 번 돈 숫자",
            role="평상시 상단 메뉴 상태의 우하단 정보 패널에서 골드 값을 16x16 숫자로 표시한다.",
            source={"x": 0, "y": 0, "w": 160, "h": 16},
            dest={"x": gold.get("onesDigitX", 0), "y": gold.get("y", 0), "w": 48, "h": 16},
            evidence=gold.get("opcodeVaHex", ""),
            refs=[gold.get("opcodeHex", ""), gold.get("numberCns", "")],
        )
    )

    table = preview.get("normalHudStatusRecordTable") or {}
    for row in (table.get("records") or [])[:9]:
        for value_kind, dest_key in [("현재값", "number_current_dest"), ("최대값", "number_max_dest")]:
            rows.append(
                consumer(
                    cns="01234567.cns",
                    group="normal HUD stat number",
                    label=f"slot {row.get('actor_slot')} {row.get('stat')} {value_kind}",
                    role="평상시 #1 HP/MP/EXP 수치를 8x8 흰색 숫자 glyph로 우정렬 표시한다.",
                    source={"x": 0, "y": 0, "w": 80, "h": 8},
                    dest=row.get(dest_key) or {},
                    evidence=table.get("draw_formula", {}).get("number_current", ""),
                    refs=[row.get("record_va_hex", "")],
                )
            )
    return rows


def build_payload() -> dict[str, Any]:
    menu = read_json(MENU_JSON)
    hud = read_json(HUD_JSON)
    status = read_json(STATUS_JSON)
    preview = read_json(HUD_PREVIEW_JSON)
    consumers = {
        "status.cns": collect_status_consumers(menu, hud, preview),
        "icon.cns": collect_icon_consumers(menu),
        "num.cns": [row for row in collect_number_consumers(preview) if row["cns"] == "num.cns"],
        "01234567.cns": [row for row in collect_number_consumers(preview) if row["cns"] == "01234567.cns"],
    }
    return {
        "version": 1,
        "kind": "hwanse-ui-cns-consumer-review",
        "sourceArtifacts": {
            "menuRightPanel": str(MENU_JSON.relative_to(ROOT)),
            "hudNormalStaticHint": str(HUD_JSON.relative_to(ROOT)),
            "statusMenuExpression": str(STATUS_JSON.relative_to(ROOT)),
            "hudMenuPreview": str(HUD_PREVIEW_JSON.relative_to(ROOT)),
        },
        "summary": [
            "status.cns는 raw grid가 아니라 cursor/page arrow/actor stack arrow/MP header/HUD gauge source로 소비된다.",
            "icon.cns는 상단 메뉴 6개와 하위 메뉴 row icon의 주 source다.",
            "num.cns는 #2 골드 표시용 16x16 숫자, 01234567.cns는 #1 HP/MP/EXP 8x8 숫자다.",
            "item.cns 장비/소모품 아이콘은 status/icon/num 소비처와 별도이므로 이 페이지에서는 링크 근거만 유지한다.",
        ],
        "consumers": consumers,
        "counts": {key: len(value) for key, value in consumers.items()},
    }


def table_html(rows: list[dict[str, Any]], title: str, cns: str) -> str:
    body = []
    for index, row in enumerate(rows):
        source = row.get("source") or {}
        refs = ", ".join(str(value) for value in row.get("refs") or [] if value)
        body.append(
            "<tr>"
            f"<td><canvas class=\"thumb\" width=\"96\" height=\"48\" data-cns=\"{html.escape(cns)}\" "
            f"data-x=\"{int(source.get('x', 0))}\" data-y=\"{int(source.get('y', 0))}\" "
            f"data-w=\"{int(source.get('w', 0))}\" data-h=\"{int(source.get('h', 0))}\"></canvas></td>"
            f"<td><strong>{html.escape(str(row.get('label', '')))}</strong><br><span>{html.escape(str(row.get('group', '')))}</span></td>"
            f"<td>{html.escape(str(row.get('role', '')))}</td>"
            f"<td><code>{html.escape(str(row.get('sourceLabel', '')))}</code></td>"
            f"<td><code>{html.escape(str(row.get('destLabel', '')))}</code></td>"
            f"<td>{html.escape(str(row.get('status', '')))}<br><small>{html.escape(str(row.get('evidence', '')))}</small><br><small>{html.escape(refs)}</small></td>"
            "</tr>"
        )
    return f"""
    <section class="panel">
      <header><h2>{html.escape(title)}</h2><span>{len(rows)} consumers</span></header>
      <div class="table-wrap">
        <table>
          <thead><tr><th>source</th><th>항목</th><th>역할</th><th>source rect</th><th>dest</th><th>근거</th></tr></thead>
          <tbody>{''.join(body)}</tbody>
        </table>
      </div>
    </section>
    """


def build_html(payload: dict[str, Any]) -> str:
    payload_json = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
    sections = "\n".join(
        table_html(payload["consumers"][cns], cns, cns)
        for cns in ["status.cns", "icon.cns", "num.cns", "01234567.cns"]
    )
    summary = "".join(f"<li>{html.escape(row)}</li>" for row in payload["summary"])
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>UI CNS 소비처 리뷰</title>
  <script src="engine/cns/renderer.js"></script>
  <style>
    :root {{ color-scheme: light; --bg:#f5f6f8; --panel:#fff; --line:#d9dee7; --text:#20242b; --muted:#687080; --chip:#eef2f7; }}
    * {{ box-sizing:border-box; }}
    body {{ margin:0; background:var(--bg); color:var(--text); font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }}
    main {{ width:min(1240px, calc(100vw - 24px)); margin:0 auto; padding:20px 0 34px; }}
    h1 {{ margin:0 0 8px; font-size:26px; }}
    h2 {{ margin:0; font-size:18px; }}
    p, li {{ line-height:1.5; }}
    .lead {{ color:var(--muted); }}
    .links {{ display:flex; flex-wrap:wrap; gap:7px; margin:12px 0 16px; }}
    .chip {{ display:inline-flex; min-height:30px; align-items:center; padding:5px 9px; border:1px solid var(--line); border-radius:7px; background:var(--chip); color:var(--text); text-decoration:none; }}
    .panel {{ border:1px solid var(--line); border-radius:8px; background:var(--panel); margin:12px 0; overflow:hidden; }}
    .panel > header {{ display:flex; justify-content:space-between; gap:12px; padding:12px 14px; background:#fafbfd; border-bottom:1px solid var(--line); }}
    .panel > header span {{ color:var(--muted); font-size:13px; }}
    .summary {{ padding:12px 16px; }}
    .table-wrap {{ overflow:auto; }}
    table {{ width:100%; border-collapse:collapse; min-width:980px; }}
    th, td {{ padding:9px 10px; border-top:1px solid #edf1f5; vertical-align:top; text-align:left; font-size:13px; }}
    thead th {{ border-top:0; background:#fbfcfe; color:#485164; }}
    td span, small {{ color:var(--muted); }}
    code {{ font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:12px; }}
    canvas.thumb {{ width:96px; height:48px; image-rendering:pixelated; background:#192033; border:1px solid #cfd6e2; }}
    @media (max-width: 760px) {{ main {{ width:min(100vw - 12px, 1240px); padding-top:14px; }} .panel > header {{ display:block; }} }}
  </style>
</head>
<body>
<main>
  <h1>UI CNS 소비처 리뷰</h1>
  <p class="lead">EXE 검증으로 승격된 <code>status.cns</code>, <code>icon.cns</code>, <code>num.cns</code>, <code>01234567.cns</code> 소비처를 한 곳에 모은 페이지입니다.</p>
  <nav class="links">
    <a class="chip" href="index.html">index</a>
    <a class="chip" href="hud_menu_preview.html">HUD menu preview</a>
    <a class="chip" href="../out/menu_right_panel_ui_review.json">right menu JSON</a>
    <a class="chip" href="status_menu_ui_expression_review.html">status UI expression</a>
    <a class="chip" href="ui_grid_review.html">UI grid</a>
  </nav>
  <section class="panel summary">
    <h2>요약</h2>
    <ul>{summary}</ul>
  </section>
  {sections}
</main>
<script>
const payload = {payload_json};
async function drawThumbs() {{
  const cache = new Map();
  async function sheet(name) {{
    if (!cache.has(name)) cache.set(name, await window.HWANSE_CNS_RENDERER.loadImageCanvas(name));
    return cache.get(name);
  }}
  for (const canvas of document.querySelectorAll("canvas.thumb")) {{
    const ctx = canvas.getContext("2d");
    ctx.imageSmoothingEnabled = false;
    const cns = canvas.dataset.cns;
    const x = Number(canvas.dataset.x || 0);
    const y = Number(canvas.dataset.y || 0);
    const w = Number(canvas.dataset.w || 1);
    const h = Number(canvas.dataset.h || 1);
    const src = await sheet(cns);
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    const scale = Math.max(1, Math.floor(Math.min(canvas.width / Math.max(1, w), canvas.height / Math.max(1, h))));
    const dw = w * scale;
    const dh = h * scale;
    ctx.drawImage(src, x, y, w, h, Math.floor((canvas.width - dw) / 2), Math.floor((canvas.height - dh) / 2), dw, dh);
  }}
}}
drawThumbs().catch((error) => console.error(error));
</script>
</body>
</html>
"""


def main() -> None:
    payload = build_payload()
    OUT_JSON.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    html_text = build_html(payload)
    OUT_HTML.write_text(html_text, encoding="utf-8")
    WEB_HTML.write_text(html_text, encoding="utf-8")
    print(f"wrote {OUT_JSON}")
    print(f"wrote {OUT_HTML}")
    print(f"wrote {WEB_HTML}")


if __name__ == "__main__":
    main()
