"""世界持久化:WorldEngine 全量可变状态的存档与恢复(G20:随时存档,断点续世)。

设计为**纯外部模块**,不侵入引擎:snapshot 只读、restore 只写回。
不可变部分(地图拓扑/人设/seed/brain/llm)不入档——调用方约定:
restore 前先用**相同参数**重新构造引擎,本模块只负责"可变状态":
时钟、世界客观事实、每个心智(意图/躯体/记忆)、叙事与说书人前情。

已知未覆盖(恢复后自愈,见模块尾注):
- actor 当前动作与调度器预约:醒来重新决策,start() 会重新排程;
- attention 收件箱里未消费的事件:丢失,但事件源头已落入世界状态;
- dialogue_until / auto_ff 等瞬时调速窗口:恢复为常规速率。
"""

from __future__ import annotations

import json
from pathlib import Path

from genesis.memory.record import MemoryKind, MemoryRecord
from genesis.obs.logging_setup import get_logger

logger = get_logger("runtime.persistence")

SNAPSHOT_VERSION = 1

# 心智上的标量可变字段(StubMind / CognitiveMind 各有一部分,getattr 容错)
_MIND_FIELDS = ("intent", "last_speech", "last_thought", "home", "dialogue_with", "gathering", "goals", "relations", "imp_acc",)
# Soma 的全部可变字段(_fear_at_h 是私有衰减锚点,必须一并存,否则恐惧曲线错位)
_SOMA_FIELDS = ("last_meal_h", "woke_at_h", "fear", "_fear_at_h")
_MISSING = object()


# ───────────────────── 导出 ─────────────────────

def snapshot_engine(engine) -> dict:
    """把引擎的全量可变状态导出为 JSON-safe dict(任何时刻可调,只读不扰动世界)。"""
    st = engine.state
    data = {
        "version": SNAPSHOT_VERSION,
        "clock": {"sim_seconds": engine.clock.sim_seconds, "base_rate": engine.base_rate},
        "state": {
            "positions": dict(st.positions),
            "weather": st.weather,
            "power_on": st.power_on,
            "dead": dict(st.dead),
            "bodies": {p: list(names) for p, names in st.bodies.items() if names},
            "facilities": {fid: f.state for fid, f in st.facilities.items()},
            "items": {iid: {"place": it.place, "holder": it.holder,
                            "destroyed": it.destroyed, "hidden": it.hidden}
                      for iid, it in st.items.items()},
        },
        "minds": {name: _snapshot_mind(mind) for name, mind in engine.minds.items()},
        "narrative": list(engine.narrative),
        "lighthouse_fuel": engine.lighthouse_fuel,
        # 批次 B 新增的可变状态(getattr 容错,老引擎/老存档双向兼容)
        "ferry_eta": getattr(engine, "ferry_eta", None),
        "executed": list(getattr(engine, "executed", [])),
        "ending": getattr(engine, "ending", None),
        "acts_burned": getattr(getattr(engine, "devices", None), "acts_burned", 0),
        "demo_done": sorted(getattr(engine, "_demo_done", ())),
        "narrator": _snapshot_narrator(engine.narrator),
    }
    logger.info("📸 世界快照:t=%s,%d 心智,%d 死亡,叙事 %d 条",
                engine.clock.label(), len(data["minds"]), len(st.dead), len(engine.narrative))
    return data


def _snapshot_mind(mind) -> dict:
    """单个心智的可变状态。StubMind / CognitiveMind 字段集不同,只存实际存在的。"""
    out: dict = {}
    for key in _MIND_FIELDS:
        val = getattr(mind, key, _MISSING)
        if val is not _MISSING:
            out[key] = val
    dialogue = getattr(mind, "dialogue", None)
    if dialogue is not None:
        out["dialogue"] = list(dialogue)
    insights = getattr(mind, "insights", None)
    if insights is not None:
        out["insights"] = list(insights)
    soma = getattr(mind, "soma", None)
    if soma is not None:
        out["soma"] = {f: getattr(soma, f) for f in _SOMA_FIELDS}
    memory = getattr(mind, "memory", None)   # 仅 CognitiveMind 有 MemoryStream
    if memory is not None:
        out["memory"] = [_record_to_dict(r) for r in memory.store.all(memory.agent_id)]
    return out


def _record_to_dict(rec: MemoryRecord) -> dict:
    return {"id": rec.id, "agent_id": rec.agent_id, "content": rec.content,
            "created_at": rec.created_at, "last_accessed": rec.last_accessed,
            "kind": rec.kind.value, "importance": rec.importance}


def _snapshot_narrator(narrator) -> dict | None:
    if narrator is None:
        return None
    return {"paras": list(narrator.paras), "summary": narrator.summary,
            "buffer": list(narrator.buffer),
            "last_write_s": getattr(narrator, "_last_write_s", 0.0)}


# ───────────────────── 恢复 ─────────────────────

def restore_engine(engine, data: dict) -> None:
    """把快照应用回一个**新构造的同世界**引擎;在 engine.start() 之前调用。

    时钟恢复:把存档的模拟时刻设为新锚点(真实时间锚到"现在"),
    时间从断点无缝续走;start() 会按恢复后的时刻重新排程唤醒。
    """
    version = data.get("version", 0)
    if version > SNAPSHOT_VERSION:
        logger.warning("存档版本 %s 高于当前支持的 %s,尽力恢复", version, SNAPSHOT_VERSION)

    # —— 时钟:先锚定模拟时刻,再恢复倍率(set_rate 会按新锚点重锚,时间连续)——
    clk = data["clock"]
    engine.clock._anchor_sim = float(clk["sim_seconds"])
    engine.clock._anchor_real = engine.clock._time_fn()
    engine.set_base_rate(float(clk["base_rate"]))

    _restore_state(engine.state, data["state"])

    for name, mdata in data.get("minds", {}).items():
        mind = engine.minds.get(name)
        if mind is None:
            logger.warning("存档中的心智「%s」在新引擎里不存在,跳过", name)
            continue
        _restore_mind(mind, mdata)

    # —— 死者退场:注销总线/调度,清空残留动作(与 kill 时的处置一致)——
    for victim in engine.state.dead:
        engine.bus.unregister(victim)
        engine.sched.drop(victim)
        mind = engine.minds.get(victim)
        if mind is not None:
            mind.actor.current = None

    engine.narrative[:] = [dict(n) for n in data.get("narrative", [])]
    engine.lighthouse_fuel = int(data.get("lighthouse_fuel", 0))
    if data.get("ferry_eta") is not None and hasattr(engine, "ferry_eta"):
        engine.ferry_eta = float(data["ferry_eta"])
    if hasattr(engine, "executed"):
        engine.executed[:] = list(data.get("executed", []))
    if hasattr(engine, "ending"):
        engine.ending = data.get("ending")
    if hasattr(engine, "devices"):
        engine.devices.acts_burned = int(data.get("acts_burned", 0))
    # 青之房若曾解锁,恢复拓扑连通(设施状态已随 facilities 恢复)
    md = engine.state.facilities.get("master_door")
    if md is not None and md.state == "open" and hasattr(engine.state, "connect"):
        engine.state.connect("二层走廊", "青之房")
    if hasattr(engine, "_demo_done"):
        engine._demo_done = set(data.get("demo_done", ()))
    _restore_narrator(engine.narrator, data.get("narrator"))
    logger.info("⏪ 世界恢复:t=%s,%d 心智,%d 死亡", engine.clock.label(),
                len(data.get("minds", {})), len(engine.state.dead))


def _restore_state(st, sdata: dict) -> None:
    """写回 WorldState。设施/物品按 id 就地改状态(对象本身由 seed 构造,保持引用)。"""
    st.positions.clear()
    st.positions.update(sdata["positions"])
    st.weather = sdata["weather"]
    st.power_on = sdata["power_on"]
    st.dead.clear()
    st.dead.update(sdata["dead"])
    st.bodies.clear()
    for place, names in sdata.get("bodies", {}).items():
        st.bodies[place] = list(names)
    for fid, fstate in sdata.get("facilities", {}).items():
        if fid in st.facilities:
            st.facilities[fid].state = fstate
        else:
            logger.warning("存档设施「%s」不存在于新世界,跳过", fid)
    for iid, idata in sdata.get("items", {}).items():
        it = st.items.get(iid)
        if it is None:
            logger.warning("存档物品「%s」不存在于新世界,跳过", iid)
            continue
        it.place = idata["place"]
        it.holder = idata["holder"]
        it.destroyed = idata["destroyed"]
        it.hidden = idata["hidden"]


def _restore_mind(mind, mdata: dict) -> None:
    for key in _MIND_FIELDS:
        if key in mdata and hasattr(mind, key):
            setattr(mind, key, mdata[key])
    dialogue = getattr(mind, "dialogue", None)
    if dialogue is not None and "dialogue" in mdata:
        dialogue.clear()
        dialogue.extend(mdata["dialogue"])
    insights = getattr(mind, "insights", None)
    if insights is not None and "insights" in mdata:
        insights.clear()
        insights.extend(mdata["insights"])
    soma = getattr(mind, "soma", None)
    if soma is not None and "soma" in mdata:
        for f in _SOMA_FIELDS:
            if f in mdata["soma"]:
                setattr(soma, f, mdata["soma"][f])
    memory = getattr(mind, "memory", None)
    if memory is not None and "memory" in mdata:
        _restore_memory(memory, mdata["memory"])


def _restore_memory(stream, records: list[dict]) -> None:
    """整体重建记忆流。优先用 store 的 replace_all(保留原 id);否则逐条 add 兜底。"""
    recs = [MemoryRecord(agent_id=r["agent_id"], content=r["content"],
                         created_at=r["created_at"], kind=MemoryKind(r["kind"]),
                         importance=r["importance"], last_accessed=r["last_accessed"],
                         id=r.get("id"))
            for r in records]
    replace = getattr(stream.store, "replace_all", None)
    if replace is not None:
        replace(stream.agent_id, recs)
    else:  # 后端不支持整体替换(如 Mem0):逐条写入,id 由后端重新分配
        for rec in recs:
            rec.id = None
            stream.store.add(rec)


def _restore_narrator(narrator, ndata: dict | None) -> None:
    if narrator is None or ndata is None:
        if ndata and narrator is None:
            logger.warning("存档含说书人前情,但新引擎未注入 narrator,跳过")
        return
    narrator.paras[:] = [dict(p) for p in ndata.get("paras", [])]
    narrator.summary = ndata.get("summary", narrator.summary)
    narrator.buffer[:] = list(ndata.get("buffer", []))
    narrator._last_write_s = float(ndata.get("last_write_s", 0.0))


# ───────────────────── 文件存取 ─────────────────────

def save_to_file(engine, path: str | Path) -> Path:
    """存档落盘(JSON,中文原样可读)。返回写入路径。"""
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(json.dumps(snapshot_engine(engine), ensure_ascii=False, indent=2),
                 encoding="utf-8")
    logger.info("💾 存档已写入 %s", p)
    return p


def load_from_file(engine, path: str | Path) -> dict:
    """从存档文件恢复到 engine(须为同参数新构造、未 start)。返回读到的快照。"""
    data = json.loads(Path(path).read_text(encoding="utf-8"))
    restore_engine(engine, data)
    return data
