#!/usr/bin/env python3
"""Summarize the descriptor context for the 0x00542a04 frontier wrapper candidate."""
from __future__ import annotations

import argparse
import html
import json
import struct
import sys
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
from summarize_script_handler_table import handler_for_opcode, section_name_for_va


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

SOURCE = "map1_01a"
TARGET = "map2_02d"
CURRENT_ROOT = 0x00540714
ROOT_TABLE_POINTER = 0x005429DC
TABLE_WINDOW_START = 0x005429A8
ROOT_TABLE_END_EXCLUSIVE = 0x00542A04
WRAPPER_ENTRY = 0x005429AC
WRAPPER_DESCRIPTOR = 0x00542A04
FRONTIER_LEAF = 0x00542AE8
FRONTIER_READER = 0x00542B0C
DESCRIPTOR_MARKER = 0x0000003F


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


def hex32(value: int) -> str:
    return f"0x{value:08x}"


def parse_hex(value: str | None) -> int | None:
    return int(value, 16) if value else None


def dword_at(exe: bytes, sections: list[dict], va: int) -> int | None:
    offset = va_to_offset(sections, va)
    if offset is None or offset + 4 > len(exe):
        return None
    return struct.unpack_from("<I", exe, offset)[0]


def refs_to_value(exe: bytes, sections: list[dict], value: int) -> list[dict]:
    needle = struct.pack("<I", value)
    refs = []
    for section in sections:
        start = section["raw"]
        end = start + section["raw_size"]
        data = exe[start:end]
        pos = data.find(needle)
        while pos >= 0:
            refs.append({
                "section": section["name"],
                "refVa": section["va"] + pos,
                "refVaHex": hex32(section["va"] + pos),
            })
            pos = data.find(needle, pos + 1)
    return refs


def field_maps_for_child(leaf_streams: list[dict], child_hex: str | None) -> list[str]:
    if not child_hex:
        return []
    row = next((item for item in leaf_streams if item.get("leafPointerHex") == child_hex), None)
    if not row:
        return []
    return [record.get("map") for record in row.get("fieldRecords") or [] if record.get("map")]


def root_table_row(exe: bytes, sections: list[dict], va: int, leaf_streams: list[dict]) -> dict:
    value = dword_at(exe, sections, va)
    row = {
        "va": va,
        "vaHex": hex32(va),
        "value": value,
        "valueHex": hex32(value) if value is not None else None,
        "insideCurrentRootEntryRun": ROOT_TABLE_POINTER <= va < ROOT_TABLE_END_EXCLUSIVE,
    }
    if value is None:
        row["kind"] = "unreadable"
        return row
    section = section_name_for_va(sections, value)
    marker = dword_at(exe, sections, value)
    child = dword_at(exe, sections, value + 4) if marker == DESCRIPTOR_MARKER else None
    if marker == DESCRIPTOR_MARKER:
        child_hex = hex32(child) if child is not None else None
        row.update({
            "kind": "descriptor-pointer",
            "descriptorHex": hex32(value),
            "descriptorMarkerHex": hex32(marker),
            "childPointerHex": child_hex,
            "childFieldMaps": field_maps_for_child(leaf_streams, child_hex),
            "pointsToWrapperDescriptor": value == WRAPPER_DESCRIPTOR,
            "childIsFrontierLeaf": child == FRONTIER_LEAF,
        })
    elif section:
        row.update({
            "kind": "pointer",
            "targetSection": section,
        })
    else:
        row["kind"] = "scalar"
    return row


def classify_wrapper_entry_ref(exe: bytes, sections: list[dict], ref: dict) -> dict:
    ref_va = ref["refVa"]
    ref_value = dword_at(exe, sections, ref_va)
    fallthrough_opcode = ref_value & 0xFF if ref_value is not None else None
    fallthrough_handler = (
        handler_for_opcode(exe, sections, fallthrough_opcode)
        if fallthrough_opcode is not None
        else {}
    )
    previous_va = ref_va - 4
    previous_value = dword_at(exe, sections, previous_va)
    previous_opcode = previous_value & 0xFF if previous_value is not None else None
    handler = handler_for_opcode(exe, sections, previous_opcode) if previous_opcode is not None else {}
    mode = (previous_value >> 8) & 0xFF if previous_value is not None else None
    stream_effect = handler.get("streamEffect") or {}
    fixed_advances = sorted({
        item.get("bytes")
        for item in stream_effect.get("fixedAdvances") or []
        if item.get("bytes")
    })
    can_jump = stream_effect.get("canJumpToDwordAtPlus4") is True
    if previous_opcode == 0x5A and mode == 0 and fixed_advances == [4] and not can_jump:
        role = "opcode-0x5a-mode0-fallthrough-word"
        promotes = False
        note = (
            "The preceding opcode 0x5a mode 0 handler advances by +4 and does not jump to [stream+4], "
            "so this dword is a following stream word rather than a selected leaf-table pointer. "
            f"Interpreted as a stream word, its low opcode is 0x{fallthrough_opcode:02x}; "
            f"that handler resolves to {fallthrough_handler.get('handlerVaHex') or '-'} "
            f"in {fallthrough_handler.get('handlerSection') or 'no executable section'}, "
            "so the word also does not provide a code-backed wrapper jump."
        )
    elif can_jump:
        role = "possible-stream-plus4-branch-target"
        promotes = True
        note = "The preceding handler can jump to [stream+4], so this needs separate runtime gate proof."
    else:
        role = "non-branch-stream-word"
        promotes = False
        note = "The preceding handler is not known to consume [stream+4] as a pointer."
    return {
        **ref,
        "previousVaHex": hex32(previous_va),
        "previousValueHex": hex32(previous_value) if previous_value is not None else None,
        "previousOpcodeHex": f"0x{previous_opcode:02x}" if previous_opcode is not None else None,
        "previousOpcodeMode": mode,
        "previousHandlerHex": handler.get("handlerVaHex"),
        "previousHandlerSection": handler.get("handlerSection"),
        "previousHandlerFixedAdvances": fixed_advances,
        "previousHandlerCanJumpToDwordAtPlus4": can_jump,
        "fallthroughWordValueHex": hex32(ref_value) if ref_value is not None else None,
        "fallthroughWordOpcodeHex": f"0x{fallthrough_opcode:02x}" if fallthrough_opcode is not None else None,
        "fallthroughWordHandlerEntryHex": fallthrough_handler.get("entryVaHex"),
        "fallthroughWordHandlerHex": fallthrough_handler.get("handlerVaHex"),
        "fallthroughWordHandlerSection": fallthrough_handler.get("handlerSection"),
        "fallthroughWordHandlerIsCode": fallthrough_handler.get("isCodeHandler") is True,
        "fallthroughWordCanJumpToDwordAtPlus4": (
            (fallthrough_handler.get("streamEffect") or {}).get("canJumpToDwordAtPlus4") is True
        ),
        "insideCurrentRootRange": CURRENT_ROOT <= ref_va < ROOT_TABLE_END_EXCLUSIVE,
        "insideCurrentRootEntryRun": ROOT_TABLE_POINTER <= ref_va < ROOT_TABLE_END_EXCLUSIVE,
        "referenceRole": role,
        "promotesWrapperSelection": promotes,
        "note": note,
    }


def build_summary(exe: bytes, leaf_streams: list[dict]) -> dict:
    sections = read_sections(exe)
    table_rows = [
        root_table_row(exe, sections, va, leaf_streams)
        for va in range(TABLE_WINDOW_START, ROOT_TABLE_END_EXCLUSIVE, 4)
    ]
    current_root_rows = [row for row in table_rows if row.get("insideCurrentRootEntryRun")]
    current_root_descriptor_rows = [
        row for row in current_root_rows if row.get("kind") == "descriptor-pointer"
    ]
    wrapper_refs = refs_to_value(exe, sections, WRAPPER_DESCRIPTOR)
    wrapper_entry_refs = [
        classify_wrapper_entry_ref(exe, sections, ref)
        for ref in refs_to_value(exe, sections, WRAPPER_ENTRY)
    ]
    frontier_refs = refs_to_value(exe, sections, FRONTIER_LEAF)
    wrapper_table_refs = [
        ref for ref in wrapper_refs
        if TABLE_WINDOW_START <= ref["refVa"] < ROOT_TABLE_END_EXCLUSIVE
    ]
    wrapper_current_root_refs = [
        ref for ref in wrapper_refs
        if ROOT_TABLE_POINTER <= ref["refVa"] < ROOT_TABLE_END_EXCLUSIVE
    ]
    frontier_current_root_refs = [
        ref for ref in frontier_refs
        if ROOT_TABLE_POINTER <= ref["refVa"] < ROOT_TABLE_END_EXCLUSIVE
    ]
    wrapper_entry_promoting_refs = [
        ref for ref in wrapper_entry_refs
        if ref.get("promotesWrapperSelection")
    ]
    wrapper_descriptor_words = [
        {
            "vaHex": hex32(WRAPPER_DESCRIPTOR + index * 4),
            "valueHex": hex32(dword_at(exe, sections, WRAPPER_DESCRIPTOR + index * 4) or 0),
        }
        for index in range(8)
    ]
    child = dword_at(exe, sections, WRAPPER_DESCRIPTOR + 4)
    conclusion = (
        "The 0x00542a04 candidate has descriptor shape 0x3f + child pointer 0x00542ae8, but its table reference "
        "is 0x005429ac, which is before the current 2:0 root table pointer 0x005429dc. The current root entry run "
        "contains later descriptors such as 0x00542a84, 0x00542a98, 0x00542aac, 0x00542ac0, and 0x00542ad4, not "
        "0x00542a04. The 0x00542ae8 reference at 0x00542a08 is therefore a descriptor child pointer rather than a "
        "direct current-root table entry. A separate current-root dword 0x0054090c equals 0x005429ac, but it follows "
        "opcode 0x5a mode 0 at 0x00540908; that handler advances by +4 and does not jump to [stream+4], so this is "
        "a fallthrough stream word rather than wrapper selection proof. Interpreted as the next stream word, "
        "0x005429ac has low opcode 0xac and that handler-table entry resolves to scalar 0x00000003 outside "
        "executable code, so the word also does not provide a code-backed wrapper jump. This keeps wrapper "
        "execution/selection unproven and the route blocked."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "rootHex": hex32(CURRENT_ROOT),
        "rootTablePointerHex": hex32(ROOT_TABLE_POINTER),
        "tableWindowHex": f"{hex32(TABLE_WINDOW_START)}..{hex32(ROOT_TABLE_END_EXCLUSIVE - 4)}",
        "currentRootEntryRunHex": f"{hex32(ROOT_TABLE_POINTER)}..{hex32(ROOT_TABLE_END_EXCLUSIVE - 4)}",
        "wrapperEntryHex": hex32(WRAPPER_ENTRY),
        "wrapperDescriptorHex": hex32(WRAPPER_DESCRIPTOR),
        "frontierLeafHex": hex32(FRONTIER_LEAF),
        "frontierReaderHex": hex32(FRONTIER_READER),
        "wrapperChildPointerHex": hex32(child) if child is not None else None,
        "wrapperChildFieldMaps": field_maps_for_child(leaf_streams, hex32(child) if child is not None else None),
        "tableRows": table_rows,
        "currentRootDescriptorCount": len(current_root_descriptor_rows),
        "currentRootDescriptorRows": current_root_descriptor_rows,
        "wrapperEntryRefs": wrapper_entry_refs,
        "wrapperEntryRefCount": len(wrapper_entry_refs),
        "wrapperEntryCurrentRootRangeRefCount": sum(1 for ref in wrapper_entry_refs if ref.get("insideCurrentRootRange")),
        "wrapperEntryCurrentRootEntryRunRefCount": sum(1 for ref in wrapper_entry_refs if ref.get("insideCurrentRootEntryRun")),
        "wrapperEntryPromotingRefCount": len(wrapper_entry_promoting_refs),
        "wrapperRefs": wrapper_refs,
        "wrapperTableRefs": wrapper_table_refs,
        "wrapperCurrentRootRefs": wrapper_current_root_refs,
        "frontierLeafRefs": frontier_refs,
        "frontierLeafCurrentRootRefs": frontier_current_root_refs,
        "wrapperRefBeforeCurrentRoot": any(ref["refVa"] < ROOT_TABLE_POINTER for ref in wrapper_table_refs),
        "currentRootReferencesWrapper": bool(wrapper_current_root_refs),
        "frontierLeafDirectCurrentRootRef": bool(frontier_current_root_refs),
        "wrapperChildIsFrontierLeaf": child == FRONTIER_LEAF,
        "wrapperDescriptorWords": wrapper_descriptor_words,
        "promotionStatus": "blocked",
        "remainingProofs": [
            "prove a runtime path selects descriptor 0x00542a04 despite its table ref being before the current root pointer",
            "or find a strict map1_01a source coordinate/hotspot",
        ],
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Wrapper Descriptor Context",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- root: `{summary['rootHex']}`",
        f"- root table pointer: `{summary['rootTablePointerHex']}`",
        f"- current root entry run: `{summary['currentRootEntryRunHex']}`",
        f"- wrapper entry: `{summary['wrapperEntryHex']}`",
        f"- wrapper descriptor: `{summary['wrapperDescriptorHex']}`",
        f"- wrapper child pointer: `{summary['wrapperChildPointerHex']}`",
        f"- frontier leaf: `{summary['frontierLeafHex']}`",
        f"- current root descriptor count: {summary['currentRootDescriptorCount']}",
        f"- wrapper ref before current root: {summary['wrapperRefBeforeCurrentRoot']}",
        f"- current root references wrapper: {summary['currentRootReferencesWrapper']}",
        f"- frontier leaf direct current-root ref: {summary['frontierLeafDirectCurrentRootRef']}",
        f"- wrapper child is frontier leaf: {summary['wrapperChildIsFrontierLeaf']}",
        f"- wrapper entry ref count: {summary['wrapperEntryRefCount']}",
        f"- wrapper entry refs in current root range: {summary['wrapperEntryCurrentRootRangeRefCount']}",
        f"- wrapper entry refs in current root entry run: {summary['wrapperEntryCurrentRootEntryRunRefCount']}",
        f"- wrapper entry promoting refs: {summary['wrapperEntryPromotingRefCount']}",
        f"- promotion status: {summary['promotionStatus']}",
        "",
        summary["conclusion"],
        "",
        "## Table Window",
        "",
        "| va | value | root entry? | kind | child | child maps |",
        "| --- | --- | --- | --- | --- | --- |",
    ]
    for row in summary["tableRows"]:
        lines.append(
            f"| `{row['vaHex']}` | `{row.get('valueHex')}` | {row.get('insideCurrentRootEntryRun')} | "
            f"{row.get('kind')} | `{row.get('childPointerHex') or '-'}` | "
            f"{', '.join(row.get('childFieldMaps') or []) or '-'} |"
        )
    lines.extend([
        "",
        "## Wrapper References",
        "",
        "| ref | section | before current root? | inside current root? |",
        "| --- | --- | --- | --- |",
    ])
    for ref in summary["wrapperRefs"]:
        before = ref["refVa"] < ROOT_TABLE_POINTER
        inside = ROOT_TABLE_POINTER <= ref["refVa"] < ROOT_TABLE_END_EXCLUSIVE
        lines.append(f"| `{ref['refVaHex']}` | {ref['section']} | {before} | {inside} |")
    lines.extend([
        "",
        "## Wrapper Entry References",
        "",
        "| ref | section | previous | opcode | mode | handler | fallthrough word | fallthrough handler | role | promotes? | note |",
        "| --- | --- | --- | --- | ---: | --- | --- | --- | --- | --- | --- |",
    ])
    for ref in summary["wrapperEntryRefs"]:
        lines.append(
            f"| `{ref['refVaHex']}` | {ref['section']} | `{ref.get('previousVaHex')}` "
            f"`{ref.get('previousValueHex')}` | `{ref.get('previousOpcodeHex')}` | "
            f"{ref.get('previousOpcodeMode')} | `{ref.get('previousHandlerHex')}` | "
            f"`{ref.get('fallthroughWordValueHex')}` / `{ref.get('fallthroughWordOpcodeHex')}` | "
            f"`{ref.get('fallthroughWordHandlerHex')}` / {ref.get('fallthroughWordHandlerSection') or '-'} | "
            f"{ref.get('referenceRole')} | {ref.get('promotesWrapperSelection')} | {ref.get('note')} |"
        )
    lines.extend(["", "## Wrapper Descriptor Words", "", "| va | value |", "| --- | --- |"])
    for row in summary["wrapperDescriptorWords"]:
        lines.append(f"| `{row['vaHex']}` | `{row['valueHex']}` |")
    lines.extend(["", "## Remaining Proofs", ""])
    lines.extend(f"- {item}" for item in summary["remainingProofs"])
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    table_body = []
    for row in summary["tableRows"]:
        table_body.append(
            "<tr>"
            f"<td><code>{html.escape(row['vaHex'])}</code></td>"
            f"<td><code>{html.escape(str(row.get('valueHex')))}</code></td>"
            f"<td>{row.get('insideCurrentRootEntryRun')}</td>"
            f"<td>{html.escape(str(row.get('kind')))}</td>"
            f"<td><code>{html.escape(row.get('childPointerHex') or '-')}</code></td>"
            f"<td>{html.escape(', '.join(row.get('childFieldMaps') or []) or '-')}</td>"
            "</tr>"
        )
    ref_body = []
    for ref in summary["wrapperRefs"]:
        before = ref["refVa"] < ROOT_TABLE_POINTER
        inside = ROOT_TABLE_POINTER <= ref["refVa"] < ROOT_TABLE_END_EXCLUSIVE
        ref_body.append(
            "<tr>"
            f"<td><code>{html.escape(ref['refVaHex'])}</code></td>"
            f"<td>{html.escape(ref['section'])}</td>"
            f"<td>{before}</td>"
            f"<td>{inside}</td>"
            "</tr>"
        )
    entry_ref_body = []
    for ref in summary["wrapperEntryRefs"]:
        previous = f"{ref.get('previousVaHex')} {ref.get('previousValueHex')}"
        entry_ref_body.append(
            "<tr>"
            f"<td><code>{html.escape(ref['refVaHex'])}</code></td>"
            f"<td>{html.escape(ref['section'])}</td>"
            f"<td><code>{html.escape(previous)}</code></td>"
            f"<td><code>{html.escape(ref.get('previousOpcodeHex') or '-')}</code></td>"
            f"<td>{html.escape(str(ref.get('previousOpcodeMode')))}</td>"
            f"<td><code>{html.escape(ref.get('previousHandlerHex') or '-')}</code></td>"
            f"<td><code>{html.escape(ref.get('fallthroughWordValueHex') or '-')}</code> / "
            f"<code>{html.escape(ref.get('fallthroughWordOpcodeHex') or '-')}</code></td>"
            f"<td><code>{html.escape(ref.get('fallthroughWordHandlerHex') or '-')}</code> / "
            f"{html.escape(ref.get('fallthroughWordHandlerSection') or '-')}</td>"
            f"<td>{html.escape(ref.get('referenceRole') or '-')}</td>"
            f"<td>{ref.get('promotesWrapperSelection')}</td>"
            f"<td>{html.escape(ref.get('note') or '-')}</td>"
            "</tr>"
        )
    word_body = "".join(
        f"<tr><td><code>{html.escape(row['vaHex'])}</code></td><td><code>{html.escape(row['valueHex'])}</code></td></tr>"
        for row in summary["wrapperDescriptorWords"]
    )
    proof_items = "".join(f"<li>{html.escape(item)}</li>" for item in summary["remainingProofs"])
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>Save Selector Wrapper Descriptor Context</title>",
        "  <style>",
        "    body { margin: 24px; background: #111; color: #eee; font: 14px system-ui, sans-serif; }",
        "    table { border-collapse: collapse; width: 100%; margin: 18px 0 28px; }",
        "    th, td { border: 1px solid #3a3a3a; padding: 6px 8px; vertical-align: top; }",
        "    th { background: #202020; position: sticky; top: 0; }",
        "    code { color: #9bd4ff; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Wrapper Descriptor Context</h1>",
        f"  <p>route: {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}; root <code>{summary['rootHex']}</code>; root table pointer <code>{summary['rootTablePointerHex']}</code>; current root entry run <code>{summary['currentRootEntryRunHex']}</code>.</p>",
        f"  <p>wrapper entry <code>{summary['wrapperEntryHex']}</code>; wrapper descriptor <code>{summary['wrapperDescriptorHex']}</code>; wrapper child pointer <code>{summary['wrapperChildPointerHex']}</code>; frontier leaf <code>{summary['frontierLeafHex']}</code>; frontier reader <code>{summary['frontierReaderHex']}</code>.</p>",
        f"  <p>wrapper ref before current root: {summary['wrapperRefBeforeCurrentRoot']}; current root references wrapper: {summary['currentRootReferencesWrapper']}; frontier leaf direct current-root ref: {summary['frontierLeafDirectCurrentRootRef']}; wrapper child is frontier leaf: {summary['wrapperChildIsFrontierLeaf']}; wrapper entry refs: {summary['wrapperEntryRefCount']}; wrapper entry promoting refs: {summary['wrapperEntryPromotingRefCount']}; promotion status: {html.escape(summary['promotionStatus'])}</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <h2>Table Window</h2>",
        "  <table><thead><tr><th>va</th><th>value</th><th>root entry?</th><th>kind</th><th>child</th><th>child maps</th></tr></thead>",
        f"  <tbody>{''.join(table_body)}</tbody></table>",
        "  <h2>Wrapper References</h2>",
        "  <table><thead><tr><th>ref</th><th>section</th><th>before current root?</th><th>inside current root?</th></tr></thead>",
        f"  <tbody>{''.join(ref_body)}</tbody></table>",
        "  <h2>Wrapper Entry References</h2>",
        "  <table><thead><tr><th>ref</th><th>section</th><th>previous</th><th>opcode</th><th>mode</th><th>handler</th><th>fallthrough word</th><th>fallthrough handler</th><th>role</th><th>promotes?</th><th>note</th></tr></thead>",
        f"  <tbody>{''.join(entry_ref_body)}</tbody></table>",
        "  <h2>Wrapper Descriptor Words</h2>",
        "  <table><thead><tr><th>va</th><th>value</th></tr></thead>",
        f"  <tbody>{word_body}</tbody></table>",
        "  <h2>Remaining Proofs</h2>",
        f"  <ul>{proof_items}</ul>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT, html_out: Path | None = None) -> Path:
    out_dir.mkdir(parents=True, exist_ok=True)
    json_out = out_dir / "save_selector_wrapper_descriptor_context.json"
    json_out.write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    if html_out is not None:
        html_out.parent.mkdir(parents=True, exist_ok=True)
        html_out.write_text(html_page(summary), encoding="utf-8")
    return json_out


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--leaf-streams", type=Path, default=OUT / "save_selector_leaf_streams.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--html-out", type=Path)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.leaf_streams, []),
    )
    json_out = write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote wrapper descriptor context -> {json_out}")


if __name__ == "__main__":
    main()
