"""VidaReflectionEngine — 周期性自我反思与记忆整合 operator

**背景**: 从 sage-agentic#6 迁移（层违规修正）。ReflectionEngine 需要
neuromem(L4) + LLM backend，必须在 sage-vida (L4) 实现。

**功能**: 定时（默认每 30 分钟）从情节记忆提取 top-K 条目，通过 LLM 总结为
高阶洞察并写入语义记忆（知识图谱）。

**触发方式**:

1. **时间间隔触发**: 调用 ``start()`` 启动后台定时协程，每 ``interval_seconds``
   执行一次 ``reflect()``。
2. **工作记忆写满触发**: 外部检测到工作记忆达到容量上限时，主动调用 ``reflect()``
   即可。
3. **手动触发**: 任何时候直接 ``await engine.reflect()``。

**核心数据流**::

    VidaMemoryBridge.recall_episodic(query, top_k)
        → 组装反思 prompt
        → LLM 生成高阶洞察
        → VidaMemoryBridge.store_semantic(insight, metadata)

**LLM 协议**: 引擎通过 ``LLMProtocol`` 调用 LLM，仅要求对象实现
``async generate(prompt: str) -> str`` 方法，与具体实现解耦。
可传入 ``sagellm`` 客户端、``AsyncReActLoop`` 中的模型对象或任意 mock。

**典型用法**::

    from sage.vida import VidaMemoryBridge, VidaReflectionEngine

    bridge = VidaMemoryBridge(config={"data_dir": "/tmp/vida"})

    class MyLLM:
        async def generate(self, prompt: str) -> str:
            # 调用真实 LLM 接口
            return await my_llm_call(prompt)

    engine = VidaReflectionEngine(
        memory_bridge=bridge,
        llm=MyLLM(),
        config={
            "interval_seconds": 1800,   # 30 分钟
            "top_k_episodes": 10,       # 每次提取情节数
            "reflection_query": "",     # 情节检索查询词（空=最近）
        },
    )

    # 手动触发一次反思
    insights = await engine.reflect()

    # 启动后台定时反思（返回后台任务）
    await engine.start()

    # … 业务逻辑 …

    # 停止后台任务
    await engine.stop()
"""

from __future__ import annotations

import asyncio
import logging
import time
from typing import Any, Protocol, runtime_checkable

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# LLM 协议 — 与具体实现解耦
# ---------------------------------------------------------------------------

_DEFAULT_REFLECTION_PROMPT_TMPL = """\
你是一个智能助手，正在分析最近的情节记忆，总结出高阶洞察与规律。

请阅读以下情节记录（按时间顺序，最多 {top_k} 条）：

{episodes}

请完成以下任务：
1. 提炼 2-4 条关键洞察（高阶规律、重复模式、值得记忆的事实）。
2. 每条洞察用一句完整的中文陈述句表达。
3. 仅输出洞察本身，每条独占一行，行首用「- 」开头，不要其他多余内容。

洞察：
"""


@runtime_checkable
class LLMProtocol(Protocol):
    """最小 LLM 调用协议。

    只要对象实现 ``async generate(prompt: str) -> str``，均可作为
    ReflectionEngine 的 LLM 后端。
    """

    async def generate(self, prompt: str) -> str:  # noqa: D102
        ...


# ---------------------------------------------------------------------------
# ReflectionEngine
# ---------------------------------------------------------------------------


class VidaReflectionEngine:
    """周期性自我反思与记忆整合 operator。

    从情节记忆中提取 top-K 条记录，经 LLM 提炼为高阶洞察，写入语义记忆。

    属性:
        memory_bridge: VidaMemoryBridge 实例，提供三层记忆接入。
        llm:           LLMProtocol 兼容对象（实现 async generate）。
        interval_seconds: 后台定时反思间隔（秒），默认 1800（30 分钟）。
        top_k_episodes:   每次提取的情节条目数，默认 10。
        reflection_query: 检索情节记忆使用的查询词，默认空字符串（返回最近记录）。
        prompt_template:  反思 prompt 模板，含 {top_k} 和 {episodes} 占位符。

    Args:
        memory_bridge: 已初始化的 VidaMemoryBridge 实例。
        llm:           实现 ``LLMProtocol`` 的 LLM 对象。
        config:        可选配置字典，支持以下键：

            - ``interval_seconds`` (int): 定时间隔（秒），默认 1800。
            - ``top_k_episodes``   (int): 每次拉取情节数，默认 10。
            - ``reflection_query`` (str): 情节检索词，默认 ``""``。
            - ``prompt_template``  (str): 自定义反思 prompt 模板。

    示例::

        engine = VidaReflectionEngine(bridge, llm, config={"interval_seconds": 60})
        insights = await engine.reflect()
        print(insights)  # ["- 用户偏好简短回答", "- 常见话题是天气与出行"]
    """

    def __init__(
        self,
        memory_bridge: Any,  # VidaMemoryBridge — 避免循环导入用 Any
        llm: LLMProtocol,
        config: dict[str, Any] | None = None,
    ) -> None:
        """初始化 VidaReflectionEngine。

        Args:
            memory_bridge: VidaMemoryBridge 实例。
            llm:           LLMProtocol 兼容 LLM 对象。
            config:        参见类文档中的 ``config`` 参数说明。
        """
        cfg = config or {}
        self.memory_bridge = memory_bridge
        self.llm = llm
        self.interval_seconds: int = int(cfg.get("interval_seconds", 1800))
        self.top_k_episodes: int = int(cfg.get("top_k_episodes", 10))
        self.reflection_query: str = str(cfg.get("reflection_query", ""))
        self.prompt_template: str = cfg.get("prompt_template", _DEFAULT_REFLECTION_PROMPT_TMPL)

        self._task: asyncio.Task[None] | None = None
        self._running: bool = False
        self._last_reflect_ts: float = 0.0

    # ------------------------------------------------------------------
    # 核心：单次反思
    # ------------------------------------------------------------------

    async def reflect(self) -> list[str]:
        """执行一次自我反思：检索情节 → LLM 总结 → 写入语义记忆。

        返回:
            本次生成的洞察列表（每项为一条字符串）。
            若情节记忆为空或 LLM 返回空内容，返回空列表。

        副作用:
            将每条洞察通过 ``memory_bridge.store_semantic()`` 写入语义记忆，
            metadata 包含 ``source="reflection"`` 及生成时间戳。
        """
        logger.info("VidaReflectionEngine: 开始执行反思 (top_k=%d)", self.top_k_episodes)

        # 1. 从情节记忆提取 top-K 条目
        episodes: list[dict[str, Any]] = await self.memory_bridge.recall_episodic(
            query=self.reflection_query,
            top_k=self.top_k_episodes,
        )

        if not episodes:
            logger.info("VidaReflectionEngine: 情节记忆为空，跳过本次反思")
            return []

        # 2. 组装反思 prompt
        episodes_text = self._format_episodes(episodes)
        prompt = self.prompt_template.format(
            top_k=len(episodes),
            episodes=episodes_text,
        )

        # 3. LLM 生成高阶洞察
        try:
            raw_output: str = await self.llm.generate(prompt)
        except Exception:  # 宽泛捕获，防止 LLM 故障中断 agent 流程
            logger.exception("VidaReflectionEngine: LLM 调用失败，跳过本次反思")
            return []

        # 4. 解析洞察列表
        insights = self._parse_insights(raw_output)

        if not insights:
            logger.warning("VidaReflectionEngine: LLM 未生成任何洞察 (raw=%r)", raw_output[:200])
            return []

        # 5. 将洞察写入语义记忆
        ts = time.time()
        for insight in insights:
            await self.memory_bridge.store_semantic(
                insight,
                metadata={
                    "source": "reflection",
                    "timestamp": ts,
                    "episode_count": len(episodes),
                },
            )

        self._last_reflect_ts = ts
        logger.info(
            "VidaReflectionEngine: 反思完成，生成 %d 条洞察，已写入语义记忆",
            len(insights),
        )
        return insights

    # ------------------------------------------------------------------
    # 后台定时任务
    # ------------------------------------------------------------------

    async def start(self) -> asyncio.Task[None]:
        """启动后台定时反思任务。

        每 ``interval_seconds`` 秒触发一次 ``reflect()``。
        重复调用时，若任务已在运行则直接返回现有任务。

        Returns:
            asyncio.Task: 后台反思任务对象。
        """
        if self._task is not None and not self._task.done():
            logger.warning("VidaReflectionEngine: 后台任务已在运行，忽略重复 start()")
            return self._task

        self._running = True
        self._task = asyncio.create_task(self._reflection_loop(), name="vida_reflection_engine")
        logger.info(
            "VidaReflectionEngine: 后台定时反思已启动 (interval=%ds)",
            self.interval_seconds,
        )
        return self._task

    async def stop(self) -> None:
        """停止后台定时反思任务（graceful shutdown）。

        等待当前 ``reflect()`` 完成后再取消，不强制中断 LLM 调用。
        """
        self._running = False
        if self._task is not None and not self._task.done():
            self._task.cancel()
            try:
                await asyncio.shield(asyncio.gather(self._task, return_exceptions=True))
            except asyncio.CancelledError:
                pass
            self._task = None
        logger.info("VidaReflectionEngine: 后台任务已停止")

    async def _reflection_loop(self) -> None:
        """内部后台定时循环，不对外暴露。"""
        while self._running:
            try:
                await self.reflect()
            except asyncio.CancelledError:
                break
            except Exception:
                logger.exception("VidaReflectionEngine: 反思循环出现未预期异常，继续运行")

            # 等待下一轮，支持中途取消
            try:
                await asyncio.sleep(self.interval_seconds)
            except asyncio.CancelledError:
                break

    # ------------------------------------------------------------------
    # 属性查询
    # ------------------------------------------------------------------

    @property
    def is_running(self) -> bool:
        """后台任务是否正在运行。"""
        return self._task is not None and not self._task.done()

    @property
    def last_reflect_timestamp(self) -> float:
        """最近一次成功反思的 Unix 时间戳（0.0 表示从未执行过）。"""
        return self._last_reflect_ts

    # ------------------------------------------------------------------
    # 内部辅助方法
    # ------------------------------------------------------------------

    @staticmethod
    def _format_episodes(episodes: list[dict[str, Any]]) -> str:
        """将情节列表格式化为 prompt 中的可读文本块。

        Args:
            episodes: recall_episodic 返回的结果列表，每项包含 ``text`` 字段。

        Returns:
            多行文本字符串，每行为「序号. 情节文本」。
        """
        lines: list[str] = []
        for i, ep in enumerate(episodes, start=1):
            text: str = ep.get("text") or ep.get("content") or str(ep)
            lines.append(f"{i}. {text.strip()}")
        return "\n".join(lines)

    @staticmethod
    def _parse_insights(raw: str) -> list[str]:
        """从 LLM 输出中解析洞察列表。

        解析策略（按优先级）：
        1. 提取以 ``- `` 开头的行（模板指定格式）。
        2. 若无 ``-`` 行，则按非空行分割，每行作为一条洞察。

        Args:
            raw: LLM 生成的原始文本。

        Returns:
            洞察字符串列表（已去除首尾空白）。
        """
        if not raw or not raw.strip():
            return []

        lines = [line.strip() for line in raw.splitlines() if line.strip()]

        # 优先提取「- 」开头的行
        bullet_lines = [
            line.lstrip("-").lstrip("•").strip()
            for line in lines
            if line.startswith(("- ", "• ", "-\t"))
        ]
        if bullet_lines:
            return [ln for ln in bullet_lines if ln]

        # 回退：所有非空行均视为洞察
        return [ln for ln in lines if ln]
