#!/usr/bin/env python3
"""Separate real opcode 0x09 stores from pointer-shaped low-byte collisions."""
from __future__ import annotations

import argparse
import html
import json
import sys
from pathlib import Path
from typing import Any

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

from probe_exe_scene_tables import read_sections
from summarize_script_handler_table import handler_for_opcode, section_name_for_va


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

SOURCE = "map1_01a"
TARGET = "map2_02d"
SOURCE_SELECTOR = "0:0"
PREDECESSOR_SELECTOR = "1:0"
CURRENT_SELECTOR = "2:0"
OPCODE09_HANDLER = 0x0040AE0E
OPCODE09_SUPPORTED_MODES = {0, 1}


def load_json(path: Path, fallback: Any) -> Any:
    if not path.exists():
        return fallback
    return json.loads(path.read_text(encoding="utf-8"))


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


def value_section(sections: list[dict], value_hex: str | None) -> str | None:
    value = parse_hex(value_hex)
    if value is None:
        return None
    return section_name_for_va(sections, value)


def classify_row(sections: list[dict], selector: str, row: dict) -> dict:
    section = value_section(sections, row.get("valueHex"))
    unsupported = row.get("storedPointerSource") == "unsupported-mode"
    mode_supported = row.get("mode") in OPCODE09_SUPPORTED_MODES
    pointer_collision = unsupported and section is not None
    stored_class = row.get("storedPointerClass") or {}
    return {
        "selector": selector,
        "vaHex": row.get("vaHex"),
        "valueHex": row.get("valueHex"),
        "valueSection": section,
        "mode": row.get("mode"),
        "modeHex": row.get("modeHex"),
        "storedPointerSource": row.get("storedPointerSource"),
        "storedPointerHex": row.get("storedPointerHex"),
        "storedPointerKind": stored_class.get("kind"),
        "storedPointerSelector": stored_class.get("selector"),
        "storesCurrentRoot": row.get("storesCurrentRoot") is True,
        "storesCurrentRange": row.get("storesCurrentRange") is True,
        "supportedStore": not unsupported,
        "modeSupportedByHandler": mode_supported,
        "unsupportedModeReturnsWithoutStore": unsupported and not mode_supported,
        "unsupportedMode": unsupported,
        "pointerCollision": pointer_collision,
        "classification": (
            "pointer-shaped low-byte collision"
            if pointer_collision else
            "unsupported mode without mapped pointer value"
            if unsupported else
            "decoded selected-pointer store"
        ),
    }


def build_summary(
    exe: bytes,
    selected_pointer_opcode_paths: dict | None = None,
    unreadable_producers: dict | None = None,
) -> dict:
    selected_pointer_opcode_paths = selected_pointer_opcode_paths or load_json(
        OUT / "save_selector_selected_pointer_opcode_paths.json",
        {},
    )
    unreadable_producers = unreadable_producers or load_json(
        OUT / "save_selector_opcode08_unreadable_producers.json",
        {},
    )
    sections = read_sections(exe)
    handler = handler_for_opcode(exe, sections, 0x09)
    rows = []
    for scan in selected_pointer_opcode_paths.get("contextScans") or []:
        selector = scan.get("selector")
        for row in scan.get("rows") or []:
            if row.get("opcodeHex") == "0x09":
                rows.append(classify_row(sections, selector, row))
    source_or_predecessor = [
        row for row in rows
        if row.get("selector") in {SOURCE_SELECTOR, PREDECESSOR_SELECTOR}
    ]
    current_rows = [row for row in rows if row.get("selector") == CURRENT_SELECTOR]
    unsupported_source_modes = sorted({
        row.get("mode")
        for row in source_or_predecessor
        if row.get("unsupportedMode") and isinstance(row.get("mode"), int)
    })
    unsupported_pointer_collisions = [
        row for row in source_or_predecessor
        if row.get("unsupportedMode") and row.get("pointerCollision")
    ]
    activation_linked_collision_count = 0
    for row in unreadable_producers.get("activations") or []:
        producer = row.get("producer") or {}
        if (
            producer.get("producerOpcodeHex") == "0x09"
            and producer.get("producerValueSection") is not None
            and producer.get("storedPointerSource") == "unsupported-mode"
        ):
            activation_linked_collision_count += 1
    conclusion = (
        "Opcode 0x09 zero-extends the stream mode byte and only implements modes 0 and 1; unsupported modes jump "
        "to the handler epilogue without a selected-pointer store. The unsupported source/predecessor opcode 0x09 "
        "rows are pointer-shaped low-byte collisions: the full dword lands in mapped .text/.data while only its low "
        "byte is 0x09. They do not decode a stored selected pointer and therefore do not provide a static path into "
        "current selector 2:0. Valid source/predecessor opcode 0x09 rows are mode 0 next-stream stores back into "
        "their own selector ranges."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "sourceSelector": SOURCE_SELECTOR,
        "predecessorSelector": PREDECESSOR_SELECTOR,
        "currentSelector": CURRENT_SELECTOR,
        "opcode09HandlerHex": handler.get("handlerVaHex"),
        "opcode09ModeSource": "zero-extended-u8",
        "opcode09SupportedModesHex": [f"0x{mode:02x}" for mode in sorted(OPCODE09_SUPPORTED_MODES)],
        "unsupportedModeReturnsWithoutStore": True,
        "opcode09RowCount": len(rows),
        "sourceOrPredecessorOpcode09RowCount": len(source_or_predecessor),
        "sourceOrPredecessorSupportedOpcode09RowCount": sum(1 for row in source_or_predecessor if row["supportedStore"]),
        "sourceOrPredecessorUnsupportedModeOpcode09RowCount": sum(1 for row in source_or_predecessor if row["unsupportedMode"]),
        "sourceOrPredecessorPointerCollisionRowCount": sum(1 for row in source_or_predecessor if row["pointerCollision"]),
        "sourceOrPredecessorUnsupportedModePointerCollisionCount": len(unsupported_pointer_collisions),
        "sourceOrPredecessorUnsupportedModesHex": [f"0x{mode:02x}" for mode in unsupported_source_modes],
        "sourceOrPredecessorUnsupportedModesAllPointerCollisions": (
            bool(unsupported_source_modes)
            and len(unsupported_pointer_collisions)
            == sum(1 for row in source_or_predecessor if row["unsupportedMode"])
        ),
        "sourceOrPredecessorCurrentRangeStoreCount": sum(1 for row in source_or_predecessor if row["storesCurrentRange"]),
        "currentOpcode09RowCount": len(current_rows),
        "currentUnsupportedModeOpcode09RowCount": sum(1 for row in current_rows if row["unsupportedMode"]),
        "activationLinkedPointerCollisionCount": activation_linked_collision_count,
        "opcode09PointerCollisionPromotesRoute": False,
        "promotionStatus": "blocked",
        "rows": rows,
        "remainingProofs": [
            "capture selected-pointer runtime state at opcode 0x08",
            "capture a real selector 2:0 gameplay save",
            "find a strict map1_01a source coordinate or hotspot",
        ],
        "conclusion": conclusion,
    }


def row_brief(row: dict) -> str:
    if row.get("pointerCollision"):
        return f"collision {row.get('valueSection')}"
    if row.get("supportedStore"):
        return f"store {row.get('storedPointerSelector') or row.get('storedPointerKind')}"
    return row.get("classification") or "-"


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Opcode 0x09 Pointer Collisions",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- opcode 0x09 handler: `{summary['opcode09HandlerHex']}`",
        f"- opcode 0x09 mode source: {summary['opcode09ModeSource']}",
        f"- opcode 0x09 supported modes: {', '.join(summary['opcode09SupportedModesHex'])}",
        f"- unsupported modes return without store: {summary['unsupportedModeReturnsWithoutStore']}",
        f"- opcode 0x09 rows: {summary['opcode09RowCount']}",
        f"- source/predecessor opcode 0x09 rows: {summary['sourceOrPredecessorOpcode09RowCount']}",
        f"- source/predecessor supported opcode 0x09 rows: {summary['sourceOrPredecessorSupportedOpcode09RowCount']}",
        f"- source/predecessor unsupported-mode opcode 0x09 rows: {summary['sourceOrPredecessorUnsupportedModeOpcode09RowCount']}",
        f"- source/predecessor pointer-collision rows: {summary['sourceOrPredecessorPointerCollisionRowCount']}",
        f"- source/predecessor unsupported-mode pointer collisions: {summary['sourceOrPredecessorUnsupportedModePointerCollisionCount']}",
        f"- source/predecessor unsupported modes: {', '.join(summary['sourceOrPredecessorUnsupportedModesHex'])}",
        f"- source/predecessor unsupported modes all pointer collisions: {summary['sourceOrPredecessorUnsupportedModesAllPointerCollisions']}",
        f"- source/predecessor current-range stores: {summary['sourceOrPredecessorCurrentRangeStoreCount']}",
        f"- current unsupported-mode opcode 0x09 rows: {summary['currentUnsupportedModeOpcode09RowCount']}",
        f"- activation-linked pointer collisions: {summary['activationLinkedPointerCollisionCount']}",
        f"- opcode09 pointer collision promotes route: {summary['opcode09PointerCollisionPromotesRoute']}",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Rows",
        "",
        "| selector | va | value | mode | mode supported | value section | stored pointer | classification |",
        "| --- | --- | --- | --- | --- | --- | --- | --- |",
    ]
    for row in summary["rows"]:
        lines.append(
            f"| `{row.get('selector')}` | `{row.get('vaHex')}` | `{row.get('valueHex')}` | "
            f"`{row.get('modeHex')}` | {row.get('modeSupportedByHandler')} | {row.get('valueSection') or '-'} | "
            f"`{row.get('storedPointerHex') or '-'}` | {row_brief(row)} |"
        )
    lines.extend(["", "## Remaining Proofs", ""])
    lines.extend(f"- {item}" for item in summary["remainingProofs"])
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    body_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(str(row.get('selector')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('vaHex')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('valueHex')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('modeHex')))}</code></td>"
        f"<td>{html.escape(str(row.get('modeSupportedByHandler')))}</td>"
        f"<td>{html.escape(str(row.get('valueSection') or '-'))}</td>"
        f"<td><code>{html.escape(str(row.get('storedPointerHex') or '-'))}</code></td>"
        f"<td>{html.escape(row_brief(row))}</td>"
        "</tr>"
        for row in summary["rows"]
    )
    proofs = "".join(f"<li>{html.escape(item)}</li>" for item in summary["remainingProofs"])
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>Save Selector Opcode 0x09 Pointer Collisions</title>",
        "  <style>",
        "    :root { color-scheme: dark; font-family: system-ui, sans-serif; background: #101010; color: #eee; }",
        "    body { margin: 0; padding: 24px; }",
        "    table { width: 100%; border-collapse: collapse; margin: 12px 0 20px; font-size: 13px; }",
        "    th, td { border-bottom: 1px solid #303030; padding: 7px 8px; text-align: left; vertical-align: top; }",
        "    th { background: #181818; color: #ddd; }",
        "    code { color: #d7f0ff; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Opcode 0x09 Pointer Collisions</h1>",
        f"  <p>route {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}; handler <code>{summary['opcode09HandlerHex']}</code>; mode source {html.escape(summary['opcode09ModeSource'])}; supported modes {html.escape(', '.join(summary['opcode09SupportedModesHex']))}; unsupported modes return without store {summary['unsupportedModeReturnsWithoutStore']}.</p>",
        f"  <p>source/predecessor opcode 0x09 rows {summary['sourceOrPredecessorOpcode09RowCount']}; source/predecessor pointer-collision rows {summary['sourceOrPredecessorPointerCollisionRowCount']}; unsupported-mode pointer collisions {summary['sourceOrPredecessorUnsupportedModePointerCollisionCount']}; unsupported modes {html.escape(', '.join(summary['sourceOrPredecessorUnsupportedModesHex']))}; unsupported modes all pointer collisions {summary['sourceOrPredecessorUnsupportedModesAllPointerCollisions']}; source/predecessor current-range stores {summary['sourceOrPredecessorCurrentRangeStoreCount']}; opcode09 pointer collision promotes route {summary['opcode09PointerCollisionPromotesRoute']}.</p>",
        f"  <p>source/predecessor supported opcode 0x09 rows {summary['sourceOrPredecessorSupportedOpcode09RowCount']}; source/predecessor unsupported-mode opcode 0x09 rows {summary['sourceOrPredecessorUnsupportedModeOpcode09RowCount']}; current unsupported-mode opcode 0x09 rows {summary['currentUnsupportedModeOpcode09RowCount']}; activation-linked pointer collisions {summary['activationLinkedPointerCollisionCount']}.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <h2>Rows</h2>",
        "  <table><thead><tr><th>selector</th><th>va</th><th>value</th><th>mode</th><th>mode supported</th><th>value section</th><th>stored pointer</th><th>classification</th></tr></thead><tbody>",
        body_rows,
        "  </tbody></table>",
        f"  <h2>Remaining Proofs</h2><ul>{proofs}</ul>",
        "</body></html>",
    ])


def write_outputs(summary: dict, out_dir: Path, html_out: Path | None = None) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_opcode09_pointer_collisions.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")),
        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")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--selected-pointer", type=Path, default=OUT / "save_selector_selected_pointer_opcode_paths.json")
    parser.add_argument("--unreadable-producers", type=Path, default=OUT / "save_selector_opcode08_unreadable_producers.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--html-out", type=Path, default=None, help="Optional HTML report output path.")
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.selected_pointer, {}),
        load_json(args.unreadable_producers, {}),
    )
    write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote save selector opcode 0x09 pointer collisions -> {args.out_dir / 'save_selector_opcode09_pointer_collisions.json'}")


if __name__ == "__main__":
    main()
