"""DeviceEngine:剧情装置引擎——世界对事件做出反应(G13)。

设计:装置 = 「触发 → 延迟 → 世界状态变化 + 事件」的声明式规则。
- 内建钩子:凶案(胶片盒倒计时+残页钉尸旁)、灯塔点亮(渡船提前)、天亮(汽笛);
- 通用规则口子 register_rule:造物主可在运行时注入自定义装置(满足 G13 期望成品第三条);
- 装置反应是**客观世界状态**:没人在场也会发生,后来者从 describe 看到既成事实。
"""

from __future__ import annotations

import itertools
from dataclasses import dataclass
from typing import Callable

from genesis.obs.logging_setup import get_logger
from genesis.runtime.bus import Event

logger = get_logger("runtime.devices")

FILM_BOX_DELAY_S = 6 * 60       # 凶案后 ~6 分钟,倒计时装置"自己"推进(G13 验收:10 分钟内)
PIN_PAGE_DELAY_S = 2 * 60       # 凶手签名(残页/记号)留在尸体旁

# 凶案倒计时装置默认配置(雾鸦岛:七个胶片盒逐幕烧毁 + 残页钉尸旁)。
# 任何世界可在 world.json 的 "kill_device" 覆盖它(道具/装置层解耦):缺则用此默认。
# 模板字段:{n}=已发生凶案数, {left}=剩余(total-n), {victim}=死者。
DEFAULT_KILL_DEVICE = {
    "countdown_facility": "film_boxes", "state_template": "act{n}", "max": 7, "total": None,
    "burn_place": "餐厅",
    "burn_message": "餐厅传来一缕焦味——『第{n}幕』的胶片盒不知何时被打开,里面的胶片烧成了一撮灰。",
    "burn_muffled": "附近飘来一缕焦糊味",
    "narr": "🎞 『第{n}幕』的胶片烧成了灰——倒计时又走了一格。",
    "signature": {"name": "钉在{victim}尸体旁的《雾鸦》剧本残页",
                  "hint": "凶手按《雾鸦》剧本'补拍'留下的签名——比对死法与哪一幕吻合,能锁定凶手是熟悉剧本的人(编剧)",
                  "message": "{victim}的尸体旁,被人用裁纸刀钉上了一页《雾鸦》的剧本残页。"},
}


@dataclass
class PendingFire:
    token: int
    handler: Callable[[], None]


class DeviceEngine:
    def __init__(self, engine) -> None:
        self.engine = engine
        self._tokens = itertools.count(1)
        self._pending: dict[int, PendingFire] = {}
        self._rules: list[tuple[str, float, Callable[[Event], None]]] = []  # (etype关键词, delay, handler)
        self.acts_burned = 0        # 已烧毁的胶片盒数(=第几幕)

    # —— 通用规则口子(造物主自定义装置)——
    def register_rule(self, content_keyword: str, delay_s: float,
                      handler: Callable[[Event], None]) -> None:
        self._rules.append((content_keyword, delay_s, handler))

    def on_event(self, e: Event) -> None:
        """挂在 bus.taps 上:匹配自定义规则,延迟触发。"""
        for keyword, delay, handler in self._rules:
            if keyword in e.content:
                self._defer(delay, lambda e=e, h=handler: h(e))

    def _defer(self, delay_s: float, handler: Callable[[], None]) -> None:
        token = next(self._tokens)
        self._pending[token] = PendingFire(token, handler)
        self.engine.sched.schedule(f"__device__:{token}",
                                   self.engine.clock.sim_seconds + delay_s, "device")

    def fire(self, token: int) -> None:
        pending = self._pending.pop(token, None)
        if pending is not None:
            try:
                pending.handler()
            except Exception:
                logger.exception("装置触发失败(token=%d)", token)

    # —— 内建装置:凶案 → 倒计时装置推进 + 凶手签名留尸旁(配置驱动,按世界解耦)——
    def on_kill(self, victim: str, place: str, scripted: bool = True) -> None:
        eng = self.engine
        cfg = getattr(eng, "_kill_device", None) or DEFAULT_KILL_DEVICE
        act_no = self.acts_burned + 1
        self.acts_burned = act_no
        total = cfg.get("total")
        left = max(0, total - act_no) if total else None
        fmt = lambda s: s.format(n=act_no, left=left, victim=victim)

        def advance_countdown():
            fac = cfg.get("countdown_facility")
            if fac and fac in eng.state.facilities:    # 缺设施则只播报、不崩(解耦:世界可不带倒计时装置)
                n = min(cfg.get("max", 999), act_no)
                eng.state.set_facility(fac, cfg["state_template"].format(n=n, left=left))
            eng.bus.publish(Event(
                etype="ambient", content=fmt(cfg["burn_message"]),
                place=cfg.get("burn_place", place), sim_t=eng.clock.sim_seconds, salience=0.75,
                muffled=cfg.get("burn_muffled", "")))
            eng._narr(fmt(cfg["narr"]))

        def leave_signature():
            from genesis.runtime.worldstate import Item
            sig = cfg["signature"]
            eng.state.add_item(Item(f"pinned_{victim}", fmt(sig["name"]), place, hidden=False,
                                    hint=sig.get("hint", "")))
            if sig.get("message"):
                eng.bus.publish(Event(etype="ambient", content=fmt(sig["message"]),
                                      place=place, sim_t=eng.clock.sim_seconds, salience=0.6))

        self._defer(FILM_BOX_DELAY_S, advance_countdown)
        if scripted and cfg.get("signature"):   # 仪式杀(在取景地、对得上死法)才留签名;临时/被打断的杀不留这破绽
            self._defer(PIN_PAGE_DELAY_S, leave_signature)

    # —— 内建装置:灯塔点亮 → 渡船提前 ——
    def on_lighthouse_lit(self) -> None:
        eng = self.engine
        new_eta = eng.clock.sim_seconds + 2 * 3600
        if new_eta < eng.ferry_eta:
            eng.ferry_eta = new_eta
            eng.sched.schedule("__ferry__", eng.ferry_eta, "ferry")
            eng.bus.publish(Event(
                etype="ambient",
                content="对岸有了回应——远处海面上亮起一点船灯!渡船提前出发了,大约两小时后靠岸。",
                scope="global", sim_t=eng.clock.sim_seconds, salience=0.8))
            eng._narr("⛴ 灯塔的光换来了回应:渡船提前出发,两小时后靠岸。")
