#!/usr/bin/env python3
"""Export battle skill/action mapping tables grounded in Hwanse2.exe.

This is intentionally narrower than a full battle engine.  It captures the
tables and code path proven by static EXE table/handler analysis:

* player actor type 1: 0x004d2488[character] -> 8-byte skill records
* shared/enemy actor type 2: *0x004d2494 -> 8-byte action records
* opcode a4: player action phase = skill id + 0x0a
* damage opcode a6 / 0x433f0e: payload + 0x16 + hitIndex*8
"""
from __future__ import annotations

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

from probe_exe_scene_tables import offset_to_va, read_sections, va_to_offset


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

CHAR_TABLE_POINTER_BASE = 0x004D2488
SHARED_TABLE_POINTER = 0x004D2494
PLAYER_PHASE_HANDLER = 0x0040CF0A
PAYLOAD_SELECTOR_HANDLER = 0x00433F0E

CHARACTERS = [
    {"key": "ataho", "name": "아타호", "charIndex": 0, "knownSkillId": 0x19, "knownSkill": "맹호스페셜", "knownPhase": 0x23},
    {"key": "rinshan", "name": "린샹", "charIndex": 1, "knownSkillId": 0x10, "knownSkill": "선렬각", "knownPhase": 0x1A},
    {"key": "smashu", "name": "스마슈", "charIndex": 2, "knownSkillId": 0x0C, "knownSkill": "쾌진격", "knownPhase": 0x16},
]


def hex32(value: int | None) -> str:
    return "" if value is None else f"0x{value:08x}"


def read_bytes(data: bytes, sections: list[dict[str, Any]], va: int, size: int) -> bytes:
    offset = va_to_offset(sections, va)
    if offset is None:
        return b""
    return data[offset:offset + size]


def read_u32(data: bytes, sections: list[dict[str, Any]], va: int) -> int:
    raw = read_bytes(data, sections, va, 4)
    return struct.unpack("<I", raw)[0] if len(raw) == 4 else 0


def decode_name(raw: bytes) -> str:
    return raw.decode("cp949", "ignore").replace("\u3000", " ").replace("　", " ").strip("\x00 ")


def file_offset_hex(sections: list[dict[str, Any]], va: int) -> str:
    offset = va_to_offset(sections, va)
    return "" if offset is None else f"0x{offset:06x}"


def find_refs(data: bytes, sections: list[dict[str, Any]], target: int) -> list[str]:
    needle = struct.pack("<I", target)
    refs: list[str] = []
    start = 0
    while True:
        index = data.find(needle, start)
        if index < 0:
            break
        va = offset_to_va(sections, index)
        if va is not None:
            refs.append(hex32(va))
        start = index + 1
    return refs


def parse_payload(data: bytes, sections: list[dict[str, Any]], payload_va: int) -> dict[str, Any]:
    raw = read_bytes(data, sections, payload_va, 128)
    if len(raw) < 22:
        return {}
    name = decode_name(raw[:16])
    marker = raw[16]
    mp_cost = raw[17]
    count = raw[18]
    prefix = raw[19:22]
    units = []
    for index in range(count):
        unit = raw[22 + index * 8: 22 + (index + 1) * 8]
        if len(unit) != 8:
            break
        units.append(unit)
    return {
        "name": name,
        "marker": marker,
        "markerHex": f"0x{marker:02x}",
        "levelOrFixed": None if marker == 0xFF else marker,
        "mpCost": mp_cost,
        "effectCount": count,
        "prefixBytesHex": prefix.hex(" "),
        "unitsHex": [unit.hex(" ") for unit in units],
        "targetScopes": sorted({unit[4] for unit in units}),
        "families": sorted({unit[5] for unit in units}),
        "statuses": sorted({unit[7] for unit in units}),
    }


def parse_record(
    data: bytes,
    sections: list[dict[str, Any]],
    entry_va: int,
    skill_id: int,
    char: dict[str, Any] | None,
    shared: bool = False,
) -> dict[str, Any] | None:
    payload_va = read_u32(data, sections, entry_va)
    meta = read_u32(data, sections, entry_va + 4)
    if payload_va == 0:
        return None
    if not (0x004D0000 <= payload_va <= 0x00560000):
        return None
    payload = parse_payload(data, sections, payload_va)
    name = payload.get("name") or ""
    if not name:
        return None
    if char is not None:
        phase = skill_id + 0x0A
        phase_source = "opcode a4 formula: actor+0x60 = skillId + 0x0a"
    else:
        phase = None
        phase_source = "enemy/shared: actor+0x5a + 0x0a in opcode a4, not direct skillId formula"
    return {
        "ownerKey": char["key"] if char else "shared",
        "ownerName": char["name"] if char else "공용/적",
        "skillId": skill_id,
        "skillIdHex": f"0x{skill_id:02x}",
        "phase": phase,
        "phaseHex": "" if phase is None else f"0x{phase:02x}",
        "phaseSource": phase_source,
        "entryVa": entry_va,
        "entryVaHex": hex32(entry_va),
        "payloadVa": payload_va,
        "payloadVaHex": hex32(payload_va),
        "fileOffsetHex": file_offset_hex(sections, payload_va),
        "meta": meta,
        "metaHex": hex32(meta),
        "iconSheet": meta >> 16,
        "iconCell": meta & 0xFFFF,
        "shared": shared,
        **payload,
    }


def parse_player_tables(data: bytes, sections: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
    table_pointers = [
        read_u32(data, sections, CHAR_TABLE_POINTER_BASE + index * 4)
        for index in range(3)
    ]
    shared_base = read_u32(data, sections, SHARED_TABLE_POINTER)
    table_ends = [table_pointers[1], table_pointers[2], shared_base]
    table_rows: list[dict[str, Any]] = []
    pointer_rows: list[dict[str, Any]] = []
    for char, base, end in zip(CHARACTERS, table_pointers, table_ends):
        count = max(0, (end - base) // 8)
        pointer_rows.append({
            **char,
            "tablePointerVa": CHAR_TABLE_POINTER_BASE + char["charIndex"] * 4,
            "tablePointerVaHex": hex32(CHAR_TABLE_POINTER_BASE + char["charIndex"] * 4),
            "tableBaseVa": base,
            "tableBaseVaHex": hex32(base),
            "tableEndVa": end,
            "tableEndVaHex": hex32(end),
            "entryCount": count,
        })
        for skill_id in range(count):
            row = parse_record(data, sections, base + skill_id * 8, skill_id, char)
            if row:
                table_rows.append(row)
    return pointer_rows, table_rows


def parse_shared_table(data: bytes, sections: list[dict[str, Any]]) -> tuple[dict[str, Any], list[dict[str, Any]]]:
    base = read_u32(data, sections, SHARED_TABLE_POINTER)
    rows: list[dict[str, Any]] = []
    for action_id in range(0, 140):
        row = parse_record(data, sections, base + action_id * 8, action_id, None, shared=True)
        if row:
            rows.append(row)
    return {
        "tablePointerVa": SHARED_TABLE_POINTER,
        "tablePointerVaHex": hex32(SHARED_TABLE_POINTER),
        "tableBaseVa": base,
        "tableBaseVaHex": hex32(base),
        "parsedRows": len(rows),
    }, rows


def build_summary() -> dict[str, Any]:
    data = EXE.read_bytes()
    sections = read_sections(data)
    player_pointers, player_rows = parse_player_tables(data, sections)
    shared_pointer, shared_rows = parse_shared_table(data, sections)
    known_rows = []
    for char in CHARACTERS:
        row = next(
            item for item in player_rows
            if item["ownerKey"] == char["key"] and item["skillId"] == char["knownSkillId"]
        )
        known_rows.append({
            **row,
            "knownSkillMatches": row["name"] == char["knownSkill"],
            "knownPhaseMatches": row["phase"] == char["knownPhase"],
            "payloadPointerRefs": find_refs(data, sections, row["payloadVa"]),
        })
    return {
        "version": 1,
        "kind": "hwanse-battle-action-mapping",
        "source": "Hwanse2.exe",
        "status": "player-skill-payload-table-grounded-phase-formula-grounded",
        "codeEvidence": {
            "playerTablePointerBaseVaHex": hex32(CHAR_TABLE_POINTER_BASE),
            "sharedTablePointerVaHex": hex32(SHARED_TABLE_POINTER),
            "playerPhaseHandlerVaHex": hex32(PLAYER_PHASE_HANDLER),
            "payloadSelectorHandlerVaHex": hex32(PAYLOAD_SELECTOR_HANDLER),
            "playerPhaseRule": "opcode a4 at 0x0040cf60..0x0040cf6e writes actor+0x60 = actor+0x59(skillId) + 0x0a for player actor type 1",
            "playerPayloadRule": "0x00433f88..0x00433fa2 reads tableBase = dword[0x004d2488 + actor+0x05*4], payload = dword[tableBase + actor+0x59*8]",
            "sharedPayloadRule": "0x00433faa..0x00433fba reads sharedBase = dword[0x004d2494], payload = dword[sharedBase + actor+0x59*8] for actor type 2",
            "hitUnitRule": "0x00433fe0..0x00433ff1 selects payload + 0x16 + word[0x0059e2a4] * 8",
        },
        "playerTablePointers": player_pointers,
        "sharedTablePointer": shared_pointer,
        "knownPlayerRows": known_rows,
        "playerRows": player_rows,
        "sharedRows": shared_rows,
        "notes": [
            "This proves skill id to effect payload for player skills.",
            "For player actor type 1, the first action phase is not a lookup table: it is skill id + 0x0a.",
            "Display script execution is handled by separate static display-VM reports; this mapping report does not depend on cinematic/runtime captures.",
        ],
    }


def markdown(summary: dict[str, Any]) -> str:
    lines = [
        "# Battle Action Mapping",
        "",
        f"- status: `{summary['status']}`",
        f"- player table pointer base: `{summary['codeEvidence']['playerTablePointerBaseVaHex']}`",
        f"- shared table pointer: `{summary['codeEvidence']['sharedTablePointerVaHex']}`",
        f"- phase handler: `{summary['codeEvidence']['playerPhaseHandlerVaHex']}`",
        f"- payload selector: `{summary['codeEvidence']['payloadSelectorHandlerVaHex']}`",
        "",
        "## Rules",
        "",
    ]
    for key in ["playerPhaseRule", "playerPayloadRule", "sharedPayloadRule", "hitUnitRule"]:
        lines.append(f"- {summary['codeEvidence'][key]}")
    lines += [
        "",
        "## Known Player Rows",
        "",
        "| actor | skill id | phase | skill | payload | MP | hits | anchor ok |",
        "| --- | ---: | ---: | --- | --- | ---: | ---: | --- |",
    ]
    for row in summary["knownPlayerRows"]:
        ok = row["knownSkillMatches"] and row["knownPhaseMatches"]
        lines.append(
            f"| {row['ownerName']} | `{row['skillIdHex']}` | `{row['phaseHex']}` | {row['name']} | "
            f"`{row['payloadVaHex']}` | {row['mpCost']} | {row['effectCount']} | `{ok}` |"
        )
    lines += [
        "",
        "## Player Tables",
        "",
        "| actor | ptr slot | table | entries |",
        "| --- | --- | --- | ---: |",
    ]
    for row in summary["playerTablePointers"]:
        lines.append(
            f"| {row['name']} | `{row['tablePointerVaHex']}` | `{row['tableBaseVaHex']}..{row['tableEndVaHex']}` | {row['entryCount']} |"
        )
    lines += [
        "",
        "## Notes",
        "",
    ]
    lines.extend(f"- {note}" for note in summary["notes"])
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict[str, Any]) -> str:
    def esc(value: Any) -> str:
        return html.escape(str(value if value is not None else ""))

    anchor_rows = "".join(
        "<tr>"
        f"<td>{esc(row['ownerName'])}</td><td><code>{esc(row['skillIdHex'])}</code></td>"
        f"<td><code>{esc(row['phaseHex'])}</code></td><td>{esc(row['name'])}</td>"
        f"<td><code>{esc(row['payloadVaHex'])}</code><br><span>{esc(row['fileOffsetHex'])}</span></td>"
        f"<td>{esc(row['mpCost'])}</td><td>{esc(row['effectCount'])}</td>"
        f"<td>{esc(row['prefixBytesHex'])}</td>"
        "</tr>"
        for row in summary["knownPlayerRows"]
    )
    player_rows = "".join(
        "<tr>"
        f"<td>{esc(row['ownerName'])}</td><td><code>{esc(row['skillIdHex'])}</code></td>"
        f"<td><code>{esc(row['phaseHex'])}</code></td><td>{esc(row['name'])}</td>"
        f"<td><code>{esc(row['payloadVaHex'])}</code></td><td>{esc(row['levelOrFixed'])}</td>"
        f"<td>{esc(row['mpCost'])}</td><td>{esc(row['effectCount'])}</td>"
        f"<td><code>{esc(row['metaHex'])}</code></td>"
        "</tr>"
        for row in summary["playerRows"]
    )
    shared_rows = "".join(
        "<tr>"
        f"<td><code>{esc(row['skillIdHex'])}</code></td><td>{esc(row['name'])}</td>"
        f"<td><code>{esc(row['payloadVaHex'])}</code></td><td>{esc(row['mpCost'])}</td>"
        f"<td>{esc(row['effectCount'])}</td><td><code>{esc(row['prefixBytesHex'])}</code></td>"
        "</tr>"
        for row in summary["sharedRows"]
    )
    rule_items = "".join(f"<li>{esc(summary['codeEvidence'][key])}</li>" for key in ["playerPhaseRule", "playerPayloadRule", "sharedPayloadRule", "hitUnitRule"])
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="../favicon.ico">
  <title>Battle Action Mapping</title>
  <style>
    body {{ margin: 20px; background: #101114; color: #f1f3f5; font-family: system-ui, sans-serif; }}
    a {{ color: #9ecbff; }} code {{ color: #ffd37a; }}
    table {{ width: 100%; border-collapse: collapse; margin: 14px 0 26px; }}
    th, td {{ border: 1px solid #30343d; padding: 6px 8px; font-size: 12px; vertical-align: top; }}
    th {{ background: #1a1d24; color: #bac2cf; position: sticky; top: 0; }}
    .panel {{ border: 1px solid #30343d; border-radius: 8px; padding: 12px; background: #151821; margin-bottom: 14px; }}
    details {{ margin: 14px 0; }}
    summary {{ cursor: pointer; color: #ffd37a; }}
  </style>
</head>
<body>
  <h1>Battle Action Mapping</h1>
  <p><a href="../web/index.html">홈</a> · <a href="battle_action_mapping.json">JSON</a> · <a href="battle_action_mapping.md">MD</a></p>
  <div class="panel">
    <p>Status: <code>{esc(summary['status'])}</code></p>
    <ul>{rule_items}</ul>
  </div>
  <h2>Known Player Rows</h2>
  <table><thead><tr><th>actor</th><th>skill id</th><th>phase</th><th>skill</th><th>payload</th><th>MP</th><th>hits</th><th>prefix</th></tr></thead><tbody>{anchor_rows}</tbody></table>
  <details open><summary>Player Skill Payload Tables</summary>
    <table><thead><tr><th>actor</th><th>skill id</th><th>phase</th><th>name</th><th>payload</th><th>level/fixed</th><th>MP</th><th>effects</th><th>meta</th></tr></thead><tbody>{player_rows}</tbody></table>
  </details>
  <details><summary>Shared / Enemy Action Payload Table</summary>
    <table><thead><tr><th>action id</th><th>name</th><th>payload</th><th>cost?</th><th>effects</th><th>prefix</th></tr></thead><tbody>{shared_rows}</tbody></table>
  </details>
</body>
</html>
"""


def main() -> None:
    summary = build_summary()
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "battle_action_mapping.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    (OUT / "battle_action_mapping.md").write_text(markdown(summary), encoding="utf-8")
    (OUT / "battle_action_mapping.html").write_text(html_page(summary), encoding="utf-8")
    print(f"wrote {OUT / 'battle_action_mapping.json'}")
    print(f"wrote {OUT / 'battle_action_mapping.md'}")
    print(f"wrote {OUT / 'battle_action_mapping.html'}")


if __name__ == "__main__":
    main()
