#!/usr/bin/env python3
"""Poll public source-save exit paths for map1_01a -> map2_02d route evidence."""
from __future__ import annotations

from argparse import Namespace
import html
from typing import Any

import probe_runtime_input_path as runtime_input
import probe_runtime_predecessor_coordinate_branch_state_poll as coord_poll
from probe_runtime_input_path import DEFAULT_PREFIX, OUT, ROOT
from probe_runtime_selected_pointer_multislot_savedata_poll import build_summary
from probe_runtime_selected_pointer_poll import write_outputs
from summarize_map_tiles import load_maps


OUTPUT_PREFIX = "runtime_selected_pointer_source_exit_branch_state_poll"
SOURCE_SAVE = "data/public_savedata/HandyHwanseEditor/bin/Debug/savedat2.dat"
SOURCE_MAP = "map1_01a"
TARGET_MAP = "map2_02d"
START_TILE = {"x": 0, "y": 2}

SOURCE_EXIT_CANDIDATES = [
    {
        "name": "source-top-auto-overrun",
        "candidateTile": {"x": 18, "y": 0},
        "candidateSide": "top",
        "tailKeys": ["Up"],
        "autoTrigger": True,
    },
    {
        "name": "source-bottom-auto-overrun",
        "candidateTile": {"x": 16, "y": 47},
        "candidateSide": "bottom",
        "tailKeys": ["Down"],
        "autoTrigger": True,
    },
    {
        "name": "source-left-manual-activate",
        "candidateTile": {"x": 3, "y": 14},
        "candidateSide": "left",
        "tailKeys": ["Return"],
        "autoTrigger": False,
    },
    {
        "name": "source-right-manual-activate",
        "candidateTile": {"x": 34, "y": 19},
        "candidateSide": "right",
        "tailKeys": ["Return"],
        "autoTrigger": False,
    },
]


def outside_tile(candidate: dict[str, Any]) -> dict[str, int]:
    tile = candidate["candidateTile"]
    side = candidate["candidateSide"]
    x = int(tile["x"])
    y = int(tile["y"])
    if side == "top":
        y -= 1
    elif side == "bottom":
        y += 1
    elif side == "left":
        x -= 1
    elif side == "right":
        x += 1
    return {"x": x, "y": y}


def planned_sequences() -> tuple[list[str], list[dict[str, Any]]]:
    maps = load_maps(ROOT / "out" / "maps.js")
    info = maps[SOURCE_MAP]
    sequences: list[str] = []
    plans: list[dict[str, Any]] = []
    for candidate in SOURCE_EXIT_CANDIDATES:
        path_keys = coord_poll.find_path(info, START_TILE, candidate["candidateTile"])
        keys = ["Down", "Return", *path_keys, *candidate["tailKeys"]]
        sequences.append(f"{candidate['name']}={','.join(keys)}")
        plans.append({
            **candidate,
            "sourceMap": SOURCE_MAP,
            "targetMap": TARGET_MAP,
            "startTile": START_TILE,
            "outsideTile": outside_tile(candidate),
            "pathStepCount": len(path_keys),
            "pathRunLengths": coord_poll.run_lengths(path_keys),
            "pathKeys": path_keys,
            "pathPoints": coord_poll.path_points(START_TILE, path_keys),
            "sequenceKeyCount": len(keys),
            "sequenceKeys": keys,
        })
    return sequences, plans


def pair_seen(row: dict[str, Any], prefix: str, slot_count: int, pair: dict[str, int]) -> list[int]:
    expected = coord_poll.pair_hex(int(pair["x"]), int(pair["y"]))
    return [
        slot
        for slot in range(slot_count)
        if expected in coord_poll.unique_pair_set(row, f"{prefix}{slot}TilePair")
    ]


def camera_pairs(row: dict[str, Any]) -> list[dict[str, Any]]:
    pairs = []
    for value in coord_poll.unique_values(row, "cameraTilePair"):
        decoded = coord_poll.unpack_pair_hex(value.get("valueHex"))
        if decoded:
            pairs.append(decoded | {"count": value.get("count")})
    return pairs


def camera_seen(row: dict[str, Any], pair: dict[str, int]) -> bool:
    expected = coord_poll.pair_hex(int(pair["x"]), int(pair["y"]))
    return expected in {
        value.get("valueHex")
        for value in coord_poll.unique_values(row, "cameraTilePair")
    }


def branch_state_all_zero(row: dict[str, Any]) -> bool:
    for index in range(12):
        values = coord_poll.unique_values(row, f"secondaryBranchState{index}")
        if len(values) != 1 or values[0].get("valueHex") != "0x00":
            return False
    return True


def analyze(summary: dict[str, Any], plans: list[dict[str, Any]]) -> dict[str, Any]:
    plans_by_name = {plan["name"]: plan for plan in plans}
    rows = []
    observed_public = summary.get("observedPublicSaveSelectors") or []
    source_save_observed = SOURCE_SAVE and "0:0" in observed_public
    any_candidate_observed = False
    any_outside_observed = False
    any_actor_or_trail_candidate = False
    any_actor_or_trail_outside = False
    any_route = bool(summary.get("anyReachedRouteSelectorContext"))
    for row in summary.get("rows") or []:
        plan = plans_by_name.get(row.get("name"))
        if not plan:
            continue
        candidate_tile = plan["candidateTile"]
        outside = plan["outsideTile"]
        actor_candidate = pair_seen(row, "actor", coord_poll.ACTOR_POINTER_SLOT_COUNT, candidate_tile)
        trail_candidate = pair_seen(row, "trail", coord_poll.ACTOR_HISTORY_SLOT_COUNT, candidate_tile)
        actor_outside = pair_seen(row, "actor", coord_poll.ACTOR_POINTER_SLOT_COUNT, outside)
        trail_outside = pair_seen(row, "trail", coord_poll.ACTOR_HISTORY_SLOT_COUNT, outside)
        camera_candidate = camera_seen(row, candidate_tile)
        camera_outside = camera_seen(row, outside)
        candidate_observed = bool(actor_candidate or trail_candidate or camera_candidate)
        outside_observed = bool(actor_outside or trail_outside or camera_outside)
        any_candidate_observed = any_candidate_observed or candidate_observed
        any_outside_observed = any_outside_observed or outside_observed
        any_actor_or_trail_candidate = any_actor_or_trail_candidate or bool(actor_candidate or trail_candidate)
        any_actor_or_trail_outside = any_actor_or_trail_outside or bool(actor_outside or trail_outside)
        rows.append({
            "name": row.get("name"),
            "sampleCount": row.get("sampleCount"),
            "eventCount": row.get("eventCount"),
            "eventsTruncated": row.get("eventsTruncated"),
            "selectors": row.get("uniqueSelectorContexts") or [],
            "candidateTile": candidate_tile,
            "outsideTile": outside,
            "candidateSide": plan.get("candidateSide"),
            "autoTrigger": plan.get("autoTrigger"),
            "cameraPairs": camera_pairs(row),
            "cameraCandidateObserved": camera_candidate,
            "cameraOutsideObserved": camera_outside,
            "actorCandidateSlots": actor_candidate,
            "trailCandidateSlots": trail_candidate,
            "actorOutsideSlots": actor_outside,
            "trailOutsideSlots": trail_outside,
            "candidateObserved": candidate_observed,
            "outsideObserved": outside_observed,
            "branchStateAllZero": branch_state_all_zero(row),
            "reachedCurrentRoot": row.get("reachedCurrentRoot"),
            "reachedRouteSelectorContext": row.get("reachedRouteSelectorContext"),
            "currentRootHitCount": row.get("currentRootHitCount"),
            "routeSelectorHitCount": row.get("routeSelectorHitCount"),
            "plan": plan,
        })
    if not source_save_observed:
        classification = "source-save-not-observed"
    elif any_route:
        classification = "route-selector-observed"
    elif any_actor_or_trail_outside:
        classification = "actor-or-trail-outside-observed-without-route"
    elif any_actor_or_trail_candidate:
        classification = "actor-or-trail-candidate-observed-without-route"
    elif any_outside_observed:
        classification = "camera-outside-observed-without-route"
    elif any_candidate_observed:
        classification = "camera-candidate-observed-without-route"
    else:
        classification = "candidate-not-observed"
    return {
        "classification": classification,
        "sequenceCount": len(rows),
        "sourceSaveSelector": "0:0",
        "sourceSaveObserved": source_save_observed,
        "observedPublicSaveSelectors": observed_public,
        "anyCandidateObserved": any_candidate_observed,
        "anyOutsideObserved": any_outside_observed,
        "anyActorOrTrailCandidateObserved": any_actor_or_trail_candidate,
        "anyActorOrTrailOutsideObserved": any_actor_or_trail_outside,
        "anyReachedCurrentRoot": summary.get("anyReachedCurrentRoot"),
        "anyReachedRouteSelectorContext": summary.get("anyReachedRouteSelectorContext"),
        "rows": rows,
    }


def source_exit_markdown(summary: dict[str, Any]) -> str:
    analysis = summary.get("sourceExitAnalysis") or {}
    lines = [
        "",
        "## Source Exit Runtime Poll",
        "",
        f"- classification: `{analysis.get('classification')}`",
        f"- source save selector `0:0` observed: {analysis.get('sourceSaveObserved')}",
        f"- observed public save selectors: `{','.join(analysis.get('observedPublicSaveSelectors') or []) or '-'}`",
        f"- candidate observed: {analysis.get('anyCandidateObserved')}",
        f"- outside observed: {analysis.get('anyOutsideObserved')}",
        f"- actor/trail candidate observed: {analysis.get('anyActorOrTrailCandidateObserved')}",
        f"- actor/trail outside observed: {analysis.get('anyActorOrTrailOutsideObserved')}",
        f"- selector 2:0/current root observed: {analysis.get('anyReachedRouteSelectorContext')} / {analysis.get('anyReachedCurrentRoot')}",
        "",
        "| sequence | candidate | samples | selectors | camera pairs | camera cand/out | actor cand/out | trail cand/out | route/current | branch state |",
        "| --- | --- | ---: | --- | --- | --- | --- | --- | --- | --- |",
    ]
    for row in analysis.get("rows") or []:
        candidate = row.get("candidateTile") or {}
        outside = row.get("outsideTile") or {}
        pairs = "; ".join(
            f"{pair.get('x')},{pair.get('y')}x{pair.get('count')}"
            for pair in row.get("cameraPairs") or []
        )
        selectors = [
            item.get("selector") if isinstance(item, dict) else str(item)
            for item in row.get("selectors") or []
        ]
        lines.append(
            f"| `{row.get('name')}` | `{candidate.get('x')},{candidate.get('y')}` -> "
            f"`{outside.get('x')},{outside.get('y')}` {row.get('candidateSide')} | "
            f"{row.get('sampleCount')} | `{','.join(selector for selector in selectors if selector) or '-'}` | "
            f"`{pairs or '-'}` | {row.get('cameraCandidateObserved')}/{row.get('cameraOutsideObserved')} | "
            f"`{','.join(str(item) for item in row.get('actorCandidateSlots') or []) or '-'}`/"
            f"`{','.join(str(item) for item in row.get('actorOutsideSlots') or []) or '-'}` | "
            f"`{','.join(str(item) for item in row.get('trailCandidateSlots') or []) or '-'}`/"
            f"`{','.join(str(item) for item in row.get('trailOutsideSlots') or []) or '-'}` | "
            f"{row.get('routeSelectorHitCount')}/{row.get('currentRootHitCount')} | "
            f"{'all-zero' if row.get('branchStateAllZero') else 'mixed'} |"
        )
    return "\n".join(lines)


def source_exit_html(summary: dict[str, Any]) -> str:
    return "\n".join([
        "<h2>Source Exit Runtime Poll</h2>",
        f"<pre>{html.escape(source_exit_markdown(summary))}</pre>",
    ])


def write_source_exit_outputs(summary: dict[str, Any]) -> None:
    write_outputs(summary, OUT, OUTPUT_PREFIX)
    html_path = OUT / f"{OUTPUT_PREFIX}.html"
    html_path.write_text(html_path.read_text(encoding="utf-8") + source_exit_html(summary), encoding="utf-8")


def main() -> None:
    runtime_input.ROUTE_WATCH_VALUES = dict(runtime_input.ROUTE_WATCH_VALUES)
    runtime_input.ROUTE_WATCH_VALUES.update(coord_poll.BRANCH_STATE_WATCH_VALUES)
    sequences, plans = planned_sequences()
    args = Namespace(
        startup_wait=18.0,
        hold=0.7,
        gap=0.25,
        interval=0.02,
        prelude="input-path",
        sequence=sequences,
        slot_source=[f"1={SOURCE_SAVE}"],
        case_aliases=True,
        staged_kind="public source selector 0:0 exit branch-state watch",
        prefix=DEFAULT_PREFIX,
        out_dir=OUT,
        output_prefix=OUTPUT_PREFIX,
    )
    coord_poll.install_coordinate_sampler()
    try:
        summary = build_summary(args)
    finally:
        coord_poll.restore_coordinate_sampler()
    analysis = analyze(summary, plans)
    summary["objective"] = "public source selector 0:0 map1_01a exit path poll"
    summary["sourceSave"] = SOURCE_SAVE
    summary["sourceMap"] = SOURCE_MAP
    summary["targetMap"] = TARGET_MAP
    summary["startTile"] = START_TILE
    summary["targetedExitCandidates"] = plans
    summary["branchStateWatchValues"] = coord_poll.BRANCH_STATE_WATCH_VALUES
    summary["coordinateWatchModel"] = {
        "cameraTileGlobalsHex": [coord_poll.hex32(coord_poll.CAMERA_TILE_X), coord_poll.hex32(coord_poll.CAMERA_TILE_Y)],
        "activeActorPointerTableHex": coord_poll.hex32(coord_poll.ACTOR_POINTER_TABLE),
        "actorObjectTileOffsetsHex": [coord_poll.hex32(coord_poll.OBJECT_TILE_X_OFFSET), coord_poll.hex32(coord_poll.OBJECT_TILE_Y_OFFSET)],
        "partyTrailRingHex": {
            "historyIndexTable": coord_poll.hex32(coord_poll.ACTOR_HISTORY_INDEX_TABLE),
            "seedTileXTable": coord_poll.hex32(coord_poll.ACTOR_SEED_X_TABLE),
            "seedTileYTable": coord_poll.hex32(coord_poll.ACTOR_SEED_Y_TABLE),
            "slotCount": coord_poll.ACTOR_HISTORY_SLOT_COUNT,
            "strideBytes": coord_poll.ACTOR_HISTORY_STRIDE_BYTES,
        },
    }
    summary["sourceExitAnalysis"] = analysis
    summary["sourceExitBranchStateSplit"] = {
        "classification": analysis.get("classification"),
        "anyCandidateObserved": analysis.get("anyCandidateObserved"),
        "anyOutsideObserved": analysis.get("anyOutsideObserved"),
        "anyActorOrTrailCandidateObserved": analysis.get("anyActorOrTrailCandidateObserved"),
        "anyActorOrTrailOutsideObserved": analysis.get("anyActorOrTrailOutsideObserved"),
        "anyReachedCurrentRoot": analysis.get("anyReachedCurrentRoot"),
        "anyReachedRouteSelectorContext": analysis.get("anyReachedRouteSelectorContext"),
    }
    summary["promotionStatus"] = "blocked"
    summary["conclusion"] = (
        f"Staged public selector 0:0 source save and polled selector, branch-state, and coordinate watches "
        f"across {summary.get('sequenceCount')} map1_01a exit path(s). "
        f"Source exit classification={analysis.get('classification')}; "
        f"selector 2:0 reached={summary.get('anyReachedRouteSelectorContext')}. "
        "This is non-promoting unless selector 2:0/current root or a strict source hotspot is observed."
    )
    coord_poll.prune_summary_for_output(summary)
    write_source_exit_outputs(summary)
    print(f"wrote source exit branch-state poll -> {OUT / (OUTPUT_PREFIX + '.html')}")


if __name__ == "__main__":
    main()
