#!/usr/bin/env python3
"""Split transition event-record evidence into target, coordinate, spawn, and condition layers."""
from __future__ import annotations

import argparse
import html
import json
import sys
from collections import Counter, defaultdict
from pathlib import Path
from urllib.parse import urlencode

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"
DATA = ROOT / "data"

FOOTPRINT_RULE = "bottom-3x1 actor footprint; a single point-table tile is not enough to prove a trigger"


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


def review_key(source: str, x: int, y: int, target: str, record: str | int | None) -> str:
    return f"{source}@{record if record is not None else 'record'}:{x},{y}->{target}"


def cns_name(stem: str) -> str:
    return stem if stem.endswith(".cns") else f"{stem}.cns"


def map_tileset_cns(maps: dict, map_name: str) -> list[str]:
    info = maps.get(map_name) or {}
    tilesets = info.get("layerTilesets") or info.get("sceneTilesets") or []
    return [cns_name(item) for item in tilesets]


def linked_strings(row: dict) -> list[str]:
    values: list[str] = []
    for choice in row.get("conditionChoices") or []:
        for value in choice.get("linkedStrings") or []:
            if isinstance(value, str) and value not in values:
                values.append(value)
    return values


def reviews_for_record(row: dict, reviews: dict[str, dict]) -> list[dict]:
    record = row.get("recordVaHex") or row.get("recordVa")
    rows = []
    for target in row.get("targets") or []:
        for point in row.get("points") or []:
            x = point.get("x")
            y = point.get("y")
            if not isinstance(x, int) or not isinstance(y, int):
                continue
            key = review_key(row["map"], x, y, target, record)
            review = reviews.get(key)
            if review:
                rows.append({**review, "key": key})
    return rows


def target_link_detail(row: dict, maps: dict, target: str, links: list[str]) -> dict:
    target_cns = cns_name(target)
    tilesets = map_tileset_cns(maps, target)
    tileset_hits = [name for name in tilesets if name in links]
    direct_hit = target_cns in links
    if direct_hit and tilesets and len(tileset_hits) == len(tilesets):
        state = "target-linked"
        reason = "target field-map CNS and all target base tilesets are linked in the event payload"
    elif direct_hit or tileset_hits:
        state = "target-linked-partial"
        reason = "target field-map CNS or target base tileset references are linked in the event payload"
    else:
        state = "target-listed-unverified"
        reason = "target appears in extracted transition list, but no direct target resource link was found in linked strings"
    return {
        "target": target,
        "state": state,
        "targetCns": target_cns,
        "targetCnsLinked": direct_hit,
        "targetTilesets": tilesets,
        "targetTilesetLinks": tileset_hits,
        "reason": reason,
    }


def classify_coordinate_state(row: dict, record_reviews: list[dict]) -> tuple[str, list[str]]:
    confirmed = [review for review in record_reviews if review.get("state") == "confirmed"]
    rejected = [review for review in record_reviews if review.get("state") == "rejected"]
    point_count = len(row.get("points") or [])
    active_count = len(row.get("activePoints") or [])
    reasons = [
        f"raw point table contains {point_count} in-bounds point(s)",
        f"activePoints contains {active_count} point(s)",
        FOOTPRINT_RULE,
    ]
    if confirmed:
        reasons.append(
            f"{len(confirmed)} review row(s) are marked confirmed, but review rows are manual/browser ledger data, not decoded trigger-consumer proof"
        )
    if rejected:
        reasons.append(f"{len(rejected)} same-record point(s) are marked rejected, so the raw point table cannot be read as all triggers")
    if not confirmed and point_count:
        return "point-table-unverified", reasons
    return "coordinate-unverified", reasons


def classify_condition_state(row: dict) -> tuple[str, list[str]]:
    choices = row.get("conditionChoices") or []
    if not choices:
        return "condition-unverified", ["no decoded condition command is attached to this transition row"]
    payloads = []
    for choice in choices:
        payloads.append(
            {
                "conditionVaHex": choice.get("conditionVaHex"),
                "payloadVaHex": choice.get("payloadVaHex"),
                "targets": choice.get("targets") or [],
                "linkedStringCount": len(choice.get("linkedStrings") or []),
            }
        )
    return "condition-payload-present-undecoded", [
        "condition/payload addresses exist, but flag checks and branch outcomes are not decoded",
        f"{len(payloads)} condition choice payload(s) were observed",
    ]


def classify_spawn_state(row: dict, record_reviews: list[dict]) -> tuple[str, list[str]]:
    spawned = [review for review in record_reviews if "spawnX" in review and "spawnY" in review]
    reasons = []
    if spawned:
        points = sorted({(review["spawnX"], review["spawnY"]) for review in spawned})
        reasons.append(f"manual review rows carry spawn tile(s): {', '.join(f'{x},{y}' for x, y in points)}")
        reasons.append("spawn tile is review metadata; no decoded EXE spawn writer has been linked to it")
        return "manual-spawn-review", reasons
    if row.get("spawnPolicy"):
        reasons.append(f"event transition exporter used spawnPolicy={row.get('spawnPolicy')}")
        reasons.append(f"spawnConfidence={row.get('spawnConfidence') or 'unknown'}")
        return "heuristic-spawn-policy", reasons
    return "spawn-unverified", ["no target spawn coordinate is decoded for this event record"]


def classify_record(row: dict, reviews: dict[str, dict], maps: dict) -> dict:
    links = linked_strings(row)
    record_reviews = reviews_for_record(row, reviews)
    review_counts = Counter(review.get("state", "unknown") for review in record_reviews)
    target_details = [target_link_detail(row, maps, target, links) for target in row.get("targets") or []]
    target_link_state = (
        "target-linked"
        if any(detail["state"] == "target-linked" for detail in target_details)
        else "target-linked-partial"
        if any(detail["state"] == "target-linked-partial" for detail in target_details)
        else "target-listed-unverified"
    )
    coordinate_state, coordinate_reasons = classify_coordinate_state(row, record_reviews)
    condition_state, condition_reasons = classify_condition_state(row)
    spawn_state, spawn_reasons = classify_spawn_state(row, record_reviews)
    promotion = (
        "target-linked-coordinate-condition-unverified"
        if target_link_state.startswith("target-linked")
        else "candidate-unverified"
    )
    coordinate_unverified = "unverified" in coordinate_state
    condition_unverified = "unverified" in condition_state or "undecoded" in condition_state
    if coordinate_unverified or condition_unverified:
        route_use = "do-not-promote-as-original-route"
    else:
        route_use = "review-before-route-use"

    point_rows = [
        {
            "x": point.get("x"),
            "y": point.get("y"),
            "active": point in (row.get("activePoints") or []),
        }
        for point in row.get("points") or []
    ]
    review_rows = []
    for review in record_reviews:
        review_rows.append(
            {
                "key": review["key"],
                "x": review.get("x"),
                "y": review.get("y"),
                "target": review.get("target"),
                "state": review.get("state"),
                "spawnX": review.get("spawnX"),
                "spawnY": review.get("spawnY"),
                "evidenceState": promotion if review.get("state") == "confirmed" else "review-rejected",
                "coordinateState": coordinate_state if review.get("state") == "confirmed" else "rejected-coordinate-candidate",
                "conditionState": condition_state if review.get("state") == "confirmed" else "not-applicable",
                "targetLinkState": target_link_state,
            }
        )

    return {
        "source": row.get("map"),
        "sceneId": row.get("sceneId"),
        "sceneIdHex": row.get("sceneIdHex"),
        "eventKind": row.get("eventKind"),
        "recordVa": row.get("recordVa"),
        "recordVaHex": row.get("recordVaHex"),
        "dispatchRefCount": row.get("dispatchRefCount"),
        "targets": row.get("targets") or [],
        "resourceRefs": links,
        "targetDetails": target_details,
        "sourceMapState": "record-owner",
        "targetLinkState": target_link_state,
        "coordinateState": coordinate_state,
        "conditionState": condition_state,
        "spawnState": spawn_state,
        "promotion": promotion,
        "routeUse": route_use,
        "pointTableState": "raw-point-table-undecoded" if row.get("points") else "no-point-table",
        "activePointState": "active-point-table-undecoded" if row.get("activePoints") else "no-active-point",
        "footprintRule": FOOTPRINT_RULE,
        "points": point_rows,
        "activePoints": row.get("activePoints") or [],
        "rawPointCount": row.get("rawPointCount"),
        "inBoundsPointCount": row.get("inBoundsPointCount"),
        "pointCountHint": row.get("pointCountHint"),
        "spawnPolicy": row.get("spawnPolicy"),
        "spawnConfidence": row.get("spawnConfidence"),
        "reviewCounts": dict(sorted(review_counts.items())),
        "reviewRows": sorted(review_rows, key=lambda item: (item.get("target") or "", item.get("y") or 0, item.get("x") or 0)),
        "coordinateReasons": coordinate_reasons,
        "conditionReasons": condition_reasons,
        "spawnReasons": spawn_reasons,
        "conditionChoices": [
            {
                "conditionVa": choice.get("conditionVa"),
                "conditionVaHex": choice.get("conditionVaHex"),
                "payloadVa": choice.get("payloadVa"),
                "payloadVaHex": choice.get("payloadVaHex"),
                "targets": choice.get("targets") or [],
                "targetSceneIds": choice.get("targetSceneIds") or {},
                "linkedStrings": choice.get("linkedStrings") or [],
            }
            for choice in row.get("conditionChoices") or []
        ],
    }


def build_review_row_index(records: list[dict]) -> dict[str, dict]:
    index = {}
    for record in records:
        for review in record.get("reviewRows") or []:
            index[review["key"]] = {
                **review,
                "source": record["source"],
                "recordVaHex": record.get("recordVaHex"),
                "sceneIdHex": record.get("sceneIdHex"),
                "eventKind": record.get("eventKind"),
                "footprintRule": record.get("footprintRule"),
            }
    return dict(sorted(index.items()))


def summarize(records: list[dict], review_rows: dict[str, dict]) -> dict:
    return {
        "recordCount": len(records),
        "reviewRowCount": len(review_rows),
        "reviewConfirmedRows": sum(1 for row in review_rows.values() if row.get("state") == "confirmed"),
        "targetLinkedRecords": sum(1 for row in records if row.get("targetLinkState") == "target-linked"),
        "coordinateUnverifiedRecords": sum(1 for row in records if "unverified" in row.get("coordinateState", "")),
        "conditionUnverifiedRecords": sum(1 for row in records if "unverified" in row.get("conditionState", "") or "undecoded" in row.get("conditionState", "")),
        "manualSpawnReviewRecords": sum(1 for row in records if row.get("spawnState") == "manual-spawn-review"),
        "routePromotableRecords": sum(1 for row in records if row.get("routeUse") != "do-not-promote-as-original-route"),
    }


def chip(text: str, kind: str = "") -> str:
    return f'<span class="chip {html.escape(kind)}">{html.escape(text)}</span>'


def html_record_row(record: dict) -> str:
    details = record.get("targetDetails") or []
    target_bits = []
    for detail in details:
        target_bits.append(
            f"{html.escape(detail['target'])}<br><small>{html.escape(detail['reason'])}</small>"
        )
    review_counts = record.get("reviewCounts") or {}
    review_text = ", ".join(f"{key}={value}" for key, value in review_counts.items()) or "-"
    reasons = "<br>".join(html.escape(reason) for reason in (record.get("coordinateReasons") or [])[:4])
    resources = ", ".join(record.get("resourceRefs") or [])
    params = urlencode({
        "map": record.get("source") or "",
        "events": "1",
        "transitionRecord": record.get("recordVaHex") or "",
        "overview": "1",
    })
    return "\n".join(
        [
            f'<tr data-record-row data-search="{html.escape(json.dumps(record, ensure_ascii=False))}">',
            f"<td><code>{html.escape(record.get('recordVaHex') or '-')}</code><br><small>{html.escape(record.get('sceneIdHex') or '-')} · kind {html.escape(str(record.get('eventKind') or '-'))}</small></td>",
            f"<td>{html.escape(record.get('source') or '-')}</td>",
            f"<td>{'<hr>'.join(target_bits) if target_bits else '-'}</td>",
            f"<td>{chip(record.get('targetLinkState') or '-', 'good' if record.get('targetLinkState') == 'target-linked' else 'warn')}</td>",
            f"<td>{chip(record.get('coordinateState') or '-', 'bad' if 'unverified' in (record.get('coordinateState') or '') else 'warn')}<br><small>{reasons}</small></td>",
            f"<td>{chip(record.get('conditionState') or '-', 'warn')}</td>",
            f"<td>{chip(record.get('spawnState') or '-', 'warn')}</td>",
            f"<td>{html.escape(review_text)}<br><small>{html.escape(record.get('routeUse') or '')}</small></td>",
            f"<td><small>{html.escape(resources)}</small></td>",
            f'<td><a href="../web/game.html?{html.escape(params)}">open</a></td>',
            "</tr>",
        ]
    )


def write_html(path: Path, records: list[dict], summary: dict) -> None:
    body = "\n".join(html_record_row(record) for record in records)
    path.write_text(
        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>환세취호전 event record structure review</title>
  <style>
    :root {{ color-scheme: dark; --bg:#0b0d0f; --panel:#15191d; --line:#313940; --text:#eff3f5; --muted:#aeb8bf; --green:#72d19b; --yellow:#f3ce62; --red:#f1797d; --blue:#6cb8ff; font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }}
    * {{ box-sizing:border-box; }}
    body {{ margin:0; background:var(--bg); color:var(--text); }}
    a {{ color:var(--blue); text-decoration:none; }}
    header {{ position:sticky; top:0; z-index:4; display:grid; gap:10px; padding:12px 16px; border-bottom:1px solid var(--line); background:rgba(11,13,15,.96); }}
    .top {{ display:flex; align-items:center; gap:12px; }}
    h1 {{ margin:0; font-size:18px; }}
    nav {{ margin-left:auto; display:flex; gap:8px; flex-wrap:wrap; }}
    nav a, .chip {{ display:inline-flex; align-items:center; min-height:28px; padding:0 9px; border:1px solid var(--line); border-radius:4px; background:#101316; color:var(--text); font-size:12px; }}
    input {{ width:100%; height:36px; padding:0 10px; border:1px solid var(--line); border-radius:4px; background:#101316; color:var(--text); }}
    main {{ padding:16px; display:grid; gap:14px; }}
    .metrics {{ display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:10px; }}
    .metric, section {{ border:1px solid var(--line); border-radius:6px; background:var(--panel); }}
    .metric {{ padding:10px; }}
    .metric span, small, .muted {{ color:var(--muted); }}
    .metric b {{ display:block; margin-top:4px; font:20px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }}
    section {{ overflow:auto; }}
    .note {{ padding:12px; color:var(--muted); line-height:1.45; }}
    table {{ width:100%; min-width:1280px; border-collapse:collapse; }}
    th,td {{ padding:8px 10px; border-bottom:1px solid var(--line); vertical-align:top; font-size:13px; line-height:1.35; }}
    th {{ position:sticky; top:98px; background:#101316; text-align:left; color:var(--muted); }}
    code {{ color:#d5e8ff; }}
    .chip.good {{ border-color:#2e6f4b; color:var(--green); }}
    .chip.warn {{ border-color:#856a1f; color:var(--yellow); }}
    .chip.bad {{ border-color:#7a373a; color:var(--red); }}
    tr[hidden] {{ display:none; }}
    @media (max-width:920px) {{ .top,.metrics {{ display:block; }} nav {{ margin:10px 0 0; }} }}
  </style>
</head>
<body>
  <header>
    <div class="top">
      <h1>Event Record Structure Review</h1>
      <nav>
        <a href="../web/index.html">관리 홈</a>
        <a href="../web/transition_review.html">전환 검토</a>
        <a href="transition_reviews.json">transition_reviews.json</a>
        <a href="event_transitions.json">event_transitions.json</a>
      </nav>
    </div>
    <input id="search" type="search" placeholder="검색: 0x00503350, map1_02b, target-linked, coordinate-unverified">
  </header>
  <main>
    <div class="metrics">
      <div class="metric"><span>records</span><b>{summary['recordCount']}</b></div>
      <div class="metric"><span>target linked</span><b>{summary['targetLinkedRecords']}</b></div>
      <div class="metric"><span>coord unverified</span><b>{summary['coordinateUnverifiedRecords']}</b></div>
      <div class="metric"><span>condition undecoded</span><b>{summary['conditionUnverifiedRecords']}</b></div>
      <div class="metric"><span>route promotable</span><b>{summary['routePromotableRecords']}</b></div>
    </div>
    <section>
      <p class="note">`state=confirmed` 리뷰는 수동/브라우저 ledger 이며, 원본 EXE의 발판 트리거/조건 분기 proof 로 자동 승격하지 않는다. 이 화면은 source map, target resource, point table, spawn metadata, condition payload 를 별도 증거층으로 분리한다.</p>
    </section>
    <section>
      <table>
        <thead>
          <tr><th>record</th><th>source</th><th>target resource link</th><th>target</th><th>coordinate</th><th>condition</th><th>spawn</th><th>reviews</th><th>resources</th><th>open</th></tr>
        </thead>
        <tbody id="rows">
{body}
        </tbody>
      </table>
    </section>
  </main>
  <script>
    const q = document.getElementById('search');
    const rows = Array.from(document.querySelectorAll('[data-record-row]'));
    q.addEventListener('input', () => {{
      const needle = q.value.trim().toLowerCase();
      for (const row of rows) row.hidden = needle && !row.dataset.search.toLowerCase().includes(needle);
    }});
    window.HWANSE_LAST_EVENT_RECORD_STRUCTURE_REVIEW = {{
      eventRecordStructureReviewImplemented: true,
      recordCount: {summary['recordCount']},
      targetLinkedRecords: {summary['targetLinkedRecords']},
      coordinateUnverifiedRecords: {summary['coordinateUnverifiedRecords']},
      conditionUnverifiedRecords: {summary['conditionUnverifiedRecords']},
      routePromotableRecords: {summary['routePromotableRecords']}
    }};
  </script>
</body>
</html>
""",
        encoding="utf-8",
    )


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--transitions", type=Path, default=OUT / "event_transitions.json")
    parser.add_argument("--reviews", type=Path, default=DATA / "transition_reviews.json")
    parser.add_argument("--maps", type=Path, default=OUT / "maps.js")
    parser.add_argument("--out", type=Path, default=OUT)
    args = parser.parse_args()

    transitions = load_json(args.transitions, [])
    reviews = load_json(args.reviews, {})
    maps = load_maps(args.maps)
    records = [classify_record(row, reviews, maps) for row in transitions]
    records.sort(key=lambda row: (row.get("source") or "", row.get("recordVaHex") or ""))
    review_rows = build_review_row_index(records)
    summary = summarize(records, review_rows)
    result = {
        "sourceFiles": {
            "eventTransitions": str(args.transitions),
            "transitionReviews": str(args.reviews),
            "maps": str(args.maps),
        },
        "classificationLegend": {
            "target-linked": "target map CNS and/or target tilesets are linked by the event payload",
            "coordinate-unverified": "point table/review row exists, but no decoded trigger consumer proves actor-footprint coordinates",
            "condition-payload-present-undecoded": "condition payload addresses exist, but flag checks/branch outcomes are not decoded",
            "manual-spawn-review": "spawn coordinate came from review metadata, not a decoded EXE spawn writer",
        },
        "summary": summary,
        "records": records,
        "reviewRows": review_rows,
    }
    args.out.mkdir(parents=True, exist_ok=True)
    (args.out / "event_record_structure_review.json").write_text(
        json.dumps(result, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    write_html(args.out / "event_record_structure_review.html", records, summary)
    print(
        "wrote event record structure review: "
        f"{summary['recordCount']} records, "
        f"{summary['targetLinkedRecords']} target-linked, "
        f"{summary['coordinateUnverifiedRecords']} coordinate-unverified"
    )


if __name__ == "__main__":
    main()
