#!/usr/bin/env python3
"""Classify current->predecessor reverse reuse hits in the selector merge gap."""
from __future__ import annotations

import argparse
import html
import json
import re
import struct
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 find_cns_strings, read_sections, va_to_offset


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"
FIRST_PREDECESSOR_FILL = 0x004844D0
PREDECESSOR_FILL_SITES = {0x004844D0, 0x004844D8}
FAILED_REVERSE_REUSE_GATE_IDS = [
    "predecessor-current-forward-bridge",
    "reverse-reuse-fill-site-hit",
    "selector-2:0-runtime-or-savedata",
    "strict-source-hotspot",
]
MISSING_EVIDENCE = [
    "predecessor/target-side forward bridge into current selector 2:0",
    "reverse reuse target at predecessor fill site or executable fill/order edge",
    "selector 2:0 gameplay savedat or selected-pointer runtime trace",
    "strict map1_01a source coordinate or hotspot",
]
EVIDENCE_REFS = [
    {
        "path": "Hwanse2.exe",
        "fields": [
            "1:0 predecessor selector root range",
            "2:0 current selector root range",
            "current->predecessor dword reuse hits",
            "predecessor fill site addresses",
        ],
    },
    {
        "path": "out/save_selector_merge_bridge_matrix.json",
        "fields": [
            "currentToPredecessorHitCount",
            "currentToPredecessorBeforeFillHitCount",
            "currentToPredecessorFillSiteHitCount",
            "forwardMergeBridgeHitCount",
            "directMergeExecutionBridgeFound",
        ],
    },
    {
        "path": "out/save_selector_merge_execution_gap.json",
        "fields": [
            "proofFound",
            "failedSelectorMergeExecutionGateIds",
            "missingEvidence",
        ],
    },
]


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


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


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


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


def cns_kind(name: str) -> str:
    if re.fullmatch(r"map\d+_\d+[a-z]\.cns", name):
        return "field-map"
    if re.fullmatch(r"map_[a-z][123]\.cns", name):
        return "tileset"
    if name.startswith("cara_"):
        return "character"
    if name.startswith("face_"):
        return "face"
    return "resource"


def decoded_text_sample(exe: bytes, sections: list[dict], va: int, size: int = 64) -> str:
    offset = va_to_offset(sections, va)
    if offset is None:
        return ""
    raw = exe[offset:offset + size].replace(b"\x00", b" ")
    text = raw.decode("cp949", errors="ignore")
    chars = []
    for char in text:
        code = ord(char)
        if char in "\r\n\t":
            chars.append(" ")
        elif 0x20 <= code <= 0x7E or 0xAC00 <= code <= 0xD7A3:
            chars.append(char)
    return " ".join("".join(chars).split())[:80]


def field_record_starts(exe: bytes, sections: list[dict], strings: dict[int, str], start: int, end: int) -> list[dict]:
    rows = []
    for va in range(start, end, 4):
        value = dword_at_va(exe, sections, va)
        name = strings.get(value or -1)
        if not name or cns_kind(name) != "field-map":
            continue
        scene_id = dword_at_va(exe, sections, va + 4)
        rows.append({
            "recordVa": va,
            "recordVaHex": hex32(va),
            "map": name.removesuffix(".cns"),
            "filename": name,
            "sceneIdHex": f"0x{scene_id:04x}" if isinstance(scene_id, int) else None,
        })
    return rows


def previous_cns_refs(
    exe: bytes,
    sections: list[dict],
    strings: dict[int, str],
    scan_start: int,
    target: int,
    limit: int = 4,
) -> list[dict]:
    refs = []
    aligned_end = target - (target % 4)
    for va in range(scan_start, aligned_end + 1, 4):
        value = dword_at_va(exe, sections, va)
        name = strings.get(value or -1)
        if not name:
            continue
        refs.append({
            "refVaHex": hex32(va),
            "name": name,
            "kind": cns_kind(name),
        })
    return refs[-limit:]


def dword_sample(exe: bytes, sections: list[dict], strings: dict[int, str], va: int, count: int = 5) -> list[dict]:
    rows = []
    for index in range(count):
        item_va = va + index * 4
        value = dword_at_va(exe, sections, item_va)
        if value is None:
            break
        row = {
            "vaHex": hex32(item_va),
            "valueHex": hex32(value),
            "lowByteHex": f"0x{value & 0xff:02x}",
        }
        if value in strings:
            row["cns"] = strings[value]
            row["kind"] = cns_kind(strings[value])
        elif va_to_offset(sections, value) is not None:
            row["kind"] = "pointer"
        else:
            row["kind"] = "scalar"
        rows.append(row)
    return rows


def nearest_field_before(field_starts: list[dict], target: int) -> dict | None:
    previous = [row for row in field_starts if row["recordVa"] <= target]
    return previous[-1] if previous else None


def classify_hit(
    exe: bytes,
    sections: list[dict],
    strings: dict[int, str],
    hit: dict,
    predecessor_start: int,
    field_starts: list[dict],
) -> dict:
    target = parse_hex(hit.get("valueHex"))
    source = parse_hex(hit.get("sourceVaHex"))
    if target is None or source is None:
        return {}
    alignment = target % 4
    previous_fields = previous_cns_refs(exe, sections, strings, predecessor_start, target)
    nearest_field = nearest_field_before(field_starts, target)
    if target in PREDECESSOR_FILL_SITES:
        classification = "fill-site-target"
    elif target < FIRST_PREDECESSOR_FILL and alignment:
        classification = "unaligned-pre-fill-payload-reuse"
    elif target < FIRST_PREDECESSOR_FILL:
        classification = "aligned-pre-fill-payload-reuse"
    else:
        classification = "post-fill-or-unknown-reuse"
    return {
        "sourceVaHex": hex32(source),
        "targetVaHex": hex32(target),
        "targetAlignment": alignment,
        "beforePredecessorFill": target < FIRST_PREDECESSOR_FILL,
        "targetsPredecessorFillSite": target in PREDECESSOR_FILL_SITES,
        "classification": classification,
        "nearestFieldRecordBefore": nearest_field,
        "previousCnsRefs": previous_fields,
        "targetDwordSample": dword_sample(exe, sections, strings, target, count=5),
        "textSample": decoded_text_sample(exe, sections, target),
    }


def build_summary(exe: bytes, merge_bridge_matrix: dict) -> dict:
    sections = read_sections(exe)
    strings = find_cns_strings(exe, sections)
    pair = next(
        (
            row for row in merge_bridge_matrix.get("pairs") or []
            if row.get("sourceSelector") == CURRENT_SELECTOR
            and row.get("targetSelector") == PREDECESSOR_SELECTOR
        ),
        None,
    )
    if not pair:
        raise ValueError("missing current->predecessor bridge pair")
    predecessor_start = parse_hex(pair.get("targetRangeHex", "").split("..", 1)[0]) or 0
    predecessor_end = parse_hex(pair.get("targetRangeHex", "").split("..", 1)[1]) or predecessor_start
    fields = field_record_starts(exe, sections, strings, predecessor_start, predecessor_end)
    rows = [
        classify_hit(exe, sections, strings, hit, predecessor_start, fields)
        for hit in pair.get("hits") or []
    ]
    rows = [row for row in rows if row]
    classification_counts: dict[str, int] = {}
    nearest_field_counts: dict[str, int] = {}
    for row in rows:
        classification = row.get("classification") or "unknown"
        classification_counts[classification] = classification_counts.get(classification, 0) + 1
        nearest = row.get("nearestFieldRecordBefore") or {}
        map_name = nearest.get("map") or "none"
        nearest_field_counts[map_name] = nearest_field_counts.get(map_name, 0) + 1
    aligned_count = sum(1 for row in rows if row.get("targetAlignment") == 0)
    unaligned_count = len(rows) - aligned_count
    conclusion = (
        "The current 2:0 -> predecessor 1:0 reverse hits are reuse pointers into the predecessor pre-fill payload "
        "area: every hit lands before the 0x004844d0/0x004844d8 secondaryBranchState fill sites, none targets a fill "
        "site, and many targets are unaligned payload/text positions. They explain shared script/resource material "
        "between the selector roots, but they are not a predecessor-to-current execution bridge and cannot prove "
        "map1_01a->map2_02d progression."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "sourceSelector": SOURCE_SELECTOR,
        "predecessorSelector": PREDECESSOR_SELECTOR,
        "currentSelector": CURRENT_SELECTOR,
        "reverseHitCount": len(rows),
        "forwardMergeBridgeHitCount": merge_bridge_matrix.get("forwardMergeBridgeHitCount"),
        "directMergeExecutionBridgeFound": merge_bridge_matrix.get("directMergeExecutionBridgeFound"),
        "beforePredecessorFillHitCount": sum(1 for row in rows if row.get("beforePredecessorFill")),
        "fillSiteHitCount": sum(1 for row in rows if row.get("targetsPredecessorFillSite")),
        "alignedTargetCount": aligned_count,
        "unalignedTargetCount": unaligned_count,
        "classificationCounts": dict(sorted(classification_counts.items())),
        "nearestFieldRecordCounts": dict(sorted(nearest_field_counts.items())),
        "sampleRows": rows[:20],
        "proofFound": False,
        "reverseReuseProofFound": False,
        "failedReverseReuseGateIds": FAILED_REVERSE_REUSE_GATE_IDS,
        "missingEvidence": MISSING_EVIDENCE,
        "promotionStatus": "blocked",
        "remainingProofs": MISSING_EVIDENCE,
        "evidenceRefs": EVIDENCE_REFS,
        "evidenceRefCount": len(EVIDENCE_REFS),
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Reverse Reuse Context",
        "",
        f"- route: `{summary['source']} -> {summary['target']}`",
        f"- current->predecessor reverse hits: {summary['reverseHitCount']}",
        f"- before predecessor fill: {summary['beforePredecessorFillHitCount']}",
        f"- fill-site hits: {summary['fillSiteHitCount']}",
        f"- aligned targets: {summary['alignedTargetCount']}",
        f"- unaligned targets: {summary['unalignedTargetCount']}",
        f"- forward merge bridge hits: {summary['forwardMergeBridgeHitCount']}",
        f"- direct merge execution bridge found: {summary['directMergeExecutionBridgeFound']}",
        f"- proof found: `{summary['proofFound']}`",
        f"- reverse reuse proof found: `{summary['reverseReuseProofFound']}`",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Classification Counts",
        "",
    ]
    lines.extend(f"- {key}: {value}" for key, value in summary["classificationCounts"].items())
    lines.extend([
        "",
        "## Sample Rows",
        "",
        "| current ref | predecessor target | class | align | nearest field | text sample |",
        "| --- | --- | --- | ---: | --- | --- |",
    ])
    for row in summary["sampleRows"]:
        nearest = row.get("nearestFieldRecordBefore") or {}
        lines.append(
            f"| `{row['sourceVaHex']}` | `{row['targetVaHex']}` | {row['classification']} | "
            f"{row['targetAlignment']} | {nearest.get('map') or '-'} `{nearest.get('recordVaHex') or '-'}` | "
            f"{row.get('textSample') or '-'} |"
        )
    lines.extend(["", "## Failed Reverse-Reuse Gates", ""])
    lines.extend(f"- {item}" for item in summary["failedReverseReuseGateIds"])
    lines.extend(["", "## Missing Evidence", ""])
    lines.extend(f"- {item}" for item in summary["missingEvidence"])
    lines.extend(["", "## Remaining Proofs", ""])
    lines.extend(f"- {item}" for item in summary["remainingProofs"])
    lines.extend(["", "## Evidence Refs", ""])
    for ref in summary["evidenceRefs"]:
        lines.append(f"- `{ref['path']}`: {', '.join(ref.get('fields') or [])}")
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    counts = "".join(
        f"<li>{html.escape(key)}: {value}</li>"
        for key, value in summary["classificationCounts"].items()
    )
    rows = []
    for row in summary["sampleRows"]:
        nearest = row.get("nearestFieldRecordBefore") or {}
        rows.append(
            "<tr>"
            f"<td><code>{html.escape(row['sourceVaHex'])}</code></td>"
            f"<td><code>{html.escape(row['targetVaHex'])}</code></td>"
            f"<td>{html.escape(row['classification'])}</td>"
            f"<td>{row['targetAlignment']}</td>"
            f"<td>{html.escape(nearest.get('map') or '-')} <code>{html.escape(nearest.get('recordVaHex') or '-')}</code></td>"
            f"<td>{html.escape(row.get('textSample') or '-')}</td>"
            "</tr>"
        )
    proofs = "".join(f"<li>{html.escape(item)}</li>" for item in summary["remainingProofs"])
    failed_gates = "".join(f"<li>{html.escape(item)}</li>" for item in summary["failedReverseReuseGateIds"])
    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 []))}</li>"
        for ref in summary["evidenceRefs"]
    )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Save Selector Reverse Reuse Context</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;max-width:1200px;margin:24px auto}table{border-collapse:collapse;width:100%}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}</style>",
        "<h1>Save Selector Reverse Reuse Context</h1>",
        f"<p>Route <code>{summary['source']} -&gt; {summary['target']}</code>; current-&gt;predecessor reverse hits: {summary['reverseHitCount']}; before predecessor fill: {summary['beforePredecessorFillHitCount']}; fill-site hits: {summary['fillSiteHitCount']}.</p>",
        f"<p>Aligned targets: {summary['alignedTargetCount']}; unaligned targets: {summary['unalignedTargetCount']}; forward merge bridge hits: {summary['forwardMergeBridgeHitCount']}; direct merge execution bridge found: {summary['directMergeExecutionBridgeFound']}; proof found: <code>{summary['proofFound']}</code>; reverse reuse proof found: <code>{summary['reverseReuseProofFound']}</code>; promotion status: <code>{html.escape(summary['promotionStatus'])}</code>.</p>",
        f"<p>{html.escape(summary['conclusion'])}</p>",
        f"<h2>Classification Counts</h2><ul>{counts}</ul>",
        "<h2>Sample Rows</h2>",
        "<table><thead><tr><th>current ref</th><th>predecessor target</th><th>class</th><th>align</th><th>nearest field</th><th>text sample</th></tr></thead><tbody>",
        "".join(rows),
        "</tbody></table>",
        f"<h2>Failed Reverse-Reuse Gates</h2><ul>{failed_gates}</ul>",
        f"<h2>Missing Evidence</h2><ul>{missing}</ul>",
        f"<h2>Remaining Proofs</h2><ul>{proofs}</ul>",
        f"<h2>Evidence Refs</h2><ul>{refs}</ul>",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT, html_out: Path | None = None) -> Path:
    out_dir.mkdir(parents=True, exist_ok=True)
    json_out = out_dir / "save_selector_reverse_reuse_context.json"
    json_out.write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        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")
    return json_out


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--merge-bridge-matrix", type=Path, default=OUT / "save_selector_merge_bridge_matrix.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--html-out", type=Path)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.merge_bridge_matrix, {}),
    )
    json_out = write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote save selector reverse reuse context -> {json_out}")


if __name__ == "__main__":
    main()
