#!/usr/bin/env python3
"""Classify the only exact map1_01a right-exit coordinate hit."""
from __future__ import annotations

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

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

from probe_exe_scene_tables import read_sections, va_to_offset


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
SOURCE = "map1_01a"
TARGET = "map2_02d"
SIDE = "right"
X = 34
Y = 19

FAILED_EXIT_COORDINATE_CONTEXT_GATE_IDS = [
    "strict-source-coordinate-owner",
    "source-field-map-owner",
    "text-or-code-reference-to-hit",
    "target-map-control-path",
]
MISSING_EVIDENCE = [
    "packed coordinate hit owned by a map1_01a field-map or strict event context",
    "owner selector containing field maps instead of only character/resource CNS data",
    "text/code reference tying the coordinate hit to route execution",
    "control path from the coordinate hit to map2_02d",
]

SCRIPT_LIKE_LOW_OPCODES = {
    0x00,
    0x01,
    0x02,
    0x03,
    0x0B,
    0x0C,
    0x0D,
    0x0E,
    0x10,
    0x11,
    0x12,
    0x13,
    0x14,
    0x15,
    0x20,
    0x21,
    0x22,
    0x23,
    0x24,
}


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


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


def selector_key(row: dict) -> str:
    return f"{row.get('group')}:{row.get('slot')}"


def owner_selector(selectors: list[dict], va: int) -> dict | None:
    roots = sorted(
        [(row["selectedPointer"], index, row) for index, row in enumerate(selectors) if isinstance(row.get("selectedPointer"), int)],
        key=lambda item: (item[0], item[1]),
    )
    for index, (root, _, row) in enumerate(roots):
        end = roots[index + 1][0] if index + 1 < len(roots) else root + 0x4000
        if root <= va < end:
            return {**row, "rootEndHex": hex32(end)}
    return None


def dword_context(exe: bytes, sections: list[dict], center_va: int, before: int = 12, after: int = 16) -> list[dict]:
    rows = []
    start = center_va - before * 4
    end = center_va + (after + 1) * 4
    for va in range(start, end, 4):
        offset = va_to_offset(sections, va)
        if offset is None or offset + 4 > len(exe):
            continue
        value = struct.unpack_from("<I", exe, offset)[0]
        lo = value & 0xFFFF
        hi = (value >> 16) & 0xFFFF
        low_opcode = value & 0xFF
        rows.append({
            "vaHex": hex32(va),
            "valueHex": hex32(value),
            "u16": [lo, hi],
            "lowOpcodeHex": hex8(low_opcode),
            "isHit": va == center_va,
            "scriptLikeLowOpcode": low_opcode in SCRIPT_LIKE_LOW_OPCODES,
            "pointerLike": 0x00400000 <= value <= 0x00600000,
        })
    return rows


def find_right_exit_row(coordinate_refs: dict) -> dict:
    for row in coordinate_refs.get("rows") or []:
        if (
            row.get("source") == SOURCE
            and row.get("target") == TARGET
            and row.get("side") == SIDE
            and row.get("x") == X
            and row.get("y") == Y
        ):
            return row
    raise ValueError("map1_01a right-exit coordinate row not found")


def build_summary(exe: bytes, selectors: list[dict], coordinate_refs: dict) -> dict:
    sections = read_sections(exe)
    right_exit = find_right_exit_row(coordinate_refs)
    xy = (right_exit.get("packedScans") or {}).get("xy") or {}
    samples = xy.get("samples") or []
    if len(samples) != 1:
        raise ValueError("expected exactly one xy sample for map1_01a right exit")
    sample = samples[0]
    hit_va = int(sample["vaHex"], 16)
    owner = owner_selector(selectors, hit_va)
    context = dword_context(exe, sections, hit_va)
    hit_row = next(row for row in context if row["isHit"])
    script_like_neighbors = sum(1 for row in context if row["scriptLikeLowOpcode"])
    pointer_neighbors = [row for row in context if row["pointerLike"]]
    classification = (
        "save-selector-script-word"
        if owner and not owner.get("fieldMaps") and hit_row["lowOpcodeHex"] == "0x22" and script_like_neighbors >= 10
        else "unclassified-coordinate-like-data"
    )
    conclusion = (
        "The only exact xy hit for map1_01a right exit 34,19 is at 0x004f9460, but its low byte is opcode-like "
        "0x22 inside selector 24:0/root 0x004f8dd8. That owner has no field maps and only links cara_at1.cns, and "
        "the surrounding words are save-selector-script-like opcodes and local pointers. This hit is therefore not a "
        "strict map1_01a coordinate table and must not promote map1_01a->map2_02d."
    )
    evidence_refs = [
        {
            "path": "Hwanse2.exe",
            "fields": [
                "0x004f9460 packed xy hit",
                "0x004f8dd8 owner selector root",
                "0x00130022 packed coordinate value",
            ],
        },
        {
            "path": "out/map_exit_coordinate_refs.json",
            "fields": [
                "rows",
                "packedScans.xy.samples",
                "promotionPolicy",
            ],
        },
        {
            "path": "out/save_scene_selectors.json",
            "fields": [
                "selectedPointer",
                "fieldMaps",
                "linkedCns",
            ],
        },
    ]
    return {
        "source": SOURCE,
        "target": TARGET,
        "side": SIDE,
        "tile": {"x": X, "y": Y},
        "xyPackedHex": xy.get("valueHex"),
        "xyHitCount": xy.get("total"),
        "xyAlignedHitCount": xy.get("alignedTotal"),
        "hitVaHex": sample.get("vaHex"),
        "hitSection": sample.get("section"),
        "hitHasTextRefs": bool(sample.get("textRefsToWindow")),
        "hitDataRefCount": len(sample.get("dataRefsToWindow") or []),
        "ownerSelector": selector_key(owner) if owner else None,
        "ownerRootHex": owner.get("selectedPointerHex") if owner else None,
        "ownerRootEndHex": owner.get("rootEndHex") if owner else None,
        "ownerFieldMaps": owner.get("fieldMaps") if owner else [],
        "ownerLinkedCns": owner.get("linkedCns") if owner else [],
        "hitLowOpcodeHex": hit_row["lowOpcodeHex"],
        "scriptLikeNeighborCount": script_like_neighbors,
        "pointerLikeNeighborCount": len(pointer_neighbors),
        "classification": classification,
        "proofFound": False,
        "exitCoordinateContextProofFound": False,
        "failedExitCoordinateContextGateIds": FAILED_EXIT_COORDINATE_CONTEXT_GATE_IDS,
        "missingEvidence": MISSING_EVIDENCE,
        "evidenceRefs": evidence_refs,
        "evidenceRefCount": len(evidence_refs),
        "promotable": False,
        "promotionStatus": "blocked",
        "contextRows": context,
        "remainingProofs": [
            "find a strict map1_01a event/hotspot coordinate table",
            "find a code path that ties a map1_01a source tile to map2_02d",
            "keep geometry-only exits in trial mode only",
        ],
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# map1_01a Right Exit Coordinate Context",
        "",
        f"- route: `{summary['source']} -> {summary['target']}`",
        f"- tile: `{summary['tile']['x']},{summary['tile']['y']}` side `{summary['side']}`",
        f"- xy packed: `{summary['xyPackedHex']}` hits {summary['xyHitCount']} aligned {summary['xyAlignedHitCount']}",
        f"- hit: `{summary['hitVaHex']}` section `{summary['hitSection']}` low opcode `{summary['hitLowOpcodeHex']}`",
        f"- owner selector: `{summary['ownerSelector']}` root `{summary['ownerRootHex']}`..`{summary['ownerRootEndHex']}`",
        f"- owner field maps: {', '.join(summary['ownerFieldMaps']) or 'none'}",
        f"- owner linked CNS: {', '.join(summary['ownerLinkedCns']) or 'none'}",
        f"- classification: {summary['classification']}",
        f"- proof found: {summary['proofFound']}",
        f"- failed exit-coordinate context gates: {', '.join(summary['failedExitCoordinateContextGateIds']) or '-'}",
        f"- missing evidence count: {len(summary['missingEvidence'])}",
        f"- evidence refs: {summary['evidenceRefCount']}",
        f"- promotable: {summary['promotable']}",
        f"- promotion status: {summary['promotionStatus']}",
        "",
        summary["conclusion"],
        "",
        "## Context Rows",
        "",
        "| va | value | u16 | low opcode | hit | script-like | pointer-like |",
        "| --- | --- | --- | --- | --- | --- | --- |",
    ]
    for row in summary["contextRows"]:
        lines.append(
            f"| `{row['vaHex']}` | `{row['valueHex']}` | {row['u16'][0]},{row['u16'][1]} | "
            f"`{row['lowOpcodeHex']}` | {row['isHit']} | {row['scriptLikeLowOpcode']} | {row['pointerLike']} |"
        )
    lines.extend(["", "## Remaining Proofs", ""])
    lines.extend(f"- {item}" for item in summary["remainingProofs"])
    lines.extend(["", "## Missing Evidence", ""])
    lines.extend(f"- {item}" for item in summary["missingEvidence"])
    lines.extend(["", "## Evidence Refs", ""])
    for ref in summary["evidenceRefs"]:
        lines.append(f"- {ref['path']}: {', '.join(ref.get('fields') or []) or '-'}")
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row['vaHex'])}</code></td>"
        f"<td><code>{html.escape(row['valueHex'])}</code></td>"
        f"<td>{row['u16'][0]},{row['u16'][1]}</td>"
        f"<td><code>{html.escape(row['lowOpcodeHex'])}</code></td>"
        f"<td>{row['isHit']}</td>"
        f"<td>{row['scriptLikeLowOpcode']}</td>"
        f"<td>{row['pointerLike']}</td>"
        "</tr>"
        for row in summary["contextRows"]
    )
    proofs = "".join(f"<li>{html.escape(item)}</li>" for item in summary["remainingProofs"])
    missing = "".join(f"<li>{html.escape(item)}</li>" for item in summary["missingEvidence"])
    refs = "".join(
        f"<li><code>{html.escape(ref['path'])}</code>: {html.escape(', '.join(ref.get('fields') or []) or '-')}</li>"
        for ref in summary["evidenceRefs"]
    )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>map1_01a Right Exit Coordinate Context</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;max-width:1100px;margin:24px auto}table{border-collapse:collapse}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}</style>",
        "<h1>map1_01a Right Exit Coordinate Context</h1>",
        f"<p>Route <code>{summary['source']} -&gt; {summary['target']}</code>, tile <code>{summary['tile']['x']},{summary['tile']['y']}</code>, xy packed <code>{summary['xyPackedHex']}</code>.</p>",
        f"<p>Hit <code>{summary['hitVaHex']}</code> low opcode <code>{summary['hitLowOpcodeHex']}</code>; owner selector <code>{summary['ownerSelector']}</code> root <code>{summary['ownerRootHex']}</code>..<code>{summary['ownerRootEndHex']}</code>; owner field maps: {html.escape(', '.join(summary['ownerFieldMaps']) or 'none')}.</p>",
        f"<p>classification: {html.escape(summary['classification'])}; proofFound: {summary['proofFound']}; failedExitCoordinateContextGates: {html.escape(', '.join(summary['failedExitCoordinateContextGateIds']) or '-')}; missingEvidenceCount: {len(summary['missingEvidence'])}; evidenceRefs: {summary['evidenceRefCount']}; promotable: {summary['promotable']}; promotion status: {html.escape(summary['promotionStatus'])}</p>",
        f"<p>{html.escape(summary['conclusion'])}</p>",
        "<table><thead><tr><th>va</th><th>value</th><th>u16</th><th>low opcode</th><th>hit</th><th>script-like</th><th>pointer-like</th></tr></thead><tbody>",
        rows,
        "</tbody></table>",
        f"<h2>Remaining Proofs</h2><ul>{proofs}</ul>",
        f"<h2>Missing Evidence</h2><ul>{missing}</ul>",
        f"<h2>Evidence Refs</h2><ul>{refs}</ul>",
    ])


def write_outputs(summary: dict, out_dir: Path) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "map1_01a_exit_coordinate_context.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--selectors", type=Path, default=OUT / "save_scene_selectors.json")
    parser.add_argument("--coordinate-refs", type=Path, default=OUT / "map_exit_coordinate_refs.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        json.loads(args.selectors.read_text(encoding="utf-8")),
        json.loads(args.coordinate_refs.read_text(encoding="utf-8")),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote map1_01a exit coordinate context -> {args.out_dir / 'map1_01a_exit_coordinate_context.json'}")


if __name__ == "__main__":
    main()
