#!/usr/bin/env python3
"""Poll public-predecessor exit paths from the observed party-trail tile."""
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_predecessor_trail_start_branch_state_poll"
SOURCE_SAVE = "data/public_savedata/flack3r/savedat2.dat"
SOURCE_MAP = "map2_02d"
TARGET_MAP = "map1_01a"
TRAIL_START_TILE = {"x": 15, "y": 26}

TRAIL_EXIT_CANDIDATES = [
    {
        "name": "predecessor-trail-left-auto",
        "sourceMap": SOURCE_MAP,
        "targetMap": TARGET_MAP,
        "startTile": TRAIL_START_TILE,
        "candidateTile": {"x": 1, "y": 14},
        "candidateSide": "left",
        "targetSpawn": {"x": 34, "y": 19, "side": "right"},
        "reason": "trail-start confirmation for the closest reciprocal map1_01a right-exit hint",
    },
    {
        "name": "predecessor-trail-top-auto",
        "sourceMap": SOURCE_MAP,
        "targetMap": TARGET_MAP,
        "startTile": TRAIL_START_TILE,
        "candidateTile": {"x": 46, "y": 0},
        "candidateSide": "top",
        "targetSpawn": {"x": 16, "y": 47, "side": "bottom"},
        "reason": "trail-start confirmation for the reciprocal map1_01a bottom-exit hint",
    },
    {
        "name": "predecessor-trail-bottom-auto",
        "sourceMap": SOURCE_MAP,
        "targetMap": TARGET_MAP,
        "startTile": TRAIL_START_TILE,
        "candidateTile": {"x": 47, "y": 47},
        "candidateSide": "bottom",
        "targetSpawn": {"x": 18, "y": 0, "side": "top"},
        "reason": "trail-start confirmation for the reciprocal map1_01a top-exit hint",
    },
    {
        "name": "predecessor-trail-right-auto",
        "sourceMap": SOURCE_MAP,
        "targetMap": TARGET_MAP,
        "startTile": TRAIL_START_TILE,
        "candidateTile": {"x": 95, "y": 16},
        "candidateSide": "right",
        "targetSpawn": {"x": 3, "y": 14, "side": "left"},
        "reason": "trail-start confirmation for the reciprocal map1_01a left-exit hint",
    },
]

TRAIL_WATCH_KEYS = {
    f"trail{index}TilePair"
    for index in range(coord_poll.ACTOR_HISTORY_SLOT_COUNT)
} | {
    f"trail{index}Direction"
    for index in range(coord_poll.ACTOR_HISTORY_SLOT_COUNT)
}


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 TRAIL_EXIT_CANDIDATES:
        target_tile = candidate["candidateTile"]
        path_keys = coord_poll.find_path(info, TRAIL_START_TILE, target_tile)
        keys = ["Down", "Return", *path_keys, "Return"]
        sequences.append(f"{candidate['name']}={','.join(keys)}")
        plans.append({
            **candidate,
            "pathStepCount": len(path_keys),
            "pathRunLengths": coord_poll.run_lengths(path_keys),
            "pathKeys": path_keys,
            "pathPoints": coord_poll.path_points(TRAIL_START_TILE, path_keys),
            "sequenceKeyCount": len(keys),
            "sequenceKeys": keys,
        })
    return sequences, plans


def coordinate_rows_for_prefix(
    row: dict[str, Any],
    prefix: str,
    slot_count: int,
    *,
    start_hex: str,
    target_hex: str,
    path_pair_hexes: set[str],
    target: dict[str, int],
) -> tuple[list[dict[str, Any]], list[int], list[int], list[int], list[int], dict[str, Any] | None]:
    slot_rows = []
    movement_slots = []
    start_slots = []
    target_slots = []
    path_slots = []
    best_slot: dict[str, Any] | None = None
    for slot in range(slot_count):
        values = coord_poll.unique_pair_set(row, f"{prefix}{slot}TilePair")
        decoded = [coord_poll.unpack_pair_hex(raw) for raw in sorted(values)]
        decoded = [item for item in decoded if item is not None]
        path_hits = values & path_pair_hexes
        min_distance = None
        if decoded:
            min_distance = min(
                abs(int(item["x"]) - int(target["x"])) + abs(int(item["y"]) - int(target["y"]))
                for item in decoded
            )
        slot_row = {
            "slot": slot,
            "uniqueTilePairCount": len(values),
            "movementObserved": len(values) > 1,
            "startObserved": start_hex in values,
            "targetObserved": target_hex in values,
            "plannedPathHitCount": len(path_hits),
            "plannedPathPointCount": len(path_pair_hexes),
            "plannedPathCoverageRatio": round(len(path_hits) / max(len(path_pair_hexes), 1), 4),
            "minManhattanDistanceToTarget": min_distance,
            "samplePairs": decoded[:12],
            "samplePairsTruncated": max(0, len(decoded) - 12),
        }
        slot_rows.append(slot_row)
        if slot_row["movementObserved"]:
            movement_slots.append(slot)
        if slot_row["startObserved"]:
            start_slots.append(slot)
        if slot_row["targetObserved"]:
            target_slots.append(slot)
        if slot_row["plannedPathHitCount"]:
            path_slots.append(slot)
        if best_slot is None or slot_row["plannedPathHitCount"] > best_slot["plannedPathHitCount"]:
            best_slot = slot_row
    return slot_rows, movement_slots, start_slots, target_slots, path_slots, best_slot


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_trail_start(summary: dict[str, Any], plans: list[dict[str, Any]]) -> dict[str, Any]:
    plans_by_name = {plan["name"]: plan for plan in plans}
    rows = []
    any_actor_start = False
    any_actor_target = False
    any_trail_start = False
    any_trail_target = False
    any_actor_movement = False
    any_trail_movement = False
    for row in summary.get("rows") or []:
        plan = plans_by_name.get(row.get("name"))
        if not plan:
            continue
        target = plan["candidateTile"]
        start_hex = coord_poll.pair_hex(int(TRAIL_START_TILE["x"]), int(TRAIL_START_TILE["y"]))
        target_hex = coord_poll.pair_hex(int(target["x"]), int(target["y"]))
        path_pair_hexes = {
            coord_poll.pair_hex(int(point["x"]), int(point["y"]))
            for point in plan.get("pathPoints") or []
        }
        actor_rows, actor_movement, actor_start, actor_target, actor_path, actor_best = coordinate_rows_for_prefix(
            row,
            "actor",
            coord_poll.ACTOR_POINTER_SLOT_COUNT,
            start_hex=start_hex,
            target_hex=target_hex,
            path_pair_hexes=path_pair_hexes,
            target=target,
        )
        trail_rows, trail_movement, trail_start, trail_target, trail_path, trail_best = coordinate_rows_for_prefix(
            row,
            "trail",
            coord_poll.ACTOR_HISTORY_SLOT_COUNT,
            start_hex=start_hex,
            target_hex=target_hex,
            path_pair_hexes=path_pair_hexes,
            target=target,
        )
        any_actor_start = any_actor_start or bool(actor_start)
        any_actor_target = any_actor_target or bool(actor_target)
        any_trail_start = any_trail_start or bool(trail_start)
        any_trail_target = any_trail_target or bool(trail_target)
        any_actor_movement = any_actor_movement or bool(actor_movement)
        any_trail_movement = any_trail_movement or bool(trail_movement)
        rows.append({
            "name": row.get("name"),
            "candidateTile": target,
            "candidateSide": plan.get("candidateSide"),
            "sequenceKeyCount": len(row.get("keys") or []),
            "sampleCount": row.get("sampleCount"),
            "eventCount": row.get("eventCount"),
            "eventsTruncated": row.get("eventsTruncated"),
            "startTilePairHex": start_hex,
            "targetTilePairHex": target_hex,
            "actorStartSlots": actor_start,
            "actorTargetSlots": actor_target,
            "actorMovementSlots": actor_movement,
            "actorPathSlots": actor_path,
            "actorBestSlot": actor_best,
            "trailStartSlots": trail_start,
            "trailTargetSlots": trail_target,
            "trailMovementSlots": trail_movement,
            "trailPathSlots": trail_path,
            "trailBestSlot": trail_best,
            "actorSlots": actor_rows,
            "trailSlots": trail_rows,
            "activeActorCountValues": coord_poll.unique_values(row, "activeActorCount"),
            "cameraTilePairs": [
                coord_poll.unpack_pair_hex(value["valueHex"]) | {"count": value.get("count")}
                for value in coord_poll.unique_values(row, "cameraTilePair")
                if coord_poll.unpack_pair_hex(value["valueHex"])
            ],
            "branchStateAllZero": branch_state_all_zero(row),
        })
    if any_actor_target or any_trail_target:
        classification = "trail-start-target-observed"
    elif any_actor_movement or any_trail_movement:
        classification = "trail-start-movement-without-target"
    elif any_trail_start:
        classification = "trail-start-observed-target-not-observed"
    else:
        classification = "trail-start-not-observed"
    return {
        "classification": classification,
        "sequenceCount": len(rows),
        "startTile": TRAIL_START_TILE,
        "anyActorStartTileObserved": any_actor_start,
        "anyActorTargetTileObserved": any_actor_target,
        "anyTrailStartTileObserved": any_trail_start,
        "anyTrailTargetTileObserved": any_trail_target,
        "anyActorMovementObserved": any_actor_movement,
        "anyTrailMovementObserved": any_trail_movement,
        "anyStartTileObserved": any_actor_start or any_trail_start,
        "anyTargetTileObserved": any_actor_target or any_trail_target,
        "rows": rows,
    }


def trail_markdown(summary: dict[str, Any]) -> str:
    analysis = summary.get("trailStartCoordinateAnalysis") or {}
    lines = [
        "",
        "## Trail-Start Coordinate Confirmation",
        "",
        f"- classification: `{analysis.get('classification')}`",
        f"- start tile: `{TRAIL_START_TILE['x']},{TRAIL_START_TILE['y']}`",
        f"- actor start observed: {analysis.get('anyActorStartTileObserved')}",
        f"- trail start observed: {analysis.get('anyTrailStartTileObserved')}",
        f"- actor target observed: {analysis.get('anyActorTargetTileObserved')}",
        f"- trail target observed: {analysis.get('anyTrailTargetTileObserved')}",
        f"- actor movement observed: {analysis.get('anyActorMovementObserved')}",
        f"- trail movement observed: {analysis.get('anyTrailMovementObserved')}",
        "",
        "| sequence | candidate | samples | actor start/target/move | trail start/target/move | best actor coverage | best trail coverage | branch state |",
        "| --- | --- | ---: | --- | --- | ---: | ---: | --- |",
    ]
    for row in analysis.get("rows") or []:
        actor_best = row.get("actorBestSlot") or {}
        trail_best = row.get("trailBestSlot") or {}
        candidate = row.get("candidateTile") or {}
        lines.append(
            f"| `{row.get('name')}` | `{candidate.get('x')},{candidate.get('y')}` {row.get('candidateSide') or ''} | "
            f"{row.get('sampleCount')} | "
            f"`{','.join(str(item) for item in row.get('actorStartSlots') or []) or '-'}`/"
            f"`{','.join(str(item) for item in row.get('actorTargetSlots') or []) or '-'}`/"
            f"`{','.join(str(item) for item in row.get('actorMovementSlots') or []) or '-'}` | "
            f"`{','.join(str(item) for item in row.get('trailStartSlots') or []) or '-'}`/"
            f"`{','.join(str(item) for item in row.get('trailTargetSlots') or []) or '-'}`/"
            f"`{','.join(str(item) for item in row.get('trailMovementSlots') or []) or '-'}` | "
            f"{actor_best.get('plannedPathHitCount')}/{actor_best.get('plannedPathPointCount')} | "
            f"{trail_best.get('plannedPathHitCount')}/{trail_best.get('plannedPathPointCount')} | "
            f"{'all-zero' if row.get('branchStateAllZero') else 'mixed'} |"
        )
    lines.extend([
        "",
        "This poll treats the `(15,26)` party trail ring value as a candidate live actor/start source, "
        "then checks both actor object `+0xe8/+0xea` and trail ring pairs while replaying reciprocal exit paths.",
    ])
    return "\n".join(lines)


def trail_html(summary: dict[str, Any]) -> str:
    return "\n".join([
        "<h2>Trail-Start Coordinate Confirmation</h2>",
        f"<pre>{html.escape(trail_markdown(summary))}</pre>",
    ])


def write_trail_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") + trail_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)
    coord_poll.EVENT_WATCH_VALUE_KEYS.update(TRAIL_WATCH_KEYS)
    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 predecessor trail-start 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_trail_start(summary, plans)
    summary["objective"] = "public predecessor reciprocal-exit path poll from observed party-trail start tile"
    summary["sourceSave"] = SOURCE_SAVE
    summary["sourceMap"] = SOURCE_MAP
    summary["targetMap"] = TARGET_MAP
    summary["trailStartTile"] = TRAIL_START_TILE
    summary["targetedExitCandidates"] = plans
    summary["branchStateWatchValues"] = coord_poll.BRANCH_STATE_WATCH_VALUES
    summary["trailStartCoordinateAnalysis"] = analysis
    summary["trailStartBranchStateSplit"] = {
        "classification": analysis.get("classification"),
        "anyStartTileObserved": analysis.get("anyStartTileObserved"),
        "anyTargetTileObserved": analysis.get("anyTargetTileObserved"),
        "anyActorMovementObserved": analysis.get("anyActorMovementObserved"),
        "anyTrailMovementObserved": analysis.get("anyTrailMovementObserved"),
        "anyReachedCurrentRoot": summary.get("anyReachedCurrentRoot"),
        "anyReachedRouteSelectorContext": summary.get("anyReachedRouteSelectorContext"),
    }
    summary["pathPlanner"] = {
        "map": SOURCE_MAP,
        "startTile": TRAIL_START_TILE,
        "startTileSource": "runtime party trail ring 0x00574550/0x00574552 observed in coordinate source scan",
        "collisionModel": "original layer1 direction flags via can_move_from_tile",
    }
    summary["conclusion"] = (
        f"Staged the public predecessor save and replayed {summary.get('sequenceCount')} reciprocal exit path(s) "
        f"from trail-observed tile {TRAIL_START_TILE['x']},{TRAIL_START_TILE['y']}. "
        f"Coordinate classification={analysis.get('classification')}; target tile observed="
        f"{analysis.get('anyTargetTileObserved')}; route selector 2:0 reached="
        f"{summary.get('anyReachedRouteSelectorContext')}. "
        "This distinguishes the camera/save coordinate from the party trail candidate before using runtime movement "
        "as predecessor route evidence."
    )
    coord_poll.prune_summary_for_output(summary)
    write_trail_outputs(summary)
    print(f"wrote predecessor trail-start branch-state poll -> {OUT / (OUTPUT_PREFIX + '.html')}")


if __name__ == "__main__":
    main()
