#!/usr/bin/env python3
"""Summarize nearby block context for event/object branch-state candidates."""
from __future__ import annotations

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

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_event_object_branch_state_candidate_links import build_summary as build_link_summary


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
WINDOW_BEFORE = 0x80
WINDOW_AFTER = 0x140
EVENT_OPCODE_LABELS = {
    0x25: "object/stat comparison group",
    0x26: "party/object condition group",
    0x27: "six-slot object condition group",
    0x31: "list selection initialization group",
}
TEXT_RE = re.compile(r"[\u3131-\u318e\uac00-\ud7a3A-Za-z0-9][\u3131-\u318e\uac00-\ud7a3A-Za-z0-9 　!?.:_+-]{1,}")


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


def parse_hex(value: str | None) -> int | None:
    return int(value, 16) if isinstance(value, str) else None


def va_for_offset(sections: list[dict], offset: int) -> int | None:
    for section in sections:
        if section["raw"] <= offset < section["raw"] + section["raw_size"]:
            return section["va"] + offset - section["raw"]
    return None


def section_for_va(sections: list[dict], va: int) -> dict | None:
    for section in sections:
        if section["va"] <= va < section["va"] + section["raw_size"]:
            return section
    return None


def dword_at(exe: bytes, offset: int) -> int | None:
    if offset < 0 or offset + 4 > len(exe):
        return None
    return struct.unpack_from("<I", exe, offset)[0]


def text_snippets(data: bytes, base_va: int) -> list[dict]:
    snippets = []
    current = bytearray()
    current_start = 0
    for index, byte in enumerate(data + b"\0"):
        if byte in {0x00, 0x40}:
            if len(current) >= 4:
                decoded = current.decode("cp949", "ignore").strip("\0 ")
                for match in TEXT_RE.finditer(decoded):
                    text = match.group(0).strip()
                    hangul_count = sum(1 for char in text if "\u3131" <= char <= "\u318e" or "\uac00" <= char <= "\ud7a3")
                    if len(text) >= 2 and hangul_count >= 2:
                        snippets.append({
                            "vaHex": hex32(base_va + current_start),
                            "text": text,
                        })
            current.clear()
            current_start = index + 1
        else:
            if not current:
                current_start = index
            current.append(byte)
    deduped = []
    seen = set()
    for item in snippets:
        key = item["text"]
        if key in seen:
            continue
        seen.add(key)
        deduped.append(item)
    return deduped[:12]


def command_pairs(data: bytes, base_va: int) -> list[dict]:
    rows = []
    for index in range(0, max(0, len(data) - 1)):
        if data[index] != 0x40:
            continue
        opcode = data[index + 1]
        if opcode == 0x00:
            continue
        rows.append({
            "vaHex": hex32(base_va + index),
            "opcodeHex": f"0x{opcode:02x}",
            "label": EVENT_OPCODE_LABELS.get(opcode),
        })
    return rows[:40]


def cns_refs(exe: bytes, sections: list[dict], strings: dict[int, str], start_off: int, end_off: int) -> list[dict]:
    rows = []
    aligned = start_off - (start_off % 4)
    for off in range(aligned, min(end_off, len(exe) - 4) + 1, 4):
        value = dword_at(exe, off)
        if value not in strings:
            continue
        rows.append({
            "refVaHex": hex32(va_for_offset(sections, off) or 0),
            "filename": strings[value],
        })
    return rows


def classify_context(row: dict, snippets: list[dict], cns: list[dict], commands: list[dict]) -> str:
    texts = " ".join(item["text"] for item in snippets)
    filenames = " ".join(item["filename"] for item in cns)
    if any(name.startswith("map") and re.fullmatch(r"map\d+_\d+[a-z]\.cns", name) for name in filenames.split()):
        return "has nearby field-map resource references; route relevance needs deeper trace"
    if any(word in texts for word in ["기본기", "공격기", "도구", "무기", "던전", "레벨"]):
        return "menu/list context, not a direct field transition block"
    if row.get("candidateVaHex", "").startswith("0x00544"):
        return "pointer-heavy selector-like data block, no nearby field-map resource"
    if commands:
        return "event/object command-like byte stream, but no current route link"
    return "raw data context, route relevance unproven"


def build_summary(
    exe: bytes,
    link_summary: dict | None = None,
    window_before: int = WINDOW_BEFORE,
    window_after: int = WINDOW_AFTER,
) -> dict:
    sections = read_sections(exe)
    strings = find_cns_strings(exe, sections)
    link_summary = link_summary or build_link_summary(exe)
    rows = []
    for row in link_summary.get("candidates") or []:
        candidate_va = parse_hex(row.get("candidateVaHex"))
        if candidate_va is None:
            continue
        section = section_for_va(sections, candidate_va)
        if section is None:
            continue
        start_va = max(section["va"], candidate_va - window_before)
        end_va = min(section["va"] + section["raw_size"], candidate_va + window_after)
        start_off = va_to_offset(sections, start_va)
        end_off = va_to_offset(sections, end_va - 1)
        if start_off is None or end_off is None:
            continue
        end_off += 1
        data = exe[start_off:end_off]
        snippets = text_snippets(data, start_va)
        cns = cns_refs(exe, sections, strings, start_off, end_off)
        commands = command_pairs(data, start_va)
        context_class = classify_context(row, snippets, cns, commands)
        rows.append({
            "candidateVaHex": row["candidateVaHex"],
            "indexHex": row["indexHex"],
            "handlerLabel": row["handlerLabel"],
            "parentBlockStartHex": row.get("parentBlockStartHex"),
            "routeRelevance": row.get("routeRelevance"),
            "windowStartHex": hex32(start_va),
            "windowEndHex": hex32(end_va),
            "textSnippets": snippets,
            "nearbyCnsRefs": cns,
            "commandPairs": commands,
            "contextClassification": context_class,
        })
    route_relevant = [row for row in rows if row["nearbyCnsRefs"]]
    menu_like = [row for row in rows if row["contextClassification"].startswith("menu/list")]
    return {
        "scope": "nearby text/resource/command context for medium event/object branch-state candidates",
        "candidateCount": len(rows),
        "nearbyCnsCandidateCount": len(route_relevant),
        "menuLikeCandidateCount": len(menu_like),
        "conclusion": (
            "The medium branch-state candidates are command-like, but the inspected windows do not expose field-map CNS "
            "resource references. Most 0x004e82xx candidates sit in CP949 menu/list text context, and the 0x00544axx/"
            "0x00544exx candidates are pointer-heavy selector-like blocks. They remain useful for VM decoding, but are "
            "not promotion evidence for the current map progression route."
        ),
        "candidates": rows,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Event/Object Branch State Block Context",
        "",
        "Nearby context for medium-confidence event/object branch-state candidates.",
        "",
        f"- candidates: {summary['candidateCount']}",
        f"- with nearby CNS refs: {summary['nearbyCnsCandidateCount']}",
        f"- menu-like candidates: {summary['menuLikeCandidateCount']}",
        "",
        summary["conclusion"],
        "",
        "| candidate | index | parent | context | text snippets | nearby CNS | command pairs |",
        "| --- | --- | --- | --- | --- | --- | --- |",
    ]
    for row in summary["candidates"]:
        texts = ", ".join(item["text"] for item in row["textSnippets"]) or "-"
        cns = ", ".join(item["filename"] for item in row["nearbyCnsRefs"]) or "-"
        commands = ", ".join(
            f"{item['vaHex']}:{item['opcodeHex']}" + (f" {item['label']}" if item.get("label") else "")
            for item in row["commandPairs"][:8]
        ) or "-"
        lines.append(
            f"| `{row['candidateVaHex']}` | `{row['indexHex']}` {row['handlerLabel']} | "
            f"`{row.get('parentBlockStartHex') or '-'}` | {row['contextClassification']} | "
            f"{texts} | {cns} | {commands} |"
        )
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    rows = []
    for row in summary["candidates"]:
        texts = "<br>".join(html.escape(item["text"]) for item in row["textSnippets"]) or "-"
        cns = "<br>".join(html.escape(item["filename"]) for item in row["nearbyCnsRefs"]) or "-"
        commands = "<br>".join(
            f"<code>{html.escape(item['vaHex'])}:{html.escape(item['opcodeHex'])}</code>"
            + (f" {html.escape(item['label'])}" if item.get("label") else "")
            for item in row["commandPairs"][:16]
        ) or "-"
        rows.append(
            "<tr>"
            f"<td><code>{html.escape(row['candidateVaHex'])}</code></td>"
            f"<td><code>{html.escape(row['indexHex'])}</code><br>{html.escape(row['handlerLabel'])}</td>"
            f"<td><code>{html.escape(row.get('parentBlockStartHex') or '-')}</code></td>"
            f"<td>{html.escape(row['contextClassification'])}</td>"
            f"<td>{texts}</td>"
            f"<td>{cns}</td>"
            f"<td>{commands}</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/Object Branch State Block Context</title>",
        "  <style>",
        "    body { margin: 24px; background: #101010; color: #eee; font: 14px system-ui, sans-serif; }",
        "    table { border-collapse: collapse; width: 100%; margin-bottom: 24px; }",
        "    th, td { border: 1px solid #333; padding: 6px 8px; vertical-align: top; }",
        "    th { background: #1d1d1d; }",
        "    code { color: #f5d76e; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Event/Object Branch State Block Context</h1>",
        f"  <p>Candidates: {summary['candidateCount']}; nearby CNS refs: {summary['nearbyCnsCandidateCount']}; "
        f"menu-like: {summary['menuLikeCandidateCount']}.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <table><thead><tr><th>candidate</th><th>index</th><th>parent</th><th>context</th><th>text snippets</th><th>nearby CNS</th><th>command pairs</th></tr></thead>",
        f"  <tbody>{''.join(rows)}</tbody></table>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT, html_out: Path | None = None) -> Path:
    out_dir.mkdir(parents=True, exist_ok=True)
    json_out = out_dir / "event_object_branch_state_block_context.json"
    json_out.write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        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")
    return json_out


def load_json(path: Path) -> dict | None:
    if not path.exists():
        return None
    return json.loads(path.read_text(encoding="utf-8"))


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--html-out", type=Path)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.out_dir / "event_object_branch_state_candidate_links.json"),
    )
    json_out = write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote event/object branch-state block context -> {json_out}")


if __name__ == "__main__":
    main()
