#!/usr/bin/env python3
"""Summarize why current battle assets are not original battle data yet."""
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_GAME = ROOT / "web" / "game.html"

BATTLE_TEXT_TABLE_KEYS = [
    "atahoActions",
    "actionSetA",
    "actionSetB",
    "battleCommands",
    "skillNamesA",
    "skillNamesB",
    "magicNames",
]

PROTOTYPE_SOURCE_MARKERS = {
    "prototypeEnemyStats": "hpMax: 42",
    "enemyVisualCandidate": "visual: enemyVisual",
    "enemyVisualNotOriginalRow": "originalEnemyRowBound: false",
    "candidateRewardFormula": "money: Math.max(0, Number(base.money || 12 + variant * 3))",
    "candidateAttackFormula": "commandDamage + levelBonus + atkBonus",
    "candidateIncomingDamageFormula": "baseDamage - defBonus",
}

FAILED_ORIGINAL_BATTLE_DATA_GATE_IDS = [
    "event-battle-dispatch",
    "enemy-row-sprite-binding",
    "enemy-stat-table",
    "formation-table",
    "encounter-table",
    "reward-table",
    "combat-formula",
]

ORIGINAL_BATTLE_DATA_MISSING_EVIDENCE = [
    "original event opcode/handler that enters battle mode",
    "enemy rows binding z*/boss_* sprites to identity, stats, and placement",
    "enemy HP/MP/ATK/DEF/speed/status-resistance table",
    "formation table placing enemy rows on btl_* backgrounds",
    "map/event encounter table selecting original formations",
    "original EXP/money/item reward table",
    "original combat damage/mitigation/status formula",
]

ORIGINAL_BATTLE_DATA_EVIDENCE_REFS = [
    {
        "path": "out/cns_payloads.json",
        "fields": ["enemyObjectSpriteImageCount", "bossSpriteImageCount", "battleSpriteImageCount"],
    },
    {
        "path": "out/text_tables.json",
        "fields": ["battleTextTableEntryCounts", "battleTextEntryCount"],
    },
    {
        "path": "out/battle_backgrounds.json",
        "fields": ["battleBackgroundTilemapCount"],
    },
    {
        "path": "out/battle_event_candidates.json",
        "fields": ["battleEventCandidateCount", "battleEventCandidateBlockCount", "battleEventCandidateBackgroundCount"],
    },
    {
        "path": "out/original_battle_resource_descriptors.json",
        "fields": [
            "battleResourceDescriptorStatus",
            "resourceDescriptorsAreEnemyRows",
            "resourceDescriptorsContainStatsOrRewards",
            "resourceDescriptorsProveBattleEntry",
        ],
    },
    {
        "path": "out/battle_enemy_candidates.json",
        "fields": [
            "battleEnemySpriteCandidateStatus",
            "battleEnemySpriteCandidatesOriginalRowsBound",
            "battleEnemySpriteCandidatesStatsOrRewardsBound",
            "battleEnemySpriteCandidatesBattleEntryProven",
        ],
    },
    {
        "path": "out/original_battle_numeric_context.json",
        "fields": [
            "battleNumericContextStatus",
            "battleNumericPromotableEnemyRowCount",
            "battleNumericPromotableRewardRowCount",
            "battleNumericBattleEntryExecutionIdentified",
        ],
    },
    {
        "path": "out/battle_action_handler_context.json",
        "fields": [
            "battleActionHandlerGrounded",
            "battleActionOpcodeInDialogueStorage",
            "battleEntryDispatchProofFound",
            "originalCombatFormulaIdentified",
        ],
    },
    {
        "path": "web/game.html",
        "fields": ["prototypeRuntimeMarkers"],
    },
]


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


def prefix(name: str) -> str:
    stem = Path(name).stem
    if "_" not in stem:
        return stem
    return stem.split("_", 1)[0]


def selected_payloads(cns_payloads: list[dict], starts: tuple[str, ...], kind: str | None = None) -> list[dict]:
    rows = [
        row for row in cns_payloads
        if str(row.get("name", "")).startswith(starts)
    ]
    if kind:
        rows = [row for row in rows if row.get("kind") == kind]
    return rows


def table_by_key(text_tables: dict) -> dict[str, dict]:
    return {
        str(table.get("key")): table
        for table in text_tables.get("tables") or []
        if table.get("key")
    }


def table_entry_count(table: dict | None) -> int:
    if not table:
        return 0
    entries = table.get("entries")
    return len(entries) if isinstance(entries, list) else int(table.get("entryCount") or 0)


def descriptor_class_summary(resource_descriptors: dict, cls: str) -> dict:
    classes = resource_descriptors.get("classSummary") or {}
    row = classes.get(cls)
    return row if isinstance(row, dict) else {}


def candidate_numeric_run_examples(battle_numeric_context: dict, limit: int = 12) -> list[dict]:
    rows = []
    for row in battle_numeric_context.get("candidateContexts") or []:
        if int(row.get("localPlainNumericRunCount") or 0) <= 0:
            continue
        first_run = row.get("backgroundFirstNumericRun") or row.get("enemyFirstNumericRun") or {}
        rows.append({
            "candidateId": row.get("candidateId") or "",
            "battleBackground": row.get("battleBackground") or "",
            "enemyCns": row.get("enemyCns") or "",
            "refDistance": row.get("refDistance", 0),
            "closeResourcePair": row.get("closeResourcePair") is True,
            "backgroundPlainNumericRunCount": row.get("backgroundPlainNumericRunCount", 0),
            "enemyPlainNumericRunCount": row.get("enemyPlainNumericRunCount", 0),
            "localPlainNumericRunCount": row.get("localPlainNumericRunCount", 0),
            "numericEvidenceClass": row.get("numericEvidenceClass") or "",
            "firstRunStartVaHex": first_run.get("startVaHex") or "-",
            "firstRunEndVaHex": first_run.get("endVaHex") or "-",
            "firstRunValues": first_run.get("values") or [],
            "promotionBlocker": row.get("promotionBlocker") or "",
        })
    return rows[:limit]


def build_summary(
    cns_payloads: list[dict],
    text_tables: dict,
    battle_backgrounds: dict,
    battle_event_candidates: dict,
    battle_resource_descriptors: dict | None = None,
    battle_enemy_candidates: dict | None = None,
    battle_numeric_context: dict | None = None,
    battle_action_handler_context: dict | None = None,
    web_index_text: str | None = None,
) -> dict:
    battle_resource_descriptors = battle_resource_descriptors or {}
    battle_enemy_candidates = battle_enemy_candidates or {}
    battle_numeric_context = battle_numeric_context or {}
    battle_action_handler_context = battle_action_handler_context or {}
    web_index_text = web_index_text if web_index_text is not None else (
        WEB_GAME.read_text(encoding="utf-8") if WEB_GAME.exists() else ""
    )
    enemy_sprite_rows = selected_payloads(cns_payloads, ("z", "boss"), "image")
    boss_rows = selected_payloads(cns_payloads, ("boss",), "image")
    battle_sprite_rows = selected_payloads(cns_payloads, ("btl_",), "image")
    tables = table_by_key(text_tables)
    battle_tables = [tables.get(key) for key in BATTLE_TEXT_TABLE_KEYS if tables.get(key)]
    battle_table_counts = {
        key: table_entry_count(tables.get(key))
        for key in BATTLE_TEXT_TABLE_KEYS
    }
    prototype_markers = {
        key: marker in web_index_text
        for key, marker in PROTOTYPE_SOURCE_MARKERS.items()
    }
    candidate_rows = battle_event_candidates.get("candidates") or []
    descriptor_checks = battle_resource_descriptors.get("checks") or {}
    background_descriptors = descriptor_class_summary(battle_resource_descriptors, "battle-background-tilemap")
    battle_sprite_descriptors = descriptor_class_summary(battle_resource_descriptors, "battle-sprite-image")
    enemy_sprite_descriptors = descriptor_class_summary(battle_resource_descriptors, "enemy-object-sprite-image")
    battle_enemy_candidate_checks = battle_enemy_candidates.get("checks") or {}
    battle_enemy_sprite_candidates_mapped = (
        battle_enemy_candidate_checks.get("battleEnemySpriteCandidatesMapped") is True
        and battle_enemy_candidate_checks.get("enemyAssetsDecoded") is True
    )
    battle_resource_descriptors_mapped = (
        descriptor_checks.get("battleBackgroundDescriptorsFound") is True
        and descriptor_checks.get("enemySpriteDescriptorsFound") is True
    )
    numeric_checks = battle_numeric_context.get("checks") or {}
    candidate_run_examples = candidate_numeric_run_examples(battle_numeric_context)
    battle_numeric_contexts_scanned = (
        numeric_checks.get("descriptorNumericContextsScanned") is True
        and numeric_checks.get("candidatePairNumericContextsScanned") is True
    )
    original_system_checks = {
        "enemySpriteAssetsExtracted": len(enemy_sprite_rows) > 0,
        "battleBackgroundsExtracted": len(battle_backgrounds) > 0,
        "battleActionTextTablesExtracted": sum(battle_table_counts.values()) > 0,
        "battleResourceDescriptorsMapped": battle_resource_descriptors_mapped,
        "battleEnemySpriteCandidatesMapped": battle_enemy_sprite_candidates_mapped,
        "battleNumericContextsScanned": battle_numeric_contexts_scanned,
        "battleActionHandlerGrounded": battle_action_handler_context.get("battleActionHandlerGrounded") is True,
        "battleActionOpcodeInDialogueStorage": (
            battle_action_handler_context.get("battleActionOpcodeInDialogueStorage") is True
        ),
        "eventBattleDispatchIdentified": False,
        "enemyRowsBoundToSprites": False,
        "enemyStatTableIdentified": False,
        "formationTableIdentified": False,
        "encounterTableIdentified": False,
        "rewardTableIdentified": False,
        "combatFormulaIdentified": False,
    }
    gaps = [
        {
            "id": "eventBattleDispatchIdentified",
            "status": "missing",
            "gap": "btl_* resources are adjacent to event-dialogue blocks, but no original event opcode path enters a battle scene.",
        },
        {
            "id": "enemyRowsBoundToSprites",
            "status": "missing",
            "gap": (
                "z*/boss_* CNS images are extracted as sprites, EXE resource-load descriptors are mapped, and "
                "battle review candidates can render nearest EXE enemy/object sprite candidates. Descriptor-local "
                "numeric context is scanned too, but no enemy row maps these graphics to enemy identity, stats, or "
                "battle placement."
            ),
        },
        {
            "id": "enemyStatTableIdentified",
            "status": "missing",
            "gap": "No HP/MP/ATK/DEF/speed/status-resistance table has been tied to the enemy sprite set or battle candidates.",
        },
        {
            "id": "formationTableIdentified",
            "status": "missing",
            "gap": "No formation table has been proven to place enemy rows on a btl_* background.",
        },
        {
            "id": "encounterTableIdentified",
            "status": "missing",
            "gap": "No map/event encounter table has been proven to choose a formation from normal gameplay.",
        },
        {
            "id": "rewardTableIdentified",
            "status": "missing",
            "gap": "The browser reward is a candidate formula based on background variant, not an original EXP/money/item reward table.",
        },
        {
            "id": "combatFormulaIdentified",
            "status": "missing",
            "gap": "The browser damage/mitigation formulas are prototype functions over savedat stats and command constants, not original code/data formulas.",
        },
    ]
    return {
        "scope": "Original battle data gap for enemy, formation, reward, and formula evidence.",
        "source": [
            "out/cns_payloads.json",
            "out/text_tables.json",
            "out/battle_backgrounds.json",
            "out/battle_event_candidates.json",
            "out/original_battle_resource_descriptors.json",
        "out/battle_enemy_candidates.json",
        "out/original_battle_numeric_context.json",
        "out/battle_action_handler_context.json",
        "web/game.html",
        ],
        "promotionStatus": "asset-and-text-evidence-only",
        "proofFound": False,
        "originalBattleDataProofFound": False,
        "failedOriginalBattleDataGateIds": FAILED_ORIGINAL_BATTLE_DATA_GATE_IDS,
        "missingEvidence": ORIGINAL_BATTLE_DATA_MISSING_EVIDENCE,
        "remainingProofs": ORIGINAL_BATTLE_DATA_MISSING_EVIDENCE,
        "battleBackgroundTilemapCount": len(battle_backgrounds),
        "battleEventCandidateCount": battle_event_candidates.get("candidateCount", len(candidate_rows)),
        "battleEventCandidateBlockCount": battle_event_candidates.get("blockCount", len({row.get("blockId") for row in candidate_rows})),
        "battleEventCandidateBackgroundCount": battle_event_candidates.get(
            "backgroundCount",
            len({row.get("battleBackground") for row in candidate_rows}),
        ),
        "enemyObjectSpriteImageCount": len(enemy_sprite_rows),
        "bossSpriteImageCount": len(boss_rows),
        "enemyObjectSpritePrefixCounts": dict(sorted(Counter(prefix(str(row.get("name", ""))) for row in enemy_sprite_rows).items())),
        "battleSpriteImageCount": len(battle_sprite_rows),
        "battleTextTableCount": len(battle_tables),
        "battleTextEntryCount": sum(battle_table_counts.values()),
        "battleTextTableEntryCounts": battle_table_counts,
        "battleResourceDescriptorStatus": battle_resource_descriptors.get("promotionStatus") or "not-generated",
        "battleResourceDescriptorRowCount": battle_resource_descriptors.get("resourceDescriptorRowCount", 0),
        "battleBackgroundDescriptorReferenceCount": background_descriptors.get("referenceCount", 0),
        "battleBackgroundDescriptorReferencedPayloadCount": background_descriptors.get("referencedPayloadCount", 0),
        "battleBackgroundDescriptorMissingExeRefs": background_descriptors.get("missingExeRefNames") or [],
        "battleSpriteDescriptorReferenceCount": battle_sprite_descriptors.get("referenceCount", 0),
        "battleSpriteDescriptorReferencedPayloadCount": battle_sprite_descriptors.get("referencedPayloadCount", 0),
        "battleSpriteDescriptorMissingExeRefs": battle_sprite_descriptors.get("missingExeRefNames") or [],
        "enemySpriteDescriptorReferenceCount": enemy_sprite_descriptors.get("referenceCount", 0),
        "enemySpriteDescriptorReferencedPayloadCount": enemy_sprite_descriptors.get("referencedPayloadCount", 0),
        "enemySpriteDescriptorMissingExeRefs": enemy_sprite_descriptors.get("missingExeRefNames") or [],
        "resourceDescriptorsAreEnemyRows": descriptor_checks.get("resourceDescriptorsAreEnemyRows") is True,
        "resourceDescriptorsContainStatsOrRewards": descriptor_checks.get("resourceDescriptorsContainStatsOrRewards") is True,
        "resourceDescriptorsProveBattleEntry": descriptor_checks.get("resourceDescriptorsProveBattleEntry") is True,
        "battleEnemySpriteCandidateStatus": battle_enemy_candidates.get("promotionStatus") or "not-generated",
        "battleEnemySpriteCandidateCount": battle_enemy_candidates.get("candidateCount", 0),
        "battleEnemySpriteCandidateUniqueSpriteCount": battle_enemy_candidates.get("uniqueEnemySpriteCount", 0),
        "battleEnemySpriteCandidateAssetCount": battle_enemy_candidates.get(
            "candidateAssetCount",
            battle_enemy_candidates.get("assetCount", 0),
        ),
        "battleEnemySpriteExtractedAssetCount": battle_enemy_candidates.get(
            "extractedEnemySpriteAssetCount",
            battle_enemy_candidates.get("assetCount", 0),
        ),
        "battleEnemySpriteRuntimeSelectableAssetCount": len(battle_enemy_candidates.get("assets") or {}),
        "battleEnemySpriteCandidatesMapped": battle_enemy_candidate_checks.get("battleEnemySpriteCandidatesMapped") is True,
        "battleEnemySpriteCandidateAssetsDecoded": battle_enemy_candidate_checks.get("enemyAssetsDecoded") is True,
        "battleEnemySpriteAllExtractedAssetsSelectable": (
            battle_enemy_candidate_checks.get("allExtractedEnemySpriteAssetsRuntimeSelectable") is True
        ),
        "battleEnemySpriteCandidatesOriginalRowsBound": (
            battle_enemy_candidate_checks.get("originalEnemyRowsBound") is True
        ),
        "battleEnemySpriteCandidatesStatsOrRewardsBound": (
            battle_enemy_candidate_checks.get("originalStatsOrRewardsBound") is True
        ),
        "battleEnemySpriteCandidatesBattleEntryProven": (
            battle_enemy_candidate_checks.get("originalBattleEntryProven") is True
        ),
        "battleNumericContextStatus": battle_numeric_context.get("promotionStatus") or "not-generated",
        "battleNumericDescriptorContextCount": battle_numeric_context.get("descriptorContextCount", 0),
        "battleNumericCandidatePairContextCount": battle_numeric_context.get("candidatePairContextCount", 0),
        "battleNumericCloseCandidatePairContextCount": battle_numeric_context.get("closeCandidatePairContextCount", 0),
        "battleNumericCandidateContextsWithLocalRuns": battle_numeric_context.get(
            "candidateContextsWithLocalPlainNumericRuns",
            0,
        ),
        "battleNumericCloseCandidateContextsWithLocalRuns": battle_numeric_context.get(
            "closeCandidateContextsWithLocalPlainNumericRuns",
            0,
        ),
        "battleNumericCandidateContextsWithEnemyRuns": battle_numeric_context.get(
            "candidateContextsWithEnemyPlainNumericRuns",
            0,
        ),
        "battleNumericCandidateContextsWithBackgroundRuns": battle_numeric_context.get(
            "candidateContextsWithBackgroundPlainNumericRuns",
            0,
        ),
        "battleNumericMaxLocalPlainRunCount": battle_numeric_context.get("maxLocalPlainNumericRunCount", 0),
        "battleNumericPlainRunCount": battle_numeric_context.get("plainNumericRunCount", 0),
        "battleNumericContextsWithPlainRuns": battle_numeric_context.get("descriptorContextsWithPlainNumericRuns", 0),
        "battleNumericPromotableEnemyRowCount": battle_numeric_context.get("promotableEnemyRowCount", 0),
        "battleNumericPromotableRewardRowCount": battle_numeric_context.get("promotableRewardRowCount", 0),
        "battleNumericRunsPromotedToEnemyRows": (
            numeric_checks.get("numericRunsPromotedToEnemyRows") is True
        ),
        "battleNumericBattleEntryExecutionIdentified": (
            numeric_checks.get("battleEntryExecutionIdentified") is True
        ),
        "battleActionHandlerStatus": battle_action_handler_context.get("promotionStatus") or "not-generated",
        "battleActionHandlerGrounded": battle_action_handler_context.get("battleActionHandlerGrounded") is True,
        "battleActionOpcodeHex": battle_action_handler_context.get("battleActionOpcodeHex") or "",
        "battleActionHandlerVaHex": battle_action_handler_context.get("battleActionHandlerVaHex") or "",
        "battleActionTableVaHex": battle_action_handler_context.get("battleActionTableVaHex") or "",
        "battleActionTableKey": battle_action_handler_context.get("battleActionTableKey") or "",
        "battleActionTableEntryCount": battle_action_handler_context.get("battleActionTableEntryCount", 0),
        "battleActionTableSamples": battle_action_handler_context.get("battleActionTableSamples") or [],
        "battleActionHandlerStorageOpcodeCount": battle_action_handler_context.get(
            "battleActionHandlerStorageOpcodeCount",
            0,
        ),
        "battleActionOpcodeInDialogueStorage": (
            battle_action_handler_context.get("battleActionOpcodeInDialogueStorage") is True
        ),
        "battleActionHandlerBattleEntryDispatchProofFound": (
            battle_action_handler_context.get("battleEntryDispatchProofFound") is True
        ),
        "battleActionHandlerOriginalCombatFormulaIdentified": (
            battle_action_handler_context.get("originalCombatFormulaIdentified") is True
        ),
        "battleNumericCandidateRunExamples": candidate_run_examples,
        "prototypeRuntimeMarkers": prototype_markers,
        "originalSystemChecks": original_system_checks,
        "originalSystemGaps": gaps,
        "evidenceRefs": ORIGINAL_BATTLE_DATA_EVIDENCE_REFS,
        "evidenceRefCount": len(ORIGINAL_BATTLE_DATA_EVIDENCE_REFS),
        "nextEvidenceNeeded": [
            "Identify the original event opcode/handler that enters battle mode.",
            "Bind EXE-nearest z*/boss_* sprite visual candidates to enemy rows and formation placement data.",
            "Find original enemy stat, combat formula, and EXP/money/item reward tables or runtime writes.",
        ],
        "conclusion": (
            f"{len(enemy_sprite_rows)} enemy/object sprite images, {len(battle_backgrounds)} battle background tilemaps, "
            f"{battle_event_candidates.get('candidateCount', len(candidate_rows))} battle-resource dialogue candidates, "
            f"and {sum(battle_table_counts.values())} battle/action text entries are extracted. EXE resource descriptor "
            f"evidence grounds {background_descriptors.get('referenceCount', 0)} battle-background refs and "
            f"{enemy_sprite_descriptors.get('referenceCount', 0)} enemy/object sprite refs; "
            f"{battle_enemy_candidates.get('candidateCount', 0)} battle review candidates can render EXE-nearest "
            "enemy/object sprite visual candidates. Event/object VM opcode 0x36 is now grounded as a battle-action "
            f"text handler at {battle_action_handler_context.get('battleActionHandlerVaHex') or 'unknown'} referencing "
            f"{battle_action_handler_context.get('battleActionTableVaHex') or 'unknown'}, but no opcode 0x36 storage "
            "or battle-entry dispatch path is proven. Numeric context scanning observes "
            f"{battle_numeric_context.get('plainNumericRunCount', 0)} descriptor-local small-number runs with "
            f"{battle_numeric_context.get('candidateContextsWithLocalPlainNumericRuns', 0)} candidate pairs carrying "
            "local runs and "
            f"{battle_numeric_context.get('promotableEnemyRowCount', 0)} promotable enemy rows. These descriptors, "
            "proximity choices, and numeric runs are resource/visual/static context evidence, not enemy rows. Enemy rows, "
            "formations, encounter dispatch, combat formulas, and reward tables are not proven."
        ),
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Original Battle Data Gap",
        "",
        summary["conclusion"],
        "",
        f"- promotion status: `{summary['promotionStatus']}`",
        f"- proof found: `{summary['proofFound']}`",
        f"- original battle data proof found: `{summary['originalBattleDataProofFound']}`",
        f"- failed original battle data gates: `{', '.join(summary['failedOriginalBattleDataGateIds'])}`",
        f"- missing evidence count: {len(summary['missingEvidence'])}",
        f"- evidence refs: {summary['evidenceRefCount']}",
        f"- enemy/object sprite images: {summary['enemyObjectSpriteImageCount']}",
        f"- boss sprite images: {summary['bossSpriteImageCount']}",
        f"- battle sprite images: {summary['battleSpriteImageCount']}",
        f"- battle background tilemaps: {summary['battleBackgroundTilemapCount']}",
        f"- battle event candidates: {summary['battleEventCandidateCount']}",
        f"- battle/action text entries: {summary['battleTextEntryCount']}",
        f"- battle resource descriptor status: `{summary['battleResourceDescriptorStatus']}`",
        f"- resource descriptor rows: {summary['battleResourceDescriptorRowCount']}",
        f"- battle enemy sprite candidate status: `{summary['battleEnemySpriteCandidateStatus']}`",
        f"- battle enemy sprite candidates: {summary['battleEnemySpriteCandidateCount']}",
        f"- battle enemy sprite candidate assets: {summary['battleEnemySpriteCandidateAssetCount']}",
        f"- runtime-selectable extracted enemy/object sprite assets: {summary['battleEnemySpriteRuntimeSelectableAssetCount']}",
        f"- battle numeric context status: `{summary['battleNumericContextStatus']}`",
        f"- battle numeric descriptor contexts: {summary['battleNumericDescriptorContextCount']}",
        f"- battle numeric plain runs: {summary['battleNumericPlainRunCount']}",
        f"- battle action handler status: `{summary['battleActionHandlerStatus']}`",
        f"- battle action handler: `{summary['battleActionOpcodeHex']} -> {summary['battleActionHandlerVaHex']} -> {summary['battleActionTableVaHex']}`",
        "",
        "## Text Tables",
        "",
        "| table | entries |",
        "| --- | ---: |",
    ]
    for key, count in summary["battleTextTableEntryCounts"].items():
        lines.append(f"| {key} | {count} |")
    lines.extend([
        "",
        "## Resource Descriptor Evidence",
        "",
        "| field | value |",
        "| --- | --- |",
    ])
    for key in [
        "battleResourceDescriptorStatus",
        "battleResourceDescriptorRowCount",
        "battleBackgroundDescriptorReferenceCount",
        "battleBackgroundDescriptorReferencedPayloadCount",
        "battleBackgroundDescriptorMissingExeRefs",
        "battleSpriteDescriptorReferenceCount",
        "battleSpriteDescriptorReferencedPayloadCount",
        "enemySpriteDescriptorReferenceCount",
        "enemySpriteDescriptorReferencedPayloadCount",
        "enemySpriteDescriptorMissingExeRefs",
        "resourceDescriptorsAreEnemyRows",
        "resourceDescriptorsContainStatsOrRewards",
        "resourceDescriptorsProveBattleEntry",
    ]:
        lines.append(f"| {key} | {summary[key]} |")
    lines.extend([
        "",
        "## Battle Enemy Sprite Candidate Evidence",
        "",
        "| field | value |",
        "| --- | --- |",
    ])
    for key in [
        "battleEnemySpriteCandidateStatus",
        "battleEnemySpriteCandidateCount",
        "battleEnemySpriteCandidateUniqueSpriteCount",
        "battleEnemySpriteCandidateAssetCount",
        "battleEnemySpriteExtractedAssetCount",
        "battleEnemySpriteRuntimeSelectableAssetCount",
        "battleEnemySpriteCandidatesMapped",
        "battleEnemySpriteCandidateAssetsDecoded",
        "battleEnemySpriteAllExtractedAssetsSelectable",
        "battleEnemySpriteCandidatesOriginalRowsBound",
        "battleEnemySpriteCandidatesStatsOrRewardsBound",
        "battleEnemySpriteCandidatesBattleEntryProven",
    ]:
        lines.append(f"| {key} | {summary[key]} |")
    lines.extend([
        "",
        "## Numeric Context Evidence",
        "",
        "| field | value |",
        "| --- | --- |",
    ])
    for key in [
        "battleNumericContextStatus",
        "battleNumericDescriptorContextCount",
        "battleNumericCandidatePairContextCount",
        "battleNumericCloseCandidatePairContextCount",
        "battleNumericCandidateContextsWithLocalRuns",
        "battleNumericCloseCandidateContextsWithLocalRuns",
        "battleNumericCandidateContextsWithEnemyRuns",
        "battleNumericCandidateContextsWithBackgroundRuns",
        "battleNumericMaxLocalPlainRunCount",
        "battleNumericPlainRunCount",
        "battleNumericContextsWithPlainRuns",
        "battleNumericPromotableEnemyRowCount",
        "battleNumericPromotableRewardRowCount",
        "battleNumericRunsPromotedToEnemyRows",
        "battleNumericBattleEntryExecutionIdentified",
    ]:
        lines.append(f"| {key} | {summary[key]} |")
    lines.extend([
        "",
        "## Battle Action Handler Evidence",
        "",
        "| field | value |",
        "| --- | --- |",
    ])
    for key in [
        "battleActionHandlerStatus",
        "battleActionHandlerGrounded",
        "battleActionOpcodeHex",
        "battleActionHandlerVaHex",
        "battleActionTableVaHex",
        "battleActionTableKey",
        "battleActionTableEntryCount",
        "battleActionTableSamples",
        "battleActionHandlerStorageOpcodeCount",
        "battleActionOpcodeInDialogueStorage",
        "battleActionHandlerBattleEntryDispatchProofFound",
        "battleActionHandlerOriginalCombatFormulaIdentified",
    ]:
        lines.append(f"| {key} | {summary[key]} |")
    lines.extend([
        "",
        "### Candidate Numeric Run Examples",
        "",
        "| candidate | background | enemy sprite | distance | close | local runs | first run | class | promotion |",
        "| --- | --- | --- | ---: | --- | ---: | --- | --- | --- |",
    ])
    for row in summary.get("battleNumericCandidateRunExamples") or []:
        values = ",".join(str(value) for value in row.get("firstRunValues") or [])
        first_run = f"{row.get('firstRunStartVaHex')}..{row.get('firstRunEndVaHex')} values={values}"
        lines.append(
            f"| {row['candidateId']} | `{row['battleBackground']}` | `{row['enemyCns']}` | "
            f"{row['refDistance']} | {row['closeResourcePair']} | {row['localPlainNumericRunCount']} | "
            f"{first_run} | {row['numericEvidenceClass']} | {row['promotionBlocker']} |"
        )
    lines.extend([
        "",
        "## Original System Checks",
        "",
        "| check | value |",
        "| --- | --- |",
    ])
    for key, value in summary["originalSystemChecks"].items():
        lines.append(f"| {key} | {value} |")
    lines.extend([
        "",
        "## Prototype Runtime Markers",
        "",
        "| marker | present |",
        "| --- | --- |",
    ])
    for key, value in summary["prototypeRuntimeMarkers"].items():
        lines.append(f"| {key} | {value} |")
    lines.extend([
        "",
        "## Gaps",
        "",
        "| id | status | gap |",
        "| --- | --- | --- |",
    ])
    for row in summary["originalSystemGaps"]:
        lines.append(f"| {row['id']} | `{row['status']}` | {row['gap']} |")
    lines.extend([
        "",
        "## Missing Evidence",
        "",
    ])
    lines.extend(f"- {item}" for item in summary["missingEvidence"])
    lines.extend([
        "",
        "## Evidence Refs",
        "",
    ])
    for ref in summary["evidenceRefs"]:
        lines.append(f"- `{ref['path']}`: {', '.join(ref['fields'])}")
    lines.extend([
        "",
        "## Next Evidence Needed",
        "",
    ])
    lines.extend(f"- {item}" for item in summary["nextEvidenceNeeded"])
    return "\n".join(lines) + "\n"


def html_page(summary: dict) -> str:
    check_rows = "".join(
        f"<tr><td>{html.escape(key)}</td><td>{html.escape(str(value))}</td></tr>"
        for key, value in summary["originalSystemChecks"].items()
    )
    marker_rows = "".join(
        f"<tr><td>{html.escape(key)}</td><td>{html.escape(str(value))}</td></tr>"
        for key, value in summary["prototypeRuntimeMarkers"].items()
    )
    gap_rows = "".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"]
    )
    text_rows = "".join(
        f"<tr><td>{html.escape(key)}</td><td>{count}</td></tr>"
        for key, count in summary["battleTextTableEntryCounts"].items()
    )
    descriptor_rows = "".join(
        f"<tr><td>{html.escape(key)}</td><td>{html.escape(str(summary[key]))}</td></tr>"
        for key in [
            "battleResourceDescriptorStatus",
            "battleResourceDescriptorRowCount",
            "battleBackgroundDescriptorReferenceCount",
            "battleBackgroundDescriptorReferencedPayloadCount",
            "battleBackgroundDescriptorMissingExeRefs",
            "battleSpriteDescriptorReferenceCount",
            "battleSpriteDescriptorReferencedPayloadCount",
            "enemySpriteDescriptorReferenceCount",
            "enemySpriteDescriptorReferencedPayloadCount",
            "enemySpriteDescriptorMissingExeRefs",
            "resourceDescriptorsAreEnemyRows",
            "resourceDescriptorsContainStatsOrRewards",
            "resourceDescriptorsProveBattleEntry",
        ]
    )
    action_rows = "".join(
        f"<tr><td>{html.escape(key)}</td><td>{html.escape(str(summary[key]))}</td></tr>"
        for key in [
            "battleActionHandlerStatus",
            "battleActionHandlerGrounded",
            "battleActionOpcodeHex",
            "battleActionHandlerVaHex",
            "battleActionTableVaHex",
            "battleActionTableKey",
            "battleActionTableEntryCount",
            "battleActionTableSamples",
            "battleActionHandlerStorageOpcodeCount",
            "battleActionOpcodeInDialogueStorage",
            "battleActionHandlerBattleEntryDispatchProofFound",
            "battleActionHandlerOriginalCombatFormulaIdentified",
        ]
    )
    enemy_candidate_rows = "".join(
        f"<tr><td>{html.escape(key)}</td><td>{html.escape(str(summary[key]))}</td></tr>"
        for key in [
            "battleEnemySpriteCandidateStatus",
            "battleEnemySpriteCandidateCount",
            "battleEnemySpriteCandidateUniqueSpriteCount",
            "battleEnemySpriteCandidateAssetCount",
            "battleEnemySpriteExtractedAssetCount",
            "battleEnemySpriteRuntimeSelectableAssetCount",
            "battleEnemySpriteCandidatesMapped",
            "battleEnemySpriteCandidateAssetsDecoded",
            "battleEnemySpriteAllExtractedAssetsSelectable",
            "battleEnemySpriteCandidatesOriginalRowsBound",
            "battleEnemySpriteCandidatesStatsOrRewardsBound",
            "battleEnemySpriteCandidatesBattleEntryProven",
        ]
    )
    numeric_rows = "".join(
        f"<tr><td>{html.escape(key)}</td><td>{html.escape(str(summary[key]))}</td></tr>"
        for key in [
            "battleNumericContextStatus",
            "battleNumericDescriptorContextCount",
            "battleNumericCandidatePairContextCount",
            "battleNumericCloseCandidatePairContextCount",
            "battleNumericCandidateContextsWithLocalRuns",
            "battleNumericCloseCandidateContextsWithLocalRuns",
            "battleNumericCandidateContextsWithEnemyRuns",
            "battleNumericCandidateContextsWithBackgroundRuns",
            "battleNumericMaxLocalPlainRunCount",
            "battleNumericPlainRunCount",
            "battleNumericContextsWithPlainRuns",
            "battleNumericPromotableEnemyRowCount",
            "battleNumericPromotableRewardRowCount",
            "battleNumericRunsPromotedToEnemyRows",
            "battleNumericBattleEntryExecutionIdentified",
        ]
    )
    numeric_example_rows = "".join(
        "<tr>"
        f"<td>{html.escape(row['candidateId'])}</td>"
        f"<td><code>{html.escape(row['battleBackground'])}</code></td>"
        f"<td><code>{html.escape(row['enemyCns'])}</code></td>"
        f"<td>{html.escape(str(row['refDistance']))}</td>"
        f"<td>{html.escape(str(row['closeResourcePair']))}</td>"
        f"<td>{html.escape(str(row['localPlainNumericRunCount']))}</td>"
        f"<td>{html.escape(str(row.get('firstRunStartVaHex')))}..{html.escape(str(row.get('firstRunEndVaHex')))} "
        f"values={html.escape(','.join(str(value) for value in row.get('firstRunValues') or []))}</td>"
        f"<td>{html.escape(row['numericEvidenceClass'])}</td>"
        f"<td>{html.escape(row['promotionBlocker'])}</td>"
        "</tr>"
        for row in summary.get("battleNumericCandidateRunExamples") or []
    )
    next_items = "".join(f"<li>{html.escape(item)}</li>" for item in summary["nextEvidenceNeeded"])
    missing_items = "".join(f"<li>{html.escape(item)}</li>" for item in summary["missingEvidence"])
    evidence_ref_items = "".join(
        f"<li><code>{html.escape(ref['path'])}</code>: {html.escape(', '.join(ref['fields']))}</li>"
        for ref in summary["evidenceRefs"]
    )
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        "  <title>Original Battle Data 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}</style>",
        "</head>",
        "<body>",
        "  <h1>Original Battle Data Gap</h1>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        (
            "  <p><b>Counts:</b> "
            f"enemy/object sprites {summary['enemyObjectSpriteImageCount']}, "
            f"battle backgrounds {summary['battleBackgroundTilemapCount']}, "
            f"battle candidates {summary['battleEventCandidateCount']}, "
            f"battle enemy sprite candidates {summary['battleEnemySpriteCandidateCount']}, "
            f"numeric contexts {summary['battleNumericDescriptorContextCount']}, "
            f"battle/action text entries {summary['battleTextEntryCount']}; "
            f"status <code>{html.escape(summary['promotionStatus'])}</code>.</p>"
        ),
        "  <h2>Proof Gates</h2>",
        f"  <p>proof found <code>{html.escape(str(summary['proofFound']))}</code>; "
        f"original battle data proof found <code>{html.escape(str(summary['originalBattleDataProofFound']))}</code>; "
        f"failed original battle data gates <code>{html.escape(','.join(summary['failedOriginalBattleDataGateIds']))}</code>; "
        f"missing evidence count <code>{len(summary['missingEvidence'])}</code>; "
        f"evidence refs <code>{summary['evidenceRefCount']}</code>.</p>",
        "  <h2>Text Tables</h2>",
        f"  <table><thead><tr><th>table</th><th>entries</th></tr></thead><tbody>{text_rows}</tbody></table>",
        "  <h2>Resource Descriptor Evidence</h2>",
        f"  <table><thead><tr><th>field</th><th>value</th></tr></thead><tbody>{descriptor_rows}</tbody></table>",
        "  <h2>Battle Enemy Sprite Candidate Evidence</h2>",
        f"  <table><thead><tr><th>field</th><th>value</th></tr></thead><tbody>{enemy_candidate_rows}</tbody></table>",
        "  <h2>Numeric Context Evidence</h2>",
        f"  <table><thead><tr><th>field</th><th>value</th></tr></thead><tbody>{numeric_rows}</tbody></table>",
        "  <h2>Battle Action Handler Evidence</h2>",
        f"  <table><thead><tr><th>field</th><th>value</th></tr></thead><tbody>{action_rows}</tbody></table>",
        "  <h3>Candidate Numeric Run Examples</h3>",
        f"  <table><thead><tr><th>candidate</th><th>background</th><th>enemy sprite</th><th>distance</th><th>close</th><th>local runs</th><th>first run</th><th>class</th><th>promotion</th></tr></thead><tbody>{numeric_example_rows}</tbody></table>",
        "  <h2>Original System Checks</h2>",
        f"  <table><thead><tr><th>check</th><th>value</th></tr></thead><tbody>{check_rows}</tbody></table>",
        "  <h2>Prototype Runtime Markers</h2>",
        f"  <table><thead><tr><th>marker</th><th>present</th></tr></thead><tbody>{marker_rows}</tbody></table>",
        "  <h2>Gaps</h2>",
        f"  <table><thead><tr><th>id</th><th>status</th><th>gap</th></tr></thead><tbody>{gap_rows}</tbody></table>",
        "  <h2>Missing Evidence</h2>",
        f"  <ul>{missing_items}</ul>",
        "  <h2>Evidence Refs</h2>",
        f"  <ul>{evidence_ref_items}</ul>",
        "  <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 / "original_battle_data_gap.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "original_battle_data_gap.md").write_text(markdown(summary), encoding="utf-8")
    (out_dir / "original_battle_data_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 / "cns_payloads.json", []),
        load_json(args.out_dir / "text_tables.json", {}),
        load_json(args.out_dir / "battle_backgrounds.json", {}),
        load_json(args.out_dir / "battle_event_candidates.json", {}),
        load_json(args.out_dir / "original_battle_resource_descriptors.json", {}),
        load_json(args.out_dir / "battle_enemy_candidates.json", {}),
        load_json(args.out_dir / "original_battle_numeric_context.json", {}),
        load_json(args.out_dir / "battle_action_handler_context.json", {}),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote original battle data gap -> {args.out_dir / 'original_battle_data_gap.md'}")


if __name__ == "__main__":
    main()
