#!/usr/bin/env python3
"""Build a compact route-guide browser smoke summary."""
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_FILE = "route_guide_browser_smoke.json"
SUMMARY_FILE = "route_guide_browser_summary.json"
SUMMARY_HTML = "route_guide_browser_summary.html"


ROUTE_ACTION_FIELDS = {
    "source",
    "sourceMap",
    "targetMap",
    "handled",
    "inputSource",
    "movementQueued",
    "routeBlockerExternalProofInputCount",
    "browserRouteAssistWaypointNudgeImplemented",
    "routeAction",
    "playerTile",
}

ROUTE_ACTION_NESTED = {
    "routeAction": {
        "action",
        "nextTarget",
        "sound",
    },
}

SOUND_FIELDS = {"soundKey", "inputSource", "soundPlayed"}
FOOT_FIELDS = {"x", "y"}
GUIDE_FIELDS = {
    "directionLabel",
    "distanceText",
    "stepBlockerText",
    "stepBlockReasons",
    "routeBlockerMissingEvidenceCount",
    "routeBlockerExternalProofInputCount",
    "routeBlockerExternalProofHandoffStatus",
    "routeBlockerGoalChecklistUrl",
    "routeBlockerStrictHotspotCandidateCount",
    "routeBlockerStrictHotspotRejectedCandidateCount",
    "routeBlockerStrictHotspotRejectionClassification",
    "lineRendered",
    "browserRouteAssistWaypointGuideImplemented",
    "browserRouteAssistWaypointNudgePromptImplemented",
}
WAYPOINT_FIELDS = {
    "sourceMap",
    "targetMap",
    "rendered",
    "browserRouteAssistWaypointImplemented",
    "originalRoutePromotionImplemented",
    "tile",
}
QUICK_FIELDS = {"action"}
RUNTIME_FIELDS = {
    "readyState",
    "trialTransitions",
    "scene",
    "mapName",
    "routeGoal",
    "routeAutoSave",
    "routeWaypointAction",
    "routeWaypoint",
    "routeWaypointGuide",
    "routeWaypointNudge",
    "foot",
    "fieldMovementSound",
    "quickObjective",
    "quickObjectiveSound",
    "routeControlAction",
    "routeControlSound",
}


def load_json(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}


def compact_json_size(payload: Any) -> int:
    return len(json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8"))


def fallback_removed_summary(path: Path, summary_path: Path) -> dict[str, Any] | None:
    if path.exists() or not summary_path.exists():
        return None
    summary = load_json(summary_path)
    if not summary:
        return None
    recorded_size = int(
        summary.get("lastRecordedSourceSizeBytes")
        or summary.get("removedRawSizeBytes")
        or summary.get("sourceSizeBytes")
        or 0
    )
    summary.update(
        {
            "status": "raw-removed-summary-retained",
            "sourceSizeBytes": 0,
            "lastRecordedSourceSizeBytes": recorded_size,
            "removedRawSizeBytes": recorded_size,
            "rawFilePresent": False,
            "rawRegeneration": "Run tools/verify_route_guide_browser.py locally to regenerate the ignored raw smoke when debugging browser regressions.",
            "boundary": (
                "Compact route-guide browser smoke contract. It keeps only the route "
                "guide, waypoint action, nudge, follow, and sound marker fields consumed "
                "by completion audits. The checked-in raw smoke was removed; the verifier "
                "can regenerate it locally as an ignored output."
            ),
        }
    )
    return summary


def pick(data: Any, fields: set[str], nested: dict[str, Any] | None = None) -> dict[str, Any]:
    if not isinstance(data, dict):
        return {}
    nested = nested or {}
    out: dict[str, Any] = {}
    for field in sorted(fields):
        if field not in data:
            continue
        value = data[field]
        if field in nested and isinstance(value, dict):
            out[field] = pick_nested(value, nested[field])
        else:
            out[field] = value
    return out


def pick_nested(data: dict[str, Any], spec: set[str] | dict[str, Any]) -> dict[str, Any]:
    if isinstance(spec, set):
        out = pick(data, spec)
        if "sound" in spec and isinstance(data.get("sound"), dict):
            out["sound"] = pick(data["sound"], SOUND_FIELDS)
        return out
    out: dict[str, Any] = {}
    for key, sub_spec in spec.items():
        if key not in data:
            continue
        value = data[key]
        if isinstance(value, dict):
            out[key] = pick_nested(value, sub_spec)
        else:
            out[key] = value
    return out


def route_action(data: Any) -> dict[str, Any]:
    out = pick(data, ROUTE_ACTION_FIELDS, ROUTE_ACTION_NESTED)
    if isinstance(out.get("routeAction"), dict) and isinstance(out["routeAction"].get("sound"), dict):
        out["routeAction"]["sound"] = pick(out["routeAction"]["sound"], SOUND_FIELDS)
    return out


def runtime(data: Any) -> dict[str, Any]:
    if not isinstance(data, dict):
        return {}
    out = pick(data, RUNTIME_FIELDS)
    if isinstance(data.get("routeWaypointAction"), dict):
        out["routeWaypointAction"] = route_action(data["routeWaypointAction"])
    if isinstance(data.get("routeWaypoint"), dict):
        out["routeWaypoint"] = pick(data["routeWaypoint"], WAYPOINT_FIELDS)
        if isinstance(data["routeWaypoint"].get("tile"), dict):
            out["routeWaypoint"]["tile"] = pick(data["routeWaypoint"]["tile"], FOOT_FIELDS)
    if isinstance(data.get("routeWaypointGuide"), dict):
        out["routeWaypointGuide"] = pick(data["routeWaypointGuide"], GUIDE_FIELDS)
    if isinstance(data.get("routeWaypointNudge"), dict):
        out["routeWaypointNudge"] = route_action(data["routeWaypointNudge"])
    if isinstance(data.get("foot"), dict):
        out["foot"] = pick(data["foot"], FOOT_FIELDS)
    if isinstance(data.get("fieldMovementSound"), dict):
        out["fieldMovementSound"] = pick(data["fieldMovementSound"], SOUND_FIELDS)
    if isinstance(data.get("quickObjective"), dict):
        out["quickObjective"] = pick(data["quickObjective"], QUICK_FIELDS)
    if isinstance(data.get("quickObjectiveSound"), dict):
        out["quickObjectiveSound"] = pick(data["quickObjectiveSound"], SOUND_FIELDS)
    if isinstance(data.get("routeControlAction"), dict):
        out["routeControlAction"] = route_action(data["routeControlAction"])
    if isinstance(data.get("routeControlSound"), dict):
        out["routeControlSound"] = pick(data["routeControlSound"], SOUND_FIELDS)
    return out


def prompt(data: Any, key: str) -> dict[str, Any]:
    value = (data or {}).get(key) if isinstance(data, dict) else {}
    if isinstance(value, dict):
        return {key: pick(value, GUIDE_FIELDS | {"text", "browserRouteAssistWaypointActionPromptImplemented"})}
    return {}


def follow(data: Any) -> dict[str, Any]:
    if not isinstance(data, dict):
        return {}
    arrival = data.get("arrival") if isinstance(data.get("arrival"), dict) else {}
    return {
        "arrived": data.get("arrived"),
        "stepCount": data.get("stepCount"),
        "arrival": {
            "foot": pick(arrival.get("foot"), FOOT_FIELDS),
            "routeWaypointGuide": pick(arrival.get("routeWaypointGuide"), GUIDE_FIELDS),
        },
    }


def build_route_guide(payload: dict[str, Any]) -> dict[str, Any]:
    return {
        "status": payload.get("status"),
        "url": payload.get("url"),
        "sourceFile": f"out/{SOURCE_FILE}",
        "guide": pick(payload.get("guide"), {"routeGoalLinks", "savedatScanLinks"}),
        "runtime": runtime(payload.get("runtime")),
        "waypointPrompt": prompt(payload.get("waypointPrompt"), "prompt"),
        "waypointEnterRuntime": runtime(payload.get("waypointEnterRuntime")),
        "waypointMobileActionRuntime": runtime(payload.get("waypointMobileActionRuntime")),
        "waypointRuntime": runtime(payload.get("waypointRuntime")),
        "waypointGuide": runtime(payload.get("waypointGuide")),
        "waypointNudgePrompt": prompt(payload.get("waypointNudgePrompt"), "actionPrompt"),
        "waypointClickNudgeRuntime": runtime(payload.get("waypointClickNudgeRuntime")),
        "waypointNudgeRuntime": runtime(payload.get("waypointNudgeRuntime")),
        "waypointToolbarNudgeRuntime": runtime(payload.get("waypointToolbarNudgeRuntime")),
        "waypointMobileNudgeRuntime": runtime(payload.get("waypointMobileNudgeRuntime")),
        "waypointFollow": follow(payload.get("waypointFollow")),
        "waypointFollowNextRuntime": runtime(payload.get("waypointFollowNextRuntime")),
        "waypointToolbarFollow": follow(payload.get("waypointToolbarFollow")),
        "waypointToolbarFollowNextRuntime": runtime(payload.get("waypointToolbarFollowNextRuntime")),
        "waypointMobileFollow": follow(payload.get("waypointMobileFollow")),
        "waypointMobileFollowNextRuntime": runtime(payload.get("waypointMobileFollowNextRuntime")),
    }


def build_summary(root: Path = ROOT) -> dict[str, Any]:
    out_dir = root / "out"
    source_path = out_dir / SOURCE_FILE
    fallback = fallback_removed_summary(source_path, out_dir / SUMMARY_FILE)
    if fallback is not None:
        return fallback
    payload = load_json(source_path)
    route_guide = build_route_guide(payload)
    return {
        "kind": "hwanse-route-guide-browser-summary",
        "source": "tools/summarize_route_guide_browser.py",
        "status": "ready",
        "sourceFile": f"out/{SOURCE_FILE}",
        "boundary": (
            "Compact route-guide browser smoke contract. It keeps only the route "
            "guide, waypoint action, nudge, follow, and sound marker fields consumed "
            "by completion audits. The raw route_guide_browser_smoke.json is verifier "
            "output and should not be committed after regeneration."
        ),
        "sourceSizeBytes": source_path.stat().st_size if source_path.exists() else 0,
        "compactSizeBytes": compact_json_size(route_guide),
        "routeGuide": route_guide,
    }


def html_page(summary: dict[str, Any]) -> str:
    route = summary["routeGuide"]
    raw_line = f"{int(summary['sourceSizeBytes']):,} bytes raw"
    if summary.get("lastRecordedSourceSizeBytes"):
        raw_line += f" (removed; last recorded {int(summary['lastRecordedSourceSizeBytes']):,} bytes)"
    rows = []
    for key, value in route.items():
        rows.append(
            "<tr>"
            f"<td><code>{html.escape(key)}</code></td>"
            f"<td>{html.escape(json.dumps(value, ensure_ascii=False, separators=(',', ':')))[:2200]}</td>"
            "</tr>"
        )
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <title>Route Guide Browser Summary</title>
  <style>
    body {{ margin: 24px; color: #202124; font-family: system-ui, sans-serif; line-height: 1.5; }}
    table {{ width: 100%; border-collapse: collapse; margin: 14px 0 28px; }}
    th, td {{ border: 1px solid #d7dce2; padding: 6px 8px; text-align: left; vertical-align: top; }}
    th {{ background: #f4f6f8; }}
    code {{ background: #f4f6f8; padding: 1px 4px; }}
  </style>
</head>
<body>
  <h1>Route Guide Browser Summary</h1>
  <p>{html.escape(summary['boundary'])}</p>
  <p><code>{html.escape(summary['sourceFile'])}</code>: {html.escape(raw_line)}, {int(summary['compactSizeBytes']):,} bytes compact.</p>
  <table>
    <thead><tr><th>field</th><th>compact value</th></tr></thead>
    <tbody>{''.join(rows)}</tbody>
  </table>
</body>
</html>
"""


def write_outputs(summary: dict[str, Any], out_dir: Path = OUT, html_out: Path | None = None) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / SUMMARY_FILE).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("--root", type=Path, default=ROOT)
    parser.add_argument(
        "--html-out",
        type=Path,
        help="Optional local HTML review output. The committed contract is JSON-only.",
    )
    args = parser.parse_args()
    root = args.root.resolve()
    html_out = args.html_out.resolve() if args.html_out else None
    write_outputs(build_summary(root), root / "out", html_out=html_out)


if __name__ == "__main__":
    main()
