#!/usr/bin/env python3
"""Cross-check scene event records against Korean text pointer candidates."""
from __future__ import annotations

import argparse
import html
import json
import struct
from pathlib import Path

from probe_exe_scene_tables import read_sections, va_to_offset


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 text_index(korean_summary: dict) -> tuple[dict[int, dict], list[dict]]:
    texts: dict[int, dict] = {}
    for row in korean_summary.get("candidates") or []:
        if isinstance(row.get("va"), int):
            texts[row["va"]] = {
                "textVaHex": row.get("vaHex") or hex32(row["va"]),
                "text": row.get("text", ""),
                "classification": row.get("classification", ""),
                "source": "candidate",
            }
    clusters = []
    for cluster in korean_summary.get("refClusters") or []:
        entries = []
        for entry in cluster.get("entries") or []:
            text_va = int(entry["textVaHex"], 16)
            texts.setdefault(text_va, {
                "textVaHex": entry["textVaHex"],
                "text": entry.get("text", ""),
                "classification": entry.get("classification", ""),
                "source": "cluster",
            })
            entries.append({
                "textVa": text_va,
                "textVaHex": entry["textVaHex"],
                "text": entry.get("text", ""),
                "classification": entry.get("classification", ""),
            })
        clusters.append({
            "startVa": int(cluster["startVaHex"], 16),
            "endVa": int(cluster["endVaHex"], 16),
            "startVaHex": cluster["startVaHex"],
            "endVaHex": cluster["endVaHex"],
            "entryCount": cluster.get("entryCount", 0),
            "sampleTexts": cluster.get("sampleTexts") or [],
            "entries": entries,
        })
    return texts, clusters


def dwords_in_window(exe: bytes, sections: list[dict], start_va: int, byte_count: int) -> list[dict]:
    offset = va_to_offset(sections, start_va)
    if offset is None:
        return []
    rows = []
    aligned_va = start_va - (start_va % 4)
    aligned_offset = offset - (start_va - aligned_va)
    for rel in range(0, byte_count, 4):
        off = aligned_offset + rel
        if off < 0 or off + 4 > len(exe):
            continue
        va = aligned_va + rel
        value = struct.unpack_from("<I", exe, off)[0]
        rows.append({
            "va": va,
            "vaHex": hex32(va),
            "value": value,
            "valueHex": hex32(value),
        })
    return rows


def event_windows(event: dict) -> list[dict]:
    windows = []
    if isinstance(event.get("recordVa"), int):
        windows.append({"kind": "record", "startVa": event["recordVa"], "bytes": 0x80})
    if isinstance(event.get("eventDispatchVa"), int):
        windows.append({"kind": "dispatch", "startVa": event["eventDispatchVa"], "bytes": 0x80})
    for ref in event.get("eventDispatchRefs") or []:
        if isinstance(ref.get("conditionVa"), int):
            windows.append({"kind": "condition", "startVa": ref["conditionVa"], "bytes": 0x60})
        if isinstance(ref.get("conditionPayloadVa"), int):
            windows.append({"kind": "payload", "startVa": ref["conditionPayloadVa"], "bytes": 0x120})
    unique = {}
    for window in windows:
        unique[(window["kind"], window["startVa"])] = window
    return list(unique.values())


def cluster_hit(value: int, clusters: list[dict]) -> dict | None:
    for cluster in clusters:
        if cluster["startVa"] <= value <= cluster["endVa"]:
            return cluster
    return None


def scan_event(exe: bytes, sections: list[dict], event: dict, texts: dict[int, dict], clusters: list[dict]) -> dict:
    text_refs = []
    table_refs = []
    seen_text = set()
    seen_table = set()
    for window in event_windows(event):
        for dword in dwords_in_window(exe, sections, window["startVa"], window["bytes"]):
            value = dword["value"]
            if value in texts:
                key = (window["kind"], dword["va"], value)
                if key not in seen_text:
                    seen_text.add(key)
                    text_refs.append({
                        "windowKind": window["kind"],
                        "refVaHex": dword["vaHex"],
                        "valueHex": dword["valueHex"],
                        **texts[value],
                    })
            cluster = cluster_hit(value, clusters)
            if cluster is not None:
                key = (window["kind"], dword["va"], cluster["startVa"])
                if key not in seen_table:
                    seen_table.add(key)
                    table_refs.append({
                        "windowKind": window["kind"],
                        "refVaHex": dword["vaHex"],
                        "valueHex": dword["valueHex"],
                        "clusterStartVaHex": cluster["startVaHex"],
                        "clusterEndVaHex": cluster["endVaHex"],
                        "entryCount": cluster["entryCount"],
                        "sampleTexts": cluster["sampleTexts"][:8],
                    })
    return {
        "map": event.get("map"),
        "sceneIdHex": event.get("sceneIdHex"),
        "eventKind": event.get("eventKind"),
        "recordVaHex": event.get("recordVaHex") or hex32(event.get("recordVa", 0)),
        "dispatchRefCount": len(event.get("eventDispatchRefs") or []),
        "windowCount": len(event_windows(event)),
        "directTextRefCount": len(text_refs),
        "textTableRefCount": len(table_refs),
        "directTextRefs": text_refs,
        "textTableRefs": table_refs,
    }


def build_summary(exe: bytes, events: list[dict], korean_summary: dict) -> dict:
    sections = read_sections(exe)
    texts, clusters = text_index(korean_summary)
    rows = [scan_event(exe, sections, event, texts, clusters) for event in events]
    direct = sum(row["directTextRefCount"] for row in rows)
    table = sum(row["textTableRefCount"] for row in rows)
    return {
        "scope": "Scene event condition/payload windows cross-checked against Korean text pointer candidates.",
        "eventCount": len(rows),
        "textCandidateCount": len(texts),
        "textClusterCount": len(clusters),
        "directTextRefCount": direct,
        "textTableRefCount": table,
        "conclusion": (
            "The currently extracted scene event records do not directly carry Korean text pointers."
            if direct == 0 and table == 0
            else "At least one scene event window contains Korean text or text-table pointers and needs opcode tracing."
        ),
        "rows": rows,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Scene Event Text References",
        "",
        summary["conclusion"],
        "",
        f"- scene event records: {summary['eventCount']}",
        f"- indexed Korean text candidates: {summary['textCandidateCount']}",
        f"- indexed text pointer clusters: {summary['textClusterCount']}",
        f"- direct text refs: {summary['directTextRefCount']}",
        f"- text table refs: {summary['textTableRefCount']}",
        "",
        "## Events",
        "",
        "| map | scene | kind | record | windows | direct text | table refs | samples |",
        "| --- | --- | ---: | --- | ---: | ---: | ---: | --- |",
    ]
    for row in summary.get("rows") or []:
        samples = []
        for ref in row.get("directTextRefs") or []:
            samples.append(ref.get("text", ""))
        for ref in row.get("textTableRefs") or []:
            samples.extend(ref.get("sampleTexts") or [])
        sample_text = ", ".join(samples[:6]).replace("|", "\\|") or "-"
        lines.append(
            f"| {row['map']} | {row['sceneIdHex']} | {row['eventKind']} | `{row['recordVaHex']}` | "
            f"{row['windowCount']} | {row['directTextRefCount']} | {row['textTableRefCount']} | {sample_text} |"
        )
    return "\n".join(lines) + "\n"


def html_page(summary: dict) -> str:
    rows = []
    for row in summary.get("rows") or []:
        samples = []
        for ref in row.get("directTextRefs") or []:
            samples.append(ref.get("text", ""))
        for ref in row.get("textTableRefs") or []:
            samples.extend(ref.get("sampleTexts") or [])
        rows.append(
            "<tr>"
            f"<td>{html.escape(str(row['map']))}</td>"
            f"<td>{html.escape(str(row['sceneIdHex']))}</td>"
            f"<td>{row['eventKind']}</td>"
            f"<td><code>{html.escape(row['recordVaHex'])}</code></td>"
            f"<td>{row['windowCount']}</td>"
            f"<td>{row['directTextRefCount']}</td>"
            f"<td>{row['textTableRefCount']}</td>"
            f"<td>{html.escape(', '.join(samples[:8]) or '-')}</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>Scene Event 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>Scene Event Text References</h1>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        f"  <p>events: {summary['eventCount']}; direct text refs: {summary['directTextRefCount']}; table refs: {summary['textTableRefCount']}</p>",
        "  <table><thead><tr><th>map</th><th>scene</th><th>kind</th><th>record</th><th>windows</th><th>direct text</th><th>table refs</th><th>samples</th></tr></thead>",
        f"  <tbody>{''.join(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 / "scene_event_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("--events", type=Path, default=OUT / "scene_events.json")
    parser.add_argument("--korean-text", type=Path, default=OUT / "korean_text_candidates.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.events),
        load_json(args.korean_text),
    )
    write_outputs(summary, args.out_dir)
    print(
        "wrote scene event text refs -> "
        f"{args.out_dir / 'scene_event_text_refs.json'} "
        f"(direct={summary['directTextRefCount']}, tables={summary['textTableRefCount']})"
    )


if __name__ == "__main__":
    main()
