#!/usr/bin/env python3
"""Review Sukyeong/result-family 0x2f element-guard byte evidence.

Family 47 clears actor bytes +0x2f, +0x30, and +0x31.  The result-family
routine itself only writes those bytes, but the damage formula consumes the
same table through actor + 0x1c + hitUnit.family.  Therefore the action payload
family names can ground the element order: 0x13 fire, 0x14 water/ice, and
0x15 wind/thunder.
"""
from __future__ import annotations

import html
import json
import re
import struct
import subprocess
from pathlib import Path
from typing import Any

from probe_exe_scene_tables import read_sections, va_to_offset


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
EXE = ROOT / "Hwanse2.exe"
ACTION_MAPPING = OUT / "battle_action_mapping.json"

RESULT_TABLE_VA = 0x00546970
SUKYEONG_FAMILY = 0x2F
SUKYEONG_ROUTINE_START = 0x00434E29
SUKYEONG_ROUTINE_STOP = 0x00434E49
OFFSETS = [0x2F, 0x30, 0x31]
ELEMENT_FAMILIES = [
    {
        "family": 0x13,
        "actorOffset": 0x2F,
        "element": "화염",
        "recommendedName": "fireGuard",
        "expectedNameTokens": ("화염", "염가", "열화", "열시", "불타", "작렬", "火"),
    },
    {
        "family": 0x14,
        "actorOffset": 0x30,
        "element": "수빙",
        "recommendedName": "waterIceGuard",
        "expectedNameTokens": ("냉기", "빙", "창룡", "냉도", "氷", "水"),
    },
    {
        "family": 0x15,
        "actorOffset": 0x31,
        "element": "풍뢰",
        "recommendedName": "windThunderGuard",
        "expectedNameTokens": ("뢰", "전뢰", "호포", "기포", "광룡", "雷"),
    },
]


def esc(value: Any) -> str:
    return html.escape(str(value if value is not None else ""))


def hx(value: int | None, width: int = 8) -> str:
    return "-" if value is None else f"0x{value:0{width}x}"


def disassemble_text() -> list[str]:
    result = subprocess.run(
        ["objdump", "-Mintel", "-d", str(EXE)],
        check=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
    )
    return result.stdout.splitlines()


def disassemble_range(start_va: int, stop_va: int) -> list[str]:
    result = subprocess.run(
        [
            "objdump",
            "-Mintel",
            "-D",
            "-b",
            "pei-i386",
            f"--start-address=0x{start_va:08x}",
            f"--stop-address=0x{stop_va:08x}",
            str(EXE),
        ],
        check=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
    )
    return [line for line in result.stdout.splitlines() if re.match(r"\s*[0-9a-f]{6,8}:", line)]


def parse_address(line: str) -> int | None:
    match = re.match(r"\s*([0-9a-f]{6,8}):", line)
    return int(match.group(1), 16) if match else None


def collect_offset_refs(lines: list[str]) -> dict[str, Any]:
    exact_refs: dict[int, list[dict[str, Any]]] = {offset: [] for offset in OFFSETS}
    byte_refs: dict[int, list[dict[str, Any]]] = {offset: [] for offset in OFFSETS}
    byte_reads: dict[int, list[dict[str, Any]]] = {offset: [] for offset in OFFSETS}
    byte_writes: dict[int, list[dict[str, Any]]] = {offset: [] for offset in OFFSETS}
    non_byte_refs: dict[int, list[dict[str, Any]]] = {offset: [] for offset in OFFSETS}

    for line in lines:
        address = parse_address(line)
        if address is None:
            continue
        for offset in OFFSETS:
            if f"+0x{offset:x}]" not in line:
                continue
            entry = {
                "address": address,
                "addressHex": hx(address),
                "line": line,
                "isFamily47Routine": SUKYEONG_ROUTINE_START <= address < SUKYEONG_ROUTINE_STOP,
            }
            exact_refs[offset].append(entry)
            if "BYTE PTR" in line:
                byte_refs[offset].append(entry)
                if re.search(r"\bmov\s+BYTE PTR", line) or re.search(r"\b(?:and|or|xor|add|sub)\s+BYTE PTR", line):
                    byte_writes[offset].append(entry)
                else:
                    byte_reads[offset].append(entry)
            else:
                non_byte_refs[offset].append(entry)

    return {
        "exactRefs": {f"+0x{offset:02x}": exact_refs[offset] for offset in OFFSETS},
        "byteRefs": {f"+0x{offset:02x}": byte_refs[offset] for offset in OFFSETS},
        "byteReads": {f"+0x{offset:02x}": byte_reads[offset] for offset in OFFSETS},
        "byteWrites": {f"+0x{offset:02x}": byte_writes[offset] for offset in OFFSETS},
        "nonByteRefs": {f"+0x{offset:02x}": non_byte_refs[offset] for offset in OFFSETS},
        "counts": {
            f"+0x{offset:02x}": {
                "exact": len(exact_refs[offset]),
                "byte": len(byte_refs[offset]),
                "byteRead": len(byte_reads[offset]),
                "byteWrite": len(byte_writes[offset]),
                "nonByte": len(non_byte_refs[offset]),
            }
            for offset in OFFSETS
        },
    }


def read_result_table_entry() -> dict[str, Any]:
    blob = EXE.read_bytes()
    sections = read_sections(blob)
    entry_va = RESULT_TABLE_VA + SUKYEONG_FAMILY * 4
    entry_offset = va_to_offset(sections, entry_va)
    if entry_offset is None:
        return {"entryVa": entry_va, "entryVaHex": hx(entry_va), "routineVa": None, "routineVaHex": "-"}
    routine_va = struct.unpack_from("<I", blob, entry_offset)[0]
    return {
        "entryVa": entry_va,
        "entryVaHex": hx(entry_va),
        "routineVa": routine_va,
        "routineVaHex": hx(routine_va),
    }


def collect_sukyeong_records() -> list[dict[str, Any]]:
    if not ACTION_MAPPING.exists():
        return []
    data = json.loads(ACTION_MAPPING.read_text(encoding="utf-8"))
    rows: list[dict[str, Any]] = []
    for index, row in enumerate((data.get("playerRows") or []) + (data.get("sharedRows") or [])):
        for unit_index, unit_hex in enumerate(row.get("unitsHex") or []):
            parts = str(unit_hex).split()
            if len(parts) != 8:
                continue
            unit = [int(part, 16) for part in parts]
            if unit[5] != SUKYEONG_FAMILY:
                continue
            rows.append(
                {
                    "recordIndex": index,
                    "unitIndex": unit_index,
                    "ownerName": row.get("ownerName") or "",
                    "name": row.get("name") or "",
                    "entryVaHex": row.get("entryVaHex") or row.get("recordVaHex") or "",
                    "unitHex": unit_hex,
                }
            )
    return rows


def collect_element_family_evidence() -> list[dict[str, Any]]:
    if not ACTION_MAPPING.exists():
        return []
    data = json.loads(ACTION_MAPPING.read_text(encoding="utf-8"))
    all_rows = (data.get("playerRows") or []) + (data.get("sharedRows") or [])
    evidence: list[dict[str, Any]] = []
    for spec in ELEMENT_FAMILIES:
        family = int(spec["family"])
        records: list[dict[str, Any]] = []
        names: list[str] = []
        token_hits = 0
        for row in all_rows:
            for unit_index, unit_hex in enumerate(row.get("unitsHex") or []):
                parts = str(unit_hex).split()
                if len(parts) != 8:
                    continue
                unit = [int(part, 16) for part in parts]
                if unit[5] != family:
                    continue
                name = str(row.get("name") or "")
                names.append(name)
                if any(token in name for token in spec["expectedNameTokens"]):
                    token_hits += 1
                records.append(
                    {
                        "ownerName": row.get("ownerName") or "",
                        "name": name,
                        "entryVaHex": row.get("entryVaHex") or "",
                        "unitIndex": unit_index,
                        "unitHex": unit_hex,
                    }
                )
        unique_names = sorted(set(names))
        evidence.append(
            {
                "family": family,
                "familyHex": f"0x{family:02x}",
                "actorOffset": spec["actorOffset"],
                "actorOffsetHex": f"+0x{spec['actorOffset']:02x}",
                "element": spec["element"],
                "recommendedName": spec["recommendedName"],
                "recordCount": len(records),
                "uniqueNameCount": len(unique_names),
                "tokenHitCount": token_hits,
                "sampleNames": ", ".join(unique_names[:18]),
                "sampleRecords": records[:16],
            }
        )
    return evidence


def build_report() -> dict[str, Any]:
    text_lines = disassemble_text()
    offset_refs = collect_offset_refs(text_lines)
    routine_lines = disassemble_range(SUKYEONG_ROUTINE_START, SUKYEONG_ROUTINE_STOP)
    table_entry = read_result_table_entry()
    records = collect_sukyeong_records()
    element_family_evidence = collect_element_family_evidence()
    byte_read_total = sum(row["byteRead"] for row in offset_refs["counts"].values())
    byte_write_total = sum(row["byteWrite"] for row in offset_refs["counts"].values())
    all_byte_refs_are_family = all(
        item["isFamily47Routine"]
        for refs in offset_refs["byteRefs"].values()
        for item in refs
    )
    return {
        "title": "전투 수경 element-guard byte 검토",
        "source": ["Hwanse2.exe", "out/battle_action_mapping.json"],
        "family": SUKYEONG_FAMILY,
        "familyHex": f"0x{SUKYEONG_FAMILY:02x}",
        "resultTableEntry": table_entry,
        "routineRange": {
            "startVa": SUKYEONG_ROUTINE_START,
            "startVaHex": hx(SUKYEONG_ROUTINE_START),
            "stopVa": SUKYEONG_ROUTINE_STOP,
            "stopVaHex": hx(SUKYEONG_ROUTINE_STOP),
        },
        "summary": [
            "result-family 0x2f는 수경 레코드에서만 사용된다.",
            "0x00546970[0x2f]는 0x00434e29 루틴을 가리킨다.",
            "0x00434e29는 target actor +0x2f, +0x30, +0x31을 각각 0으로 clear한다.",
            "damage formula는 target actor +0x1c + hitUnit.family를 가변 index로 읽는다. 따라서 family 0x13/0x14/0x15는 각각 actor +0x2f/+0x30/+0x31에 대응한다.",
            "action payload 기술명 집계상 family 0x13은 화염계, 0x14는 수빙계, 0x15는 풍뢰계로 묶인다.",
            "따라서 수경이 clear하는 세 바이트는 fireGuard(+0x2f), waterIceGuard(+0x30), windThunderGuard(+0x31)로 승격한다.",
            "+0x30은 WORD/DWORD/PTR 참조가 다른 코드에도 있으므로, 이 결론은 damage formula의 byte-table 가변 index 경로와 수경의 BYTE PTR clear 경로에 한정한다.",
        ],
        "conclusion": {
            "grounded": [
                "family 47 is Sukyeong/reset path",
                "actor +0x2f/+0x30/+0x31 are cleared together by family 47",
                "damage formula consumes actor +0x1c + family, so family 0x13/0x14/0x15 maps to actor +0x2f/+0x30/+0x31",
                "action payload family names ground 0x13 fire, 0x14 water/ice, 0x15 wind/thunder",
            ],
            "notGrounded": [
                "browser runner exact visual duration of Sukyeong's guard state",
            ],
            "recommendedNames": ["fireGuard", "waterIceGuard", "windThunderGuard"],
        },
        "sukyeongRecords": records,
        "elementFamilyEvidence": element_family_evidence,
        "offsetRefs": offset_refs,
        "routineLines": routine_lines,
        "checks": {
            "byteReadTotal": byte_read_total,
            "byteWriteTotal": byte_write_total,
            "allByteRefsAreFamily47Routine": all_byte_refs_are_family,
        },
        "openQuestions": [],
    }


def write_md(report: dict[str, Any]) -> None:
    lines = [
        f"# {report['title']}",
        "",
        f"- family: `{report['familyHex']}`",
        f"- table entry: `{report['resultTableEntry']['entryVaHex']}` -> `{report['resultTableEntry']['routineVaHex']}`",
        "",
        "## 결론",
    ]
    lines.extend(f"- {item}" for item in report["summary"])
    lines.extend(["", "## 수경 레코드", "", "| record | 기술 | unit |", "|---|---|---|"])
    for row in report["sukyeongRecords"]:
        label = f"{row['ownerName']} {row['name']}".strip()
        lines.append(f"| #{row['recordIndex']}.{row['unitIndex']} `{row['entryVaHex']}` | {label} | `{row['unitHex']}` |")
    lines.extend(["", "## Element Family Evidence", "", "| family | actor offset | element | name | count | samples |", "|---|---|---|---|---:|---|"])
    for row in report["elementFamilyEvidence"]:
        lines.append(
            f"| `{row['familyHex']}` | `{row['actorOffsetHex']}` | {row['element']} | `{row['recommendedName']}` | {row['recordCount']} | {row['sampleNames']} |"
        )
    lines.extend(["", "## Offset Direct-Reference Counts", "", "| offset | exact | byte | byte read | byte write | non-byte |", "|---|---:|---:|---:|---:|---:|"])
    for offset, counts in report["offsetRefs"]["counts"].items():
        lines.append(
            f"| `{offset}` | {counts['exact']} | {counts['byte']} | {counts['byteRead']} | {counts['byteWrite']} | {counts['nonByte']} |"
        )
    lines.extend(["", "## Family 47 Routine", "", "```asm"])
    lines.extend(report["routineLines"])
    lines.extend(["```", "", "## Byte-Sized References"])
    for offset, refs in report["offsetRefs"]["byteRefs"].items():
        lines.extend(["", f"### `{offset}`", "", "```asm"])
        lines.extend(item["line"] for item in refs)
        lines.append("```")
    lines.extend(["", "## 미확정"])
    if report["openQuestions"]:
        lines.extend(f"- {item}" for item in report["openQuestions"])
    else:
        lines.append("- 없음")
    lines.append("")
    (OUT / "battle_sukyeong_element_guard_review.md").write_text("\n".join(lines), encoding="utf-8")


def write_html(report: dict[str, Any]) -> None:
    summary = "".join(f"<li>{esc(item)}</li>" for item in report["summary"])
    record_rows = "".join(
        f"<tr><td class=\"mono\">#{esc(row['recordIndex'])}.{esc(row['unitIndex'])}<br>{esc(row['entryVaHex'])}</td><td>{esc((row['ownerName'] + ' ' + row['name']).strip())}</td><td class=\"mono\">{esc(row['unitHex'])}</td></tr>"
        for row in report["sukyeongRecords"]
    )
    count_rows = "".join(
        f"<tr><td class=\"mono\">{esc(offset)}</td><td>{esc(counts['exact'])}</td><td>{esc(counts['byte'])}</td><td>{esc(counts['byteRead'])}</td><td>{esc(counts['byteWrite'])}</td><td>{esc(counts['nonByte'])}</td></tr>"
        for offset, counts in report["offsetRefs"]["counts"].items()
    )
    family_rows = "".join(
        f"<tr><td class=\"mono\">{esc(row['familyHex'])}</td><td class=\"mono\">{esc(row['actorOffsetHex'])}</td><td>{esc(row['element'])}</td><td class=\"mono\">{esc(row['recommendedName'])}</td><td>{esc(row['recordCount'])}</td><td>{esc(row['sampleNames'])}</td></tr>"
        for row in report["elementFamilyEvidence"]
    )
    byte_ref_blocks = "".join(
        f"<h3>{esc(offset)}</h3><pre>{esc(chr(10).join(item['line'] for item in refs))}</pre>"
        for offset, refs in report["offsetRefs"]["byteRefs"].items()
    )
    open_items = "".join(f"<li>{esc(item)}</li>" for item in report["openQuestions"]) or "<li>없음</li>"
    html_text = f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>{esc(report['title'])}</title>
  <style>
    body {{ margin:0; background:#f6f7f9; color:#182230; font-family:system-ui,-apple-system,Segoe UI,sans-serif; line-height:1.45; }}
    main {{ max-width:1180px; margin:0 auto; padding:24px; }}
    section, header {{ background:white; border:1px solid #d8dee6; border-radius:8px; margin-bottom:16px; padding:16px; }}
    h1 {{ margin:0 0 8px; font-size:25px; }}
    h2 {{ margin:0 0 12px; font-size:18px; }}
    h3 {{ margin:14px 0 8px; font-size:15px; }}
    table {{ width:100%; border-collapse:collapse; font-size:13px; }}
    th,td {{ border:1px solid #d8dee6; padding:8px; vertical-align:top; }}
    th {{ background:#eef2f7; text-align:left; }}
    pre {{ margin:0; padding:12px; overflow:auto; background:#101827; color:#d1d5db; border-radius:6px; font-size:12px; }}
    a {{ color:#2457a6; text-decoration:none; }}
    a:hover {{ text-decoration:underline; }}
    .mono {{ font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }}
    .muted {{ color:#667085; }}
  </style>
</head>
<body>
<main>
  <header>
    <h1>{esc(report['title'])}</h1>
    <p class="muted">family <span class="mono">{esc(report['familyHex'])}</span>, table <span class="mono">{esc(report['resultTableEntry']['entryVaHex'])}</span> -> <span class="mono">{esc(report['resultTableEntry']['routineVaHex'])}</span></p>
    <p><a href="../web/index.html">홈</a> · <a href="battle_result_family_dispatch_review.html">result-family</a> · <a href="battle_sukyeong_element_guard_review.json">JSON</a> · <a href="battle_sukyeong_element_guard_review.md">MD</a></p>
  </header>
  <section><h2>결론</h2><ul>{summary}</ul></section>
  <section>
    <h2>수경 레코드</h2>
    <table><thead><tr><th>record</th><th>기술</th><th>unit</th></tr></thead><tbody>{record_rows}</tbody></table>
  </section>
  <section>
    <h2>Element Family Evidence</h2>
    <table><thead><tr><th>family</th><th>actor offset</th><th>element</th><th>name</th><th>count</th><th>sample skill names</th></tr></thead><tbody>{family_rows}</tbody></table>
  </section>
  <section>
    <h2>Offset Direct-Reference Counts</h2>
    <table><thead><tr><th>offset</th><th>exact</th><th>byte</th><th>byte read</th><th>byte write</th><th>non-byte</th></tr></thead><tbody>{count_rows}</tbody></table>
  </section>
  <section><h2>Family 47 Routine</h2><pre>{esc(chr(10).join(report['routineLines']))}</pre></section>
  <section><h2>Byte-Sized References</h2>{byte_ref_blocks}</section>
  <section><h2>미확정</h2><ul>{open_items}</ul></section>
</main>
</body>
</html>
"""
    (OUT / "battle_sukyeong_element_guard_review.html").write_text(html_text, encoding="utf-8")


def main() -> None:
    OUT.mkdir(exist_ok=True)
    report = build_report()
    (OUT / "battle_sukyeong_element_guard_review.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    write_md(report)
    write_html(report)
    print("wrote battle_sukyeong_element_guard_review")


if __name__ == "__main__":
    main()
