#!/usr/bin/env python3
"""Review conditional-looking map route payloads without promoting them.

This pass looks at the already extracted event transition payloads and the raw
scene event dispatch references.  A source record can contain several condition
choices that each point at a different field map payload.  That is a strong
static fanout pattern, but not yet proof of the runtime condition or source
hotspot.  The output therefore keeps route/flag proof blocked.
"""
from __future__ import annotations

import argparse
import html
import json
import re
import sys
from collections import Counter
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

from summarize_map_tiles import load_maps  # noqa: E402


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

FIELD_MAP_RE = re.compile(r"map\d+_\d+[a-z]\.cns$")
TILESET_RE = re.compile(r"map_[a-z][123]\.cns$")
PROMOTION_STATUS = "conditional-map-route-pattern-static-only"


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


def load_json(path: Path, default):
    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()
    result: list[str] = []
    for value in values:
        if value in seen:
            continue
        seen.add(value)
        result.append(value)
    return result


def event_index(events: list[dict]) -> dict[int, dict]:
    return {row["recordVa"]: row for row in events if isinstance(row.get("recordVa"), int)}


def classify_scalar(item: dict) -> str:
    value = item.get("value")
    lo = item.get("lo")
    hi = item.get("hi")
    if value == 0x3F:
        return "sentinel-0x3f"
    if value in (0, 1):
        return "structural-small"
    if value in (0x15D, 0x15E):
        return "small-scalar-candidate-0x15d-family"
    if isinstance(lo, int) and lo in (0x3026, 0x3012, 0xC012, 0x0132):
        return "packed-shape-scalar"
    if isinstance(hi, int) and hi != 0:
        return "packed-or-coordinate-scalar"
    return "unclassified-scalar-candidate"


def scalar_candidates(ref: dict) -> list[dict]:
    rows = []
    for item in ref.get("conditionFirstDwords", []):
        if item.get("pointer") or item.get("string") is not None:
            continue
        rows.append(
            {
                "va": item.get("va"),
                "vaHex": hx(item.get("va")),
                "value": item.get("value"),
                "hex": item.get("hex") or hx(item.get("value")),
                "lo": item.get("lo"),
                "hi": item.get("hi"),
                "classification": classify_scalar(item),
                "flagProof": "unproven",
            }
        )
    return rows


def linked_strings(ref: dict) -> dict:
    strings = ref.get("conditionLinkedStrings", [])
    field_maps = unique([value[:-4] for value in strings if FIELD_MAP_RE.fullmatch(value)])
    tilesets = unique([value[:-4] for value in strings if TILESET_RE.fullmatch(value)])
    resources = unique(
        [
            value[:-4]
            for value in strings
            if value.endswith(".cns") and not FIELD_MAP_RE.fullmatch(value) and not TILESET_RE.fullmatch(value)
        ]
    )
    other = unique([value for value in strings if not value.endswith(".cns")])
    return {
        "fieldMaps": field_maps,
        "tilesets": tilesets,
        "resources": resources,
        "other": other,
        "all": strings,
    }


def payload_field_map_evidence(ref: dict, maps: dict) -> list[dict]:
    rows = []
    dwords = ref.get("conditionPayloadDwords", [])
    for index, item in enumerate(dwords):
        linked = item.get("string")
        if not isinstance(linked, str) or not FIELD_MAP_RE.fullmatch(linked):
            continue
        target = linked[:-4]
        next_item = dwords[index + 1] if index + 1 < len(dwords) else {}
        next_value = next_item.get("value")
        expected_scene_id = (maps.get(target) or {}).get("sceneId")
        if isinstance(expected_scene_id, int) and next_value == expected_scene_id:
            evidence = "next-dword-matches-map-scene-id"
        elif isinstance(next_value, int):
            evidence = "target-string-next-dword-not-map-scene-id"
        else:
            evidence = "target-string-without-next-dword-scene-id"
        rows.append(
            {
                "target": target,
                "stringVa": item.get("value"),
                "stringVaHex": hx(item.get("value")),
                "dwordVa": item.get("va"),
                "dwordVaHex": hx(item.get("va")),
                "nextDwordVa": next_item.get("va"),
                "nextDwordVaHex": hx(next_item.get("va")),
                "nextDwordValue": next_value,
                "nextDwordHex": hx(next_value, 4) if isinstance(next_value, int) else None,
                "expectedSceneId": expected_scene_id,
                "expectedSceneIdHex": hx(expected_scene_id, 4),
                "sceneIdEvidence": evidence,
            }
        )
    return rows


def refs_for_record(record: dict, event: dict) -> list[dict]:
    choices = record.get("conditionChoices", [])
    raw_refs = event.get("eventDispatchRefs", []) if event else []
    by_condition = {ref.get("conditionVa"): ref for ref in raw_refs}
    rows = []
    for choice in choices:
        raw = by_condition.get(choice.get("conditionVa")) or {}
        rows.append(
            {
                "choice": choice,
                "raw": raw,
            }
        )
    return rows


def analyze(transitions: list[dict], events: list[dict], maps: dict) -> dict:
    events_by_record = event_index(events)
    records = []
    scalar_counter: Counter[str] = Counter()
    scene_evidence_counter: Counter[str] = Counter()
    choice_count = 0
    field_payload_choice_count = 0

    for transition in transitions:
        event = events_by_record.get(transition.get("recordVa"), {})
        target_count = len(transition.get("targets") or [])
        choices = []
        for pair in refs_for_record(transition, event):
            choice = pair["choice"]
            raw = pair["raw"]
            linked = linked_strings(raw)
            payload_evidence = payload_field_map_evidence(raw, maps)
            scalars = scalar_candidates(raw)
            for scalar in scalars:
                scalar_counter[scalar["classification"]] += 1
            for row in payload_evidence:
                scene_evidence_counter[row["sceneIdEvidence"]] += 1
            if payload_evidence:
                field_payload_choice_count += 1
            choice_count += 1
            choices.append(
                {
                    "conditionVa": choice.get("conditionVa"),
                    "conditionVaHex": choice.get("conditionVaHex") or hx(choice.get("conditionVa")),
                    "payloadVa": choice.get("payloadVa"),
                    "payloadVaHex": choice.get("payloadVaHex") or hx(choice.get("payloadVa")),
                    "targets": choice.get("targets", []),
                    "targetSceneIds": choice.get("targetSceneIds", {}),
                    "linked": linked,
                    "payloadFieldMapEvidence": payload_evidence,
                    "conditionScalarCandidates": scalars,
                    "conditionConsumerProofFound": False,
                    "flagLikeValueProofFound": False,
                }
            )

        if target_count > 1 and len(choices) > 1:
            classification = "conditional-fanout-candidate"
        else:
            classification = "single-target-condition-wrapper"
        records.append(
            {
                "map": transition.get("map"),
                "sceneId": transition.get("sceneId"),
                "sceneIdHex": transition.get("sceneIdHex"),
                "recordVa": transition.get("recordVa"),
                "recordVaHex": transition.get("recordVaHex") or hx(transition.get("recordVa")),
                "eventKind": transition.get("eventKind"),
                "targets": transition.get("targets", []),
                "targetCount": target_count,
                "conditionChoiceCount": len(choices),
                "classification": classification,
                "routePromotionStatus": "blocked-static-pattern-only",
                "conditionConsumerProofFound": False,
                "flagLikeValueProofFound": False,
                "coordinateProofStatus": "source-trigger-unverified",
                "choices": choices,
                "activePoints": transition.get("activePoints", []),
                "pointCountHint": transition.get("pointCountHint"),
                "dispatchRefCount": transition.get("dispatchRefCount"),
            }
        )

    summary = {
        "recordCount": len(records),
        "conditionChoiceCount": choice_count,
        "multiTargetRecordCount": sum(1 for row in records if row["targetCount"] > 1),
        "conditionalFanoutRecordCount": sum(1 for row in records if row["classification"] == "conditional-fanout-candidate"),
        "singleTargetConditionWrapperCount": sum(1 for row in records if row["classification"] == "single-target-condition-wrapper"),
        "fieldMapPayloadChoiceCount": field_payload_choice_count,
        "sceneIdMatchEvidenceCount": scene_evidence_counter.get("next-dword-matches-map-scene-id", 0),
        "sceneIdEvidenceCounts": dict(scene_evidence_counter),
        "scalarCandidateCounts": dict(scalar_counter),
        "conditionConsumerProofFound": False,
        "flagLikeValueProofFound": False,
        "routeProofFound": False,
        "promotionStatus": PROMOTION_STATUS,
    }
    return {
        "kind": "hwanse-scene-event-conditional-map-route-review",
        "promotionStatus": PROMOTION_STATUS,
        "summary": summary,
        "records": records,
        "notes": [
            "Multiple target maps under one source record are static conditional fanout candidates.",
            "The scalar values near the condition payload are not promoted to scenario flags until a consumer proves their meaning.",
            "Coordinate tables remain source-trigger-unverified; this review only groups target/resource payload evidence.",
        ],
    }


def esc(value) -> 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 render_html(payload: dict) -> str:
    summary = payload["summary"]
    record_rows = []
    choice_rows = []
    for row in payload["records"]:
        cls = "warn" if row["classification"] == "conditional-fanout-candidate" else "muted"
        record_rows.append(
            "<tr>"
            f"<td><code>{esc(row['map'])}</code></td>"
            f"<td><code>{esc(row['recordVaHex'])}</code></td>"
            f"<td>{esc(row['eventKind'])}</td>"
            f"<td>{tag(row['classification'], cls)}</td>"
            f"<td>{'<br>'.join(f'<code>{esc(t)}</code>' for t in row['targets'])}</td>"
            f"<td>{esc(row['conditionChoiceCount'])}</td>"
            f"<td>{tag(row['routePromotionStatus'], 'bad')}</td>"
            "</tr>"
        )
        for choice in row["choices"]:
            payload_bits = []
            for ev in choice["payloadFieldMapEvidence"]:
                payload_bits.append(
                    f"<code>{esc(ev['target'])}</code> {esc(ev['sceneIdEvidence'])} "
                    f"<span class=\"muted\">{esc(ev.get('expectedSceneIdHex') or '-')}</span>"
                )
            scalar_bits = []
            for scalar in choice["conditionScalarCandidates"]:
                scalar_bits.append(
                    f"<code>{esc(scalar['hex'])}</code> {tag(scalar['classification'], 'muted')}"
                )
            choice_rows.append(
                "<tr>"
                f"<td><code>{esc(row['map'])}</code><br><span class=\"muted\">{esc(row['recordVaHex'])}</span></td>"
                f"<td><code>{esc(choice['conditionVaHex'])}</code><br><code>{esc(choice['payloadVaHex'])}</code></td>"
                f"<td>{'<br>'.join(f'<code>{esc(t)}</code>' for t in choice['targets'])}</td>"
                f"<td>{'<br>'.join(payload_bits) if payload_bits else '-'}</td>"
                f"<td>{'<br>'.join(f'<code>{esc(t)}</code>' for t in choice['linked']['tilesets']) or '-'}</td>"
                f"<td>{'<br>'.join(f'<code>{esc(r)}</code>' for r in choice['linked']['resources']) or '-'}</td>"
                f"<td>{'<br>'.join(scalar_bits) if scalar_bits else '-'}</td>"
                f"<td>{tag('condition consumer false', 'bad')} {tag('flag proof false', 'bad')}</td>"
                "</tr>"
            )

    data = json.dumps(
        {
            "promotionStatus": payload["promotionStatus"],
            "conditionalFanoutRecordCount": summary["conditionalFanoutRecordCount"],
            "conditionChoiceCount": summary["conditionChoiceCount"],
            "conditionConsumerProofFound": summary["conditionConsumerProofFound"],
            "flagLikeValueProofFound": summary["flagLikeValueProofFound"],
            "routeProofFound": summary["routeProofFound"],
        },
        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>조건부 맵 이동 패턴 검토</title>
  <style>
    :root {{ color-scheme: light; --bg:#f6f7f9; --panel:#fff; --head:#eef2f6; --border:#d8dee6; --ink:#17202a; --muted:#607080; }}
    * {{ 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 {{ 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 {{ 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; margin:1px; }}
    .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-event-conditional-map-route-review">
  <header>
    <div>
      <h1>조건부 맵 이동 패턴 검토</h1>
      <p class="muted">같은 source event record 주변에 target map, scene id, resource, scalar 후보가 fanout으로 묶이는지 본다. 실제 조건/트리거 proof는 아직 승격하지 않는다.</p>
    </div>
    <nav aria-label="review links">
      <a href="index.html">관리 홈</a>
      <a href="scene_event_vm_review.html">Scene/Event VM</a>
      <a href="scene_event_coordinate_semantics_review.html">좌표 의미</a>
      <a href="scene_event_vm_branch_flag_review.html">branch/flag</a>
      <a href="scene_event_global_flag_candidate_review.html">global flag 후보</a>
      <a href="../out/scene_event_conditional_map_route_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['recordCount']}</strong><span>transition records</span></div>
      <div class="metric"><strong>{summary['conditionChoiceCount']}</strong><span>condition choices</span></div>
      <div class="metric"><strong>{summary['conditionalFanoutRecordCount']}</strong><span>conditional fanout candidates</span></div>
      <div class="metric"><strong>{summary['singleTargetConditionWrapperCount']}</strong><span>single-target wrappers</span></div>
      <div class="metric"><strong>{summary['sceneIdMatchEvidenceCount']}</strong><span>target scene-id next-dword matches</span></div>
      <div class="metric"><strong>false</strong><span>condition consumer / flag proof</span></div>
    </div>
  </section>

  <section>
    <div class="head"><h2>레코드별 fanout</h2><span>source map 기준</span></div>
    <table>
      <thead><tr><th>source</th><th>record</th><th>kind</th><th>class</th><th>targets</th><th>choices</th><th>proof state</th></tr></thead>
      <tbody>{''.join(record_rows)}</tbody>
    </table>
  </section>

  <section>
    <div class="head"><h2>condition choice / payload detail</h2><span>flag-like scalar는 미승격</span></div>
    <table>
      <thead><tr><th>source</th><th>condition/payload</th><th>target</th><th>scene id evidence</th><th>tilesets</th><th>resources</th><th>scalar candidates</th><th>proof</th></tr></thead>
      <tbody>{''.join(choice_rows)}</tbody>
    </table>
  </section>
</main>
<script>
window.HWANSE_SCENE_EVENT_CONDITIONAL_MAP_ROUTE_REVIEW_READY = {{
  conditionalMapRouteReviewImplemented: true,
  promotionStatus: "{PROMOTION_STATUS}",
  conditionalFanoutRecordCount: {summary['conditionalFanoutRecordCount']},
  conditionChoiceCount: {summary['conditionChoiceCount']},
  conditionConsumerProofFound: false,
  flagLikeValueProofFound: false,
  routeProofFound: false,
  targetSceneIdNextDwordMatchCount: {summary['sceneIdMatchEvidenceCount']},
  summary: {data}
}};
</script>
</body>
</html>
"""


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--scene-events", type=Path, default=OUT / "scene_events.json")
    parser.add_argument("--event-transitions", type=Path, default=OUT / "event_transitions.json")
    parser.add_argument("--maps", type=Path, default=OUT / "maps.js")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--web-dir", type=Path, default=WEB)
    args = parser.parse_args()

    events = load_json(args.scene_events, [])
    transitions = load_json(args.event_transitions, [])
    maps = load_maps(args.maps)
    payload = analyze(transitions, events, maps)

    args.out_dir.mkdir(parents=True, exist_ok=True)
    args.web_dir.mkdir(parents=True, exist_ok=True)
    json_path = args.out_dir / "scene_event_conditional_map_route_review.json"
    html_path = args.out_dir / "scene_event_conditional_map_route_review.html"
    web_path = args.web_dir / "scene_event_conditional_map_route_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())
