#!/usr/bin/env python3
"""Ground the generic VM opcode 0x14 branch shape.

Opcode 0x14 is easy to misread because the opcode byte overlaps the low byte of
the branch target dword.  A plausible command therefore starts with a
file-backed target VA whose low byte is 0x14, but raw VA-shaped hits are still
only candidates until a known stream boundary reaches them.  This report records
that rule and explicitly checks the HUD top-menu post-selectedRoot table, where
a byte also looks like 0x14 but the overlapped dword is not a VA.
"""
from __future__ import annotations

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

from probe_exe_scene_tables import offset_to_va, read_sections, va_to_offset


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

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

GENERAL_VM_TABLE_VA = 0x00440538
OPCODE14_HANDLER_VA = 0x00403890
HUD_POST_SELECTED_ROOT_TABLE_VA = 0x0047E670


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 section_name_for(sections: list[dict[str, Any]], va: int | None) -> str | None:
    if va is None:
        return None
    for section in sections:
        start = int(section["va"])
        end = start + int(section["raw_size"])
        if start <= va < end:
            return str(section["name"])
    return None


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


def source_mode(byte1: int) -> str:
    left = byte1 & 0xC0
    if left == 0x40:
        left_text = "left=global-u32[0x59db60 + byte2*4]"
    elif left == 0x80:
        left_text = "left=object-word[byte2]"
    elif left == 0xC0:
        left_text = "left=linked/base-word[object+0xa8 + byte2]"
    else:
        left_text = "left=implicit/default"

    right = byte1 & 0x30
    if right == 0x00:
        right_text = "right=inline-u16(stream+4); false advance +8"
    elif right == 0x10:
        right_text = "right=global-u32[0x59db60 + byte3*4]; false advance +4"
    elif right == 0x20:
        right_text = "right=object-word[byte3]; false advance +4"
    elif right == 0x30:
        right_text = "right=linked/base-word[object+0xa8 + byte3]; false advance +4"
    else:
        right_text = "right=unknown"
    return f"{left_text}; {right_text}"


def expected_length(byte1: int) -> int:
    return 8 if (byte1 & 0x30) == 0x00 else 4


def scan_opcode14_candidates(exe: bytes, sections: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
    valid: list[dict[str, Any]] = []
    invalid: list[dict[str, Any]] = []
    for section in sections:
        if section.get("name") not in {".data", ".rdata"}:
            continue
        start = int(section["raw"])
        end = start + int(section["raw_size"])
        pos = exe.find(b"\x14", start, end)
        while pos != -1:
            va = offset_to_va(sections, pos)
            if va is not None and pos + 8 <= len(exe):
                target = struct.unpack_from("<I", exe, pos)[0]
                byte1 = exe[pos + 1]
                row = {
                    "va": va,
                    "vaHex": hx(va),
                    "rawFirst8Hex": exe[pos : pos + 8].hex(" "),
                    "target": target,
                    "targetVaHex": hx(target),
                    "targetIsVa": va_to_offset(sections, target) is not None,
                    "targetSection": section_name_for(sections, target),
                    "byte1Hex": f"0x{byte1:02x}",
                    "byte2Hex": f"0x{exe[pos + 2]:02x}",
                    "byte3Hex": f"0x{exe[pos + 3]:02x}",
                    "expectedLength": expected_length(byte1),
                    "sourceMode": source_mode(byte1),
                }
                if row["targetIsVa"] and (target & 0xFF) == 0x14:
                    row["classification"] = "va-shaped-overlapped-target-candidate"
                    valid.append(row)
                else:
                    row["classification"] = "invalid-or-data-byte"
                    invalid.append(row)
            pos = exe.find(b"\x14", pos + 1, end)
    return valid, invalid


def build_report() -> dict[str, Any]:
    exe = EXE.read_bytes()
    sections = read_sections(exe)
    handler = dword_at(exe, sections, GENERAL_VM_TABLE_VA + 0x14 * 4)
    valid, invalid = scan_opcode14_candidates(exe, sections)
    hud_first = dword_at(exe, sections, HUD_POST_SELECTED_ROOT_TABLE_VA)
    hud_row = next((row for row in invalid if row["va"] == HUD_POST_SELECTED_ROOT_TABLE_VA), None)
    mode_counts = Counter(row["byte1Hex"] for row in valid)
    section_counts = Counter(row["targetSection"] for row in valid)
    invalid_reason_counts = Counter(
        "target-not-va" if not row["targetIsVa"] else "target-low-byte-not-0x14"
        for row in invalid
    )

    summary = {
        "opcode14HandlerVaHex": hx(handler),
        "opcode14HandlerGrounded": handler == OPCODE14_HANDLER_VA,
        "vaShapedOverlappedTargetCandidateCount": len(valid),
        "invalidOrDataByteCount": len(invalid),
        "vaShapedModeByteCounts": dict(sorted(mode_counts.items())),
        "vaShapedTargetSectionCounts": dict(sorted(section_counts.items())),
        "invalidReasonCounts": dict(sorted(invalid_reason_counts.items())),
        "hudPostSelectedRootVaHex": hx(HUD_POST_SELECTED_ROOT_TABLE_VA),
        "hudPostSelectedRootFirstDwordHex": hx(hud_first),
        "hudRejectedAsOpcode14": bool(hud_row),
        "decision": (
            "Opcode 0x14 handler semantics are grounded as an overlapped-target conditional "
            "branch shape: dword[stream] is the true target and must be file-backed with low "
            "byte 0x14 for a plausible command.  Raw VA-shaped hits are not promoted by "
            "themselves; a known stream boundary must reach them.  The false path advance is "
            "+8 only for inline-u16 right operands, otherwise +4.  The HUD 0x0047e670 byte is "
            "not a valid 0x14 command because dword[stream] is 0x0002c614, not a file-backed VA."
        ),
    }
    return {
        "kind": "hwanse-generic-vm-opcode14-branch-review",
        "source": "tools/build_generic_vm_opcode14_branch_review.py",
        "status": "opcode14-branch-shape-grounded-boundary-filter-required",
        "summary": summary,
        "semantics": {
            "handlerTableEntryVaHex": hx(GENERAL_VM_TABLE_VA + 0x14 * 4),
            "handlerVaHex": hx(handler),
            "branchTargetEncoding": "target is dword[stream], so opcode 0x14 overlaps the target low byte",
            "truePath": "object+0x40 = dword[stream]",
            "falsePath": "advance +8 for inline-u16 right operand, otherwise advance +4",
            "compareHelperVaHex": "0x0040370a",
        },
        "hudFalsePositiveRow": hud_row,
        "vaShapedCandidateSamples": valid[:80],
        "invalidSamples": invalid[:80],
        "negativeEvidence": [
            "Do not classify a raw byte 0x14 as a command unless dword[stream] is a file-backed VA ending in 0x14.",
            "0x0047e670 is explicitly rejected by that rule and remains a post-selectedRoot table/data row.",
        ],
    }


def table(rows: list[dict[str, Any]]) -> str:
    body = []
    for row in rows:
        body.append(
            "<tr>"
            f"<td><code>{h(row.get('vaHex'))}</code></td>"
            f"<td><code>{h(row.get('targetVaHex'))}</code><br>{h(row.get('targetSection'))}</td>"
            f"<td>{h(row.get('byte1Hex'))}<br>{h(row.get('sourceMode'))}</td>"
            f"<td>{h(row.get('expectedLength'))}</td>"
            f"<td><code>{h(row.get('rawFirst8Hex'))}</code></td>"
            "</tr>"
        )
    return "<table><thead><tr><th>VA</th><th>target</th><th>mode</th><th>len</th><th>raw</th></tr></thead><tbody>" + "".join(body) + "</tbody></table>"


def render_html(report: dict[str, Any]) -> str:
    summary = report["summary"]
    cards = [
        ("status", report["status"]),
        ("handler", summary["opcode14HandlerVaHex"]),
        ("VA-shaped", summary["vaShapedOverlappedTargetCandidateCount"]),
        ("invalid", summary["invalidOrDataByteCount"]),
        ("HUD rejected", summary["hudRejectedAsOpcode14"]),
    ]
    card_html = "".join(f"<div class='card'><b>{h(k)}</b><span>{h(v)}</span></div>" for k, v in cards)
    payload = json.dumps(report, ensure_ascii=False)
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <title>Generic VM Opcode 0x14 Branch Review</title>
  <style>
    body {{ margin: 0; padding: 24px; font-family: system-ui, sans-serif; background: #f6f1e8; color: #1e2430; }}
    h1 {{ margin: 0 0 8px; }}
    h2 {{ margin: 22px 0 8px; }}
    .sub {{ color: #606875; margin-top: 0; }}
    .cards {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 10px; margin: 18px 0; }}
    .card {{ background: #fffaf2; border: 1px solid #d8c8aa; border-radius: 6px; padding: 10px 12px; }}
    .card b {{ display: block; color: #6a421b; font-size: 12px; }}
    .card span {{ font-weight: 700; }}
    section {{ background: white; border: 1px solid #decfb2; border-radius: 6px; padding: 14px; margin: 14px 0; overflow: auto; }}
    table {{ width: 100%; border-collapse: collapse; font-size: 13px; }}
    th, td {{ border-bottom: 1px solid #eadcc5; text-align: left; vertical-align: top; padding: 7px 8px; }}
    th {{ background: #f3e6d0; }}
    code {{ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }}
    pre {{ white-space: pre-wrap; background: #272822; color: #f8f8f2; padding: 12px; border-radius: 6px; }}
  </style>
</head>
<body>
  <h1>Generic VM Opcode 0x14 Branch Review</h1>
  <p class="sub">opcode 0x14의 target-overlap branch 형태와 HUD false positive 배제 규칙.</p>
  <div class="cards">{card_html}</div>
  <section><h2>Decision</h2><p>{h(summary["decision"])}</p></section>
  <section><h2>HUD False Positive</h2><pre>{h(json.dumps(report["hudFalsePositiveRow"], ensure_ascii=False, indent=2))}</pre></section>
  <section><h2>VA-shaped Candidate Samples</h2>{table(report["vaShapedCandidateSamples"])}</section>
  <section><h2>Invalid Samples</h2>{table(report["invalidSamples"])}</section>
  <section><h2>Raw JSON</h2><pre id="payload"></pre></section>
  <script>
    const payload = {payload};
    document.getElementById('payload').textContent = JSON.stringify(payload, null, 2);
  </script>
</body>
</html>
"""


def main() -> None:
    report = build_report()
    OUT.mkdir(exist_ok=True)
    WEB.mkdir(exist_ok=True)
    JSON_OUT.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
    HTML_OUT.write_text(render_html(report), encoding="utf-8")
    print("generic_vm_opcode14_branch_review ok")


if __name__ == "__main__":
    main()
