#!/usr/bin/env python3
"""Link scene text sequence candidates to resource records without over-promoting.

`scene_text_sequence_review` already groups text entries into `scene-seq-*`
flows.  This pass makes the resource side explicit:

* selector-root groups can share one selected root range with linked CNS/map
  resources,
* scene-proximity groups can sit near a concrete scene/resource record,
* direct event-root execution proof is still absent.

The resulting report is a review surface for "scene unit" candidates, not a
claim that the game event VM has been fully bound to each script 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"
PROMOTION_STATUS = "scene-seq-resource-link-candidate-direct-root-blocked"


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


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


def unique(values: list[str]) -> list[str]:
    seen: set[str] = set()
    rows: list[str] = []
    for value in values:
        if not value or value in seen:
            continue
        seen.add(value)
        rows.append(value)
    return rows


def compact_text(value: str, limit: int = 260) -> str:
    text = " / ".join(str(value or "").splitlines()).strip()
    if len(text) <= limit:
        return text
    return text[: limit - 1] + "..."


def selector_key(row: dict[str, Any]) -> str:
    group = row.get("group")
    slot = row.get("slot")
    return f"{group}:{slot}"


def selector_index(selectors: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
    rows: dict[str, dict[str, Any]] = {}
    for row in selectors:
        key = selector_key(row)
        rows[key] = row
    return rows


def manifest_index(manifest: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
    return {row.get("map", ""): row for row in manifest if row.get("map")}


def manifest_records_for_maps(maps: list[str], manifest_by_map: dict[str, dict[str, Any]]) -> list[dict[str, Any]]:
    records = []
    for name in maps:
        row = manifest_by_map.get(name)
        if not row:
            continue
        records.append(
            {
                "map": row.get("map", ""),
                "sceneIdHex": row.get("sceneIdHex", ""),
                "recordVaHex": row.get("recordVaHex") or hx(row.get("recordVa")),
                "resourceCount": len(row.get("resources") or []),
                "resources": [
                    item.get("filename") or item.get("name") or ""
                    for item in row.get("resources") or []
                ],
            }
        )
    return records


def group_link_class(group: dict[str, Any], records: list[dict[str, Any]]) -> str:
    if group.get("contextKind") == "selector-root":
        if records:
            return "selector-root-resource-text-range-candidate"
        return "selector-root-resource-only-text-range-candidate"
    if group.get("contextKind") == "scene-proximity":
        return "nearest-resource-record-proximity-candidate"
    return "unlinked-text-sequence"


def sequence_rows(group: dict[str, Any]) -> list[dict[str, Any]]:
    rows = []
    for seq in group.get("sequences") or []:
        rows.append(
            {
                "id": seq.get("id", ""),
                "entryStartVaHex": seq.get("entryStartVaHex", ""),
                "entryEndVaHex": seq.get("entryEndVaHex", ""),
                "textStartVaHex": seq.get("textStartVaHex", ""),
                "textEndVaHex": seq.get("textEndVaHex", ""),
                "entryCount": seq.get("entryCount", 0),
                "promptCount": seq.get("promptCount", 0),
                "choiceCount": len(seq.get("choiceReviews") or []),
                "sample": compact_text(seq.get("sample") or "", 420),
                "nearestSceneCounts": seq.get("nearestSceneCounts") or {},
            }
        )
    return rows


def analyze(
    sequence_review: dict[str, Any],
    manifest: list[dict[str, Any]],
    selectors: list[dict[str, Any]],
) -> dict[str, Any]:
    manifest_by_map = manifest_index(manifest)
    selectors_by_key = selector_index(selectors)
    groups = []
    class_counts: Counter[str] = Counter()
    context_counts: Counter[str] = Counter()
    selector_with_manifest_count = 0
    selector_without_manifest_count = 0

    for group in sequence_review.get("groups") or []:
        context_kind = group.get("contextKind") or "unknown"
        context_counts[context_kind] += 1
        selector = group.get("selector") or ""
        selector_row = selectors_by_key.get(selector) if selector else None
        field_maps = unique(group.get("fieldMaps") or (selector_row or {}).get("fieldMaps") or [])
        resources = unique(group.get("resources") or (selector_row or {}).get("linkedCns") or [])
        records = manifest_records_for_maps(field_maps, manifest_by_map)
        if context_kind == "scene-proximity" and not records and group.get("map") and group.get("map") != "unbound":
            records = manifest_records_for_maps([group["map"]], manifest_by_map)
        link_class = group_link_class(group, records)
        class_counts[link_class] += 1
        if context_kind == "selector-root" and records:
            selector_with_manifest_count += 1
        elif context_kind == "selector-root":
            selector_without_manifest_count += 1
        seq_rows = sequence_rows(group)
        groups.append(
            {
                "id": group.get("id", ""),
                "contextKind": context_kind,
                "contextLabel": group.get("contextLabel") or group.get("map") or "unbound",
                "selector": selector,
                "rootVaHex": group.get("rootVaHex") or "",
                "rootEndVaHex": group.get("rootEndVaHex") or "",
                "map": group.get("map") or "unbound",
                "sceneIdHex": group.get("sceneIdHex") or "",
                "recordVaHex": group.get("recordVaHex") or "",
                "fieldMaps": field_maps,
                "resources": resources,
                "resourceRecords": records,
                "linkClass": link_class,
                "evidenceStatus": group.get("evidenceStatus") or "",
                "sequenceCount": len(seq_rows),
                "entryCount": group.get("entryCount", 0),
                "promptCount": group.get("promptCount", 0),
                "choiceCount": group.get("choiceCount", 0),
                "sequences": seq_rows,
                "directSameEventRootProofFound": False,
                "directEventRootProofFound": False,
                "notes": [
                    "Text and resources share a selector/root range or nearby scene record candidate.",
                    "This is not proof that one event root executes both the resource load and every prompt sequence.",
                ],
            }
        )

    summary = {
        "groupCount": len(groups),
        "sequenceCount": sum(row["sequenceCount"] for row in groups),
        "promptCount": sum(row["promptCount"] for row in groups),
        "choiceCount": sum(row["choiceCount"] for row in groups),
        "selectorRootRangeCandidateGroupCount": context_counts.get("selector-root", 0),
        "nearestSceneProximityCandidateGroupCount": context_counts.get("scene-proximity", 0),
        "selectorRootWithManifestRecordCount": selector_with_manifest_count,
        "selectorRootResourceOnlyCount": selector_without_manifest_count,
        "manifestRecordLinkedGroupCount": sum(1 for row in groups if row["resourceRecords"]),
        "manifestRecordLinkCount": sum(len(row["resourceRecords"]) for row in groups),
        "directSameEventRootProofFound": False,
        "directEventRootProofFound": False,
        "promotionStatus": PROMOTION_STATUS,
        "contextCounts": dict(context_counts),
        "linkClassCounts": dict(class_counts),
    }
    return {
        "kind": "hwanse-scene-seq-resource-record-link-review",
        "promotionStatus": PROMOTION_STATUS,
        "summary": summary,
        "groups": groups,
        "notes": [
            "selector-root groups are useful scene-unit candidates because text entries and linked CNS resources live under the same selected root range.",
            "nearest-scene groups are weaker proximity candidates around scene/resource records.",
            "direct event root proof remains blocked until the producer/consumer path from scene/event record to selected text root is identified.",
        ],
    }


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


def tag(value: str, cls: str = "") -> str:
    return f'<span class="tag {cls}">{esc(value)}</span>'


def code_list(values: list[str], limit: int = 12) -> str:
    if not values:
        return '<span class="muted">-</span>'
    bits = [f"<code>{esc(value)}</code>" for value in values[:limit]]
    if len(values) > limit:
        bits.append(f'<span class="muted">+{len(values) - limit}</span>')
    return " ".join(bits)


def record_list(records: list[dict[str, Any]], limit: int = 10) -> str:
    if not records:
        return '<span class="muted">-</span>'
    bits = []
    for row in records[:limit]:
        bits.append(
            f"<code>{esc(row['map'])}</code> "
            f"<span class=\"muted\">{esc(row.get('sceneIdHex') or '')} {esc(row.get('recordVaHex') or '')}</span>"
        )
    if len(records) > limit:
        bits.append(f'<span class="muted">+{len(records) - limit}</span>')
    return "<br>".join(bits)


def render_html(payload: dict[str, Any]) -> str:
    summary = payload["summary"]
    group_rows = []
    sequence_rows_html = []
    for row in payload["groups"]:
        cls = "good" if row["linkClass"] == "selector-root-resource-text-range-candidate" else "warn"
        if row["linkClass"] == "selector-root-resource-only-text-range-candidate":
            cls = "muted"
        group_rows.append(
            "<tr>"
            f"<td><code>{esc(row['id'])}</code><br><span class=\"muted\">{esc(row['contextLabel'])}</span></td>"
            f"<td>{tag(row['linkClass'], cls)}<br>{tag(row['evidenceStatus'], 'muted')}</td>"
            f"<td><code>{esc(row.get('rootVaHex') or row.get('recordVaHex') or '-')}</code><br>"
            f"<span class=\"muted\">{esc(row.get('rootEndVaHex') or row.get('sceneIdHex') or '')}</span></td>"
            f"<td>{code_list(row['fieldMaps'], 8)}</td>"
            f"<td>{code_list(row['resources'], 8)}</td>"
            f"<td>{record_list(row['resourceRecords'], 8)}</td>"
            f"<td>{esc(row['sequenceCount'])}</td>"
            f"<td>{esc(row['promptCount'])}</td>"
            f"<td>{tag('direct event-root false', 'bad')}</td>"
            "</tr>"
        )
        for seq in row.get("sequences") or []:
            sequence_rows_html.append(
                "<tr>"
                f"<td><code>{esc(seq['id'])}</code><br><span class=\"muted\">{esc(row['id'])}</span></td>"
                f"<td>{tag(row['linkClass'], cls)}</td>"
                f"<td><code>{esc(seq['entryStartVaHex'])}</code>..<code>{esc(seq['entryEndVaHex'])}</code><br>"
                f"<code>{esc(seq['textStartVaHex'])}</code>..<code>{esc(seq['textEndVaHex'])}</code></td>"
                f"<td>{esc(seq['entryCount'])}</td>"
                f"<td>{esc(seq['promptCount'])}</td>"
                f"<td>{esc(seq['choiceCount'])}</td>"
                f"<td>{esc(seq['sample'])}</td>"
                "</tr>"
            )

    summary_json = json.dumps(
        {
            "promotionStatus": payload["promotionStatus"],
            "groupCount": summary["groupCount"],
            "sequenceCount": summary["sequenceCount"],
            "selectorRootRangeCandidateGroupCount": summary["selectorRootRangeCandidateGroupCount"],
            "nearestSceneProximityCandidateGroupCount": summary["nearestSceneProximityCandidateGroupCount"],
            "directSameEventRootProofFound": False,
            "directEventRootProofFound": False,
        },
        ensure_ascii=False,
        indent=2,
    )
    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-seq / resource record 연결 검토</title>
  <style>
    :root {{ --bg:#f6f7f9; --panel:#fff; --head:#eef2f6; --border:#d8dee6; --ink:#17202a; --muted:#647384; }}
    * {{ 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:1600px; 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 {{ background:var(--panel); border:1px solid var(--border); border-radius:8px; margin:14px 0; overflow:hidden; }}
    .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(180px,1fr)); gap:10px; }}
    .metric {{ border:1px solid var(--border); border-radius:6px; background:#f8fafc; padding:10px; }}
    .metric strong {{ display:block; font-size:22px; }}
    .table-wrap {{ overflow:auto; }}
    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; position:sticky; top:0; z-index:1; }}
    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; margin:1px; }}
    .tag.good {{ color:#0f6a38; background:#e7f6ec; }}
    .tag.warn {{ color:#a15c00; background:#fff4df; }}
    .tag.bad {{ color:#b42318; background:#fdebea; }}
    .tag.muted {{ color:#607080; background:#edf2f7; }}
    .muted {{ color:var(--muted); }}
    @media (max-width:760px) {{ header {{ display:block; }} nav {{ justify-content:flex-start; margin-top:10px; }} }}
  </style>
</head>
<body>
<main data-page="scene-seq-resource-record-link-review">
  <header>
    <div>
      <h1>scene-seq / resource record 연결 검토</h1>
      <p class="muted">대사 흐름 후보와 resource 묶음이 같은 selector/root 후보 안에 들어오는지 분리한다. 직접 실행 root proof는 아직 false다.</p>
    </div>
    <nav aria-label="review links">
      <a href="index.html">관리 홈</a>
      <a href="selector_root_structure_review.html">selector-root 구조</a>
      <a href="resource_loader_consumer_trace_review.html">resource loader</a>
      <a href="scene_event_vm_review.html">Scene/Event VM</a>
      <a href="scene_event_text_consumer_trace_review.html">text consumer</a>
      <a href="scene_event_vm_prompt_sequence_review.html">prompt sequence</a>
      <a href="../out/scene_text_sequence_review.html">sequence evidence</a>
      <a href="../out/scene_seq_resource_record_link_review.json">JSON</a>
    </nav>
  </header>

  <section>
    <div class="head"><h2>요약</h2><span class="muted">{esc(payload['promotionStatus'])}</span></div>
    <div class="body metrics">
      <div class="metric"><strong>{summary['groupCount']}</strong><span>scene text groups</span></div>
      <div class="metric"><strong>{summary['sequenceCount']}</strong><span>scene-seq flows</span></div>
      <div class="metric"><strong>{summary['selectorRootRangeCandidateGroupCount']}</strong><span>selector-root candidates</span></div>
      <div class="metric"><strong>{summary['nearestSceneProximityCandidateGroupCount']}</strong><span>nearest scene candidates</span></div>
      <div class="metric"><strong>{summary['manifestRecordLinkedGroupCount']}</strong><span>manifest-linked groups</span></div>
      <div class="metric"><strong>false</strong><span>direct same event-root proof</span></div>
    </div>
  </section>

  <section>
    <div class="head"><h2>그룹별 연결 상태</h2><span>selector/root · resources · scene manifest</span></div>
    <div class="table-wrap">
      <table>
        <thead><tr><th>group</th><th>link class</th><th>root / record</th><th>field maps</th><th>resources</th><th>manifest records</th><th>seq</th><th>prompts</th><th>proof</th></tr></thead>
        <tbody>{''.join(group_rows)}</tbody>
      </table>
    </div>
  </section>

  <section>
    <div class="head"><h2>scene-seq 세부</h2><span>대사 흐름 후보별 entry/text range</span></div>
    <div class="table-wrap">
      <table>
        <thead><tr><th>sequence</th><th>link class</th><th>entry/text range</th><th>entries</th><th>prompts</th><th>choices</th><th>sample</th></tr></thead>
        <tbody>{''.join(sequence_rows_html)}</tbody>
      </table>
    </div>
  </section>
</main>
<script>
window.HWANSE_SCENE_SEQ_RESOURCE_RECORD_LINK_REVIEW_READY = {{
  sceneSeqResourceRecordLinkImplemented: true,
  promotionStatus: "{PROMOTION_STATUS}",
  groupCount: {summary['groupCount']},
  sequenceCount: {summary['sequenceCount']},
  selectorRootRangeCandidateGroupCount: {summary['selectorRootRangeCandidateGroupCount']},
  nearestSceneProximityCandidateGroupCount: {summary['nearestSceneProximityCandidateGroupCount']},
  manifestRecordLinkedGroupCount: {summary['manifestRecordLinkedGroupCount']},
  directSameEventRootProofFound: false,
  directEventRootProofFound: false,
  summary: {summary_json}
}};
</script>
</body>
</html>
"""


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--sequence-review", type=Path, default=OUT / "scene_text_sequence_review.json")
    parser.add_argument("--scene-manifest", type=Path, default=OUT / "scene_manifest.json")
    parser.add_argument("--selectors", type=Path, default=OUT / "save_scene_selectors.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--web-dir", type=Path, default=WEB)
    args = parser.parse_args()

    sequence_review = load_json(args.sequence_review, {})
    manifest = load_json(args.scene_manifest, [])
    selectors = load_json(args.selectors, [])
    payload = analyze(sequence_review, manifest, selectors)

    args.out_dir.mkdir(parents=True, exist_ok=True)
    args.web_dir.mkdir(parents=True, exist_ok=True)
    json_path = args.out_dir / "scene_seq_resource_record_link_review.json"
    html_path = args.out_dir / "scene_seq_resource_record_link_review.html"
    web_path = args.web_dir / "scene_seq_resource_record_link_review.html"
    html_text = render_html(payload)
    json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    html_path.write_text(html_text, encoding="utf-8")
    web_path.write_text(html_text, encoding="utf-8")
    print(f"wrote {json_path}")
    print(f"wrote {html_path}")
    print(f"wrote {web_path}")
    return 0


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