"""resolve.py — Storyboard 解析器（v2 导演模式管线的中枢）.

把导演写的 storyboard.json（语义动词 + 符号时间引用）展开成 resolved.json
（每实体的纯数值轨道），渲染器（build_html2.py / build_ae2.py）只做插值，
不做任何语义判断。时间一律钉到 plan.json / timing.json 的真实词时刻。

用法:
    py -3.14 scripts/resolve.py <storyboard.json> <plan.json> <timing.json> <resolved.json>

详见 docs/storyboard-spec.md。
"""
from __future__ import annotations

import json
import re
import sys


def load(path: str) -> dict:
    with open(path, encoding="utf-8") as f:
        return json.load(f)


# ---------------------------------------------------------------------------
# 时间引用解析
# ---------------------------------------------------------------------------

_REF_SCENE = re.compile(r"^s(\d+)\.(start|end)([+-]\d+(?:\.\d+)?)?$")
_REF_KW = re.compile(r"^kw:(.+?)(?:#(\d+))?([+-]\d+(?:\.\d+)?)?$")
_REF_WORD = re.compile(r"^w:(.+?)(?:#(\d+))?([+-]\d+(?:\.\d+)?)?$")


class Resolver:
    def __init__(self, plan: dict, timing: dict):
        self.scenes = {s["id"]: s for s in plan["scenes"]}
        self.words = timing["words"]
        # 关键词出现表：text -> [绝对秒...]
        self.kw_times: dict[str, list[float]] = {}
        for s in plan["scenes"]:
            for kw in s.get("keywords", []):
                self.kw_times.setdefault(kw["text"], []).append(float(kw["t"]))

    def time(self, ref) -> float:
        if isinstance(ref, (int, float)):
            return float(ref)
        ref = str(ref).strip()
        m = _REF_SCENE.match(ref)
        if m:
            sc = self.scenes.get(int(m.group(1)))
            if sc is None:
                raise ValueError(f"时间引用指向不存在的幕: {ref}")
            base = float(sc["start"] if m.group(2) == "start" else sc["end"])
            return base + float(m.group(3) or 0)
        m = _REF_KW.match(ref)
        if m:
            times = self.kw_times.get(m.group(1))
            if not times:
                raise ValueError(f"plan.json 里没有关键词: {ref}")
            idx = int(m.group(2) or 1) - 1
            return times[idx] + float(m.group(3) or 0)
        m = _REF_WORD.match(ref)
        if m:
            needle, nth = m.group(1), int(m.group(2) or 1)
            hits = [w for w in self.words if needle in w["text"]]
            if len(hits) < nth:
                raise ValueError(f"timing.json 里找不到词: {ref}")
            return float(hits[nth - 1]["start"]) + float(m.group(3) or 0)
        try:
            return float(ref)
        except ValueError:
            raise ValueError(f"无法解析时间引用: {ref!r}")


# 景别预设：shot 名 → (zoom, cam_y)
SHOT_PRESETS = {"closeup": (1.9, -100.0), "medium": (1.0, 0.0), "wide": (0.5, 0.0)}

# 手势动词 → 默认时长
GESTURES = {"wave": 0.9, "point": 0.8, "nod": 0.6, "shake_head": 0.6,
            "jump": 0.7, "spin": 0.8, "look": 0.4}


# ---------------------------------------------------------------------------
# 主流程
# ---------------------------------------------------------------------------

def main(storyboard_path: str, plan_path: str, timing_path: str, out_path: str) -> None:
    sb = load(storyboard_path)
    plan = load(plan_path)
    timing = load(timing_path)
    rt = Resolver(plan, timing)

    locations: dict[str, float] = {k: float(v) for k, v in sb["stage"].get("locations", {}).items()}

    def loc(v) -> float:
        if isinstance(v, str):
            if v not in locations:
                raise ValueError(f"未定义的地名: {v}")
            return locations[v]
        return float(v)

    audio_end = float(timing["audio_duration"]) + 1.0

    # --- 角色轨道骨架 ---
    actors: dict[str, dict] = {}
    for name, spec in sb.get("cast", {}).items():
        actors[name] = {
            "x0": loc(spec.get("x", 0)),
            "face0": spec.get("face", "calm"),
            "hidden0": bool(spec.get("hidden", False)),
            "moves": [], "places": [], "poses": [], "faces": [],
            "bounces": [], "vis": [], "gestures": [],
        }

    def actor_x_at(name: str, t: float) -> float:
        """回放 moves/places 求角色在 t 时刻的世界 x（线性插值）。"""
        act = actors[name]
        x = act["x0"]
        events = ([("p", p["t"], p) for p in act["places"]] +
                  [("m", m["t0"], m) for m in act["moves"]])
        for kind, et, e in sorted(events, key=lambda v: v[1]):
            if et > t:
                break
            if kind == "p":
                x = e["x"]
            else:
                if t >= e["t1"]:
                    x = e["to"]
                else:
                    frac = (t - e["t0"]) / (e["t1"] - e["t0"])
                    x = x + (e["to"] - x) * frac
        return x

    def entity_x_at(ref: str, t: float) -> float:
        """target 可以是角色 / 道具 / 地名 / 数字。"""
        if ref in actors:
            return actor_x_at(ref, t)
        if ref in prop_by_id and prop_by_id[ref].get("x") is not None:
            return prop_by_id[ref]["x"]
        return loc(ref)

    # --- 道具骨架 ---
    props_out: list[dict] = []
    prop_by_id: dict[str, dict] = {}
    item_ids: set[str] = set()
    for p in sb.get("props", []):
        entry = {
            "id": p["id"], "type": p["type"], "label": p.get("label", ""),
            "x": loc(p["x"]) if "x" in p else None,
            "hidden": bool(p.get("hidden", False)),
            "parallax": float(p.get("parallax", 1.0)),
            "events": [],
        }
        if p["type"] == "coin" or "carrier" in p:
            item_ids.add(p["id"])
            entry["initial_carrier"] = p.get("carrier")
        prop_by_id[p["id"]] = entry
        props_out.append(entry)

    # 物品动词先收集，按时间排序后再串成 segments
    item_verbs: dict[str, list[dict]] = {i: [] for i in item_ids}
    item_bounces: dict[str, list[dict]] = {i: [] for i in item_ids}

    camera: list[dict] = []
    warnings: list[str] = []

    fx_list: list[dict] = []
    no_autofx_ranges: list[tuple[float, float]] = []

    def make_cam_seg(cam: dict, t0: float, t1: float, hard: bool) -> dict:
        zoom, cam_y = SHOT_PRESETS.get(cam.get("shot", "medium"), SHOT_PRESETS["medium"])
        seg = {"t0": t0, "t1": t1, "mode": cam["mode"],
               "zoom": float(cam.get("zoom", zoom)),
               "cam_y": float(cam.get("cam_y", cam_y))}
        if hard:
            seg["hard_cut"] = True
        if cam["mode"] == "fixed":
            seg["x"] = loc(cam["x"])
        else:
            seg["target"] = cam["target"]
        return seg

    for scene in sb["scenes"]:
        sc = rt.scenes[scene["plan_scene"]]
        t0, t1 = float(sc["start"]), float(sc["end"])
        if scene.get("no_auto_fx"):
            no_autofx_ranges.append((t0, t1))
        # 镜头：主机位 + 镜内硬切
        cuts = sorted([{"t": rt.time(c["at"]), "camera": c["camera"]}
                       for c in scene.get("cuts", [])], key=lambda c: c["t"])
        seg_start, seg_cam, hard = t0, scene["camera"], False
        for c in cuts:
            camera.append(make_cam_seg(seg_cam, seg_start, c["t"], hard))
            seg_start, seg_cam, hard = c["t"], c["camera"], True
        camera.append(make_cam_seg(seg_cam, seg_start, t1, hard))

        for a in scene["actions"]:
            verb = a["verb"]
            if verb in ("walk_to", "run_to"):
                act = actors[a["actor"]]
                act["moves"].append({
                    "t0": rt.time(a["t0"]), "t1": rt.time(a["t1"]),
                    "to": loc(a["to"]), "gait": "run" if verb == "run_to" else "walk",
                })
            elif verb == "place":
                actors[a["actor"]]["places"].append({"t": rt.time(a["at"]), "x": loc(a["to"])})
            elif verb == "pose":
                actors[a["actor"]]["poses"].append({"t": rt.time(a["at"]), "preset": a["preset"]})
            elif verb == "emote":
                actors[a["actor"]]["faces"].append({"t": rt.time(a["at"]), "face": a["face"]})
            elif verb == "bounce":
                ev = {"t": rt.time(a["at"]), "times": int(a.get("times", 2)),
                      "height": float(a.get("height", 60))}
                if "actor" in a:
                    actors[a["actor"]]["bounces"].append(ev)
                else:
                    item_bounces[a["item"]].append(ev)
            elif verb == "appear":
                actors[a["actor"]]["vis"].append({"t": rt.time(a["at"]), "on": True})
            elif verb == "vanish":
                actors[a["actor"]]["vis"].append({"t": rt.time(a["at"]), "on": False})
            elif verb in GESTURES:
                t = rt.time(a["at"])
                g = {"t": t, "name": verb, "dur": float(a.get("dur", GESTURES[verb]))}
                if verb == "jump":
                    g["height"] = float(a.get("height", 160))
                if "target" in a:
                    g["dir"] = 1 if entity_x_at(a["target"], t) >= actor_x_at(a["actor"], t) else -1
                actors[a["actor"]]["gestures"].append(g)
            elif verb == "fx":
                t = rt.time(a["at"])
                entry = {"t": t, "kind": a["kind"]}
                for k in ("n", "dur", "glyph"):
                    if k in a:
                        entry[k] = a[k]
                if "actor" in a:
                    entry["actor"] = a["actor"]
                elif "prop" in a:
                    entry["x"] = prop_by_id[a["prop"]]["x"]
                elif "x" in a:
                    entry["x"] = loc(a["x"])
                fx_list.append(entry)
            elif verb in ("give", "emerge", "hide_item", "orbit", "hops"):
                item_verbs[a["item"]].append({**a, "_t": rt.time(
                    a.get("at", a.get("t0", 0)))})
            elif verb in ("show", "hide"):
                prop_by_id[a["prop"]]["events"].append(
                    {"t": rt.time(a["at"]), "kind": verb})
            elif verb == "grow":
                prop_by_id[a["prop"]]["events"].append({
                    "t": rt.time(a["t0"]), "kind": "grow", "t1": rt.time(a["t1"]),
                    "from": float(a.get("from", 0.5)), "to": float(a.get("to", 1.5)),
                })
            elif verb in ("shop_close", "shop_open"):
                prop_by_id[a["prop"]]["events"].append(
                    {"t": rt.time(a["at"]), "kind": verb.replace("shop_", "")})
            else:
                raise ValueError(f"未知动词: {verb}")

    # 镜头轨道必须无缝覆盖 [0, 片尾]：幕间空隙归属前一段（v3 实拍 bug：
    # 空隙时刻找不到归属段会兜底到最后一段，镜头瞬跳到片尾的银行/城市方向）。
    camera.sort(key=lambda c: c["t0"])
    if camera:
        camera[0]["t0"] = 0.0
        for i in range(len(camera) - 1):
            camera[i]["t1"] = camera[i + 1]["t0"]
        camera[-1]["t1"] = audio_end

    # --- 把物品动词串成连续 segments ---
    items_out: dict[str, dict] = {}
    for item_id in item_ids:
        verbs = sorted(item_verbs[item_id], key=lambda v: v["_t"])
        segs: list[dict] = []
        carrier = prop_by_id[item_id].get("initial_carrier")
        static_x: float | None = None
        cursor = 0.0

        def close_open_segment(until: float) -> None:
            nonlocal cursor
            if until <= cursor + 1e-6:
                cursor = max(cursor, until)
                return
            if carrier is not None:
                segs.append({"mode": "carried", "t0": cursor, "t1": until, "carrier": carrier})
            elif static_x is not None:
                segs.append({"mode": "static", "t0": cursor, "t1": until, "x": static_x})
            else:
                segs.append({"mode": "hidden", "t0": cursor, "t1": until})
            cursor = until

        for v in verbs:
            verb = v["verb"]
            if verb == "give":
                t = rt.time(v["at"]); dur = float(v.get("dur", 0.6))
                if carrier != v["from"]:
                    warnings.append(
                        f"{item_id}: give 时 carrier 是 {carrier}，分镜写的是 {v['from']}")
                close_open_segment(t)
                segs.append({"mode": "arc", "t0": t, "t1": t + dur,
                             "from_carrier": carrier or v["from"], "to_carrier": v["to_actor"]})
                carrier, static_x, cursor = v["to_actor"], None, t + dur
            elif verb == "emerge":
                t0v, t1v = rt.time(v["t0"]), rt.time(v["t1"])
                close_open_segment(t0v)
                segs.append({"mode": "arc", "t0": t0v, "t1": t1v,
                             "from_x": prop_by_id[v["from_prop"]]["x"],
                             "to_carrier": v["to_actor"]})
                carrier, static_x, cursor = v["to_actor"], None, t1v
            elif verb == "hide_item":
                t = rt.time(v["at"])
                close_open_segment(t)
                segs.append({"mode": "shrink", "t0": t, "t1": t + 0.4, "carrier": carrier})
                carrier, static_x, cursor = None, None, t + 0.4
            elif verb == "orbit":
                t0v, t1v = rt.time(v["t0"]), rt.time(v["t1"])
                center = prop_by_id[v["around_prop"]]["x"]
                close_open_segment(t0v)
                segs.append({"mode": "orbit", "t0": t0v, "t1": t1v, "center_x": center,
                             "radius": float(v.get("radius", 420)),
                             "loops": float(v.get("loops", 2))})
                carrier, static_x, cursor = None, center, t1v
            elif verb == "hops":
                t0v, t1v = rt.time(v["t0"]), rt.time(v["t1"])
                close_open_segment(t0v)
                segs.append({"mode": "hops", "t0": t0v, "t1": t1v,
                             "from": loc(v["from"]), "to": loc(v["to"])})
                carrier, static_x, cursor = None, loc(v["to"]), t1v
        close_open_segment(audio_end)
        items_out[item_id] = {"segments": segs, "bounces": sorted(
            item_bounces[item_id], key=lambda b: b["t"])}

    # 物品不再按普通道具渲染
    props_out = [p for p in props_out if p["id"] not in item_ids]

    # --- 校验 + move 回填显式 from（渲染器禁止自行猜起点）---
    for name, act in actors.items():
        for lst in ("moves", "places", "poses", "faces", "bounces", "vis", "gestures"):
            act[lst].sort(key=lambda e: e.get("t", e.get("t0", 0)))
        prev_end = None
        for mv in act["moves"]:
            if mv["t1"] <= mv["t0"]:
                raise ValueError(f"{name} 的移动时间倒挂: {mv}")
            if prev_end is not None and mv["t0"] < prev_end - 1e-6:
                warnings.append(f"{name} 两段移动重叠 @{mv['t0']:.2f}")
            prev_end = mv["t1"]
            mv["from"] = actor_x_at(name, mv["t0"])

    # --- 自动特效 ---
    def autofx_ok(t: float) -> bool:
        return not any(a <= t <= b for a, b in no_autofx_ranges)

    for name, act in actors.items():
        for mv in act["moves"]:
            if mv["gait"] == "run" and autofx_ok(mv["t0"]):
                fx_list.append({"t": mv["t0"], "t1": mv["t1"],
                                "kind": "speed_lines", "actor": name})
            if autofx_ok(mv["t1"]):
                fx_list.append({"t": mv["t1"], "kind": "dust", "x": mv["to"]})
        for g in act["gestures"]:
            if g["name"] == "jump" and autofx_ok(g["t"]):
                fx_list.append({"t": g["t"] + g["dur"] * 0.85, "kind": "dust",
                                "x": actor_x_at(name, g["t"])})
        for b in act["bounces"]:
            if autofx_ok(b["t"]):
                fx_list.append({"t": b["t"] + 0.2, "kind": "dust",
                                "x": actor_x_at(name, b["t"]), "small": True})
    for item_id, tr in items_out.items():
        for seg in tr["segments"]:
            if seg["mode"] == "arc" and seg.get("to_carrier") and autofx_ok(seg["t1"]):
                fx_list.append({"t": seg["t1"], "kind": "sparkle",
                                "actor": seg["to_carrier"], "anchor": "hand"})
            if seg["mode"] == "hops" and autofx_ok(seg["t0"]):
                n = max(1, int((seg["t1"] - seg["t0"]) / 0.45))
                for k in range(1, n + 1):
                    tt = seg["t0"] + k * 0.45
                    if tt > seg["t1"]:
                        break
                    frac = (tt - seg["t0"]) / (seg["t1"] - seg["t0"])
                    fx_list.append({"t": tt, "kind": "dust", "small": True,
                                    "x": seg["from"] + (seg["to"] - seg["from"]) * frac})
    for p in props_out:
        for ev in p["events"]:
            if not autofx_ok(ev["t"]):
                continue
            if ev["kind"] == "close":
                fx_list.append({"t": ev["t"] + 0.25, "kind": "dust", "x": p["x"]})
                fx_list.append({"t": ev["t"], "kind": "shake", "amp": 12, "dur": 0.35})
            elif ev["kind"] == "grow":
                fx_list.append({"t": ev["t1"], "kind": "shockwave", "x": p["x"]})
            elif ev["kind"] == "show" and p["type"] == "cake":
                fx_list.append({"t": ev["t"], "kind": "shockwave", "x": p["x"]})
    # v4 自动特效：物品飞行拖尾、硬切白闪、全片环境微粒
    for item_id, tr in items_out.items():
        for seg in tr["segments"]:
            if seg["mode"] in ("arc", "hops") and autofx_ok(seg["t0"]):
                fx_list.append({"t": seg["t0"], "t1": seg["t1"],
                                "kind": "trail", "item": item_id})
    for seg in camera:
        if seg.get("hard_cut") and autofx_ok(seg["t0"]):
            fx_list.append({"t": seg["t0"], "kind": "flash", "dur": 0.12})
    fx_list.append({"t": 0.0, "t1": audio_end, "kind": "ambient"})
    fx_list.sort(key=lambda e: e["t"])

    resolved = {
        "meta": {
            "audio_duration": float(timing["audio_duration"]),
            "ground_y": float(sb["stage"].get("ground_y", 200)),
            "K": 0.9,
        },
        "actors": actors,
        "items": items_out,
        "props": props_out,
        "camera": camera,
        "fx": fx_list,
    }
    with open(out_path, "w", encoding="utf-8") as f:
        json.dump(resolved, f, ensure_ascii=False, indent=2)

    n_act = sum(len(s["actions"]) for s in sb["scenes"])
    print(f"[resolve] {len(sb['scenes'])} scenes, {n_act} actions -> "
          f"{len(actors)} actors / {len(items_out)} items / {len(props_out)} props")
    for w in warnings:
        print(f"[resolve][warn] {w}")


if __name__ == "__main__":
    if len(sys.argv) != 5:
        print(__doc__)
        sys.exit(1)
    main(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
