#!/usr/bin/env python3
"""Index opcode 0x24 mode-1 action payload patterns across selector VM roots."""
from __future__ import annotations

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

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

from probe_exe_scene_tables import find_cns_strings, 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"

ROUTE_SOURCE = "map1_01a"
ROUTE_TARGET = "map2_02d"
CURRENT_OPCODE24_VA = 0x005428E4
LOOKAHEAD_DWORDS = 16
MAX_ROOT_BYTES = 0x8000

ROUTE_CNS = {
    f"{ROUTE_SOURCE}.cns",
    f"{ROUTE_TARGET}.cns",
}

FRONTIER_VALUES = {
    0x00542AE8: "frontier leaf",
    0x00542B0C: "frontier reader",
    0x0053F46F: "frontier reader false branch target",
    0x005429A8: "leaf table start",
    0x005429DC: "current root table pointer",
    0x00542A04: "wrapper leaf",
}


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:
    if not isinstance(value, str):
        return None
    return int(value, 16)


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 root_ranges(selectors: list[dict]) -> list[dict]:
    roots = []
    for row in selectors:
        selected = parse_hex(row.get("selectedPointerHex"))
        if selected is None:
            continue
        roots.append(
            {
                "selector": f"{row.get('group')}:{row.get('slot')}",
                "start": selected,
                "fieldMaps": row.get("fieldMaps") or [],
                "linkedCns": row.get("linkedCns") or [],
            }
        )
    roots.sort(key=lambda item: item["start"])
    ranges = []
    for index, root in enumerate(roots):
        next_start = roots[index + 1]["start"] if index + 1 < len(roots) else root["start"] + MAX_ROOT_BYTES
        end = next_start
        if end <= root["start"] or end - root["start"] > MAX_ROOT_BYTES:
            end = root["start"] + MAX_ROOT_BYTES
        ranges.append({**root, "end": end})
    return ranges


def classify_value(
    value: int | None,
    sections: list[dict],
    cns_strings: dict[int, str],
    local_start: int,
    local_end: int,
) -> dict:
    if value is None:
        return {"kind": "missing", "meaning": "-"}
    if value in FRONTIER_VALUES:
        return {"kind": "route-frontier", "meaning": FRONTIER_VALUES[value], "targetVaHex": hex32(value)}
    cns = cns_strings.get(value)
    if cns:
        if cns in ROUTE_CNS:
            kind = "route-cns"
        elif cns.startswith("map") and cns.endswith(".cns"):
            kind = "field-map-cns"
        else:
            kind = "cns"
        return {"kind": kind, "meaning": cns, "targetVaHex": hex32(value)}
    if local_start <= value <= local_end:
        return {"kind": "local-payload-pointer", "meaning": "pointer inside local lookahead window", "targetVaHex": hex32(value)}
    section = section_name_for_va(sections, value)
    if section:
        return {"kind": "exe-pointer", "meaning": section, "targetVaHex": hex32(value)}
    if value <= 0xFFFF:
        return {"kind": "small-scalar", "meaning": "small scalar"}
    if (value >> 16) <= 0x400 and (value & 0xFFFF) <= 0x10000:
        return {"kind": "pair", "meaning": "packed pair or script word"}
    return {"kind": "scalar", "meaning": "non-pointer scalar"}


def row_signature(lookahead: list[dict]) -> str:
    return ",".join(item["kind"] for item in lookahead)


def scan_roots(exe: bytes, sections: list[dict], selectors: list[dict]) -> list[dict]:
    cns_strings = find_cns_strings(exe, sections)
    rows = []
    for root in root_ranges(selectors):
        for va in range(root["start"], root["end"], 4):
            value = dword_at(exe, sections, va)
            if value is None or value & 0xFF != 0x24:
                continue
            pointer_collision = va_to_offset(sections, value) is not None
            mode = (value >> 8) & 0xFF
            local_start = va + 4
            local_end = va + LOOKAHEAD_DWORDS * 4
            lookahead = []
            for index in range(1, LOOKAHEAD_DWORDS + 1):
                item_va = va + index * 4
                item_value = dword_at(exe, sections, item_va)
                item_class = classify_value(item_value, sections, cns_strings, local_start, local_end)
                low_byte = item_value & 0xFF if item_value is not None else None
                handler = handler_for_opcode(exe, sections, low_byte) if low_byte is not None else {}
                lookahead.append(
                    {
                        "offsetHex": f"+0x{index * 4:x}",
                        "vaHex": hex32(item_va),
                        "valueHex": hex32(item_value) if item_value is not None else None,
                        "kind": item_class["kind"],
                        "meaning": item_class["meaning"],
                        "targetVaHex": item_class.get("targetVaHex"),
                        "lowByteHex": f"0x{low_byte:02x}" if low_byte is not None else None,
                        "lowByteHandlerSection": handler.get("handlerSection"),
                        "lowByteIsCodeHandler": handler.get("isCodeHandler"),
                    }
                )
            kind_counts = Counter(item["kind"] for item in lookahead)
            rows.append(
                {
                    "selector": root["selector"],
                    "rootRangeHex": f"{hex32(root['start'])}..{hex32(root['end'])}",
                    "fieldMaps": root["fieldMaps"],
                    "linkedCns": root["linkedCns"],
                    "va": va,
                    "vaHex": hex32(va),
                    "relativeOffsetHex": f"+0x{va - root['start']:x}",
                    "value": value,
                    "valueHex": hex32(value),
                    "mode": mode,
                    "modeHex": f"0x{mode:02x}",
                    "byte2Hex": f"0x{(value >> 16) & 0xFF:02x}",
                    "byte3Hex": f"0x{(value >> 24) & 0xFF:02x}",
                    "pointerCollision": pointer_collision,
                    "opcodeCandidate": not pointer_collision,
                    "mode1Candidate": (not pointer_collision) and mode == 1,
                    "currentBoundary": va == CURRENT_OPCODE24_VA,
                    "kindSignature": row_signature(lookahead),
                    "lookaheadKindCounts": dict(sorted(kind_counts.items())),
                    "directFrontierOperand": any(item["kind"] == "route-frontier" for item in lookahead),
                    "routeCnsOperand": any(item["kind"] == "route-cns" for item in lookahead),
                    "fieldMapCnsOperand": any(item["kind"] in {"route-cns", "field-map-cns"} for item in lookahead),
                    "localPayloadPointer": any(item["kind"] == "local-payload-pointer" for item in lookahead),
                    "codeLowByteCount": sum(1 for item in lookahead if item.get("lowByteIsCodeHandler") is True),
                    "dataLowByteCount": sum(1 for item in lookahead if item.get("lowByteHandlerSection") == ".data"),
                    "lookahead": lookahead,
                }
            )
    return rows


def brief_row(row: dict) -> dict:
    return {
        "selector": row.get("selector"),
        "vaHex": row.get("vaHex"),
        "valueHex": row.get("valueHex"),
        "relativeOffsetHex": row.get("relativeOffsetHex"),
        "rootRangeHex": row.get("rootRangeHex"),
        "fieldMaps": row.get("fieldMaps") or [],
        "kindSignature": row.get("kindSignature"),
        "lookaheadKindCounts": row.get("lookaheadKindCounts") or {},
        "directFrontierOperand": row.get("directFrontierOperand"),
        "routeCnsOperand": row.get("routeCnsOperand"),
        "fieldMapCnsOperand": row.get("fieldMapCnsOperand"),
        "localPayloadPointer": row.get("localPayloadPointer"),
        "codeLowByteCount": row.get("codeLowByteCount"),
        "dataLowByteCount": row.get("dataLowByteCount"),
        "lookahead": row.get("lookahead") or [],
    }


def build_summary(exe: bytes, selectors: list[dict]) -> dict:
    sections = read_sections(exe)
    rows = scan_roots(exe, sections, selectors)
    opcode_rows = [row for row in rows if row["opcodeCandidate"]]
    mode1_rows = [row for row in rows if row["mode1Candidate"]]
    current = next((row for row in rows if row["currentBoundary"]), None)
    current_value = current.get("value") if current else None
    current_signature = current.get("kindSignature") if current else None
    current_value_peers = [
        row for row in mode1_rows
        if current_value is not None and row.get("value") == current_value
    ]
    current_signature_peers = [
        row for row in mode1_rows
        if current_signature is not None and row.get("kindSignature") == current_signature
    ]
    route_scope_rows = [
        row for row in mode1_rows
        if ROUTE_SOURCE in (row.get("fieldMaps") or []) or ROUTE_TARGET in (row.get("fieldMaps") or [])
    ]
    value_counts = Counter(row["valueHex"] for row in mode1_rows)
    top_values = [
        {
            "valueHex": value_hex,
            "count": count,
            "selectorCount": len({
                row["selector"] for row in mode1_rows
                if row["valueHex"] == value_hex
            }),
        }
        for value_hex, count in value_counts.most_common(20)
    ]
    direct_frontier = [row for row in mode1_rows if row["directFrontierOperand"]]
    route_cns = [row for row in mode1_rows if row["routeCnsOperand"]]
    field_map_cns = [row for row in mode1_rows if row["fieldMapCnsOperand"]]
    conclusion = (
        "Opcode 0x24 mode 1 is common in selector VM roots, and the current route boundary's exact dword "
        "appears in many unrelated roots. Across all mode-1 candidates, the next 16 dwords do not contain "
        "direct frontier values or CNS filename operands. This supports treating the current 0x005428e4 region "
        "as generic action/state payload evidence, not as a strict map1_01a->map2_02d transition proof."
    )
    return {
        "title": "Opcode 0x24 Action Payload Pattern Index",
        "scope": "selector VM roots from original EXE scene-selector table; no savedata slot parsing",
        "source": ROUTE_SOURCE,
        "target": ROUTE_TARGET,
        "currentOpcode24VaHex": hex32(CURRENT_OPCODE24_VA),
        "lookaheadDwords": LOOKAHEAD_DWORDS,
        "selectorRootCount": len(root_ranges(selectors)),
        "lowByte24RowCount": len(rows),
        "opcodeCandidateCount": len(opcode_rows),
        "pointerCollisionCount": len(rows) - len(opcode_rows),
        "mode1CandidateCount": len(mode1_rows),
        "mode1SelectorCount": len({row["selector"] for row in mode1_rows}),
        "mode1CurrentRouteScopeRowCount": len(route_scope_rows),
        "mode1CurrentRouteScopeSelectorCount": len({row["selector"] for row in route_scope_rows}),
        "directFrontierOperandCount": len(direct_frontier),
        "routeCnsOperandCount": len(route_cns),
        "fieldMapCnsOperandCount": len(field_map_cns),
        "localPayloadPointerRowCount": sum(1 for row in mode1_rows if row["localPayloadPointer"]),
        "currentBoundaryFound": current is not None,
        "currentBoundary": brief_row(current) if current else None,
        "currentValuePeerCount": len(current_value_peers),
        "currentValuePeerSelectorCount": len({row["selector"] for row in current_value_peers}),
        "currentValuePeerSamples": [brief_row(row) for row in current_value_peers[:40]],
        "currentSignaturePeerCount": len(current_signature_peers),
        "currentSignaturePeerSelectorCount": len({row["selector"] for row in current_signature_peers}),
        "currentSignaturePeerSamples": [brief_row(row) for row in current_signature_peers[:20]],
        "topMode1Values": top_values,
        "frontierOperandSamples": [brief_row(row) for row in direct_frontier[:20]],
        "routeCnsOperandSamples": [brief_row(row) for row in route_cns[:20]],
        "fieldMapCnsOperandSamples": [brief_row(row) for row in field_map_cns[:20]],
        "promotionStatus": "blocked",
        "classification": "generic-action-state-payload",
        "remainingProofs": [
            "runtime producer trace for opcode 0x24 mode1 source byte 0x0059e348",
            "strict map1_01a source coordinate or hotspot",
            "control-flow proof that selector leaf/wrapper execution reaches the frontier reader",
        ],
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Opcode 0x24 Action Payload Pattern Index",
        "",
        f"Scope: {summary['scope']}.",
        "",
        f"- route under test: `{summary['source']} -> {summary['target']}`",
        f"- current opcode 0x24 boundary: `{summary['currentOpcode24VaHex']}`",
        f"- lookahead: {summary['lookaheadDwords']} dwords",
        f"- selector roots scanned: {summary['selectorRootCount']}",
        f"- low-byte 0x24 rows: {summary['lowByte24RowCount']}",
        f"- opcode candidates: {summary['opcodeCandidateCount']}",
        f"- pointer collisions: {summary['pointerCollisionCount']}",
        f"- mode 1 candidates: {summary['mode1CandidateCount']} across {summary['mode1SelectorCount']} selector roots",
        f"- route-scope mode 1 candidates: {summary['mode1CurrentRouteScopeRowCount']} across {summary['mode1CurrentRouteScopeSelectorCount']} selector roots",
        f"- direct frontier operands in mode 1 lookahead: {summary['directFrontierOperandCount']}",
        f"- route CNS operands in mode 1 lookahead: {summary['routeCnsOperandCount']}",
        f"- field-map CNS operands in mode 1 lookahead: {summary['fieldMapCnsOperandCount']}",
        f"- mode 1 rows with local payload pointers: {summary['localPayloadPointerRowCount']}",
        f"- current boundary found: {summary['currentBoundaryFound']}",
        f"- current value peers: {summary['currentValuePeerCount']} rows across {summary['currentValuePeerSelectorCount']} selector roots",
        f"- current signature peers: {summary['currentSignaturePeerCount']} rows across {summary['currentSignaturePeerSelectorCount']} selector roots",
        f"- classification: {summary['classification']}",
        f"- promotion status: {summary['promotionStatus']}",
        "",
        summary["conclusion"],
        "",
        "## Current Boundary",
        "",
    ]
    current = summary.get("currentBoundary") or {}
    if current:
        lines.extend([
            f"- selector: `{current.get('selector')}`",
            f"- root range: `{current.get('rootRangeHex')}`",
            f"- va/value: `{current.get('vaHex')}` / `{current.get('valueHex')}`",
            f"- field maps: {', '.join(current.get('fieldMaps') or []) or '-'}",
            f"- lookahead kind counts: `{json.dumps(current.get('lookaheadKindCounts') or {}, sort_keys=True)}`",
            "",
            "| offset | va | value | kind | meaning | low byte handler |",
            "| --- | --- | --- | --- | --- | --- |",
        ])
        for item in current.get("lookahead") or []:
            lines.append(
                f"| `{item.get('offsetHex')}` | `{item.get('vaHex')}` | `{item.get('valueHex')}` | "
                f"{item.get('kind')} | {html.escape(str(item.get('meaning')))} | "
                f"{item.get('lowByteHandlerSection') or '-'} |"
            )
        lines.append("")
    lines.extend([
        "## Top Mode 1 Values",
        "",
        "| value | rows | selector roots |",
        "| --- | ---: | ---: |",
    ])
    for row in summary["topMode1Values"]:
        lines.append(f"| `{row['valueHex']}` | {row['count']} | {row['selectorCount']} |")
    lines.extend([
        "",
        "## Current Value Peer Samples",
        "",
        "| selector | va | value | route maps | local payload ptr | frontier operand | route CNS operand |",
        "| --- | --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary["currentValuePeerSamples"]:
        route_maps = ", ".join(row.get("fieldMaps") or []) or "-"
        lines.append(
            f"| `{row.get('selector')}` | `{row.get('vaHex')}` | `{row.get('valueHex')}` | "
            f"{route_maps} | {row.get('localPayloadPointer')} | {row.get('directFrontierOperand')} | {row.get('routeCnsOperand')} |"
        )
    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:
    current = summary.get("currentBoundary") or {}
    current_rows = ""
    if current:
        current_rows = "".join(
            "<tr>"
            f"<td><code>{html.escape(str(item.get('offsetHex')))}</code></td>"
            f"<td><code>{html.escape(str(item.get('vaHex')))}</code></td>"
            f"<td><code>{html.escape(str(item.get('valueHex')))}</code></td>"
            f"<td>{html.escape(str(item.get('kind')))}</td>"
            f"<td>{html.escape(str(item.get('meaning')))}</td>"
            f"<td>{html.escape(str(item.get('lowByteHandlerSection') or '-'))}</td>"
            "</tr>"
            for item in current.get("lookahead") or []
        )
    value_rows = "".join(
        "<tr>"
        f"<td><code>{html.escape(row['valueHex'])}</code></td>"
        f"<td>{row['count']}</td>"
        f"<td>{row['selectorCount']}</td>"
        "</tr>"
        for row in summary["topMode1Values"]
    )
    peer_rows = "".join(
        "<tr>"
        f"<td><code>{html.escape(str(row.get('selector')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('vaHex')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('valueHex')))}</code></td>"
        f"<td>{html.escape(', '.join(row.get('fieldMaps') or []) or '-')}</td>"
        f"<td>{html.escape(str(row.get('localPayloadPointer')))}</td>"
        f"<td>{html.escape(str(row.get('directFrontierOperand')))}</td>"
        f"<td>{html.escape(str(row.get('routeCnsOperand')))}</td>"
        "</tr>"
        for row in summary["currentValuePeerSamples"]
    )
    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>Opcode 0x24 Action Payload Pattern Index</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>Opcode 0x24 Action Payload Pattern Index</h1>",
        f"  <p>Scope: {html.escape(summary['scope'])}.</p>",
        "  <ul>",
        f"    <li>route: <code>{html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}</code></li>",
        f"    <li>current opcode 0x24 boundary: <code>{summary['currentOpcode24VaHex']}</code></li>",
        f"    <li>selector roots scanned: {summary['selectorRootCount']}</li>",
        f"    <li>mode 1 candidates: {summary['mode1CandidateCount']} across {summary['mode1SelectorCount']} selector roots</li>",
        f"    <li>route-scope mode 1 candidates: {summary['mode1CurrentRouteScopeRowCount']} across {summary['mode1CurrentRouteScopeSelectorCount']} selector roots</li>",
        f"    <li>direct frontier operands: {summary['directFrontierOperandCount']}</li>",
        f"    <li>route CNS operands: {summary['routeCnsOperandCount']}</li>",
        f"    <li>field-map CNS operands: {summary['fieldMapCnsOperandCount']}</li>",
        f"    <li>current value peers: {summary['currentValuePeerCount']} rows across {summary['currentValuePeerSelectorCount']} roots</li>",
        f"    <li>current signature peers: {summary['currentSignaturePeerCount']} rows across {summary['currentSignaturePeerSelectorCount']} roots</li>",
        f"    <li>classification: {html.escape(summary['classification'])}</li>",
        f"    <li>promotion status: {html.escape(summary['promotionStatus'])}</li>",
        "  </ul>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <h2>Current Boundary</h2>",
        f"  <p>selector <code>{html.escape(str(current.get('selector')))}</code>; root <code>{html.escape(str(current.get('rootRangeHex')))}</code>; value <code>{html.escape(str(current.get('valueHex')))}</code>.</p>",
        "  <table><thead><tr><th>offset</th><th>va</th><th>value</th><th>kind</th><th>meaning</th><th>low-byte handler section</th></tr></thead>",
        f"  <tbody>{current_rows}</tbody></table>",
        "  <h2>Top Mode 1 Values</h2>",
        "  <table><thead><tr><th>value</th><th>rows</th><th>selector roots</th></tr></thead>",
        f"  <tbody>{value_rows}</tbody></table>",
        "  <h2>Current Value Peer Samples</h2>",
        "  <table><thead><tr><th>selector</th><th>va</th><th>value</th><th>field maps</th><th>local ptr</th><th>frontier operand</th><th>route CNS operand</th></tr></thead>",
        f"  <tbody>{peer_rows}</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) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "opcode24_action_payload_patterns.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")),
        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")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--selectors", type=Path, default=OUT / "save_scene_selectors.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--html-out", type=Path, default=None, help="Optional HTML report output path.")
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.selectors, []),
    )
    write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote opcode24 action payload patterns -> {args.out_dir / 'opcode24_action_payload_patterns.json'}")


if __name__ == "__main__":
    main()
