#!/usr/bin/env python3
"""Review the resource command VM bridge without promoting routes.

This pass separates three layers that were previously easy to blur:

* active/object scripts are driven by the generic table at 0x00440538,
* resource packages are driven by a separate command VM at 0x00423a2f,
* resource opcode 0x10 applies a field-map CNS payload to the live map grids.

Finding opcode 0x10 proves how a map is loaded once a resource package is
selected.  It does not by itself prove a concrete source->target route.  Map
movement remains manual until the selected package/root producer is tied to a
movement trigger and a destination.
"""
from __future__ import annotations

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

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

from probe_exe_scene_tables import read_sections, va_to_offset  # noqa: E402


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

ACTIVE_HANDLER_TABLE = 0x00440538
RESOURCE_RUNNER = 0x00423A2F
RESOURCE_HANDLER_TABLE = 0x0048ACA8
RESOURCE_CURSOR_GLOBAL = 0x0055B2FC
RESOURCE_STOP_GLOBAL = 0x0055B310
RESOURCE_OPCODE10_HANDLER = 0x0042449C
RESOURCE_PAYLOAD_LOAD = 0x00422F7C
RESOURCE_POST_MAP_REFRESH = 0x00424920
MAP_LAYER0_DEST = 0x00595AF0
MAP_LAYER1_COLLISION_DEST = 0x0058D7D0
MAP_WIDTH_GLOBAL = 0x00595ADA
MAP_HEIGHT_GLOBAL = 0x00595ADC
ACTIVE_OPCODE5E_HANDLER = 0x004079FF
ACTIVE_OPCODE33_HANDLER = 0x00405EF8

PROMOTION_STATUS = "resource-command-vm-map-loader-grounded-route-target-unproven"


def hx(value: int | None, width: int = 8) -> str:
    if value is None:
        return ""
    return f"0x{value:0{width}x}"


def esc(value: Any) -> str:
    return html.escape(str(value if value is not None else ""))


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


def read_at(exe: bytes, sections: list[dict[str, Any]], va: int, size: int) -> bytes:
    offset = va_to_offset(sections, va)
    if offset is None:
        raise ValueError(f"VA {hx(va)} is outside raw sections")
    return exe[offset : offset + size]


def u32(exe: bytes, sections: list[dict[str, Any]], va: int) -> int:
    return struct.unpack("<I", read_at(exe, sections, va, 4))[0]


def scan_call_refs(exe: bytes, sections: list[dict[str, Any]], target: int) -> list[int]:
    text = next(section for section in sections if section["name"] == ".text")
    blob = exe[text["raw"] : text["raw"] + text["raw_size"]]
    rows: list[int] = []
    for offset in range(0, len(blob) - 4):
        if blob[offset] != 0xE8:
            continue
        rel = struct.unpack_from("<i", blob, offset + 1)[0]
        source = text["va"] + offset
        if source + 5 + rel == target:
            rows.append(source)
    return rows


def resource_handler_rows(exe: bytes, sections: list[dict[str, Any]]) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    for opcode in range(0x21):
        handler = u32(exe, sections, RESOURCE_HANDLER_TABLE + opcode * 4)
        rows.append(
            {
                "opcode": opcode,
                "opcodeHex": f"0x{opcode:02x}",
                "handlerVa": handler,
                "handlerVaHex": hx(handler),
                "role": (
                    "stop"
                    if opcode == 0x00
                    else "field-map-layer-load"
                    if opcode == 0x10
                    else "surface/resource-command"
                ),
                "isMapLoader": handler == RESOURCE_OPCODE10_HANDLER,
            }
        )
    return rows


def active_handler_opcode(exe: bytes, sections: list[dict[str, Any]], handler: int) -> int | None:
    for opcode in range(0x100):
        try:
            value = u32(exe, sections, ACTIVE_HANDLER_TABLE + opcode * 4)
        except ValueError:
            break
        if value == handler:
            return opcode
    return None


def field_map_opcode10_rows(selector_review: dict[str, Any], exe: bytes, sections: list[dict[str, Any]]) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    for root in selector_review.get("roots") or []:
        for span in root.get("spans") or []:
            if span.get("kind") != "resource-string-ref-cluster":
                continue
            for ref in span.get("rows") or []:
                if ref.get("resourceKind") != "map" and ref.get("kind") != "field-map-string-ref":
                    continue
                ref_va = int(ref["va"])
                prev = u32(exe, sections, ref_va - 4)
                rows.append(
                    {
                        "rootVaHex": root.get("rootVaHex"),
                        "refVa": ref_va,
                        "refVaHex": ref.get("vaHex") or hx(ref_va),
                        "recordVaHex": hx(ref_va - 4),
                        "mapName": ref.get("string", ""),
                        "precedingDwordHex": hx(prev),
                        "isOpcode10Record": prev == 0x10,
                    }
                )
    return rows


def active_opcode5e_rows(inventory: dict[str, Any], exe: bytes, sections: list[dict[str, Any]]) -> list[dict[str, Any]]:
    from summarize_object_payload_442c75_callers import decode_stream

    rows: list[dict[str, Any]] = []
    for script in inventory.get("scripts") or []:
        script_va = int(script["scriptVaHex"], 16)
        stream = decode_stream(exe, sections, script_va, max_commands=120, max_bytes=0x400)
        for command in stream.get("commands") or []:
            if command.get("opcode") != 0x5E:
                continue
            rows.append(
                {
                    "scriptVaHex": script["scriptVaHex"],
                    "commandVaHex": command.get("vaHex"),
                    "mode": command.get("mode"),
                    "payloadVaHex": command.get("payloadVaHex"),
                    "rawHex": command.get("rawHex"),
                    "scriptClassification": script.get("classification"),
                    "routeProofFound": stream.get("routeProofFound", False),
                    "mapLoaderRefFound": stream.get("mapLoaderRefFound", False),
                }
            )
    return rows


def raw_opcode5e_counts(exe: bytes, sections: list[dict[str, Any]]) -> dict[str, Any]:
    rows: list[dict[str, Any]] = []
    counts: Counter[int] = Counter()
    payloads: Counter[str] = Counter()
    for mode in range(4):
        pattern = bytes([0x5E, mode, 0x00, 0x00])
        for section in sections:
            if section["name"] not in {".data", ".rdata"}:
                continue
            blob = exe[section["raw"] : section["raw"] + section["raw_size"]]
            offset = 0
            while True:
                hit = blob.find(pattern, offset)
                if hit < 0:
                    break
                va = section["va"] + hit
                payload = None
                if mode == 1 and hit + 8 <= len(blob):
                    payload = struct.unpack_from("<I", blob, hit + 4)[0]
                    payloads[hx(payload)] += 1
                counts[mode] += 1
                if len(rows) < 80:
                    rows.append(
                        {
                            "vaHex": hx(va),
                            "mode": mode,
                            "payloadVaHex": hx(payload) if payload is not None else "",
                        }
                    )
                offset = hit + 1
    return {
        "modeCounts": {f"mode{mode}": count for mode, count in sorted(counts.items())},
        "mode1PayloadCounts": dict(payloads.most_common()),
        "sampleRows": rows,
    }


def global_resource_root_rows(exe: bytes, sections: list[dict[str, Any]], selector_review: dict[str, Any]) -> list[dict[str, Any]]:
    # Active opcode 0x33 can choose one of these globals and run the resource VM.
    # The first is a compact UI package; the second is retained as a package root
    # but not promoted to a map route.
    roots = {
        "0x0047e350": u32(exe, sections, 0x0047E350),
        "0x0047e354": u32(exe, sections, 0x0047E354),
    }
    root_lookup = {root.get("rootVaHex"): root for root in selector_review.get("roots") or []}
    rows = []
    for global_hex, root_va in roots.items():
        selector_root = root_lookup.get(hx(root_va))
        rows.append(
            {
                "globalVaHex": global_hex,
                "rootVaHex": hx(root_va),
                "selectorRootClass": selector_root.get("rootClass", "") if selector_root else "",
                "linkedCns": (selector_root.get("linkedCns", [])[:12] if selector_root else []),
                "fieldMaps": (selector_root.get("fieldMaps", [])[:12] if selector_root else []),
            }
        )
    return rows


def build_payload(exe_path: Path) -> dict[str, Any]:
    exe = exe_path.read_bytes()
    sections = read_sections(exe)
    selector_review = load_json(OUT / "selector_root_structure_review.json", {})
    inventory = load_json(OUT / "active_object_script_inventory.json", {})

    handler_rows = resource_handler_rows(exe, sections)
    opcode10_rows = field_map_opcode10_rows(selector_review, exe, sections)
    opcode5e_rows = active_opcode5e_rows(inventory, exe, sections)
    raw_5e = raw_opcode5e_counts(exe, sections)
    global_roots = global_resource_root_rows(exe, sections, selector_review)

    map_opcode_count = sum(1 for row in opcode10_rows if row["isOpcode10Record"])
    map_opcode_bad = [row for row in opcode10_rows if not row["isOpcode10Record"]]
    strict_5e_route_rows = [
        row for row in opcode5e_rows if row.get("routeProofFound") or row.get("mapLoaderRefFound")
    ]
    active_5e_mode_counts = Counter(row.get("mode") for row in opcode5e_rows)
    direct_runner_calls = scan_call_refs(exe, sections, RESOURCE_RUNNER)
    payload = {
        "kind": "hwanse-resource-command-vm-bridge-review",
        "promotionStatus": PROMOTION_STATUS,
        "summary": {
            "resourceRunnerHex": hx(RESOURCE_RUNNER),
            "resourceHandlerTableHex": hx(RESOURCE_HANDLER_TABLE),
            "resourceCursorGlobalHex": hx(RESOURCE_CURSOR_GLOBAL),
            "resourceStopGlobalHex": hx(RESOURCE_STOP_GLOBAL),
            "resourceOpcode10HandlerHex": hx(RESOURCE_OPCODE10_HANDLER),
            "mapLayer0DestHex": hx(MAP_LAYER0_DEST),
            "mapLayer1CollisionDestHex": hx(MAP_LAYER1_COLLISION_DEST),
            "fieldMapRefCount": len(opcode10_rows),
            "fieldMapOpcode10RecordCount": map_opcode_count,
            "fieldMapOpcode10AllMatched": len(opcode10_rows) == map_opcode_count and bool(opcode10_rows),
            "uniqueOpcode10MapCount": len({row["mapName"] for row in opcode10_rows}),
            "activeOpcode5e": active_handler_opcode(exe, sections, ACTIVE_OPCODE5E_HANDLER),
            "activeOpcode33": active_handler_opcode(exe, sections, ACTIVE_OPCODE33_HANDLER),
            "activeOpcode5eHandlerHex": hx(ACTIVE_OPCODE5E_HANDLER),
            "activeOpcode33HandlerHex": hx(ACTIVE_OPCODE33_HANDLER),
            "activeOpcode5eStrictRowCount": len(opcode5e_rows),
            "activeOpcode5eStrictModeCounts": dict(sorted((str(k), v) for k, v in active_5e_mode_counts.items())),
            "activeOpcode5eStrictRouteProofRows": len(strict_5e_route_rows),
            "rawOpcode5eModeCounts": raw_5e["modeCounts"],
            "directResourceRunnerCallCount": len(direct_runner_calls),
            "directResourceRunnerCallsHex": [hx(row) for row in direct_runner_calls],
            "routeProofFound": False,
            "sceneAutoTransitionClaim": False,
            "manualMovementAssumption": True,
        },
        "decisions": [
            {
                "id": "resource-vm-runner",
                "status": "grounded",
                "decision": "0x00423a2f is a separate resource command VM runner.",
                "evidence": "sets 0x0055b2fc from the argument, dispatches opcodes <=0x20 through 0x0048aca8, stops on 0x0055b310",
            },
            {
                "id": "resource-opcode10-map-loader",
                "status": "grounded",
                "decision": "resource opcode 0x10 applies a field-map CNS payload to live map/collision buffers.",
                "evidence": "handler 0x0042449c reads [stream+4], copies layer0 to 0x00595af0 and layer1/collision to 0x0058d7d0, then advances +8",
            },
            {
                "id": "selector-field-map-opcode10-records",
                "status": "grounded",
                "decision": "selector-root field-map CNS refs are opcode 0x10 records.",
                "evidence": f"{map_opcode_count}/{len(opcode10_rows)} field-map refs have preceding dword 0x00000010",
            },
            {
                "id": "active-opcode5e-resource-bridge",
                "status": "partial",
                "decision": "active opcode 0x5e can invoke the resource VM, but strict decoded rows do not prove a route.",
                "evidence": f"strict rows={len(opcode5e_rows)}, route/map-loader rows={len(strict_5e_route_rows)}; handler has direct calls to 0x00423a2f",
            },
            {
                "id": "route-target",
                "status": "blocked",
                "decision": "do not promote source->target map transitions from this evidence alone.",
                "evidence": "producer that selects a concrete opcode10 package/root from a manual movement trigger remains unresolved",
            },
        ],
        "resourceHandlerRows": handler_rows,
        "fieldMapOpcode10Rows": opcode10_rows,
        "fieldMapOpcode10BadRows": map_opcode_bad,
        "activeOpcode5eRows": opcode5e_rows,
        "activeOpcode5eRouteRows": strict_5e_route_rows,
        "rawOpcode5e": raw_5e,
        "globalResourceRootRows": global_roots,
        "nonClaims": [
            "A resource opcode 0x10 record proves that a map can be loaded from a selected package, not which exit selected it.",
            "Active opcode 0x5e proves an active-script-to-resource-VM bridge, not automatic scene-driven movement.",
            "Scene records are not promoted to map transitions here; movement remains manual until the selected package producer is tied to the player trigger.",
        ],
    }
    return payload


def render_html(payload: dict[str, Any]) -> str:
    s = payload["summary"]
    metrics = [
        ("field-map opcode10", f"{s['fieldMapOpcode10RecordCount']}/{s['fieldMapRefCount']}"),
        ("unique maps", s["uniqueOpcode10MapCount"]),
        ("active 0x5e rows", s["activeOpcode5eStrictRowCount"]),
        ("route proof", s["routeProofFound"]),
    ]
    metric_html = "".join(
        f"<div class='metric'><span>{esc(label)}</span><strong>{esc(value)}</strong></div>"
        for label, value in metrics
    )
    decision_rows = "".join(
        "<tr>"
        f"<td><code>{esc(row['id'])}</code></td><td><span class='tag {esc(row['status'])}'>{esc(row['status'])}</span></td>"
        f"<td>{esc(row['decision'])}</td><td>{esc(row['evidence'])}</td>"
        "</tr>"
        for row in payload["decisions"]
    )
    handler_rows = "".join(
        "<tr>"
        f"<td><code>{row['opcodeHex']}</code></td><td><code>{row['handlerVaHex']}</code></td>"
        f"<td>{esc(row['role'])}{' <b>map loader</b>' if row['isMapLoader'] else ''}</td>"
        "</tr>"
        for row in payload["resourceHandlerRows"]
    )
    map_rows = "".join(
        "<tr>"
        f"<td><code>{esc(row['rootVaHex'])}</code></td><td><code>{esc(row['recordVaHex'])}</code></td>"
        f"<td><code>{esc(row['refVaHex'])}</code></td><td>{esc(row['mapName'])}</td>"
        "</tr>"
        for row in payload["fieldMapOpcode10Rows"][:120]
    )
    bridge_rows = "".join(
        "<tr>"
        f"<td><code>{esc(row['scriptVaHex'])}</code></td><td><code>{esc(row['commandVaHex'])}</code></td>"
        f"<td>{esc(row['mode'])}</td><td><code>{esc(row.get('payloadVaHex') or '')}</code></td>"
        f"<td>{esc(row['routeProofFound'])}</td>"
        "</tr>"
        for row in payload["activeOpcode5eRows"]
    )
    global_rows = "".join(
        "<tr>"
        f"<td><code>{esc(row['globalVaHex'])}</code></td><td><code>{esc(row['rootVaHex'])}</code></td>"
        f"<td>{esc(row['selectorRootClass'] or '-')}</td><td>{esc(', '.join(row['fieldMaps']) or '-')}</td>"
        f"<td>{esc(', '.join(row['linkedCns']) or '-')}</td>"
        "</tr>"
        for row in payload["globalResourceRootRows"]
    )
    non_claims = "".join(f"<li>{esc(item)}</li>" for item in payload["nonClaims"])
    return f"""<!doctype html>
<meta charset="utf-8">
<title>Resource Command VM Bridge Review</title>
<style>
body{{font-family:system-ui,sans-serif;background:#101214;color:#e5e7eb;margin:24px;line-height:1.45}}
a{{color:#93c5fd}} code{{color:#bfdbfe}} table{{border-collapse:collapse;width:100%;margin:14px 0}}
td,th{{border:1px solid #374151;padding:7px 9px;vertical-align:top}} th{{background:#1f2937}}
.metrics{{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px;margin:14px 0}}
.metric{{background:#181c20;border:1px solid #2f3640;border-radius:6px;padding:10px}}
.metric span{{display:block;color:#9ca3af;font-size:12px}} .metric strong{{font-size:24px}}
.tag{{border-radius:999px;padding:2px 8px;background:#374151}} .grounded{{background:#065f46}} .partial{{background:#92400e}} .blocked{{background:#7f1d1d}}
</style>
<h1>Resource Command VM Bridge Review</h1>
<p><code>{esc(payload['promotionStatus'])}</code></p>
<div class="metrics">{metric_html}</div>
<p>Resource command VM과 active/object script VM 사이의 다리는 확인했다. 단, 목적지 맵 선택/route proof는 아직 승격하지 않는다.</p>
<h2>Decisions</h2>
<table><thead><tr><th>id</th><th>status</th><th>decision</th><th>evidence</th></tr></thead><tbody>{decision_rows}</tbody></table>
<h2>Resource Handler Table</h2>
<table><thead><tr><th>opcode</th><th>handler</th><th>role</th></tr></thead><tbody>{handler_rows}</tbody></table>
<h2>Field Map Opcode 0x10 Rows</h2>
<table><thead><tr><th>root</th><th>record</th><th>ref</th><th>map</th></tr></thead><tbody>{map_rows}</tbody></table>
<h2>Active Opcode 0x5e Strict Rows</h2>
<table><thead><tr><th>script</th><th>command</th><th>mode</th><th>payload</th><th>route proof</th></tr></thead><tbody>{bridge_rows}</tbody></table>
<h2>Active Opcode 0x33 Global Resource Roots</h2>
<table><thead><tr><th>global</th><th>root</th><th>class</th><th>field maps</th><th>linked CNS</th></tr></thead><tbody>{global_rows}</tbody></table>
<h2>Non-claims</h2><ul>{non_claims}</ul>
<script>
window.HWANSE_RESOURCE_COMMAND_VM_BRIDGE_SUMMARY = {json.dumps(payload['summary'], ensure_ascii=False)};
</script>
"""


def write_outputs(payload: dict[str, Any], out_dir: Path) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "resource_command_vm_bridge_review.json").write_text(
        json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--exe", type=Path, default=EXE)
    parser.add_argument("--out", type=Path, default=OUT)
    args = parser.parse_args()
    payload = build_payload(args.exe)
    write_outputs(payload, args.out)
    print(json.dumps(payload["summary"], ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
