#!/usr/bin/env python3
"""Summarize strict event source coverage for the current route blocker."""
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 = "map1_01a"
TARGET = "map2_02d"
CONFIRMED_SOURCE = "map1_02b"
CONFIRMED_TARGET = "map1_01a"


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


def load_maps(path: Path) -> dict:
    if not path.exists():
        return {}
    text = path.read_text(encoding="utf-8")
    prefix = "window.HWANSE_MAPS = "
    if not text.startswith(prefix):
        return {}
    return json.loads(text[len(prefix):].rstrip(";\n"))


def unique(values: list[str]) -> list[str]:
    seen = set()
    result = []
    for value in values:
        if not value or value in seen:
            continue
        result.append(value)
        seen.add(value)
    return result


def event_pairs(event_transitions: list[dict]) -> list[dict]:
    rows = []
    for event in event_transitions:
        source = event.get("map")
        if not source:
            continue
        for target in unique(event.get("targets") or []):
            rows.append({
                "source": source,
                "target": target,
                "recordVaHex": event.get("recordVaHex"),
                "sceneIdHex": event.get("sceneIdHex"),
                "eventKind": event.get("eventKind"),
                "rawPointCount": event.get("rawPointCount"),
                "activePointCount": len(event.get("activePoints") or []),
            })
    return rows


def transition_review_counts(transition_reviews: dict) -> dict[tuple[str, str], dict]:
    counts: dict[tuple[str, str], dict] = {}
    for review in transition_reviews.values():
        source = review.get("source")
        target = review.get("target")
        if not source or not target:
            continue
        row = counts.setdefault((source, target), {"confirmed": 0, "rejected": 0, "other": 0})
        state = review.get("state")
        if state == "confirmed":
            row["confirmed"] += 1
        elif state == "rejected":
            row["rejected"] += 1
        else:
            row["other"] += 1
    return counts


def strict_cluster_roles(field_roots: dict, map_name: str) -> list[dict]:
    rows = []
    for cluster in field_roots.get("clusters") or []:
        if not cluster.get("eventRecordCount"):
            continue
        roles = []
        if map_name in (cluster.get("eventSources") or []):
            roles.append("event-source")
        if map_name in (cluster.get("eventFieldLinks") or []):
            roles.append("event-target-link")
        if map_name in (cluster.get("manifestMaps") or []):
            roles.append("manifest-record")
        if not roles:
            continue
        rows.append({
            "clusterStartHex": cluster.get("clusterStartHex"),
            "clusterEndHex": cluster.get("clusterEndHex"),
            "classification": cluster.get("classification"),
            "roles": roles,
            "manifestMaps": cluster.get("manifestMaps") or [],
            "eventSources": cluster.get("eventSources") or [],
            "eventFieldLinks": cluster.get("eventFieldLinks") or [],
            "eventRecordCount": cluster.get("eventRecordCount"),
        })
    return rows


def build_summary(
    maps: dict,
    event_transitions: list[dict],
    transition_reviews: dict,
    field_roots: dict,
    playable_progress: dict | None = None,
) -> dict:
    playable_progress = playable_progress or {}
    pairs = event_pairs(event_transitions)
    review_counts = transition_review_counts(transition_reviews)
    source_maps = unique([row.get("map") for row in event_transitions if row.get("map")])
    target_maps = unique([pair["target"] for pair in pairs])
    pairs_by_source: dict[str, list[dict]] = {}
    for pair in pairs:
        pairs_by_source.setdefault(pair["source"], []).append(pair)
    source_rows = []
    for source in sorted(source_maps):
        targets = unique([pair["target"] for pair in pairs_by_source.get(source, [])])
        confirmed = sum((review_counts.get((source, target)) or {}).get("confirmed", 0) for target in targets)
        rejected = sum((review_counts.get((source, target)) or {}).get("rejected", 0) for target in targets)
        source_rows.append({
            "source": source,
            "eventRecordCount": sum(1 for row in event_transitions if row.get("map") == source),
            "targetCount": len(targets),
            "targets": targets,
            "confirmedReviewCount": confirmed,
            "rejectedReviewCount": rejected,
        })
    direct_pairs = [pair for pair in pairs if pair["source"] == SOURCE and pair["target"] == TARGET]
    source_events = [row for row in event_transitions if row.get("map") == SOURCE]
    target_incoming = [pair for pair in pairs if pair["target"] == TARGET]
    confirmed_pairs = [pair for pair in pairs if pair["source"] == CONFIRMED_SOURCE and pair["target"] == CONFIRMED_TARGET]
    source_roles = strict_cluster_roles(field_roots, SOURCE)
    target_roles = strict_cluster_roles(field_roots, TARGET)
    reachable = playable_progress.get("reachableFromStart") or []
    reachable_uncovered = [name for name in reachable if name not in source_maps]
    field_map_count = len(maps) or playable_progress.get("mapCount") or 0
    route_status = {
        "source": SOURCE,
        "target": TARGET,
        "sourceIsStrictEventSource": SOURCE in source_maps,
        "targetIsStrictEventTarget": TARGET in target_maps,
        "sourceStrictEventCount": len(source_events),
        "targetStrictIncomingCount": len(target_incoming),
        "directStrictPairCount": len(direct_pairs),
        "sourceStrictClusterRoleCount": len(source_roles),
        "sourceAsEventSourceStrictClusterCount": sum(1 for row in source_roles if "event-source" in row["roles"]),
        "sourceAsEventTargetStrictClusterCount": sum(1 for row in source_roles if "event-target-link" in row["roles"]),
        "sourceAsManifestStrictClusterCount": sum(1 for row in source_roles if "manifest-record" in row["roles"]),
        "targetStrictClusterRoleCount": len(target_roles),
        "promotionStatus": "blocked",
    }
    confirmed_reference = {
        "source": CONFIRMED_SOURCE,
        "target": CONFIRMED_TARGET,
        "directStrictPairCount": len(confirmed_pairs),
        "confirmedReviewCount": (review_counts.get((CONFIRMED_SOURCE, CONFIRMED_TARGET)) or {}).get("confirmed", 0),
        "rejectedReviewCount": (review_counts.get((CONFIRMED_SOURCE, CONFIRMED_TARGET)) or {}).get("rejected", 0),
        "eventRecords": confirmed_pairs,
    }
    return {
        "title": "Strict Event Source Coverage",
        "scope": "global source-map coverage of extracted strict event transition records",
        "fieldMapCount": field_map_count,
        "strictEventRecordCount": len(event_transitions),
        "strictEventSourceMapCount": len(source_maps),
        "strictEventTargetMapCount": len(target_maps),
        "strictEventPairCount": len(pairs),
        "strictEventSourceCoveragePercent": round((len(source_maps) / field_map_count) * 100, 2) if field_map_count else 0,
        "strictEventSources": source_maps,
        "strictEventTargets": target_maps,
        "sourceRows": source_rows,
        "routeStatus": route_status,
        "sourceStrictClusterRoles": source_roles,
        "targetStrictClusterRoles": target_roles,
        "confirmedReference": confirmed_reference,
        "reachableFromStart": reachable,
        "reachableStrictSourceUncovered": reachable_uncovered,
        "promotionAllowed": False,
        "promotionStatus": "blocked",
        "conclusion": (
            "Extracted strict event transitions cover a small set of source maps. "
            "map1_01a is not a strict event source; its strict cluster role is incoming/manifest context from "
            "map1_02b -> map1_01a, while map2_02d is not an extracted strict event target. "
            "Therefore map1_01a -> map2_02d still needs a strict source hotspot, captured selector 2:0 save, "
            "or runtime selected-pointer trace before promotion."
        ),
    }


def markdown(summary: dict) -> str:
    route = summary["routeStatus"]
    confirmed = summary["confirmedReference"]
    lines = [
        "# Strict Event Source Coverage",
        "",
        f"- field maps: {summary.get('fieldMapCount')}",
        f"- strict event records: {summary.get('strictEventRecordCount')}",
        f"- strict event source maps: {summary.get('strictEventSourceMapCount')} ({summary.get('strictEventSourceCoveragePercent')}%)",
        f"- strict event pairs: {summary.get('strictEventPairCount')}",
        f"- route: `{route.get('source')} -> {route.get('target')}`",
        f"- route direct strict pair count: {route.get('directStrictPairCount')}",
        f"- route source strict event count: {route.get('sourceStrictEventCount')}",
        f"- route target strict incoming count: {route.get('targetStrictIncomingCount')}",
        f"- route source as strict source clusters: {route.get('sourceAsEventSourceStrictClusterCount')}",
        f"- route source as strict target-link clusters: {route.get('sourceAsEventTargetStrictClusterCount')}",
        f"- route source as manifest strict clusters: {route.get('sourceAsManifestStrictClusterCount')}",
        f"- promotion: `{summary.get('promotionStatus')}` / allowed `{summary.get('promotionAllowed')}`",
        "",
        summary.get("conclusion") or "",
        "",
        "## Confirmed Reference",
        "",
        f"- route: `{confirmed.get('source')} -> {confirmed.get('target')}`",
        f"- direct strict pair count: {confirmed.get('directStrictPairCount')}",
        f"- confirmed review count: {confirmed.get('confirmedReviewCount')}",
        f"- rejected review count: {confirmed.get('rejectedReviewCount')}",
        "",
        "## Strict Source Maps",
        "",
        "| source | events | targets | confirmed reviews | rejected reviews | target maps |",
        "| --- | ---: | ---: | ---: | ---: | --- |",
    ]
    for row in summary.get("sourceRows") or []:
        lines.append(
            f"| `{row.get('source')}` | {row.get('eventRecordCount')} | {row.get('targetCount')} | "
            f"{row.get('confirmedReviewCount')} | {row.get('rejectedReviewCount')} | "
            f"{', '.join(row.get('targets') or []) or '-'} |"
        )
    lines.extend([
        "",
        "## Current Route Strict Cluster Roles",
        "",
        "| cluster | roles | manifests | event sources | event links |",
        "| --- | --- | --- | --- | --- |",
    ])
    for row in summary.get("sourceStrictClusterRoles") or []:
        lines.append(
            f"| `{row.get('clusterStartHex')}..{row.get('clusterEndHex')}` | "
            f"{', '.join(row.get('roles') or []) or '-'} | "
            f"{', '.join(row.get('manifestMaps') or []) or '-'} | "
            f"{', '.join(row.get('eventSources') or []) or '-'} | "
            f"{', '.join(row.get('eventFieldLinks') or []) or '-'} |"
        )
    if not summary.get("sourceStrictClusterRoles"):
        lines.append("| - | - | - | - | - |")
    lines.extend([
        "",
        "## Reachable Source Coverage",
        "",
        f"- reachable from start: {', '.join(summary.get('reachableFromStart') or []) or '-'}",
        f"- reachable maps without strict event source coverage: {', '.join(summary.get('reachableStrictSourceUncovered') or []) or '-'}",
        "",
    ])
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    def esc(value: Any) -> str:
        return html.escape(str(value))

    source_rows = "\n".join(
        "<tr>"
        f"<td><code>{esc(row.get('source'))}</code></td>"
        f"<td>{esc(row.get('eventRecordCount'))}</td>"
        f"<td>{esc(row.get('targetCount'))}</td>"
        f"<td>{esc(row.get('confirmedReviewCount'))}</td>"
        f"<td>{esc(row.get('rejectedReviewCount'))}</td>"
        f"<td>{esc(', '.join(row.get('targets') or []) or '-')}</td>"
        "</tr>"
        for row in summary.get("sourceRows") or []
    )
    role_rows = "\n".join(
        "<tr>"
        f"<td><code>{esc(row.get('clusterStartHex'))}..{esc(row.get('clusterEndHex'))}</code></td>"
        f"<td>{esc(', '.join(row.get('roles') or []) or '-')}</td>"
        f"<td>{esc(', '.join(row.get('manifestMaps') or []) or '-')}</td>"
        f"<td>{esc(', '.join(row.get('eventSources') or []) or '-')}</td>"
        f"<td>{esc(', '.join(row.get('eventFieldLinks') or []) or '-')}</td>"
        "</tr>"
        for row in summary.get("sourceStrictClusterRoles") or []
    ) or "<tr><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>"
    route = summary.get("routeStatus") or {}
    confirmed = summary.get("confirmedReference") or {}
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Strict Event Source Coverage</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;max-width:1100px;margin:24px auto}table{border-collapse:collapse;width:100%}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}</style>",
        "<h1>Strict Event Source Coverage</h1>",
        "<ul>",
        f"<li>field maps: {esc(summary.get('fieldMapCount'))}</li>",
        f"<li>strict event records: {esc(summary.get('strictEventRecordCount'))}</li>",
        f"<li>strict event source maps: {esc(summary.get('strictEventSourceMapCount'))} ({esc(summary.get('strictEventSourceCoveragePercent'))}%)</li>",
        f"<li>strict event pairs: {esc(summary.get('strictEventPairCount'))}</li>",
        f"<li>route: <code>{esc(route.get('source'))} -&gt; {esc(route.get('target'))}</code></li>",
        f"<li>route direct strict pair count: {esc(route.get('directStrictPairCount'))}</li>",
        f"<li>route source strict event count: {esc(route.get('sourceStrictEventCount'))}</li>",
        f"<li>route target strict incoming count: {esc(route.get('targetStrictIncomingCount'))}</li>",
        f"<li>route source as strict source clusters: {esc(route.get('sourceAsEventSourceStrictClusterCount'))}</li>",
        f"<li>route source as strict target-link clusters: {esc(route.get('sourceAsEventTargetStrictClusterCount'))}</li>",
        f"<li>route source as manifest strict clusters: {esc(route.get('sourceAsManifestStrictClusterCount'))}</li>",
        f"<li>promotion: <code>{esc(summary.get('promotionStatus'))}</code>; allowed {esc(summary.get('promotionAllowed'))}</li>",
        "</ul>",
        f"<p>{esc(summary.get('conclusion') or '')}</p>",
        "<h2>Confirmed Reference</h2>",
        f"<p><code>{esc(confirmed.get('source'))} -&gt; {esc(confirmed.get('target'))}</code>; "
        f"direct strict pairs {esc(confirmed.get('directStrictPairCount'))}; "
        f"confirmed reviews {esc(confirmed.get('confirmedReviewCount'))}; "
        f"rejected reviews {esc(confirmed.get('rejectedReviewCount'))}.</p>",
        "<h2>Strict Source Maps</h2>",
        "<table><thead><tr><th>source</th><th>events</th><th>targets</th><th>confirmed reviews</th><th>rejected reviews</th><th>target maps</th></tr></thead><tbody>",
        source_rows,
        "</tbody></table>",
        "<h2>Current Route Strict Cluster Roles</h2>",
        "<table><thead><tr><th>cluster</th><th>roles</th><th>manifests</th><th>event sources</th><th>event links</th></tr></thead><tbody>",
        role_rows,
        "</tbody></table>",
        "<h2>Reachable Source Coverage</h2>",
        f"<p>Reachable from start: <code>{esc(', '.join(summary.get('reachableFromStart') or []) or '-')}</code></p>",
        f"<p>Reachable maps without strict event source coverage: <code>{esc(', '.join(summary.get('reachableStrictSourceUncovered') or []) or '-')}</code></p>",
    ])


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


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        load_maps(args.out_dir / "maps.js"),
        load_json(args.out_dir / "event_transitions.json", []),
        load_json(args.out_dir / "transition_reviews.json", {}),
        load_json(args.out_dir / "field_map_record_roots.json", {}),
        load_json(args.out_dir / "playable_progress.json", {}),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote strict event source coverage -> {args.out_dir / 'strict_event_source_coverage.html'}")


if __name__ == "__main__":
    main()
