#!/usr/bin/env python3
"""Summarize the producer boundary for status-window short comments."""
from __future__ import annotations

import html
import json
import struct
from pathlib import Path
from typing import Any

from probe_exe_scene_tables import read_sections, va_to_offset
from summarize_object_payload_442c75_callers import decode_stream


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
WEB = ROOT / "web"
EXE = ROOT / "Hwanse2.exe"

STATUS_PAYLOAD_VA = 0x004E842E
TOP_MENU_OBJECT_SEQUENCE_VA = 0x004DDC6C
STATUS_BACKING_BLOCK_VA = 0x004576D8
STATUS_BACKING_BLOCK_SIZE = 0x72
ACTOR_ROWS_VA = 0x00457750
ACTOR_ROWS_SIZE = 0x288
ACTOR_ROW_STRIDE = 0xD8
GLOBAL_VAR_TABLE_VA = 0x0059DB60
GLOBAL_VAR_TABLE_SIZE = 0x200
ACTIVE_ACTOR_POINTER_TABLE_VA = 0x0059DB30


def read_json(name: str) -> dict[str, Any]:
    try:
        data = json.loads((OUT / name).read_text(encoding="utf-8"))
    except FileNotFoundError:
        return {}
    return data if isinstance(data, dict) else {}


def summary(data: dict[str, Any]) -> dict[str, Any]:
    value = data.get("summary")
    return value if isinstance(value, dict) else {}


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


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


def hx(value: int | None, width: int = 8) -> str:
    if value is None:
        return ""
    return f"0x{value:0{width}x}"


def file_offset_to_va(sections: list[dict[str, Any]], offset: int) -> int | None:
    for section in sections:
        raw_start = int(section["raw"])
        raw_end = raw_start + int(section["raw_size"])
        if raw_start <= offset < raw_end:
            return int(section["va"]) + offset - raw_start
    return None


def read_at_va(exe: bytes, sections: list[dict[str, Any]], va: int, size: int) -> bytes:
    off = va_to_offset(sections, va)
    if off is None:
        return b""
    return exe[off : off + size]


def scan_payload_consumers(exe: bytes, sections: list[dict[str, Any]]) -> dict[str, Any]:
    """Find static command/data refs to the status-window payload.

    The short-comment selector array itself is inline and has no normal pointer
    refs.  The owning status payload, however, is consumed by active-object
    command streams through opcode 0x2f mode 2.  That grounds the consumer while
    keeping the selector-index producer unresolved.
    """
    target = struct.pack("<I", STATUS_PAYLOAD_VA)
    refs: list[dict[str, Any]] = []
    off = exe.find(target)
    while off != -1:
        ref_va = file_offset_to_va(sections, off)
        command_va = ref_va - 4 if ref_va is not None else None
        command_raw = read_at_va(exe, sections, command_va, 8) if command_va is not None else b""
        is_prompt_payload = len(command_raw) == 8 and command_raw[0] == 0x2F
        refs.append(
            {
                "refVaHex": hx(ref_va),
                "commandVaHex": hx(command_va),
                "commandRawHex": command_raw.hex(" "),
                "classification": "prompt-text-opcode-0x2f-payload" if is_prompt_payload else "non-command-data-ref",
                "modeHex": hx(command_raw[1], 2) if is_prompt_payload else "",
            }
        )
        off = exe.find(target, off + 1)

    decoded_top = decode_stream(exe, sections, TOP_MENU_OBJECT_SEQUENCE_VA, max_commands=32, max_bytes=0x180)
    status_payload_commands = [
        {
            "vaHex": row.get("vaHex"),
            "opcodeHex": row.get("opcodeHex"),
            "rawHex": row.get("rawHex"),
            "summary": row.get("summary"),
        }
        for row in decoded_top.get("commands", [])
        if STATUS_PAYLOAD_VA.to_bytes(4, "little").hex(" ") in str(row.get("rawHex", ""))
    ]
    return {
        "statusPayloadVaHex": hx(STATUS_PAYLOAD_VA),
        "topMenuObjectSequenceVaHex": hx(TOP_MENU_OBJECT_SEQUENCE_VA),
        "refCount": len(refs),
        "promptPayloadCommandCount": sum(1 for row in refs if row["classification"] == "prompt-text-opcode-0x2f-payload"),
        "refs": refs,
        "decodedTopStreamStatusCommands": status_payload_commands,
    }


def decode_post_selector_actor_branch(
    exe: bytes,
    sections: list[dict[str, Any]],
    post_selector_branch: dict[str, Any],
) -> dict[str, Any]:
    entries = []
    actor_slots = []
    for entry in post_selector_branch.get("entries", []):
        pointer_hex = entry.get("pointerVaHex") or ""
        try:
            va = int(pointer_hex, 16)
        except ValueError:
            continue
        first = read_at_va(exe, sections, va, 4)
        second = read_at_va(exe, sections, va + 4, 8)
        row = {
            "index": entry.get("index"),
            "entryVaHex": pointer_hex,
            "firstRawHex": first.hex(" "),
            "classification": "unknown",
            "actorSlotCandidate": None,
            "continuationVaHex": "",
            "continuationRawHex": second.hex(" "),
        }
        if len(first) == 4 and first[:3] == b"\x40\x1b\x02":
            slot = first[3]
            actor_slots.append(slot)
            row["classification"] = "actor-slot-branch-entry"
            row["actorSlotCandidate"] = slot
            if len(second) == 8 and second[:4] == b"\x40\x01\x00\x00":
                row["continuationVaHex"] = hx(struct.unpack_from("<I", second, 4)[0])
                row["continuationKind"] = "shared-continuation-pointer"
            else:
                row["continuationKind"] = "inline-continuation"
        entries.append(row)
    return {
        "branchListVaHex": post_selector_branch.get("recordVaHex"),
        "entryCount": len(entries),
        "actorSlotCandidates": actor_slots,
        "actorSlotBranchGrounded": actor_slots == [0, 1, 2],
        "interpretation": (
            "selector array 뒤의 40 1f 3-entry branch list는 각 entry가 40 1b 02 actorSlot으로 시작한다. "
            "따라서 selector group 내부에서 현재 actor slot(0/1/2)별 문구 테이블을 고르는 consumer는 grounded로 볼 수 있다. "
            "아직 미확정인 것은 9개 selector group 중 어떤 group을 고르는 시나리오/상태 producer다."
        ),
        "entries": entries,
    }


def exact_value_refs(exe: bytes, sections: list[dict[str, Any]], value: int) -> list[str]:
    target = struct.pack("<I", value)
    refs: list[str] = []
    off = exe.find(target)
    while off != -1:
        va = file_offset_to_va(sections, off)
        if va is not None:
            refs.append(hx(va))
        off = exe.find(target, off + 1)
    return refs


def scan_backing_store_evidence(exe: bytes, sections: list[dict[str, Any]]) -> dict[str, Any]:
    backing_refs = exact_value_refs(exe, sections, STATUS_BACKING_BLOCK_VA)
    actor_row_refs = exact_value_refs(exe, sections, ACTOR_ROWS_VA)
    global_var_refs = exact_value_refs(exe, sections, GLOBAL_VAR_TABLE_VA)
    active_actor_refs = exact_value_refs(exe, sections, ACTIVE_ACTOR_POINTER_TABLE_VA)

    fixed_refs = [
        {
            "refVaHex": "0x004064e4",
            "kind": "active-object-selector-base-setter",
            "evidence": "active-object side writes object+0xa8 = 0x004576d8",
        },
        {
            "refVaHex": "0x0041d8d7",
            "kind": "text-status-selector-base-setter",
            "evidence": "40 1b mode 1 handler writes object+0xa8 = 0x004576d8",
        },
        {
            "refVaHex": "0x00422262",
            "kind": "backing-subrange-pointer",
            "evidence": "chooses 0x004576d8 + 0x20 when function arg is nonzero",
        },
        {
            "refVaHex": "0x00422272",
            "kind": "backing-subrange-pointer",
            "evidence": "chooses 0x004576d8 + 0x14 when function arg is zero",
        },
        {
            "refVaHex": "0x004233fc",
            "kind": "persistent-load",
            "evidence": "reads 0x72 bytes into 0x004576d8, then 0x288 bytes into 0x00457750, then 0x200 bytes into 0x0059db60",
        },
        {
            "refVaHex": "0x00423535",
            "kind": "persistent-save",
            "evidence": "writes 0x72 bytes from 0x004576d8, then 0x288 bytes from 0x00457750, then 0x200 bytes from 0x0059db60",
        },
    ]
    offset_refs = [
        {
            "offsetHex": "0x01",
            "addressHex": hx(STATUS_BACKING_BLOCK_VA + 1),
            "writerVaHex": "0x004248d5",
            "evidence": "copies BYTE [0x55b2fc + 1] into 0x004576d9 before running a status/text payload",
            "status": "direct-writer-grounded",
        },
        {
            "offsetHex": "0x10",
            "addressHex": hx(STATUS_BACKING_BLOCK_VA + 0x10),
            "writerVaHex": "0x0043218f",
            "evidence": "increments active actor count at 0x004576e8 while registering an actor row",
            "status": "direct-writer-grounded",
        },
        {
            "offsetHex": "0x11",
            "addressHex": hx(STATUS_BACKING_BLOCK_VA + 0x11),
            "writerVaHex": "0x00432169",
            "evidence": "stores actor slot byte into 0x004576e9 + count during active actor registration",
            "status": "direct-writer-grounded",
        },
        {
            "offsetHex": "0x00",
            "addressHex": hx(STATUS_BACKING_BLOCK_VA),
            "writerVaHex": "",
            "evidence": "no direct static writer to the exact selector byte was found in the current absolute-ref scan",
            "status": "writer-unproven",
        },
    ]
    return {
        "backingBlockVaHex": hx(STATUS_BACKING_BLOCK_VA),
        "backingBlockSizeBytes": STATUS_BACKING_BLOCK_SIZE,
        "actorRowsVaHex": hx(ACTOR_ROWS_VA),
        "actorRowsSizeBytes": ACTOR_ROWS_SIZE,
        "actorRowStrideBytes": ACTOR_ROW_STRIDE,
        "globalVarTableVaHex": hx(GLOBAL_VAR_TABLE_VA),
        "globalVarTableSizeBytes": GLOBAL_VAR_TABLE_SIZE,
        "activeActorPointerTableVaHex": hx(ACTIVE_ACTOR_POINTER_TABLE_VA),
        "exactBackingRefCount": len(backing_refs),
        "exactBackingRefs": backing_refs,
        "actorRowRefCount": len(actor_row_refs),
        "globalVarRefCount": len(global_var_refs),
        "activeActorPointerRefCount": len(active_actor_refs),
        "fixedRefs": fixed_refs,
        "offsetRefs": offset_refs,
        "interpretation": (
            "0x004576d8 is a 0x72-byte persistent/global status block. "
            "It is saved/loaded next to the 3 actor rows at 0x00457750 and the global byte table at 0x0059db60. "
            "The status-comment selector source uses object+0xa8+0 with object+0xa8 set to this block, but the exact writer for offset +0 is still not statically proven."
        ),
    }


def build_report() -> dict[str, Any]:
    binding = read_json("status_comment_selector_binding_review.json")
    binding_summary = summary(binding)
    status_ui = summary(read_json("status_menu_ui_expression_review.json"))
    status_vm_payload = read_json("status_menu_vm_table_review.json")
    status_vm = summary(status_vm_payload)

    selector_array = binding.get("selectorArray") if isinstance(binding.get("selectorArray"), dict) else {}
    producer_probe = binding.get("selectorProducerProbe") if isinstance(binding.get("selectorProducerProbe"), dict) else {}
    groups = binding.get("groups") if isinstance(binding.get("groups"), list) else []
    conclusions = binding.get("conclusions") if isinstance(binding.get("conclusions"), list) else []
    opcode14_tables = status_vm_payload.get("opcode14Tables") if isinstance(status_vm_payload.get("opcode14Tables"), list) else []
    opcode1f_lists = status_vm_payload.get("opcode1fBranchLists") if isinstance(status_vm_payload.get("opcode1fBranchLists"), list) else []

    selector_record = next(
        (
            row
            for row in opcode14_tables
            if row.get("classification") == "status-comment-block-selector-array"
        ),
        {},
    )
    post_selector_branch = next(
        (
            row
            for row in opcode1f_lists
            if row.get("recordVaHex") == "0x004e8660"
        ),
        {},
    )
    exe = EXE.read_bytes()
    sections = read_sections(exe)
    payload_consumers = scan_payload_consumers(exe, sections)
    actor_branch = decode_post_selector_actor_branch(exe, sections, post_selector_branch)
    backing_store = scan_backing_store_evidence(exe, sections)

    group_rows = []
    for group in groups:
        block = group.get("commentBlock") if isinstance(group.get("commentBlock"), dict) else {}
        parsed = group.get("parsedCommentTables") if isinstance(group.get("parsedCommentTables"), list) else []
        samples = []
        for table in parsed[:3]:
            sample = table.get("sample")
            if sample:
                samples.append(sample)
        group_rows.append(
            {
                "tableIdHex": group.get("tableIdHex") or block.get("tableIdHex"),
                "blockVaHex": block.get("blockVaHex"),
                "markerSlotCount": block.get("commentTableMarkerSlotCount"),
                "emptySlotCount": block.get("emptyOrTerminatorSlotCount"),
                "parsedTableCount": len(parsed),
                "classifications": sorted({table.get("classification") for table in parsed if table.get("classification")}),
                "sample": " / ".join(samples[:2]),
            }
        )

    surfaces = [
        {
            "surface": "short comment 40 13 01 text groups",
            "promotion": "confirmed-text-groups",
            "finding": (
                f"commentGroupCount={binding_summary.get('commentGroupCount')}; "
                f"actorTableCount={binding_summary.get('actorTableCount')}; "
                f"commentBlockCount={binding_summary.get('commentBlockCount')}."
            ),
            "gap": "텍스트와 pointer group은 확정됐지만 현재 상황/시나리오 값 선택은 아니다.",
        },
        {
            "surface": "inline selector array",
            "promotion": "confirmed-inline-array / consumer-pending",
            "finding": (
                f"array={selector_array.get('arrayVaHex')}; "
                f"count={selector_array.get('count')}; "
                f"allBlocksMatched={selector_array.get('allBlocksMatched')}; "
                f"order={selector_array.get('foundTableIdOrder')}."
            ),
            "gap": selector_array.get("normalXrefEvidence", "no normal xref evidence"),
        },
        {
            "surface": "producer probe",
            "promotion": "negative-xref / VM-consumer-only",
            "finding": (
                f"anchor refs={sum(row.get('pointerRefCount', 0) for row in producer_probe.get('anchorRefs', []))}; "
                f"outside entry refs={producer_probe.get('entryRefOutsideSelectorCount')}; "
                f"status={producer_probe.get('status')}."
            ),
            "gap": producer_probe.get("interpretation", "producer remains unresolved"),
        },
        {
            "surface": "status/menu 0x40 VM table context",
            "promotion": "confirmed-control-handlers / backing-byte-writer-pending",
            "finding": (
                f"opcode14TableCount={status_vm.get('opcode14TableCount')}; "
                f"opcode13TableCount={status_vm.get('opcode13TableCount')}; "
                f"interpreter={status_vm.get('textControlInterpreterVaHex')}; "
                f"handlerTable={status_vm.get('textControlHandlerTableVaHex')}."
            ),
            "gap": "40 14 selector source는 좁혀졌지만 source byte/word를 쓰는 writer와 게임 내 의미는 아직 별도 추적 대상이다.",
        },
        {
            "surface": "status window payload relation",
            "promotion": "status-window-core-and-payload-consumer-confirmed / selector-index-producer-unverified",
            "finding": (
                f"status payload {status_ui.get('statusPayloadVaHex')}..{status_ui.get('statusPayloadEndVaHex')}; "
                f"region {((status_ui.get('windowRegion') or {}).get('index'))}; "
                f"status {status_ui.get('status', 'status-window-expression-grounded-core')}; "
                f"0x2f payload commands={payload_consumers.get('promptPayloadCommandCount')}."
            ),
            "gap": "상태창 payload 소비는 grounded. 남은 미확정은 payload 내부 0x004e8610 selector array의 index/entry를 고르는 producer다.",
        },
        {
            "surface": "post-selector actor branch",
            "promotion": "actor-slot-consumer-grounded / selector-group-producer-unverified",
            "finding": (
                f"branch={actor_branch.get('branchListVaHex')}; "
                f"slots={actor_branch.get('actorSlotCandidates')}; "
                f"grounded={actor_branch.get('actorSlotBranchGrounded')}."
            ),
            "gap": "actor slot 0/1/2 branch는 보이지만, 9개 selector group 중 현재 group을 고르는 producer는 아직 없다.",
        },
        {
            "surface": "0x004576d8 backing status block",
            "promotion": "persistent-block-grounded / offset-0-writer-pending",
            "finding": (
                f"block={backing_store.get('backingBlockVaHex')} size={backing_store.get('backingBlockSizeBytes')}; "
                f"refs={backing_store.get('exactBackingRefCount')}; "
                f"actorRows={backing_store.get('actorRowsVaHex')} size={backing_store.get('actorRowsSizeBytes')}; "
                f"globalVars={backing_store.get('globalVarTableVaHex')} size={backing_store.get('globalVarTableSizeBytes')}."
            ),
            "gap": "block 저장/로드와 몇몇 offset writer는 grounded지만 selector source인 +0의 직접 writer/의미는 아직 미확정이다.",
        },
    ]

    selector_boundary = [
        {
            "surface": "status core payload",
            "va": f"{status_ui.get('statusPayloadVaHex')}..{status_ui.get('statusPayloadEndVaHex')}",
            "role": "region #6 상태창 본문 직접 draw payload",
            "evidence": "40 2e actor numeric, 40 30 equipment-name, 40 24 portrait, 40 0e labels are decoded in status_menu_ui_expression_review.",
            "producerMeaning": "short-comment selector producer가 아니라 상태창의 고정/actor-field 렌더 표면이다.",
        },
        {
            "surface": "status payload consumers",
            "va": payload_consumers.get("statusPayloadVaHex"),
            "role": "active-object command stream consumes region #6 payload",
            "evidence": (
                f"refs={payload_consumers.get('refCount')}; "
                f"0x2f payload commands={payload_consumers.get('promptPayloadCommandCount')}; "
                f"top stream={payload_consumers.get('topMenuObjectSequenceVaHex')}"
            ),
            "producerMeaning": "payload 실행 경계는 확정됐지만, selector index 산출은 이 command의 operand가 아니라 payload 내부 VM 처리에 남는다.",
        },
        {
            "surface": "selector inline header",
            "va": selector_array.get("headerVaHex"),
            "role": "40 1b 01 + 40 14 03 00 count record",
            "evidence": (
                f"{selector_array.get('headerInterpretation', '')}; "
                f"40 14 source={selector_record.get('sourceExpression')}; "
                f"nearest base={((selector_record.get('nearestPrecedingBaseSelector') or {}).get('baseExpression'))}"
            ),
            "producerMeaning": (
                "9-entry array를 여는 inline text/status control record다. "
                "source는 object+0xa8+0이고 직전 40 1b 01 00이 object+0xa8=0x004576d8로 세팅한다."
            ),
        },
        {
            "surface": "selector entry array",
            "va": selector_array.get("arrayVaHex"),
            "role": "table id 0x77..0x7f block pointer array",
            "evidence": (
                f"record={selector_record.get('recordVaHex')}; "
                f"entryTable={selector_record.get('entryTableVaHex')}; "
                f"kinds={selector_record.get('entryKindCounts')}"
            ),
            "producerMeaning": "각 블록 포인터와 index source는 확정됐다. 남은 것은 0x004576d8+0 backing value의 writer/의미다.",
        },
        {
            "surface": "backing selector base block",
            "va": backing_store.get("backingBlockVaHex"),
            "role": "40 1b 01이 object+0xa8로 지정하는 persistent/global status block",
            "evidence": backing_store.get("interpretation"),
            "producerMeaning": "selector byte가 들어있는 블록의 정체는 확정 쪽으로 승격. offset +0 writer/게임 내 의미는 미확정.",
        },
        {
            "surface": "post-selector local branch list",
            "va": post_selector_branch.get("recordVaHex"),
            "role": "selector payload 뒤의 40 1f local branch list",
            "evidence": (
                f"pointerCount={post_selector_branch.get('pointerCount')}; "
                f"entries={[entry.get('pointerVaHex') for entry in post_selector_branch.get('entries', [])]}"
            ),
            "producerMeaning": "selector 다음의 VM 흐름 후보지만, selector index나 entry index write/read 증거는 아직 없다.",
        },
        {
            "surface": "normal x86 xref probe",
            "va": selector_array.get("arrayVaHex"),
            "role": "negative proof",
            "evidence": (
                f"anchor refs={sum(row.get('pointerRefCount', 0) for row in producer_probe.get('anchorRefs', []))}; "
                f"outside entry refs={producer_probe.get('entryRefOutsideSelectorCount')}"
            ),
            "producerMeaning": "ordinary pointer consumer가 아니라 0x40 VM payload interpreter 안에서 처리되는 표면으로 봐야 한다.",
        },
    ]

    return {
        "kind": "hwanse-status-comment-producer-boundary-review",
        "status": "comment-data-grounded-selector-source-grounded-writer-unproven",
        "source": [
            "out/status_comment_selector_binding_review.json",
            "out/status_menu_ui_expression_review.json",
            "out/status_menu_vm_table_review.json",
            "tools/build_status_comment_producer_boundary_review.py",
        ],
        "summary": {
            "surfaceCount": len(surfaces),
            "confirmedDataSurfaceCount": sum(1 for row in surfaces if row["promotion"].startswith("confirmed")),
            "selectorArrayCount": binding_summary.get("selectorArrayCount"),
            "selectorArrayAllBlocksMatched": binding_summary.get("selectorArrayAllBlocksMatched"),
            "commentGroupCount": binding_summary.get("commentGroupCount"),
            "actorTableCount": binding_summary.get("actorTableCount"),
            "selectorProducerProven": False,
            "selectorSourceGrounded": bool(selector_record.get("sourceExpression")),
            "selectorProducerProbeStatus": binding_summary.get("selectorProducerProbeStatus"),
            "statusPayloadConsumerGrounded": payload_consumers.get("promptPayloadCommandCount", 0) > 0,
            "statusPayloadPromptCommandCount": payload_consumers.get("promptPayloadCommandCount", 0),
            "postSelectorActorBranchGrounded": actor_branch.get("actorSlotBranchGrounded", False),
            "backingStoreGrounded": backing_store.get("exactBackingRefCount") == 6,
            "backingStoreExactRefCount": backing_store.get("exactBackingRefCount"),
            "backingOffset0WriterProven": False,
            "decision": (
                "상태창 짧은 문구의 9개 group, actor table, 0x004e8610 selector array는 확정됐다. "
                "상태창 payload를 실행하는 0x2f consumer, 0x41b66d text/status control interpreter, "
                "40 14 selector source, selector 뒤 actor-slot branch도 확인됐다. "
                "0x004576d8 backing block의 저장/로드 구조도 확인됐다. "
                "하지만 backing selector value offset +0을 쓰는 writer와 게임 내 의미는 아직 증명되지 않았다."
            ),
        },
        "surfaces": surfaces,
        "selectorBoundary": selector_boundary,
        "payloadConsumers": payload_consumers,
        "postSelectorActorBranch": actor_branch,
        "backingStoreEvidence": backing_store,
        "groupRows": group_rows,
        "conclusions": conclusions,
        "nextFrontier": [
            "status/menu payload consumer, 40 14 source, actor-slot branch, 0x004576d8 backing block 저장/로드는 grounded. 이제 0x004576d8+0 backing value writer를 찾는다.",
            "scene-root-contained group 7개와 local-pointer-owned group 2개를 분리해, selector index가 scene group 순서인지 상태 flag인지 대조한다.",
            "상태창을 여는 opener가 확정되면 그 call path에서 0x004576d8 주변 write를 함께 추적한다.",
            "정적 상태에서는 0x40 VM interpreter의 남은 handler 중 40 1f/40 2e/40 30 쪽 소비 의미를 더 디코드한다. 런타임 trace는 기본 경로로 두지 않는다.",
        ],
    }


def render_html(report: dict[str, Any]) -> str:
    s = report["summary"]
    cards = [
        ("status", report["status"]),
        ("groups", s["commentGroupCount"]),
        ("actor tables", s["actorTableCount"]),
        ("selector array", s["selectorArrayCount"]),
        ("selector source", "grounded" if s.get("selectorSourceGrounded") else "unproven"),
        ("producer", "proven" if s["selectorProducerProven"] else "unproven"),
    ]
    card_html = "".join(f"<div class='card'><b>{h(k)}</b><span>{h(v)}</span></div>" for k, v in cards)
    surface_rows = []
    for row in report["surfaces"]:
        surface_rows.append(
            "<tr>"
            f"<td><b>{h(row['surface'])}</b></td>"
            f"<td>{h(row['promotion'])}</td>"
            f"<td>{h(row['finding'])}</td>"
            f"<td>{h(row['gap'])}</td>"
            "</tr>"
        )
    boundary_rows = []
    for row in report.get("selectorBoundary", []):
        boundary_rows.append(
            "<tr>"
            f"<td><b>{h(row.get('surface'))}</b></td>"
            f"<td><code>{h(row.get('va'))}</code></td>"
            f"<td>{h(row.get('role'))}</td>"
            f"<td>{h(row.get('evidence'))}</td>"
            f"<td>{h(row.get('producerMeaning'))}</td>"
            "</tr>"
        )
    payload_consumer_rows = []
    for row in (report.get("payloadConsumers") or {}).get("refs", []):
        payload_consumer_rows.append(
            "<tr>"
            f"<td><code>{h(row.get('refVaHex'))}</code></td>"
            f"<td><code>{h(row.get('commandVaHex'))}</code></td>"
            f"<td>{h(row.get('classification'))}</td>"
            f"<td>{h(row.get('modeHex'))}</td>"
            f"<td><code>{h(row.get('commandRawHex'))}</code></td>"
            "</tr>"
        )
    decoded_status_rows = []
    for row in (report.get("payloadConsumers") or {}).get("decodedTopStreamStatusCommands", []):
        decoded_status_rows.append(
            "<tr>"
            f"<td><code>{h(row.get('vaHex'))}</code></td>"
            f"<td><code>{h(row.get('opcodeHex'))}</code></td>"
            f"<td><code>{h(row.get('rawHex'))}</code></td>"
            f"<td>{h(row.get('summary'))}</td>"
            "</tr>"
        )
    actor_branch_rows = []
    for row in (report.get("postSelectorActorBranch") or {}).get("entries", []):
        actor_branch_rows.append(
            "<tr>"
            f"<td>{h(row.get('index'))}</td>"
            f"<td><code>{h(row.get('entryVaHex'))}</code></td>"
            f"<td>{h(row.get('classification'))}</td>"
            f"<td>{h(row.get('actorSlotCandidate'))}</td>"
            f"<td>{h(row.get('continuationKind'))}</td>"
            f"<td><code>{h(row.get('continuationVaHex'))}</code></td>"
            f"<td><code>{h(row.get('firstRawHex'))}</code></td>"
            "</tr>"
        )
    group_rows = []
    for row in report["groupRows"]:
        group_rows.append(
            "<tr>"
            f"<td><code>{h(row['tableIdHex'])}</code></td>"
            f"<td><code>{h(row['blockVaHex'])}</code></td>"
            f"<td>{h(row['markerSlotCount'])}/{h(row['emptySlotCount'])}</td>"
            f"<td>{h(row['parsedTableCount'])}</td>"
            f"<td>{h(', '.join(row['classifications']))}</td>"
            f"<td>{h(row['sample'])}</td>"
            "</tr>"
        )
    conclusions = "".join(f"<li>{h(item)}</li>" for item in report["conclusions"])
    frontier = "".join(f"<li>{h(item)}</li>" for item in report["nextFrontier"])
    payload = json.dumps(report, ensure_ascii=False)
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Status Comment Producer Boundary Review</title>
  <style>
    body {{ margin:0; background:#101318; color:#edf1f7; font-family:system-ui,sans-serif; }}
    main {{ max-width:1240px; margin:0 auto; padding:24px; }}
    a {{ color:#8ecbff; }}
    .nav {{ display:flex; flex-wrap:wrap; gap:8px; margin-bottom:16px; }}
    .chip {{ border:1px solid #334155; border-radius:999px; padding:6px 10px; text-decoration:none; background:#161b22; }}
    .summary {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:12px; margin:16px 0; }}
    .card {{ border:1px solid #2b3544; border-radius:8px; padding:12px; background:#161b22; }}
    .card b {{ display:block; color:#9fb1c9; font-size:12px; text-transform:uppercase; }}
    .card span {{ display:block; margin-top:8px; font-size:18px; overflow-wrap:anywhere; }}
    section {{ border:1px solid #273244; border-radius:10px; padding:16px; margin:16px 0; background:#141922; overflow:auto; }}
    table {{ border-collapse:collapse; width:100%; min-width:1040px; font-size:13px; }}
    th,td {{ border-bottom:1px solid #283342; padding:8px; text-align:left; vertical-align:top; }}
    th {{ color:#b9c7dc; background:#111722; }}
    code {{ color:#dbeafe; overflow-wrap:anywhere; }}
    pre {{ white-space:pre-wrap; background:#0b0f14; border:1px solid #253044; border-radius:8px; padding:12px; max-height:360px; overflow:auto; }}
  </style>
</head>
<body>
<main>
  <div class="nav">
    <a class="chip" href="index.html">index</a>
    <a class="chip" href="status_comment_selector_binding_review.html">selector binding</a>
    <a class="chip" href="status_menu_ui_expression_review.html">status UI</a>
    <a class="chip" href="status_menu_vm_table_review.html">0x40 VM tables</a>
  </div>
  <h1>Status Comment Producer Boundary Review</h1>
  <p>{h(s["decision"])}</p>
  <div class="summary">{card_html}</div>
  <section>
    <h2>Producer Boundary</h2>
    <table>
      <thead><tr><th>surface</th><th>promotion</th><th>finding</th><th>gap</th></tr></thead>
      <tbody>{''.join(surface_rows)}</tbody>
    </table>
  </section>
  <section>
    <h2>Selector Boundary</h2>
    <table>
      <thead><tr><th>surface</th><th>VA</th><th>role</th><th>evidence</th><th>producer meaning</th></tr></thead>
      <tbody>{''.join(boundary_rows)}</tbody>
    </table>
  </section>
  <section>
    <h2>Status Payload Consumers</h2>
    <p>상태창 payload 실행 경계입니다. short-comment selector index producer 확정 증거는 아닙니다.</p>
    <table>
      <thead><tr><th>ref</th><th>command</th><th>classification</th><th>mode</th><th>raw</th></tr></thead>
      <tbody>{''.join(payload_consumer_rows)}</tbody>
    </table>
    <h3>Decoded Top Stream Status Commands</h3>
    <table>
      <thead><tr><th>VA</th><th>op</th><th>raw</th><th>summary</th></tr></thead>
      <tbody>{''.join(decoded_status_rows)}</tbody>
    </table>
    <h3>Post-Selector Actor Branch</h3>
    <p>{h((report.get('postSelectorActorBranch') or {}).get('interpretation'))}</p>
    <table>
      <thead><tr><th>index</th><th>entry</th><th>classification</th><th>actor slot</th><th>continuation</th><th>target</th><th>raw</th></tr></thead>
      <tbody>{''.join(actor_branch_rows)}</tbody>
    </table>
  </section>
  <section>
    <h2>Comment Groups</h2>
    <table>
      <thead><tr><th>table id</th><th>block</th><th>marker/empty slots</th><th>tables</th><th>classification</th><th>sample</th></tr></thead>
      <tbody>{''.join(group_rows)}</tbody>
    </table>
  </section>
  <section>
    <h2>Conclusions</h2>
    <ul>{conclusions}</ul>
  </section>
  <section>
    <h2>Next Frontier</h2>
    <ul>{frontier}</ul>
  </section>
  <section>
    <h2>Raw JSON</h2>
    <pre id="json"></pre>
  </section>
</main>
<script>
window.HWANSE_STATUS_COMMENT_PRODUCER_BOUNDARY_REVIEW = {payload};
document.getElementById('json').textContent = JSON.stringify(window.HWANSE_STATUS_COMMENT_PRODUCER_BOUNDARY_REVIEW, null, 2);
</script>
</body>
</html>
"""


def main() -> int:
    report = build_report()
    OUT.mkdir(exist_ok=True)
    WEB.mkdir(exist_ok=True)
    write_json(OUT / "status_comment_producer_boundary_review.json", report)
    html_text = render_html(report)
    print("status_comment_producer_boundary_review ok")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
