#!/usr/bin/env python3
"""Summarize Korean text refs inside event/object VM handlers."""
from __future__ import annotations

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

from exe_call_scanner import direct_text_calls
from probe_exe_scene_tables import read_sections, va_to_offset
from summarize_text_routine_callers import classify_caller, text_index


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


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


def load_json(path: Path):
    return json.loads(path.read_text(encoding="utf-8"))


def handler_ranges(dispatch_summary: dict) -> list[dict]:
    grouped: dict[int, dict] = {}
    for entry in dispatch_summary.get("tableEntries") or []:
        if entry.get("handlerSection") != ".text" or not entry.get("handlerVaHex"):
            continue
        handler_va = int(entry["handlerVaHex"], 16)
        row = grouped.setdefault(handler_va, {
            "handlerVa": handler_va,
            "handlerVaHex": entry["handlerVaHex"],
            "opcodes": [],
            "entryVaHexes": [],
        })
        row["opcodes"].append(entry["opcodeHex"])
        row["entryVaHexes"].append(entry["entryVaHex"])
    rows = sorted(grouped.values(), key=lambda row: row["handlerVa"])
    for index, row in enumerate(rows):
        end_va = rows[index + 1]["handlerVa"] if index + 1 < len(rows) else row["handlerVa"] + 0x400
        row["endVa"] = end_va
        row["endVaHex"] = hex32(end_va)
        row["byteCount"] = max(0, end_va - row["handlerVa"])
    return rows


def scan_range_refs(
    exe: bytes,
    sections: list[dict],
    start_va: int,
    end_va: int,
    texts: dict[int, dict],
    clusters: list[dict],
) -> tuple[list[dict], list[dict], str]:
    start_off = va_to_offset(sections, start_va)
    end_off = va_to_offset(sections, end_va)
    if start_off is None or end_off is None or end_off <= start_off:
        return [], [], ""
    data = exe[start_off:end_off]
    text_refs = []
    table_refs = []
    seen_text = set()
    seen_table = set()
    for index in range(0, max(0, len(data) - 3)):
        value = struct.unpack_from("<I", data, index)[0]
        ref_va = start_va + index
        if value in texts and (ref_va, value) not in seen_text:
            seen_text.add((ref_va, value))
            text_refs.append({
                "refVaHex": hex32(ref_va),
                "valueHex": hex32(value),
                **texts[value],
            })
        for cluster in clusters:
            if cluster["matchStartVa"] <= value <= cluster["matchEndVa"] and (ref_va, cluster["startVa"]) not in seen_table:
                seen_table.add((ref_va, cluster["startVa"]))
                table_refs.append({
                    "refVaHex": hex32(ref_va),
                    "valueHex": hex32(value),
                    "clusterStartVaHex": cluster["startVaHex"],
                    "clusterEndVaHex": cluster["endVaHex"],
                    "entryCount": cluster["entryCount"],
                    "classificationCounts": cluster["classificationCounts"],
                    "sampleTexts": cluster["sampleTexts"][:8],
                })
    return text_refs, table_refs, data[:160].hex(" ")


def classify_handler(text_refs: list[dict], table_refs: list[dict], text_routine_calls: list[dict]) -> str:
    classification = classify_caller(text_refs, table_refs)
    if classification != "unclassified":
        return classification
    if text_routine_calls:
        return "text-routine-runtime-source"
    return "unclassified"


def sample_text(text_refs: list[dict], table_refs: list[dict]) -> str:
    samples = []
    for ref in text_refs:
        samples.append(ref.get("text", ""))
    for ref in table_refs:
        samples.extend(ref.get("sampleTexts") or [])
    return ", ".join(samples[:8]) or "-"


def build_summary(
    exe: bytes,
    korean_summary: dict,
    dispatch_summary: dict,
    exe_path: Path | None = None,
) -> dict:
    sections = read_sections(exe)
    texts, clusters = text_index(korean_summary)
    routine_targets = {
        int(row["targetVaHex"], 16)
        for row in korean_summary.get("textRoutineCandidates") or []
        if row.get("targetVaHex")
    }
    text_calls, call_scan_method = direct_text_calls(exe, sections, exe_path)
    rows = []
    for handler in handler_ranges(dispatch_summary):
        start = handler["handlerVa"]
        end = handler["endVa"]
        direct_calls = [call for call in text_calls if start <= call["callVa"] < end]
        text_routine_calls = [call for call in direct_calls if call["targetVa"] in routine_targets]
        text_refs, table_refs, code_bytes = scan_range_refs(exe, sections, start, end, texts, clusters)
        if not text_routine_calls and not text_refs and not table_refs:
            continue
        call_target_counts = Counter(call["targetVaHex"] for call in direct_calls)
        rows.append({
            **handler,
            "classification": classify_handler(text_refs, table_refs, text_routine_calls),
            "textRoutineCallCount": len(text_routine_calls),
            "textRoutineCalls": text_routine_calls[:12],
            "directTextRefCount": len(text_refs),
            "directTextRefs": text_refs[:12],
            "textTableRefCount": len(table_refs),
            "textTableRefs": table_refs[:12],
            "directCallTargetCounts": dict(sorted(call_target_counts.items())),
            "codeBytes": code_bytes,
            "samples": sample_text(text_refs, table_refs),
        })
    class_counts = Counter(row["classification"] for row in rows)
    direct_text_routine_handlers = sum(1 for row in rows if row["textRoutineCallCount"])
    return {
        "scope": "event/object VM handlers cross-checked against executable Korean text refs and text routine calls",
        "handlerCount": len(handler_ranges(dispatch_summary)),
        "matchedHandlerCount": len(rows),
        "directTextRoutineHandlerCount": direct_text_routine_handlers,
        "directTextCallScanMethod": call_scan_method,
        "classificationCounts": dict(sorted(class_counts.items())),
        "conclusion": (
            "The event/object VM table has one handler with a direct call to a known Korean text routine candidate "
            "(opcode 0x0b -> 0x0041b579), but that handler has no local static Korean text/table reference. "
            "Other matched handlers reference item/action text tables. No story-dialogue handler has been proven from "
            "these static handler bodies."
        ),
        "rows": rows,
    }


def call_summary(row: dict) -> str:
    calls = row.get("textRoutineCalls") or []
    return ", ".join(f"{call['callVaHex']}->{call['targetVaHex']}" for call in calls) or "-"


def markdown(summary: dict) -> str:
    lines = [
        "# Event Handler Text References",
        "",
        summary["conclusion"],
        "",
        f"- scanned handlers: {summary['handlerCount']}",
        f"- matched handlers: {summary['matchedHandlerCount']}",
        f"- direct text-routine handlers: {summary['directTextRoutineHandlerCount']}",
        f"- direct call scan: {summary.get('directTextCallScanMethod', '-')}",
        f"- classes: {json.dumps(summary.get('classificationCounts') or {}, ensure_ascii=False)}",
        "",
        "| opcodes | handler range | class | text routine calls | direct text | table refs | samples | call targets |",
        "| --- | --- | --- | --- | ---: | ---: | --- | --- |",
    ]
    for row in summary.get("rows") or []:
        targets = ", ".join(
            f"{target}:{count}"
            for target, count in sorted((row.get("directCallTargetCounts") or {}).items())[:8]
        ) or "-"
        samples = row["samples"].replace("|", "\\|")
        lines.append(
            f"| {', '.join(f'`{opcode}`' for opcode in row['opcodes'])} | "
            f"`{row['handlerVaHex']}..{row['endVaHex']}` | {row['classification']} | "
            f"{call_summary(row)} | {row['directTextRefCount']} | {row['textTableRefCount']} | "
            f"{samples} | {targets} |"
        )
    if not summary.get("rows"):
        lines.append("| - | - | - | - | - | - | - | - |")
    return "\n".join(lines) + "\n"


def html_page(summary: dict) -> str:
    rows = []
    for row in summary.get("rows") or []:
        targets = ", ".join(
            f"{target}:{count}"
            for target, count in sorted((row.get("directCallTargetCounts") or {}).items())[:8]
        ) or "-"
        rows.append(
            "<tr>"
            f"<td>{html.escape(', '.join(row['opcodes']))}</td>"
            f"<td><code>{html.escape(row['handlerVaHex'])}..{html.escape(row['endVaHex'])}</code></td>"
            f"<td>{html.escape(row['classification'])}</td>"
            f"<td>{html.escape(call_summary(row))}</td>"
            f"<td>{row['directTextRefCount']}</td>"
            f"<td>{row['textTableRefCount']}</td>"
            f"<td>{html.escape(row['samples'])}</td>"
            f"<td>{html.escape(targets)}</td>"
            "</tr>"
        )
    body_rows = "".join(rows) or '<tr><td colspan="8">No matched handlers.</td></tr>'
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>Event Handler Text References</title>",
        "  <style>body{font-family:system-ui,sans-serif;background:#101010;color:#eee;margin:24px}table{border-collapse:collapse;width:100%}td,th{border:1px solid #333;padding:6px 8px;vertical-align:top}th{background:#1d1d1d}code{color:#f5d76e}</style>",
        "</head>",
        "<body>",
        "  <h1>Event Handler Text References</h1>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        f"  <p>Scanned handlers: {summary['handlerCount']}; matched: {summary['matchedHandlerCount']}; "
        f"direct text-routine handlers: {summary['directTextRoutineHandlerCount']}; "
        f"direct call scan: {html.escape(summary.get('directTextCallScanMethod', '-'))}.</p>",
        "  <table><thead><tr><th>opcodes</th><th>handler range</th><th>class</th><th>text routine calls</th><th>direct text</th><th>table refs</th><th>samples</th><th>call targets</th></tr></thead>",
        f"  <tbody>{body_rows}</tbody></table>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "event_handler_text_refs.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--korean-text", type=Path, default=OUT / "korean_text_candidates.json")
    parser.add_argument("--dispatch", type=Path, default=OUT / "save_selector_branch_state_dispatch.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.korean_text),
        load_json(args.dispatch),
        args.exe,
    )
    write_outputs(summary, args.out_dir)
    print(
        "wrote event handler text refs -> "
        f"{args.out_dir / 'event_handler_text_refs.json'} "
        f"({summary['matchedHandlerCount']} handlers)"
    )


if __name__ == "__main__":
    main()
