#!/usr/bin/env python3
"""Build a compact dialogue prompt render pipeline review.

This report joins the pieces that are now grounded separately:

* prompt/window templates,
* event-object VM text source/render opcodes,
* inline ``40 35 aa bb`` speaker portrait selectors.

It deliberately keeps the unresolved producer side separate.  The renderer can
now be described, but the exact scene/event root selection path is still a
different proof.
"""
from __future__ import annotations

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


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

JSON_OUT = OUT / "dialogue_prompt_render_pipeline_review.json"
HTML_OUT = WEB / "dialogue_prompt_render_pipeline_review.html"
WEB_HTML_OUT = WEB / "dialogue_prompt_render_pipeline_review.html"


def load_json(path: Path, default: Any) -> Any:
    if not path.exists():
        return default
    return json.loads(path.read_text(encoding="utf-8"))


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


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


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


def short(value: Any, limit: int = 220) -> str:
    if isinstance(value, (dict, list)):
        value = json.dumps(value, ensure_ascii=False, sort_keys=True)
    text = " ".join(str(value or "").split())
    return text if len(text) <= limit else text[: limit - 1] + "…"


def sample_inline_rows(face_review: dict[str, Any]) -> list[dict[str, Any]]:
    wanted = {
        "story-prompt-2373",
        "story-prompt-4235",
    }
    rows = []
    for row in face_review.get("inline4035SpeakerRowsSample") or []:
        if row.get("promptId") in wanted:
            rows.append(row)
    # story-prompt-4235 has multiple speakers in one render block.  Keep the
    # rows from that block together when available.
    render_4235 = next((row.get("renderVaHex") for row in rows if row.get("promptId") == "story-prompt-4235"), None)
    if render_4235:
        for row in face_review.get("inline4035SpeakerRowsSample") or []:
            if row.get("renderVaHex") == render_4235 and row not in rows:
                rows.append(row)
    return rows[:12]


def build_payload() -> dict[str, Any]:
    template_review = load_json(OUT / "dialogue_template_text_face_review.json", {})
    text_consumer = load_json(OUT / "scene_event_text_consumer_trace_review.json", {})
    selected_root = load_json(OUT / "selected_scene_text_root_consumer_review.json", {})
    face_review = load_json(OUT / "dialogue_face_selector_gap_review.json", {})
    opcode_dict = load_json(OUT / "scene_event_vm_opcode_dictionary.json", {})
    prompt_sequence = load_json(OUT / "scene_event_vm_prompt_sequence_review.json", {})

    opcode_by_hex = {
        row.get("opcodeHex"): row
        for row in opcode_dict.get("opcodeDictionary") or []
        if row.get("opcodeHex") in {"0x06", "0x09", "0x0b", "0x0d", "0x35"}
    }
    template_summary = template_review.get("summary") or {}
    text_summary = text_consumer.get("summary") or {}
    root_summary = selected_root.get("summary") or {}
    face_summary = face_review.get("summary") or {}

    chain = [
        {
            "step": "window template / region",
            "status": "grounded",
            "evidence": (
                f"dialogue regions {template_summary.get('dialogueRegionIndexes')} use templates "
                f"{template_summary.get('dialogueRegionTemplates')}; region draw routine "
                f"{template_summary.get('regionDrawFunctionVaHex')}"
            ),
            "remainingGap": "runtime template selection per prompt height/layout is not fully named.",
        },
        {
            "step": "selected root consumer",
            "status": "grounded consumer, producer open",
            "evidence": (
                f"selected root global {root_summary.get('selectedPointerGlobalHex')} is consumed by "
                f"{root_summary.get('consumerHandlerVaHex')}; reads at {root_summary.get('consumerReadVaHex')}"
            ),
            "remainingGap": "normal scene/event route -> exact selected root producer is still not proven.",
        },
        {
            "step": "text source setup opcode 0x0d",
            "status": "grounded",
            "evidence": (opcode_by_hex.get("0x0d") or {}).get("effect"),
            "remainingGap": "per-scene use of 0x0d is data-stream state, not direct textVa code refs.",
        },
        {
            "step": "text render opcode 0x0b",
            "status": "grounded",
            "evidence": (opcode_by_hex.get("0x0b") or {}).get("effect"),
            "remainingGap": "specific prompt stream execution order remains partial around choice branches.",
        },
        {
            "step": "input wait opcode 0x06",
            "status": "grounded",
            "evidence": (opcode_by_hex.get("0x06") or {}).get("effect"),
            "remainingGap": "browser replay only approximates the wait loop.",
        },
        {
            "step": "inline portrait selector 40 35 aa bb",
            "status": "grounded",
            "evidence": (
                f"{face_summary.get('inline4035SpeakerOpcodeCount')} selectors; "
                f"speaker/face mismatches {face_summary.get('inline4035SpeakerFaceMismatchCount')}; "
                f"aa status {face_summary.get('inline4035ArgAStatus')}"
            ),
            "remainingGap": "the opcode 0x35 handler body is not fully named, but storage semantics are grounded by all observed speaker rows.",
        },
    ]

    decisions = [
        {
            "item": "Can prompt text and portrait be rendered from current data?",
            "answer": "yes",
            "reason": "window template, text render opcode, wait opcode, and inline portrait selector are all grounded enough for replay/rendering.",
        },
        {
            "item": "Can this prove scene story flow?",
            "answer": "not yet",
            "reason": "the selected root consumer is known, but the producer that chooses an exact scene/root and choice branch remains unresolved.",
        },
        {
            "item": "Best next reverse-trace target",
            "answer": "selected-root producer / branch target",
            "reason": "following the prompt renderer further will mostly re-prove drawing; finding who writes the selected root connects scene -> prompt.",
        },
    ]

    return {
        "scope": "dialogue prompt render pipeline",
        "promotionStatus": "render-pipeline-grounded-root-producer-open",
        "summary": {
            "dialogueRegionIndexes": template_summary.get("dialogueRegionIndexes"),
            "dialogueRegionTemplates": template_summary.get("dialogueRegionTemplates"),
            "textRenderHandlerVaHex": template_summary.get("textRenderHandlerVaHex"),
            "textSourceHandlerVaHex": template_summary.get("textSourceHandlerVaHex"),
            "selectedPointerGlobalHex": root_summary.get("selectedPointerGlobalHex"),
            "selectedRootConsumerVerified": root_summary.get("consumerVerified"),
            "routeTextBindingProofFound": text_summary.get("routeTextBindingProofFound"),
            "textConsumerProofFound": text_summary.get("textConsumerProofFound"),
            "inline4035SpeakerOpcodeCount": face_summary.get("inline4035SpeakerOpcodeCount"),
            "inline4035SpeakerFaceMismatchCount": face_summary.get("inline4035SpeakerFaceMismatchCount"),
            "inline4035ArgAStatus": face_summary.get("inline4035ArgAStatus"),
            "promptSequencePromotionStatus": prompt_sequence.get("promotionStatus"),
        },
        "chain": chain,
        "portraitSlots": face_summary.get("inline4035ArgALayoutHints") or [],
        "faceLegend": face_review.get("manualFaceFrameLegend") or [],
        "sampleInlineSelectors": sample_inline_rows(face_review),
        "opcodeConsumers": {key: opcode_by_hex.get(key) for key in ["0x0d", "0x0b", "0x06", "0x09", "0x35"]},
        "decisions": decisions,
        "remainingProofs": [
            "scene/event record or input route that writes the selected root global",
            "selected choice value -> next prompt target",
            "full semantic name for opcode 0x35 handler internals",
        ],
        "sourceArtifacts": {
            "dialogueTemplateTextFace": "out/dialogue_template_text_face_review.json",
            "sceneEventTextConsumer": "out/scene_event_text_consumer_trace_review.json",
            "selectedSceneTextRootConsumer": "out/selected_scene_text_root_consumer_review.json",
            "dialogueFaceSelector": "out/dialogue_face_selector_gap_review.json",
            "promptSequence": "out/scene_event_vm_prompt_sequence_review.json",
        },
    }


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


def render_html(payload: dict[str, Any]) -> str:
    summary_rows = [[esc(k), esc(v)] for k, v in payload["summary"].items()]
    chain_rows = [
        [
            f"<strong>{esc(row['step'])}</strong>",
            f"<span class=\"tag {'good' if row['status'] == 'grounded' else 'warn'}\">{esc(row['status'])}</span>",
            esc(short(row["evidence"], 360)),
            esc(short(row["remainingGap"], 260)),
        ]
        for row in payload["chain"]
    ]
    slot_rows = [
        [
            f"<code>{esc(row.get('argAHex'))}</code>",
            esc(row.get("promptClass")),
            esc(row.get("positionSlot")),
            esc(row.get("positionSummary")),
            esc(short(row.get("evidence"), 260)),
        ]
        for row in payload.get("portraitSlots") or []
    ]
    face_rows = [
        [
            esc(row.get("frameIndex")),
            esc(row.get("label")),
            esc(row.get("usage")),
            esc(row.get("source")),
        ]
        for row in payload.get("faceLegend") or []
    ]
    sample_rows = [
        [
            esc(row.get("promptId")),
            f"<code>{esc(row.get('opcodeVaHex'))}</code>",
            f"<code>40 35 {int(row.get('argA', 0)):02x} {int(row.get('faceId', 0)):02x}</code>",
            esc(row.get("speaker")),
            esc(row.get("argAPositionSummary")),
            esc(short(row.get("textSample"), 220)),
        ]
        for row in payload.get("sampleInlineSelectors") or []
    ]
    decision_rows = [
        [esc(row["item"]), esc(row["answer"]), esc(row["reason"])]
        for row in payload.get("decisions") or []
    ]
    remaining = "".join(f"<li>{esc(item)}</li>" for item in payload.get("remainingProofs") or [])

    return "\n".join(
        [
            "<!doctype html>",
            '<html lang="ko">',
            "<head>",
            '  <meta charset="utf-8">',
            '  <meta name="viewport" content="width=device-width, initial-scale=1">',
            '  <link rel="icon" href="../favicon.ico" />',
            "  <title>대화 프롬프트 렌더 파이프라인</title>",
            "  <style>",
            "    body{margin:24px;background:#f6f7f9;color:#17202a;font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;line-height:1.45}",
            "    main{max-width:1320px;margin:0 auto}",
            "    section{background:white;border:1px solid #d8dee6;border-radius:8px;margin:14px 0;overflow:hidden}",
            "    h1{font-size:24px;margin:0 0 6px} h2{font-size:17px;margin:0;padding:12px 14px;background:#eef2f6;border-bottom:1px solid #d8dee6}",
            "    p{margin:8px 0;color:#607080}.body{padding:14px}.links{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0}.links a{font-weight:700;color:#185abc;text-decoration:none}",
            "    table{width:100%;border-collapse:collapse}th,td{border-bottom:1px solid #d8dee6;padding:8px 9px;text-align:left;vertical-align:top}th{background:#f8fafc;color:#344050;font-size:13px}",
            "    code{background:#f1f5f9;border:1px solid #d8dee6;border-radius:4px;padding:1px 4px}.tag{display:inline-block;border-radius:999px;padding:2px 7px;font-size:12px;font-weight:700}.good{background:#d8f3ef;color:#0f766e}.warn{background:#fff2cc;color:#a15c00}",
            "    ul{margin:8px 0 0 20px;padding:0}",
            "  </style>",
            "</head>",
            "<body><main>",
            "  <h1>대화 프롬프트 렌더 파이프라인</h1>",
            f"  <p>상태: <strong>{esc(payload['promotionStatus'])}</strong>. 프롬프트 창/텍스트 렌더/초상화 선택은 렌더링 가능한 수준으로 접지됐고, 남은 핵심은 scene/event가 어떤 root를 선택하는지이다.</p>",
            '  <div class="links"><a href="dialogue_template_text_face_review.html">template/text/face</a><a href="scene_event_text_consumer_trace_review.html">text consumer</a><a href="selected_scene_text_root_consumer_review.html">selected root consumer</a><a href="dialogue_face_selector_gap_review.html">face selector</a><a href="scene_event_vm_prompt_sequence_review.html">prompt sequence</a><a href="../out/dialogue_prompt_render_pipeline_review.json">JSON</a></div>',
            "  <section><h2>요약</h2><div class=\"body\">" + table(["key", "value"], summary_rows) + "</div></section>",
            "  <section><h2>렌더 파이프라인</h2><div class=\"body\">" + table(["step", "status", "evidence", "remaining gap"], chain_rows) + "</div></section>",
            "  <section><h2>초상화 위치 슬롯</h2><div class=\"body\">" + table(["aa", "class", "slot", "summary", "evidence"], slot_rows) + "</div></section>",
            "  <section><h2>face_01 프레임</h2><div class=\"body\">" + table(["frame", "label", "usage", "source"], face_rows) + "</div></section>",
            "  <section><h2>대표 inline selector</h2><div class=\"body\">" + table(["prompt", "opcode VA", "raw", "speaker", "slot", "text"], sample_rows) + "</div></section>",
            "  <section><h2>판정</h2><div class=\"body\">" + table(["item", "answer", "reason"], decision_rows) + f"<h3>남은 증거</h3><ul>{remaining}</ul></div></section>",
            "</main></body></html>",
            "",
        ]
    )


def main() -> None:
    OUT.mkdir(parents=True, exist_ok=True)
    WEB.mkdir(parents=True, exist_ok=True)
    payload = build_payload()
    write_json(JSON_OUT, payload)
    html_text = render_html(payload)
    write_text(HTML_OUT, html_text)
    write_text(WEB_HTML_OUT, html_text)
    print(f"wrote {JSON_OUT}")
    print(f"wrote {WEB_HTML_OUT}")


if __name__ == "__main__":
    main()
