#!/usr/bin/env python3
"""Classify the parent context of the grounded HUD top-menu sequence.

The top menu object sequence at 0x004ddc6c draws/attaches the #6/#3/#4 HUD
menu regions.  Earlier scans proved the payload, but not the normal-field
ESC/X opener that schedules it.  This review keeps those two facts separate:

* 0x004ddc6c is the grounded top-menu construction sequence.
* Its only direct pointer reference is the operand of a VM call command inside
  0x0047e6a0, whose own parent context is not referenced as a normal field
  input/open-menu root.
"""
from __future__ import annotations

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

from probe_exe_scene_tables import offset_to_va, read_sections, va_to_offset
from summarize_object_payload_442c75_callers import decode_stream


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

TOP_MENU_OBJECT_SEQUENCE_VA = 0x004DDC6C
TOP_MENU_WRAPPER_VA = 0x0047E6A0
TOP_MENU_PRE_WRAPPER_GATE_VA = 0x0047E688
TOP_MENU_REJECT_LOOP_VA = 0x0047E67C
TOP_MENU_WRAPPER_CALL_COMMAND_VA = 0x0047E6AC
TOP_MENU_WRAPPER_CALL_OPERAND_VA = 0x0047E6B0
TOP_MENU_PARENT_CONTEXT_VA = 0x0047E624
INTRO_TITLE_RESOURCE_LIST_VA = 0x004A2CB0
STATUS_REGION6_CHILD_SCRIPT_VA = 0x004DEE18
RIGHT_PANEL_REGION3_CHILD_SCRIPT_VA = 0x004DEE58
RIGHT_PANEL_REGION4_CHILD_SCRIPT_VA = 0x004DEE98
RIGHT_PANEL_REGION4_PAGE_SCRIPT_VA = 0x004DE468


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


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


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 all_refs(exe: bytes, sections: list[dict[str, Any]], value: int) -> list[int]:
    needle = struct.pack("<I", value)
    refs: list[int] = []
    pos = exe.find(needle)
    while pos != -1:
        va = offset_to_va(sections, pos)
        if va is not None:
            refs.append(va)
        pos = exe.find(needle, pos + 1)
    return refs


def text_refs(exe: bytes, sections: list[dict[str, Any]], value: int) -> list[int]:
    needle = struct.pack("<I", value)
    refs: list[int] = []
    for section in sections:
        if section.get("name") != ".text":
            continue
        start = int(section["raw"])
        end = start + int(section["raw_size"])
        pos = exe.find(needle, start, end)
        while pos != -1:
            va = offset_to_va(sections, pos)
            if va is not None:
                refs.append(va)
            pos = exe.find(needle, pos + 1, end)
    return refs


def command_digest(exe: bytes, sections: list[dict[str, Any]], start_va: int, max_commands: int = 16) -> dict[str, Any]:
    decoded = decode_stream(exe, sections, start_va, max_commands=max_commands, max_bytes=0x160)
    return {
        "startVa": start_va,
        "startVaHex": hx(start_va),
        "decodedCommandCount": decoded.get("decodedCommandCount", 0),
        "commands": [
            {
                "vaHex": command.get("vaHex"),
                "opcodeHex": command.get("opcodeHex"),
                "opcodeName": command.get("opcodeName"),
                "length": command.get("length"),
                "summary": command.get("summary"),
                "rawHex": command.get("rawHex"),
            }
            for command in decoded.get("commands", [])
        ],
    }


def c_string_list(
    exe: bytes,
    sections: list[dict[str, Any]],
    va: int,
    size: int = 0x100,
    limit: int = 16,
    skip: int = 0,
) -> list[str]:
    offset = va_to_offset(sections, va)
    if offset is None:
        return []
    raw = exe[offset + skip: offset + skip + size]
    names: list[str] = []
    for token in raw.split(b"\x00"):
        if not token:
            if names:
                break
            continue
        if len(token) == 4 and struct.unpack("<I", token)[0] > 0x00400000:
            continue
        if any(byte < 0x20 or byte > 0x7E for byte in token):
            break
        try:
            text = token.decode("ascii")
        except UnicodeDecodeError:
            break
        if "." not in text and "_" not in text and not text.isalnum():
            break
        names.append(text)
        if len(names) >= limit:
            break
    return names


def build_report() -> dict[str, Any]:
    exe = EXE.read_bytes()
    sections = read_sections(exe)

    top_refs = all_refs(exe, sections, TOP_MENU_OBJECT_SEQUENCE_VA)
    wrapper_refs = all_refs(exe, sections, TOP_MENU_WRAPPER_VA)
    pre_wrapper_refs = all_refs(exe, sections, TOP_MENU_PRE_WRAPPER_GATE_VA)
    reject_loop_refs = all_refs(exe, sections, TOP_MENU_REJECT_LOOP_VA)
    parent_refs = all_refs(exe, sections, TOP_MENU_PARENT_CONTEXT_VA)
    region6_refs = all_refs(exe, sections, STATUS_REGION6_CHILD_SCRIPT_VA)
    region3_refs = all_refs(exe, sections, RIGHT_PANEL_REGION3_CHILD_SCRIPT_VA)
    region4_refs = all_refs(exe, sections, RIGHT_PANEL_REGION4_CHILD_SCRIPT_VA)
    page_refs = all_refs(exe, sections, RIGHT_PANEL_REGION4_PAGE_SCRIPT_VA)
    intro_title_resource_names = c_string_list(exe, sections, INTRO_TITLE_RESOURCE_LIST_VA, skip=4)

    wrapper_call_raw = read_at(exe, sections, TOP_MENU_WRAPPER_CALL_COMMAND_VA, 8)
    wrapper_call_operand = struct.unpack_from("<I", wrapper_call_raw, 4)[0]
    wrapper_calls_top_menu = (
        wrapper_call_raw[:4] == b"\x04\x00\x00\x00"
        and wrapper_call_operand == TOP_MENU_OBJECT_SEQUENCE_VA
        and TOP_MENU_WRAPPER_CALL_OPERAND_VA in top_refs
    )

    top_ref_is_operand = top_refs == [TOP_MENU_WRAPPER_CALL_OPERAND_VA]
    normal_field_esc_opener_proven = False

    summary = {
        "topMenuObjectSequenceVaHex": hx(TOP_MENU_OBJECT_SEQUENCE_VA),
        "topMenuSequenceAllRefCount": len(top_refs),
        "topMenuSequenceAllRefsHex": [hx(ref) for ref in top_refs],
        "topMenuSequenceTextRefCount": len(text_refs(exe, sections, TOP_MENU_OBJECT_SEQUENCE_VA)),
        "topMenuWrapperVaHex": hx(TOP_MENU_WRAPPER_VA),
        "topMenuWrapperDirectRefCount": len(wrapper_refs),
        "topMenuWrapperDirectRefsHex": [hx(ref) for ref in wrapper_refs],
        "topMenuPreWrapperGateVaHex": hx(TOP_MENU_PRE_WRAPPER_GATE_VA),
        "topMenuPreWrapperGateDirectRefCount": len(pre_wrapper_refs),
        "topMenuPreWrapperGateDirectRefsHex": [hx(ref) for ref in pre_wrapper_refs],
        "topMenuRejectLoopVaHex": hx(TOP_MENU_REJECT_LOOP_VA),
        "topMenuRejectLoopDirectRefCount": len(reject_loop_refs),
        "topMenuRejectLoopDirectRefsHex": [hx(ref) for ref in reject_loop_refs],
        "topMenuParentContextVaHex": hx(TOP_MENU_PARENT_CONTEXT_VA),
        "topMenuParentDirectRefCount": len(parent_refs),
        "topMenuParentDirectRefsHex": [hx(ref) for ref in parent_refs],
        "wrapperCallCommandVaHex": hx(TOP_MENU_WRAPPER_CALL_COMMAND_VA),
        "wrapperCallOperandVaHex": hx(wrapper_call_operand),
        "wrapperCallsTopMenuSequence": wrapper_calls_top_menu,
        "topSequenceRefIsWrapperOperandOnly": top_ref_is_operand,
        "statusRegion6ChildScriptRefCount": len(region6_refs),
        "rightPanelRegion3ChildScriptRefCount": len(region3_refs),
        "rightPanelRegion4ChildScriptRefCount": len(region4_refs),
        "rightPanelPageScriptRefCount": len(page_refs),
        "introTitleResourceListVaHex": hx(INTRO_TITLE_RESOURCE_LIST_VA),
        "introTitleResourceListPreview": intro_title_resource_names,
        "parentContextClassification": (
            "intro/title-resource-context"
            if {"compile.cns", "aaa.cns", "logo_00.cns", "title.cns"}.issubset(set(intro_title_resource_names))
            else "unclassified-parent-context"
        ),
        "normalFieldEscOpenerProven": normal_field_esc_opener_proven,
        "decision": (
            "0x004ddc6c top-menu sequence is grounded as the #6/#3/#4 menu construction stream, "
            "but its only direct reference is the inline operand of the 0x04 call command at 0x0047e6ac. "
            "The immediate pre-wrapper gate at 0x0047e688 is now decoded as two 0x6e active group-key branches "
            "and one 0xe6 active-descriptor-count branch, all targeting the stop/jump loop at 0x0047e67c when the "
            "gate fails. "
            "The nearby parent resource list contains intro/title assets such as compile.cns, aaa.cns, logo_00.cns, "
            "title.cns, and btl_k*.  The wrapper 0x0047e6a0 and parent context 0x0047e624 have no direct pointer refs, "
            "so this pass does not prove the normal-field ESC/X opener."
        ),
    }

    return {
        "kind": "hwanse-hud-menu-top-sequence-context-review",
        "source": "tools/build_hud_menu_top_sequence_context_review.py",
        "status": "top-menu-sequence-grounded-opener-unproven",
        "summary": summary,
        "contexts": {
            "parentContext": command_digest(exe, sections, TOP_MENU_PARENT_CONTEXT_VA, max_commands=22),
            "preWrapperGate": command_digest(exe, sections, TOP_MENU_PRE_WRAPPER_GATE_VA, max_commands=8),
            "rejectLoop": command_digest(exe, sections, TOP_MENU_REJECT_LOOP_VA, max_commands=4),
            "wrapper": command_digest(exe, sections, TOP_MENU_WRAPPER_VA, max_commands=14),
            "topMenuSequence": command_digest(exe, sections, TOP_MENU_OBJECT_SEQUENCE_VA, max_commands=22),
        },
        "referenceSurfaces": [
            {
                "label": "top menu object sequence",
                "vaHex": hx(TOP_MENU_OBJECT_SEQUENCE_VA),
                "refsHex": [hx(ref) for ref in top_refs],
                "classification": "grounded sequence; only ref is wrapper operand",
            },
            {
                "label": "pre-wrapper gate",
                "vaHex": hx(TOP_MENU_PRE_WRAPPER_GATE_VA),
                "refsHex": [hx(ref) for ref in pre_wrapper_refs],
                "classification": "active group-key / active-descriptor-count gate; target refs point to reject loop",
            },
            {
                "label": "pre-wrapper reject loop",
                "vaHex": hx(TOP_MENU_REJECT_LOOP_VA),
                "refsHex": [hx(ref) for ref in reject_loop_refs],
                "classification": "stop-tick then jump back toward selected-root call",
            },
            {
                "label": "top menu wrapper",
                "vaHex": hx(TOP_MENU_WRAPPER_VA),
                "refsHex": [hx(ref) for ref in wrapper_refs],
                "classification": "unreferenced wrapper; not opener proof",
            },
            {
                "label": "top menu parent context",
                "vaHex": hx(TOP_MENU_PARENT_CONTEXT_VA),
                "refsHex": [hx(ref) for ref in parent_refs],
                "classification": "unreferenced parent context; adjacent resource list is intro/title-oriented",
            },
            {
                "label": "intro/title resource list",
                "vaHex": hx(INTRO_TITLE_RESOURCE_LIST_VA),
                "refsHex": [hx(ref) for ref in all_refs(exe, sections, INTRO_TITLE_RESOURCE_LIST_VA)],
                "classification": ", ".join(intro_title_resource_names[:10]),
            },
            {
                "label": "region #6 child script",
                "vaHex": hx(STATUS_REGION6_CHILD_SCRIPT_VA),
                "refsHex": [hx(ref) for ref in region6_refs],
                "classification": "referenced by top-menu sequence",
            },
            {
                "label": "region #3 child script",
                "vaHex": hx(RIGHT_PANEL_REGION3_CHILD_SCRIPT_VA),
                "refsHex": [hx(ref) for ref in region3_refs],
                "classification": "referenced by top-menu sequence",
            },
            {
                "label": "region #4 child script",
                "vaHex": hx(RIGHT_PANEL_REGION4_CHILD_SCRIPT_VA),
                "refsHex": [hx(ref) for ref in region4_refs],
                "classification": "referenced by top-menu sequence",
            },
            {
                "label": "shifted/right-panel page script",
                "vaHex": hx(RIGHT_PANEL_REGION4_PAGE_SCRIPT_VA),
                "refsHex": [hx(ref) for ref in page_refs],
                "classification": "secondary page/menu payload context; not normal opener",
            },
        ],
        "negativeEvidence": [
            "No direct pointer refs to the wrapper stream at 0x0047e6a0.",
            "No direct pointer refs to the parent context stream at 0x0047e624.",
            "The sole 0x004ddc6c ref is 0x0047e6b0, an operand address inside the 0x04 command beginning at 0x0047e6ac.",
            "The repeated 7c e6 47 00 bytes before the wrapper are not opcode 0x7c commands; they are little-endian target pointers to 0x0047e67c.",
            "The adjacent resource list at 0x004a2cb0 is intro/title-oriented, not a normal-field HUD menu context.",
            "This review does not find an input-edge, field-mode, or descriptor-add producer that schedules the wrapper from normal field state.",
        ],
        "nextFrontier": [
            "Find the upstream selector/descriptor producer that schedules 0x0047e6a0 or its parent context without relying on direct pointer refs.",
            "Prioritize field mode/state transitions after ESC/X edge handling, not menu-internal cursor handlers.",
            "Do not promote 0x004ddc6c itself as the opener; it is the menu construction callee.",
        ],
    }


def render_html(report: dict[str, Any]) -> str:
    summary = report["summary"]
    card_items = [
        ("status", report["status"]),
        ("top seq", summary["topMenuObjectSequenceVaHex"]),
        ("top refs", summary["topMenuSequenceAllRefCount"]),
        ("pre-gate refs", summary["topMenuPreWrapperGateDirectRefCount"]),
        ("wrapper refs", summary["topMenuWrapperDirectRefCount"]),
        ("parent refs", summary["topMenuParentDirectRefCount"]),
        ("parent class", summary["parentContextClassification"]),
        ("opener", summary["normalFieldEscOpenerProven"]),
    ]
    cards = "".join(f"<div class='card'><b>{h(k)}</b><span>{h(v)}</span></div>" for k, v in card_items)

    def render_commands(context: dict[str, Any]) -> str:
        return "".join(
            "<tr>"
            f"<td><code>{h(command.get('vaHex'))}</code></td>"
            f"<td><code>{h(command.get('opcodeHex'))}</code><br>{h(command.get('opcodeName'))}</td>"
            f"<td>{h(command.get('summary'))}</td>"
            f"<td><code>{h(command.get('rawHex'))}</code></td>"
            "</tr>"
            for command in context.get("commands", [])
        )

    context_tables = ""
    for key, title in [
        ("parentContext", "Parent Context 0x0047e624"),
        ("rejectLoop", "Reject Loop 0x0047e67c"),
        ("preWrapperGate", "Pre-wrapper Gate 0x0047e688"),
        ("wrapper", "Wrapper 0x0047e6a0"),
        ("topMenuSequence", "Top Menu Sequence 0x004ddc6c"),
    ]:
        context = report["contexts"][key]
        context_tables += f"""
        <section>
          <h2>{h(title)}</h2>
          <table>
            <thead><tr><th>VA</th><th>opcode</th><th>summary</th><th>raw</th></tr></thead>
            <tbody>{render_commands(context)}</tbody>
          </table>
        </section>
        """

    ref_rows = "".join(
        "<tr>"
        f"<td>{h(row['label'])}<br><code>{h(row['vaHex'])}</code></td>"
        f"<td>{h(row['classification'])}</td>"
        f"<td><code>{h(', '.join(row['refsHex']) or '-')}</code></td>"
        "</tr>"
        for row in report["referenceSurfaces"]
    )
    negatives = "".join(f"<li>{h(item)}</li>" for item in report["negativeEvidence"])
    next_frontier = "".join(f"<li>{h(item)}</li>" for item in report["nextFrontier"])
    payload = json.dumps(report, ensure_ascii=False)
    html_doc = f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>HUD Menu Top Sequence Context Review</title>
  <style>
    body {{ margin:0; background:#101318; color:#edf1f7; font-family:system-ui,sans-serif; }}
    main {{ max-width:1280px; margin:0 auto; padding:24px; }}
    a {{ color:#8ecbff; }}
    .nav {{ display:flex; flex-wrap:wrap; gap:8px; margin-bottom:16px; }}
    .chip {{ border:1px solid #334155; border-radius:999px; padding:6px 10px; text-decoration:none; background:#161b22; }}
    .summary {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:12px; margin:16px 0; }}
    .card {{ border:1px solid #2b3544; border-radius:8px; padding:12px; background:#161b22; }}
    .card b {{ display:block; color:#9fb1c9; font-size:12px; text-transform:uppercase; }}
    .card span {{ display:block; margin-top:8px; font-size:18px; overflow-wrap:anywhere; }}
    section {{ border:1px solid #273244; border-radius:10px; padding:16px; margin:16px 0; background:#141922; overflow:auto; }}
    table {{ border-collapse:collapse; width:100%; min-width:1040px; font-size:13px; }}
    th,td {{ border-bottom:1px solid #283342; padding:8px; text-align:left; vertical-align:top; }}
    th {{ color:#b9c7dc; background:#111722; }}
    code {{ color:#dbeafe; overflow-wrap:anywhere; }}
    pre {{ white-space:pre-wrap; background:#0b0f14; border:1px solid #253044; border-radius:8px; padding:12px; max-height:420px; overflow:auto; }}
  </style>
</head>
<body>
<main>
  <div class="nav">
    <a class="chip" href="index.html">index</a>
    <a class="chip" href="hud_menu_opener_frontier_review.html">opener frontier</a>
    <a class="chip" href="hud_menu_selected_root_frontier_review.html">selected-root frontier</a>
    <a class="chip" href="hud_menu_preview.html">HUD menu preview</a>
    <a class="chip" href="../out/hud_menu_top_sequence_context_review.json">JSON</a>
  </div>
  <h1>HUD Menu Top Sequence Context Review</h1>
  <p>{h(summary["decision"])}</p>
  <div class="summary">{cards}</div>
  <section>
    <h2>Reference Surfaces</h2>
    <table><thead><tr><th>surface</th><th>classification</th><th>refs</th></tr></thead><tbody>{ref_rows}</tbody></table>
  </section>
  {context_tables}
  <section>
    <h2>Negative Evidence</h2>
    <ul>{negatives}</ul>
  </section>
  <section>
    <h2>Next Frontier</h2>
    <ul>{next_frontier}</ul>
  </section>
  <section>
    <h2>Raw JSON</h2>
    <pre id="json"></pre>
  </section>
</main>
<script>
window.HWANSE_HUD_MENU_TOP_SEQUENCE_CONTEXT_REVIEW = {payload};
document.getElementById('json').textContent = JSON.stringify(window.HWANSE_HUD_MENU_TOP_SEQUENCE_CONTEXT_REVIEW, null, 2);
</script>
</body>
</html>
"""
    return "\n".join(line.rstrip() for line in html_doc.splitlines()) + "\n"


def main() -> int:
    report = build_report()
    OUT.mkdir(exist_ok=True)
    WEB.mkdir(exist_ok=True)
    (OUT / "hud_menu_top_sequence_context_review.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (WEB / "hud_menu_top_sequence_context_review.html").write_text(render_html(report), encoding="utf-8")
    print("hud_menu_top_sequence_context_review ok")
    return 0


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