#!/usr/bin/env python3
"""Build a prompt/choice boundary review for the Scene/Event VM goal.

The map1_01a trace already contains useful text-flow candidates, but the raw
trace mixes prompt text, choice labels, wait/render opcodes, and branch calls in
one long table.  This builder separates those layers and deliberately keeps the
unproven pieces blocked instead of promoting the whole sequence as executed
story flow.
"""
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 = ROOT / "web"


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


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 h(value: Any) -> str:
    return html.escape("" if value is None else str(value))


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


def rel(path: Path) -> str:
    try:
        return path.resolve().relative_to(ROOT).as_posix()
    except ValueError:
        return path.as_posix()


def prompt_text(prompt: dict[str, Any], limit: int = 180) -> str:
    return short((prompt.get("displayText") or "").replace("\n", " / "), limit)


def command_boundary_rows(command_rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
    focused = {"0x00", "0x02", "0x06", "0x09", "0x0b", "0x18", "0x35"}
    rows = []
    for row in command_rows:
        if row.get("opcodeHex") not in focused:
            continue
        rows.append(
            {
                "promptOrder": row.get("promptOrder"),
                "commandOrderInPrompt": row.get("commandOrderInPrompt"),
                "vaHex": row.get("vaHex"),
                "opcodeHex": row.get("opcodeHex"),
                "opcodeEvidenceLevel": row.get("opcodeEvidenceLevel"),
                "label": row.get("label"),
                "roles": row.get("roles") or [],
                "branchTargetVaHex": row.get("branchTargetVaHex"),
                "rawBytes": row.get("rawBytes"),
            }
        )
    return rows


def choice_boundaries(prompts: list[dict[str, Any]]) -> list[dict[str, Any]]:
    choices = [row for row in prompts if row.get("status") == "choice-marker-delimited"]
    result = []
    for choice in choices:
        order = int(choice.get("order") or 0)
        prev_prompt = next((row for row in reversed(prompts) if int(row.get("order") or 0) < order), None)
        next_choice_order = next(
            (
                int(row.get("order") or 0)
                for row in prompts
                if int(row.get("order") or 0) > order and row.get("status") == "choice-marker-delimited"
            ),
            10**9,
        )
        following = [
            row
            for row in prompts
            if order < int(row.get("order") or 0) < next_choice_order
        ]
        trace_choice_prompts = [
            row for row in following if "0x18" in (row.get("traceOpcodeSet") or [])
        ]
        branch_calls = []
        for row in trace_choice_prompts:
            for trace_row in row.get("trace") or []:
                if trace_row.get("opcodeHex") == "0x09":
                    branch_calls.append(
                        {
                            "promptOrder": row.get("order"),
                            "vaHex": trace_row.get("vaHex"),
                            "branchTargetVaHex": trace_row.get("branchTargetVaHex"),
                            "rawBytes": trace_row.get("rawBytes"),
                        }
                    )
        choice_markers = []
        for row in trace_choice_prompts:
            for trace_row in row.get("trace") or []:
                if trace_row.get("opcodeHex") == "0x18":
                    choice_markers.append(
                        {
                            "promptOrder": row.get("order"),
                            "vaHex": trace_row.get("vaHex"),
                            "rawBytes": trace_row.get("rawBytes"),
                            "roles": trace_row.get("roles") or [],
                        }
                    )
        result.append(
            {
                "choicePromptOrder": choice.get("order"),
                "choicePromptId": choice.get("promptId"),
                "choiceStartVaHex": choice.get("startVaHex"),
                "choiceText": prompt_text(choice, 240),
                "previousPromptOrder": (prev_prompt or {}).get("order"),
                "previousPromptText": prompt_text(prev_prompt or {}, 180),
                "followingCandidateCount": len(following),
                "followingCandidates": [
                    {
                        "order": row.get("order"),
                        "promptId": row.get("promptId"),
                        "status": row.get("status"),
                        "startVaHex": row.get("startVaHex"),
                        "hasChoiceOpcode": "0x18" in (row.get("traceOpcodeSet") or []),
                        "text": prompt_text(row, 180),
                    }
                    for row in following[:8]
                ],
                "tracePromptOrdersWithChoiceOpcode": [row.get("order") for row in trace_choice_prompts],
                "choiceOpcodeRows": choice_markers,
                "branchCallRows": branch_calls,
                "selectionToNextPromptProofFound": False,
                "promotion": "partial" if choice_markers or branch_calls else "candidate",
                "remainingGap": (
                    "choice marker and branch-call rows are visible, but selected option -> next prompt target is not proven"
                    if choice_markers or branch_calls
                    else "choice text is identified, but the opcode/control rows for this displayed slice are not shown"
                ),
            }
        )
    return result


def prompt_rows(prompts: list[dict[str, Any]]) -> list[dict[str, Any]]:
    rows = []
    for row in prompts:
        rows.append(
            {
                "order": row.get("order"),
                "promptId": row.get("promptId"),
                "matchedStoryPromptId": row.get("matchedStoryPromptId"),
                "status": row.get("status"),
                "evidenceLevel": row.get("evidenceLevel"),
                "startVaHex": row.get("startVaHex"),
                "renderVaHex": row.get("renderVaHex"),
                "waitVaHex": row.get("waitVaHex"),
                "traceOpcodeSet": row.get("traceOpcodeSet") or [],
                "text": prompt_text(row, 220),
            }
        )
    return rows


def build(args: argparse.Namespace) -> dict[str, Any]:
    trace_payload = load_json(args.map1_trace, {})
    trace = trace_payload.get("map1_01aTrace") or {}
    prompts = trace.get("prompts") or []
    command_rows = trace.get("commandTraceRows") or []
    boundaries = command_boundary_rows(command_rows)
    choices = choice_boundaries(prompts)
    opcode_counts = Counter(row.get("opcodeHex") for row in boundaries)
    choice_rows_shown = [row for row in prompts if row.get("status") == "choice-marker-delimited"]
    render_rows = [row for row in boundaries if row.get("opcodeHex") == "0x0b"]
    wait_rows = [row for row in boundaries if row.get("opcodeHex") == "0x06"]
    branch_rows = [row for row in boundaries if row.get("opcodeHex") == "0x09"]
    choice_opcode_rows = [row for row in boundaries if row.get("opcodeHex") == "0x18"]
    end_rows = [row for row in boundaries if row.get("opcodeHex") == "0x00"]
    choice_marker_rows = [
        marker
        for choice in choices
        for marker in (choice.get("choiceOpcodeRows") or [])
    ]
    choice_branch_rows = [
        branch
        for choice in choices
        for branch in (choice.get("branchCallRows") or [])
    ]

    summary = {
        "map": trace.get("map"),
        "selectedGroupId": trace.get("selectedGroupId"),
        "selectedSequenceId": trace.get("selectedSequenceId"),
        "selectedRootVaHex": trace.get("selectedRootVaHex"),
        "selectedRootEndVaHex": trace.get("selectedRootEndVaHex"),
        "selectedEvidenceStatus": trace.get("selectedEvidenceStatus"),
        "promotionStatus": "prompt-sequence-partial-choice-branch-blocked",
        "promptSequenceProofFound": False,
        "promptRootCandidatePartial": bool(trace.get("selectedRootVaHex")),
        "directScenePromptBindingFound": False,
        "choiceBranchProofFound": False,
        "selectionToNextPromptProofFound": False,
        "promptCountInSequence": trace.get("promptCountInSequence"),
        "promptsShown": trace.get("promptsShown"),
        "choiceCountInSequence": trace.get("choiceCountInSequence"),
        "choiceRowsShown": len(choice_rows_shown),
        "commandTraceRowsShown": trace.get("commandTraceRowsShown"),
        "boundaryRowCount": len(boundaries),
        "renderOpcodeRowCount": len(render_rows),
        "waitOpcodeRowCount": len(wait_rows),
        "choiceOpcodeRowCount": len(choice_opcode_rows),
        "branchCallRowCount": len(branch_rows),
        "choiceBoundaryMarkerRowCount": len(choice_marker_rows),
        "choiceBoundaryBranchCallRowCount": len(choice_branch_rows),
        "endClearRowCount": len(end_rows),
        "opcodeBoundaryCounts": dict(sorted(opcode_counts.items())),
    }

    decisions = [
        {
            "item": "selected text-root prompt range",
            "promotion": "partial",
            "evidence": f"{summary['selectedGroupId']} / {summary['selectedSequenceId']} root {summary['selectedRootVaHex']}..{summary['selectedRootEndVaHex']}",
            "remainingGap": "direct normal scene record -> executed prompt root binding is still missing",
        },
        {
            "item": "prompt render/wait boundary",
            "promotion": "grounded-in-trace",
            "evidence": f"0x0b render rows={summary['renderOpcodeRowCount']}, 0x06 wait rows={summary['waitOpcodeRowCount']}",
            "remainingGap": "rows are still selector-root-range trace evidence rather than full field-scene runtime proof",
        },
        {
            "item": "choice marker boundary",
            "promotion": "partial",
            "evidence": f"choice text rows shown={summary['choiceRowsShown']}, choice-boundary opcode 0x18 rows={summary['choiceBoundaryMarkerRowCount']}",
            "remainingGap": "choice text rows and opcode 0x18 rows are not always in the same prompt row, so it remains a boundary candidate",
        },
        {
            "item": "choice branch call",
            "promotion": "partial-blocked",
            "evidence": f"choice-boundary opcode 0x09 branch-call rows={summary['choiceBoundaryBranchCallRowCount']}",
            "remainingGap": "selected option value and next prompt target are not proven",
        },
        {
            "item": "prompt sequence as story flow",
            "promotion": "blocked",
            "evidence": "early Ataho cave text matches the observed text sample",
            "remainingGap": "sequence contains candidate alternatives around choices; it must not be treated as a single linear script",
        },
    ]

    return {
        "scope": "scene/event VM prompt sequence and choice boundary review",
        "promotionStatus": summary["promotionStatus"],
        "summary": summary,
        "decisions": decisions,
        "choiceBoundaries": choices,
        "promptRows": prompt_rows(prompts),
        "commandBoundaryRows": boundaries,
        "remainingProofs": [
            "direct map1_01a scene record -> selected prompt root binding",
            "selected choice value -> next prompt target edge",
            "branch target 0x004bfdb4 role for each choice path",
            "normal field-scene runtime proof that this candidate sequence is the executed sequence",
            "full unshown prompt/choice coverage beyond the first 32 prompt rows",
        ],
        "sourceArtifacts": {
            "map1Trace": rel(args.map1_trace),
            "sceneVmReference": "docs/SCENE_EVENT_VM_REFERENCE.md",
            "sceneVmGoal": "docs/SCENE_EVENT_VM_GOAL.md",
        },
    }


def html_doc(payload: dict[str, Any]) -> str:
    s = payload["summary"]

    def tag(status: Any) -> str:
        text = str(status)
        cls = "good" if text in {"grounded", "confirmed", "grounded-in-trace"} else "warn" if "partial" in text or "grounded" in text else "bad"
        return f'<span class="tag {cls}">{h(text)}</span>'

    metrics = [
        ("sequence", f"{s['selectedGroupId']} / {s['selectedSequenceId']}"),
        ("root", f"{s['selectedRootVaHex']}..{s['selectedRootEndVaHex']}"),
        ("prompts", f"{s['promptsShown']} shown / {s['promptCountInSequence']} sequence"),
        ("choices", f"{s['choiceRowsShown']} shown / {s['choiceCountInSequence']} sequence"),
        ("choice boundary markers", s["choiceBoundaryMarkerRowCount"]),
        ("choice boundary branch calls", s["choiceBoundaryBranchCallRowCount"]),
        ("branch proof", s["choiceBranchProofFound"]),
    ]
    metric_html = "\n".join(f"<div class='metric'><span>{h(k)}</span><strong>{h(v)}</strong></div>" for k, v in metrics)
    decision_rows = "\n".join(
        f"<tr><td>{h(row['item'])}</td><td>{tag(row['promotion'])}</td><td>{h(row['evidence'])}</td><td>{h(row['remainingGap'])}</td></tr>"
        for row in payload["decisions"]
    )
    choice_rows = []
    for row in payload["choiceBoundaries"]:
        candidates = "<br>".join(
            f"#{h(item['order'])} {h(item['promptId'])} {h(item['text'])}"
            for item in row["followingCandidates"]
        )
        branch_calls = "<br>".join(
            f"<code>{h(item.get('vaHex'))}</code> -> <code>{h(item.get('branchTargetVaHex'))}</code>"
            for item in row["branchCallRows"]
        )
        marker_raws = "<br>".join(
            f"<code>{h(item.get('vaHex'))}</code> <code>{h(item.get('rawBytes'))}</code>"
            for item in row["choiceOpcodeRows"]
        )
        choice_rows.append(
            f"<tr><td>{h(row['choicePromptOrder'])}</td><td><code>{h(row['choiceStartVaHex'])}</code></td><td>{h(row['choiceText'])}</td><td>{h(row['tracePromptOrdersWithChoiceOpcode'])}</td><td>{marker_raws or '-'}</td><td>{branch_calls or '-'}</td><td>{candidates}</td><td>{h(row['remainingGap'])}</td></tr>"
        )
    prompt_rows_html = "\n".join(
        f"<tr><td>{h(row['order'])}</td><td>{tag(row['evidenceLevel'])}</td><td>{h(row['status'])}</td><td><code>{h(row['startVaHex'])}</code></td><td><code>{h(row['promptId'])}</code></td><td>{h(row['text'])}</td><td>{h(', '.join(row['traceOpcodeSet']))}</td></tr>"
        for row in payload["promptRows"]
    )
    boundary_rows = "\n".join(
        f"<tr><td>{h(row['promptOrder'])}</td><td>{h(row['commandOrderInPrompt'])}</td><td><code>{h(row['vaHex'])}</code></td><td><code>{h(row['opcodeHex'])}</code></td><td>{h(row['label'])}</td><td>{h(', '.join(row['roles']))}</td><td><code>{h(row['branchTargetVaHex'] or '-')}</code></td><td><code>{h(row['rawBytes'])}</code></td></tr>"
        for row in payload["commandBoundaryRows"]
    )
    proofs = "\n".join(f"<li>{h(row)}</li>" for row in payload["remainingProofs"])
    artifacts = "\n".join(f"<li><code>{h(k)}</code>: <a href='../{h(v)}'>{h(v)}</a></li>" for k, v in payload["sourceArtifacts"].items())
    return f"""<!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>Scene/Event VM prompt sequence 검토</title>
  <style>
    :root {{ color-scheme: light; --border:#d8dee6; --ink:#17202a; --muted:#607080; --panel:#fff; --head:#eef2f6; --bg:#f6f7f9; }}
    * {{ box-sizing:border-box; }}
    body {{ margin:0; background:var(--bg); color:var(--ink); font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; line-height:1.45; }}
    main {{ max-width:1500px; margin:0 auto; padding:18px; }}
    header {{ display:flex; justify-content:space-between; align-items:flex-start; gap:16px; margin-bottom:14px; }}
    h1 {{ margin:0; font-size:24px; }}
    h2 {{ margin:0; font-size:17px; }}
    a {{ color:#185abc; font-weight:700; text-decoration:none; }}
    a:hover {{ text-decoration:underline; }}
    nav {{ display:flex; flex-wrap:wrap; gap:10px; justify-content:flex-end; }}
    section, details {{ background:var(--panel); border:1px solid var(--border); border-radius:8px; margin:14px 0; overflow:hidden; }}
    summary {{ cursor:pointer; padding:12px 14px; background:var(--head); font-weight:800; }}
    .head {{ display:flex; justify-content:space-between; gap:12px; padding:12px 14px; background:var(--head); border-bottom:1px solid var(--border); }}
    .body {{ padding:14px; }}
    .metrics {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:10px; }}
    .metric {{ border:1px solid var(--border); border-radius:6px; background:#f8fafc; padding:10px; }}
    .metric span {{ display:block; color:var(--muted); font-size:12px; }}
    .metric strong {{ display:block; font-size:17px; word-break:break-word; }}
    table {{ width:100%; border-collapse:collapse; }}
    th,td {{ padding:8px 10px; border-bottom:1px solid var(--border); vertical-align:top; text-align:left; font-size:13px; }}
    th {{ background:#f8fafc; color:#344050; }}
    code {{ font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }}
    .tag {{ display:inline-block; padding:2px 7px; border-radius:999px; background:#edf2f7; color:#334155; font-size:12px; white-space:nowrap; }}
    .tag.good {{ color:#0f766e; background:#e6f4f1; }}
    .tag.warn {{ color:#a15c00; background:#fff4df; }}
    .tag.bad {{ color:#b42318; background:#fdebea; }}
    .muted {{ color:var(--muted); }}
  </style>
</head>
<body>
<main data-page="scene-event-vm-prompt-sequence-review">
  <header>
    <div>
      <h1>Scene/Event VM prompt sequence 검토</h1>
      <p class="muted">map1_01a 후보 대사 흐름에서 prompt, wait/render, choice, branch-call 경계를 분리한다.</p>
    </div>
    <nav aria-label="review links">
      <a href="index.html">관리 홈</a>
      <a href="scene_event_vm_review.html">VM 검토</a>
      <a href="scene_event_vm_execution_route_review.html">실행 루트</a>
      <a href="scene_event_vm_branch_flag_review.html">branch/flag</a>
      <a href="scene_event_vm_choice_target_review.html">choice target</a>
      <a href="scene_event_vm_random_gate_review.html">random gate</a>
      <a href="selected_scene_text_root_consumer_review.html">consumer</a>
      <a href="scene_event_runtime_evidence_handoff.html">runtime handoff</a>
      <a href="../out/scene_event_vm_prompt_sequence_review.json">prompt sequence JSON</a>
    </nav>
  </header>
  <section>
    <div class="head"><h2>요약</h2><span>{tag(payload['promotionStatus'])}</span></div>
    <div class="body metrics">{metric_html}</div>
  </section>
  <section>
    <div class="head"><h2>Decisions</h2><span>후보/partial/blocked 분리</span></div>
    <table><thead><tr><th>item</th><th>promotion</th><th>evidence</th><th>remaining gap</th></tr></thead><tbody>{decision_rows}</tbody></table>
  </section>
  <section>
    <div class="head"><h2>Choice Boundaries</h2><span>{s['choiceRowsShown']} shown</span></div>
    <table><thead><tr><th>choice</th><th>start</th><th>choice text</th><th>trace prompt with 0x18</th><th>0x18 marker raw</th><th>branch calls</th><th>following candidates</th><th>remaining gap</th></tr></thead><tbody>{''.join(choice_rows)}</tbody></table>
  </section>
  <section>
    <div class="head"><h2>Prompt Rows</h2><span>{s['promptsShown']} shown</span></div>
    <table><thead><tr><th>#</th><th>level</th><th>status</th><th>start</th><th>prompt</th><th>text</th><th>opcodes</th></tr></thead><tbody>{prompt_rows_html}</tbody></table>
  </section>
  <details>
    <summary>Command Boundary Rows</summary>
    <table><thead><tr><th>prompt</th><th>cmd</th><th>VA</th><th>opcode</th><th>label</th><th>roles</th><th>branch target</th><th>raw</th></tr></thead><tbody>{boundary_rows}</tbody></table>
  </details>
  <section>
    <div class="head"><h2>Remaining Proofs</h2><span>goal continuation</span></div>
    <div class="body"><ul>{proofs}</ul></div>
  </section>
  <details>
    <summary>Source Artifacts</summary>
    <div class="body"><ul>{artifacts}</ul></div>
  </details>
</main>
<script>
window.HWANSE_SCENE_EVENT_VM_PROMPT_SEQUENCE_REVIEW_READY = true;
window.HWANSE_SCENE_EVENT_VM_PROMPT_SEQUENCE_REVIEW = {{
  promptSequenceProofFound: false,
  promptRootCandidatePartial: {str(s['promptRootCandidatePartial']).lower()},
  directScenePromptBindingFound: false,
  choiceBranchProofFound: false,
  selectionToNextPromptProofFound: false,
  promptsShown: {s['promptsShown']},
  choiceCountInSequence: {s['choiceCountInSequence']},
  choiceRowsShown: {s['choiceRowsShown']},
  choiceOpcodeRowCount: {s['choiceOpcodeRowCount']},
  branchCallRowCount: {s['branchCallRowCount']},
  choiceBoundaryMarkerRowCount: {s['choiceBoundaryMarkerRowCount']},
  choiceBoundaryBranchCallRowCount: {s['choiceBoundaryBranchCallRowCount']},
  promotionStatus: "{h(payload['promotionStatus'])}"
}};
</script>
</body>
</html>
"""


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("--map1-trace", type=Path, default=OUT / "map1_01a_scene_trace.json")
    parser.add_argument("--json-out", type=Path, default=OUT / "scene_event_vm_prompt_sequence_review.json")
    parser.add_argument("--html-out", type=Path, default=WEB / "scene_event_vm_prompt_sequence_review.html")
    parser.add_argument("--web-out", type=Path, default=WEB / "scene_event_vm_prompt_sequence_review.html")
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    payload = build(args)
    write_json(args.json_out, payload)
    html_payload = html_doc(payload)
    write_text(args.html_out, html_payload)
    write_text(args.web_out, html_payload)
    print(f"wrote scene/event VM prompt sequence review -> {args.web_out}")


if __name__ == "__main__":
    main()
