"""EventBus:世界事件的发布-订阅总线,感知从"轮询快照"变为"实时推送"。

可见性三档(docs/agent-runtime-v2.md §1.3):
- place:同地点全量感知;相邻地点收到"衰减版"(闻其声不见其形);
- global:全岛广播(惨叫/停电/钟声),可带方位提示;
- private:只投给指定收件人(如动作反馈)。

显著度 salience ∈ [0,1] 决定收件人是否被立即打断(由 Attention 层判定,总线只负责送达)。
总线不依赖 World 具体实现,通过 locator 回调解耦(单测可用字典桩)。
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Callable

from genesis.obs.logging_setup import get_logger

logger = get_logger("runtime.bus")


@dataclass
class Event:
    etype: str                  # speech / action / state_change / ambient / feedback …
    content: str                # 人类可读描述(直接可入记忆)
    place: str = ""             # 事发地点("" = 无地点,如系统公告)
    actor: str = ""             # 发起者("" = 世界本身)
    sim_t: float = 0.0          # 模拟时刻(秒)
    salience: float = 0.3       # 显著度:≥0.8 触发立即打断
    scope: str = "place"        # place / global / private
    to: list[str] = field(default_factory=list)      # scope=private 的收件人
    muffled: str = ""           # 相邻地点听到的衰减版("" = 相邻不可感知)
    data: dict = field(default_factory=dict)         # 结构化负载(状态变更详情等)


class EventBus:
    def __init__(
        self,
        occupants_of: Callable[[str], list[str]],     # 地点 -> 在场 agent ids
        adjacent_of: Callable[[str], list[str]],      # 地点 -> 相邻地点
    ) -> None:
        self._occupants_of = occupants_of
        self._adjacent_of = adjacent_of
        self._inboxes: dict[str, Callable[[Event], None]] = {}
        self.taps: list[Callable[[Event], None]] = []   # 旁路观察者(说书人等),收到所有事件

    def register(self, agent_id: str, deliver: Callable[[Event], None]) -> None:
        self._inboxes[agent_id] = deliver

    def unregister(self, agent_id: str) -> None:
        self._inboxes.pop(agent_id, None)

    def publish(self, event: Event) -> list[str]:
        """投递事件,返回实际收到的 agent 列表(便于测试与日志)。"""
        for tap in self.taps:
            try:
                tap(event)
            except Exception:
                logger.exception("tap 观察者异常(忽略)")
        delivered: list[str] = []
        if event.scope == "private":
            for aid in event.to:
                self._send(aid, event, delivered)
        elif event.scope == "global":
            for aid in list(self._inboxes):
                if aid != event.actor:
                    self._send(aid, event, delivered)
        else:  # place
            for aid in self._occupants_of(event.place):
                if aid != event.actor:
                    self._send(aid, event, delivered)
            if event.muffled:  # 相邻地点的衰减感知(走廊设计的根)
                weak = Event(etype=event.etype, content=event.muffled, place=event.place,
                             actor=event.actor, sim_t=event.sim_t,
                             salience=max(0.1, event.salience - 0.25), scope="place")
                seen = set(delivered) | {event.actor}
                for adj in self._adjacent_of(event.place):
                    for aid in self._occupants_of(adj):
                        if aid not in seen:
                            self._send(aid, weak, delivered)
                            seen.add(aid)
        logger.debug("event[%s] %s @%s → %s", event.etype, event.content[:30], event.place, delivered)
        return delivered

    def _send(self, aid: str, event: Event, delivered: list[str]) -> None:
        deliver = self._inboxes.get(aid)
        if deliver is not None:
            deliver(event)
            delivered.append(aid)
