#!/usr/bin/env python3
"""Create a diagnostic savedat vector for the selector 2:0 route blocker."""
from __future__ import annotations

import argparse
import html
import json
from pathlib import Path
from urllib.parse import urlencode

from parse_savedata import parse_savedata


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
SYNTHETIC_SAVE_NAME = "synthetic_savedat_selector_2_0.dat"
SYNTHETIC_SAVE_SIZE = 0x04FA
GROUP = 2
SLOT = 0
TILE_X = 34
TILE_Y = 19
SOURCE = "map1_01a"
TARGET = "map2_02d"
FAILED_SYNTHETIC_SAVEDATA_GATE_IDS = [
    "captured-gameplay-save",
    "runtime-selected-pointer-trace",
    "strict-hotspot-or-runtime-trigger",
]
SYNTHETIC_SAVEDATA_MISSING_EVIDENCE = [
    "real captured savedat with selector 2:0 from gameplay",
    "runtime trace showing 0x0059de30 becomes 0x00540714 before opcode 8 uses it",
    "strict map1_01a hotspot or equivalent runtime trigger",
]
SYNTHETIC_SAVEDATA_EVIDENCE_REFS = [
    {
        "path": "out/synthetic_savedat_selector_2_0.dat",
        "fields": [
            "save offsets 0x0002/0x0003",
            "tile offsets 0x0004/0x0006",
            "constructed diagnostic bytes",
        ],
    },
    {
        "path": "out/save_scene_selectors.json",
        "fields": [
            "selector 2:0",
            "selectedPointerHex",
            "fieldMaps",
        ],
    },
    {
        "path": "out/save_selector_real_savedata_evidence_gap.json",
        "fields": [
            "realSelector20SaveFound",
            "routePromotionRealSaveCount",
            "proofFound",
        ],
    },
    {
        "path": "out/runtime_selected_pointer_poll.json",
        "fields": [
            "currentRootHex",
            "observedSelectors",
            "anyReachedRouteSelectorContext",
        ],
    },
]


def write_u16(data: bytearray, offset: int, value: int) -> None:
    data[offset] = value & 0xFF
    data[offset + 1] = (value >> 8) & 0xFF


def build_synthetic_savedata() -> bytes:
    data = bytearray(SYNTHETIC_SAVE_SIZE)
    data[0x0002] = GROUP
    data[0x0003] = SLOT
    write_u16(data, 0x0004, TILE_X)
    write_u16(data, 0x0006, TILE_Y)
    data[0x0010] = 1
    data[0x0011] = 0
    data[0x006C] = 1
    write_u16(data, 0x0078, 1)
    write_u16(data, 0x007A, 75)
    write_u16(data, 0x007C, 75)
    write_u16(data, 0x007E, 75)
    write_u16(data, 0x0080, 20)
    write_u16(data, 0x0082, 20)
    write_u16(data, 0x0084, 20)
    return bytes(data)


def browser_url(group: int, slot: int, x: int, y: int, *, overview: bool = True) -> str:
    query = {
        "saveGroup": str(group),
        "saveSlot": str(slot),
        "saveX": str(x),
        "saveY": str(y),
        "events": "1",
    }
    if overview:
        query["overview"] = "1"
    return f"/web/game.html?{urlencode(query)}"


def explicit_url(map_name: str, x: int, y: int) -> str:
    return f"/web/game.html?{urlencode({'map': map_name, 'startTile': f'{x},{y}', 'events': '1', 'overview': '1'})}"


def build_summary(
    out_dir: Path = OUT,
    exe_path: Path = ROOT / "Hwanse2.exe",
    maps_index: Path = OUT / "maps_runtime.js",
) -> dict:
    out_dir.mkdir(parents=True, exist_ok=True)
    save_path = out_dir / SYNTHETIC_SAVE_NAME
    save_path.write_bytes(build_synthetic_savedata())
    parsed = parse_savedata(save_path, exe_path, maps_index)
    selector = parsed["sceneSelector"]
    position = parsed["scenePositionCandidate"]
    field_maps = selector.get("linkedResources", {}).get("fieldMaps") or []
    contains_route_pair = SOURCE in field_maps and TARGET in field_maps
    conclusion = (
        "This constructed savedat vector proves only that save offsets 0x0002=2 and 0x0003=0 resolve through "
        "the original selector loader to selected pointer 0x00540714 and expose both map1_01a and map2_02d in "
        "the selector resource list. It is not a captured gameplay save or runtime execution trace, so it must "
        "not promote map1_01a -> map2_02d by itself."
    )
    return {
        "kind": "constructed-diagnostic-savedat",
        "file": str(save_path),
        "fileName": save_path.name,
        "size": save_path.stat().st_size,
        "source": SOURCE,
        "target": TARGET,
        "selector": "2:0",
        "selectorBytes": {
            "groupOffsetHex": "0x0002",
            "group": GROUP,
            "slotOffsetHex": "0x0003",
            "slot": SLOT,
        },
        "tile": {
            "xOffsetHex": "0x0004",
            "yOffsetHex": "0x0006",
            "x": position["x"]["value"],
            "y": position["y"]["value"],
        },
        "gateProbeBytes": {
            "activeSlotCountOffsetHex": "0x0010",
            "activeSlotCount": 1,
            "activeOrderOffsetHex": "0x0011",
            "activeOrderBytes": [0],
            "activeFlagOffsetHex": "0x006c",
            "activeFlagByte": 1,
            "saveRuntimeGateValuesHex": {
                "0x00e2": "0x00",
                "0x00e4": "0x00",
            },
        },
        "selectedPointerHex": selector.get("selectedPointerHex"),
        "rowPointerHex": selector.get("rowPointerHex"),
        "selectedPointerGlobalHex": selector.get("runtimeGlobalVaHex"),
        "fieldMaps": field_maps,
        "containsRoutePair": contains_route_pair,
        "webStartCandidate": parsed.get("webStartCandidate"),
        "browserUrl": browser_url(GROUP, SLOT, TILE_X, TILE_Y),
        "sourceMapUrl": explicit_url(SOURCE, TILE_X, TILE_Y),
        "targetMapUrl": explicit_url(TARGET, TILE_X, TILE_Y),
        "notCapturedSave": True,
        "notRuntimeTrace": True,
        "notRoutePromotionProof": True,
        "routePromotionStatus": "blocked",
        "promotionStatus": "blocked",
        "proofFound": False,
        "syntheticSavedataProbeProofFound": False,
        "failedSyntheticSavedataGateIds": FAILED_SYNTHETIC_SAVEDATA_GATE_IDS,
        "missingEvidence": SYNTHETIC_SAVEDATA_MISSING_EVIDENCE,
        "evidenceRefs": SYNTHETIC_SAVEDATA_EVIDENCE_REFS,
        "evidenceRefCount": len(SYNTHETIC_SAVEDATA_EVIDENCE_REFS),
        "parsedSummary": parsed,
        "nextEvidenceNeeded": [
            "capture a real savedat with selector 2:0 from gameplay",
            "or trace 0x0059de30 becoming 0x00540714 before opcode 8 uses it",
            "and still prove a strict map1_01a hotspot or equivalent runtime trigger",
        ],
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Synthetic Savedata Selector Probe",
        "",
        f"- kind: `{summary['kind']}`",
        f"- file: `{summary['fileName']}` ({summary['size']} bytes)",
        f"- route under investigation: {summary['source']} -> {summary['target']}",
        f"- selector bytes: `0x0002={summary['selectorBytes']['group']}`, `0x0003={summary['selectorBytes']['slot']}`",
        f"- tile bytes: `0x0004={summary['tile']['x']}`, `0x0006={summary['tile']['y']}`",
        f"- selected pointer: `{summary['selectedPointerHex']}`",
        f"- field maps include route pair: {summary['containsRoutePair']}",
        f"- not a real route proof: {summary['notRoutePromotionProof']}",
        f"- promotion status: `{summary['promotionStatus']}`",
        f"- proof found: {summary['proofFound']}",
        f"- synthetic savedata probe proof found: {summary['syntheticSavedataProbeProofFound']}",
        f"- failed synthetic savedata gates: `{','.join(summary['failedSyntheticSavedataGateIds'])}`",
        f"- missing evidence count: {len(summary['missingEvidence'])}",
        f"- evidence refs: {summary['evidenceRefCount']}",
        "",
        summary["conclusion"],
        "",
        "## Missing Evidence",
        "",
        *[f"- {item}" for item in summary["missingEvidence"]],
        "",
        "## Evidence Refs",
        "",
        "| path | fields |",
        "| --- | --- |",
        *[
            f"| `{row['path']}` | {', '.join(row.get('fields') or []) or '-'} |"
            for row in summary["evidenceRefs"]
        ],
        "",
        "## Open",
        "",
        f"- selector URL: `{summary['browserUrl']}`",
        f"- source map URL: `{summary['sourceMapUrl']}`",
        f"- target map URL: `{summary['targetMapUrl']}`",
        "",
        "## Field Maps",
        "",
        ", ".join(summary["fieldMaps"]) or "-",
        "",
        "## Next Evidence Needed",
        "",
    ]
    lines.extend(f"- {item}" for item in summary["nextEvidenceNeeded"])
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    fields = "".join(f"<li>{html.escape(name)}</li>" for name in summary["fieldMaps"])
    evidence = "".join(f"<li>{html.escape(item)}</li>" for item in summary["nextEvidenceNeeded"])
    missing_items = "".join(f"<li>{html.escape(item)}</li>" for item in summary["missingEvidence"])
    ref_rows = "".join(
        "<tr>"
        f"<td><code>{html.escape(row.get('path') or '-')}</code></td>"
        f"<td>{html.escape(', '.join(row.get('fields') or []) or '-')}</td>"
        "</tr>"
        for row in summary["evidenceRefs"]
    )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Synthetic Savedata Selector Probe</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;max-width:1100px;margin:24px auto}table{border-collapse:collapse;width:100%}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}a{color:#9bd4ff}</style>",
        "<h1>Synthetic Savedata Selector Probe</h1>",
        f"<p>Route under investigation: {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}; selector <code>{html.escape(summary['selector'])}</code>; selected pointer <code>{html.escape(summary['selectedPointerHex'] or '-')}</code>.</p>",
        (
            f"<p>Constructed file: <code>{html.escape(summary['fileName'])}</code> ({summary['size']} bytes). "
            f"This is not a real route proof: {summary['notRoutePromotionProof']}; "
            f"promotion status <code>{summary['promotionStatus']}</code>; "
            f"proof found: {summary['proofFound']}; "
            f"synthetic savedata probe proof found: {summary['syntheticSavedataProbeProofFound']}; "
            "failed synthetic savedata gates: "
            f"<code>{html.escape(','.join(summary['failedSyntheticSavedataGateIds']))}</code>; "
            f"missing evidence count: {len(summary['missingEvidence'])}; "
            f"evidence refs: {summary['evidenceRefCount']}.</p>"
        ),
        f"<p>{html.escape(summary['conclusion'])}</p>",
        "<h2>Missing Evidence</h2>",
        f"<ul>{missing_items}</ul>",
        "<h2>Evidence Refs</h2>",
        f"<table><thead><tr><th>path</th><th>fields</th></tr></thead><tbody>{ref_rows}</tbody></table>",
        "<h2>Open</h2>",
        f"<p><a href=\"..{html.escape(summary['browserUrl'])}\">selector URL</a> · <a href=\"..{html.escape(summary['sourceMapUrl'])}\">source map</a> · <a href=\"..{html.escape(summary['targetMapUrl'])}\">target map</a></p>",
        "<h2>Field Maps</h2>",
        f"<ul>{fields}</ul>",
        "<h2>Next Evidence Needed</h2>",
        f"<ul>{evidence}</ul>",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    (out_dir / "synthetic_savedata_selector_probe.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "synthetic_savedata_selector_probe.md").write_text(markdown(summary), encoding="utf-8")
    (out_dir / "synthetic_savedata_selector_probe.html").write_text(html_page(summary), encoding="utf-8")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--maps-index", type=Path, default=OUT / "maps_runtime.js")
    args = parser.parse_args()
    summary = build_summary(args.out_dir, args.exe, args.maps_index)
    write_outputs(summary, args.out_dir)
    print(f"wrote synthetic savedata selector probe -> {args.out_dir / 'synthetic_savedata_selector_probe.md'}")


if __name__ == "__main__":
    main()
