#!/usr/bin/env python3
"""Compare map1_01a route candidates against all strict-event tile signatures."""
from __future__ import annotations

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

from summarize_map_tiles import load_maps
from summarize_original_collision_route_audit import in_bounds, tile_at


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
SOURCE = "map1_01a"
TARGET = "map2_02d"
ROUTE = f"{SOURCE} -> {TARGET}"
FAILED_STRICT_EVENT_TILE_SIGNATURE_GATE_IDS = [
    "target-linked-strict-event-signature",
    "direct-source-target-strict-event",
    "tile-hotspot-confirmation",
]
STRICT_EVENT_TILE_SIGNATURE_MISSING_EVIDENCE = [
    "target-linked strict event tile signature for map2_02d",
    "direct map1_01a -> map2_02d strict event signature",
    "tile hotspot confirmation beyond rejected/generic geometry matches",
]
STRICT_EVENT_TILE_SIGNATURE_EVIDENCE_REFS = [
    {"path": "out/maps.js", "fields": ["map1_01a", "map2_02d", "layers"]},
    {"path": "out/event_transitions.json", "fields": ["map", "targets", "events"]},
    {"path": "data/transition_reviews.json", "fields": ["source", "target", "state", "recordVaHex"]},
    {
        "path": "out/map1_01a_tile_hotspot_pattern_contrast.json",
        "fields": ["candidates", "matchesConfirmedLowNibble"],
    },
]


def load_json(path: Path, fallback: Any) -> Any:
    if not path.exists():
        return fallback
    return json.loads(path.read_text(encoding="utf-8"))


def review_key(row: dict) -> tuple[str | None, str | None, str | None, int | None, int | None]:
    return (
        row.get("source"),
        row.get("target"),
        row.get("recordVaHex"),
        row.get("x"),
        row.get("y"),
    )


def review_index(transition_reviews: dict) -> dict[tuple[str | None, str | None, str | None, int | None, int | None], dict]:
    return {review_key(row): row for row in transition_reviews.values()}


def edge_role(info: dict, x: int, y: int) -> str:
    roles = []
    if y == 0:
        roles.append("top")
    if y == info["height"] - 1:
        roles.append("bottom")
    if x == 0:
        roles.append("left")
    if x == info["width"] - 1:
        roles.append("right")
    return "+".join(roles) if roles else "interior"


def tile_signature(info: dict, x: int, y: int) -> dict:
    if not in_bounds(info, x, y):
        return {
            "x": x,
            "y": y,
            "inBounds": False,
            "edgeRole": "out-of-bounds",
            "centerPairKey": "oob",
            "centerLowNibbleKey": "oob",
            "low3x3Key": "oob",
            "pair3x3Key": "oob",
        }
    layer0, layer1 = tile_at(info, x, y)
    low_values = []
    pair_values = []
    for dy in (-1, 0, 1):
        for dx in (-1, 0, 1):
            nx = x + dx
            ny = y + dy
            if not in_bounds(info, nx, ny):
                low_values.append("--")
                pair_values.append("--/--")
                continue
            n0, n1 = tile_at(info, nx, ny)
            low_values.append(f"{n1 & 0x0f:x}")
            pair_values.append(f"{n0:04x}/{n1:04x}")
    return {
        "x": x,
        "y": y,
        "inBounds": True,
        "edgeRole": edge_role(info, x, y),
        "layer0": layer0,
        "layer1": layer1,
        "layer0Hex": f"0x{layer0:04x}",
        "layer1Hex": f"0x{layer1:04x}",
        "lowNibbleHex": f"0x{layer1 & 0x0f:x}",
        "centerPairKey": f"{layer0:04x}/{layer1:04x}",
        "centerLowNibbleKey": f"{layer1 & 0x0f:x}",
        "low3x3Key": ",".join(low_values),
        "pair3x3Key": ",".join(pair_values),
    }


def event_point_rows(map_data: dict, event_transitions: list[dict], transition_reviews: dict) -> list[dict]:
    reviews = review_index(transition_reviews)
    rows = []
    for event in event_transitions:
        source_map = event.get("map")
        if source_map not in map_data:
            continue
        info = map_data[source_map]
        for point in event.get("points") or []:
            x = point.get("x")
            y = point.get("y")
            if not isinstance(x, int) or not isinstance(y, int):
                continue
            targets = event.get("targets") or []
            signature = tile_signature(info, x, y)
            matching_reviews = [
                reviews.get((source_map, target, event.get("recordVaHex"), x, y))
                for target in targets
                if reviews.get((source_map, target, event.get("recordVaHex"), x, y))
            ]
            review_states = sorted({row.get("state") for row in matching_reviews if row})
            rows.append({
                "map": source_map,
                "targets": targets,
                "recordVaHex": event.get("recordVaHex"),
                "x": x,
                "y": y,
                "signature": signature,
                "reviewStates": review_states,
                "hasConfirmedReview": "confirmed" in review_states,
                "hasRejectedReview": "rejected" in review_states,
            })
    return rows


def route_candidate_rows(map_data: dict, tile_hotspot_pattern: dict) -> list[dict]:
    if SOURCE not in map_data:
        return []
    info = map_data[SOURCE]
    target_info = map_data.get(TARGET)
    rows = []
    for row in tile_hotspot_pattern.get("currentRows") or []:
        x = row.get("x")
        y = row.get("y")
        if not isinstance(x, int) or not isinstance(y, int):
            continue
        target_spawn = row.get("targetSpawn") or {}
        target_spawn_x = target_spawn.get("x")
        target_spawn_y = target_spawn.get("y")
        target_spawn_signature = None
        if (
            target_info is not None
            and isinstance(target_spawn_x, int)
            and isinstance(target_spawn_y, int)
        ):
            target_spawn_signature = tile_signature(target_info, target_spawn_x, target_spawn_y)
        rows.append({
            "source": SOURCE,
            "target": TARGET,
            "side": row.get("side"),
            "x": x,
            "y": y,
            "signature": tile_signature(info, x, y),
            "targetSpawn": {
                "side": target_spawn.get("side"),
                "x": target_spawn_x,
                "y": target_spawn_y,
                "originalStandable": target_spawn.get("originalStandable"),
                "inwardMoveAllowed": target_spawn.get("inwardMoveAllowed"),
            },
            "targetSpawnSignature": target_spawn_signature,
            "sourceOriginalStandable": ((row.get("sourceTile") or {}).get("originalStandable")),
            "targetSpawnOriginalStandable": target_spawn.get("originalStandable"),
        })
    return rows


def compact_match(row: dict) -> dict:
    signature = row.get("signature") or {}
    return {
        "map": row.get("map"),
        "targets": row.get("targets") or [],
        "recordVaHex": row.get("recordVaHex"),
        "x": row.get("x"),
        "y": row.get("y"),
        "edgeRole": signature.get("edgeRole"),
        "centerPairKey": signature.get("centerPairKey"),
        "low3x3Key": signature.get("low3x3Key"),
        "reviewStates": row.get("reviewStates") or [],
        "hasConfirmedReview": row.get("hasConfirmedReview") is True,
        "hasRejectedReview": row.get("hasRejectedReview") is True,
    }


def count_owner_pairs(rows: list[dict]) -> list[dict]:
    counts: dict[str, int] = {}
    for row in rows:
        targets = ",".join(row.get("targets") or []) or "-"
        owner = f"{row.get('map')}->{targets}"
        counts[owner] = counts.get(owner, 0) + 1
    return [
        {"owner": owner, "count": count}
        for owner, count in sorted(counts.items(), key=lambda item: (-item[1], item[0]))
    ]


def strict_signature_matches(signature: dict, strict_points: list[dict], key: str) -> list[dict]:
    value = signature.get(key)
    if value in (None, "oob"):
        return []
    return [
        row for row in strict_points
        if (row.get("signature") or {}).get(key) == value
    ]


def candidate_match_summary(candidate: dict, strict_points: list[dict]) -> dict:
    signature = candidate.get("signature") or {}
    target_spawn_signature = candidate.get("targetSpawnSignature") or {}
    center_pair_matches = strict_signature_matches(signature, strict_points, "centerPairKey")
    low3x3_matches = strict_signature_matches(signature, strict_points, "low3x3Key")
    pair3x3_matches = strict_signature_matches(signature, strict_points, "pair3x3Key")
    target_spawn_center_pair_matches = strict_signature_matches(
        target_spawn_signature,
        strict_points,
        "centerPairKey",
    )
    target_spawn_low3x3_matches = strict_signature_matches(
        target_spawn_signature,
        strict_points,
        "low3x3Key",
    )
    target_spawn_pair3x3_matches = strict_signature_matches(
        target_spawn_signature,
        strict_points,
        "pair3x3Key",
    )
    direct_source_target_matches = [
        row for row in strict_points
        if row.get("map") == SOURCE and TARGET in (row.get("targets") or [])
    ]
    target_link_matches = [
        row for row in strict_points
        if TARGET in (row.get("targets") or [])
    ]
    center_pair_same_source_matches = [
        row for row in center_pair_matches
        if row.get("map") == SOURCE
    ]
    center_pair_target_link_matches = [
        row for row in center_pair_matches
        if TARGET in (row.get("targets") or [])
    ]
    center_pair_confirmed_matches = [
        row for row in center_pair_matches
        if row.get("hasConfirmedReview") is True
    ]
    center_pair_rejected_matches = [
        row for row in center_pair_matches
        if row.get("hasRejectedReview") is True
    ]
    confirmed_pair3x3_matches = [row for row in pair3x3_matches if row.get("hasConfirmedReview") is True]
    target_spawn_center_pair_target_map_matches = [
        row for row in target_spawn_center_pair_matches
        if row.get("map") == TARGET
    ]
    target_spawn_low3x3_target_map_matches = [
        row for row in target_spawn_low3x3_matches
        if row.get("map") == TARGET
    ]
    target_spawn_pair3x3_target_map_matches = [
        row for row in target_spawn_pair3x3_matches
        if row.get("map") == TARGET
    ]
    target_spawn_low3x3_target_link_matches = [
        row for row in target_spawn_low3x3_matches
        if TARGET in (row.get("targets") or [])
    ]
    target_spawn_low3x3_confirmed_matches = [
        row for row in target_spawn_low3x3_matches
        if row.get("hasConfirmedReview") is True
    ]
    target_spawn_low3x3_rejected_matches = [
        row for row in target_spawn_low3x3_matches
        if row.get("hasRejectedReview") is True
    ]
    return {
        **candidate,
        "centerPairStrictEventMatchCount": len(center_pair_matches),
        "centerPairSameSourceMatchCount": len(center_pair_same_source_matches),
        "centerPairTargetLinkedMatchCount": len(center_pair_target_link_matches),
        "centerPairConfirmedReviewMatchCount": len(center_pair_confirmed_matches),
        "centerPairRejectedReviewMatchCount": len(center_pair_rejected_matches),
        "centerPairAllMatchesRejectedReview": (
            bool(center_pair_matches)
            and len(center_pair_rejected_matches) == len(center_pair_matches)
        ),
        "centerPairOwnerPairs": count_owner_pairs(center_pair_matches),
        "low3x3StrictEventMatchCount": len(low3x3_matches),
        "pair3x3StrictEventMatchCount": len(pair3x3_matches),
        "confirmedReviewPair3x3MatchCount": len(confirmed_pair3x3_matches),
        "targetSpawnCenterPairStrictEventMatchCount": len(target_spawn_center_pair_matches),
        "targetSpawnLow3x3StrictEventMatchCount": len(target_spawn_low3x3_matches),
        "targetSpawnPair3x3StrictEventMatchCount": len(target_spawn_pair3x3_matches),
        "targetSpawnCenterPairTargetMapMatchCount": len(target_spawn_center_pair_target_map_matches),
        "targetSpawnLow3x3TargetMapMatchCount": len(target_spawn_low3x3_target_map_matches),
        "targetSpawnPair3x3TargetMapMatchCount": len(target_spawn_pair3x3_target_map_matches),
        "targetSpawnLow3x3TargetLinkedMatchCount": len(target_spawn_low3x3_target_link_matches),
        "targetSpawnLow3x3ConfirmedReviewMatchCount": len(target_spawn_low3x3_confirmed_matches),
        "targetSpawnLow3x3RejectedReviewMatchCount": len(target_spawn_low3x3_rejected_matches),
        "targetSpawnLow3x3OwnerPairs": count_owner_pairs(target_spawn_low3x3_matches),
        "targetLinkedStrictEventMatchCount": len(target_link_matches),
        "directSourceTargetStrictEventMatchCount": len(direct_source_target_matches),
        "sampleCenterPairMatches": [compact_match(row) for row in center_pair_matches[:6]],
        "sampleLow3x3Matches": [compact_match(row) for row in low3x3_matches[:6]],
        "samplePair3x3Matches": [compact_match(row) for row in pair3x3_matches[:6]],
        "sampleTargetSpawnCenterPairMatches": [
            compact_match(row) for row in target_spawn_center_pair_matches[:6]
        ],
        "sampleTargetSpawnLow3x3Matches": [
            compact_match(row) for row in target_spawn_low3x3_matches[:6]
        ],
        "sampleTargetSpawnPair3x3Matches": [
            compact_match(row) for row in target_spawn_pair3x3_matches[:6]
        ],
        "tileSignaturePromotes": False,
        "promotionStatus": "blocked",
    }


def build_summary(
    map_data: dict | None = None,
    event_transitions: list[dict] | None = None,
    transition_reviews: dict | None = None,
    tile_hotspot_pattern: dict | None = None,
) -> dict:
    map_data = map_data if map_data is not None else load_maps(OUT / "maps.js")
    event_transitions = event_transitions if event_transitions is not None else load_json(
        OUT / "event_transitions.json",
        [],
    )
    transition_reviews = transition_reviews if transition_reviews is not None else load_json(
        ROOT / "data" / "transition_reviews.json",
        {},
    )
    tile_hotspot_pattern = tile_hotspot_pattern if tile_hotspot_pattern is not None else load_json(
        OUT / "map1_01a_tile_hotspot_pattern_contrast.json",
        {},
    )
    strict_points = event_point_rows(map_data, event_transitions, transition_reviews)
    target_map_strict_points = [row for row in strict_points if row.get("map") == TARGET]
    candidates = [
        candidate_match_summary(row, strict_points)
        for row in route_candidate_rows(map_data, tile_hotspot_pattern)
    ]
    strict_event_target_record_count = sum(1 for row in event_transitions if TARGET in (row.get("targets") or []))
    direct_source_target_record_count = sum(
        1 for row in event_transitions
        if row.get("map") == SOURCE and TARGET in (row.get("targets") or [])
    )
    reviewed_strict_point_count = sum(1 for row in strict_points if row.get("reviewStates"))
    confirmed_strict_point_count = sum(1 for row in strict_points if row.get("hasConfirmedReview") is True)
    rejected_strict_point_count = sum(1 for row in strict_points if row.get("hasRejectedReview") is True)
    all_direct_matches_zero = all(row["directSourceTargetStrictEventMatchCount"] == 0 for row in candidates)
    all_target_matches_zero = all(row["targetLinkedStrictEventMatchCount"] == 0 for row in candidates)
    all_confirmed_pair3x3_zero = all(row["confirmedReviewPair3x3MatchCount"] == 0 for row in candidates)
    center_pair_match_count = sum(row["centerPairStrictEventMatchCount"] for row in candidates)
    center_pair_same_source_match_count = sum(row["centerPairSameSourceMatchCount"] for row in candidates)
    center_pair_target_linked_match_count = sum(row["centerPairTargetLinkedMatchCount"] for row in candidates)
    center_pair_confirmed_review_match_count = sum(row["centerPairConfirmedReviewMatchCount"] for row in candidates)
    center_pair_rejected_review_match_count = sum(row["centerPairRejectedReviewMatchCount"] for row in candidates)
    center_pair_owner_counts: dict[str, int] = {}
    for row in candidates:
        for owner_row in row.get("centerPairOwnerPairs") or []:
            owner = owner_row.get("owner")
            if owner:
                center_pair_owner_counts[owner] = center_pair_owner_counts.get(owner, 0) + int(owner_row.get("count") or 0)
    center_pair_owner_pairs = [
        {"owner": owner, "count": count}
        for owner, count in sorted(center_pair_owner_counts.items(), key=lambda item: (-item[1], item[0]))
    ]
    target_spawn_center_pair_match_count = sum(
        row["targetSpawnCenterPairStrictEventMatchCount"] for row in candidates
    )
    target_spawn_low3x3_match_count = sum(
        row["targetSpawnLow3x3StrictEventMatchCount"] for row in candidates
    )
    target_spawn_pair3x3_match_count = sum(
        row["targetSpawnPair3x3StrictEventMatchCount"] for row in candidates
    )
    target_spawn_center_pair_target_map_match_count = sum(
        row["targetSpawnCenterPairTargetMapMatchCount"] for row in candidates
    )
    target_spawn_low3x3_target_map_match_count = sum(
        row["targetSpawnLow3x3TargetMapMatchCount"] for row in candidates
    )
    target_spawn_pair3x3_target_map_match_count = sum(
        row["targetSpawnPair3x3TargetMapMatchCount"] for row in candidates
    )
    target_spawn_low3x3_target_linked_match_count = sum(
        row["targetSpawnLow3x3TargetLinkedMatchCount"] for row in candidates
    )
    target_spawn_low3x3_confirmed_review_match_count = sum(
        row["targetSpawnLow3x3ConfirmedReviewMatchCount"] for row in candidates
    )
    target_spawn_low3x3_rejected_review_match_count = sum(
        row["targetSpawnLow3x3RejectedReviewMatchCount"] for row in candidates
    )
    target_spawn_low3x3_owner_counts: dict[str, int] = {}
    for row in candidates:
        for owner_row in row.get("targetSpawnLow3x3OwnerPairs") or []:
            owner = owner_row.get("owner")
            if owner:
                target_spawn_low3x3_owner_counts[owner] = (
                    target_spawn_low3x3_owner_counts.get(owner, 0)
                    + int(owner_row.get("count") or 0)
                )
    target_spawn_low3x3_owner_pairs = [
        {"owner": owner, "count": count}
        for owner, count in sorted(
            target_spawn_low3x3_owner_counts.items(),
            key=lambda item: (-item[1], item[0]),
        )
    ]
    target_spawn_low3x3_generic_only = (
        target_spawn_low3x3_match_count > 0
        and target_spawn_low3x3_target_linked_match_count == 0
        and target_spawn_low3x3_target_map_match_count == 0
        and target_spawn_center_pair_match_count == 0
        and target_spawn_pair3x3_match_count == 0
    )
    return {
        "title": "map1_01a Strict Event Tile Signature Scan",
        "source": SOURCE,
        "target": TARGET,
        "route": ROUTE,
        "promotionStatus": "blocked",
        "proofFound": False,
        "strictEventTileSignatureProofFound": False,
        "failedStrictEventTileSignatureGateIds": FAILED_STRICT_EVENT_TILE_SIGNATURE_GATE_IDS,
        "missingEvidence": STRICT_EVENT_TILE_SIGNATURE_MISSING_EVIDENCE,
        "evidenceRefs": STRICT_EVENT_TILE_SIGNATURE_EVIDENCE_REFS,
        "evidenceRefCount": len(STRICT_EVENT_TILE_SIGNATURE_EVIDENCE_REFS),
        "tileHotspotConfirmed": False,
        "strictSourceCoordinateFound": False,
        "tileSignaturePromotes": False,
        "strictEventRecordCount": len(event_transitions),
        "strictEventPointCount": len(strict_points),
        "reviewedStrictEventPointCount": reviewed_strict_point_count,
        "confirmedStrictEventPointCount": confirmed_strict_point_count,
        "rejectedStrictEventPointCount": rejected_strict_point_count,
        "targetLinkedStrictEventRecordCount": strict_event_target_record_count,
        "directSourceTargetStrictEventRecordCount": direct_source_target_record_count,
        "targetSpawnStrictEventPointCount": len(target_map_strict_points),
        "targetSpawnTargetMapStrictEventPointCount": len(target_map_strict_points),
        "candidateCount": len(candidates),
        "candidatesWithCenterPairMatchCount": sum(
            1 for row in candidates if row["centerPairStrictEventMatchCount"] > 0
        ),
        "centerPairMatchCount": center_pair_match_count,
        "centerPairSameSourceMatchCount": center_pair_same_source_match_count,
        "centerPairTargetLinkedMatchCount": center_pair_target_linked_match_count,
        "centerPairConfirmedReviewMatchCount": center_pair_confirmed_review_match_count,
        "centerPairRejectedReviewMatchCount": center_pair_rejected_review_match_count,
        "allCenterPairMatchesRejectedReview": (
            center_pair_match_count > 0
            and center_pair_rejected_review_match_count == center_pair_match_count
        ),
        "centerPairOwnerPairs": center_pair_owner_pairs,
        "candidatesWithLow3x3MatchCount": sum(
            1 for row in candidates if row["low3x3StrictEventMatchCount"] > 0
        ),
        "candidatesWithPair3x3MatchCount": sum(
            1 for row in candidates if row["pair3x3StrictEventMatchCount"] > 0
        ),
        "candidatesWithTargetSpawnCenterPairMatchCount": sum(
            1 for row in candidates if row["targetSpawnCenterPairStrictEventMatchCount"] > 0
        ),
        "candidatesWithTargetSpawnLow3x3MatchCount": sum(
            1 for row in candidates if row["targetSpawnLow3x3StrictEventMatchCount"] > 0
        ),
        "candidatesWithTargetSpawnPair3x3MatchCount": sum(
            1 for row in candidates if row["targetSpawnPair3x3StrictEventMatchCount"] > 0
        ),
        "targetSpawnCenterPairStrictEventMatchCount": target_spawn_center_pair_match_count,
        "targetSpawnLow3x3StrictEventMatchCount": target_spawn_low3x3_match_count,
        "targetSpawnPair3x3StrictEventMatchCount": target_spawn_pair3x3_match_count,
        "targetSpawnCenterPairTargetMapMatchCount": target_spawn_center_pair_target_map_match_count,
        "targetSpawnLow3x3TargetMapMatchCount": target_spawn_low3x3_target_map_match_count,
        "targetSpawnPair3x3TargetMapMatchCount": target_spawn_pair3x3_target_map_match_count,
        "targetSpawnLow3x3TargetLinkedMatchCount": target_spawn_low3x3_target_linked_match_count,
        "targetSpawnLow3x3ConfirmedReviewMatchCount": target_spawn_low3x3_confirmed_review_match_count,
        "targetSpawnLow3x3RejectedReviewMatchCount": target_spawn_low3x3_rejected_review_match_count,
        "targetSpawnLow3x3OwnerPairs": target_spawn_low3x3_owner_pairs,
        "targetSpawnLow3x3GenericOnly": target_spawn_low3x3_generic_only,
        "allTargetSpawnCenterPairMatchesZero": all(
            row["targetSpawnCenterPairStrictEventMatchCount"] == 0 for row in candidates
        ),
        "allTargetSpawnPair3x3MatchesZero": all(
            row["targetSpawnPair3x3StrictEventMatchCount"] == 0 for row in candidates
        ),
        "allTargetSpawnTargetMapStrictEventsZero": (
            len(target_map_strict_points) == 0
            and target_spawn_center_pair_target_map_match_count == 0
            and target_spawn_low3x3_target_map_match_count == 0
            and target_spawn_pair3x3_target_map_match_count == 0
        ),
        "allDirectSourceTargetStrictEventMatchesZero": all_direct_matches_zero,
        "allTargetLinkedStrictEventMatchesZero": all_target_matches_zero,
        "allConfirmedReviewPair3x3MatchesZero": all_confirmed_pair3x3_zero,
        "candidateRows": candidates,
        "interpretation": {
            "tileSignatureAlonePromotes": False,
            "centerPairMatchesPromote": False,
            "reason": (
                "Strict-event tile signatures are diagnostic unless a row is linked to the same source and target. "
                "The center-pair matches are rejected review rows owned by other source-target rows, no extracted "
                "strict event targets map2_02d, no map1_01a strict event points link to map2_02d, and target-spawn "
                "tile signatures have no center/pair3x3 match or target-map strict-event point."
            ),
        },
        "conclusion": (
            "The global strict-event tile signature scan does not confirm a map1_01a -> map2_02d hotspot. "
            "Current route candidates may share center or neighborhood tile signatures with strict-event points, "
            "but the matching center-pair signatures are rejected review rows owned by other source-target rows, "
            "none of those strict events targets map2_02d, and none is a direct map1_01a source row. Target-spawn "
            "tiles add only generic low3x3 overlaps, with no center/pair3x3 match and no strict-event points on "
            "map2_02d. "
            "Therefore tile signature evidence remains diagnostic-only and tile hotspot confirmation is still blocked."
        ),
    }


def html_page(summary: dict) -> str:
    center_owner_text = ", ".join(
        f"{row.get('owner')}:{row.get('count')}"
        for row in summary.get("centerPairOwnerPairs") or []
    ) or "-"
    target_spawn_low3x3_owner_text = ", ".join(
        f"{row.get('owner')}:{row.get('count')}"
        for row in summary.get("targetSpawnLow3x3OwnerPairs") or []
    ) or "-"
    rows = []
    for row in summary["candidateRows"]:
        sig = row.get("signature") or {}
        target_spawn = row.get("targetSpawn") or {}
        rows.append(
            "<tr>"
            f"<td>{html.escape(str(row.get('side')))}</td>"
            f"<td><code>{row.get('x')},{row.get('y')}</code></td>"
            f"<td><code>{target_spawn.get('x')},{target_spawn.get('y')}</code></td>"
            f"<td>{html.escape(str(sig.get('edgeRole')))}</td>"
            f"<td><code>{html.escape(str(sig.get('centerPairKey')))}</code></td>"
            f"<td><code>{html.escape(str(sig.get('centerLowNibbleKey')))}</code></td>"
            f"<td>{row.get('centerPairStrictEventMatchCount')}</td>"
            f"<td>{row.get('centerPairSameSourceMatchCount')}</td>"
            f"<td>{row.get('centerPairTargetLinkedMatchCount')}</td>"
            f"<td>{row.get('centerPairConfirmedReviewMatchCount')}</td>"
            f"<td>{row.get('centerPairRejectedReviewMatchCount')}</td>"
            f"<td>{row.get('low3x3StrictEventMatchCount')}</td>"
            f"<td>{row.get('pair3x3StrictEventMatchCount')}</td>"
            f"<td>{row.get('targetSpawnCenterPairStrictEventMatchCount')}/"
            f"{row.get('targetSpawnLow3x3StrictEventMatchCount')}/"
            f"{row.get('targetSpawnPair3x3StrictEventMatchCount')}</td>"
            f"<td>{row.get('targetSpawnCenterPairTargetMapMatchCount')}/"
            f"{row.get('targetSpawnLow3x3TargetMapMatchCount')}/"
            f"{row.get('targetSpawnPair3x3TargetMapMatchCount')}</td>"
            f"<td>{row.get('targetLinkedStrictEventMatchCount')}</td>"
            f"<td>{row.get('directSourceTargetStrictEventMatchCount')}</td>"
            f"<td>{row.get('confirmedReviewPair3x3MatchCount')}</td>"
            f"<td>{html.escape(str(row.get('promotionStatus')))}</td>"
            "</tr>"
        )
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>map1_01a Strict Event Tile Signature Scan</title>",
        "  <style>",
        "    :root { color-scheme: dark; font-family: system-ui, sans-serif; background:#101010; color:#eee; }",
        "    body { margin: 0; padding: 24px; }",
        "    table { width: 100%; border-collapse: collapse; font-size: 13px; }",
        "    th, td { border-bottom: 1px solid #303030; padding: 7px 8px; text-align: left; vertical-align: top; }",
        "    th { background:#181818; position: sticky; top: 0; }",
        "    p { color:#bbb; max-width: 1100px; line-height: 1.45; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>map1_01a Strict Event Tile Signature Scan</h1>",
        f"  <p>Route <code>{html.escape(summary['route'])}</code>; promotion status <code>{summary['promotionStatus']}</code>; tileHotspotConfirmed <code>{summary['tileHotspotConfirmed']}</code>.</p>",
        f"  <p>proof found <code>{summary['proofFound']}</code>; failed strict event tile signature gates <code>{html.escape(', '.join(summary['failedStrictEventTileSignatureGateIds']))}</code>; missing evidence count <code>{len(summary['missingEvidence'])}</code>; evidence refs <code>{summary.get('evidenceRefCount')}</code>.</p>",
        f"  <p>Strict event records/points <code>{summary['strictEventRecordCount']}/{summary['strictEventPointCount']}</code>; target-linked strict event records <code>{summary['targetLinkedStrictEventRecordCount']}</code>; direct source-target strict event records <code>{summary['directSourceTargetStrictEventRecordCount']}</code>.</p>",
        f"  <p>candidates with center/low3x3/pair3x3 matches <code>{summary['candidatesWithCenterPairMatchCount']}/{summary['candidatesWithLow3x3MatchCount']}/{summary['candidatesWithPair3x3MatchCount']}</code>; center-pair ownership total/same-source/target-linked/confirmed-review/rejected-review <code>{summary['centerPairMatchCount']}/{summary['centerPairSameSourceMatchCount']}/{summary['centerPairTargetLinkedMatchCount']}/{summary['centerPairConfirmedReviewMatchCount']}/{summary['centerPairRejectedReviewMatchCount']}</code>; center-pair owners <code>{html.escape(center_owner_text)}</code>; all center-pair matches rejected-review only <code>{summary['allCenterPairMatchesRejectedReview']}</code>; all target-linked matches zero <code>{summary['allTargetLinkedStrictEventMatchesZero']}</code>; all direct source-target matches zero <code>{summary['allDirectSourceTargetStrictEventMatchesZero']}</code>.</p>",
        f"  <p>target-spawn target-map strict event points <code>{summary['targetSpawnTargetMapStrictEventPointCount']}</code>; target-spawn strict-event center/low3x3/pair3x3 matches <code>{summary['targetSpawnCenterPairStrictEventMatchCount']}/{summary['targetSpawnLow3x3StrictEventMatchCount']}/{summary['targetSpawnPair3x3StrictEventMatchCount']}</code>; target-spawn target-map center/low3x3/pair3x3 matches <code>{summary['targetSpawnCenterPairTargetMapMatchCount']}/{summary['targetSpawnLow3x3TargetMapMatchCount']}/{summary['targetSpawnPair3x3TargetMapMatchCount']}</code>; all target-spawn center/pair3x3/target-map matches zero <code>{summary['allTargetSpawnCenterPairMatchesZero']}/{summary['allTargetSpawnPair3x3MatchesZero']}/{summary['allTargetSpawnTargetMapStrictEventsZero']}</code>.</p>",
        f"  <p>target-spawn low3x3 target-linked/confirmed/rejected matches <code>{summary['targetSpawnLow3x3TargetLinkedMatchCount']}/{summary['targetSpawnLow3x3ConfirmedReviewMatchCount']}/{summary['targetSpawnLow3x3RejectedReviewMatchCount']}</code>; owners <code>{html.escape(target_spawn_low3x3_owner_text)}</code>; generic-only <code>{summary['targetSpawnLow3x3GenericOnly']}</code>.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <h2>Missing Evidence</h2>",
        "  <ul>",
        *(f"    <li>{html.escape(item)}</li>" for item in summary["missingEvidence"]),
        "  </ul>",
        "  <h2>Candidate Rows</h2>",
        "  <table><thead><tr><th>side</th><th>tile</th><th>target spawn</th><th>edge</th><th>center</th><th>low</th><th>center matches</th><th>same source</th><th>target-linked center</th><th>confirmed center</th><th>rejected center</th><th>low3x3</th><th>pair3x3</th><th>target-spawn center/low3x3/pair3x3</th><th>target-spawn target-map</th><th>target-linked</th><th>direct</th><th>confirmed pair3x3</th><th>status</th></tr></thead><tbody>",
        *rows,
        "  </tbody></table>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "map1_01a_strict_event_tile_signature_scan.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "map1_01a_strict_event_tile_signature_scan.html").write_text(
        html_page(summary),
        encoding="utf-8",
    )


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--maps", type=Path, default=OUT / "maps.js")
    parser.add_argument("--event-transitions", type=Path, default=OUT / "event_transitions.json")
    parser.add_argument("--transition-reviews", type=Path, default=ROOT / "data" / "transition_reviews.json")
    parser.add_argument("--tile-hotspot-pattern", type=Path, default=OUT / "map1_01a_tile_hotspot_pattern_contrast.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        load_maps(args.maps),
        load_json(args.event_transitions, []),
        load_json(args.transition_reviews, {}),
        load_json(args.tile_hotspot_pattern, {}),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote strict event tile signature scan -> {args.out_dir / 'map1_01a_strict_event_tile_signature_scan.html'}")


if __name__ == "__main__":
    main()
