#!/usr/bin/env python3
"""Build a compact title-start 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 = "title_start_browser_smoke.json"
SUMMARY_FILE = "title_start_browser_summary.json"
SUMMARY_HTML = "title_start_browser_summary.html"

DROP_KEYS = {
    "armed",
    "beforeSnapshot",
    "commandItems",
    "directPixelSamples",
    "href",
    "marker",  # Large route marker snapshots; compact fields are copied below.
    "payloadBeforeMutation",
    "plannedMovement",
    "progressDetails",
    "routePathOptions",
    "savedPayload",
    "startSnapshot",
    "victoryAutoSave",
    "victoryResult",
    "victorySummary",
}

FOOT_FIELDS = {"x", "y"}
TITLE_STATE_FIELDS = {
    "bootError",
    "hasScreen",
    "mapName",
    "playHudLines",
    "quickLoadHidden",
    "quickLoadText",
    "readyState",
    "routeGuideHidden",
    "scene",
    "search",
    "selectedTitleMenuIndex",
    "tileReviewHidden",
    "titleLoaded",
    "titleMenuKeys",
    "titleMenuLabels",
    "titleMenuMode",
    "titleMenuRender",
    "titleRouteGoal",
    "titleSavedatScan",
}
RUNTIME_STATE_FIELDS = {
    "atahoHp",
    "atahoHpMax",
    "fieldEncounterMenuLabel",
    "fieldEncounterMode",
    "fieldEncounterStep",
    "hasLoadedSaveSummary",
    "hasRuntimeState",
    "hasScreen",
    "herbCount",
    "inputCode",
    "loaded",
    "mapName",
    "movementInput",
    "originalEncounterRuntimeImplemented",
    "originalEventVmRuntimeImplemented",
    "originalPartyJoinEventImplemented",
    "originalRoutePromotionImplemented",
    "originalSavedataParserAvailable",
    "originalStoryFlagRuntimeImplemented",
    "pending",
    "prototypeProgress",
    "publicSaveValue",
    "quickLoadHidden",
    "quickLoadText",
    "readyState",
    "routeAssistAutoSave",
    "routeGuideHidden",
    "routeNextHidden",
    "routeNextText",
    "routeNextTitle",
    "routePathHidden",
    "routePathValue",
    "routeState",
    "runtimeCharacters",
    "runtimeItems",
    "runtimeMoney",
    "saveHudLines",
    "saved",
    "scene",
    "selectedRouteGoal",
    "storyFlags",
    "trialTransitions",
}
MARKER_FIELDS = {
    "activeId",
    "action",
    "active",
    "autoSaved",
    "autoSavedMap",
    "autoSavedRouteGoal",
    "blockId",
    "blockReasonCount",
    "browserFieldEncounterFeedbackImplemented",
    "browserInventoryItemFeedbackImplemented",
    "browserMapTransitionFeedbackImplemented",
    "browserRuntimeSaveFeedbackImplemented",
    "completion",
    "counts",
    "countAfter",
    "countBefore",
    "enabled",
    "externalProofInputId",
    "feedback",
    "fieldEncounterSound",
    "fieldEncounterSoundPlayed",
    "fieldEncounterSoundSrc",
    "foundCount",
    "hpAfter",
    "hpBefore",
    "inventoryItemSound",
    "inventoryItemSoundPlayed",
    "inventoryItemSoundSrc",
    "itemKey",
    "itemName",
    "key",
    "loaded",
    "missingEvidenceCount",
    "modeFeedback",
    "nextAction",
    "originalSavedataParserAvailable",
    "routeBlockerShortText",
    "routeEvidence",
    "routeProgressFeedback",
    "saved",
    "selector",
    "source",
    "stepCount",
    "target",
    "text",
    "title",
    "transitionSound",
    "transitionSoundPlayed",
    "transitionSoundSrc",
    "x",
    "y",
}
CLICK_FIELDS = {
    "before",
    "clientX",
    "clientY",
    "index",
    "keys",
    "labels",
    "ok",
    "request",
    "routeGoalKeys",
    "routeGoalLabels",
    "routeGoalWindow",
    "sampleClientX",
    "sampleClientY",
    "sampleIndex",
    "sampleKeys",
    "sampleLabels",
    "selectedTitleMenuIndex",
    "targetClientX",
    "targetClientY",
    "targetIndex",
    "text",
    "title",
    "titleMenuKeys",
    "titleMenuMode",
    "visibleTargetIndex",
}


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_title_start_browser.py locally to regenerate the ignored raw smoke when debugging browser regressions.",
            "boundary": (
                "Compact title-start browser smoke contract. It keeps title/menu, "
                "start/continue, route-assist, inventory feedback, save feedback, "
                "map-transition feedback, and field-encounter markers 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]) -> dict[str, Any]:
    if not isinstance(data, dict):
        return {}
    return {key: data[key] for key in sorted(fields) if key in data}


def compact_log_entry(entry: Any) -> Any:
    if not isinstance(entry, dict):
        return entry
    out = pick(entry, MARKER_FIELDS | FOOT_FIELDS)
    for key, value in entry.items():
        if key in out or key in DROP_KEYS:
            continue
        if isinstance(value, (str, int, float, bool)) or value is None:
            out[key] = value
        elif key in {"foot", "tile"} and isinstance(value, dict):
            out[key] = pick(value, FOOT_FIELDS)
    return out


def compact_progress(data: Any) -> dict[str, Any]:
    if not isinstance(data, dict):
        return {}
    out = pick(data, {"counts"})
    if isinstance(data.get("counts"), dict):
        out["counts"] = data["counts"]
    return out


def compact_marker(data: Any) -> dict[str, Any]:
    if not isinstance(data, dict):
        return {}
    out = pick(data, MARKER_FIELDS | FOOT_FIELDS)
    if isinstance(data.get("routeEvidence"), dict):
        out["routeEvidence"] = pick(data["routeEvidence"], MARKER_FIELDS | {"missingEvidenceCount", "externalProofInputId"})
    if isinstance(data.get("routeProgressFeedback"), dict):
        out["routeProgressFeedback"] = compact_marker(data["routeProgressFeedback"])
    if isinstance(data.get("modeFeedback"), dict):
        out["modeFeedback"] = compact_marker(data["modeFeedback"])
    if isinstance(data.get("feedback"), dict):
        out["feedback"] = compact_marker(data["feedback"])
    if isinstance(data.get("completion"), dict):
        out["completion"] = compact_marker(data["completion"])
    return out


def compact_loaded_save_summary(data: Any) -> dict[str, Any]:
    if not isinstance(data, dict):
        return {}
    out = pick(data, {"selector", "x", "y"})
    if isinstance(data.get("routeEvidence"), dict):
        out["routeEvidence"] = compact_marker(data["routeEvidence"])
    return out


def compact_movement(data: Any) -> dict[str, Any]:
    if not isinstance(data, dict):
        return {}
    return pick(data, {"beforeFoot", "afterFoot", "before", "after"})


def compact_state(data: Any) -> dict[str, Any]:
    if not isinstance(data, dict):
        return {}
    out: dict[str, Any] = {}
    for key, value in data.items():
        if key in DROP_KEYS:
            continue
        if key == "foot" and isinstance(value, dict):
            out[key] = pick(value, FOOT_FIELDS)
        elif key in {"before", "after"} and isinstance(value, dict):
            out[key] = compact_state(value)
        elif key in {
            "fieldEncounter",
            "fieldEncounterMode",
            "fieldEncounterModeAutoSave",
            "routeAssistAutoSave",
            "routeCandidateAutoSave",
            "autoSave",
            "step",
            "mode",
            "titleRouteGoal",
            "titleSavedatScan",
            "titlePublicSavedat",
            "objectiveAction",
            "objectiveBefore",
            "objectiveCompletionNotice",
            "objectiveItemFeedbackLast",
            "objectiveItemFeedbackLastRender",
            "objectiveSoundState",
            "soundState",
            "runtimeSaveFeedbackLast",
            "runtimeSaveFeedbackLastRender",
            "itemFeedbackLast",
            "itemFeedbackLastRender",
            "objectiveResult",
            "objectiveCompletionNotice",
            "openMarker",
            "selectedMarker",
            "inventoryOpenMarker",
            "inventorySelectionMarker",
            "selectionLog",
            "routeState",
        } and isinstance(value, dict):
            out[key] = compact_marker(value)
        elif key == "loadedSaveSummary" and isinstance(value, dict):
            out[key] = compact_loaded_save_summary(value)
        elif key == "progress" and isinstance(value, dict):
            out[key] = compact_progress(value)
        elif key == "prototypeProgress" and isinstance(value, dict):
            out[key] = compact_progress(value)
        elif key == "movement" and isinstance(value, dict):
            out[key] = compact_movement(value)
        elif key.endswith("Log") or key.endswith("Render"):
            if isinstance(value, list):
                out[key] = [compact_log_entry(entry) for entry in value]
            elif isinstance(value, dict):
                out[key] = compact_log_entry(value)
            else:
                out[key] = value
        elif key in TITLE_STATE_FIELDS or key in RUNTIME_STATE_FIELDS:
            if isinstance(value, dict):
                out[key] = compact_state(value)
            elif isinstance(value, list):
                out[key] = [compact_log_entry(entry) for entry in value]
            else:
                out[key] = value
        elif key in CLICK_FIELDS:
            if isinstance(value, dict):
                out[key] = compact_state(value)
            else:
                out[key] = value
        elif isinstance(value, dict):
            nested = compact_state(value)
            if nested:
                out[key] = nested
        elif isinstance(value, list):
            compacted = [compact_log_entry(entry) for entry in value]
            if compacted:
                out[key] = compacted
        elif isinstance(value, (str, int, float, bool)) or value is None:
            out[key] = value
    return out


def compact_top(payload: dict[str, Any]) -> dict[str, Any]:
    out: dict[str, Any] = {
        "status": payload.get("status"),
        "url": payload.get("url"),
        "titleChecksum": payload.get("titleChecksum"),
        "mapChecksum": payload.get("mapChecksum"),
        "sourceFile": f"out/{SOURCE_FILE}",
    }
    for key, value in payload.items():
        if key in out:
            continue
        if isinstance(value, dict):
            out[key] = compact_state(value)
        elif isinstance(value, (str, int, float, bool)) or value is None:
            out[key] = value
    return out


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)
    title_start = compact_top(payload)
    return {
        "kind": "hwanse-title-start-browser-summary",
        "source": "tools/summarize_title_start_browser.py",
        "status": "ready",
        "sourceFile": f"out/{SOURCE_FILE}",
        "boundary": (
            "Compact title-start browser smoke contract. It keeps title/menu, "
            "start/continue, route-assist, inventory feedback, save feedback, "
            "map-transition feedback, and field-encounter markers consumed by "
            "completion audits. The raw title_start_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(title_start),
        "titleStart": title_start,
    }


def html_page(summary: dict[str, Any]) -> str:
    title_start = summary["titleStart"]
    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 title_start.items():
        rows.append(
            "<tr>"
            f"<td><code>{html.escape(str(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>Title Start 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>Title Start 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, sort_keys=True, 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("--out-dir", type=Path, default=OUT)
    parser.add_argument("--html-out", type=Path)
    args = parser.parse_args()
    summary = build_summary(args.root)
    write_outputs(summary, args.out_dir, args.html_out)


if __name__ == "__main__":
    main()
