#!/usr/bin/env python3
"""Classify the current frontier reader payloads as sprite/image rect data."""
from __future__ import annotations

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

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

from extract_scene_events import read_point_table
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"
FAILED_FRONTIER_PAYLOAD_GATE_IDS = [
    "in-bounds-source-point-table",
    "payload-text-or-code-reference",
    "strict-hotspot",
]
FRONTIER_PAYLOAD_MISSING_EVIDENCE = [
    "payload decodes as in-bounds map1_01a source point table",
    "code/text reference tying payload to a transition hotspot",
    "strict map1_01a source hotspot in resource payload",
]
FRONTIER_PAYLOAD_REMAINING_PROOFS = [
    "decode payload as an in-bounds map1_01a point or hotspot table",
    "find code/text refs tying payload to transition hotspot semantics",
    "find strict hotspot evidence outside resource payload shape",
]
FRONTIER_PAYLOAD_EVIDENCE_REFS = [
    {
        "path": "Hwanse2.exe",
        "fields": [".text", ".rdata", ".data"],
    },
    {
        "path": "out/cns_payloads.json",
        "fields": ["name", "width", "height", "kind"],
    },
    {
        "path": "out/maps.js",
        "fields": ["map1_01a", "width", "height"],
    },
    {
        "path": "out/save_selector_frontier_reader_branch_context.json",
        "fields": ["siblingGates", "passOutcome", "evidenceRefs", "evidenceRefCount"],
    },
]


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


def load_maps_js(path: Path) -> dict:
    text = path.read_text(encoding="utf-8")
    prefix = "window.HWANSE_MAPS = "
    if not text.startswith(prefix) or not text.rstrip().endswith(";"):
        raise ValueError(f"{path} does not contain the expected maps.js wrapper")
    return json.loads(text[len(prefix) :].rstrip(";\n"))


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


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


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 read_dwords(exe: bytes, sections: list[dict], start_va: int, count: int) -> list[int]:
    values = []
    for index in range(count):
        value = dword_at_va(exe, sections, start_va + index * 4)
        if value is None:
            break
        values.append(value)
    return values


def section_for_offset(sections: list[dict], offset: int) -> dict | None:
    for section in sections:
        start = section["raw"]
        end = section["raw"] + section["raw_size"]
        if start <= offset < end:
            return section
    return None


def offset_to_va(sections: list[dict], offset: int) -> int | None:
    for section in sections:
        start = section["raw"]
        end = section["raw"] + section["raw_size"]
        if start <= offset < end:
            return section["va"] + offset - start
    return None


def scan_refs_to_range(
    exe: bytes,
    sections: list[dict],
    start_va: int,
    end_va: int,
    section_names: set[str],
    sample_limit: int = 12,
) -> dict:
    count = 0
    samples = []
    for section in sections:
        if section["name"] not in section_names:
            continue
        raw_start = section["raw"]
        raw_end = section["raw"] + section["raw_size"]
        raw = exe[raw_start:raw_end]
        for index in range(0, max(0, len(raw) - 3)):
            value = struct.unpack_from("<I", raw, index)[0]
            if not (start_va <= value < end_va):
                continue
            count += 1
            if len(samples) >= sample_limit:
                continue
            ref_va = section["va"] + index
            samples.append({
                "section": section["name"],
                "refVaHex": hex32(ref_va),
                "valueHex": hex32(value),
            })
    return {"count": count, "samples": samples}


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


def rect_from_quad(values: list[int], width: int, height: int) -> dict:
    if len(values) != 4:
        return {"valid": False, "values": values}
    x0, y0, x1, y1 = values
    valid = 0 <= x0 < x1 <= width and 0 <= y0 < y1 <= height
    return {
        "x0": x0,
        "y0": y0,
        "x1": x1,
        "y1": y1,
        "width": x1 - x0,
        "height": y1 - y0,
        "valid": valid,
        "allValuesMultipleOf16": all(value % 16 == 0 for value in values),
    }


def analyze_payload(
    exe: bytes,
    sections: list[dict],
    payload_va: int,
    image_width: int,
    image_height: int,
    source_width: int,
    source_height: int,
    sample_dword_count: int = 32,
) -> dict:
    dwords = read_dwords(exe, sections, payload_va, sample_dword_count)
    rects = []
    contiguous_valid = 0
    first_invalid = None
    for index in range(0, len(dwords) - 3, 4):
        rect = rect_from_quad(dwords[index:index + 4], image_width, image_height)
        rect["index"] = index // 4
        rect["vaHex"] = hex32(payload_va + index * 4)
        rects.append(rect)
        if rect["valid"] and first_invalid is None:
            contiguous_valid += 1
        elif first_invalid is None:
            first_invalid = rect
    raw_points, in_bounds = read_point_table(exe, sections, payload_va, source_width, source_height)
    range_text = scan_refs_to_range(exe, sections, payload_va, payload_va + sample_dword_count * 4, {".text"})
    range_data = scan_refs_to_range(exe, sections, payload_va, payload_va + sample_dword_count * 4, {".data", ".rdata"})
    scalar_count = sum(1 for value in dwords if value <= max(image_width, image_height))
    classification = (
        "sprite-source-rect-table-like"
        if contiguous_valid > 0 and len(in_bounds) == 0 and range_text["count"] == 0
        else "unclassified-payload"
    )
    return {
        "payloadVaHex": hex32(payload_va),
        "sampleDwordCount": len(dwords),
        "scalarWithinImageBoundCount": scalar_count,
        "rectSampleCount": len(rects),
        "contiguousValidImageRectCount": contiguous_valid,
        "firstInvalidRect": first_invalid,
        "firstRects": rects[:8],
        "allContiguousRectsMultipleOf16": all(
            rect.get("allValuesMultipleOf16") for rect in rects[:contiguous_valid]
        ) if contiguous_valid else False,
        "pointScan": {
            "rawPointCount": len(raw_points),
            "inBoundsPointCount": len(in_bounds),
            "firstRawPoints": raw_points[:8],
            "firstInBoundsPoints": in_bounds[:8],
        },
        "rangeTextRefCount": range_text["count"],
        "rangeDataRefCount": range_data["count"],
        "rangeDataRefs": range_data["samples"],
        "classification": classification,
        "promotionEvidence": False,
    }


def build_summary(
    exe: bytes,
    cns_payloads: list[dict],
    map_data: dict,
    frontier_reader_branch_context: dict,
) -> dict:
    sections = read_sections(exe)
    payloads_by_name = cns_index(cns_payloads)
    source_map = map_data[SOURCE]
    source_width = int(source_map["width"])
    source_height = int(source_map["height"])
    rows = []
    for gate in frontier_reader_branch_context.get("siblingGates") or []:
        cns = (gate.get("falseTargetKind") or {}).get("cns")
        image = payloads_by_name.get(cns or "") or {}
        payload_va = int_from_hex(gate.get("trueFallthroughValueHex"))
        if not cns or not image or payload_va is None:
            continue
        payload = analyze_payload(
            exe,
            sections,
            payload_va,
            int(image.get("width") or 0),
            int(image.get("height") or 0),
            source_width,
            source_height,
        )
        rows.append({
            "branchVaHex": gate.get("branchVaHex"),
            "selectionBufferOffsetHex": gate.get("selectionBufferOffsetHex"),
            "resourceCns": cns,
            "resourceKind": image.get("kind"),
            "resourceWidth": image.get("width"),
            "resourceHeight": image.get("height"),
            "payloadVaHex": gate.get("trueFallthroughValueHex"),
            "payload": payload,
            "classification": payload["classification"],
            "promotionEvidence": False,
        })
    rect_like_count = sum(1 for row in rows if row.get("classification") == "sprite-source-rect-table-like")
    in_bounds_points = sum(((row.get("payload") or {}).get("pointScan") or {}).get("inBoundsPointCount", 0) for row in rows)
    text_ref_count = sum((row.get("payload") or {}).get("rangeTextRefCount", 0) for row in rows)
    all_fit = bool(rows) and all((row.get("payload") or {}).get("contiguousValidImageRectCount", 0) > 0 for row in rows)
    conclusion = (
        "The 0x00542b0c reader pass payload and its sibling payloads fit the paired character CNS image "
        "dimensions as source-rectangle tables. They do not decode as in-bounds map1_01a tile point tables "
        "and have no .text refs in the sampled payload windows, so they remain sprite/resource payload evidence "
        "rather than a strict map transition hotspot."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "sourceMapSize": {"width": source_width, "height": source_height},
        "readerVaHex": frontier_reader_branch_context.get("readerVaHex"),
        "readerPassPayloadVaHex": ((frontier_reader_branch_context.get("passOutcome") or {}).get("valueHex")),
        "gateCount": len(rows),
        "resourceImageGateCount": sum(1 for row in rows if row.get("resourceKind") == "image"),
        "rectLikePayloadGateCount": rect_like_count,
        "allPayloadsFitPairedImages": all_fit,
        "sourceInBoundsPointCount": in_bounds_points,
        "payloadTextRefCount": text_ref_count,
        "rows": rows,
        "proofFound": False,
        "frontierPayloadHotspotProofFound": False,
        "failedFrontierPayloadGateIds": FAILED_FRONTIER_PAYLOAD_GATE_IDS,
        "missingEvidence": FRONTIER_PAYLOAD_MISSING_EVIDENCE,
        "remainingProofs": FRONTIER_PAYLOAD_REMAINING_PROOFS,
        "evidenceRefs": FRONTIER_PAYLOAD_EVIDENCE_REFS,
        "evidenceRefCount": len(FRONTIER_PAYLOAD_EVIDENCE_REFS),
        "strictHotspotFound": False,
        "promotionStatus": "blocked",
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Frontier Payload Shape",
        "",
        f"- route: `{summary.get('source')} -> {summary.get('target')}`",
        f"- reader: `{summary.get('readerVaHex')}`",
        f"- reader pass payload: `{summary.get('readerPassPayloadVaHex')}`",
        f"- rect-like payload gates: {summary.get('rectLikePayloadGateCount')}/{summary.get('gateCount')}",
        f"- all payloads fit paired images: {summary.get('allPayloadsFitPairedImages')}",
        f"- source in-bounds point count: {summary.get('sourceInBoundsPointCount')}",
        f"- payload text ref count: {summary.get('payloadTextRefCount')}",
        f"- proof found: {summary.get('proofFound')}",
        f"- frontier payload hotspot proof found: {summary.get('frontierPayloadHotspotProofFound')}",
        f"- failed frontier payload gates: {', '.join(summary.get('failedFrontierPayloadGateIds') or []) or '-'}",
        f"- missing evidence count: {len(summary.get('missingEvidence') or [])}",
        f"- evidence refs: {summary.get('evidenceRefCount')}",
        f"- promotion status: `{summary.get('promotionStatus')}`",
        "",
        summary.get("conclusion") or "",
        "",
        "## Missing Evidence",
        "",
    ]
    lines.extend(f"- {item}" for item in summary.get("missingEvidence") or [])
    lines.extend([
        "",
        "## Payload Gates",
        "",
        "| branch | offset | resource | image | payload | class | rects | points | text refs |",
        "| --- | --- | --- | --- | --- | --- | ---: | --- | ---: |",
    ])
    for row in summary.get("rows") or []:
        payload = row.get("payload") or {}
        points = payload.get("pointScan") or {}
        lines.append(
            f"| `{row.get('branchVaHex')}` | `{row.get('selectionBufferOffsetHex')}` | "
            f"{row.get('resourceCns')} | {row.get('resourceWidth')}x{row.get('resourceHeight')} | "
            f"`{row.get('payloadVaHex')}` | {row.get('classification')} | "
            f"{payload.get('contiguousValidImageRectCount')} | "
            f"raw={points.get('rawPointCount')}, inBounds={points.get('inBoundsPointCount')} | "
            f"{payload.get('rangeTextRefCount')} |"
        )
    lines.extend(["", "## First Rectangles", ""])
    for row in summary.get("rows") or []:
        payload = row.get("payload") or {}
        lines.append(f"### {row.get('resourceCns')} / `{row.get('payloadVaHex')}`")
        lines.append("")
        lines.append("| index | VA | rect | valid |")
        lines.append("| ---: | --- | --- | --- |")
        for rect in payload.get("firstRects") or []:
            lines.append(
                f"| {rect.get('index')} | `{rect.get('vaHex')}` | "
                f"{rect.get('x0')},{rect.get('y0')}..{rect.get('x1')},{rect.get('y1')} | "
                f"{rect.get('valid')} |"
            )
        lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    rows = []
    failed_gates = "".join(
        f"<li>{html.escape(item)}</li>" for item in summary.get("failedFrontierPayloadGateIds") or []
    )
    missing = "".join(f"<li>{html.escape(item)}</li>" for item in summary.get("missingEvidence") or [])
    for row in summary.get("rows") or []:
        payload = row.get("payload") or {}
        points = payload.get("pointScan") or {}
        rows.append(
            "<tr>"
            f"<td><code>{html.escape(str(row.get('branchVaHex')))}</code></td>"
            f"<td><code>{html.escape(str(row.get('selectionBufferOffsetHex')))}</code></td>"
            f"<td>{html.escape(str(row.get('resourceCns')))}</td>"
            f"<td>{row.get('resourceWidth')}x{row.get('resourceHeight')}</td>"
            f"<td><code>{html.escape(str(row.get('payloadVaHex')))}</code></td>"
            f"<td>{html.escape(str(row.get('classification')))}</td>"
            f"<td>{payload.get('contiguousValidImageRectCount')}</td>"
            f"<td>raw={points.get('rawPointCount')}, inBounds={points.get('inBoundsPointCount')}</td>"
            f"<td>{payload.get('rangeTextRefCount')}</td>"
            "</tr>"
        )
    rect_tables = []
    for row in summary.get("rows") or []:
        rect_rows = []
        for rect in (row.get("payload") or {}).get("firstRects") or []:
            rect_rows.append(
                "<tr>"
                f"<td>{rect.get('index')}</td>"
                f"<td><code>{html.escape(str(rect.get('vaHex')))}</code></td>"
                f"<td>{rect.get('x0')},{rect.get('y0')}..{rect.get('x1')},{rect.get('y1')}</td>"
                f"<td>{rect.get('valid')}</td>"
                "</tr>"
            )
        rect_tables.append(
            f"<h3>{html.escape(str(row.get('resourceCns')))} / <code>{html.escape(str(row.get('payloadVaHex')))}</code></h3>"
            "<table><thead><tr><th>index</th><th>VA</th><th>rect</th><th>valid</th></tr></thead>"
            f"<tbody>{''.join(rect_rows)}</tbody></table>"
        )
    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 Frontier Payload Shape</title>",
        "  <style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;margin:24px}table{border-collapse:collapse;width:100%;max-width:1280px;margin-bottom:22px}td,th{border:1px solid #333;padding:6px 8px;text-align:left;vertical-align:top}th{background:#1f1f1f}code{color:#9bd4ff}</style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Frontier Payload Shape</h1>",
        f"  <p>route <code>{html.escape(str(summary.get('source')))} -&gt; {html.escape(str(summary.get('target')))}</code>; "
        f"reader <code>{html.escape(str(summary.get('readerVaHex')))}</code>; "
        f"reader pass payload <code>{html.escape(str(summary.get('readerPassPayloadVaHex')))}</code>; "
        f"rect-like payload gates <code>{summary.get('rectLikePayloadGateCount')}/{summary.get('gateCount')}</code>; "
        f"source in-bounds point count <code>{summary.get('sourceInBoundsPointCount')}</code>; "
        f"proofFound <code>{summary.get('proofFound')}</code>; "
        f"frontierPayloadHotspotProofFound <code>{summary.get('frontierPayloadHotspotProofFound')}</code>; "
        f"missingEvidenceCount <code>{len(summary.get('missingEvidence') or [])}</code>; "
        f"evidence refs <code>{summary.get('evidenceRefCount')}</code>; "
        f"promotion status <code>{html.escape(str(summary.get('promotionStatus')))}</code>.</p>",
        f"  <p>{html.escape(summary.get('conclusion') or '')}</p>",
        f"  <h2>Failed Frontier Payload Gates</h2><ul>{failed_gates}</ul>",
        f"  <h2>Missing Evidence</h2><ul>{missing}</ul>",
        "  <h2>Payload Gates</h2>",
        "  <table><thead><tr><th>branch</th><th>offset</th><th>resource</th><th>image</th><th>payload</th><th>class</th><th>rects</th><th>points</th><th>text refs</th></tr></thead>",
        f"  <tbody>{''.join(rows)}</tbody></table>",
        "  <h2>First Rectangles</h2>",
        *rect_tables,
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_frontier_payload_shape.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        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()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.out_dir / "cns_payloads.json", []),
        load_maps_js(args.out_dir / "maps.js"),
        load_json(args.out_dir / "save_selector_frontier_reader_branch_context.json", {}),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote frontier payload shape -> {args.out_dir / 'save_selector_frontier_payload_shape.json'}")


if __name__ == "__main__":
    main()
