"""Narrator(说书人):把多 agent 事件流实时写成小说连载(docs/agent-runtime-v2.md §9)。

- 独立观察者,从 EventBus 旁路(tap)收集**公开**事件(private 不读,天然防剧透);
- 每攒一个叙事窗口(≥10 模拟分钟或事件够一批)调一次 LLM:
  前情概要 + 本窗口事件 → 续写 200–400 字第三人称悬疑正文 + 滚动更新概要;
- 输出 paras 供 viz 左栏连载;LLM 失败/超时该窗口顺延,不丢事件。
"""

from __future__ import annotations

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

logger = get_logger("runtime.narrator")

WINDOW_S = 600.0          # 叙事窗口:10 模拟分钟(瘦身主靠缩短每段篇幅,见 SYSTEM_TMPL)
BATCH_MAX = 14            # 或攒满 14 条事件提前成段

SYSTEM_TMPL = (
    "你是悬疑小说{novel}的执笔者,以第三人称全知视角实时连载一桩孤岛悬案。"
    "文风:阴郁克制、像阿加莎·克里斯蒂,多写动作与神态、少下断语。"
    "【篇幅·要短】每段**短而紧**:60-120 字,只抓此刻**最要紧的一两笔**(一桩动作、一句机锋、一个神色)。"
    "不要面面俱到地铺陈每个房间——镜头聚在最有戏的地方,其余至多一句带过。宁可短,绝不注水。"
    "【铁律】登场人物**只有**:{roster}{ghost_clause};"
    "绝对不许发明任何新人物、新名字或新道具;只写下面提供的事件中发生的事,"
    "不要替人物编造未发生的行为,也不许给人物发明事件里没有的伤口、绷带、随身物件——"
    "氛围(风雨、灯火)可点到为止,涉及人与物的**事实**必须来自事件;不要点破谁是凶手。"
    "输出 JSON:{{\"prose\": \"本段正文(60-120字,务必简短)\", \"summary\": \"截至目前的前情概要(120字内)\"}}"
)


class Narrator:
    def __init__(self, llm, roster: list[str] | None = None, *,
                 novel_title: str = "《雾鸦》", ghost: str | None = "阮青",
                 opening: str | None = None) -> None:
        """说书人世界设定可配置(叙事层解耦):novel_title=连载书名,ghost=只存在于回忆中的死者(可缺),opening=开篇前情。"""
        self.llm = llm
        ghost_clause = f"(以及十年前死去、只存在于回忆中的{ghost})" if ghost else ""
        self.system = SYSTEM_TMPL.format(
            novel=novel_title, ghost_clause=ghost_clause,
            roster="、".join(roster or ["顾长风(男)", "邵铭轩(男)", "程亦深(男)", "唐曼(女)", "白聿(男)", "温言(女)", "江离(男)"]))
        self.buffer: list[str] = []
        self.summary = opening or "暴风雨之夜,七位《雾鸦》旧人被一纸邀请骗回雾鸦岛,刚看完那场不可能存在的放映。"
        self.paras: list[dict] = []
        self._last_write_s = 0.0
        self.busy = False
        self.urgent = False        # 高显著度事件(凶案/停电)→ 下个帧循环立即成段(G17)

    # —— 事件旁路(挂到 EventBus.taps)——
    def tap(self, e: Event) -> None:
        # 全知但克制:会话对白(private speech)可见——那是"场上的戏";
        # 其余 private(动作反馈/单独揭示,如"你亲眼目睹凶手")仍然屏蔽,防剧透。
        if e.scope == "private" and e.etype != "speech":
            return
        if e.salience < 0.3:
            return
        if e.etype in ("speech", "action", "ambient", "transit"):
            tag = f"[{e.place}] " if e.place else ""
            self.buffer.append(tag + e.content)
            if e.salience >= 0.85:
                self.urgent = True

    def due(self, now_s: float) -> bool:
        if not self.buffer or self.busy:
            return False
        if self.urgent:
            return True            # 凶案级事件:立刻重头描写,不等窗口
        return (now_s - self._last_write_s) >= WINDOW_S or len(self.buffer) >= BATCH_MAX

    # —— 成段(阻塞,调用方丢线程池)——
    def write(self, now_s: float, time_label: str, panorama: str = "") -> None:
        self.urgent = False
        batch, self.buffer = self.buffer[:24], self.buffer[24:]
        user = "\n".join([
            f"【前情概要】{self.summary}",
            f"【时间】{time_label}",
            *([f"【此刻全景】{panorama}"] if panorama else []),
            "【本段事件】",
            *(f"- {b}" for b in batch),
            "续写正文并更新概要(JSON):",
        ])
        try:
            out = self.llm.complete([{"role": "system", "content": self.system},
                                     {"role": "user", "content": user}],
                                    temperature=0.8, timeout=50, _tag="narrator")
        except Exception as e:
            logger.warning("说书人卡壳:%s(事件退回缓冲)", e)
            self.buffer = batch + self.buffer
            return
        d = parse_json_block(out)
        prose = str(d.get("prose", "")).strip() or out.strip()
        if prose:
            self.paras.append({"t": time_label, "text": prose})
            self._last_write_s = now_s
        if d.get("summary"):
            self.summary = str(d["summary"])[:300]
        logger.info("📖 续写一段(%d字,概要%d字)", len(prose), len(self.summary))
