#!/usr/bin/env python3
"""Promote HUD runtime surface handles that can be tied to static CNS evidence.

The HUD-open runtime trace does not name CNS files directly.  It does expose
surface handles, source rectangles, destination rectangles, and call sites.
This report only promotes handles when the runtime draw pattern also matches a
static EXE/CNS consumer that was already grounded.
"""
from __future__ import annotations

import html
import json
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any


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

RUNTIME_REVIEW = OUT / "runtime_trace_hud_open_review.json"
CNS_CATALOG = OUT / "cns_precut_catalog_review.json"
MENU_RIGHT_PANEL = OUT / "menu_right_panel_ui_review.json"
STATUS_MENU = OUT / "status_menu_ui_expression_review.json"
SURFACE_CATALOG = OUT / "surface_wrapper_catalog_review.json"

JSON_OUT = OUT / "hud_runtime_surface_handle_review.json"
HTML_OUT = WEB / "hud_runtime_surface_handle_review.html"

TEXT_SURFACE = "0x100979C0"
FRAME_SURFACE = "0x1004C438"
SCREEN_SURFACE = "0x10001330"

REGION_BY_RECT = {
    "0,0,416,352": {"regionIndex": 6, "name": "left status/menu panel"},
    "416,0,640,96": {"regionIndex": 3, "name": "right top menu panel"},
    "416,96,640,352": {"regionIndex": 4, "name": "right submenu panel"},
    "0,352,416,480": {"regionIndex": 1, "name": "bottom left HUD panel"},
    "416,352,640,480": {"regionIndex": 2, "name": "bottom right info panel"},
}


def h(value: Any) -> str:
    return html.escape("" if value is None else str(value), quote=True)


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


def compact_row(row: dict[str, Any]) -> dict[str, Any]:
    parsed = row.get("parsed") or {}
    return {
        "seq": row.get("seq"),
        "frame": row.get("frame"),
        "pc": row.get("pc"),
        "name": row.get("name"),
        "source": row.get("source"),
        "destination": row.get("destination"),
        "sourceLabel": row.get("sourceLabel"),
        "destinationLabel": row.get("destinationLabel"),
        "sourceResourceName": row.get("sourceResourceName"),
        "destinationResourceName": row.get("destinationResourceName"),
        "sourceSurfaceSize": row.get("sourceSurfaceSize"),
        "destinationSurfaceSize": row.get("destinationSurfaceSize"),
        "flags": row.get("flags"),
        "srcRect": parsed.get("srcRect"),
        "dstRect": parsed.get("dstRect"),
        "dstPoint": parsed.get("dstPoint"),
        "srcColorKey": parsed.get("srcColorKey"),
        "appliedColorKey": parsed.get("appliedColorKey"),
        "detail": row.get("detail"),
    }


def summarize_pair(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
    grouped: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list)
    for row in rows:
        grouped[(str(row.get("source")), str(row.get("destination")))].append(row)

    out: list[dict[str, Any]] = []
    for (source, destination), pair_rows in sorted(grouped.items()):
        src_rects = Counter((row.get("parsed") or {}).get("srcRect") for row in pair_rows)
        dst_values = Counter(
            (row.get("parsed") or {}).get("dstPoint") or (row.get("parsed") or {}).get("dstRect")
            for row in pair_rows
        )
        out.append(
            {
                "source": source,
                "destination": destination,
                "count": len(pair_rows),
                "pcCounts": dict(Counter(row.get("pc") for row in pair_rows)),
                "nameCounts": dict(Counter(row.get("name") for row in pair_rows)),
                "flagsCounts": dict(Counter(row.get("flags") for row in pair_rows)),
                "srcRectTop": src_rects.most_common(10),
                "dstTop": dst_values.most_common(10),
                "sampleRows": [compact_row(row) for row in pair_rows[:8]],
            }
        )
    return out


def rows_for(rows: list[dict[str, Any]], source: str | None = None, destination: str | None = None) -> list[dict[str, Any]]:
    out = []
    for row in rows:
        if source and str(row.get("source")).lower() != source.lower():
            continue
        if destination and str(row.get("destination")).lower() != destination.lower():
            continue
        out.append(row)
    return out


def cns_row(catalog: dict[str, Any], asset: str) -> dict[str, Any]:
    for row in catalog.get("rows") or []:
        if row.get("asset") == asset:
            return row
    return {}


def rect_set(rows: list[dict[str, Any]]) -> list[str]:
    return sorted({str((row.get("parsed") or {}).get("srcRect")) for row in rows})


def handle_labels(trace: dict[str, Any]) -> dict[str, dict[str, Any]]:
    labels: dict[str, dict[str, Any]] = {}
    for row in trace.get("surfaceSnapshotRows") or []:
        handle = str(row.get("surfaceHandle") or "")
        if not handle:
            continue
        labels.setdefault(handle, {}).update(
            {
                "surfaceHandle": handle,
                "sourceLabel": row.get("sourceLabel"),
                "sourceResourceName": row.get("sourceResourceName"),
                "sourceSurfaceSize": row.get("sourceSurfaceSize"),
                "resourceName": row.get("resourceName"),
                "resourceType": row.get("resourceType"),
                "archiveOffset": row.get("archiveOffset"),
                "imageWidth": row.get("imageWidth"),
                "imageHeight": row.get("imageHeight"),
                "colorKey": row.get("colorKey"),
                "pixelHash": row.get("pixelHash"),
                "paletteHash": row.get("paletteHash"),
                "notes": row.get("notes"),
            }
        )
    for row in trace.get("resourceSurfaceLabelRows") or []:
        handle = str(row.get("surfaceHandle") or "")
        if not handle:
            continue
        labels.setdefault(handle, {}).update(
            {
                "surfaceHandle": handle,
                "sourceLabel": row.get("sourceLabel"),
                "sourceResourceName": row.get("sourceResourceName"),
                "sourceSurfaceSize": row.get("sourceSurfaceSize"),
                "resourceName": row.get("resourceName"),
                "resourceType": row.get("resourceType"),
                "archiveOffset": row.get("archiveOffset"),
                "imageWidth": row.get("imageWidth"),
                "imageHeight": row.get("imageHeight"),
                "colorKey": row.get("colorKey"),
                "pixelHash": row.get("pixelHash"),
                "notes": row.get("notes"),
            }
        )
    return labels


def choose_trace(runtime: dict[str, Any]) -> dict[str, Any]:
    traces = runtime.get("traces") or [{}]
    labeled = [trace for trace in traces if trace.get("runtimeSignals", {}).get("resourceSurfaceLabelCount")]
    if labeled:
        return sorted(labeled, key=lambda trace: str((trace.get("manifest") or {}).get("exported_at") or ""))[-1]
    return traces[-1] if traces else {}


def resource_label_for(labels: dict[str, dict[str, Any]], handle: str) -> dict[str, Any]:
    return labels.get(handle, {})


def build_handle_rows(draw_rows: list[dict[str, Any]], catalog: dict[str, Any], labels: dict[str, dict[str, Any]]) -> list[dict[str, Any]]:
    window_rows = rows_for(draw_rows, "0x1025D108", TEXT_SURFACE)
    status_rows = rows_for(draw_rows, "0x1025D038", TEXT_SURFACE)
    hud_to_frame = rows_for(draw_rows, TEXT_SURFACE, FRAME_SURFACE)
    frame_to_screen = rows_for(draw_rows, FRAME_SURFACE, SCREEN_SURFACE)
    title_tile_rows = rows_for(draw_rows, "0x102113D0", FRAME_SURFACE)
    effect_rows = rows_for(draw_rows, "0x10211458", FRAME_SURFACE)
    actor_like_rows = []
    for handle in ("0x10211280", "0x10211284", "0x10211288"):
        actor_like_rows.extend(rows_for(draw_rows, handle, FRAME_SURFACE))

    status_catalog = cns_row(catalog, "status")
    window_catalog = catalog.get("windowCatalog") or cns_row(catalog, "window")

    rows = [
        {
            "handle": "0x1025D108",
            "classification": "window.cns source surface",
            "confidence": "High",
            "promotion": "runtime-confirmed",
            "whyPromoted": [
                "The handle only draws into the HUD text/window surface 0x100979C0 in this capture.",
                "The first draw sequence is the exact 16x16 window-template top row: left cap 0,0,16,16, repeated middle 16,0,32,16, right cap 32,0,48,16.",
                "The static catalog classifies window.cns as a template/region system rather than a raw source-rect table.",
            ],
            "staticEvidence": {
                "asset": "window",
                "cns": "window.cns",
                "mode": window_catalog.get("mode"),
                "status": window_catalog.get("status"),
                "templateCount": window_catalog.get("templateCount"),
                "regionCount": window_catalog.get("regionCount"),
            },
            "resourceLabel": resource_label_for(labels, "0x1025D108"),
            "runtimeEvidence": {
                "rowCount": len(window_rows),
                "sourceRects": rect_set(window_rows),
                "sampleRows": [compact_row(row) for row in window_rows[:16]],
            },
        },
        {
            "handle": "0x1025D038",
            "classification": "status.cns source surface",
            "confidence": "High",
            "promotion": "runtime-confirmed-with-static-consumer-match",
            "whyPromoted": [
                "The handle draws only status-sized cursor/header/arrow rectangles into the HUD text/window surface.",
                "Runtime rect 96,0,112,16 at 440,72 matches the grounded top-menu cursor consumer.",
                "Runtime rect 192,16,224,32 at 600,328 matches the grounded detail-page right-arrow consumer.",
                "Runtime rect 32,32,64,48 at 600,104 matches the grounded MP/header status.cns consumer.",
                "The static CNS catalog classifies status.cns as descriptor +4 source-rect table 0x0047e3e0 with 23 frames.",
            ],
            "staticEvidence": {
                "asset": "status",
                "cns": "status.cns",
                "mode": status_catalog.get("mode"),
                "status": status_catalog.get("status"),
                "bestTableStartVaHex": status_catalog.get("bestTableStartVaHex"),
                "bestFrameCount": status_catalog.get("bestFrameCount"),
                "knownConsumers": [
                    "menu_right_panel_ui_review: status.cns #3 top menu cursor at x=440+index*32,y=72",
                    "menu_right_panel_ui_review: status.cns #15/#16/#17/#18 page arrows at x=424/600,y=328",
                    "menu_right_panel_ui_review: status.cns MP header at x=600,y=104",
                ],
            },
            "resourceLabel": resource_label_for(labels, "0x1025D038"),
            "runtimeEvidence": {
                "rowCount": len(status_rows),
                "sourceRects": rect_set(status_rows),
                "sampleRows": [compact_row(row) for row in status_rows],
            },
        },
        {
            "handle": TEXT_SURFACE,
            "classification": "assembled HUD text/window surface",
            "confidence": "High",
            "promotion": "runtime-confirmed",
            "whyPromoted": [
                "window.cns/status.cns draws and GDI TextOutA writes target this surface.",
                "The surface is copied to frame surface 0x1004C438 by the known HUD region rectangles.",
            ],
            "staticEvidence": {
                "regions": REGION_BY_RECT,
            },
            "runtimeEvidence": {
                "rowCount": len(hud_to_frame),
                "sourceRects": rect_set(hud_to_frame),
                "sampleRows": [compact_row(row) for row in hud_to_frame[:12]],
            },
        },
        {
            "handle": FRAME_SURFACE,
            "classification": "frame/backing composition surface",
            "confidence": "High",
            "promotion": "runtime-confirmed",
            "whyPromoted": [
                "Field/map/sprite/HUD sources draw onto this surface.",
                "The completed 640x480 frame is copied to 0x10001330 every visible frame.",
            ],
            "runtimeEvidence": {
                "rowCount": len(frame_to_screen),
                "sourceRects": rect_set(frame_to_screen),
                "sampleRows": [compact_row(row) for row in frame_to_screen[:6]],
            },
        },
        {
            "handle": SCREEN_SURFACE,
            "classification": "screen/backbuffer destination",
            "confidence": "Medium",
            "promotion": "runtime-observed-destination",
            "whyPromoted": [
                "The handle receives repeated full-frame 640x480 Blt copies from 0x1004C438.",
                "The trace does not include DirectDraw surface creation labels, so the exact front/back role remains runtime-observed rather than statically named.",
            ],
            "runtimeEvidence": {
                "incomingRows": len(frame_to_screen),
                "sampleRows": [compact_row(row) for row in frame_to_screen[:6]],
            },
        },
        {
            "handle": "0x102113D0",
            "classification": "title.cns source surface used by field/title-tile draw helper",
            "confidence": "High filename / Medium role",
            "promotion": "resource-label-confirmed-role-review",
            "whyPromoted": [
                "The new runtime trace labels this handle as title.cns with archive offset and pixel hash.",
                "It draws 16x16 source rects directly to the frame surface before the HUD surface is composed.",
                "The CNS filename is now confirmed; the engine role is still review-grade because this field HUD trace does not explain why title.cns is used as a tile-like source here.",
            ],
            "resourceLabel": resource_label_for(labels, "0x102113D0"),
            "runtimeEvidence": {
                "rowCount": len(title_tile_rows),
                "sourceRects": rect_set(title_tile_rows),
                "sampleRows": [compact_row(row) for row in title_tile_rows[:12]],
            },
        },
        {
            "handle": "0x10211458",
            "classification": "btl_efc.cns source surface used by 48x64 color-keyed draw",
            "confidence": "High filename / Medium role",
            "promotion": "resource-label-confirmed-role-review",
            "whyPromoted": [
                "The new runtime trace labels this handle as btl_efc.cns with archive offset and pixel hash.",
                "It draws a 48x64 color-keyed rectangle to the frame surface in the same field HUD capture.",
                "The CNS filename is confirmed; the exact field role of this battle-effect surface needs a targeted draw-context trace if it matters.",
            ],
            "resourceLabel": resource_label_for(labels, "0x10211458"),
            "runtimeEvidence": {
                "rowCount": len(effect_rows),
                "sourceRects": rect_set(effect_rows),
                "sampleRows": [compact_row(row) for row in effect_rows[:12]],
            },
        },
        {
            "handle": "0x10211280 / 0x10211284 / 0x10211288",
            "classification": "labeled actor-like draw source surfaces",
            "confidence": "High filenames / Medium semantic role",
            "promotion": "resource-label-confirmed-role-review",
            "whyPromoted": [
                "The new runtime trace labels 0x10211280 as cara_sm1.cns, 0x10211284 as cara_rs1.cns, and 0x10211288 as title.cns.",
                "All three draw the same 48x64 frame-sized rect to the frame surface with separate color keys.",
                "The file labels are confirmed. The title.cns entry in an actor-like slot is intentionally left as a role question, not overwritten by older assumptions.",
            ],
            "resourceLabel": {
                "0x10211280": resource_label_for(labels, "0x10211280"),
                "0x10211284": resource_label_for(labels, "0x10211284"),
                "0x10211288": resource_label_for(labels, "0x10211288"),
            },
            "runtimeEvidence": {
                "rowCount": len(actor_like_rows),
                "sourceRects": rect_set(actor_like_rows),
                "sampleRows": [compact_row(row) for row in actor_like_rows[:12]],
            },
        },
    ]
    return rows


def build() -> dict[str, Any]:
    runtime = read_json(RUNTIME_REVIEW)
    catalog = read_json(CNS_CATALOG)
    menu_right = read_json(MENU_RIGHT_PANEL)
    status_menu = read_json(STATUS_MENU)
    surface_catalog = read_json(SURFACE_CATALOG)
    trace = choose_trace(runtime)
    draw_rows = trace.get("drawSurfaceRows") or []
    text_rows = trace.get("textDrawRows") or []
    labels = handle_labels(trace)

    handle_rows = build_handle_rows(draw_rows, catalog, labels)
    promoted = [row for row in handle_rows if "confirmed" in str(row.get("promotion", ""))]
    pending = [row for row in handle_rows if "confirmed" not in str(row.get("promotion", ""))]

    text_destinations = Counter(str(row.get("destination")) for row in text_rows)
    surface_pairs = summarize_pair(draw_rows)

    return {
        "version": 1,
        "kind": "hwanse-hud-runtime-surface-handle-review",
        "status": "hud-window-status-surface-handles-promoted",
        "source": str(Path(__file__).relative_to(ROOT)),
        "inputs": {
            "runtimeTrace": str(RUNTIME_REVIEW.relative_to(ROOT)),
            "cnsCatalog": str(CNS_CATALOG.relative_to(ROOT)),
            "menuRightPanel": str(MENU_RIGHT_PANEL.relative_to(ROOT)),
            "statusMenuExpression": str(STATUS_MENU.relative_to(ROOT)),
            "surfaceWrapperCatalog": str(SURFACE_CATALOG.relative_to(ROOT)),
        },
        "trace": {
            "traceId": trace.get("traceId"),
            "eventCount": trace.get("eventCount"),
            "frameRange": trace.get("frameRange"),
            "classification": trace.get("classification"),
            "jsonlPath": trace.get("jsonlPath"),
            "exportedAt": (trace.get("manifest") or {}).get("exported_at"),
        },
        "summary": {
            "drawSurfaceRows": len(draw_rows),
            "textDrawRows": len(text_rows),
            "surfaceSnapshotRows": len(trace.get("surfaceSnapshotRows") or []),
            "resourceSurfaceLabelRows": len(trace.get("resourceSurfaceLabelRows") or []),
            "resourceLabelCount": len(labels),
            "surfacePairCount": len(surface_pairs),
            "promotedHandleCount": len(promoted),
            "pendingHandleCount": len(pending),
            "textSurface": TEXT_SURFACE,
            "frameSurface": FRAME_SURFACE,
            "screenSurface": SCREEN_SURFACE,
            "textDestinations": dict(text_destinations),
            "windowCatalogMode": (catalog.get("windowCatalog") or {}).get("mode"),
            "menuRightPanelStatus": menu_right.get("status"),
            "statusMenuStatus": status_menu.get("status"),
            "surfaceWrapperStatus": surface_catalog.get("status"),
        },
        "handleRows": handle_rows,
        "surfacePairs": surface_pairs,
        "resourceSurfaceLabels": labels,
        "conclusions": [
            "The new HUD-open trace directly labels loaded CNS/resource surfaces, so window.cns/status.cns and previously pending field/actor-like source handles now have confirmed filenames.",
            "The trace still separates filename confirmation from semantic role confirmation; title.cns and btl_efc.cns appearances in field HUD drawing remain role-review items.",
            "Menu navigation, deeper submenu writes, and dynamic HP/MP/EXP numeric glyph producers still need targeted traces.",
        ],
        "nextTraceRequirements": [
            "Capture menu navigation after HUD open: arrows, Enter, ESC with mem-write and object-stream-change.",
            "Capture changing HP/MP/EXP values or equipment selection preview to identify numeric glyph producers.",
            "If the title.cns actor-like draw role matters, capture a short pre-HUD field idle trace with active object/resource-slot labels.",
        ],
    }


def write_html(data: dict[str, Any]) -> None:
    summary = data["summary"]
    rows: list[str] = [
        "<!doctype html><meta charset=\"utf-8\">",
        "<title>HUD Runtime Surface Handle Review</title>",
        """
<style>
body{font-family:system-ui,-apple-system,Segoe UI,sans-serif;margin:24px;background:#f5f3ef;color:#1f2328}
a{color:#145f8f}.top{display:flex;gap:10px;flex-wrap:wrap;margin:12px 0 20px}
.chip{display:inline-block;padding:6px 10px;border:1px solid #bbb;border-radius:999px;background:#fff;text-decoration:none;color:#222}
.panel{background:#fff;border:1px solid #d7d0c4;border-radius:8px;padding:16px;margin:14px 0;box-shadow:0 1px 3px #0001}
h1{margin:0 0 8px;font-size:26px}h2{font-size:18px;margin:0 0 12px}
table{border-collapse:collapse;width:100%;font-size:13px}th,td{border:1px solid #ddd;padding:7px;vertical-align:top}
th{background:#ece5d7;text-align:left}code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}
.ok{color:#0f6b2f;font-weight:700}.warn{color:#916400;font-weight:700}.low{color:#8a2d2d;font-weight:700}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:10px}.small{font-size:12px;color:#555}
details{margin-top:6px}
</style>
""",
        "<h1>HUD Runtime Surface Handle Review</h1>",
        "<p>HUD-open runtime trace의 DirectDraw surface handle을 기존 정적 CNS 근거와 대조해 승격 가능한 것만 정리합니다.</p>",
        '<div class="top"><a class="chip" href="index.html">index</a>'
        '<a class="chip" href="runtime_trace_hud_open_review.html">runtime trace</a>'
        '<a class="chip" href="hud_runtime_static_bridge_review.html">runtime bridge</a>'
        '<a class="chip" href="../out/hud_runtime_surface_handle_review.json">json</a></div>',
        '<section class="panel"><h2>Summary</h2><div class="grid">',
    ]
    for key in [
        "drawSurfaceRows",
        "textDrawRows",
        "surfaceSnapshotRows",
        "resourceSurfaceLabelRows",
        "resourceLabelCount",
        "surfacePairCount",
        "promotedHandleCount",
        "pendingHandleCount",
        "textSurface",
        "frameSurface",
        "screenSurface",
        "windowCatalogMode",
        "menuRightPanelStatus",
        "statusMenuStatus",
        "surfaceWrapperStatus",
    ]:
        rows.append(f"<div><b>{h(key)}</b><br><code>{h(summary.get(key))}</code></div>")
    rows.append("</div></section>")

    rows.append('<section class="panel"><h2>Handle Classification</h2><table><tr><th>handle</th><th>classification</th><th>confidence</th><th>promotion</th><th>evidence</th><th>runtime rects</th></tr>')
    for row in data["handleRows"]:
        confidence = str(row.get("confidence"))
        cls = "ok" if confidence == "High" else "warn" if confidence == "Medium" else "low"
        evidence = "<br>".join(h(item) for item in row.get("whyPromoted") or [])
        runtime = row.get("runtimeEvidence") or {}
        rects = "<br>".join(f"<code>{h(rect)}</code>" for rect in runtime.get("sourceRects") or [])
        details = json.dumps(row, ensure_ascii=False, indent=2)
        rows.append(
            "<tr>"
            f"<td><code>{h(row.get('handle'))}</code></td>"
            f"<td>{h(row.get('classification'))}</td>"
            f"<td class=\"{cls}\">{h(confidence)}</td>"
            f"<td>{h(row.get('promotion'))}</td>"
            f"<td>{evidence}<details><summary>raw</summary><pre>{h(details)}</pre></details></td>"
            f"<td>{rects}</td>"
            "</tr>"
        )
    rows.append("</table></section>")

    rows.append('<section class="panel"><h2>Surface Pair Summary</h2><table><tr><th>source</th><th>destination</th><th>count</th><th>PCs</th><th>src rect top</th><th>dst top</th></tr>')
    for row in data["surfacePairs"]:
        rows.append(
            "<tr>"
            f"<td><code>{h(row.get('source'))}</code></td>"
            f"<td><code>{h(row.get('destination'))}</code></td>"
            f"<td>{h(row.get('count'))}</td>"
            f"<td><code>{h(row.get('pcCounts'))}</code></td>"
            f"<td><code>{h(row.get('srcRectTop'))}</code></td>"
            f"<td><code>{h(row.get('dstTop'))}</code></td>"
            "</tr>"
        )
    rows.append("</table></section>")

    rows.append('<section class="panel"><h2>Conclusions</h2><ul>')
    for item in data["conclusions"]:
        rows.append(f"<li>{h(item)}</li>")
    rows.append("</ul><h2>Next Trace Requirements</h2><ul>")
    for item in data["nextTraceRequirements"]:
        rows.append(f"<li>{h(item)}</li>")
    rows.append("</ul></section>")

    HTML_OUT.write_text("\n".join(rows) + "\n", encoding="utf-8")


def main() -> None:
    OUT.mkdir(parents=True, exist_ok=True)
    WEB.mkdir(parents=True, exist_ok=True)
    data = build()
    JSON_OUT.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    write_html(data)
    print(f"wrote {JSON_OUT.relative_to(ROOT)}")
    print(f"wrote {HTML_OUT.relative_to(ROOT)}")


if __name__ == "__main__":
    main()
