#!/usr/bin/env python3
"""Summarize grounded event/script VM semantics and remaining execution gaps."""
from __future__ import annotations

import argparse
import html
import json
import re
from pathlib import Path
from typing import Any


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


def load_json(path: Path) -> dict:
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except FileNotFoundError:
        return {}
    return data if isinstance(data, dict) else {}


def code_handler_rows(rows: list[dict]) -> list[dict]:
    return [row for row in rows if row.get("handlerSection") == ".text"]


def unique_handlers(rows: list[dict]) -> set[str]:
    return {
        str(row.get("handlerVaHex"))
        for row in rows
        if row.get("handlerVaHex")
    }


def json_value(value: Any) -> str:
    if isinstance(value, (dict, list)):
        return json.dumps(value, ensure_ascii=False, sort_keys=True)
    return str(value)


def text_contains(text: str, needle: str) -> bool:
    return needle in str(text or "")


def parse_first_int(pattern: str, text: str, default: int = 0) -> int:
    match = re.search(pattern, str(text or ""))
    if not match:
        return default
    try:
        return int(match.group(1))
    except ValueError:
        return default


def parse_opcode_list(pattern: str, text: str) -> list[str]:
    match = re.search(pattern, str(text or ""))
    if not match:
        return []
    return [part for part in match.group(1).split(",") if part]


def dialogue_candidate_payload(payload: dict | None) -> dict:
    payload = payload or {}
    dialogue = payload.get("dialogue")
    return dialogue if isinstance(dialogue, dict) else payload


def browser_dialogue_replay_summary(smoke: dict | None) -> dict:
    smoke = dialogue_candidate_payload(smoke)
    dialogue_chrome = str(smoke.get("dialogueChrome") or "")
    dialogue_control_flow = str(smoke.get("dialogueControlFlow") or "")
    dialogue_render_state = str(smoke.get("dialogueRenderState") or "")
    dialogue_display_style = str(smoke.get("dialogueDisplayStyle") or "")
    dialogue_display_style_probe = str(smoke.get("dialogueDisplayStyleProbe") or "")
    dialogue_cursor_relative = str(smoke.get("dialogueCursorRelativeLayout") or "")
    dialogue_literal_payload = str(smoke.get("dialogueLiteralPayload") or "")
    dialogue_object_focus = str(smoke.get("dialogueObjectFocus") or "")
    pointer_dialogue_advance = str(smoke.get("pointerDialogueAdvance") or "")
    keyboard_input_wait_latch = str(smoke.get("keyboardInputWaitLatch") or "")
    return {
        "source": "out/dialogue_candidate_review_summary.json",
        "status": smoke.get("status") or "not-generated",
        "groundedControlEffectsExecuted": text_contains(dialogue_chrome, "groundedControlEffects=True"),
        "groundedControlExecutedEventCount": parse_first_int(r"groundedControlEvents=(\d+)", dialogue_chrome),
        "groundedControlExecutedOpcodeSet": parse_opcode_list(r"groundedControlOpcodes=([0-9a-fx,]+)", dialogue_chrome),
        "groundedControlWaitForInputReleaseCount": parse_first_int(r"groundedControlWaits=(\d+)", dialogue_chrome),
        "groundedControlBranchCallCount": parse_first_int(r"groundedControlBranches=(\d+)", dialogue_chrome),
        "groundedControlObjectPositionCount": parse_first_int(r"groundedControlObjects=(\d+)", dialogue_chrome),
        "controlFlowSummary": dialogue_control_flow,
        "branchExecutionStepCount": parse_first_int(r"branchExecSteps=(\d+)", dialogue_control_flow),
        "branchExecutionRenderCount": parse_first_int(r"branchExecRenders=(\d+)", dialogue_control_flow),
        "branchExecutionLineAdvanceCount": parse_first_int(r"branchExecLineAdvances=(\d+)", dialogue_control_flow),
        "branchExecutionWaitCount": parse_first_int(r"branchExecWaits=(\d+)", dialogue_control_flow),
        "branchExecutionReturnCount": parse_first_int(r"branchExecReturns=(\d+)", dialogue_control_flow),
        "branchTextOrigin": "5:0x0045ee44:16,16" if text_contains(dialogue_control_flow, "branchTextOrigin=5:0x0045ee44:16,16") else "",
        "branchGlobalTimer": "5:0x0045ee4c:0" if text_contains(dialogue_control_flow, "branchGlobalTimer=5:0x0045ee4c:0") else "",
        "renderStateSnapshotCount": parse_first_int(r"snapshots=(\d+)", dialogue_render_state),
        "renderStateSummary": dialogue_render_state,
        "displayStyleSummary": dialogue_display_style,
        "displayStyleProbeSummary": dialogue_display_style_probe,
        "cursorRelativeSummary": dialogue_cursor_relative,
        "literalPayloadSummary": dialogue_literal_payload,
        "objectFocusSummary": dialogue_object_focus,
        "inputWaitSummary": " ".join(part for part in [pointer_dialogue_advance, keyboard_input_wait_latch] if part),
        "browserEventVmControlFlowSummaryImplemented": text_contains(
            dialogue_control_flow,
            "browserEventVmControlFlowSummaryImplemented=True",
        ),
        "browserEventVmBranchExecutionImplemented": text_contains(
            dialogue_control_flow,
            "browserEventVmBranchExecutionImplemented=True",
        ),
        "browserEventVmBranchTextOriginImplemented": text_contains(
            dialogue_control_flow,
            "browserEventVmBranchTextOriginImplemented=True",
        ),
        "browserEventVmBranchGlobalTimerImplemented": text_contains(
            dialogue_control_flow,
            "browserEventVmBranchGlobalTimerImplemented=True",
        ),
        "browserEventVmRenderStateSnapshotImplemented": text_contains(
            dialogue_render_state,
            "browserEventVmRenderStateSnapshotImplemented=True",
        ),
        "browserEventVmDisplayStyleImplemented": (
            text_contains(dialogue_display_style, "browserEventVmDisplayStyleImplemented=True")
            and text_contains(dialogue_display_style_probe, "browserEventVmDisplayStyleImplemented=True")
        ),
        "browserEventVmDisplayTimerStateImplemented": text_contains(
            dialogue_display_style,
            "browserEventVmDisplayTimerStateImplemented=True",
        ),
        "browserEventVmBranchDisplayStyleImplemented": text_contains(
            dialogue_display_style_probe,
            "branchDisplayStyle=12:0x004ec78e:1:emphasis",
        ),
        "browserEventVmBranchObjectPositionImplemented": (
            text_contains(dialogue_display_style_probe, "branchObjectPosition=12:0x004ec79a:144,248")
            and text_contains(dialogue_cursor_relative, "branchObject=5:0x004e8000:0,352")
        ),
        "browserEventVmBranchDisplayTimerImplemented": (
            text_contains(dialogue_display_style_probe, "branchDisplayTimer=12:0x004ec7a2:2880")
            and text_contains(dialogue_cursor_relative, "branchDisplayTimer=5:0x004e7ffc:1856")
        ),
        "browserEventVmCursorRelativeLayoutImplemented": text_contains(
            dialogue_cursor_relative,
            "browserEventVmCursorRelativeLayoutImplemented=True",
        ),
        "browserEventVmBranchCursorRelativeImplemented": text_contains(
            dialogue_cursor_relative,
            "browserEventVmBranchCursorRelativeImplemented=True",
        ),
        "browserEventVmBranchDataBankImplemented": text_contains(
            dialogue_cursor_relative,
            "branchDataBank=4:0x004e8088:0",
        ),
        "browserEventVmLiteralTextPayloadLineImplemented": text_contains(
            dialogue_literal_payload,
            "browserEventVmLiteralTextPayloadLineImplemented=True",
        ),
        "browserEventVmObjectPositionFocusImplemented": text_contains(
            dialogue_object_focus,
            "browserEventVmObjectPositionFocusImplemented=True",
        ),
        "browserDialogueInputWaitLatchImplemented": (
            text_contains(pointer_dialogue_advance, "branchWaitLatch=True")
            and text_contains(keyboard_input_wait_latch, "browserDialogueInputWaitLatchImplemented=True")
        ),
        "browserGroundedControlReplayImplemented": (
            smoke.get("status") == "passed"
            and text_contains(dialogue_chrome, "groundedControlEffects=True")
            and text_contains(dialogue_control_flow, "browserEventVmBranchExecutionImplemented=True")
            and text_contains(dialogue_render_state, "browserEventVmRenderStateSnapshotImplemented=True")
            and text_contains(dialogue_display_style_probe, "branchDisplayStyle=12:0x004ec78e:1:emphasis")
            and text_contains(dialogue_cursor_relative, "browserEventVmCursorRelativeLayoutImplemented=True")
            and text_contains(dialogue_literal_payload, "browserEventVmLiteralTextPayloadLineImplemented=True")
            and text_contains(dialogue_object_focus, "browserEventVmObjectPositionFocusImplemented=True")
        ),
        "browserEventVmFullImplementation": False,
        "originalEventVmRuntimeImplemented": False,
        "originalStoryFlagRuntimeImplemented": False,
    }


def promotion_gate_rows() -> list[dict]:
    return [
        {
            "id": "fullInstructionLengthDecode",
            "status": "missing",
            "requiredEvidence": "Decode the full save-selector/event-object VM instruction length space.",
            "currentEvidence": "Only text-source, cursor, and selected control/display opcode lengths are grounded.",
        },
        {
            "id": "fullOperandLayoutDecode",
            "status": "missing",
            "requiredEvidence": "Decode operand layouts and stack/state effects for the full relevant VM opcode set.",
            "currentEvidence": "Text-source, cursor, and selected control/display effects are grounded; the rest remains partial.",
        },
        {
            "id": "routeLinkedEventVmExecution",
            "status": "missing",
            "requiredEvidence": "Capture normal map interaction dispatching a story dialogue through the original event/object VM.",
            "currentEvidence": "Indexed browser dialogue candidates replay extracted metadata without original route dispatch.",
        },
        {
            "id": "sceneEventDialogueBinding",
            "status": "missing",
            "requiredEvidence": "Bind scene event records or runtime dispatch to a story dialogue block.",
            "currentEvidence": "Scene event text refs are zero and dialogue blocks are storage-indexed candidates.",
        },
        {
            "id": "originalStoryFlagMutation",
            "status": "missing",
            "requiredEvidence": "Identify and execute original story/event flag mutations for completed dialogue.",
            "currentEvidence": "Browser progress flags are prototype-local save/load markers.",
        },
        {
            "id": "fullBrowserEventVmImplementation",
            "status": "missing",
            "requiredEvidence": "Execute the full original event/object VM bytecode path in the browser runtime.",
            "currentEvidence": "Browser replay is limited to known opcode traces for indexed dialogue candidates.",
        },
        {
            "id": "eventDrivenBattleEntry",
            "status": "missing",
            "requiredEvidence": "Prove event VM dispatch into original battle entry.",
            "currentEvidence": "Dialogue-battle links and battle rows are browser prototype surfaces.",
        },
    ]


def promotion_gate_summary() -> dict:
    required_gates = promotion_gate_rows()
    missing_gate_ids = [
        row["id"]
        for row in required_gates
        if row["status"] != "passed"
    ]
    return {
        "source": "out/event_vm_semantics_gap.json",
        "classification": "browser-partial-known-opcode-replay-not-original-event-vm-runtime",
        "browserReplayScope": (
            "known text-source/cursor/control/display opcode replay for indexed dialogue candidates"
        ),
        "browserReplayPromotesOriginalEventVm": False,
        "allRequiredGatesPassed": False,
        "requiredGateCount": len(required_gates),
        "missingGateCount": len(missing_gate_ids),
        "missingGateIds": missing_gate_ids,
        "requiredGates": required_gates,
    }


def build_summary(
    script_handler_table: dict,
    branch_state_dispatch: dict,
    event_handler_text_refs: dict,
    event_text_source_flow: dict,
    event_vm_opcode_semantics: dict,
    event_dialogue_blocks: dict,
    scene_event_text_refs: dict,
    candidate_dialogue_progress_browser_smoke: dict | None = None,
) -> dict:
    script_entries = script_handler_table.get("entries") or []
    dispatch_table = branch_state_dispatch.get("eventHandlerTable") or {}
    dispatch_entries = branch_state_dispatch.get("tableEntries") or []
    dispatch_code_entries = code_handler_rows(dispatch_entries)
    script_summary = {
        "source": "out/script_handler_table.json",
        "scope": script_handler_table.get("scope") or "not-generated",
        "handlerTableVaHex": script_handler_table.get("handlerTableVaHex"),
        "opcodeCount": script_handler_table.get("opcodeCount", len(script_entries)),
        "codeHandlerCount": script_handler_table.get("codeHandlerCount", 0),
        "defaultHandlerCount": script_handler_table.get("defaultHandlerCount", 0),
        "defaultHandlerVaHex": script_handler_table.get("defaultHandlerVaHex"),
        "firstByteCandidateOnly": True,
        "instructionLengthFullyDecoded": False,
        "operandLayoutFullyDecoded": False,
    }
    event_table_summary = {
        "source": "out/save_selector_branch_state_dispatch.json",
        "tableVaHex": dispatch_table.get("tableVaHex"),
        "entryCount": dispatch_table.get("entryCount", len(dispatch_entries)),
        "tableEntryCount": len(dispatch_entries),
        "textEntryCount": len(dispatch_code_entries),
        "uniqueCodeHandlerCount": len(unique_handlers(dispatch_code_entries)),
        "dispatcherVaHex": dispatch_table.get("dispatcherVaHex"),
        "dispatchCallVaHex": dispatch_table.get("dispatchCallVaHex"),
        "dispatcherVerified": dispatch_table.get("dispatcherVerified") is True,
        "handlerScanCount": event_handler_text_refs.get("handlerCount", 0),
        "matchedTextHandlerCount": event_handler_text_refs.get("matchedHandlerCount", 0),
        "directTextRoutineHandlerCount": event_handler_text_refs.get("directTextRoutineHandlerCount", 0),
        "classificationCounts": event_handler_text_refs.get("classificationCounts") or {},
    }
    text_flow_summary = {
        "source": "out/event_text_source_flow.json",
        "commandCount": event_text_source_flow.get("commandCount", 0),
        "commandCountsByOpcode": event_text_source_flow.get("commandCountsByOpcode") or {},
        "opcode0dMediumCommandCount": event_text_source_flow.get("opcode0dMediumCommandCount", 0),
        "routeLinkedCommandCount": event_text_source_flow.get("routeLinkedCommandCount", 0),
        "routeLinkedCommandsArePointerOverlap": event_text_source_flow.get("opcode0cAllPointerOverlap") is True,
        "pointerOverlapCountsByOpcode": event_text_source_flow.get("pointerOverlapCountsByOpcode") or {},
        "confidenceCounts": event_text_source_flow.get("confidenceCounts") or {},
    }
    opcode_coverage = event_vm_opcode_semantics.get("coverage") or {}
    opcode_checks = event_vm_opcode_semantics.get("checks") or {}
    opcode_semantics_summary = {
        "source": "out/event_vm_opcode_semantics.json",
        "status": event_vm_opcode_semantics.get("promotionStatus") or "not-generated",
        "dispatcherVerified": opcode_checks.get("eventObjectDispatcherVerified") is True,
        "textSourceOpcodeLengthsDecoded": opcode_checks.get("textSourceOpcodeLengthsDecoded") is True,
        "textSourceOperandSemanticsGrounded": opcode_checks.get("textSourceOperandSemanticsGrounded") is True,
        "decodedTextSourceOpcodeLengths": opcode_coverage.get("decodedTextSourceOpcodeLengths") or {},
        "displayCursorOpcodeGrounded": opcode_checks.get("displayCursorOpcodeGrounded") is True,
        "decodedDisplayCursorOpcodeLengths": opcode_coverage.get("decodedDisplayCursorOpcodeLengths") or {},
        "displayCursorCommandCount": opcode_coverage.get("displayCursorCommandCount", 0),
        "controlOpcodeSemanticsGrounded": opcode_checks.get("controlOpcodeSemanticsGrounded") is True,
        "groundedControlOpcodeCount": opcode_coverage.get("groundedControlOpcodeCount", 0),
        "decodedControlOpcodeLengths": opcode_coverage.get("decodedControlOpcodeLengths") or {},
        "variableLengthControlOpcodes": opcode_coverage.get("variableLengthControlOpcodes") or {},
        "controlOpcodeCommandCounts": opcode_coverage.get("controlOpcodeCommandCounts") or {},
        "groundedControlCommandCount": opcode_coverage.get("groundedControlCommandCount", 0),
        "groundedControlEventCount": opcode_coverage.get("groundedControlEventCount", 0),
        "groundedControlEventOpcodeSet": opcode_coverage.get("groundedControlEventOpcodeSet") or [],
        "groundedControlSampleEffectExecutionCount": opcode_coverage.get(
            "groundedControlSampleEffectExecutionCount",
            0,
        ),
        "browserGroundedControlEventsPreserved": (
            opcode_checks.get("browserGroundedControlEventsPreserved") is True
        ),
        "literalStorageOpcodeObserved": opcode_checks.get("literalStorageOpcodeObserved") is True,
        "literalStorageEventCount": opcode_coverage.get("literalStorageEventCount", 0),
        "opcode0cPointerOverlapNotPromoted": opcode_checks.get("opcode0cPointerOverlapNotPromoted") is True,
        "browserPartialReplayMatchesTextSemantics": (
            opcode_checks.get("browserPartialReplayMatchesTextSemantics") is True
        ),
        "routeLinkedEventVmExecution": opcode_checks.get("routeLinkedEventVmExecution") is True,
        "browserFullEventVmImplemented": opcode_checks.get("browserFullEventVmImplemented") is True,
        "fullInstructionLengthFullyDecoded": opcode_checks.get("fullInstructionLengthFullyDecoded") is True,
        "fullOperandLayoutFullyDecoded": opcode_checks.get("fullOperandLayoutFullyDecoded") is True,
    }
    dialogue_summary = {
        "source": "out/event_dialogue_blocks.json",
        "blockCount": event_dialogue_blocks.get("blockCount", 0),
        "dialogueLikeBlockCount": event_dialogue_blocks.get("dialogueLikeBlockCount", 0),
        "routeLinkedBlockCount": event_dialogue_blocks.get("routeLinkedBlockCount", 0),
        "classificationCounts": event_dialogue_blocks.get("classificationCounts") or {},
        "partialVmReplayBlockCount": sum(
            1
            for block in event_dialogue_blocks.get("blocks") or []
            if (block.get("vmTrace") or {}).get("browserEventVmPartialReplayImplemented") is True
        ),
        "partialVmReplayOpcodeSet": sorted({
            opcode
            for block in event_dialogue_blocks.get("blocks") or []
            for opcode in ((block.get("vmTrace") or {}).get("implementedOpcodes") or [])
        }),
        "literalVmReplayBlockCount": sum(
            1
            for block in event_dialogue_blocks.get("blocks") or []
            if ((block.get("vmTrace") or {}).get("literalTextEventCount") or 0) > 0
        ),
        "literalVmReplayEventCount": sum(
            int((block.get("vmTrace") or {}).get("literalTextEventCount") or 0)
            for block in event_dialogue_blocks.get("blocks") or []
        ),
        "groundedControlReplayBlockCount": sum(
            1
            for block in event_dialogue_blocks.get("blocks") or []
            if ((block.get("vmTrace") or {}).get("groundedControlEventCount") or 0) > 0
        ),
        "groundedControlReplayEventCount": sum(
            int((block.get("vmTrace") or {}).get("groundedControlEventCount") or 0)
            for block in event_dialogue_blocks.get("blocks") or []
        ),
        "groundedControlReplayOpcodeSet": sorted({
            opcode
            for block in event_dialogue_blocks.get("blocks") or []
            for opcode in ((block.get("vmTrace") or {}).get("groundedControlOpcodeSet") or [])
        }),
    }
    scene_text_summary = {
        "source": "out/scene_event_text_refs.json",
        "eventCount": scene_event_text_refs.get("eventCount", 0),
        "directTextRefCount": scene_event_text_refs.get("directTextRefCount", 0),
        "textTableRefCount": scene_event_text_refs.get("textTableRefCount", 0),
        "textCandidateCount": scene_event_text_refs.get("textCandidateCount", 0),
        "textClusterCount": scene_event_text_refs.get("textClusterCount", 0),
    }
    browser_replay_summary = browser_dialogue_replay_summary(candidate_dialogue_progress_browser_smoke)
    promotion_gate = promotion_gate_summary()
    checks = {
        "scriptHandlerTableMapped": bool(script_summary["handlerTableVaHex"]),
        "eventObjectHandlerTableMapped": (
            bool(event_table_summary["tableVaHex"])
            and event_table_summary["dispatcherVerified"]
            and event_table_summary["tableEntryCount"] == event_table_summary["entryCount"]
        ),
        "opcode0bTextRoutineGrounded": event_table_summary["directTextRoutineHandlerCount"] >= 1,
        "opcode0dTextSourceGrounded": text_flow_summary["opcode0dMediumCommandCount"] > 0,
        "textSourceOpcodeLengthsDecoded": opcode_semantics_summary["textSourceOpcodeLengthsDecoded"],
        "textSourceOperandSemanticsGrounded": opcode_semantics_summary["textSourceOperandSemanticsGrounded"],
        "displayCursorOpcodeGrounded": opcode_semantics_summary["displayCursorOpcodeGrounded"],
        "controlOpcodeSemanticsGrounded": opcode_semantics_summary["controlOpcodeSemanticsGrounded"],
        "browserGroundedControlEventsPreserved": opcode_semantics_summary["browserGroundedControlEventsPreserved"],
        "literalStorageOpcodeObserved": opcode_semantics_summary["literalStorageOpcodeObserved"],
        "opcode0cPointerOverlapNotPromoted": opcode_semantics_summary["opcode0cPointerOverlapNotPromoted"],
        "dialogueStorageIndexed": dialogue_summary["blockCount"] > 0,
        "browserDialogueProgressSmoke": browser_replay_summary["status"] == "passed",
        "browserGroundedControlReplayImplemented": browser_replay_summary["browserGroundedControlReplayImplemented"],
        "browserEventVmBranchExecution": browser_replay_summary["browserEventVmBranchExecutionImplemented"],
        "browserEventVmRenderStateSnapshot": browser_replay_summary["browserEventVmRenderStateSnapshotImplemented"],
        "browserEventVmDisplayStyle": browser_replay_summary["browserEventVmDisplayStyleImplemented"],
        "browserEventVmCursorRelativeLayout": browser_replay_summary["browserEventVmCursorRelativeLayoutImplemented"],
        "browserEventVmLiteralPayloadLine": browser_replay_summary["browserEventVmLiteralTextPayloadLineImplemented"],
        "browserEventVmObjectPositionFocus": browser_replay_summary["browserEventVmObjectPositionFocusImplemented"],
        "browserDialogueInputWaitLatch": browser_replay_summary["browserDialogueInputWaitLatchImplemented"],
        "instructionLengthFullyDecoded": False,
        "operandLayoutFullyDecoded": False,
        "routeLinkedEventVmExecution": False,
        "sceneEventDirectTextRefs": scene_text_summary["directTextRefCount"] > 0,
        "browserEventVmPartialReplay": dialogue_summary["partialVmReplayBlockCount"] > 0,
        "browserReplayPromotesOriginalEventVm": False,
        "browserEventVmImplementation": False,
        "eventVmPromotionGatePassed": False,
        "storyDialogueRouteProof": False,
    }
    gap_summary = [
        {
            "id": "instructionLengthFullyDecoded",
            "status": "missing",
            "gap": (
                "Text-source event/object opcodes 0x0b/0x0c/0x0d, cursor opcode 0x02, and control/display "
                "opcodes 0x03/0x04/0x06/0x07/0x08/0x09/0x0a/0x0e/0x15/0x1b/0x37 now have grounded lengths, "
                "variants, or stack effects, but the full "
                "save-selector and event/object VM instruction set is not decoded."
            ),
        },
        {
            "id": "operandLayoutFullyDecoded",
            "status": "missing",
            "gap": (
                "Text-source operand effects, opcode 0x02 cursor effects, and several control/display effects "
                "are grounded, but many event/object opcodes and the save-selector stream operand layouts remain partial."
            ),
        },
        {
            "id": "routeLinkedEventVmExecution",
            "status": "missing",
            "gap": "No normal map interaction has been captured dispatching a story dialogue block through the event/object VM.",
        },
        {
            "id": "sceneEventDirectTextRefs",
            "status": "missing",
            "gap": "Current scene event windows have no direct Korean text pointer or text-table references.",
        },
        {
            "id": "browserEventVmImplementation",
            "status": "missing",
            "gap": (
                "The web runtime now records a partial 0x02/0x0d/0x0b/0x35 replay trace for indexed dialogue "
                "candidates and WebKit-smoke-verified bounded branch/control replay for dialogue render/style/cursor/"
                "object/input-wait snapshots, but it still does not execute the full original event/object VM bytecode."
            ),
        },
    ]
    return {
        "scope": "Event/object VM and save-selector script VM semantic coverage for dialogue proof.",
        "source": [
            "out/script_handler_table.json",
            "out/save_selector_branch_state_dispatch.json",
            "out/event_handler_text_refs.json",
            "out/event_text_source_flow.json",
            "out/event_vm_opcode_semantics.json",
            "out/event_dialogue_blocks.json",
            "out/scene_event_text_refs.json",
            "out/dialogue_candidate_review_summary.json",
        ],
        "promotionStatus": "handler-storage-grounded-execution-missing",
        "browserReplayClassification": promotion_gate["classification"],
        "browserReplayScope": promotion_gate["browserReplayScope"],
        "browserReplayPromotesOriginalEventVm": promotion_gate["browserReplayPromotesOriginalEventVm"],
        "saveSelectorScriptHandlerTable": script_summary,
        "eventObjectHandlerTable": event_table_summary,
        "eventTextSourceFlow": text_flow_summary,
        "eventVmOpcodeSemantics": opcode_semantics_summary,
        "dialogueStorageIndex": dialogue_summary,
        "sceneEventTextRefs": scene_text_summary,
        "browserDialogueReplaySmoke": browser_replay_summary,
        "eventVmPromotionGate": promotion_gate,
        "checks": checks,
        "gapSummary": gap_summary,
        "remainingOriginalEventVmProofInputs": promotion_gate["missingGateIds"],
        "nextEvidenceNeeded": [
            "Capture route-linked event/object VM execution from normal map interaction.",
            "Decode full instruction lengths and operand layouts for the relevant VM opcodes.",
            "Bind scene event records or runtime dispatch to a story dialogue block instead of storage-only candidates.",
            "Keep browser partial replay non-promoting until every event VM promotion gate has passed.",
        ],
        "conclusion": (
            "Handler tables, opcode 0x0b text rendering, opcode 0x0d text-source production, and dialogue "
            "storage blocks are grounded. The evidence still cannot promote story dialogue execution because "
            "only the text-source subset, opcode 0x02 cursor/separator, and a small control/display opcode "
            "subset are grounded; route-linked VM execution is absent, scene event text refs are zero, and the "
            "browser runtime only records partial dialogue replay plus bounded browser-side branch/control replay "
            "for indexed dialogue candidates rather than executing the full original event VM bytecode. The "
            "event VM promotion gate classifies that browser replay as non-promoting until route dispatch, full "
            "decode, story flag mutation, full browser VM execution, and event-driven battle entry are proven."
        ),
    }


def markdown(summary: dict) -> str:
    sections = [
        ("Save-Selector Script Handler Table", summary["saveSelectorScriptHandlerTable"]),
        ("Event/Object Handler Table", summary["eventObjectHandlerTable"]),
        ("Event Text Source Flow", summary["eventTextSourceFlow"]),
        ("Event VM Opcode Semantics", summary["eventVmOpcodeSemantics"]),
        ("Dialogue Storage Index", summary["dialogueStorageIndex"]),
        ("Scene Event Text Refs", summary["sceneEventTextRefs"]),
        ("Browser Dialogue Replay Smoke", summary["browserDialogueReplaySmoke"]),
        ("Event VM Promotion Gate", summary["eventVmPromotionGate"]),
        ("Checks", summary["checks"]),
    ]
    lines = [
        "# Event VM Semantics Gap",
        "",
        summary["conclusion"],
        "",
        f"- promotion status: `{summary['promotionStatus']}`",
        f"- browserReplayClassification: `{summary['browserReplayClassification']}`",
        f"- browserReplayPromotesOriginalEventVm: `{summary['browserReplayPromotesOriginalEventVm']}`",
        f"- remainingOriginalEventVmProofInputs: `{json_value(summary['remainingOriginalEventVmProofInputs'])}`",
        "",
    ]
    for title, rows in sections:
        lines.extend([
            f"## {title}",
            "",
            "| field | value |",
            "| --- | --- |",
        ])
        for key, value in rows.items():
            lines.append(f"| {key} | {json_value(value)} |")
        lines.append("")
    lines.extend([
        "## Gaps",
        "",
        "| id | status | gap |",
        "| --- | --- | --- |",
    ])
    for row in summary["gapSummary"]:
        lines.append(f"| {row['id']} | `{row['status']}` | {row['gap']} |")
    lines.extend([
        "",
        "## Next Evidence Needed",
        "",
    ])
    lines.extend(f"- {item}" for item in summary["nextEvidenceNeeded"])
    return "\n".join(lines) + "\n"


def html_table(rows: dict) -> str:
    return "\n".join(
        f"<tr><td>{html.escape(key)}</td><td>{html.escape(json_value(value))}</td></tr>"
        for key, value in rows.items()
    )


def html_page(summary: dict) -> str:
    sections = [
        ("Save-Selector Script Handler Table", summary["saveSelectorScriptHandlerTable"]),
        ("Event/Object Handler Table", summary["eventObjectHandlerTable"]),
        ("Event Text Source Flow", summary["eventTextSourceFlow"]),
        ("Event VM Opcode Semantics", summary["eventVmOpcodeSemantics"]),
        ("Dialogue Storage Index", summary["dialogueStorageIndex"]),
        ("Scene Event Text Refs", summary["sceneEventTextRefs"]),
        ("Browser Dialogue Replay Smoke", summary["browserDialogueReplaySmoke"]),
        ("Event VM Promotion Gate", summary["eventVmPromotionGate"]),
        ("Checks", summary["checks"]),
    ]
    section_html = []
    for title, rows in sections:
        section_html.extend([
            f"  <h2>{html.escape(title)}</h2>",
            (
                "  <table><thead><tr><th>field</th><th>value</th></tr></thead>"
                f"<tbody>{html_table(rows)}</tbody></table>"
            ),
        ])
    gap_rows = "\n".join(
        "<tr>"
        f"<td>{html.escape(row['id'])}</td>"
        f"<td><code>{html.escape(row['status'])}</code></td>"
        f"<td>{html.escape(row['gap'])}</td>"
        "</tr>"
        for row in summary["gapSummary"]
    )
    next_items = "\n".join(f"<li>{html.escape(item)}</li>" for item in summary["nextEvidenceNeeded"])
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        "  <title>Event VM Semantics Gap</title>",
        "  <style>body{font-family:system-ui,sans-serif;margin:24px;line-height:1.45;max-width:1200px}table{border-collapse:collapse;width:100%;margin:16px 0}td,th{border:1px solid #ddd;padding:6px 8px;text-align:left;vertical-align:top}th{background:#f5f5f5}code{white-space:nowrap}</style>",
        "</head>",
        "<body>",
        "  <h1>Event VM Semantics Gap</h1>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        f"  <p><b>Promotion status:</b> <code>{html.escape(summary['promotionStatus'])}</code></p>",
        (
            "  <p><b>browserReplayClassification:</b> "
            f"<code>{html.escape(summary['browserReplayClassification'])}</code></p>"
        ),
        (
            "  <p><b>browserReplayPromotesOriginalEventVm:</b> "
            f"<code>{html.escape(json_value(summary['browserReplayPromotesOriginalEventVm']))}</code></p>"
        ),
        (
            "  <p><b>remainingOriginalEventVmProofInputs:</b> "
            f"<code>{html.escape(json_value(summary['remainingOriginalEventVmProofInputs']))}</code></p>"
        ),
        *section_html,
        "  <h2>Gaps</h2>",
        f"  <table><thead><tr><th>id</th><th>status</th><th>gap</th></tr></thead><tbody>{gap_rows}</tbody></table>",
        "  <h2>Next Evidence Needed</h2>",
        f"  <ul>{next_items}</ul>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "event_vm_semantics_gap.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "event_vm_semantics_gap.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_json(args.out_dir / "script_handler_table.json"),
        load_json(args.out_dir / "save_selector_branch_state_dispatch.json"),
        load_json(args.out_dir / "event_handler_text_refs.json"),
        load_json(args.out_dir / "event_text_source_flow.json"),
        load_json(args.out_dir / "event_vm_opcode_semantics.json"),
        load_json(args.out_dir / "event_dialogue_blocks.json"),
        load_json(args.out_dir / "scene_event_text_refs.json"),
        load_json(args.out_dir / "dialogue_candidate_review_summary.json"),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote event VM semantics gap -> {args.out_dir / 'event_vm_semantics_gap.html'}")


if __name__ == "__main__":
    main()
