#!/usr/bin/env python3
"""Extract field-map transition candidates from scene event records."""
from __future__ import annotations

import argparse
import json
import re
from pathlib import Path


FIELD_MAP_RE = re.compile(r"map\d+_\d+[a-z]\.cns$")


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


def field_map_links(record: dict) -> list[str]:
    links = [
        linked[:-4]
        for ref in record.get("eventDispatchRefs", [])
        for linked in ref.get("conditionLinkedStrings", [])
        if FIELD_MAP_RE.fullmatch(linked)
    ]
    return unique([link for link in links if link != record.get("map")])


def hex_scene_id(value: int | None) -> str | None:
    if not isinstance(value, int):
        return None
    return f"0x{value:04x}"


def condition_target_scene_ids(record: dict, ref: dict) -> dict[str, dict]:
    result = {}
    dwords = ref.get("conditionPayloadDwords", [])
    for index, item in enumerate(dwords[:-1]):
        linked = item.get("string")
        if not isinstance(linked, str) or not FIELD_MAP_RE.fullmatch(linked):
            continue
        target = linked[:-4]
        if target == record.get("map"):
            continue
        scene_id = dwords[index + 1].get("value")
        if not isinstance(scene_id, int):
            continue
        result[target] = {
            "sceneId": scene_id,
            "sceneIdHex": hex_scene_id(scene_id),
            "payloadVa": ref.get("conditionPayloadVa"),
            "payloadVaHex": (
                f"0x{ref['conditionPayloadVa']:08x}"
                if isinstance(ref.get("conditionPayloadVa"), int)
                else None
            ),
        }
    return result


def condition_choices(record: dict) -> list[dict]:
    choices = []
    for ref in record.get("eventDispatchRefs", []):
        targets = unique(
            [
                linked[:-4]
                for linked in ref.get("conditionLinkedStrings", [])
                if FIELD_MAP_RE.fullmatch(linked)
            ]
        )
        targets = [target for target in targets if target != record.get("map")]
        if not targets:
            continue
        target_scene_ids = condition_target_scene_ids(record, ref)
        choices.append(
            {
                "conditionVa": ref.get("conditionVa"),
                "conditionVaHex": f"0x{ref['conditionVa']:08x}" if ref.get("conditionVa") else None,
                "payloadVa": ref.get("conditionPayloadVa"),
                "payloadVaHex": (
                    f"0x{ref['conditionPayloadVa']:08x}"
                    if isinstance(ref.get("conditionPayloadVa"), int)
                    else None
                ),
                "targets": targets,
                "targetSceneIds": {
                    target: target_scene_ids[target]
                    for target in targets
                    if target in target_scene_ids
                },
                "linkedStrings": ref.get("conditionLinkedStrings", []),
            }
        )
    return choices


def transition_records(events: list[dict]) -> list[dict]:
    transitions = []
    for record in events:
        links = field_map_links(record)
        if not links:
            continue
        choices = condition_choices(record)
        points = record.get("inBoundsPoints") or record.get("points", [])
        active_points = record.get("activePoints") or []
        transitions.append(
            {
                "map": record["map"],
                "sceneId": record["sceneId"],
                "sceneIdHex": record["sceneIdHex"],
                "eventKind": record["eventKind"],
                "recordVa": record["recordVa"],
                "recordVaHex": f"0x{record['recordVa']:08x}",
                "targets": links,
                "conditionChoices": choices,
                "activePoints": active_points,
                "points": points,
                "spawnPolicy": "sameTileNearestStandable",
                "spawnConfidence": "heuristic",
                "rawPointCount": record.get("rawPointCount"),
                "inBoundsPointCount": record.get("inBoundsPointCount"),
                "pointCountHint": record.get("pointCountHint"),
                "dispatchRefCount": len(record.get("eventDispatchRefs", [])),
            }
        )
    return transitions


def markdown(transitions: list[dict], title: str = "# Event Transition Candidates", web_prefix: str = "../web") -> str:
    lines = [
        title,
        "",
        "Generated from `eventDispatchRefs[].conditionLinkedStrings` field-map links.",
        "",
        "These are candidate transitions only. Hotspot tiles use the in-bounds coordinate table from each scene record; `pointCountHint` is retained as metadata because the hinted first point can be a blocked marker rather than a standable trigger.",
        "",
        "| source map | scene | kind | record | targets | check link | spawn policy | active points | trigger points | raw points | dispatch refs |",
        "| --- | --- | ---: | --- | --- | --- | --- | --- | --- | ---: | ---: |",
    ]
    for item in transitions:
        active_points = ", ".join(f"{point['x']},{point['y']}" for point in item.get("activePoints", [])[:4])
        points = ", ".join(f"{point['x']},{point['y']}" for point in item["points"][:24])
        if len(item["points"]) > 24:
            points += f", ... +{len(item['points']) - 24}"
        first_point = item["points"][0] if item["points"] else None
        check_link = "-"
        if first_point:
            check_link = (
                f"[open]({web_prefix}/game.html?map={item['map']}"
                f"&startTile={first_point['x']},{first_point['y']}&events=1&overview=1)"
            )
        choices = []
        for choice in item.get("conditionChoices", [])[:6]:
            label = choice.get("conditionVaHex") or "-"
            target_labels = []
            for target in choice.get("targets", []):
                scene_info = choice.get("targetSceneIds", {}).get(target, {})
                scene_label = scene_info.get("sceneIdHex")
                target_labels.append(f"{target} {scene_label}" if scene_label else target)
            payload = choice.get("payloadVaHex")
            if payload:
                label = f"{label}/{payload}"
            choices.append(f"{label}: {', '.join(target_labels)}")
        lines.append(
            "| {source} | {scene} | {kind} | `{record}` | {targets} | {check} | {spawn} | {active} | {points} | {raw} | {refs} |".format(
                source=item["map"],
                scene=item["sceneIdHex"],
                kind=item["eventKind"],
                record=item["recordVaHex"],
                targets="<br>".join(choices) if choices else ", ".join(item["targets"]),
                check=check_link,
                spawn=f"{item.get('spawnPolicy', '-')} ({item.get('spawnConfidence', '-')})",
                active=active_points or "-",
                points=points or "-",
                raw=item.get("rawPointCount") or "-",
                refs=item["dispatchRefCount"],
            )
        )
    lines.append("")
    return "\n".join(lines)


def rows_by_map(transitions: list[dict]) -> dict[str, list[dict]]:
    grouped: dict[str, list[dict]] = {}
    for item in transitions:
        grouped.setdefault(item["map"], []).append(item)
    return {name: grouped[name] for name in sorted(grouped)}


def write_map_pages(transitions: list[dict], out_dir: Path) -> list[dict]:
    out_dir.mkdir(parents=True, exist_ok=True)
    index_rows = []
    for map_name, rows in rows_by_map(transitions).items():
        path = out_dir / f"{map_name}.md"
        path.write_text(
            markdown(rows, f"# {map_name} Event Transition Candidates", "../../web"),
            encoding="utf-8",
        )
        index_rows.append({
            "map": map_name,
            "rows": len(rows),
            "points": sum(len(row.get("points", [])) for row in rows),
            "path": path,
        })
    return index_rows


def index_markdown(index_rows: list[dict], out_dir: Path) -> str:
    lines = [
        "# Event Transition Candidate Index",
        "",
        "Generated from `out/event_transitions.json`.",
        "",
        "| map | rows | points | page |",
        "| --- | ---: | ---: | --- |",
    ]
    for row in index_rows:
        rel = row["path"].relative_to(out_dir)
        lines.append(f"| {row['map']} | {row['rows']} | {row['points']} | [{rel.name}]({rel.as_posix()}) |")
    lines.append("")
    return "\n".join(lines)


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--events", type=Path, default=Path("out/scene_events.json"))
    parser.add_argument("--json-out", type=Path, default=Path("out/event_transitions.json"))
    parser.add_argument("--js-out", type=Path, default=Path("out/event_transitions.js"))
    parser.add_argument("--md-out", "--out", dest="md_out", type=Path, default=None)
    parser.add_argument("--split-dir", type=Path, default=None)
    args = parser.parse_args()

    events = json.loads(args.events.read_text(encoding="utf-8"))
    transitions = transition_records(events)
    args.json_out.parent.mkdir(parents=True, exist_ok=True)
    args.json_out.write_text(json.dumps(transitions, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    args.js_out.write_text(
        "window.HWANSE_EVENT_TRANSITIONS = "
        + json.dumps(transitions, ensure_ascii=False, indent=2)
        + ";\n",
        encoding="utf-8",
    )
    if args.md_out:
        args.md_out.write_text(markdown(transitions), encoding="utf-8")
    if args.split_dir:
        index_rows = write_map_pages(transitions, args.split_dir)
        (args.split_dir / "index.md").write_text(index_markdown(index_rows, args.split_dir), encoding="utf-8")
    print(f"wrote {len(transitions)} event transition candidates -> {args.json_out}")


if __name__ == "__main__":
    main()
