#!/usr/bin/env python3
"""Summarize the source-side runtime exit poll without treating failed save staging as route evidence."""
from __future__ import annotations

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


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
SOURCE_POLL = OUT / "runtime_selected_pointer_source_exit_branch_state_poll.json"
LOAD_CONFIRMED_SOURCE_POLL = OUT / "runtime_selected_pointer_source_exit_branch_state_load_confirmed_poll.json"


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


def candidate_rows(analysis: dict[str, Any]) -> list[dict[str, Any]]:
    return [
        {
            "name": row.get("name"),
            "sampleCount": row.get("sampleCount"),
            "selectors": [
                item.get("selector") if isinstance(item, dict) else str(item)
                for item in row.get("selectors") or []
            ],
            "candidateTile": row.get("candidateTile"),
            "outsideTile": row.get("outsideTile"),
            "candidateSide": row.get("candidateSide"),
            "cameraCandidateObserved": row.get("cameraCandidateObserved"),
            "cameraOutsideObserved": row.get("cameraOutsideObserved"),
            "actorCandidateSlots": row.get("actorCandidateSlots") or [],
            "trailCandidateSlots": row.get("trailCandidateSlots") or [],
            "actorOutsideSlots": row.get("actorOutsideSlots") or [],
            "trailOutsideSlots": row.get("trailOutsideSlots") or [],
            "routeSelectorHitCount": row.get("routeSelectorHitCount"),
            "currentRootHitCount": row.get("currentRootHitCount"),
            "branchStateAllZero": row.get("branchStateAllZero"),
        }
        for row in analysis.get("rows") or []
    ]


def poll_brief(source: str, source_poll: dict[str, Any]) -> dict[str, Any]:
    analysis = source_poll.get("sourceExitAnalysis") or {}
    observed_public = source_poll.get("observedPublicSaveSelectors") or analysis.get("observedPublicSaveSelectors") or []
    observed_selectors = source_poll.get("observedSelectors") or []
    public_selectors = source_poll.get("publicSaveSelectors") or []
    source_save_selector = analysis.get("sourceSaveSelector") or "0:0"
    source_save_observed = source_save_selector in observed_public
    raw_classification = analysis.get("classification")
    rows = candidate_rows(analysis)
    return {
        "source": source,
        "sourceSaveObserved": source_save_observed,
        "observedPublicSaveSelectors": observed_public,
        "publicSaveSelectors": public_selectors,
        "observedSelectors": observed_selectors,
        "sampleCount": source_poll.get("sampleCount"),
        "sequenceCount": source_poll.get("sequenceCount"),
        "sourceExitRawClassification": raw_classification,
        "candidateObserved": bool(analysis.get("anyCandidateObserved")),
        "outsideObserved": bool(analysis.get("anyOutsideObserved")),
        "actorOrTrailCandidateObserved": bool(analysis.get("anyActorOrTrailCandidateObserved")),
        "actorOrTrailOutsideObserved": bool(analysis.get("anyActorOrTrailOutsideObserved")),
        "anyReachedRouteSelectorContext": bool(analysis.get("anyReachedRouteSelectorContext")),
        "anyReachedCurrentRoot": bool(analysis.get("anyReachedCurrentRoot")),
        "candidateRowCount": len(rows),
        "candidateRows": rows,
    }


def unique_ordered(values: list[Any]) -> list[Any]:
    seen = set()
    out = []
    for value in values:
        key = json.dumps(value, sort_keys=True) if isinstance(value, (dict, list)) else value
        if key in seen:
            continue
        seen.add(key)
        out.append(value)
    return out


def build_summary(
    source_poll: dict[str, Any],
    load_confirmed_source_poll: dict[str, Any] | None = None,
) -> dict[str, Any]:
    load_confirmed_source_poll = load_confirmed_source_poll or {}
    raw = poll_brief("runtime_selected_pointer_source_exit_branch_state_poll", source_poll)
    load_confirmed = poll_brief(
        "runtime_selected_pointer_source_exit_branch_state_load_confirmed_poll",
        load_confirmed_source_poll,
    )
    preferred = load_confirmed if load_confirmed.get("sourceSaveObserved") else raw
    source_save_selector = "0:0"
    source_save_observed = bool(raw.get("sourceSaveObserved") or load_confirmed.get("sourceSaveObserved"))
    route_hit = bool(raw.get("anyReachedRouteSelectorContext") or load_confirmed.get("anyReachedRouteSelectorContext"))
    current_hit = bool(raw.get("anyReachedCurrentRoot") or load_confirmed.get("anyReachedCurrentRoot"))
    candidate_observed = bool(raw.get("candidateObserved") or load_confirmed.get("candidateObserved"))
    outside_observed = bool(raw.get("outsideObserved") or load_confirmed.get("outsideObserved"))
    actor_trail_candidate = bool(
        raw.get("actorOrTrailCandidateObserved") or load_confirmed.get("actorOrTrailCandidateObserved")
    )
    actor_trail_outside = bool(
        raw.get("actorOrTrailOutsideObserved") or load_confirmed.get("actorOrTrailOutsideObserved")
    )
    raw_classification = raw.get("sourceExitRawClassification")
    load_confirmed_classification = load_confirmed.get("sourceExitRawClassification")
    if not source_save_observed:
        classification = "source-save-not-observed"
        promotion_status = "blocked"
        conclusion = (
            "The source-side runtime poll staged a public selector 0:0 save, but the original input path did not "
            "observe selector 0:0 at runtime. The poll only observed non-route selector state, so its exit-path "
            "coordinate rows cannot prove a strict map1_01a source hotspot."
        )
    elif route_hit or current_hit:
        classification = "route-observed"
        promotion_status = "candidate"
        conclusion = "A source-side runtime poll observed selector 2:0/current root."
    elif not candidate_observed and not outside_observed:
        classification = "source-save-observed-candidate-not-observed"
        promotion_status = "blocked"
        if raw.get("sourceSaveObserved"):
            conclusion = (
                "The raw source-exit poll and the load-confirmed prefix both observed public selector 0:0. "
                "Even with source-save observation, none of the four map1_01a exit candidate tiles, outside "
                "tiles, selector 2:0, or the current root were observed, so this remains source-load context "
                "rather than strict hotspot proof."
            )
        else:
            conclusion = (
                "The original source-exit poll did not observe the staged source save, but the load-confirmed "
                "prefix did observe public selector 0:0. Even in that load-confirmed run, none of the four "
                "map1_01a exit candidate tiles, outside tiles, selector 2:0, or the current root were observed, "
                "so this remains source-load context rather than strict hotspot proof."
            )
    else:
        classification = load_confirmed_classification or raw_classification or "source-save-observed-exit-nonroute"
        promotion_status = "blocked"
        conclusion = (
            "The source-side runtime poll observed the staged source save, but it remains non-promoting unless "
            "selector 2:0/current root or a strict source hotspot is observed."
        )
    return {
        "source": preferred.get("source"),
        "sourceMap": load_confirmed_source_poll.get("sourceMap") or source_poll.get("sourceMap"),
        "targetMap": load_confirmed_source_poll.get("targetMap") or source_poll.get("targetMap"),
        "sourceSave": load_confirmed_source_poll.get("sourceSave") or source_poll.get("sourceSave"),
        "sourceSaveSelector": source_save_selector,
        "sourceSaveObserved": source_save_observed,
        "observedPublicSaveSelectors": unique_ordered(
            list(raw.get("observedPublicSaveSelectors") or [])
            + list(load_confirmed.get("observedPublicSaveSelectors") or [])
        ),
        "publicSaveSelectors": unique_ordered(
            list(raw.get("publicSaveSelectors") or []) + list(load_confirmed.get("publicSaveSelectors") or [])
        ),
        "observedSelectors": preferred.get("observedSelectors") or [],
        "sampleCount": preferred.get("sampleCount"),
        "sequenceCount": preferred.get("sequenceCount"),
        "sourceExitRawClassification": raw_classification,
        "sourceExitLoadConfirmedClassification": load_confirmed_classification,
        "sourceExitRuntimeClassification": classification,
        "candidateObserved": candidate_observed,
        "outsideObserved": outside_observed,
        "actorOrTrailCandidateObserved": actor_trail_candidate,
        "actorOrTrailOutsideObserved": actor_trail_outside,
        "anyReachedRouteSelectorContext": route_hit,
        "anyReachedCurrentRoot": current_hit,
        "candidateRowCount": preferred.get("candidateRowCount"),
        "candidateRows": preferred.get("candidateRows") or [],
        "rawSourcePoll": raw,
        "loadConfirmedExitPoll": load_confirmed,
        "promotionStatus": promotion_status,
        "strictSourceHotspotProofFound": False,
        "routePromotionEvidenceFound": route_hit or current_hit,
        "conclusion": conclusion,
    }


def markdown(summary: dict[str, Any]) -> str:
    lines = [
        "# Runtime Source Exit Poll Context",
        "",
        f"- route: `{summary.get('sourceMap')}` -> `{summary.get('targetMap')}`",
        f"- source save selector observed: {summary.get('sourceSaveObserved')} (`{summary.get('sourceSaveSelector')}`)",
        f"- public selectors staged/observed: `{list_text(summary.get('publicSaveSelectors'))}` / `{list_text(summary.get('observedPublicSaveSelectors'))}`",
        f"- observed selectors: `{list_text(summary.get('observedSelectors'))}`",
        f"- sample/sequence count: {summary.get('sampleCount')} / {summary.get('sequenceCount')}",
        f"- classification: `{summary.get('sourceExitRuntimeClassification')}`",
        f"- raw source-exit classification: `{summary.get('sourceExitRawClassification')}`",
        f"- load-confirmed classification: `{summary.get('sourceExitLoadConfirmedClassification')}`",
        f"- route/current observed: {summary.get('anyReachedRouteSelectorContext')} / {summary.get('anyReachedCurrentRoot')}",
        f"- promotion status: `{summary.get('promotionStatus')}`",
        "",
        summary.get("conclusion") or "",
        "",
        "## Poll Coverage",
        "",
        "| poll | source observed | samples | sequences | selectors | public observed | candidate/outside | route/current |",
        "| --- | --- | ---: | ---: | --- | --- | --- | --- |",
    ]
    for row in [summary.get("rawSourcePoll") or {}, summary.get("loadConfirmedExitPoll") or {}]:
        lines.append(
            f"| `{row.get('source')}` | {row.get('sourceSaveObserved')} | {row.get('sampleCount')} | "
            f"{row.get('sequenceCount')} | `{list_text(row.get('observedSelectors'))}` | "
            f"`{list_text(row.get('observedPublicSaveSelectors'))}` | "
            f"{row.get('candidateObserved')}/{row.get('outsideObserved')} | "
            f"{row.get('anyReachedRouteSelectorContext')}/{row.get('anyReachedCurrentRoot')} |"
        )
    lines.extend([
        "",
        "| sequence | candidate | samples | selectors | camera cand/out | actor cand/out | trail cand/out | route/current | branch state |",
        "| --- | --- | ---: | --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary.get("candidateRows") or []:
        candidate = row.get("candidateTile") or {}
        outside = row.get("outsideTile") 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')} | `{list_text(row.get('selectors'))}` | "
            f"{row.get('cameraCandidateObserved')}/{row.get('cameraOutsideObserved')} | "
            f"`{list_text(row.get('actorCandidateSlots'))}`/`{list_text(row.get('actorOutsideSlots'))}` | "
            f"`{list_text(row.get('trailCandidateSlots'))}`/`{list_text(row.get('trailOutsideSlots'))}` | "
            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 Poll Context</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], out_dir: Path = OUT, html_out: Path | None = None) -> None:
    (out_dir / "runtime_source_exit_poll_context.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")),
        encoding="utf-8",
    )
    if html_out is not None:
        html_out.parent.mkdir(parents=True, exist_ok=True)
        html_out.write_text(html_page(summary), encoding="utf-8")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--html-out", type=Path)
    parser.add_argument(
        "--allow-missing-source-poll",
        action="store_true",
        help=(
            "Rebuild even when the large raw source poll is absent. By default, "
            "an existing compact summary is preserved so raw-poll evidence is not "
            "accidentally overwritten with an empty source summary."
        ),
    )
    args = parser.parse_args()
    summary_path = OUT / "runtime_source_exit_poll_context.json"
    if not SOURCE_POLL.exists() and summary_path.exists() and not args.allow_missing_source_poll:
        summary = json.loads(summary_path.read_text(encoding="utf-8"))
        if args.html_out is not None:
            args.html_out.parent.mkdir(parents=True, exist_ok=True)
            args.html_out.write_text(html_page(summary), encoding="utf-8")
        print(f"retained existing runtime source exit poll context -> {summary_path}")
        return
    source_poll = json.loads(SOURCE_POLL.read_text(encoding="utf-8")) if SOURCE_POLL.exists() else {}
    load_confirmed_source_poll = (
        json.loads(LOAD_CONFIRMED_SOURCE_POLL.read_text(encoding="utf-8"))
        if LOAD_CONFIRMED_SOURCE_POLL.exists()
        else {}
    )
    summary = build_summary(source_poll, load_confirmed_source_poll)
    write_outputs(summary, OUT, html_out=args.html_out)
    print(f"wrote runtime source exit poll context -> {OUT / 'runtime_source_exit_poll_context.json'}")


if __name__ == "__main__":
    main()
