#!/usr/bin/env python3
"""Summarize the gap between event/battle prototype coverage and original systems."""
from __future__ import annotations

import argparse
import html
import json
from pathlib import Path


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


def count_event_dialogue_blocks(data: dict) -> int:
    if isinstance(data.get("blockCount"), int):
        return data["blockCount"]
    blocks = data.get("blocks")
    return len(blocks) if isinstance(blocks, list) else 0


def count_battle_backgrounds(data: dict) -> int:
    if isinstance(data.get("backgroundCount"), int):
        return data["backgroundCount"]
    backgrounds = data.get("backgrounds")
    if isinstance(backgrounds, list):
        return len(backgrounds)
    if isinstance(backgrounds, dict):
        return len(backgrounds)
    if isinstance(data, dict):
        return len([key for key in data if str(key).startswith("btl_")])
    return 0


def count_battle_candidates(data: dict) -> int:
    if isinstance(data.get("candidateCount"), int):
        return data["candidateCount"]
    candidates = data.get("candidates")
    return len(candidates) if isinstance(candidates, list) else 0


def smoke_text(smoke: dict, key: str) -> str:
    checks = smoke.get("checks") if isinstance(smoke, dict) else {}
    value = checks.get(key) if isinstance(checks, dict) else ""
    return value if isinstance(value, str) else ""


def has_all(text: str, needles: list[str]) -> bool:
    return all(needle in text for needle in needles)


def build_event_vm_text_evidence(
    event_text_source_flow: dict | None,
    event_handler_text_refs: dict | None,
    scene_event_text_refs: dict | None,
) -> dict:
    event_text_source_flow = event_text_source_flow or {}
    event_handler_text_refs = event_handler_text_refs or {}
    scene_event_text_refs = scene_event_text_refs or {}
    route_linked_count = event_text_source_flow.get("routeLinkedCommandCount", 0)
    route_linked_pointer_overlap = event_text_source_flow.get("opcode0cAllPointerOverlap") is True
    opcode0b_text_routine = event_handler_text_refs.get("directTextRoutineHandlerCount", 0) >= 1
    opcode0d_context_producer = event_text_source_flow.get("opcode0dMediumCommandCount", 0) > 0
    scene_direct_refs = scene_event_text_refs.get("directTextRefCount", 0)
    scene_table_refs = scene_event_text_refs.get("textTableRefCount", 0)
    route_linked_story_dialogue_proof = (
        route_linked_count > 0
        and not route_linked_pointer_overlap
        and (scene_direct_refs > 0 or scene_table_refs > 0)
    )
    return {
        "source": [
            "out/event_text_source_flow.json",
            "out/event_handler_text_refs.json",
            "out/scene_event_text_refs.json",
        ],
        "handlerCount": event_handler_text_refs.get("handlerCount", 0),
        "matchedHandlerCount": event_handler_text_refs.get("matchedHandlerCount", 0),
        "directTextRoutineHandlerCount": event_handler_text_refs.get("directTextRoutineHandlerCount", 0),
        "opcode0bTextRoutineGrounded": opcode0b_text_routine,
        "opcode0dContextProducerGrounded": opcode0d_context_producer,
        "commandCount": event_text_source_flow.get("commandCount", 0),
        "opcode0dMediumCommandCount": event_text_source_flow.get("opcode0dMediumCommandCount", 0),
        "routeLinkedCommandCount": route_linked_count,
        "routeLinkedCommandsArePointerOverlap": route_linked_pointer_overlap,
        "sceneEventCount": scene_event_text_refs.get("eventCount", 0),
        "sceneEventDirectTextRefCount": scene_direct_refs,
        "sceneEventTextTableRefCount": scene_table_refs,
        "routeLinkedStoryDialogueProof": route_linked_story_dialogue_proof,
        "classification": "text-handler-and-storage-grounded-route-execution-missing",
        "conclusion": (
            "Opcode 0x0b text rendering and opcode 0x0d context+0x28 source commands are grounded, "
            "but the route-linked command hits are pointer-byte overlaps and current scene event windows have no "
            "direct Korean text/table refs. This is text storage/handler evidence, not route-linked story dialogue proof."
        ),
    }


def build_savedata_coverage_evidence(savedata_sample_deltas: dict | None) -> dict:
    savedata_sample_deltas = savedata_sample_deltas or {}
    coverage = savedata_sample_deltas.get("semanticCoverage") or {}
    equipment = coverage.get("equipmentEvidence") or {}
    status = coverage.get("equipmentStatusStoryOffsetStatus") or "not-generated"
    return {
        "source": "out/savedata_sample_deltas.json",
        "status": status,
        "expectedSaveSize": coverage.get("expectedSaveSize", 0),
        "knownFieldCount": coverage.get("knownFieldCount", 0),
        "knownSemanticByteCount": coverage.get("knownSemanticByteCount", 0),
        "unknownByteCount": coverage.get("unknownByteCount", 0),
        "unknownRangeCount": coverage.get("unknownRangeCount", 0),
        "varyingByteCount": coverage.get("varyingByteCount", 0),
        "knownVaryingByteCount": coverage.get("knownVaryingByteCount", 0),
        "unknownVaryingByteCount": coverage.get("unknownVaryingByteCount", 0),
        "fieldCategoryByteCounts": coverage.get("fieldCategoryByteCounts") or {},
        "equipmentEvidenceStatus": equipment.get("status"),
        "unsupportedOriginalSystems": coverage.get("unsupportedOriginalSystems") or [],
        "conclusion": coverage.get("conclusion") or (
            "Savedata semantic coverage has not been generated, so equipment/status/story offsets cannot be promoted."
        ),
    }


def build_original_battle_data_evidence(original_battle_data_gap: dict | None) -> dict:
    original_battle_data_gap = original_battle_data_gap or {}
    checks = original_battle_data_gap.get("originalSystemChecks") or {}
    return {
        "source": "out/original_battle_data_gap.json",
        "status": original_battle_data_gap.get("promotionStatus") or "not-generated",
        "proofFound": original_battle_data_gap.get("proofFound"),
        "originalBattleDataProofFound": original_battle_data_gap.get("originalBattleDataProofFound"),
        "failedOriginalBattleDataGateIds": original_battle_data_gap.get("failedOriginalBattleDataGateIds") or [],
        "missingEvidence": original_battle_data_gap.get("missingEvidence") or [],
        "evidenceRefCount": original_battle_data_gap.get("evidenceRefCount"),
        "enemyObjectSpriteImageCount": original_battle_data_gap.get("enemyObjectSpriteImageCount", 0),
        "bossSpriteImageCount": original_battle_data_gap.get("bossSpriteImageCount", 0),
        "battleSpriteImageCount": original_battle_data_gap.get("battleSpriteImageCount", 0),
        "battleBackgroundTilemapCount": original_battle_data_gap.get("battleBackgroundTilemapCount", 0),
        "battleEventCandidateCount": original_battle_data_gap.get("battleEventCandidateCount", 0),
        "battleTextEntryCount": original_battle_data_gap.get("battleTextEntryCount", 0),
        "battleResourceDescriptorsMapped": checks.get("battleResourceDescriptorsMapped") is True,
        "battleResourceDescriptorStatus": original_battle_data_gap.get("battleResourceDescriptorStatus") or "not-generated",
        "battleResourceDescriptorRowCount": original_battle_data_gap.get("battleResourceDescriptorRowCount", 0),
        "battleBackgroundDescriptorReferenceCount": original_battle_data_gap.get("battleBackgroundDescriptorReferenceCount", 0),
        "battleBackgroundDescriptorReferencedPayloadCount": original_battle_data_gap.get(
            "battleBackgroundDescriptorReferencedPayloadCount", 0
        ),
        "battleBackgroundDescriptorMissingExeRefs": original_battle_data_gap.get(
            "battleBackgroundDescriptorMissingExeRefs"
        ) or [],
        "battleSpriteDescriptorReferenceCount": original_battle_data_gap.get("battleSpriteDescriptorReferenceCount", 0),
        "battleSpriteDescriptorReferencedPayloadCount": original_battle_data_gap.get(
            "battleSpriteDescriptorReferencedPayloadCount", 0
        ),
        "enemySpriteDescriptorReferenceCount": original_battle_data_gap.get("enemySpriteDescriptorReferenceCount", 0),
        "enemySpriteDescriptorReferencedPayloadCount": original_battle_data_gap.get(
            "enemySpriteDescriptorReferencedPayloadCount", 0
        ),
        "enemySpriteDescriptorMissingExeRefs": original_battle_data_gap.get("enemySpriteDescriptorMissingExeRefs") or [],
        "resourceDescriptorsAreEnemyRows": original_battle_data_gap.get("resourceDescriptorsAreEnemyRows") is True,
        "resourceDescriptorsContainStatsOrRewards": original_battle_data_gap.get(
            "resourceDescriptorsContainStatsOrRewards"
        ) is True,
        "resourceDescriptorsProveBattleEntry": original_battle_data_gap.get("resourceDescriptorsProveBattleEntry") is True,
        "battleEnemySpriteCandidatesMapped": checks.get("battleEnemySpriteCandidatesMapped") is True,
        "battleEnemySpriteCandidateStatus": original_battle_data_gap.get(
            "battleEnemySpriteCandidateStatus"
        ) or "not-generated",
        "battleEnemySpriteCandidateCount": original_battle_data_gap.get("battleEnemySpriteCandidateCount", 0),
        "battleEnemySpriteCandidateUniqueSpriteCount": original_battle_data_gap.get(
            "battleEnemySpriteCandidateUniqueSpriteCount", 0
        ),
        "battleEnemySpriteCandidateAssetCount": original_battle_data_gap.get("battleEnemySpriteCandidateAssetCount", 0),
        "battleEnemySpriteExtractedAssetCount": original_battle_data_gap.get("battleEnemySpriteExtractedAssetCount", 0),
        "battleEnemySpriteRuntimeSelectableAssetCount": original_battle_data_gap.get(
            "battleEnemySpriteRuntimeSelectableAssetCount",
            0,
        ),
        "battleEnemySpriteAllExtractedAssetsSelectable": original_battle_data_gap.get(
            "battleEnemySpriteAllExtractedAssetsSelectable"
        ) is True,
        "battleEnemySpriteCandidatesOriginalRowsBound": original_battle_data_gap.get(
            "battleEnemySpriteCandidatesOriginalRowsBound"
        ) is True,
        "battleEnemySpriteCandidatesStatsOrRewardsBound": original_battle_data_gap.get(
            "battleEnemySpriteCandidatesStatsOrRewardsBound"
        ) is True,
        "battleEnemySpriteCandidatesBattleEntryProven": original_battle_data_gap.get(
            "battleEnemySpriteCandidatesBattleEntryProven"
        ) is True,
        "battleNumericContextsScanned": checks.get("battleNumericContextsScanned") is True,
        "battleNumericContextStatus": original_battle_data_gap.get("battleNumericContextStatus") or "not-generated",
        "battleNumericDescriptorContextCount": original_battle_data_gap.get(
            "battleNumericDescriptorContextCount", 0
        ),
        "battleNumericCandidatePairContextCount": original_battle_data_gap.get(
            "battleNumericCandidatePairContextCount", 0
        ),
        "battleNumericCloseCandidatePairContextCount": original_battle_data_gap.get(
            "battleNumericCloseCandidatePairContextCount", 0
        ),
        "battleNumericCandidateContextsWithLocalRuns": original_battle_data_gap.get(
            "battleNumericCandidateContextsWithLocalRuns",
            0,
        ),
        "battleNumericCloseCandidateContextsWithLocalRuns": original_battle_data_gap.get(
            "battleNumericCloseCandidateContextsWithLocalRuns",
            0,
        ),
        "battleNumericCandidateContextsWithEnemyRuns": original_battle_data_gap.get(
            "battleNumericCandidateContextsWithEnemyRuns",
            0,
        ),
        "battleNumericCandidateContextsWithBackgroundRuns": original_battle_data_gap.get(
            "battleNumericCandidateContextsWithBackgroundRuns",
            0,
        ),
        "battleNumericMaxLocalPlainRunCount": original_battle_data_gap.get(
            "battleNumericMaxLocalPlainRunCount",
            0,
        ),
        "battleNumericPlainRunCount": original_battle_data_gap.get("battleNumericPlainRunCount", 0),
        "battleNumericContextsWithPlainRuns": original_battle_data_gap.get(
            "battleNumericContextsWithPlainRuns", 0
        ),
        "battleNumericPromotableEnemyRowCount": original_battle_data_gap.get(
            "battleNumericPromotableEnemyRowCount", 0
        ),
        "battleNumericPromotableRewardRowCount": original_battle_data_gap.get(
            "battleNumericPromotableRewardRowCount", 0
        ),
        "battleNumericRunsPromotedToEnemyRows": original_battle_data_gap.get(
            "battleNumericRunsPromotedToEnemyRows"
        ) is True,
        "battleNumericBattleEntryExecutionIdentified": original_battle_data_gap.get(
            "battleNumericBattleEntryExecutionIdentified"
        ) is True,
        "battleActionHandlerStatus": original_battle_data_gap.get("battleActionHandlerStatus") or "not-generated",
        "battleActionHandlerGrounded": original_battle_data_gap.get("battleActionHandlerGrounded") is True,
        "battleActionOpcodeHex": original_battle_data_gap.get("battleActionOpcodeHex") or "",
        "battleActionHandlerVaHex": original_battle_data_gap.get("battleActionHandlerVaHex") or "",
        "battleActionTableVaHex": original_battle_data_gap.get("battleActionTableVaHex") or "",
        "battleActionTableKey": original_battle_data_gap.get("battleActionTableKey") or "",
        "battleActionTableEntryCount": original_battle_data_gap.get("battleActionTableEntryCount", 0),
        "battleActionHandlerStorageOpcodeCount": original_battle_data_gap.get(
            "battleActionHandlerStorageOpcodeCount",
            0,
        ),
        "battleActionOpcodeInDialogueStorage": original_battle_data_gap.get(
            "battleActionOpcodeInDialogueStorage"
        ) is True,
        "battleActionHandlerBattleEntryDispatchProofFound": original_battle_data_gap.get(
            "battleActionHandlerBattleEntryDispatchProofFound"
        ) is True,
        "battleActionHandlerOriginalCombatFormulaIdentified": original_battle_data_gap.get(
            "battleActionHandlerOriginalCombatFormulaIdentified"
        ) is True,
        "eventBattleDispatchIdentified": checks.get("eventBattleDispatchIdentified") is True,
        "enemyRowsBoundToSprites": checks.get("enemyRowsBoundToSprites") is True,
        "enemyStatTableIdentified": checks.get("enemyStatTableIdentified") is True,
        "formationTableIdentified": checks.get("formationTableIdentified") is True,
        "encounterTableIdentified": checks.get("encounterTableIdentified") is True,
        "rewardTableIdentified": checks.get("rewardTableIdentified") is True,
        "combatFormulaIdentified": checks.get("combatFormulaIdentified") is True,
        "prototypeRuntimeMarkers": original_battle_data_gap.get("prototypeRuntimeMarkers") or {},
        "conclusion": original_battle_data_gap.get("conclusion") or (
            "Original battle data gap has not been generated, so enemy/formula/reward systems cannot be promoted."
        ),
    }


def build_event_vm_semantics_gap_evidence(event_vm_semantics_gap: dict | None) -> dict:
    event_vm_semantics_gap = event_vm_semantics_gap or {}
    script_table = event_vm_semantics_gap.get("saveSelectorScriptHandlerTable") or {}
    event_table = event_vm_semantics_gap.get("eventObjectHandlerTable") or {}
    text_flow = event_vm_semantics_gap.get("eventTextSourceFlow") or {}
    opcode_semantics = event_vm_semantics_gap.get("eventVmOpcodeSemantics") or {}
    dialogue_storage = event_vm_semantics_gap.get("dialogueStorageIndex") or {}
    scene_refs = event_vm_semantics_gap.get("sceneEventTextRefs") or {}
    browser_replay = event_vm_semantics_gap.get("browserDialogueReplaySmoke") or {}
    promotion_gate = event_vm_semantics_gap.get("eventVmPromotionGate") or {}
    checks = event_vm_semantics_gap.get("checks") or {}
    return {
        "source": "out/event_vm_semantics_gap.json",
        "status": event_vm_semantics_gap.get("promotionStatus") or "not-generated",
        "browserReplayClassification": (
            event_vm_semantics_gap.get("browserReplayClassification")
            or promotion_gate.get("classification")
            or "not-generated"
        ),
        "browserReplayScope": (
            event_vm_semantics_gap.get("browserReplayScope")
            or promotion_gate.get("browserReplayScope")
            or ""
        ),
        "browserReplayPromotesOriginalEventVm": (
            event_vm_semantics_gap.get("browserReplayPromotesOriginalEventVm") is True
            or promotion_gate.get("browserReplayPromotesOriginalEventVm") is True
        ),
        "eventVmPromotionGatePassed": promotion_gate.get("allRequiredGatesPassed") is True,
        "eventVmPromotionRequiredGateCount": promotion_gate.get("requiredGateCount", 0),
        "eventVmPromotionMissingGateCount": promotion_gate.get("missingGateCount", 0),
        "eventVmPromotionMissingGateIds": promotion_gate.get("missingGateIds") or [],
        "scriptHandlerTableVaHex": script_table.get("handlerTableVaHex"),
        "scriptHandlerOpcodeCount": script_table.get("opcodeCount", 0),
        "scriptHandlerCodeHandlerCount": script_table.get("codeHandlerCount", 0),
        "scriptHandlerDefaultHandlerCount": script_table.get("defaultHandlerCount", 0),
        "scriptFirstByteCandidateOnly": script_table.get("firstByteCandidateOnly") is True,
        "eventObjectTableVaHex": event_table.get("tableVaHex"),
        "eventObjectDispatchEntryCount": event_table.get("entryCount", 0),
        "eventObjectUniqueCodeHandlerCount": event_table.get("uniqueCodeHandlerCount", 0),
        "eventObjectDispatcherVerified": event_table.get("dispatcherVerified") is True,
        "eventHandlerScanCount": event_table.get("handlerScanCount", 0),
        "eventHandlerMatchedTextCount": event_table.get("matchedTextHandlerCount", 0),
        "directTextRoutineHandlerCount": event_table.get("directTextRoutineHandlerCount", 0),
        "textCommandCount": text_flow.get("commandCount", 0),
        "opcode0dMediumCommandCount": text_flow.get("opcode0dMediumCommandCount", 0),
        "routeLinkedCommandCount": text_flow.get("routeLinkedCommandCount", 0),
        "routeLinkedCommandsArePointerOverlap": text_flow.get("routeLinkedCommandsArePointerOverlap") is True,
        "textSourceOpcodeLengthsDecoded": opcode_semantics.get("textSourceOpcodeLengthsDecoded") is True,
        "textSourceOperandSemanticsGrounded": opcode_semantics.get("textSourceOperandSemanticsGrounded") is True,
        "decodedTextSourceOpcodeLengths": opcode_semantics.get("decodedTextSourceOpcodeLengths") or {},
        "displayCursorOpcodeGrounded": opcode_semantics.get("displayCursorOpcodeGrounded") is True,
        "decodedDisplayCursorOpcodeLengths": opcode_semantics.get("decodedDisplayCursorOpcodeLengths") or {},
        "displayCursorCommandCount": opcode_semantics.get("displayCursorCommandCount", 0),
        "controlOpcodeSemanticsGrounded": opcode_semantics.get("controlOpcodeSemanticsGrounded") is True,
        "groundedControlOpcodeCount": opcode_semantics.get("groundedControlOpcodeCount", 0),
        "decodedControlOpcodeLengths": opcode_semantics.get("decodedControlOpcodeLengths") or {},
        "variableLengthControlOpcodes": opcode_semantics.get("variableLengthControlOpcodes") or {},
        "controlOpcodeCommandCounts": opcode_semantics.get("controlOpcodeCommandCounts") or {},
        "groundedControlCommandCount": opcode_semantics.get("groundedControlCommandCount", 0),
        "groundedControlEventCount": opcode_semantics.get("groundedControlEventCount", 0),
        "groundedControlEventOpcodeSet": opcode_semantics.get("groundedControlEventOpcodeSet") or [],
        "groundedControlSampleEffectExecutionCount": opcode_semantics.get(
            "groundedControlSampleEffectExecutionCount",
            0,
        ),
        "browserGroundedControlEventsPreserved": opcode_semantics.get(
            "browserGroundedControlEventsPreserved"
        ) is True,
        "browserReplayStatus": browser_replay.get("status") or "not-generated",
        "browserGroundedControlReplayImplemented": browser_replay.get(
            "browserGroundedControlReplayImplemented"
        ) is True,
        "browserGroundedControlExecutedEventCount": browser_replay.get(
            "groundedControlExecutedEventCount",
            0,
        ),
        "browserGroundedControlExecutedOpcodeSet": browser_replay.get(
            "groundedControlExecutedOpcodeSet"
        ) or [],
        "browserBranchExecutionStepCount": browser_replay.get("branchExecutionStepCount", 0),
        "browserBranchExecutionRenderCount": browser_replay.get("branchExecutionRenderCount", 0),
        "browserBranchExecutionLineAdvanceCount": browser_replay.get(
            "branchExecutionLineAdvanceCount",
            0,
        ),
        "browserBranchExecutionWaitCount": browser_replay.get("branchExecutionWaitCount", 0),
        "browserRenderStateSnapshotCount": browser_replay.get("renderStateSnapshotCount", 0),
        "browserEventVmBranchExecutionImplemented": browser_replay.get(
            "browserEventVmBranchExecutionImplemented"
        ) is True,
        "browserEventVmRenderStateSnapshotImplemented": browser_replay.get(
            "browserEventVmRenderStateSnapshotImplemented"
        ) is True,
        "browserEventVmDisplayStyleImplemented": browser_replay.get(
            "browserEventVmDisplayStyleImplemented"
        ) is True,
        "browserEventVmCursorRelativeLayoutImplemented": browser_replay.get(
            "browserEventVmCursorRelativeLayoutImplemented"
        ) is True,
        "browserEventVmLiteralTextPayloadLineImplemented": browser_replay.get(
            "browserEventVmLiteralTextPayloadLineImplemented"
        ) is True,
        "browserEventVmObjectPositionFocusImplemented": browser_replay.get(
            "browserEventVmObjectPositionFocusImplemented"
        ) is True,
        "literalStorageOpcodeObserved": opcode_semantics.get("literalStorageOpcodeObserved") is True,
        "literalStorageEventCount": opcode_semantics.get("literalStorageEventCount", 0),
        "opcode0cPointerOverlapNotPromoted": opcode_semantics.get("opcode0cPointerOverlapNotPromoted") is True,
        "dialogueBlockCount": dialogue_storage.get("blockCount", 0),
        "routeLinkedDialogueBlockCount": dialogue_storage.get("routeLinkedBlockCount", 0),
        "sceneEventDirectTextRefCount": scene_refs.get("directTextRefCount", 0),
        "sceneEventTextTableRefCount": scene_refs.get("textTableRefCount", 0),
        "scriptHandlerTableMapped": checks.get("scriptHandlerTableMapped") is True,
        "eventObjectHandlerTableMapped": checks.get("eventObjectHandlerTableMapped") is True,
        "instructionLengthFullyDecoded": checks.get("instructionLengthFullyDecoded") is True,
        "operandLayoutFullyDecoded": checks.get("operandLayoutFullyDecoded") is True,
        "routeLinkedEventVmExecution": checks.get("routeLinkedEventVmExecution") is True,
        "browserEventVmImplementation": checks.get("browserEventVmImplementation") is True,
        "storyDialogueRouteProof": checks.get("storyDialogueRouteProof") is True,
        "conclusion": event_vm_semantics_gap.get("conclusion") or (
            "Event VM semantic gap has not been generated, so VM handler/storage evidence cannot be promoted."
        ),
    }


def build_summary(
    event_dialogue_blocks: dict,
    battle_backgrounds: dict,
    battle_event_candidates: dict,
    mobile_browser_smoke: dict | None = None,
    event_text_source_flow: dict | None = None,
    event_handler_text_refs: dict | None = None,
    scene_event_text_refs: dict | None = None,
    savedata_sample_deltas: dict | None = None,
    original_battle_data_gap: dict | None = None,
    event_vm_semantics_gap: dict | None = None,
) -> dict:
    mobile_browser_smoke = mobile_browser_smoke or {}
    dialogue_smoke = smoke_text(mobile_browser_smoke, "dialogue")
    battle_smoke = smoke_text(mobile_browser_smoke, "battle")
    savedat_battle_item_smoke = smoke_text(mobile_browser_smoke, "savedatBattleItem")
    savedat_menu_item_smoke = smoke_text(mobile_browser_smoke, "savedatMenuItem")
    save_point_candidate_smoke = smoke_text(mobile_browser_smoke, "savePointCandidate")
    event_object_preview_smoke = smoke_text(mobile_browser_smoke, "eventObjectPreview")
    smoke_checks = {
        "source": "out/mobile_browser_controls_summary.json",
        "dialogueSmokeVerified": has_all(dialogue_smoke, ["dialogueButton", "event-dialogue-block-005"]),
        "battleSmokeVerified": has_all(battle_smoke, ["battleButton", "rewardGranted=True"]),
        "savedatBattleItemSmokeVerified": has_all(
            savedat_battle_item_smoke,
            ["ATK=22 DEF=7", "약초 3->2", "revive=Smashu"],
        ),
        "savedatMenuItemSmokeVerified": has_all(savedat_menu_item_smoke, ["antidote", "savedHp"]),
        "savePointCandidateSmokeVerified": has_all(
            save_point_candidate_smoke,
            ["save-point-candidates:map1_01a", "scope=global-review", "count=6", "runtime=False"],
        ),
        "eventObjectPreviewSmokeVerified": has_all(
            event_object_preview_smoke,
            ["map2_03l", "candidates=3", "loaded=3", "assets=zm_2,zg_kni,zs_rg", "events=True"],
        ),
        "raw": {
            "dialogue": dialogue_smoke,
            "battle": battle_smoke,
            "savedatBattleItem": savedat_battle_item_smoke,
            "savedatMenuItem": savedat_menu_item_smoke,
            "savePointCandidate": save_point_candidate_smoke,
            "eventObjectPreview": event_object_preview_smoke,
        },
    }
    original_system_checks = {
        "eventTextHandlerAndStorageGrounded": False,
        "battleResourceDescriptorsMapped": False,
        "battleEnemySpriteCandidatesMapped": False,
        "routeLinkedEventVmExecution": False,
        "eventDrivenBattleEntry": False,
        "originalEnemyData": False,
        "originalCombatFormulas": False,
        "originalRewards": False,
        "equipmentOwnershipAndEquippedOffsets": False,
        "originalStatusFlagOffsets": False,
        "storyFlagMutation": False,
    }
    event_vm_text_evidence = build_event_vm_text_evidence(
        event_text_source_flow,
        event_handler_text_refs,
        scene_event_text_refs,
    )
    event_vm_semantics_gap_evidence = build_event_vm_semantics_gap_evidence(event_vm_semantics_gap)
    savedata_coverage_evidence = build_savedata_coverage_evidence(savedata_sample_deltas)
    original_battle_data_evidence = build_original_battle_data_evidence(original_battle_data_gap)
    original_system_checks["eventTextHandlerAndStorageGrounded"] = (
        event_vm_text_evidence["opcode0bTextRoutineGrounded"]
        and event_vm_text_evidence["opcode0dContextProducerGrounded"]
    )
    original_system_checks["battleResourceDescriptorsMapped"] = original_battle_data_evidence[
        "battleResourceDescriptorsMapped"
    ]
    original_system_checks["battleEnemySpriteCandidatesMapped"] = original_battle_data_evidence[
        "battleEnemySpriteCandidatesMapped"
    ]
    prototype_coverage = [
        {
            "id": "candidateDialoguePlayback",
            "title": "Candidate dialogue playback",
            "status": "prototype-smoke-verified" if smoke_checks["dialogueSmokeVerified"] else "prototype-only",
            "evidence": "dialogueButton opens event-dialogue-block-005 and advances a line in WebKit smoke.",
        },
        {
            "id": "battleReviewScene",
            "title": "Battle review scene",
            "status": "prototype-smoke-verified" if smoke_checks["battleSmokeVerified"] else "prototype-only",
            "evidence": "battleButton starts a candidate battle and smoke verifies rewardGranted=True.",
        },
        {
            "id": "saveBackedStatsSkillsItems",
            "title": "Save-backed stats, skills, and items",
            "status": (
                "prototype-smoke-verified"
                if smoke_checks["savedatBattleItemSmokeVerified"] and smoke_checks["savedatMenuItemSmokeVerified"]
                else "prototype-only"
            ),
            "evidence": "Public savedat smoke verifies ATK/DEF, skill MP sync, item consumption, status cure, and revive modeling.",
        },
        {
            "id": "savePointCandidateReview",
            "title": "Read-only save-point candidate review",
            "status": (
                "prototype-smoke-verified"
                if smoke_checks["savePointCandidateSmokeVerified"]
                else "prototype-only"
            ),
            "evidence": "The map menu opens the save-point candidate runtime subset as a read-only review dialogue in WebKit smoke.",
        },
        {
            "id": "eventObjectPreview",
            "title": "Linked event object preview",
            "status": (
                "prototype-smoke-verified"
                if smoke_checks["eventObjectPreviewSmokeVerified"]
                else "prototype-only"
            ),
            "evidence": "events=1 overlays linked z*.cns object sprite candidates on map2_03l active points in WebKit smoke.",
        },
        {
            "id": "manualPartyBattleRows",
            "title": "Manual party battle rows",
            "status": "prototype-fallback",
            "evidence": "party=rinshan,smash can populate prototype battle rows, but this is not original join/story state.",
        },
    ]
    original_system_gaps = [
        {
            "id": "routeLinkedEventVmExecution",
            "title": "Route-linked event VM execution",
            "status": "missing",
            "gap": (
                "Dialogue playback is candidate/debug playback. Opcode 0x0b/0x0d text handler/storage evidence is "
                "grounded, but route-linked command hits are pointer overlaps and no map event route has been proven "
                "to dispatch a story dialogue block. Event VM semantic status is "
                f"{event_vm_semantics_gap_evidence['status']}."
            ),
        },
        {
            "id": "eventDrivenBattleEntry",
            "title": "Event-driven battle entry",
            "status": "missing",
            "gap": "The web battle scene is manually opened from UI/query state, not entered by original event battle opcode logic.",
        },
        {
            "id": "originalEnemyData",
            "title": "Original enemy data",
            "status": "missing",
            "gap": (
                f"{original_battle_data_evidence['enemyObjectSpriteImageCount']} enemy/object sprite images and "
                f"{original_battle_data_evidence['battleEventCandidateCount']} battle-resource dialogue candidates are "
                "extracted. EXE resource descriptors map "
                f"{original_battle_data_evidence['enemySpriteDescriptorReferenceCount']} enemy/object sprite refs and "
                f"{original_battle_data_evidence['battleBackgroundDescriptorReferenceCount']} battle-background refs, "
                f"and {original_battle_data_evidence['battleEnemySpriteCandidateCount']} battle review candidates have "
                "EXE-nearest enemy/object sprite visual candidates. Numeric context scanning observes "
                f"{original_battle_data_evidence['battleNumericPlainRunCount']} local small-number runs with "
                f"{original_battle_data_evidence['battleNumericCandidateContextsWithLocalRuns']} candidate pairs carrying "
                "local runs and "
                f"{original_battle_data_evidence['battleNumericPromotableEnemyRowCount']} promotable enemy rows, "
                f"and opcode {original_battle_data_evidence['battleActionOpcodeHex']} handler "
                f"{original_battle_data_evidence['battleActionHandlerVaHex']} grounds the "
                f"{original_battle_data_evidence['battleActionTableKey']} action text table while "
                f"battleActionOpcodeInDialogueStorage={original_battle_data_evidence['battleActionOpcodeInDialogueStorage']}. "
                "Those are resource/proximity/static context rows; enemy rows, formations, encounter tables, and "
                "per-battle enemy stats remain unidentified."
            ),
        },
        {
            "id": "originalCombatFormulas",
            "title": "Original combat formulas",
            "status": "missing",
            "gap": (
                "Damage, defense, skill cost/effect, speed, hit, status, and targeting formulas are prototype "
                f"approximations; original combatFormulaIdentified={original_battle_data_evidence['combatFormulaIdentified']}."
            ),
        },
        {
            "id": "originalRewards",
            "title": "Original rewards",
            "status": "missing",
            "gap": (
                "Victory money/EXP/item rewards are candidate prototype values, not original reward table execution; "
                f"rewardTableIdentified={original_battle_data_evidence['rewardTableIdentified']}."
            ),
        },
        {
            "id": "equipmentOwnershipAndEquippedOffsets",
            "title": "Equipment ownership/equipped offsets",
            "status": "missing",
            "gap": (
                "Equipment labels exist in extracted text/UI evidence, but savedat ownership and equipped-slot offsets "
                "are not mapped. Savedata semantic coverage currently maps "
                f"{savedata_coverage_evidence['knownSemanticByteCount']}/"
                f"{savedata_coverage_evidence['expectedSaveSize']} bytes and leaves "
                f"{savedata_coverage_evidence['unknownByteCount']} bytes unknown."
            ),
        },
        {
            "id": "originalStatusFlagOffsets",
            "title": "Original status flag offsets",
            "status": "missing",
            "gap": (
                "Poison/paralysis and fallen-state handling use web-local modeled status state, not original save/runtime "
                "flag offsets. Public samples still have "
                f"{savedata_coverage_evidence['unknownVaryingByteCount']} varying bytes inside unmapped savedata ranges."
            ),
        },
        {
            "id": "storyFlagMutation",
            "title": "Story flag mutation",
            "status": "missing",
            "gap": (
                "Party joins, event completion, battle completion, and story flags are not mutated through original state "
                f"logic; savedata status is {savedata_coverage_evidence['status']}."
            ),
        },
    ]
    event_count = count_event_dialogue_blocks(event_dialogue_blocks)
    background_count = count_battle_backgrounds(battle_backgrounds)
    candidate_count = count_battle_candidates(battle_event_candidates)
    return {
        "source": [
            "out/event_dialogue_blocks.json",
            "out/battle_backgrounds.json",
            "out/battle_event_candidates.json",
            "out/mobile_browser_controls_summary.json",
            "out/savedata_sample_deltas.json",
            "out/original_battle_data_gap.json",
            "out/event_vm_semantics_gap.json",
        ],
        "promotionStatus": "prototype-gap-audit",
        "eventDialogueBlockCount": event_count,
        "battleBackgroundCount": background_count,
        "battleEventCandidateCount": candidate_count,
        "eventVmTextEvidence": event_vm_text_evidence,
        "eventVmSemanticsGapEvidence": event_vm_semantics_gap_evidence,
        "savedataCoverageEvidence": savedata_coverage_evidence,
        "originalBattleDataEvidence": original_battle_data_evidence,
        "smokeChecks": smoke_checks,
        "implementedPrototypeCoverage": prototype_coverage,
        "originalSystemChecks": original_system_checks,
        "originalSystemGaps": original_system_gaps,
        "nextEvidenceNeeded": [
            "Capture route-linked event VM execution that opens a dialogue block from normal map interaction.",
            "Trace original inn/statue/recovery NPC save-point scripts before enabling original save behavior.",
            "Trace original object/NPC descriptor execution before promoting linked z*.cns previews to gameplay objects.",
            "Identify original battle-entry dispatch and bind EXE resource descriptors to encounter/enemy rows in event scripts.",
            "Map enemy data, combat formulas, reward tables, equipment offsets, status flags, and story-state mutations.",
        ],
        "conclusion": (
            f"{event_count} event dialogue blocks, {background_count} battle backgrounds, and {candidate_count} "
            "battle-adjacent dialogue candidates support a smoke-verified browser prototype. The prototype should "
            "remain partial because route-linked event VM execution, original battle entry, enemy/formula/reward data, "
            "equipment offsets, original status flags, and story-state mutation are still missing. Savedata coverage "
            f"maps {savedata_coverage_evidence['knownSemanticByteCount']}/"
            f"{savedata_coverage_evidence['expectedSaveSize']} bytes semantically, so the remaining save-state systems "
            "cannot be promoted from the current evidence. Original battle data evidence remains "
            f"{original_battle_data_evidence['status']}; event VM semantics remain "
            f"{event_vm_semantics_gap_evidence['status']}."
        ),
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Event/Battle Gap Audit",
        "",
        summary["conclusion"],
        "",
        f"- promotion status: `{summary['promotionStatus']}`",
        f"- event dialogue blocks: {summary['eventDialogueBlockCount']}",
        f"- battle backgrounds: {summary['battleBackgroundCount']}",
        f"- battle event candidates: {summary['battleEventCandidateCount']}",
        "",
        "## Smoke Checks",
        "",
        "| check | value |",
        "| --- | --- |",
    ]
    for key, value in summary["smokeChecks"].items():
        if key == "raw":
            continue
        lines.append(f"| {key} | {value} |")
    lines.extend([
        "",
        "## Event VM Text Evidence",
        "",
        "| field | value |",
        "| --- | --- |",
    ])
    for key, value in summary["eventVmTextEvidence"].items():
        if key in {"source", "conclusion"}:
            continue
        lines.append(f"| {key} | {value} |")
    lines.extend([
        "",
        summary["eventVmTextEvidence"]["conclusion"],
        "",
        "",
        "## Event VM Semantics Gap Evidence",
        "",
        summary["eventVmSemanticsGapEvidence"]["conclusion"],
        "",
        "| field | value |",
        "| --- | --- |",
    ])
    for key, value in summary["eventVmSemanticsGapEvidence"].items():
        if key in {"source", "conclusion"}:
            continue
        lines.append(f"| {key} | {value} |")
    lines.extend([
        "",
        "",
        "## Savedata Coverage Evidence",
        "",
        summary["savedataCoverageEvidence"]["conclusion"],
        "",
        "| field | value |",
        "| --- | --- |",
    ])
    for key, value in summary["savedataCoverageEvidence"].items():
        if key in {"source", "conclusion", "fieldCategoryByteCounts", "unsupportedOriginalSystems"}:
            continue
        lines.append(f"| {key} | {value} |")
    lines.extend([
        "",
        "",
        "## Original Battle Data Evidence",
        "",
        summary["originalBattleDataEvidence"]["conclusion"],
        "",
        "| field | value |",
        "| --- | --- |",
    ])
    for key, value in summary["originalBattleDataEvidence"].items():
        if key in {"source", "conclusion", "prototypeRuntimeMarkers"}:
            continue
        lines.append(f"| {key} | {value} |")
    lines.extend([
        "",
        "",
        "## Prototype Coverage",
        "",
        "| id | status | evidence |",
        "| --- | --- | --- |",
    ])
    for row in summary["implementedPrototypeCoverage"]:
        lines.append(f"| {row['id']} | `{row['status']}` | {row['evidence']} |")
    lines.extend([
        "",
        "## Original System Gaps",
        "",
        "| id | status | gap |",
        "| --- | --- | --- |",
    ])
    for row in summary["originalSystemGaps"]:
        lines.append(f"| {row['id']} | `{row['status']}` | {row['gap']} |")
    lines.extend([
        "",
        "## Next Evidence Needed",
        "",
    ])
    for item in summary["nextEvidenceNeeded"]:
        lines.append(f"- {item}")
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    smoke_rows = "\n".join(
        f"<tr><td>{html.escape(key)}</td><td>{html.escape(str(value))}</td></tr>"
        for key, value in summary["smokeChecks"].items()
        if key != "raw"
    )
    coverage_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['evidence'])}</td>"
        "</tr>"
        for row in summary["implementedPrototypeCoverage"]
    )
    event_vm_rows = "\n".join(
        f"<tr><td>{html.escape(key)}</td><td>{html.escape(str(value))}</td></tr>"
        for key, value in summary["eventVmTextEvidence"].items()
        if key not in {"source", "conclusion"}
    )
    event_vm_semantics_rows = "\n".join(
        f"<tr><td>{html.escape(key)}</td><td>{html.escape(str(value))}</td></tr>"
        for key, value in summary["eventVmSemanticsGapEvidence"].items()
        if key not in {"source", "conclusion"}
    )
    savedata_rows = "\n".join(
        f"<tr><td>{html.escape(key)}</td><td>{html.escape(str(value))}</td></tr>"
        for key, value in summary["savedataCoverageEvidence"].items()
        if key not in {"source", "conclusion", "fieldCategoryByteCounts", "unsupportedOriginalSystems"}
    )
    battle_data_rows = "\n".join(
        f"<tr><td>{html.escape(key)}</td><td>{html.escape(str(value))}</td></tr>"
        for key, value in summary["originalBattleDataEvidence"].items()
        if key not in {"source", "conclusion", "prototypeRuntimeMarkers"}
    )
    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["originalSystemGaps"]
    )
    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/Battle Gap Audit</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/Battle Gap Audit</h1>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        (
            "  <p><b>Coverage:</b> "
            f"dialogue blocks {summary['eventDialogueBlockCount']}, "
            f"battle backgrounds {summary['battleBackgroundCount']}, "
            f"battle candidates {summary['battleEventCandidateCount']}; "
            f"status <code>{html.escape(summary['promotionStatus'])}</code>.</p>"
        ),
        "  <h2>Smoke Checks</h2>",
        f"  <table><thead><tr><th>check</th><th>value</th></tr></thead><tbody>{smoke_rows}</tbody></table>",
        "  <h2>Event VM Text Evidence</h2>",
        f"  <p>{html.escape(summary['eventVmTextEvidence']['conclusion'])}</p>",
        f"  <table><thead><tr><th>field</th><th>value</th></tr></thead><tbody>{event_vm_rows}</tbody></table>",
        "  <h2>Event VM Semantics Gap Evidence</h2>",
        f"  <p>{html.escape(summary['eventVmSemanticsGapEvidence']['conclusion'])}</p>",
        f"  <table><thead><tr><th>field</th><th>value</th></tr></thead><tbody>{event_vm_semantics_rows}</tbody></table>",
        "  <h2>Savedata Coverage Evidence</h2>",
        f"  <p>{html.escape(summary['savedataCoverageEvidence']['conclusion'])}</p>",
        f"  <table><thead><tr><th>field</th><th>value</th></tr></thead><tbody>{savedata_rows}</tbody></table>",
        "  <h2>Original Battle Data Evidence</h2>",
        f"  <p>{html.escape(summary['originalBattleDataEvidence']['conclusion'])}</p>",
        f"  <table><thead><tr><th>field</th><th>value</th></tr></thead><tbody>{battle_data_rows}</tbody></table>",
        "  <h2>Prototype Coverage</h2>",
        f"  <table><thead><tr><th>id</th><th>status</th><th>evidence</th></tr></thead><tbody>{coverage_rows}</tbody></table>",
        "  <h2>Original System 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_battle_gap_audit.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "event_battle_gap_audit.md").write_text(markdown(summary), encoding="utf-8")
    (out_dir / "event_battle_gap_audit.html").write_text(html_page(summary), encoding="utf-8")


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 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 / "event_dialogue_blocks.json"),
        load_json(args.out_dir / "battle_backgrounds.json"),
        load_json(args.out_dir / "battle_event_candidates.json"),
        (load_json(args.out_dir / "mobile_browser_controls_summary.json").get("controls") or {}),
        load_json(args.out_dir / "event_text_source_flow.json"),
        load_json(args.out_dir / "event_handler_text_refs.json"),
        load_json(args.out_dir / "scene_event_text_refs.json"),
        load_json(args.out_dir / "savedata_sample_deltas.json"),
        load_json(args.out_dir / "original_battle_data_gap.json"),
        load_json(args.out_dir / "event_vm_semantics_gap.json"),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote event/battle gap audit -> {args.out_dir / 'event_battle_gap_audit.md'}")


if __name__ == "__main__":
    main()
