#!/usr/bin/env python3
"""Try short public-predecessor load sequences and scan for field-entry coordinate sources."""
from __future__ import annotations

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

from probe_runtime_input_path import DEFAULT_PREFIX, OUT, ROOT
from probe_runtime_predecessor_coordinate_source_scan import (
    SOURCE_SAVE,
    classify,
    run_scan,
)
from probe_runtime_selected_pointer_multislot_savedata_poll import (
    cleanup_temporary_saves,
    prepare_temporary_saves,
)
from runtime_case_aliases import cleanup_case_aliases, prepare_case_aliases


OUTPUT_PREFIX = "runtime_predecessor_field_entry_sequence_scan"
SEQUENCES = [
    {"name": "load-only", "keys": []},
    {"name": "load-return", "keys": ["Return"]},
    {"name": "load-space", "keys": ["space"]},
    {"name": "load-z", "keys": ["z"]},
    {"name": "load-escape", "keys": ["Escape"]},
    {"name": "load-up", "keys": ["Up"]},
    {"name": "load-down", "keys": ["Down"]},
    {"name": "load-left", "keys": ["Left"]},
    {"name": "load-right", "keys": ["Right"]},
    {"name": "load-down-enter", "keys": ["Down", "Return"]},
    {"name": "load-down-enter-enter", "keys": ["Down", "Return", "Return"]},
    {"name": "load-down-enter-space", "keys": ["Down", "Return", "space"]},
    {"name": "load-down-enter-z", "keys": ["Down", "Return", "z"]},
    {"name": "load-down-enter-escape", "keys": ["Down", "Return", "Escape"]},
    {"name": "load-up-enter", "keys": ["Up", "Return"]},
    {"name": "load-left-enter", "keys": ["Left", "Return"]},
    {"name": "load-down-enter-right-return", "keys": ["Down", "Return", "Right", "Return"]},
    {"name": "load-down-down-enter", "keys": ["Down", "Down", "Return"]},
    {"name": "load-down-space", "keys": ["Down", "space"]},
    {"name": "load-down-z", "keys": ["Down", "z"]},
    {"name": "load-right-return", "keys": ["Right", "Return"]},
    {"name": "load-right-z", "keys": ["Right", "z"]},
]
SEQUENCE_BY_NAME = {row["name"]: row for row in SEQUENCES}
FAILED_PREDECESSOR_FIELD_ENTRY_GATE_IDS = [
    "field-entry-input-sequence",
    "selector-2-0-snapshot",
    "route-relevant-runtime-object-tile",
    "selected-root-execution-proof",
]
PREDECESSOR_FIELD_ENTRY_MISSING_EVIDENCE_BY_GATE = {
    "field-entry-input-sequence": (
        "input sequence from the public predecessor save that reaches selector 2:0 "
        "or a route-relevant runtime object tile"
    ),
    "selector-2-0-snapshot": "transient or final selector 2:0 snapshot during field-entry follow-up",
    "route-relevant-runtime-object-tile": (
        "plausible nonzero route object +0xe8/+0xea tile in runtime pointer tables"
    ),
    "selected-root-execution-proof": (
        "selected-root execution proof linking the field-entry path to current root 0x00540714"
    ),
}
PREDECESSOR_FIELD_ENTRY_EVIDENCE_REFS = [
    {
        "path": "data/public_savedata/flack3r/savedat2.dat",
        "fields": ["sourceSave", "timing", "sequenceCount"],
    },
    {
        "path": "tools/probe_runtime_predecessor_field_entry_sequence_scan.py",
        "fields": ["SEQUENCES", "run_scan", "summarize_row"],
    },
    {
        "path": "out/runtime_predecessor_field_entry_sequence_scan.json",
        "fields": [
            "rows",
            "fieldEntryCandidateCount",
            "snapshotRouteCandidateCount",
            "finalSelectorCounts",
            "snapshotSelectorCounts",
        ],
    },
    {
        "path": "out/runtime_predecessor_coordinate_source_scan.json",
        "fields": [
            "classification",
            "coordinateSourceRejectionClassification",
            "promotionStatus",
        ],
    },
]


def final_snapshot(row: dict[str, Any]) -> dict[str, Any]:
    snapshots = row.get("snapshots") or []
    return snapshots[-1] if snapshots else {}


def plausible_object_tile_hits(row: dict[str, Any]) -> list[dict[str, Any]]:
    hits = []
    for table in row.get("pointerTables") or []:
        for entry in table.get("entries") or []:
            for field in entry.get("fieldPairs") or []:
                if (
                    field.get("name") == "tile_e8_ea"
                    and field.get("plausibleTile") is True
                    and [field.get("x"), field.get("y")] not in ([0, 0], [999, 999])
                ):
                    hits.append({
                        "table": table.get("name"),
                        "slot": entry.get("slot"),
                        "pointerStaticHex": entry.get("pointerStaticHex"),
                        "x": field.get("x"),
                        "y": field.get("y"),
                    })
    return hits


def snapshot_selector_counts(scan: dict[str, Any]) -> dict[str, int]:
    counts: dict[str, int] = {}
    for snapshot in scan.get("snapshots") or []:
        selector = (snapshot.get("selectedPointerContext") or {}).get("selector") or "-"
        counts[selector] = counts.get(selector, 0) + 1
    return counts


def snapshot_camera_tile_counts(scan: dict[str, Any]) -> dict[str, int]:
    counts: dict[str, int] = {}
    for snapshot in scan.get("snapshots") or []:
        camera = snapshot.get("cameraTile") or {}
        key = f"{camera.get('x')},{camera.get('y')}"
        counts[key] = counts.get(key, 0) + 1
    return counts


def summarize_row(name: str, keys: list[str], scan: dict[str, Any]) -> dict[str, Any]:
    final = final_snapshot(scan)
    selector = (final.get("selectedPointerContext") or {}).get("selector")
    camera = final.get("cameraTile") or {}
    object_hits = plausible_object_tile_hits(scan)
    snapshot_selectors = snapshot_selector_counts(scan)
    return {
        "name": name,
        "keys": keys,
        "classification": classify(scan),
        "finalSelectedPointerStaticHex": final.get("selectedPointerStaticHex"),
        "finalSelector": selector,
        "finalCameraTile": camera,
        "finalActiveOrderCount": final.get("activeOrderCount"),
        "finalActiveOrderBytes": final.get("activeOrderBytes") or [],
        "snapshotSelectorCounts": snapshot_selectors,
        "snapshotCameraTileCounts": snapshot_camera_tile_counts(scan),
        "snapshotCount": sum(snapshot_selectors.values()),
        "snapshotRouteCandidate": snapshot_selectors.get("2:0", 0) > 0,
        "plausibleObjectTileHits": object_hits,
        "fieldEntryCandidate": bool(object_hits) or selector == "2:0",
        "scan": scan,
    }


def parse_key_sequence(raw: str) -> list[str]:
    return [part.strip() for part in raw.split(",") if part.strip()]


def selected_sequences(args: argparse.Namespace) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    if args.sequence_name:
        for name in args.sequence_name:
            rows.append(SEQUENCE_BY_NAME[name])
    if args.sequence:
        for index, raw in enumerate(args.sequence, start=1):
            if ":" in raw:
                name, key_text = raw.split(":", 1)
                name = name.strip() or f"custom-{index}"
            else:
                name = f"custom-{index}"
                key_text = raw
            rows.append({"name": name, "keys": parse_key_sequence(key_text)})
    if not rows:
        rows = list(SEQUENCES)
    if args.sequence_limit is not None:
        rows = rows[: max(0, args.sequence_limit)]
    return rows


def build_summary(args: argparse.Namespace) -> dict[str, Any]:
    rows = []
    sequences = selected_sequences(args)
    for sequence in sequences:
        scan_args = Namespace(
            startup_wait=args.startup_wait,
            hold=args.hold,
            gap=args.gap,
            final_wait=args.final_wait,
            sequence=sequence["keys"],
            prefix=args.prefix,
        )
        scan = run_scan(scan_args)
        rows.append(summarize_row(sequence["name"], sequence["keys"], scan))
    field_candidates = [row for row in rows if row.get("fieldEntryCandidate")]
    selector_counts: dict[str, int] = {}
    camera_counts: dict[str, int] = {}
    classification_counts: dict[str, int] = {}
    active_order_counts: dict[str, int] = {}
    snapshot_selector_counts_total: dict[str, int] = {}
    snapshot_camera_counts_total: dict[str, int] = {}
    snapshot_count = 0
    for row in rows:
        selector = row.get("finalSelector") or "-"
        selector_counts[selector] = selector_counts.get(selector, 0) + 1
        camera = row.get("finalCameraTile") or {}
        camera_key = f"{camera.get('x')},{camera.get('y')}"
        camera_counts[camera_key] = camera_counts.get(camera_key, 0) + 1
        classification = row.get("classification") or "-"
        classification_counts[classification] = classification_counts.get(classification, 0) + 1
        active_key = ",".join(str(value) for value in row.get("finalActiveOrderBytes") or []) or "-"
        active_order_counts[active_key] = active_order_counts.get(active_key, 0) + 1
        snapshot_count += int(row.get("snapshotCount") or 0)
        for selector, count in (row.get("snapshotSelectorCounts") or {}).items():
            snapshot_selector_counts_total[selector] = snapshot_selector_counts_total.get(selector, 0) + int(count)
        for camera, count in (row.get("snapshotCameraTileCounts") or {}).items():
            snapshot_camera_counts_total[camera] = snapshot_camera_counts_total.get(camera, 0) + int(count)
    snapshot_route_candidates = [row for row in rows if row.get("snapshotRouteCandidate")]
    route_relevant_runtime_object_tile_found = any(
        row.get("plausibleObjectTileHits") for row in rows
    )
    selector_2_final_observed = selector_counts.get("2:0", 0) > 0
    selector_2_snapshot_observed = snapshot_selector_counts_total.get("2:0", 0) > 0
    field_entry_route_candidate_found = bool(
        field_candidates
        or snapshot_route_candidates
        or selector_2_final_observed
        or selector_2_snapshot_observed
        or route_relevant_runtime_object_tile_found
    )
    selected_root_execution_proof_found = False
    failed_gate_ids = []
    if not field_entry_route_candidate_found:
        failed_gate_ids.append("field-entry-input-sequence")
    if not selector_2_snapshot_observed:
        failed_gate_ids.append("selector-2-0-snapshot")
    if not route_relevant_runtime_object_tile_found:
        failed_gate_ids.append("route-relevant-runtime-object-tile")
    if not selected_root_execution_proof_found:
        failed_gate_ids.append("selected-root-execution-proof")
    missing_evidence = [
        PREDECESSOR_FIELD_ENTRY_MISSING_EVIDENCE_BY_GATE[gate_id]
        for gate_id in failed_gate_ids
    ]
    proof_found = not failed_gate_ids
    return {
        "objective": "broadened sequence sweep for a public-predecessor field-entry state with live actor coordinates",
        "sourceSave": str(SOURCE_SAVE.relative_to(ROOT)),
        "timing": {
            "startupWaitSeconds": args.startup_wait,
            "holdSeconds": args.hold,
            "gapSeconds": args.gap,
            "finalWaitSeconds": args.final_wait,
        },
        "sequenceCount": len(rows),
        "fieldEntryCandidateCount": len(field_candidates),
        "fieldEntryCandidateNames": [row["name"] for row in field_candidates],
        "snapshotCount": snapshot_count,
        "snapshotRouteCandidateCount": len(snapshot_route_candidates),
        "snapshotRouteCandidateNames": [row["name"] for row in snapshot_route_candidates],
        "finalSelectorCounts": selector_counts,
        "finalCameraTileCounts": camera_counts,
        "snapshotSelectorCounts": snapshot_selector_counts_total,
        "snapshotCameraTileCounts": snapshot_camera_counts_total,
        "classificationCounts": classification_counts,
        "activeOrderPatternCounts": active_order_counts,
        "promotionStatus": "diagnostic-only",
        "proofFound": proof_found,
        "predecessorFieldEntryProofFound": field_entry_route_candidate_found,
        "fieldEntryRouteCandidateFound": field_entry_route_candidate_found,
        "selector2FinalObserved": selector_2_final_observed,
        "selector2SnapshotObserved": selector_2_snapshot_observed,
        "routeRelevantRuntimeObjectTileFound": route_relevant_runtime_object_tile_found,
        "selectedRootExecutionProofFound": selected_root_execution_proof_found,
        "failedPredecessorFieldEntryGateIds": failed_gate_ids,
        "missingEvidence": missing_evidence,
        "remainingProofs": missing_evidence,
        "evidenceRefs": PREDECESSOR_FIELD_ENTRY_EVIDENCE_REFS,
        "evidenceRefCount": len(PREDECESSOR_FIELD_ENTRY_EVIDENCE_REFS),
        "rows": rows,
        "conclusion": (
            "This sweep tries load-menu follow-up, direction, confirm/cancel, and short movement/action sequences "
            "and checks whether any one reaches selector 2:0 or exposes a plausible nonzero runtime object "
            "+0xe8/+0xea tile. It is diagnostic evidence for finding the original field-entry input path."
        ),
    }


def markdown(summary: dict[str, Any]) -> str:
    lines = [
        "# Runtime Predecessor Field-Entry Sequence Scan",
        "",
        f"- sequence count: {summary.get('sequenceCount')}",
        f"- field-entry candidates: `{','.join(summary.get('fieldEntryCandidateNames') or []) or '-'}`",
        f"- snapshot route candidates: `{','.join(summary.get('snapshotRouteCandidateNames') or []) or '-'}`",
        f"- final selector counts: `{json.dumps(summary.get('finalSelectorCounts') or {}, sort_keys=True)}`",
        f"- final camera tile counts: `{json.dumps(summary.get('finalCameraTileCounts') or {}, sort_keys=True)}`",
        f"- snapshot selector counts: `{json.dumps(summary.get('snapshotSelectorCounts') or {}, sort_keys=True)}`",
        f"- snapshot camera tile counts: `{json.dumps(summary.get('snapshotCameraTileCounts') or {}, sort_keys=True)}`",
        f"- classification counts: `{json.dumps(summary.get('classificationCounts') or {}, sort_keys=True)}`",
        f"- promotion status: `{summary.get('promotionStatus')}`",
        f"- proof found: {summary.get('proofFound')}",
        f"- predecessor field-entry proof found: {summary.get('predecessorFieldEntryProofFound')}",
        f"- failed predecessor field-entry gates: `{','.join(summary.get('failedPredecessorFieldEntryGateIds') or [])}`",
        f"- missing evidence count: {len(summary.get('missingEvidence') or [])}",
        f"- evidence refs: {summary.get('evidenceRefCount')}",
        "",
        "## Missing Evidence",
        "",
        *[f"- {item}" for item in summary.get("missingEvidence") or []],
        "",
        "## Evidence Refs",
        "",
        "| path | fields |",
        "| --- | --- |",
        *[
            f"| `{row.get('path')}` | {', '.join(row.get('fields') or []) or '-'} |"
            for row in summary.get("evidenceRefs") or []
        ],
        "",
        summary.get("conclusion") or "",
        "",
        "| sequence | keys | final selector | camera | active count | active order | coordinate class | object tile hits |",
        "| --- | --- | --- | --- | ---: | --- | --- | --- |",
    ]
    for row in summary.get("rows") or []:
        camera = row.get("finalCameraTile") or {}
        hits = "; ".join(
            f"{hit.get('table')}[{hit.get('slot')}]={hit.get('x')},{hit.get('y')}"
            for hit in row.get("plausibleObjectTileHits") or []
        )
        lines.append(
            f"| `{row.get('name')}` | `{','.join(row.get('keys') or [])}` | "
            f"`{row.get('finalSelector') or '-'}` | `{camera.get('x')},{camera.get('y')}` | "
            f"{row.get('finalActiveOrderCount')} | "
            f"`{','.join(str(value) for value in row.get('finalActiveOrderBytes') or [])}` | "
            f"`{row.get('classification')}` | `{hits or '-'}` |"
        )
    return "\n".join(lines) + "\n"


def write_outputs(summary: dict[str, Any], out_dir: Path = OUT) -> 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",
    )


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--sequence-name",
        action="append",
        choices=sorted(SEQUENCE_BY_NAME),
        help="run one named built-in sequence; repeatable; defaults to every built-in sequence",
    )
    parser.add_argument(
        "--sequence",
        action="append",
        default=[],
        help="run a custom comma-separated key sequence, optionally named as name:Key,Key",
    )
    parser.add_argument(
        "--sequence-limit",
        type=int,
        help="limit the selected sequence list, useful for short smoke probes",
    )
    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("--final-wait", type=float, default=1.0)
    parser.add_argument("--prefix", type=Path, default=DEFAULT_PREFIX)
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument(
        "--list-sequences",
        action="store_true",
        help="print built-in sequence names and exit without running Wine",
    )
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    if args.list_sequences:
        for row in SEQUENCES:
            print(f"{row['name']}: {','.join(row['keys']) or '-'}")
        return
    case_aliases = prepare_case_aliases(ROOT, True)
    temporary_saves = []
    directory_info = {"createdDirectory": False}
    try:
        temporary_saves, directory_info = prepare_temporary_saves([(1, SOURCE_SAVE)])
        summary = build_summary(args)
    finally:
        cleanup_temporary_saves(temporary_saves, directory_info["createdDirectory"])
        cleanup_case_aliases(ROOT, case_aliases)
    write_outputs(summary, args.out_dir)
    print(f"wrote predecessor field-entry sequence scan -> {args.out_dir / (OUTPUT_PREFIX + '.json')}")


if __name__ == "__main__":
    main()
