#!/usr/bin/env python3
"""List callers of Korean text routine candidates and nearby text-table refs."""
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


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 = {}
    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", ""),
            }
    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", ""),
            })
            entries.append(entry)
        start_va = int(cluster["startVaHex"], 16)
        end_va = int(cluster["endVaHex"], 16)
        clusters.append({
            "startVa": start_va,
            "endVa": end_va,
            "matchStartVa": max(0, start_va - 0x40),
            "matchEndVa": end_va + 0x40,
            "startVaHex": cluster["startVaHex"],
            "endVaHex": cluster["endVaHex"],
            "entryCount": cluster.get("entryCount", 0),
            "classificationCounts": cluster.get("classificationCounts") or {},
            "sampleTexts": cluster.get("sampleTexts") or [],
            "entries": entries,
        })
    return texts, clusters


def scan_calls_to_targets(
    exe: bytes,
    sections: list[dict],
    targets: set[int],
    exe_path: Path | None = None,
) -> tuple[list[dict], str]:
    calls, method = direct_text_calls(exe, sections, exe_path)
    return [call for call in calls if call["targetVa"] in targets], method


def scan_context_refs(
    exe: bytes,
    sections: list[dict],
    center_va: int,
    texts: dict[int, dict],
    clusters: list[dict],
    radius: int = 0x60,
) -> tuple[list[dict], list[dict], str]:
    start_va = max(0, center_va - radius)
    end_va = center_va + radius
    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.hex(" ")


def classify_caller(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 [])
    joined = " ".join(samples)
    if any(word in joined for word in ["파일", "드라이브", "환세취호전 ver"]):
        return "system-ui"
    if any(word in joined for word in ["약초", "해독초", "마법의 물약", "마수석"]):
        return "item-menu"
    if any(word in joined for word in ["정권", "돌려차기", "던지기", "도주", "방어"]):
        return "battle-action"
    if samples:
        return "text-table"
    return "unclassified"


def build_summary(exe: bytes, korean_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")
    }
    calls = []
    matched_calls, call_scan_method = scan_calls_to_targets(exe, sections, routine_targets, exe_path)
    for call in matched_calls:
        text_refs, table_refs, code_bytes = scan_context_refs(exe, sections, call["callVa"], texts, clusters)
        classification = classify_caller(text_refs, table_refs)
        calls.append({
            **call,
            "classification": classification,
            "directTextRefCount": len(text_refs),
            "textTableRefCount": len(table_refs),
            "directTextRefs": text_refs[:12],
            "textTableRefs": table_refs[:12],
            "contextBytes": code_bytes,
        })
    grouped = defaultdict(list)
    for call in calls:
        grouped[call["targetVaHex"]].append(call)
    routines = []
    for target_hex, rows in grouped.items():
        class_counts = Counter(row["classification"] for row in rows)
        routines.append({
            "targetVaHex": target_hex,
            "callerCount": len(rows),
            "classificationCounts": dict(sorted(class_counts.items())),
            "callers": rows,
        })
    routines.sort(key=lambda row: (-row["callerCount"], row["targetVaHex"]))
    class_counts = Counter(call["classification"] for call in calls)
    return {
        "scope": "Callers of Korean text routine candidates discovered near executable text tables.",
        "routineCount": len(routines),
        "callerCount": len(calls),
        "directTextCallScanMethod": call_scan_method,
        "classificationCounts": dict(sorted(class_counts.items())),
        "conclusion": (
            "Known Korean text routine callers are currently tied to system, item, and battle/action text-table contexts. "
            "No story-dialogue caller class has been identified from these direct callsites."
        ),
        "routines": routines,
    }


def caller_samples(call: dict) -> str:
    samples = []
    for ref in call.get("directTextRefs") or []:
        samples.append(ref.get("text", ""))
    for ref in call.get("textTableRefs") or []:
        samples.extend(ref.get("sampleTexts") or [])
    return ", ".join(samples[:6]) or "-"


def markdown(summary: dict) -> str:
    lines = [
        "# Text Routine Callers",
        "",
        summary["conclusion"],
        "",
        f"- routine targets: {summary['routineCount']}",
        f"- caller count: {summary['callerCount']}",
        f"- direct call scan: {summary.get('directTextCallScanMethod', '-')}",
        f"- classes: {json.dumps(summary['classificationCounts'], ensure_ascii=False)}",
        "",
        "## Routines",
        "",
        "| routine | callers | classes |",
        "| --- | ---: | --- |",
    ]
    for routine in summary.get("routines") or []:
        lines.append(
            f"| `{routine['targetVaHex']}` | {routine['callerCount']} | "
            f"{json.dumps(routine['classificationCounts'], ensure_ascii=False)} |"
        )
    lines.extend([
        "",
        "## Callers",
        "",
        "| call | target | class | direct text | table refs | samples |",
        "| --- | --- | --- | ---: | ---: | --- |",
    ])
    for routine in summary.get("routines") or []:
        for call in routine.get("callers") or []:
            samples = caller_samples(call).replace("|", "\\|")
            lines.append(
                f"| `{call['callVaHex']}` | `{call['targetVaHex']}` | {call['classification']} | "
                f"{call['directTextRefCount']} | {call['textTableRefCount']} | {samples} |"
            )
    return "\n".join(lines) + "\n"


def html_page(summary: dict) -> str:
    routine_rows = "".join(
        "<tr>"
        f"<td><code>{html.escape(routine['targetVaHex'])}</code></td>"
        f"<td>{routine['callerCount']}</td>"
        f"<td>{html.escape(json.dumps(routine['classificationCounts'], ensure_ascii=False))}</td>"
        "</tr>"
        for routine in summary.get("routines") or []
    )
    caller_rows = []
    for routine in summary.get("routines") or []:
        for call in routine.get("callers") or []:
            caller_rows.append(
                "<tr>"
                f"<td><code>{html.escape(call['callVaHex'])}</code></td>"
                f"<td><code>{html.escape(call['targetVaHex'])}</code></td>"
                f"<td>{html.escape(call['classification'])}</td>"
                f"<td>{call['directTextRefCount']}</td>"
                f"<td>{call['textTableRefCount']}</td>"
                f"<td>{html.escape(caller_samples(call))}</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>Text Routine Callers</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>Text Routine Callers</h1>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        f"  <p>Direct call scan: {html.escape(summary.get('directTextCallScanMethod', '-'))}; callers: {summary['callerCount']}.</p>",
        "  <h2>Routines</h2>",
        "  <table><thead><tr><th>routine</th><th>callers</th><th>classes</th></tr></thead>",
        f"  <tbody>{routine_rows}</tbody></table>",
        "  <h2>Callers</h2>",
        "  <table><thead><tr><th>call</th><th>target</th><th>class</th><th>direct text</th><th>table refs</th><th>samples</th></tr></thead>",
        f"  <tbody>{''.join(caller_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 / "text_routine_callers.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("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(args.exe.read_bytes(), load_json(args.korean_text), args.exe)
    write_outputs(summary, args.out_dir)
    print(
        "wrote text routine callers -> "
        f"{args.out_dir / 'text_routine_callers.json'} "
        f"({summary['callerCount']} callers)"
    )


if __name__ == "__main__":
    main()
