#!/usr/bin/env python3
"""Summarize Scene/Event VM command-stream candidates.

This report deliberately separates two very different evidence layers:

* command-like byte streams whose opcodes have handler evidence;
* event/transition records that carry map/resource/condition payloads, but are
  not yet proven to be executed VM command lists.

The goal is to find places worth deeper reverse-engineering without promoting
resource adjacency or manual transition review rows into original route proof.
"""
from __future__ import annotations

import argparse
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"
WEB = ROOT / "web"


ROLE_GROUPS = {
    "prompt/text": {"0x0b", "0x0d", "0x35"},
    "wait/input": {"0x06"},
    "branch/call": {"0x09", "0x0a"},
    "object-position": {"0x07"},
    "display/cursor": {"0x02", "0x03", "0x04", "0x08", "0x0e", "0x15", "0x37"},
    "choice-marker": {"0x18"},
    "flag/check candidate": {"0x10", "0x11", "0x12", "0x22", "0x26", "0x2c", "0x2f", "0x59", "0x5c"},
}


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


def write_json(path: Path, payload: Any) -> None:
    path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def write_text(path: Path, text: str) -> None:
    path.write_text(text, encoding="utf-8")


def h(value: Any) -> str:
    return html.escape("" if value is None else str(value))


def short(value: Any, limit: int = 220) -> str:
    if isinstance(value, list) and all(not isinstance(item, (dict, list)) for item in value):
        value = " / ".join(str(item) for item in value)
    if isinstance(value, (dict, list)):
        value = json.dumps(value, ensure_ascii=False, sort_keys=True)
    text = " ".join((str(value) if value is not None else "").split())
    return text if len(text) <= limit else text[: limit - 1] + "..."


def first(items: list[Any], limit: int = 6) -> list[Any]:
    return list(items[:limit])


def role_flags(opcode_counts: dict[str, int]) -> dict[str, bool]:
    opcodes = set(opcode_counts)
    return {role: bool(opcodes & members) for role, members in ROLE_GROUPS.items()}


def role_list(opcode_counts: dict[str, int]) -> list[str]:
    flags = role_flags(opcode_counts)
    return [role for role, present in flags.items() if present]


def opcode_count_sum(opcode_counts: dict[str, int], opcodes: set[str]) -> int:
    return sum(int(opcode_counts.get(op, 0) or 0) for op in opcodes)


def build_command_blocks(dialogue_blocks: dict[str, Any]) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    for block in dialogue_blocks.get("blocks") or []:
        opcode_counts = {str(k): int(v or 0) for k, v in (block.get("commandOpcodeCounts") or {}).items()}
        roles = role_list(opcode_counts)
        route_contexts = block.get("routeContexts") or []
        resources = block.get("resources") or []
        field_maps = block.get("fieldMaps") or []
        text_lines = block.get("textLines") or []
        text_sample = block.get("sampleText") or " / ".join(line.get("text", "") for line in text_lines[:8])
        source_commands = block.get("sourceCommandVas") or []
        render_commands = block.get("renderCommandVas") or []

        if route_contexts:
            promotion = "route-window-candidate"
        elif opcode_count_sum(opcode_counts, ROLE_GROUPS["prompt/text"]) and opcode_count_sum(opcode_counts, ROLE_GROUPS["wait/input"]):
            promotion = "storage-command-block-route-unlinked"
        else:
            promotion = "command-like-storage-candidate"

        rows.append(
            {
                "id": block.get("blockId"),
                "startVaHex": block.get("startVaHex"),
                "endVaHex": block.get("endVaHex"),
                "byteCount": block.get("byteCount"),
                "classification": block.get("classification"),
                "promotion": promotion,
                "roles": roles,
                "opcodeCounts": opcode_counts,
                "sourceCommandVas": source_commands,
                "renderCommandVas": render_commands,
                "sourceCommandCount": len(source_commands),
                "renderCommandCount": len(render_commands),
                "textLineCount": block.get("textLineCount") or len(text_lines),
                "textSample": short(text_sample, 260),
                "resources": [res.get("name") for res in resources if res.get("name")],
                "fieldMaps": field_maps,
                "routeContexts": route_contexts,
                "evidence": [
                    "opcode counts and handlers are grounded for known opcodes",
                    "CP949 text bytes are co-located with the command-looking block",
                ],
                "missingEvidence": [
                    "no strict scene record -> executed stream binding",
                    "no source hotspot/control path proof",
                ],
            }
        )
    return rows


def build_text_source_rows(text_flow: dict[str, Any]) -> list[dict[str, Any]]:
    rows = []
    for row in text_flow.get("selectedRows") or []:
        snippets = row.get("nearbyTextSnippets") or []
        cns = row.get("nearbyCns") or []
        rows.append(
            {
                "vaHex": row.get("vaHex"),
                "opcodeHex": row.get("opcodeHex"),
                "handlerVaHex": row.get("handlerVaHex"),
                "confidence": row.get("confidence"),
                "classification": row.get("classification"),
                "cnsEvidenceStatus": row.get("cnsEvidenceStatus"),
                "nearbyText": " / ".join(short(snippet.get("text"), 60) for snippet in snippets[:8]),
                "nearbyCns": [item.get("name") for item in cns[:6] if item.get("name")],
                "linkedPromptCount": row.get("linkedPromptCount"),
                "linkedFlowGroupCount": row.get("linkedFlowGroupCount"),
            }
        )
    return rows


def build_transition_payload_rows(event_record: dict[str, Any]) -> list[dict[str, Any]]:
    rows = []
    for rec in event_record.get("records") or []:
        rows.append(
            {
                "source": rec.get("source"),
                "recordVaHex": rec.get("recordVaHex"),
                "sceneIdHex": rec.get("sceneIdHex"),
                "eventKind": rec.get("eventKind"),
                "targets": rec.get("targets") or [],
                "targetLinkState": rec.get("targetLinkState"),
                "coordinateState": rec.get("coordinateState"),
                "conditionState": rec.get("conditionState"),
                "spawnState": rec.get("spawnState"),
                "promotion": rec.get("promotion"),
                "routeUse": rec.get("routeUse"),
                "activePoints": rec.get("activePoints") or [],
                "rawPointCount": rec.get("rawPointCount"),
                "conditionChoiceCount": len(rec.get("conditionChoices") or []),
                "resourceRefs": rec.get("resourceRefs") or [],
                "missingEvidence": [
                    "decoded trigger-coordinate consumer",
                    "decoded condition branch outcome",
                    "decoded spawn writer",
                ],
            }
        )
    return rows


def build_opcode_role_rows(opcode_dictionary: dict[str, Any]) -> list[dict[str, Any]]:
    rows = []
    for row in opcode_dictionary.get("opcodeDictionary") or []:
        op = row.get("opcodeHex")
        role = next((name for name, members in ROLE_GROUPS.items() if op in members), "")
        if not role and row.get("evidenceLevel") != "grounded":
            continue
        rows.append(
            {
                "opcodeHex": op,
                "roleGroup": role or row.get("role"),
                "evidenceLevel": row.get("evidenceLevel"),
                "decodedLength": row.get("decodedLength"),
                "handlerVaHex": row.get("handlerVaHex"),
                "commandCount": row.get("commandCount"),
                "effect": short(row.get("effect"), 260),
                "nextStep": row.get("nextStep"),
            }
        )
    return rows


def build_branch_flag_rows(branch_flag: dict[str, Any], branch_state_candidates: dict[str, Any]) -> dict[str, Any]:
    return {
        "promotionStatus": branch_flag.get("promotionStatus"),
        "summary": branch_flag.get("summary") or {},
        "decisions": branch_flag.get("decisions") or [],
        "rawCandidateSummary": {
            "candidateCount": branch_state_candidates.get("candidateCount"),
            "candidateCountsByIndex": branch_state_candidates.get("candidateCountsByIndex"),
            "candidateCountsByConfidence": branch_state_candidates.get("candidateCountsByConfidence"),
            "conclusion": branch_state_candidates.get("conclusion"),
        },
    }


def build_payload(args: argparse.Namespace) -> dict[str, Any]:
    opcode_dictionary = load_json(args.opcode_dictionary, {})
    dialogue_blocks = load_json(args.dialogue_blocks, {})
    text_flow = load_json(args.text_flow, {})
    event_record = load_json(args.event_record, {})
    branch_flag = load_json(args.branch_flag, {})
    branch_state_candidates = load_json(args.branch_state_candidates, {})
    execution_route = load_json(args.execution_route, {})
    choice_target = load_json(args.choice_target, {})

    command_blocks = build_command_blocks(dialogue_blocks)
    text_source_rows = build_text_source_rows(text_flow)
    transition_payload_rows = build_transition_payload_rows(event_record)
    opcode_role_rows = build_opcode_role_rows(opcode_dictionary)
    branch_flag_rows = build_branch_flag_rows(branch_flag, branch_state_candidates)

    role_counts = Counter(role for row in command_blocks for role in row.get("roles", []))
    promotion_counts = Counter(row.get("promotion") for row in command_blocks)
    transition_state_counts = Counter(row.get("promotion") for row in transition_payload_rows)

    summary = {
        "scope": "scene/event command stream candidates",
        "promotionStatus": "candidate-streams-found-route-binding-blocked",
        "commandBlockCount": len(command_blocks),
        "dialogueLikeBlockCount": dialogue_blocks.get("dialogueLikeBlockCount"),
        "routeLinkedCommandBlockCount": dialogue_blocks.get("routeLinkedBlockCount"),
        "mapLinkedCommandBlockCount": dialogue_blocks.get("mapLinkedBlockCount"),
        "commandBlockPromotionCounts": dict(promotion_counts),
        "commandBlockRoleCounts": dict(role_counts),
        "textSourceCommandCount": text_flow.get("commandCount"),
        "textSourceSelectedRowCount": len(text_source_rows),
        "textSourceCommandCountsByOpcode": text_flow.get("commandCountsByOpcode"),
        "transitionPayloadRecordCount": len(transition_payload_rows),
        "transitionPayloadStateCounts": dict(transition_state_counts),
        "choiceTargetPromotionStatus": choice_target.get("promotionStatus"),
        "branchFlagPromotionStatus": branch_flag.get("promotionStatus"),
        "executionRoutePromotionStatus": execution_route.get("promotionStatus"),
        "fullSceneCommandListProofFound": False,
        "mapTransitionCommandOpcodeProofFound": False,
        "conclusion": (
            "Prompt/render/wait/branch/object-position command-looking blocks exist, "
            "but map transition records are still target/resource payloads and no full "
            "scene record -> executed command stream has been proven."
        ),
    }

    return {
        "summary": summary,
        "roleGroups": {k: sorted(v) for k, v in ROLE_GROUPS.items()},
        "commandBlocks": command_blocks,
        "textSourceRows": text_source_rows,
        "transitionPayloadRows": transition_payload_rows,
        "opcodeRoleRows": opcode_role_rows,
        "branchFlagRows": branch_flag_rows,
        "sourceArtifacts": {
            "opcodeDictionary": str(args.opcode_dictionary.relative_to(ROOT)),
            "dialogueBlocks": str(args.dialogue_blocks.relative_to(ROOT)),
            "textFlow": str(args.text_flow.relative_to(ROOT)),
            "eventRecord": str(args.event_record.relative_to(ROOT)),
            "branchFlag": str(args.branch_flag.relative_to(ROOT)),
            "branchStateCandidates": str(args.branch_state_candidates.relative_to(ROOT)),
            "executionRoute": str(args.execution_route.relative_to(ROOT)),
            "choiceTarget": str(args.choice_target.relative_to(ROOT)),
        },
    }


def table(headers: list[str], rows: list[list[Any]]) -> str:
    head = "".join(f"<th>{h(col)}</th>" for col in headers)
    body = []
    for row in rows:
        body.append("<tr>" + "".join(f"<td>{h(col)}</td>" for col in row) + "</tr>")
    return f"<table><thead><tr>{head}</tr></thead><tbody>{''.join(body)}</tbody></table>"


def render_html(payload: dict[str, Any]) -> str:
    summary = payload["summary"]
    command_rows = []
    for row in payload["commandBlocks"]:
        command_rows.append(
            [
                row["id"],
                f"{row['startVaHex']}..{row['endVaHex']}",
                row["promotion"],
                ", ".join(row["roles"]),
                short(row["opcodeCounts"], 160),
                row["textSample"],
                ", ".join(row["resources"][:8]),
            ]
        )

    transition_rows = []
    for row in payload["transitionPayloadRows"]:
        transition_rows.append(
            [
                row["source"],
                row["recordVaHex"],
                ", ".join(row["targets"]),
                row["targetLinkState"],
                row["coordinateState"],
                row["conditionState"],
                row["promotion"],
                row["routeUse"],
            ]
        )

    opcode_rows = []
    for row in payload["opcodeRoleRows"]:
        opcode_rows.append(
            [
                row["opcodeHex"],
                row["roleGroup"],
                row["evidenceLevel"],
                row["decodedLength"],
                row["commandCount"],
                row["effect"],
            ]
        )

    text_rows = []
    for row in payload["textSourceRows"][:120]:
        text_rows.append(
            [
                row["vaHex"],
                row["opcodeHex"],
                row["confidence"],
                row["cnsEvidenceStatus"],
                row["nearbyText"],
                ", ".join(row["nearbyCns"]),
            ]
        )

    decisions = payload["branchFlagRows"].get("decisions") or []
    decision_rows = [
        [
            item.get("item"),
            item.get("promotion"),
            item.get("evidence"),
            item.get("remainingGap"),
        ]
        for item in decisions
    ]

    cards = "".join(
        f"<div class='card'><b>{h(k)}</b><span>{h(v)}</span></div>"
        for k, v in [
            ("status", summary["promotionStatus"]),
            ("command blocks", summary["commandBlockCount"]),
            ("role counts", short(summary["commandBlockRoleCounts"], 180)),
            ("transition payloads", summary["transitionPayloadRecordCount"]),
            ("full command list proof", summary["fullSceneCommandListProofFound"]),
        ]
    )

    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <link rel="icon" href="../favicon.ico" />
  <title>Scene/Event VM Command Stream Candidates</title>
  <style>
    :root {{ color-scheme: light; --border:#d9dee8; --muted:#5f6b7a; --ink:#17202a; --bg:#f6f8fb; --panel:#fff; --warn:#8a5a00; --good:#0f6b3a; }}
    body {{ margin:0; font-family: system-ui, -apple-system, Segoe UI, sans-serif; background:var(--bg); color:var(--ink); }}
    main {{ max-width: 1440px; margin:0 auto; padding:24px; }}
    nav {{ display:flex; flex-wrap:wrap; gap:8px; margin:12px 0 18px; }}
    nav a {{ color:#24559a; text-decoration:none; border:1px solid var(--border); background:#fff; padding:6px 9px; border-radius:6px; }}
    h1 {{ margin:0 0 6px; font-size:24px; }}
    h2 {{ margin:24px 0 10px; font-size:18px; }}
    .sub {{ color:var(--muted); line-height:1.45; }}
    .cards {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); gap:10px; margin:16px 0; }}
    .card {{ background:var(--panel); border:1px solid var(--border); border-radius:8px; padding:12px; display:flex; flex-direction:column; gap:6px; }}
    .card span {{ color:var(--muted); font-size:13px; }}
    .note {{ background:#fff8e8; border:1px solid #efd28c; color:#5d4100; padding:10px 12px; border-radius:8px; }}
    table {{ width:100%; border-collapse:collapse; background:#fff; border:1px solid var(--border); font-size:13px; }}
    th, td {{ border-bottom:1px solid var(--border); border-right:1px solid var(--border); padding:7px 8px; vertical-align:top; text-align:left; }}
    th {{ background:#eef2f7; position:sticky; top:0; z-index:1; }}
    td {{ max-width:380px; overflow-wrap:anywhere; }}
    details {{ margin:14px 0; }}
    summary {{ cursor:pointer; color:#24559a; font-weight:600; }}
    .scroll {{ max-height:520px; overflow:auto; border:1px solid var(--border); background:#fff; }}
  </style>
</head>
<body>
<main>
  <h1>Scene/Event VM Command Stream Candidates</h1>
  <p class="sub">프롬프트 출력, 선택지, 맵 전환, 캐릭터 위치, flag/check가 한 command list 안에 있는지 검토하기 위한 후보 페이지다. 리소스 payload와 실행 명령 스트림을 분리해서 표시한다.</p>
  <nav>
    <a href="index.html">home</a>
    <a href="scene_event_vm_review.html">opcode dictionary</a>
    <a href="scene_event_vm_execution_route_review.html">execution route</a>
    <a href="scene_event_vm_prompt_sequence_review.html">prompt sequence</a>
    <a href="scene_event_vm_choice_target_review.html">choice target</a>
    <a href="scene_event_vm_branch_flag_review.html">branch/flag</a>
    <a href="../out/scene_event_vm_command_stream_candidates.json">JSON</a>
  </nav>
  <div class="cards">{cards}</div>
  <p class="note">{h(summary['conclusion'])}</p>

  <h2>Command-Like Blocks</h2>
  <p class="sub">대사/렌더/wait/branch/object-position opcode가 같은 저장 블록에 모인 후보. `route-linked`가 아니므로 scene 실행 흐름으로 승격하지 않는다.</p>
  <div class="scroll">{table(['id','range','promotion','roles','opcode counts','text sample','resources'], command_rows)}</div>

  <h2>Transition Payload Records</h2>
  <p class="sub">맵/리소스/condition payload는 잡혔지만, 아직 명령 opcode stream으로 해석된 것은 아니다.</p>
  <div class="scroll">{table(['source','record','targets','target','coordinate','condition','promotion','route use'], transition_rows)}</div>

  <details open>
    <summary>Opcode Role Surface</summary>
    <div class="scroll">{table(['opcode','role group','evidence','length','count','effect'], opcode_rows)}</div>
  </details>

  <details>
    <summary>Text Source Command Rows</summary>
    <div class="scroll">{table(['va','opcode','confidence','cns evidence','nearby text','nearby CNS'], text_rows)}</div>
  </details>

  <details>
    <summary>Branch/Flag Decisions</summary>
    <div class="scroll">{table(['item','promotion','evidence','remaining gap'], decision_rows)}</div>
  </details>
</main>
</body>
</html>
"""


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--opcode-dictionary", type=Path, default=OUT / "scene_event_vm_opcode_dictionary.json")
    parser.add_argument("--dialogue-blocks", type=Path, default=OUT / "event_dialogue_blocks.json")
    parser.add_argument("--text-flow", type=Path, default=OUT / "event_text_source_flow.json")
    parser.add_argument("--event-record", type=Path, default=OUT / "event_record_structure_review.json")
    parser.add_argument("--branch-flag", type=Path, default=OUT / "scene_event_vm_branch_flag_review.json")
    parser.add_argument("--branch-state-candidates", type=Path, default=OUT / "event_object_branch_state_stream_candidates.json")
    parser.add_argument("--execution-route", type=Path, default=OUT / "scene_event_vm_execution_route_review.json")
    parser.add_argument("--choice-target", type=Path, default=OUT / "scene_event_vm_choice_target_review.json")
    parser.add_argument("--json-out", type=Path, default=OUT / "scene_event_vm_command_stream_candidates.json")
    parser.add_argument("--html-out", type=Path, default=WEB / "scene_event_vm_command_stream_candidates.html")
    parser.add_argument("--web-out", type=Path, default=WEB / "scene_event_vm_command_stream_candidates.html")
    args = parser.parse_args()

    payload = build_payload(args)
    write_json(args.json_out, payload)
    html_text = render_html(payload)
    write_text(args.html_out, html_text)
    write_text(args.web_out, html_text)
    print(f"wrote {args.json_out}")
    print(f"wrote {args.web_out}")


if __name__ == "__main__":
    main()
