#!/usr/bin/env python3
"""Summarize feasible runtime checkpoint entry paths for Hwanse2.exe validation."""
from __future__ import annotations

import html
import json
from collections import Counter
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"


def load_json(path: Path, default: Any) -> Any:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return default


def rel(path: Path) -> str:
    try:
        return str(path.relative_to(ROOT))
    except ValueError:
        return str(path)


def exists_link(path: str, label: str | None = None) -> dict[str, Any]:
    target = ROOT / path
    return {
        "label": label or Path(path).name,
        "path": path,
        "href": f"../{path}" if path.startswith("out/") else path,
        "exists": target.exists(),
    }


def selector_key(row: dict[str, Any]) -> str:
    return f"{row.get('group')}:{row.get('slot')}"


def selector_checkpoint_rows(selectors: list[dict[str, Any]]) -> list[dict[str, Any]]:
    rows = []
    for row in selectors:
        maps = row.get("fieldMaps") or []
        if not maps:
            continue
        rows.append({
            "selector": selector_key(row),
            "selectedPointerHex": row.get("selectedPointerHex"),
            "fieldMapCount": len(maps),
            "fieldMaps": maps,
            "linkedCnsPreview": (row.get("linkedCns") or [])[:10],
            "checkpointUse": "save-selector-candidate",
            "runtimeStatus": "requires-captured-save-or-full-state",
        })
    return rows


def save_file_rows() -> list[dict[str, Any]]:
    rows = []
    for directory in [ROOT / "SaveData", ROOT / "SAVEDATA"]:
        for path in sorted(directory.glob("savedat*.dat")) if directory.exists() else []:
            rows.append({
                "path": rel(path),
                "size": path.stat().st_size,
                "status": "captured-or-staged-file-present",
            })
    return rows


def runtime_evidence_rows() -> list[dict[str, Any]]:
    specs = [
        ("opening-noinput", "out/runtime_validation_opening/runtime_opening_noinput_poll.html", "실행/화면/메모리 poll 성립"),
        ("title-reach", "out/runtime_validation_title_reach/runtime_opening_noinput_poll.html", "자연 실행으로 타이틀 이미지 도달"),
        ("title-menu-return", "out/runtime_validation_title_accept/runtime_visual_key_sequence_title_return.html", "타이틀 Return 입력으로 50:0 메뉴 루트 진입"),
        ("title-newgame-mixed", "out/runtime_validation_title_newgame_mixed/runtime_visual_key_sequence_title_newgame_mixed.html", "Return + Down action-mask + z로 신규 시작 첫 장면 진입"),
        ("visual-key-battle", "out/runtime_validation_visual/runtime_visual_key_sequence_battle_space.html", "전투 화면 + key-buffer + actor script timeline"),
        ("visual-key-logo", "out/runtime_validation_visual/runtime_visual_key_sequence_skip_space.html", "시작 로고 구간 key-buffer 타이밍 반례"),
        ("input-path", "out/runtime_input_path_probe.json", "X key state vs key-buffer 직접 입력 분리"),
        ("runtime-movement", "out/runtime_movement.html", "필드 이동/충돌/party trail 런타임 근거"),
        ("save-path", "out/runtime_save_path_context.json", "SaveData 경로/loader/read blocks/selected pointer store"),
        ("patched-selector-followup", "out/runtime_patched_selector_followup_context.json", "constructed selector 진단이 promotion proof가 아닌 이유"),
        ("selector-review", "web/selector_review.html", "selector frontier proof matrix"),
    ]
    rows = []
    for key, path, meaning in specs:
        link = exists_link(path, key)
        link["meaning"] = meaning
        rows.append(link)
    return rows


def method_rows(data: dict[str, Any]) -> list[dict[str, Any]]:
    opening = data["opening"]
    visual = data["visualBattle"]
    title_menu = data["titleMenu"]
    title_newgame = data["titleNewGame"]
    input_path = data["inputPath"]
    save_files = data["saveFiles"]
    save_context = data["saveContext"]
    patched = data["patchedFollowup"]
    return [
        {
            "method": "natural-runtime",
            "label": "자연 실행",
            "status": "usable",
            "scope": "인트로/타이틀/초기 메뉴처럼 실행만으로 도달하는 구간",
            "evidence": f"openingSelectedObserved={bool(opening.get('openingSelectedObserved'))}",
            "limit": "임의 시나리오 위치로 점프하지는 못함",
        },
        {
            "method": "key-buffer-automation",
            "label": "키버퍼 자동 입력",
            "status": "usable",
            "scope": "현재 런타임 상태에서 메뉴/이동/전투 입력을 자동화",
            "evidence": f"keyBufferPokeChangedSelectedPointer={bool(input_path.get('keyBufferPokeChangedSelectedPointer'))}; visualSelectors={','.join(visual.get('observedSelectors') or []) or '-'}",
            "limit": "멀리 있는 이벤트는 시간이 오래 걸리고, 이벤트 flag를 건너뛰지는 못함",
        },
        {
            "method": "title-newgame-automation",
            "label": "타이틀 신규 시작 자동 진입",
            "status": "usable" if title_newgame else "missing-evidence",
            "scope": "원본 EXE 자연 실행 후 타이틀에서 신규 시작 첫 장면까지 진입",
            "evidence": (
                f"titleMenuSelectors={','.join(title_menu.get('observedSelectors') or []) or '-'}; "
                f"newGameSelectors={','.join(title_newgame.get('observedSelectors') or []) or '-'}; "
                "sequence=Return@key-buffer,Down@action-mask,z@key-buffer"
            ),
            "limit": "100초 전후 자연 대기가 필요하며, 50:0 루트는 메뉴와 신규 시작 초기 프롬프트를 함께 포함한다",
        },
        {
            "method": "captured-save-checkpoint",
            "label": "캡처 세이브 체크포인트",
            "status": "blocked" if not save_files else "ready-to-test",
            "scope": "실제 게임이 저장한 SaveData/savedatN.dat에서 특정 위치로 즉시 진입",
            "evidence": f"savePathKnown={bool(save_context)}; presentFiles={len(save_files)}",
            "limit": "현재 repo에는 실제 savedat*.dat가 없음. 합성 save는 promotion proof로 쓰지 않음",
        },
        {
            "method": "constructed-save-selector",
            "label": "합성 save selector",
            "status": "diagnostic-only",
            "scope": "selector/좌표 byte를 바꿔 loader 반응을 보는 진단",
            "evidence": "patched public-base diagnostic reached constructed selector in prior runs",
            "limit": "정상 게임 상태가 아니므로 맵 이동/시나리오 확정 증거로 승격 금지",
        },
        {
            "method": "selected-pointer-patch",
            "label": "selected-pointer 직접 패치",
            "status": "blocked",
            "scope": "0x0059de30만 바꿔 scene root를 바꾸는 시도",
            "evidence": "selector alone is insufficient; actor/camera/active-order/flags are separate",
            "limit": "화면/오브젝트/flag 상태가 동기화되지 않으면 깨진 상태가 됨",
        },
        {
            "method": "full-runtime-state-patch",
            "label": "전체 런타임 상태 패치",
            "status": "candidate",
            "scope": "selected root + active object order + actor tile/camera + flags를 함께 맞춰 체크포인트 구성",
            "evidence": f"patchedFollowupAvailable={bool(patched)}; movementRuntimeGrounded=True",
            "limit": "아직 flag/scene VM 상태 전체 layout이 확정되지 않아 좁은 실험만 가능",
        },
    ]


def status_class(status: str) -> str:
    if status in {"usable", "ready-to-test"}:
        return "good"
    if status in {"diagnostic-only", "candidate"}:
        return "warn"
    return "bad"


def build_summary() -> dict[str, Any]:
    selectors = load_json(OUT / "save_scene_selectors.json", [])
    opening = load_json(OUT / "runtime_validation_opening" / "runtime_opening_noinput_poll.json", {})
    if not opening:
        opening = load_json(OUT / "runtime_opening_noinput_poll.json", {})
    visual_battle = load_json(OUT / "runtime_validation_visual" / "runtime_visual_key_sequence_battle_space.json", {})
    title_reach = load_json(OUT / "runtime_validation_title_reach" / "runtime_opening_noinput_poll.json", {})
    title_menu = load_json(OUT / "runtime_validation_title_accept" / "runtime_visual_key_sequence_title_return.json", {})
    title_newgame = load_json(OUT / "runtime_validation_title_newgame_mixed" / "runtime_visual_key_sequence_title_newgame_mixed.json", {})
    input_path = load_json(OUT / "runtime_input_path_probe.json", {})
    save_context = load_json(OUT / "runtime_save_path_context.json", {})
    patched_followup = load_json(OUT / "runtime_patched_selector_followup_context.json", {})
    checkpoint_rows = selector_checkpoint_rows(selectors)
    map_counts = Counter(map_name for row in checkpoint_rows for map_name in row["fieldMaps"])
    data = {
        "opening": opening,
        "visualBattle": visual_battle,
        "titleReach": title_reach,
        "titleMenu": title_menu,
        "titleNewGame": title_newgame,
        "inputPath": input_path,
        "saveContext": save_context,
        "patchedFollowup": patched_followup,
        "saveFiles": save_file_rows(),
    }
    methods = method_rows(data)
    summary = {
        "objective": "runtime checkpoint entry strategy for original Hwanse2.exe validation",
        "status": "checkpoint-entry-methods-separated",
        "methodRows": methods,
        "runtimeEvidenceRows": runtime_evidence_rows(),
        "selectorCheckpointRows": checkpoint_rows,
        "selectorCheckpointCount": len(checkpoint_rows),
        "selectorTotalCount": len(selectors),
        "saveFileRows": data["saveFiles"],
        "saveFileCount": len(data["saveFiles"]),
        "topMapSelectorCounts": [
            {"map": map_name, "selectorCount": count}
            for map_name, count in map_counts.most_common(24)
        ],
        "runtimeFindings": {
            "openingRuntimeObserved": bool(opening.get("openingSelectedObserved")),
            "titleReachObserved": bool(title_reach),
            "titleMenuReturnObservedSelectors": title_menu.get("observedSelectors") or [],
            "titleNewGameMixedObservedSelectors": title_newgame.get("observedSelectors") or [],
            "titleNewGameFirstSceneVisualEvidence": bool(title_newgame),
            "visualBattleObservedSelectors": visual_battle.get("observedSelectors") or [],
            "keyBufferInputWorks": bool(input_path.get("keyBufferPokeChangedSelectedPointer")),
            "xEventOnlyInsufficient": not bool(input_path.get("xEventChangedSelectedPointer")),
            "capturedSaveFilesPresent": bool(data["saveFiles"]),
        },
        "nextActions": [
            {
                "action": "title-newgame-runtime-baseline",
                "status": "usable",
                "detail": "자연 실행 100초 후 Return@key-buffer, Down@action-mask, z@key-buffer로 신규 시작 첫 장면까지 진입 가능하다.",
            },
            {
                "action": "captured-save-intake",
                "status": "blocked-until-save-file",
                "detail": "실제 게임이 만든 SaveData/savedatN.dat를 받으면 selector/좌표/active order를 읽고 바로 런타임 load 검증으로 연결한다.",
            },
            {
                "action": "visual-load-menu-probe",
                "status": "ready-after-save-file",
                "detail": "세이브 파일이 있으면 key-buffer 입력 + screenshot + selected-pointer로 로드 메뉴 진입을 검증한다.",
            },
            {
                "action": "full-state-checkpoint-patch",
                "status": "candidate-only",
                "detail": "selected-pointer 단독이 아니라 actor/camera/active-order/flag 묶음이 확인되는 좁은 케이스부터 실험한다.",
            },
        ],
        "conclusion": (
            "Runtime validation is not limited to manually playing to every point, but arbitrary checkpoint entry needs either "
            "a captured save or a fully synchronized runtime state patch. The currently proven general tools are process "
            "memory polling, screenshots, and direct key-buffer input. Selector-only or synthetic-save routes remain diagnostic."
        ),
    }
    return summary


def html_page(summary: dict[str, Any]) -> str:
    methods = "\n".join(
        "<tr>"
        f"<td><strong>{html.escape(row['label'])}</strong><br><code>{html.escape(row['method'])}</code></td>"
        f"<td><span class=\"tag {status_class(row['status'])}\">{html.escape(row['status'])}</span></td>"
        f"<td>{html.escape(row['scope'])}</td>"
        f"<td><code>{html.escape(row['evidence'])}</code></td>"
        f"<td>{html.escape(row['limit'])}</td>"
        "</tr>"
        for row in summary["methodRows"]
    )
    evidence = "\n".join(
        "<tr>"
        f"<td><a href=\"{html.escape(row['href'])}\">{html.escape(row['label'])}</a></td>"
        f"<td>{html.escape(row['meaning'])}</td>"
        f"<td><span class=\"tag {'good' if row['exists'] else 'bad'}\">{html.escape('present' if row['exists'] else 'missing')}</span></td>"
        "</tr>"
        for row in summary["runtimeEvidenceRows"]
    )
    selector_rows = "\n".join(
        "<tr data-selector-row>"
        f"<td><code>{html.escape(row['selector'])}</code></td>"
        f"<td><code>{html.escape(str(row['selectedPointerHex']))}</code></td>"
        f"<td>{row['fieldMapCount']}</td>"
        f"<td>{html.escape(', '.join(row['fieldMaps']))}</td>"
        f"<td><span class=\"tag warn\">{html.escape(row['runtimeStatus'])}</span></td>"
        "</tr>"
        for row in summary["selectorCheckpointRows"]
    )
    map_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row['map'])}</code></td>"
        f"<td>{row['selectorCount']}</td>"
        "</tr>"
        for row in summary["topMapSelectorCounts"]
    )
    save_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row['path'])}</code></td>"
        f"<td>{row['size']}</td>"
        f"<td>{html.escape(row['status'])}</td>"
        "</tr>"
        for row in summary["saveFileRows"]
    ) or '<tr><td colspan="3" class="muted">현재 실제 savedat*.dat 없음</td></tr>'
    actions = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row['action'])}</code></td>"
        f"<td><span class=\"tag {status_class(row['status'].split('-', 1)[0])}\">{html.escape(row['status'])}</span></td>"
        f"<td>{html.escape(row['detail'])}</td>"
        "</tr>"
        for row in summary["nextActions"]
    )
    findings = summary["runtimeFindings"]
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Runtime Checkpoint Entry Review</title>
  <style>
    body {{ margin: 24px; background: #f6f7f9; color: #17202a; font: 14px system-ui, sans-serif; }}
    a {{ color: #185abc; text-decoration: none; }}
    a:hover {{ text-decoration: underline; }}
    code {{ color: #7a4b00; }}
    .panel {{ background: white; border: 1px solid #d8dee6; border-radius: 8px; padding: 14px; margin: 14px 0; }}
    .metrics {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; }}
    .metric {{ background: #f8fafc; border: 1px solid #d8dee6; border-radius: 6px; padding: 10px; }}
    .metric b {{ display: block; font-size: 22px; }}
    table {{ width: 100%; border-collapse: collapse; background: white; }}
    th, td {{ border-bottom: 1px solid #d8dee6; padding: 8px 10px; text-align: left; vertical-align: top; }}
    th {{ background: #eef2f6; position: sticky; top: 0; }}
    .table-wrap {{ overflow: auto; border: 1px solid #d8dee6; border-radius: 6px; max-height: 620px; }}
    .tag {{ display: inline-block; padding: 2px 7px; border-radius: 999px; background: #edf2f7; color: #334155; font-size: 12px; white-space: nowrap; }}
    .tag.good {{ color: #0f766e; background: #e6f4f1; }}
    .tag.warn {{ color: #a15c00; background: #fff4df; }}
    .tag.bad {{ color: #b42318; background: #fdebea; }}
    .muted {{ color: #607080; }}
    input {{ width: min(520px, 100%); padding: 9px 11px; border: 1px solid #d8dee6; border-radius: 6px; font: inherit; }}
  </style>
</head>
<body>
  <h1>Runtime Checkpoint Entry Review</h1>
  <p><a href="../web/index.html">Home</a> · <a href="../web/selector_review.html">selector review</a> · <a href="runtime_movement.html">runtime movement</a></p>
  <div class="panel">
    <p>{html.escape(summary['conclusion'])}</p>
    <div class="metrics">
      <div class="metric"><span>selector checkpoints</span><b>{summary['selectorCheckpointCount']}/{summary['selectorTotalCount']}</b></div>
      <div class="metric"><span>captured saves</span><b>{summary['saveFileCount']}</b></div>
      <div class="metric"><span>key-buffer input</span><b>{str(findings['keyBufferInputWorks'])}</b></div>
      <div class="metric"><span>opening runtime</span><b>{str(findings['openingRuntimeObserved'])}</b></div>
    </div>
  </div>
  <h2>Entry Methods</h2>
  <div class="table-wrap"><table><thead><tr><th>method</th><th>status</th><th>scope</th><th>evidence</th><th>limit</th></tr></thead><tbody>{methods}</tbody></table></div>
  <h2>Runtime Evidence</h2>
  <div class="table-wrap"><table><thead><tr><th>link</th><th>meaning</th><th>status</th></tr></thead><tbody>{evidence}</tbody></table></div>
  <h2>Save Files</h2>
  <div class="table-wrap"><table><thead><tr><th>path</th><th>size</th><th>status</th></tr></thead><tbody>{save_rows}</tbody></table></div>
  <h2>Selector Checkpoint Candidates</h2>
  <p class="muted">실제 저장 파일 또는 전체 런타임 상태가 있어야 이 후보들이 검증 체크포인트가 된다. selector 단독 패치는 승격 근거가 아니다.</p>
  <input id="selectorSearch" type="search" placeholder="검색: selector, map name, root" autocomplete="off">
  <div class="table-wrap"><table><thead><tr><th>selector</th><th>root</th><th>maps</th><th>field maps</th><th>runtime status</th></tr></thead><tbody id="selectorRows">{selector_rows}</tbody></table></div>
  <h2>Map Selector Density</h2>
  <div class="table-wrap"><table><thead><tr><th>map</th><th>selector candidates</th></tr></thead><tbody>{map_rows}</tbody></table></div>
  <h2>Next Actions</h2>
  <div class="table-wrap"><table><thead><tr><th>action</th><th>status</th><th>detail</th></tr></thead><tbody>{actions}</tbody></table></div>
  <script>
    const search = document.getElementById("selectorSearch");
    search.addEventListener("input", () => {{
      const q = search.value.trim().toLowerCase();
      document.querySelectorAll("[data-selector-row]").forEach((row) => {{
        row.hidden = q && !row.textContent.toLowerCase().includes(q);
      }});
    }});
    window.HWANSE_RUNTIME_CHECKPOINT_ENTRY_READY = true;
    window.HWANSE_LAST_RUNTIME_CHECKPOINT_ENTRY = {{
      selectorCheckpointCount: {summary['selectorCheckpointCount']},
      capturedSaveFileCount: {summary['saveFileCount']},
      keyBufferInputWorks: {str(findings['keyBufferInputWorks']).lower()},
      openingRuntimeObserved: {str(findings['openingRuntimeObserved']).lower()}
    }};
  </script>
</body>
</html>
"""


def write_outputs(summary: dict[str, Any], out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "runtime_checkpoint_entry_review.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")) + "\n",
        encoding="utf-8",
    )


def main() -> None:
    summary = build_summary()
    write_outputs(summary)
    print(f"runtime checkpoint entry review -> {OUT / 'runtime_checkpoint_entry_review.json'}")
    print(summary["conclusion"])


if __name__ == "__main__":
    main()
