#!/usr/bin/env python3
"""Poll patched selector 2:0 saves at each map1_01a exit candidate."""
from __future__ import annotations

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

import probe_runtime_input_path as runtime_input
from probe_runtime_input_path import DEFAULT_PREFIX, OUT, ROOT
from probe_runtime_predecessor_direction_sweep_branch_state_poll import BRANCH_STATE_WATCH_VALUES
from probe_runtime_selected_pointer_multislot_savedata_poll import build_summary as build_multislot_summary


BASE_SAVE = ROOT / "data" / "public_savedata" / "flack3r" / "savedat2.dat"
OUTPUT_PREFIX = "runtime_patched_selector_exit_candidates_poll"
CURRENT_SELECTOR_GROUP = 2
CURRENT_SELECTOR_SLOT = 0

EXIT_CANDIDATES = [
    {
        "side": "top",
        "tile": {"x": 18, "y": 0},
        "keys": ["Down", "Return", "Up", "Up"],
        "autoTrigger": True,
    },
    {
        "side": "bottom",
        "tile": {"x": 16, "y": 47},
        "keys": ["Down", "Return", "Down", "Down"],
        "autoTrigger": True,
    },
    {
        "side": "left",
        "tile": {"x": 3, "y": 14},
        "keys": ["Down", "Return", "Left", "Left"],
        "autoTrigger": False,
    },
    {
        "side": "right",
        "tile": {"x": 34, "y": 19},
        "keys": ["Down", "Return", "Right", "Right"],
        "autoTrigger": False,
    },
]


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


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


def read_u16(data: bytes, offset: int) -> int:
    return data[offset] | (data[offset + 1] << 8)


def build_candidate_save(candidate: dict[str, Any], out_dir: Path) -> dict[str, Any]:
    data = bytearray(BASE_SAVE.read_bytes())
    tile = candidate["tile"]
    data[0x0002] = CURRENT_SELECTOR_GROUP
    data[0x0003] = CURRENT_SELECTOR_SLOT
    write_u16(data, 0x0004, int(tile["x"]))
    write_u16(data, 0x0006, int(tile["y"]))
    path = out_dir / f"runtime_patched_public_savedat_selector_2_0_{candidate['side']}.dat"
    path.write_bytes(data)
    return {
        "side": candidate["side"],
        "path": str(path.relative_to(ROOT)),
        "size": len(data),
        "selector": f"{data[0x0002]}:{data[0x0003]}",
        "tile": {"x": read_u16(data, 0x0004), "y": read_u16(data, 0x0006)},
        "autoTrigger": candidate.get("autoTrigger"),
        "keys": candidate["keys"],
    }


def selector_counts(row: dict[str, Any]) -> str:
    return ",".join(
        f"{context.get('selector')}x{context.get('count')}"
        for context in row.get("uniqueSelectorContexts") or []
    ) or "-"


def watch_value_counts(summary: dict[str, Any], name: str) -> str:
    rows = (summary.get("observedWatchValues") or {}).get(name) or []
    return ",".join(
        f"{row.get('valueHex')}x{row.get('count')}"
        for row in rows
        if row.get("valueHex") is not None
    ) or "-"


def branch_state_nonzero(summary: dict[str, Any]) -> bool:
    for name, rows in (summary.get("observedWatchValues") or {}).items():
        if not name.startswith("secondaryBranchState"):
            continue
        if any(row.get("valueHex") not in (None, "0x00") for row in rows):
            return True
    return False


def first_non_initial_events(summary: dict[str, Any]) -> list[dict[str, Any]]:
    events = []
    for row in summary.get("rows") or []:
        for event in row.get("events") or []:
            if event.get("phase") == "initial":
                continue
            context = event.get("selectorContext") or {}
            events.append({
                "phase": event.get("phase"),
                "elapsedMs": event.get("elapsedMs"),
                "selectedPointerStaticHex": event.get("selectedPointerStaticHex"),
                "selector": context.get("selector"),
                "watchValues": event.get("watchValues") or {},
                "pressedKeyOffsets": event.get("pressedKeyOffsets") or [],
            })
            if len(events) >= 12:
                return events
    return events


def poll_candidate(
    candidate: dict[str, Any],
    save_row: dict[str, Any],
    args: argparse.Namespace,
) -> dict[str, Any]:
    sequence_name = f"{candidate['side']}-load-exit"
    key_text = ",".join(candidate["keys"])
    poll_args = Namespace(
        startup_wait=args.startup_wait,
        hold=args.hold,
        gap=args.gap,
        interval=args.interval,
        prelude="input-path",
        sequence=[f"{sequence_name}={key_text}"],
        slot_source=[f"1={save_row['path']}"],
        case_aliases=True,
        staged_kind=f"patched public-base selector 2:0 {candidate['side']} exit diagnostic",
        prefix=args.prefix,
        out_dir=args.out_dir,
        output_prefix=f"{OUTPUT_PREFIX}_{candidate['side']}",
    )
    summary = build_multislot_summary(poll_args)
    row = (summary.get("rows") or [{}])[0]
    return {
        "side": candidate["side"],
        "tile": candidate["tile"],
        "autoTrigger": candidate.get("autoTrigger"),
        "save": save_row,
        "sequenceName": sequence_name,
        "keys": candidate["keys"],
        "sampleCount": summary.get("sampleCount"),
        "observedSelectors": summary.get("observedSelectors") or [],
        "selectorCounts": selector_counts(row),
        "anyReachedCurrentRoot": summary.get("anyReachedCurrentRoot"),
        "anyReachedRouteSelectorContext": summary.get("anyReachedRouteSelectorContext"),
        "anyReachedStagedSelector": summary.get("anyReachedPublicSaveSelector"),
        "branchStateNonzero": branch_state_nonzero(summary),
        "opcode24Mode1SourceValues": watch_value_counts(summary, "opcode24Mode1Source"),
        "opcode24RuntimeFlagValues": watch_value_counts(summary, "opcode24RuntimeFlag"),
        "opcode24CurrentObjectIndexValues": watch_value_counts(summary, "opcode24CurrentObjectIndex"),
        "firstEvents": first_non_initial_events(summary),
        "rawSummary": summary,
    }


def build_summary(args: argparse.Namespace) -> dict[str, Any]:
    runtime_input.ROUTE_WATCH_VALUES = dict(runtime_input.ROUTE_WATCH_VALUES)
    runtime_input.ROUTE_WATCH_VALUES.update(BRANCH_STATE_WATCH_VALUES)
    args.out_dir.mkdir(parents=True, exist_ok=True)
    saves = [build_candidate_save(candidate, args.out_dir) for candidate in EXIT_CANDIDATES]
    save_by_side = {row["side"]: row for row in saves}
    rows = [
        poll_candidate(candidate, save_by_side[candidate["side"]], args)
        for candidate in EXIT_CANDIDATES
    ]
    triggered_route_selector = [
        row["side"] for row in rows if row.get("anyReachedRouteSelectorContext")
    ]
    nonzero_branch_state = [
        row["side"] for row in rows if row.get("branchStateNonzero")
    ]
    conclusion = (
        "Patched public-base selector 2:0 exit-candidate diagnostics exercised all four map1_01a geometry exits. "
        "These runs are constructed-save diagnostics, not captured gameplay saves. They only promote the route if a "
        "candidate shows a strict runtime trigger independent of the constructed save; otherwise they calibrate which "
        "candidate coordinates do or do not perturb selected-pointer and branch-state watch values."
    )
    return {
        "objective": "patched public-base selector 2:0 exit-candidate movement poll",
        "source": "map1_01a",
        "target": "map2_02d",
        "stagedKind": "constructed patched public-base diagnostic",
        "baseSave": str(BASE_SAVE.relative_to(ROOT)),
        "startupWaitSeconds": args.startup_wait,
        "holdSeconds": args.hold,
        "gapSeconds": args.gap,
        "pollIntervalSeconds": args.interval,
        "candidateSaveRows": saves,
        "candidateRows": rows,
        "candidateCount": len(rows),
        "candidateReachedRouteSelectorSides": triggered_route_selector,
        "candidateBranchStateNonzeroSides": nonzero_branch_state,
        "anyCandidateReachedRouteSelector": bool(triggered_route_selector),
        "anyCandidateBranchStateNonzero": bool(nonzero_branch_state),
        "promotionStatus": "diagnostic-only",
        "conclusion": conclusion,
    }


def markdown(summary: dict[str, Any]) -> str:
    lines = [
        "# Runtime Patched Selector Exit Candidate Poll",
        "",
        f"- objective: {summary['objective']}",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- staged kind: `{summary['stagedKind']}`",
        f"- base save: `{summary['baseSave']}`",
        f"- candidates: {summary['candidateCount']}",
        f"- any candidate reached selector 2:0: {summary['anyCandidateReachedRouteSelector']}",
        f"- any candidate branch-state nonzero: {summary['anyCandidateBranchStateNonzero']}",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Candidate Saves",
        "",
        "| side | save | selector | tile | auto | keys |",
        "| --- | --- | --- | --- | --- | --- |",
    ]
    for row in summary["candidateSaveRows"]:
        tile = row["tile"]
        lines.append(
            f"| {row['side']} | `{row['path']}` | `{row['selector']}` | "
            f"`{tile['x']},{tile['y']}` | {row.get('autoTrigger')} | `{','.join(row['keys'])}` |"
        )
    lines.extend([
        "",
        "## Poll Results",
        "",
        "| side | tile | samples | selectors | route 2:0 | branch nonzero | mode1 | runtime flag | object index |",
        "| --- | --- | ---: | --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary["candidateRows"]:
        tile = row["tile"]
        lines.append(
            f"| {row['side']} | `{tile['x']},{tile['y']}` | {row.get('sampleCount')} | "
            f"`{row.get('selectorCounts') or '-'}` | {row.get('anyReachedRouteSelectorContext')} | "
            f"{row.get('branchStateNonzero')} | `{row.get('opcode24Mode1SourceValues')}` | "
            f"`{row.get('opcode24RuntimeFlagValues')}` | `{row.get('opcode24CurrentObjectIndexValues')}` |"
        )
    lines.extend(["", "## Event Samples", ""])
    for row in summary["candidateRows"]:
        lines.append(f"### {row['side']}")
        for event in row.get("firstEvents") or []:
            watch = ", ".join(
                f"{name}={value.get('valueHex')}"
                for name, value in (event.get("watchValues") or {}).items()
                if isinstance(value, dict)
            ) or "-"
            lines.append(
                f"- {event.get('phase')} +{event.get('elapsedMs')}ms -> "
                f"`{event.get('selectedPointerStaticHex')}` selector `{event.get('selector') or '-'}` "
                f"watch={watch} pressed={event.get('pressedKeyOffsets')}"
            )
        lines.append("")
    return "\n".join(lines)


def html_page(summary: dict[str, Any]) -> str:
    save_rows = "\n".join(
        "<tr>"
        f"<td>{html.escape(str(row['side']))}</td>"
        f"<td><code>{html.escape(str(row['path']))}</code></td>"
        f"<td><code>{html.escape(str(row['selector']))}</code></td>"
        f"<td><code>{row['tile']['x']},{row['tile']['y']}</code></td>"
        f"<td>{html.escape(str(row.get('autoTrigger')))}</td>"
        f"<td><code>{html.escape(','.join(row['keys']))}</code></td>"
        "</tr>"
        for row in summary["candidateSaveRows"]
    )
    poll_rows = "\n".join(
        "<tr>"
        f"<td>{html.escape(str(row['side']))}</td>"
        f"<td><code>{row['tile']['x']},{row['tile']['y']}</code></td>"
        f"<td>{html.escape(str(row.get('sampleCount')))}</td>"
        f"<td><code>{html.escape(str(row.get('selectorCounts') or '-'))}</code></td>"
        f"<td>{html.escape(str(row.get('anyReachedRouteSelectorContext')))}</td>"
        f"<td>{html.escape(str(row.get('branchStateNonzero')))}</td>"
        f"<td><code>{html.escape(str(row.get('opcode24Mode1SourceValues')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('opcode24RuntimeFlagValues')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('opcode24CurrentObjectIndexValues')))}</code></td>"
        "</tr>"
        for row in summary["candidateRows"]
    )
    return "\n".join([
        "<!doctype html>",
        '<html lang="en"><head><meta charset="utf-8"><title>Runtime Patched Selector Exit Candidate Poll</title>',
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;margin:24px}table{border-collapse:collapse;width:100%;font-size:13px}td,th{border-bottom:1px solid #333;padding:6px 8px;text-align:left;vertical-align:top}code{color:#9bd4ff}</style>",
        "</head><body>",
        "<h1>Runtime Patched Selector Exit Candidate Poll</h1>",
        f"<p>route {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}; candidates {summary['candidateCount']}; promotion status <code>{html.escape(summary['promotionStatus'])}</code>.</p>",
        f"<p>{html.escape(summary['conclusion'])}</p>",
        "<h2>Candidate Saves</h2>",
        "<table><thead><tr><th>side</th><th>save</th><th>selector</th><th>tile</th><th>auto</th><th>keys</th></tr></thead><tbody>",
        save_rows,
        "</tbody></table>",
        "<h2>Poll Results</h2>",
        "<table><thead><tr><th>side</th><th>tile</th><th>samples</th><th>selectors</th><th>route 2:0</th><th>branch nonzero</th><th>mode1</th><th>runtime flag</th><th>object index</th></tr></thead><tbody>",
        poll_rows,
        "</tbody></table>",
        "</body></html>",
    ])


def write_outputs(summary: dict[str, Any], out_dir: Path, output_prefix: str) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / f"{output_prefix}.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")) + "\n",
        encoding="utf-8",
    )
    (out_dir / f"{output_prefix}.md").write_text(markdown(summary), encoding="utf-8")
    (out_dir / f"{output_prefix}.html").write_text(html_page(summary), encoding="utf-8")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--startup-wait", type=float, default=18.0)
    parser.add_argument("--hold", type=float, default=0.7)
    parser.add_argument("--gap", type=float, default=0.25)
    parser.add_argument("--interval", type=float, default=0.02)
    parser.add_argument("--prefix", type=Path, default=DEFAULT_PREFIX)
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--output-prefix", default=OUTPUT_PREFIX)
    args = parser.parse_args()
    summary = build_summary(args)
    write_outputs(summary, args.out_dir, args.output_prefix)
    print(f"wrote patched selector exit-candidate poll -> {args.out_dir / (args.output_prefix + '.md')}")


if __name__ == "__main__":
    main()
