#!/usr/bin/env python3
"""Contrast confirmed transition hotspot tiles with map1_01a route candidates."""
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 can_stand_at, footprint_checks, in_bounds, side_direction, tile_at


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
CONFIRMED_SOURCE = "map1_02b"
CONFIRMED_TARGET = "map1_01a"
SOURCE = "map1_01a"
TARGET = "map2_02d"
FAILED_TILE_HOTSPOT_PATTERN_GATE_IDS = [
    "strict-event-record",
    "transition-review-row",
    "confirmed-signature-match",
    "tile-hotspot-confirmation",
]
TILE_HOTSPOT_PATTERN_MISSING_EVIDENCE = [
    "strict map1_01a -> map2_02d event record",
    "transition-review row for map1_01a -> map2_02d",
    "confirmed hotspot center-pair or 3x3 signature match for the current candidates",
    "tile hotspot confirmation tied to a source exit",
]
TILE_HOTSPOT_PATTERN_EVIDENCE_REFS = [
    {
        "path": "out/maps.js",
        "description": "decoded tile layers used for confirmed/current hotspot signature comparison",
    },
    {
        "path": "data/transition_reviews.json",
        "description": "manual transition review rows for confirmed and blocked route pairs",
    },
    {
        "path": "out/event_transitions.json",
        "description": "strict event-transition records and points for the compared route pairs",
    },
    {
        "path": "out/original_collision_route_audit.json",
        "description": "current map1_01a geometry exit candidates and target-spawn tile checks",
    },
]


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


def hex_layer(value: int | None) -> str | None:
    if value is None:
        return None
    return f"0x{value:04x}"


def review_rows(reviews: dict, source: str, target: str, state: str | None = None) -> list[dict]:
    rows = [
        row for row in reviews.values()
        if row.get("source") == source and row.get("target") == target
        and (state is None or row.get("state") == state)
    ]
    return sorted(rows, key=lambda row: (row.get("recordVaHex") or "", row.get("x") or -1, row.get("y") or -1))


def event_row(events: list[dict], source: str, target: str) -> dict:
    for row in events:
        if row.get("map") == source and target in (row.get("targets") or []):
            return row
    return {}


def tile_evidence(info: dict, x: int, y: int, dx: int = 0, dy: int = 0) -> dict:
    if not (0 <= x < info["width"] and 0 <= y < info["height"]):
        return {
            "x": x,
            "y": y,
            "inBounds": False,
            "layer0": None,
            "layer1": None,
            "layer1Hex": None,
            "lowNibbleHex": None,
            "originalStandable": False,
            "footprintPassCount": 0,
            "footprintTileCount": 0,
            "edgeDistance": None,
        }
    layer0, layer1 = tile_at(info, x, y)
    footprint = footprint_checks(info, x, y, dx, dy)
    pass_count = sum(1 for row in footprint if row.get("passes") is True)
    return {
        "x": x,
        "y": y,
        "inBounds": True,
        "layer0": layer0,
        "layer1": layer1,
        "layer1Hex": hex_layer(layer1),
        "lowNibbleHex": f"0x{layer1 & 0x0F:01x}",
        "originalStandable": can_stand_at(info, x, y, dx, dy),
        "footprintPassCount": pass_count,
        "footprintTileCount": len(footprint),
        "edgeDistance": min(x, y, info["width"] - 1 - x, info["height"] - 1 - y),
        "footprint": footprint,
    }


def tile_signature(info: dict, x: int, y: int) -> dict:
    if not in_bounds(info, x, y):
        return {
            "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 {
        "centerPairKey": f"{layer0:04x}/{layer1:04x}",
        "centerLowNibbleKey": f"{layer1 & 0x0f:x}",
        "low3x3Key": ",".join(low_values),
        "pair3x3Key": ",".join(pair_values),
    }


def confirmed_rows(map_data: dict, reviews: dict) -> list[dict]:
    info = map_data[CONFIRMED_SOURCE]
    rows = []
    for review in review_rows(reviews, CONFIRMED_SOURCE, CONFIRMED_TARGET):
        x = int(review["x"])
        y = int(review["y"])
        tile = tile_evidence(info, int(review["x"]), int(review["y"]))
        rows.append({
            "source": CONFIRMED_SOURCE,
            "target": CONFIRMED_TARGET,
            "x": review.get("x"),
            "y": review.get("y"),
            "state": review.get("state"),
            "spawnX": review.get("spawnX"),
            "spawnY": review.get("spawnY"),
            "recordVaHex": review.get("recordVaHex"),
            "eventKind": review.get("eventKind"),
            "tile": tile,
            "signature": tile_signature(info, x, y),
        })
    return rows


def current_candidate_rows(map_data: dict, original_collision_route_audit: dict, reviews: dict) -> list[dict]:
    info = map_data[SOURCE]
    route_review_count = len(review_rows(reviews, SOURCE, TARGET))
    rows = []
    for candidate in original_collision_route_audit.get("routeCandidates") or []:
        side = str(candidate.get("side") or "")
        dx, dy = side_direction(side)
        x = int(candidate.get("x"))
        y = int(candidate.get("y"))
        tile = tile_evidence(info, x, y)
        outward_tile = tile_evidence(info, x, y, dx, dy)
        target_spawn = candidate.get("targetSpawnOriginalLayer1Flags") or {}
        rows.append({
            "source": SOURCE,
            "target": TARGET,
            "side": side,
            "x": x,
            "y": y,
            "edgeDistance": candidate.get("edgeDistance"),
            "autoTrigger": candidate.get("autoTrigger"),
            "tileClassStandable": candidate.get("tileClassStandable"),
            "sourceTile": tile,
            "sourceTileSignature": tile_signature(info, x, y),
            "sourceOutwardFootprint": {
                "direction": candidate.get("outwardDirection"),
                "originalStandableForOutwardCheck": outward_tile.get("originalStandable"),
                "outwardFootprintDirectionClear": candidate.get("outwardFootprintDirectionClear"),
                "outwardMoveWouldStayInBounds": candidate.get("outwardMoveWouldStayInBounds"),
                "outwardMoveAllowedInsideMap": candidate.get("outwardMoveAllowedInsideMap"),
            },
            "targetSpawn": {
                "side": target_spawn.get("side"),
                "x": target_spawn.get("x"),
                "y": target_spawn.get("y"),
                "originalStandable": target_spawn.get("originalStandable"),
                "inwardMoveAllowed": target_spawn.get("inwardMoveAllowed"),
                "layer0": target_spawn.get("layer0"),
                "layer1": target_spawn.get("layer1"),
                "lowNibbleHex": target_spawn.get("lowNibbleHex"),
            },
            "strictTransitionReviewRowsForRoute": route_review_count,
            "strictReviewState": "missing",
        })
    return rows


def build_summary(
    map_data: dict | None = None,
    transition_reviews: dict | None = None,
    event_transitions: list[dict] | None = None,
    original_collision_route_audit: dict | None = None,
) -> dict:
    map_data = map_data if map_data is not None else load_maps(OUT / "maps.js")
    transition_reviews = transition_reviews if transition_reviews is not None else load_json(
        ROOT / "data" / "transition_reviews.json",
        {},
    )
    event_transitions = event_transitions if event_transitions is not None else load_json(
        OUT / "event_transitions.json",
        [],
    )
    original_collision_route_audit = original_collision_route_audit if original_collision_route_audit is not None else load_json(
        OUT / "original_collision_route_audit.json",
        {},
    )
    confirmed = confirmed_rows(map_data, transition_reviews)
    current = current_candidate_rows(map_data, original_collision_route_audit, transition_reviews)
    confirmed_event = event_row(event_transitions, CONFIRMED_SOURCE, CONFIRMED_TARGET)
    current_event = event_row(event_transitions, SOURCE, TARGET)
    confirmed_states = {row.get("state") for row in confirmed}
    confirmed_lownibbles = sorted({
        (row.get("tile") or {}).get("lowNibbleHex")
        for row in confirmed
        if row.get("state") == "confirmed"
    })
    confirmed_center_pairs = sorted({
        (row.get("signature") or {}).get("centerPairKey")
        for row in confirmed
        if row.get("state") == "confirmed"
    })
    confirmed_low3x3 = sorted({
        (row.get("signature") or {}).get("low3x3Key")
        for row in confirmed
        if row.get("state") == "confirmed"
    })
    confirmed_pair3x3 = sorted({
        (row.get("signature") or {}).get("pair3x3Key")
        for row in confirmed
        if row.get("state") == "confirmed"
    })
    current_low_match_count = sum(
        1 for row in current
        if (row.get("sourceTile") or {}).get("lowNibbleHex") in confirmed_lownibbles
    )
    current_center_pair_match_count = sum(
        1 for row in current
        if (row.get("sourceTileSignature") or {}).get("centerPairKey") in confirmed_center_pairs
    )
    current_low3x3_match_count = sum(
        1 for row in current
        if (row.get("sourceTileSignature") or {}).get("low3x3Key") in confirmed_low3x3
    )
    current_pair3x3_match_count = sum(
        1 for row in current
        if (row.get("sourceTileSignature") or {}).get("pair3x3Key") in confirmed_pair3x3
    )
    current_original_standable = sum(
        1 for row in current
        if (row.get("sourceTile") or {}).get("originalStandable") is True
    )
    current_target_spawn_standable = sum(
        1 for row in current
        if (row.get("targetSpawn") or {}).get("originalStandable") is True
    )
    return {
        "title": "map1_01a Tile Hotspot Pattern Contrast",
        "confirmedReferenceRoute": f"{CONFIRMED_SOURCE} -> {CONFIRMED_TARGET}",
        "route": f"{SOURCE} -> {TARGET}",
        "source": SOURCE,
        "target": TARGET,
        "proofFound": False,
        "tileHotspotPatternProofFound": False,
        "failedTileHotspotPatternGateIds": FAILED_TILE_HOTSPOT_PATTERN_GATE_IDS,
        "missingEvidence": TILE_HOTSPOT_PATTERN_MISSING_EVIDENCE,
        "evidenceRefs": TILE_HOTSPOT_PATTERN_EVIDENCE_REFS,
        "evidenceRefCount": len(TILE_HOTSPOT_PATTERN_EVIDENCE_REFS),
        "tileHotspotConfirmed": False,
        "strictSourceCoordinateFound": False,
        "promotionStatus": "blocked",
        "confirmedReviewCount": len([row for row in confirmed if row.get("state") == "confirmed"]),
        "confirmedRejectedCount": len([row for row in confirmed if row.get("state") == "rejected"]),
        "confirmedReviewStates": sorted(state for state in confirmed_states if state),
        "confirmedEventRecordHex": confirmed_event.get("recordVaHex"),
        "confirmedEventPointCount": len(confirmed_event.get("points") or []),
        "confirmedActivePointCount": len(confirmed_event.get("activePoints") or []),
        "confirmedRows": confirmed,
        "currentCandidateCount": len(current),
        "currentStrictEventRecordHex": current_event.get("recordVaHex"),
        "currentStrictEventPointCount": len(current_event.get("points") or []),
        "currentStrictTransitionReviewCount": len(review_rows(transition_reviews, SOURCE, TARGET)),
        "currentOriginalStandableCandidateCount": current_original_standable,
        "currentTargetSpawnOriginalStandableCount": current_target_spawn_standable,
        "currentCandidatesMatchingConfirmedLowNibbleCount": current_low_match_count,
        "currentCandidatesMatchingConfirmedCenterPairCount": current_center_pair_match_count,
        "currentCandidatesMatchingConfirmedLow3x3Count": current_low3x3_match_count,
        "currentCandidatesMatchingConfirmedPair3x3Count": current_pair3x3_match_count,
        "currentRows": current,
        "interpretation": {
            "tileSignatureAlonePromotes": False,
            "reason": (
                "The confirmed reference route has an event record and confirmed review rows. The current route has "
                "geometry candidates and passable layer1 signatures, but no strict event record or transition review "
                "row for map1_01a -> map2_02d. Exact center-pair and 3x3 signatures from confirmed hotspot rows "
                "do not match the current route candidates."
            ),
        },
        "conclusion": (
            "The current map1_01a candidates can look tile-compatible under original layer1 flags, and their target "
            "spawns are standable. That does not confirm a hotspot: the exact confirmed hotspot center-pair and "
            "3x3 signatures do not match the current candidates, and the only confirmed pattern is still an event "
            "record plus reviewed source tiles on map1_02b -> map1_01a. For map1_01a -> map2_02d there are zero "
            "strict event rows and zero transition-review rows, so tile hotspot confirmation remains blocked."
        ),
    }


def markdown(summary: dict) -> str:
    lines = [
        "# map1_01a Tile Hotspot Pattern Contrast",
        "",
        f"Route: `{summary['route']}`",
        f"Confirmed reference: `{summary['confirmedReferenceRoute']}`",
        "",
        "## Gate",
        "",
        f"- proofFound: `{summary['proofFound']}`",
        f"- tileHotspotPatternProofFound: `{summary['tileHotspotPatternProofFound']}`",
        f"- tileHotspotConfirmed: `{summary['tileHotspotConfirmed']}`",
        f"- strictSourceCoordinateFound: `{summary['strictSourceCoordinateFound']}`",
        f"- promotionStatus: `{summary['promotionStatus']}`",
        f"- confirmed reviews: `{summary['confirmedReviewCount']}` confirmed / `{summary['confirmedRejectedCount']}` rejected",
        f"- current strict event point count: `{summary['currentStrictEventPointCount']}`",
        f"- current transition review rows: `{summary['currentStrictTransitionReviewCount']}`",
        f"- current original-standable source candidates: `{summary['currentOriginalStandableCandidateCount']}/{summary['currentCandidateCount']}`",
        f"- current standable target spawns: `{summary['currentTargetSpawnOriginalStandableCount']}/{summary['currentCandidateCount']}`",
        f"- current candidates matching confirmed layer1 low nibble: `{summary['currentCandidatesMatchingConfirmedLowNibbleCount']}/{summary['currentCandidateCount']}`",
        f"- current candidates matching confirmed center-pair signature: `{summary['currentCandidatesMatchingConfirmedCenterPairCount']}/{summary['currentCandidateCount']}`",
        f"- current candidates matching confirmed low3x3 signature: `{summary['currentCandidatesMatchingConfirmedLow3x3Count']}/{summary['currentCandidateCount']}`",
        f"- current candidates matching confirmed pair3x3 signature: `{summary['currentCandidatesMatchingConfirmedPair3x3Count']}/{summary['currentCandidateCount']}`",
        "",
        "## Failed Gates",
        "",
    ]
    lines.extend(f"- `{item}`" for item in summary["failedTileHotspotPatternGateIds"])
    lines.extend([
        "",
        "## Missing Evidence",
        "",
    ])
    lines.extend(f"- {item}" for item in summary["missingEvidence"])
    lines.extend([
        "",
        "## Evidence Refs",
        "",
    ])
    lines.extend(
        f"- `{row['path']}`: {row['description']}"
        for row in summary["evidenceRefs"]
    )
    lines.extend([
        "",
        "## Confirmed Reference Rows",
        "",
        "| tile | state | spawn | record | layer0 | layer1 | low | standable | edge |",
        "| --- | --- | --- | --- | ---: | --- | --- | ---: | ---: |",
    ])
    for row in summary["confirmedRows"]:
        tile = row.get("tile") or {}
        spawn = f"{row.get('spawnX')},{row.get('spawnY')}" if row.get("spawnX") is not None else "-"
        lines.append(
            f"| {row.get('x')},{row.get('y')} | {row.get('state')} | {spawn} | `{row.get('recordVaHex')}` | "
            f"{tile.get('layer0')} | `{tile.get('layer1Hex')}` | `{tile.get('lowNibbleHex')}` | "
            f"{tile.get('originalStandable')} | {tile.get('edgeDistance')} |"
        )
    lines.extend([
        "",
        "## Current Candidate Rows",
        "",
        "| side | tile | edge | auto | layer0 | layer1 | center | low | standable | target spawn | review rows |",
        "| --- | --- | ---: | ---: | ---: | --- | --- | --- | ---: | --- | ---: |",
    ])
    for row in summary["currentRows"]:
        tile = row.get("sourceTile") or {}
        sig = row.get("sourceTileSignature") or {}
        target = row.get("targetSpawn") or {}
        lines.append(
            f"| {row.get('side')} | {row.get('x')},{row.get('y')} | {row.get('edgeDistance')} | "
            f"{row.get('autoTrigger')} | {tile.get('layer0')} | `{tile.get('layer1Hex')}` | "
            f"`{sig.get('centerPairKey')}` | `{tile.get('lowNibbleHex')}` | {tile.get('originalStandable')} | "
            f"{target.get('side')} {target.get('x')},{target.get('y')} standable={target.get('originalStandable')} | "
            f"{row.get('strictTransitionReviewRowsForRoute')} |"
        )
    lines.extend([
        "",
        "## Conclusion",
        "",
        summary["conclusion"],
        "",
    ])
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    confirmed_rows = []
    for row in summary["confirmedRows"]:
        tile = row.get("tile") or {}
        spawn = f"{row.get('spawnX')},{row.get('spawnY')}" if row.get("spawnX") is not None else "-"
        confirmed_rows.append(
            f"<tr><td>{row.get('x')},{row.get('y')}</td><td>{html.escape(str(row.get('state')))}</td>"
            f"<td>{html.escape(spawn)}</td><td><code>{html.escape(str(row.get('recordVaHex')))}</code></td>"
            f"<td>{tile.get('layer0')}</td><td><code>{html.escape(str(tile.get('layer1Hex')))}</code></td>"
            f"<td><code>{html.escape(str(tile.get('lowNibbleHex')))}</code></td>"
            f"<td>{tile.get('originalStandable')}</td><td>{tile.get('edgeDistance')}</td></tr>"
        )
    current_rows = []
    for row in summary["currentRows"]:
        tile = row.get("sourceTile") or {}
        sig = row.get("sourceTileSignature") or {}
        target = row.get("targetSpawn") or {}
        target_text = f"{target.get('side')} {target.get('x')},{target.get('y')} standable={target.get('originalStandable')}"
        current_rows.append(
            f"<tr><td>{html.escape(str(row.get('side')))}</td><td>{row.get('x')},{row.get('y')}</td>"
            f"<td>{row.get('edgeDistance')}</td><td>{row.get('autoTrigger')}</td><td>{tile.get('layer0')}</td>"
            f"<td><code>{html.escape(str(tile.get('layer1Hex')))}</code></td>"
            f"<td><code>{html.escape(str(sig.get('centerPairKey')))}</code></td>"
            f"<td><code>{html.escape(str(tile.get('lowNibbleHex')))}</code></td>"
            f"<td>{tile.get('originalStandable')}</td><td>{html.escape(target_text)}</td>"
            f"<td>{row.get('strictTransitionReviewRowsForRoute')}</td></tr>"
        )
    failed_gates = "".join(
        f"<li><code>{html.escape(item)}</code></li>"
        for item in summary["failedTileHotspotPatternGateIds"]
    )
    missing_evidence = "".join(
        f"<li>{html.escape(item)}</li>"
        for item in summary["missingEvidence"]
    )
    evidence_refs = "".join(
        f"<li><code>{html.escape(row['path'])}</code>: {html.escape(row['description'])}</li>"
        for row in summary["evidenceRefs"]
    )
    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 Tile Hotspot Pattern Contrast</title>",
        "  <style>",
        "    :root { color-scheme: dark; font-family: system-ui, sans-serif; background: #101010; color: #eee; }",
        "    body { margin: 0; padding: 24px; }",
        "    h1 { margin: 0 0 8px; font-size: 24px; }",
        "    h2 { margin: 26px 0 10px; font-size: 18px; }",
        "    p { margin: 0 0 14px; color: #bbb; max-width: 1000px; line-height: 1.45; }",
        "    table { width: 100%; border-collapse: collapse; margin: 0 0 16px; font-size: 13px; }",
        "    th, td { border-bottom: 1px solid #303030; padding: 7px 8px; text-align: left; vertical-align: top; }",
        "    th { position: sticky; top: 0; background: #181818; z-index: 1; color: #ddd; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>map1_01a Tile Hotspot Pattern Contrast</h1>",
        f"  <p>Route <code>{html.escape(summary['route'])}</code>; proofFound <code>{summary['proofFound']}</code>; tileHotspotConfirmed <code>{summary['tileHotspotConfirmed']}</code>; promotion <code>{html.escape(summary['promotionStatus'])}</code>.</p>",
        f"  <p>Confirmed-signature matches: low nibble <code>{summary['currentCandidatesMatchingConfirmedLowNibbleCount']}/{summary['currentCandidateCount']}</code>; center-pair <code>{summary['currentCandidatesMatchingConfirmedCenterPairCount']}/{summary['currentCandidateCount']}</code>; low3x3 <code>{summary['currentCandidatesMatchingConfirmedLow3x3Count']}/{summary['currentCandidateCount']}</code>; pair3x3 <code>{summary['currentCandidatesMatchingConfirmedPair3x3Count']}/{summary['currentCandidateCount']}</code>.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <h2>Failed Gates</h2>",
        f"  <ul>{failed_gates}</ul>",
        "  <h2>Missing Evidence</h2>",
        f"  <ul>{missing_evidence}</ul>",
        "  <h2>Evidence Refs</h2>",
        f"  <ul>{evidence_refs}</ul>",
        "  <h2>Confirmed Reference Rows</h2>",
        "  <table><thead><tr><th>tile</th><th>state</th><th>spawn</th><th>record</th><th>layer0</th><th>layer1</th><th>low</th><th>standable</th><th>edge</th></tr></thead><tbody>",
        *confirmed_rows,
        "  </tbody></table>",
        "  <h2>Current Candidate Rows</h2>",
        "  <table><thead><tr><th>side</th><th>tile</th><th>edge</th><th>auto</th><th>layer0</th><th>layer1</th><th>center</th><th>low</th><th>standable</th><th>target spawn</th><th>review rows</th></tr></thead><tbody>",
        *current_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_tile_hotspot_pattern_contrast.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "map1_01a_tile_hotspot_pattern_contrast.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("--transition-reviews", type=Path, default=ROOT / "data" / "transition_reviews.json")
    parser.add_argument("--event-transitions", type=Path, default=OUT / "event_transitions.json")
    parser.add_argument("--original-collision-route-audit", type=Path, default=OUT / "original_collision_route_audit.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        load_maps(args.maps),
        load_json(args.transition_reviews, {}),
        load_json(args.event_transitions, []),
        load_json(args.original_collision_route_audit, {}),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote map1_01a tile hotspot pattern contrast -> {args.out_dir / 'map1_01a_tile_hotspot_pattern_contrast.html'}")


if __name__ == "__main__":
    main()
