#!/usr/bin/env python3
"""Adaptively wait for source selector 0:0, then drive map1_01a exit candidates."""
from __future__ import annotations

import html
import json
import shutil
import subprocess
import time
from pathlib import Path
from typing import Any, Callable

import probe_runtime_input_path as runtime_input
import probe_runtime_predecessor_coordinate_branch_state_poll as coord_poll
import probe_runtime_source_exit_branch_state_poll as source_exit
from probe_runtime_input_path import (
    CURRENT_ROOT,
    DEFAULT_PREFIX,
    OUT,
    ROOT,
    env_for,
    find_free_display,
    find_hwanse_pid,
    hex32,
    load_selector_contexts,
    loaded_base,
    parse_windows,
    run,
    selected_pointer_context,
    truncate,
    write_key_buffer,
)
from probe_runtime_key_sequences import KEY_OFFSETS, run_input_path_prelude
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
from summarize_map_tiles import load_maps


OUTPUT_PREFIX = "runtime_selected_pointer_source_exit_adaptive_coordinate_poll"
SOURCE_SELECTOR = "0:0"
TARGET_SELECTOR = "2:0"
SOURCE_START_TILE = {"x": 0, "y": 2}
SOURCE_START_ORIGIN = "camera"
SOURCE_READY_WATCH_KEY = "cameraTilePair"
SOURCE_SAVE = ROOT / "data" / "public_savedata" / "HandyHwanseEditor" / "bin" / "Debug" / "savedat2.dat"
STARTUP_WAIT_SECONDS = 18.0
POLL_INTERVAL_SECONDS = 0.02
KEY_HOLD_SECONDS = 0.7
KEY_GAP_SECONDS = 0.25
SOURCE_WAIT_SECONDS = 2.0
LOAD_NUDGE_KEYS = ["Down", "Return"]
ATTEMPTS_PER_CANDIDATE = 3


def selector_label_for_sample(sample: dict[str, Any], roots: list[int], contexts: dict[int, dict]) -> str:
    context = selected_pointer_context(sample.get("selectedPointerStaticHex"), roots, contexts)
    return (context or {}).get("selector") or "-"


def watch_hex(sample: dict[str, Any], name: str) -> str | None:
    row = (sample.get("watchValues") or {}).get(name) or {}
    return row.get("valueHex")


def source_ready_sample(sample: dict[str, Any], roots: list[int], contexts: dict[int, dict]) -> bool:
    return (
        selector_label_for_sample(sample, roots, contexts) == SOURCE_SELECTOR
        and watch_hex(sample, SOURCE_READY_WATCH_KEY) == coord_poll.pair_hex(
            SOURCE_START_TILE["x"], SOURCE_START_TILE["y"]
        )
    )


def source_ready_event(event: dict[str, Any]) -> bool:
    return (
        (event.get("selectorContext") or {}).get("selector") == SOURCE_SELECTOR
        and event.get(SOURCE_READY_WATCH_KEY) == coord_poll.pair_hex(
            SOURCE_START_TILE["x"], SOURCE_START_TILE["y"]
        )
    )


def outside_tile(candidate: dict[str, Any]) -> dict[str, int]:
    return source_exit.outside_tile(candidate)


def planned_candidates() -> list[dict[str, Any]]:
    maps = load_maps(ROOT / "out" / "maps.js")
    info = maps[source_exit.SOURCE_MAP]
    plans = []
    for candidate in source_exit.SOURCE_EXIT_CANDIDATES:
        path_keys = coord_poll.find_path(info, source_exit.START_TILE, candidate["candidateTile"])
        plans.append({
            **candidate,
            "sourceMap": source_exit.SOURCE_MAP,
            "targetMap": source_exit.TARGET_MAP,
            "startTile": source_exit.START_TILE,
            "outsideTile": outside_tile(candidate),
            "pathStepCount": len(path_keys),
            "pathKeys": path_keys,
            "pathRunLengths": coord_poll.run_lengths(path_keys),
            "tailKeys": candidate["tailKeys"],
        })
    return plans


def init_state(name: str, keys: list[str]) -> dict[str, Any]:
    return {
        "name": name,
        "keys": keys,
        "startedAt": time.time(),
        "sampleCount": 0,
        "events": [],
        "eventsTruncated": 0,
        "uniqueSelectorContexts": {},
        "uniqueWatchValues": {},
        "currentRootHitCount": 0,
        "routeSelectorHitCount": 0,
        "lastSelectedPointerStaticHex": None,
        "lastWatchValues": {},
    }


def compact_context(context: dict[str, Any] | None) -> dict[str, Any] | None:
    if not context:
        return None
    return {
        "selector": context.get("selector"),
        "rootHex": context.get("rootHex"),
        "offsetHex": context.get("offsetHex"),
        "fieldMaps": context.get("fieldMaps") or [],
        "equalsCurrentRouteRoot": context.get("equalsCurrentRouteRoot"),
    }


def record_sample(
    state: dict[str, Any],
    *,
    sample: dict[str, Any],
    phase: str,
    roots: list[int],
    contexts: dict[int, dict],
) -> bool:
    state["sampleCount"] += 1
    selected = sample.get("selectedPointerStaticHex")
    context = selected_pointer_context(selected, roots, contexts)
    selector = (context or {}).get("selector") or "-"
    watch_values = {
        name: row.get("valueHex")
        for name, row in (sample.get("watchValues") or {}).items()
        if isinstance(row, dict)
    }
    if selected:
        pointer_row = state.setdefault("uniqueSelectedPointers", {}).setdefault(
            selected,
            {
                "selectedPointerStaticHex": selected,
                "count": 0,
                "selectorContext": compact_context(context),
            },
        )
        pointer_row["count"] += 1
        selector_row = state["uniqueSelectorContexts"].setdefault(
            selector,
            {
                "selector": selector,
                "count": 0,
                "rootHex": (context or {}).get("rootHex"),
                "fieldMaps": (context or {}).get("fieldMaps") or [],
            },
        )
        selector_row["count"] += 1
        if selected == hex32(CURRENT_ROOT):
            state["currentRootHitCount"] += 1
        if selector == TARGET_SELECTOR:
            state["routeSelectorHitCount"] += 1
    for watch_name, value_hex in watch_values.items():
        values = state["uniqueWatchValues"].setdefault(watch_name, {})
        values[value_hex] = values.get(value_hex, 0) + 1
    changed = selected != state.get("lastSelectedPointerStaticHex") or watch_values != state.get("lastWatchValues")
    target_hit = selected == hex32(CURRENT_ROOT) or selector == TARGET_SELECTOR
    source_hit = source_ready_sample(sample, roots, contexts)
    if changed or target_hit or source_hit or state["sampleCount"] == 1:
        if len(state["events"]) < 220:
            state["events"].append({
                "phase": phase,
                "sampleIndex": state["sampleCount"],
                "elapsedMs": round((time.time() - state["startedAt"]) * 1000, 1),
                "selectedPointerStaticHex": selected,
                "selectorContext": compact_context(context),
                "cameraTilePair": watch_hex(sample, "cameraTilePair"),
                "actor0TilePair": watch_hex(sample, "actor0TilePair"),
                "trail0TilePair": watch_hex(sample, "trail0TilePair"),
                "secondaryBranchState0": watch_hex(sample, "secondaryBranchState0"),
                "readOk": sample.get("readOk"),
            })
        else:
            state["eventsTruncated"] += 1
    state["lastSelectedPointerStaticHex"] = selected
    state["lastWatchValues"] = watch_values
    return source_hit


def poll_for(
    state: dict[str, Any],
    *,
    pid: int,
    base: int,
    duration: float,
    phase: str,
    roots: list[int],
    contexts: dict[int, dict],
    stop_when: Callable[[dict[str, Any]], bool] | None = None,
) -> dict[str, Any]:
    deadline = time.time() + duration
    while time.time() < deadline:
        sample = runtime_input.sample_process(pid, base)
        source_hit = record_sample(state, sample=sample, phase=phase, roots=roots, contexts=contexts)
        if source_hit and (stop_when is None or stop_when(sample)):
            return {"stopped": True, "reason": "source-ready", "sample": sample}
        if stop_when is not None and stop_when(sample):
            return {"stopped": True, "reason": "custom", "sample": sample}
        time.sleep(POLL_INTERVAL_SECONDS)
    return {"stopped": False, "reason": "timeout", "sample": None}


def drive_key(
    state: dict[str, Any],
    *,
    key_name: str,
    pid: int,
    base: int,
    roots: list[int],
    contexts: dict[int, dict],
    phase_prefix: str,
) -> dict[str, Any]:
    offset = KEY_OFFSETS[key_name]
    write = write_key_buffer(pid, base, offset, duration=KEY_HOLD_SECONDS)
    hold_result = poll_for(
        state,
        pid=pid,
        base=base,
        duration=0.01,
        phase=f"{phase_prefix}:after-write:{key_name}",
        roots=roots,
        contexts=contexts,
    )
    gap_result = poll_for(
        state,
        pid=pid,
        base=base,
        duration=KEY_GAP_SECONDS,
        phase=f"{phase_prefix}:gap:{key_name}",
        roots=roots,
        contexts=contexts,
    )
    return {"key": key_name, "write": write, "holdPoll": hold_result, "gapPoll": gap_result}


def focus_windows(env: dict[str, str]) -> dict[str, Any]:
    class_search = run(["xdotool", "search", "--class", "hwanse2"], env) if shutil.which("xdotool") else {"output": ""}
    name_search = run(["xdotool", "search", "--name", "hwanse"], env) if shutil.which("xdotool") else {"output": ""}
    windows = parse_windows([class_search.get("output", ""), name_search.get("output", "")])
    focused = []
    for window in windows[:8]:
        run(["xdotool", "windowactivate", "--sync", window], env)
        run(["xdotool", "windowfocus", window], env)
        run(["xdotool", "mousemove", "--window", window, "320", "240", "click", "1"], env)
        focused.append(window)
        time.sleep(0.05)
    return {"classSearch": class_search, "nameSearch": name_search, "windows": windows, "focusedWindows": focused}


def unique_watch_rows(state: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
    return {
        name: [
            {"valueHex": value_hex, "count": count}
            for value_hex, count in sorted(values.items(), key=lambda item: (-item[1], item[0] or ""))
        ]
        for name, values in sorted((state.get("uniqueWatchValues") or {}).items())
    }


def decoded_pairs_from_state(state: dict[str, Any], key: str) -> list[dict[str, Any]]:
    pairs = []
    for row in unique_watch_rows(state).get(key) or []:
        decoded = coord_poll.unpack_pair_hex(row.get("valueHex"))
        if decoded:
            pairs.append(decoded | {"count": row.get("count"), "valueHex": row.get("valueHex")})
    return pairs


def pair_seen(state: dict[str, Any], key: str, pair: dict[str, int]) -> bool:
    expected = coord_poll.pair_hex(pair["x"], pair["y"])
    return any(row.get("valueHex") == expected for row in unique_watch_rows(state).get(key) or [])


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


def run_candidate(plan: dict[str, Any], roots: list[int], contexts: dict[int, dict], attempt: int) -> dict[str, Any]:
    display = find_free_display()
    env = env_for(DEFAULT_PREFIX, display)
    DEFAULT_PREFIX.mkdir(parents=True, exist_ok=True)
    xvfb = subprocess.Popen(
        ["Xvfb", display, "-screen", "0", "1280x1024x24"],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        errors="replace",
    )
    time.sleep(1)
    wine = subprocess.Popen(
        ["wine", "explorer", "/desktop=hwanse,640x480", "Hwanse2.exe"],
        cwd=ROOT,
        env=env,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        errors="replace",
    )
    time.sleep(STARTUP_WAIT_SECONDS)
    pid = find_hwanse_pid()
    base = loaded_base(pid) if pid else None
    focus = focus_windows(env)
    prelude_rows = run_input_path_prelude(focus.get("windows") or [], env, pid, base) if pid and base else []
    keys = [*plan["pathKeys"], *plan["tailKeys"]]
    row_name = f"{plan['name']}-attempt{attempt}"
    state = init_state(row_name, keys)
    source_result = {"stopped": False, "reason": "not-run", "sample": None}
    path_steps: list[dict[str, Any]] = []
    if pid and base:
        source_result = poll_for(
            state,
            pid=pid,
            base=base,
            duration=SOURCE_WAIT_SECONDS,
            phase="source-wait",
            roots=roots,
            contexts=contexts,
        )
        if not source_result.get("stopped"):
            for key_name in LOAD_NUDGE_KEYS:
                path_steps.append(
                    drive_key(
                        state,
                        key_name=key_name,
                        pid=pid,
                        base=base,
                        roots=roots,
                        contexts=contexts,
                        phase_prefix="load-nudge",
                    )
                )
                if any(event.get("selectorContext", {}).get("selector") == SOURCE_SELECTOR for event in state["events"]):
                    source_result = {"stopped": True, "reason": f"source-seen-after-{key_name}", "sample": None}
                    break
        source_ready = any(source_ready_event(event) for event in state["events"])
        if source_ready:
            for key_name in keys:
                path_steps.append(
                    drive_key(
                        state,
                        key_name=key_name,
                        pid=pid,
                        base=base,
                        roots=roots,
                        contexts=contexts,
                        phase_prefix="exit-path",
                    )
                )
            poll_for(state, pid=pid, base=base, duration=0.5, phase="final", roots=roots, contexts=contexts)
    subprocess.run(["wineserver", "-k"], cwd=ROOT, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5)
    try:
        wine_output, _ = wine.communicate(timeout=5)
    except subprocess.TimeoutExpired:
        wine.kill()
        wine_output, _ = wine.communicate(timeout=5)
    xvfb.terminate()
    try:
        xvfb_output, _ = xvfb.communicate(timeout=5)
    except subprocess.TimeoutExpired:
        xvfb.kill()
        xvfb_output, _ = xvfb.communicate(timeout=5)
    watch_rows = unique_watch_rows(state)
    candidate = plan["candidateTile"]
    outside = plan["outsideTile"]
    candidate_seen = pair_seen(state, "cameraTilePair", candidate)
    outside_seen = pair_seen(state, "cameraTilePair", outside)
    actor_candidate_slots = [
        slot for slot in range(coord_poll.ACTOR_POINTER_SLOT_COUNT) if pair_seen(state, f"actor{slot}TilePair", candidate)
    ]
    trail_candidate_slots = [
        slot for slot in range(coord_poll.ACTOR_HISTORY_SLOT_COUNT) if pair_seen(state, f"trail{slot}TilePair", candidate)
    ]
    actor_outside_slots = [
        slot for slot in range(coord_poll.ACTOR_POINTER_SLOT_COUNT) if pair_seen(state, f"actor{slot}TilePair", outside)
    ]
    trail_outside_slots = [
        slot for slot in range(coord_poll.ACTOR_HISTORY_SLOT_COUNT) if pair_seen(state, f"trail{slot}TilePair", outside)
    ]
    source_ready = any(source_ready_event(event) for event in state["events"])
    return {
        "name": row_name,
        "candidateName": plan["name"],
        "attempt": attempt,
        "display": display,
        "linuxPid": pid,
        "loadedBaseHex": hex32(base),
        "focusedWindows": focus.get("focusedWindows") or [],
        "preludeRowCount": len(prelude_rows),
        "sourceReady": source_ready,
        "sourceResult": {k: v for k, v in source_result.items() if k != "sample"},
        "pathExecuted": source_ready,
        "sampleCount": state["sampleCount"],
        "events": state["events"],
        "eventsTruncated": state["eventsTruncated"],
        "uniqueSelectorContexts": list(state["uniqueSelectorContexts"].values()),
        "uniqueWatchValues": watch_rows,
        "currentRootHitCount": state["currentRootHitCount"],
        "routeSelectorHitCount": state["routeSelectorHitCount"],
        "cameraPairs": decoded_pairs_from_state(state, "cameraTilePair"),
        "candidateTile": candidate,
        "outsideTile": outside,
        "candidateSide": plan["candidateSide"],
        "cameraCandidateObserved": candidate_seen,
        "cameraOutsideObserved": outside_seen,
        "actorCandidateSlots": actor_candidate_slots,
        "trailCandidateSlots": trail_candidate_slots,
        "actorOutsideSlots": actor_outside_slots,
        "trailOutsideSlots": trail_outside_slots,
        "branchStateAllZero": branch_state_all_zero(state),
        "pathStepCount": plan["pathStepCount"],
        "pathRunLengths": plan["pathRunLengths"],
        "tailKeys": plan["tailKeys"],
        "pathStepRows": path_steps[-6:],
        "wineOutput": truncate(wine_output or "", 1500),
        "xvfbOutput": truncate(xvfb_output or "", 1500),
    }


def list_text(values: list[Any] | None) -> str:
    return ",".join(str(value) for value in values or []) or "-"


def build_summary() -> dict[str, Any]:
    runtime_input.ROUTE_WATCH_VALUES = dict(runtime_input.ROUTE_WATCH_VALUES)
    runtime_input.ROUTE_WATCH_VALUES.update(coord_poll.BRANCH_STATE_WATCH_VALUES)
    roots, contexts = load_selector_contexts()
    plans = planned_candidates()
    temporary_saves: list[dict[str, Any]] = []
    directory_info: dict[str, Any] = {"createdDirectory": False}
    cleanup: dict[str, Any] = {}
    case_aliases = prepare_case_aliases(ROOT, True)
    alias_cleanup: dict[str, Any] = {}
    coord_poll.install_coordinate_sampler()
    try:
        temporary_saves, directory_info = prepare_temporary_saves([(1, SOURCE_SAVE)])
        rows = []
        for plan in plans:
            for attempt in range(1, ATTEMPTS_PER_CANDIDATE + 1):
                row = run_candidate(plan, roots, contexts, attempt)
                rows.append(row)
                if row.get("sourceReady"):
                    break
    finally:
        coord_poll.restore_coordinate_sampler()
        cleanup = cleanup_temporary_saves(temporary_saves, directory_info["createdDirectory"])
        alias_cleanup = cleanup_case_aliases(ROOT, case_aliases)
    observed_selectors = []
    for row in rows:
        for context in row.get("uniqueSelectorContexts") or []:
            selector = context.get("selector")
            if selector and selector not in observed_selectors:
                observed_selectors.append(selector)
    any_source_ready = any(row.get("sourceReady") for row in rows)
    any_candidate = any(row.get("cameraCandidateObserved") or row.get("actorCandidateSlots") or row.get("trailCandidateSlots") for row in rows)
    any_outside = any(row.get("cameraOutsideObserved") or row.get("actorOutsideSlots") or row.get("trailOutsideSlots") for row in rows)
    any_route = any(row.get("routeSelectorHitCount") for row in rows)
    any_current = any(row.get("currentRootHitCount") for row in rows)
    if any_route or any_current:
        classification = "route-selector-observed"
    elif any_source_ready and any_outside:
        classification = "source-ready-outside-observed-without-route"
    elif any_source_ready and any_candidate:
        classification = "source-ready-candidate-observed-without-route"
    elif any_source_ready:
        classification = "source-ready-no-exit-coordinate"
    else:
        classification = "source-not-ready"
    return {
        "objective": "adaptive source 0:0 wait before map1_01a exit routeAssist candidates",
        "sourceMap": source_exit.SOURCE_MAP,
        "targetMap": source_exit.TARGET_MAP,
        "sourceSelector": SOURCE_SELECTOR,
        "sourceStartTile": SOURCE_START_TILE,
        "sourceStartOrigin": SOURCE_START_ORIGIN,
        "sourceReadyWatchKey": SOURCE_READY_WATCH_KEY,
        "targetSelector": TARGET_SELECTOR,
        "currentRootHex": hex32(CURRENT_ROOT),
        "sourceSave": str(SOURCE_SAVE.relative_to(ROOT)),
        "temporarySaves": temporary_saves,
        "cleanup": cleanup,
        "caseAliases": case_aliases,
        "caseAliasCleanup": alias_cleanup,
        "startupWaitSeconds": STARTUP_WAIT_SECONDS,
        "pollIntervalSeconds": POLL_INTERVAL_SECONDS,
        "sourceWaitSeconds": SOURCE_WAIT_SECONDS,
        "loadNudgeKeys": LOAD_NUDGE_KEYS,
        "attemptsPerCandidate": ATTEMPTS_PER_CANDIDATE,
        "sequenceCount": len(rows),
        "candidatePlanCount": len(plans),
        "sampleCount": sum(row.get("sampleCount") or 0 for row in rows),
        "observedSelectors": observed_selectors,
        "sourceReadyCount": sum(1 for row in rows if row.get("sourceReady")),
        "candidateObserved": any_candidate,
        "outsideObserved": any_outside,
        "anyReachedRouteSelectorContext": bool(any_route),
        "anyReachedCurrentRoot": bool(any_current),
        "classification": classification,
        "promotionStatus": "blocked" if classification != "route-selector-observed" else "candidate",
        "plans": plans,
        "rows": rows,
        "conclusion": (
            f"Adaptive source-exit poll classification={classification}; sourceReady={any_source_ready}; "
            f"route selector 2:0 reached={bool(any_route)}. This remains non-promoting unless selector 2:0/current "
            "root or a strict source exit hotspot is observed."
        ),
    }


def markdown(summary: dict[str, Any]) -> str:
    lines = [
        "# Runtime Source Exit Adaptive Coordinate Poll",
        "",
        f"- route: `{summary.get('sourceMap')}` -> `{summary.get('targetMap')}`",
        f"- classification: `{summary.get('classification')}`",
        f"- sample/sequence count: {summary.get('sampleCount')} / {summary.get('sequenceCount')}",
        f"- observed selectors: `{list_text(summary.get('observedSelectors'))}`",
        f"- source-ready rows: {summary.get('sourceReadyCount')}",
        f"- candidate/outside observed: {summary.get('candidateObserved')} / {summary.get('outsideObserved')}",
        f"- route/current observed: {summary.get('anyReachedRouteSelectorContext')} / {summary.get('anyReachedCurrentRoot')}",
        f"- promotion status: `{summary.get('promotionStatus')}`",
        "",
        summary.get("conclusion") or "",
        "",
        "| sequence | source ready | path | samples | selectors | camera pairs | candidate/outside | actor/trail cand | route/current | branch state |",
        "| --- | --- | --- | ---: | --- | --- | --- | --- | --- | --- |",
    ]
    for row in summary.get("rows") or []:
        selectors = [item.get("selector") for item in row.get("uniqueSelectorContexts") or [] if item.get("selector")]
        pairs = "; ".join(f"{pair.get('x')},{pair.get('y')}x{pair.get('count')}" for pair in row.get("cameraPairs") or [])
        actor_trail = bool(row.get("actorCandidateSlots") or row.get("trailCandidateSlots"))
        lines.append(
            f"| `{row.get('name')}` | {row.get('sourceReady')} | {row.get('pathStepCount')}+{list_text(row.get('tailKeys'))} | "
            f"{row.get('sampleCount')} | `{list_text(selectors)}` | `{pairs or '-'}` | "
            f"{row.get('cameraCandidateObserved')}/{row.get('cameraOutsideObserved')} | {actor_trail} | "
            f"{row.get('routeSelectorHitCount')}/{row.get('currentRootHitCount')} | "
            f"{'all-zero' if row.get('branchStateAllZero') else 'mixed'} |"
        )
    return "\n".join(lines) + "\n"


def html_page(summary: dict[str, Any]) -> str:
    return (
        "<!doctype html><meta charset=\"utf-8\"><title>Runtime Source Exit Adaptive Coordinate Poll</title>"
        "<style>body{font-family:sans-serif;line-height:1.4}table{border-collapse:collapse}"
        "td,th{border:1px solid #ccc;padding:4px 6px}code{background:#f3f3f3;padding:1px 3px}</style>"
        f"<pre>{html.escape(markdown(summary))}</pre>"
    )


def write_outputs(summary: dict[str, Any]) -> None:
    (OUT / f"{OUTPUT_PREFIX}.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    (OUT / f"{OUTPUT_PREFIX}.html").write_text(html_page(summary), encoding="utf-8")


def main() -> None:
    summary = build_summary()
    write_outputs(summary)
    print(f"wrote adaptive source exit coordinate poll -> {OUT / (OUTPUT_PREFIX + '.html')}")


if __name__ == "__main__":
    main()
