#!/usr/bin/env python3
"""Summarize original EXE battle resource descriptor evidence.

This deliberately stops at resource descriptors. The goal is to separate
proven EXE-backed battle/background/sprite load records from still-missing
enemy stat, formation, encounter, reward, and combat formula tables.
"""
from __future__ import annotations

import argparse
import html
import json
import re
import struct
import sys
from collections import Counter
from pathlib import Path
from typing import Any

sys.path.insert(0, str(Path(__file__).resolve().parent))

from probe_exe_scene_tables import find_cns_strings, offset_to_va, read_sections, va_to_offset


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

BATTLE_TILEMAP_RE = re.compile(r"^btl_[a-z]\d\.cns$")
BATTLE_RESOURCE_RE = re.compile(r"^btl_.*\.cns$")
ENEMY_RESOURCE_RE = re.compile(r"^(?:z|boss).*\.cns$")


def load_json(path: Path, fallback: Any) -> Any:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except FileNotFoundError:
        return fallback


def dword_at_va(data: bytes, sections: list[dict], va: int) -> int | None:
    offset = va_to_offset(sections, va)
    if offset is None or offset + 4 > len(data):
        return None
    return struct.unpack_from("<I", data, offset)[0]


def find_value_refs(data: bytes, sections: list[dict], value: int) -> list[dict]:
    needle = struct.pack("<I", value)
    refs = []
    search = 0
    while True:
        hit = data.find(needle, search)
        if hit < 0:
            break
        search = hit + 1
        ref_va = offset_to_va(sections, hit)
        if ref_va is None:
            continue
        refs.append({"fileOffset": hit, "refVa": ref_va})
    return refs


def payload_index(cns_payloads: list[dict]) -> dict[str, dict]:
    return {str(row.get("name")): row for row in cns_payloads if row.get("name")}


def resource_class(name: str, payload: dict | None) -> str:
    kind = payload.get("kind") if payload else ""
    if BATTLE_TILEMAP_RE.fullmatch(name) and kind == "tilemap":
        return "battle-background-tilemap"
    if BATTLE_RESOURCE_RE.fullmatch(name) and kind == "image":
        return "battle-sprite-image"
    if ENEMY_RESOURCE_RE.fullmatch(name) and kind == "image":
        return "enemy-object-sprite-image"
    return ""


def classify_shape(values: dict[str, int | None], cls: str) -> str:
    if cls == "battle-background-tilemap":
        if (
            values.get("minus4") == 0x10
            and values.get("minus8") == 0x000B300C
            and values.get("minus12") == 0x00100010
        ):
            return "tileset-pair-background-load"
    if cls == "enemy-object-sprite-image":
        if (
            values.get("minus8") == 0x01
            and values.get("minus4") in {0x001C9014, 0x001C9011}
            and values.get("plus8") == 0x001C900C
            and values.get("plus12") == 0x00
        ):
            return "sprite-resource-load"
    if cls == "battle-sprite-image":
        if values.get("minus8") == 0x01 and values.get("plus12") == 0x00:
            return "battle-sprite-resource-load"
    return "mixed-resource-script"


def collect_reference_rows(data: bytes, sections: list[dict], strings: dict[int, str], payloads: dict[str, dict]) -> list[dict]:
    rows = []
    for string_va, name in sorted(strings.items(), key=lambda item: (item[1], item[0])):
        payload = payloads.get(name)
        cls = resource_class(name, payload)
        if not cls:
            continue
        for ref in find_value_refs(data, sections, string_va):
            ref_va = ref["refVa"]
            values = {
                "minus16": dword_at_va(data, sections, ref_va - 16),
                "minus12": dword_at_va(data, sections, ref_va - 12),
                "minus8": dword_at_va(data, sections, ref_va - 8),
                "minus4": dword_at_va(data, sections, ref_va - 4),
                "plus4": dword_at_va(data, sections, ref_va + 4),
                "plus8": dword_at_va(data, sections, ref_va + 8),
                "plus12": dword_at_va(data, sections, ref_va + 12),
                "plus16": dword_at_va(data, sections, ref_va + 16),
            }
            rows.append(
                {
                    "name": name,
                    "class": cls,
                    "payloadKind": payload.get("kind") if payload else "",
                    "width": payload.get("width") if payload else None,
                    "height": payload.get("height") if payload else None,
                    "stringVa": string_va,
                    "stringVaHex": f"0x{string_va:08x}",
                    "refVa": ref_va,
                    "refVaHex": f"0x{ref_va:08x}",
                    "fileOffset": ref["fileOffset"],
                    "fileOffsetHex": f"0x{ref['fileOffset']:06x}",
                    "values": values,
                    "shape": classify_shape(values, cls),
                }
            )
    return rows


def class_summary(rows: list[dict], payloads: dict[str, dict], cls: str, payload_predicate) -> dict:
    payload_names = sorted(name for name, row in payloads.items() if payload_predicate(name, row))
    class_rows = [row for row in rows if row["class"] == cls]
    referenced_names = sorted({row["name"] for row in class_rows})
    shape_counts = Counter(row["shape"] for row in class_rows)
    ref_counts_by_name = Counter(row["name"] for row in class_rows)
    return {
        "payloadCount": len(payload_names),
        "referencedPayloadCount": len(referenced_names),
        "referenceCount": len(class_rows),
        "missingExeRefNames": [name for name in payload_names if name not in referenced_names],
        "multiRefNames": {
            name: count
            for name, count in sorted(ref_counts_by_name.items())
            if count > 1
        },
        "shapeCounts": dict(sorted(shape_counts.items())),
        "sampleRows": class_rows[:12],
    }


def build_summary(data: bytes, sections: list[dict], cns_payloads: list[dict]) -> dict:
    strings = find_cns_strings(data, sections)
    payloads = payload_index(cns_payloads)
    rows = collect_reference_rows(data, sections, strings, payloads)
    by_class = {
        "battle-background-tilemap": class_summary(
            rows,
            payloads,
            "battle-background-tilemap",
            lambda name, row: BATTLE_TILEMAP_RE.fullmatch(name) and row.get("kind") == "tilemap",
        ),
        "battle-sprite-image": class_summary(
            rows,
            payloads,
            "battle-sprite-image",
            lambda name, row: BATTLE_RESOURCE_RE.fullmatch(name) and row.get("kind") == "image",
        ),
        "enemy-object-sprite-image": class_summary(
            rows,
            payloads,
            "enemy-object-sprite-image",
            lambda name, row: ENEMY_RESOURCE_RE.fullmatch(name) and row.get("kind") == "image",
        ),
    }
    background_shapes = by_class["battle-background-tilemap"]["shapeCounts"]
    enemy_shapes = by_class["enemy-object-sprite-image"]["shapeCounts"]
    checks = {
        "battleBackgroundDescriptorsFound": by_class["battle-background-tilemap"]["referenceCount"] > 0,
        "battleBackgroundDescriptorsStandard": background_shapes.get("tileset-pair-background-load", 0)
        == by_class["battle-background-tilemap"]["referenceCount"],
        "enemySpriteDescriptorsFound": by_class["enemy-object-sprite-image"]["referenceCount"] > 0,
        "enemySpriteDescriptorsMostlyStandard": enemy_shapes.get("sprite-resource-load", 0)
        == by_class["enemy-object-sprite-image"]["referenceCount"],
        "resourceDescriptorsAreEnemyRows": False,
        "resourceDescriptorsContainStatsOrRewards": False,
        "resourceDescriptorsProveBattleEntry": False,
    }
    return {
        "scope": "EXE-backed battle/background/enemy resource descriptor evidence.",
        "source": ["Hwanse2.exe", "out/cns_payloads.json"],
        "promotionStatus": "resource-descriptors-grounded-data-gap-remains",
        "classSummary": by_class,
        "checks": checks,
        "resourceDescriptorRowCount": len(rows),
        "resourceDescriptorRows": rows,
        "conclusion": (
            "Battle background and enemy/object sprite CNS resources are backed by EXE resource-load descriptor rows. "
            "These descriptors prove resource binding shape, but they are not enemy stat rows, formations, encounter dispatch, "
            "reward tables, combat formulas, or battle-entry event execution."
        ),
    }


def sample_row_text(row: dict) -> str:
    values = row.get("values") or {}
    return " ".join(
        f"{key}=0x{value:08x}" if isinstance(value, int) else f"{key}=-"
        for key, value in [
            ("minus12", values.get("minus12")),
            ("minus8", values.get("minus8")),
            ("minus4", values.get("minus4")),
            ("plus8", values.get("plus8")),
            ("plus12", values.get("plus12")),
        ]
    )


def markdown(summary: dict) -> str:
    lines = [
        "# Original Battle Resource Descriptors",
        "",
        summary["conclusion"],
        "",
        f"- promotion status: `{summary['promotionStatus']}`",
        f"- descriptor rows: {summary['resourceDescriptorRowCount']}",
        "",
        "## Class Summary",
        "",
        "| class | payloads | referenced payloads | refs | shapes | missing EXE refs | multi-ref names |",
        "| --- | ---: | ---: | ---: | --- | --- | --- |",
    ]
    for cls, row in summary["classSummary"].items():
        shapes = ", ".join(f"{key}={value}" for key, value in (row.get("shapeCounts") or {}).items()) or "-"
        missing = ", ".join(row.get("missingExeRefNames") or []) or "-"
        multi = ", ".join(f"{key}:{value}" for key, value in (row.get("multiRefNames") or {}).items()) or "-"
        lines.append(
            f"| {cls} | {row['payloadCount']} | {row['referencedPayloadCount']} | "
            f"{row['referenceCount']} | {shapes} | {missing} | {multi} |"
        )
    lines.extend([
        "",
        "## Checks",
        "",
        "| check | value |",
        "| --- | --- |",
    ])
    for key, value in summary["checks"].items():
        lines.append(f"| {key} | {value} |")
    lines.extend([
        "",
        "## Samples",
        "",
        "| class | name | ref | shape | values |",
        "| --- | --- | --- | --- | --- |",
    ])
    for cls, class_row in summary["classSummary"].items():
        for row in (class_row.get("sampleRows") or [])[:4]:
            lines.append(
                f"| {cls} | `{row['name']}` | `{row['refVaHex']}` | "
                f"{row['shape']} | {sample_row_text(row)} |"
            )
    return "\n".join(lines) + "\n"


def html_page(summary: dict) -> str:
    class_rows = []
    for cls, row in summary["classSummary"].items():
        shapes = ", ".join(f"{key}={value}" for key, value in (row.get("shapeCounts") or {}).items()) or "-"
        missing = ", ".join(row.get("missingExeRefNames") or []) or "-"
        multi = ", ".join(f"{key}:{value}" for key, value in (row.get("multiRefNames") or {}).items()) or "-"
        class_rows.append(
            "<tr>"
            f"<td>{html.escape(cls)}</td>"
            f"<td>{row['payloadCount']}</td>"
            f"<td>{row['referencedPayloadCount']}</td>"
            f"<td>{row['referenceCount']}</td>"
            f"<td>{html.escape(shapes)}</td>"
            f"<td>{html.escape(missing)}</td>"
            f"<td>{html.escape(multi)}</td>"
            "</tr>"
        )
    check_rows = "".join(
        f"<tr><td>{html.escape(key)}</td><td>{html.escape(str(value))}</td></tr>"
        for key, value in summary["checks"].items()
    )
    sample_rows = []
    for cls, class_row in summary["classSummary"].items():
        for row in (class_row.get("sampleRows") or [])[:4]:
            sample_rows.append(
                "<tr>"
                f"<td>{html.escape(cls)}</td>"
                f"<td><code>{html.escape(row['name'])}</code></td>"
                f"<td><code>{html.escape(row['refVaHex'])}</code></td>"
                f"<td>{html.escape(row['shape'])}</td>"
                f"<td>{html.escape(sample_row_text(row))}</td>"
                "</tr>"
            )
    return f"""<!doctype html>
<meta charset="utf-8">
<title>Original Battle Resource Descriptors</title>
<style>
body {{ font-family: system-ui, sans-serif; margin: 24px; background: #101418; color: #e8edf2; }}
table {{ border-collapse: collapse; width: 100%; margin: 16px 0; }}
td, th {{ border: 1px solid #39424c; padding: 6px 8px; vertical-align: top; }}
th {{ background: #202832; }}
code {{ color: #9bd4ff; }}
</style>
<h1>Original Battle Resource Descriptors</h1>
<p>{html.escape(summary['conclusion'])}</p>
<p>Promotion status: <code>{html.escape(summary['promotionStatus'])}</code></p>
<h2>Class Summary</h2>
<table><thead><tr><th>class</th><th>payloads</th><th>referenced</th><th>refs</th><th>shapes</th><th>missing</th><th>multi-ref</th></tr></thead><tbody>{''.join(class_rows)}</tbody></table>
<h2>Checks</h2>
<table><tbody>{check_rows}</tbody></table>
<h2>Samples</h2>
<table><thead><tr><th>class</th><th>name</th><th>ref</th><th>shape</th><th>values</th></tr></thead><tbody>{''.join(sample_rows)}</tbody></table>
"""


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "original_battle_resource_descriptors.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "original_battle_resource_descriptors.md").write_text(markdown(summary), encoding="utf-8")
    (out_dir / "original_battle_resource_descriptors.html").write_text(html_page(summary), 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)
    args = parser.parse_args()

    data = args.exe.read_bytes()
    sections = read_sections(data)
    cns_payloads = load_json(args.out_dir / "cns_payloads.json", [])
    summary = build_summary(data, sections, cns_payloads)
    write_outputs(summary, args.out_dir)


if __name__ == "__main__":
    main()
