# -*- coding: utf-8 -*-
"""
生命周期协调器
在主 Agent 生命周期的关键节点（PreLLM/PostLLM/PreTool/PostTool）统一调度
SkillLoader / MCPClientManager / HookRunner 三模块。

设计原则：
- 不抽象"扩展"概念，仅持三实例 + 暴露 4 个事件方法
- 不持有 Session，仅通过参数传入——避免长期引用 + 多线程问题
- 子 Agent 不接入 host：通过 create_subagent_registry() 已天然隔离
- skill 关键词匹配只对当前用户输入做，已注入的 skill 不重复注入（避免 system prompt 膨胀）
"""
import json
from pathlib import Path
from typing import Any, Dict, List, Optional

from ..skills import SkillLoader, create_default_loader
from ..mcp import MCPClientManager, create_default_manager
from ..hooks import HookRunner, create_default_runner
from ..knowledge import KnowledgeLoader, create_default_loader as create_default_knowledge_loader
from ..knowledge.curator import KnowledgeCurator
from ..tools.registry import ToolRegistry
from ..extension_context import ExtensionContext, MCPToolProxy
from .. import logger


class LifecycleHost:
    """持 SkillLoader + MCPClientManager + HookRunner，统一暴露生命周期事件。

    使用方式：
        host = LifecycleHost.create_default(working_dir)
        host.start()
        host.attach_mcp_tools(REGISTRY)
        ... 主循环 ...
        host.shutdown()
    """

    def __init__(self, skills: SkillLoader, mcp: MCPClientManager,
                 hooks: HookRunner, working_dir: str,
                 knowledge: Optional[KnowledgeLoader] = None,
                 knowledge_curator: Optional[KnowledgeCurator] = None):
        self._skills = skills
        self._mcp = mcp
        self._hooks = hooks
        self._working_dir = working_dir
        # 已注入到 session system prompt 的 skill 名集合（去重，避免膨胀）
        self._injected_skills: set = set()
        # MCP 扩展上下文（通过门面隔离注册边界）
        self._mcp_ctx: Optional[ExtensionContext] = None
        # 知识库加载器（可为 None，如禁用知识库）
        self._knowledge: Optional[KnowledgeLoader] = knowledge
        # 已注入到 session system prompt 的知识名集合（去重，避免索引膨胀）
        self._injected_knowledge: set = set()
        # 知识库生命周期管理者（可为 None）
        self._knowledge_curator: Optional[KnowledgeCurator] = knowledge_curator

    @classmethod
    def create_default(cls, working_dir: str) -> "LifecycleHost":
        """工厂：从 ~/.abyss/ 加载三模块默认配置"""
        knowledge_dirs = {
            "global": Path.home() / ".abyss" / "knowledge",
            "project": Path(working_dir) / ".knowledge",
        }
        return cls(
            skills=create_default_loader(),
            mcp=create_default_manager(),
            hooks=create_default_runner(),
            working_dir=working_dir,
            knowledge=create_default_knowledge_loader(working_dir),
            knowledge_curator=KnowledgeCurator(knowledge_dirs),
        )

    def start(self) -> None:
        """启动：扫描 skills、加载知识库、启动 MCP 子进程。失败仅告警不抛。"""
        try:
            self._skills.discover()
        except Exception as e:
            logger.error(f"SkillLoader discover 失败: {e}")
        if self._knowledge is not None:
            try:
                self._knowledge.discover()
            except Exception as e:
                logger.error(f"KnowledgeLoader discover 失败: {e}")
        try:
            self._mcp.start_all()
        except Exception as e:
            logger.error(f"MCP start_all 失败: {e}")

    def attach_mcp_tools(self, registry: ToolRegistry) -> int:
        """通过 ExtensionContext 把 MCP 工具注册到主 ToolRegistry。
        返回注册数量。幂等：重复调用先销毁旧 context 再重建。
        """
        # 先清理旧的 MCP context
        if self._mcp_ctx is not None:
            self._mcp_ctx.unregister_all()
        self._mcp_ctx = ExtensionContext(registry, source="mcp")

        count = 0
        for schema in self._mcp.get_openai_schemas():
            func = schema.get("function", {})
            full_name = func.get("name", "")
            if not full_name or full_name in registry:
                continue
            proxy = MCPToolProxy(
                full_name=full_name,
                description=func.get("description", ""),
                input_schema=func.get("parameters", {"type": "object", "properties": {}}),
                mcp=self._mcp,
            )
            if self._mcp_ctx.register_tool(proxy) is not None:
                count += 1
        return count

    def pre_llm(self, session, user_input: str) -> None:
        """关键词匹配 skill，命中且未注入过则追加到 session system 消息；
        随后追加知识库索引注入（仅索引，非完整内容）。

        preference 类知识在第一次 pre_llm 时无条件注入并常驻（不依赖关键词匹配），
        因为用户名字、偏好、常用语言等基础画像信息被问到时必须立刻可答。
        其他类知识（bugfix/experience/tool）仍走关键词匹配。
        """
        matched = self._skills.match_by_keyword(user_input, limit=3)
        if matched:
            new_skills = [s for s in matched if s.name not in self._injected_skills]
            if new_skills:
                self._inject_skills(session, new_skills)
        # 知识库索引注入
        if self._knowledge:
            # preference 类知识：第一次无条件注入并常驻（不依赖关键词匹配）
            self._inject_preferences(session)
            # 其他类知识：按关键词匹配注入（按类型控制数量，避免索引膨胀）
            matched_k = self._knowledge.match_by_keyword(user_input, limit=8)
            cat_limits = {"preference": 0, "bugfix": 2, "experience": 3, "tool": 1}
            cat_counts: Dict[str, int] = {}
            filtered = []
            for k in matched_k:
                cat = k.category or "experience"
                if cat_counts.get(cat, 0) < cat_limits.get(cat, 3):
                    filtered.append(k)
                    cat_counts[cat] = cat_counts.get(cat, 0) + 1
            new_k = [k for k in filtered if k.name not in self._injected_knowledge]
            if new_k:
                index_lines = self._knowledge.build_index_lines(new_k)
                if index_lines:
                    session.append_to_system(
                        "\n\n## 相关知识索引\n"
                        "以下是可能相关的知识条目（仅索引，非完整内容）。"
                        "如需详细知识，调用 knowledge_fetch(name) 拉取完整正文：\n"
                        + index_lines
                    )
                    for k in new_k:
                        self._injected_knowledge.add(k.name)

    def _inject_preferences(self, session) -> None:
        """第一次调用时无条件注入所有 preference 类知识到 session system prompt 并常驻。

        preference 类（用户名字、偏好、常用语言、说话方式）是基础用户画像，
        必须常驻 system prompt，这样用户问"我是谁/我喜欢什么"时 Agent 能立刻回答，
        无需关键词匹配触发，也无需 knowledge_fetch 工具调用。
        """
        if not self._knowledge:
            return
        new_prefs = [
            k for k in self._knowledge.list_all()
            if k.category == "preference" and k.name not in self._injected_knowledge
        ]
        if not new_prefs:
            return
        index_lines = self._knowledge.build_index_lines(new_prefs)
        if index_lines:
            session.append_to_system(
                "\n\n## 用户画像（常驻知识）\n"
                "以下是已知的用户偏好信息，回答相关问题时直接参考，无需调用工具：\n"
                + index_lines
            )
            for k in new_prefs:
                self._injected_knowledge.add(k.name)

    def inject_skill_by_name(self, session, name: str) -> bool:
        """按名手动注入 skill 到 session。返回是否成功。"""
        skill = self._skills.get(name)
        if skill is None or skill.name in self._injected_skills:
            return False
        self._inject_skills(session, [skill])
        return True

    def _inject_skills(self, session, skills: list) -> None:
        """内部：把 skills 列表注入 session system prompt 并记录去重。"""
        ext = self._skills.build_system_prompt_extension(skills)
        session.append_to_system("\n\n" + ext)
        for s in skills:
            self._injected_skills.add(s.name)

    def get_knowledge(self, name: str):
        """按名获取知识条目，供 knowledge_fetch 工具调用。找不到返回 None。"""
        if self._knowledge is None:
            return None
        return self._knowledge.get(name)

    def bump_knowledge_use(self, name: str) -> None:
        """记录知识使用次数，供 knowledge_fetch 工具调用。"""
        if self._knowledge_curator:
            self._knowledge_curator.bump_use(name)

    def pre_tool(self, name: str, args: dict) -> Dict[str, Any]:
        """运行 PreToolUse 钩子。返回 {block: bool, reason: str}。"""
        ctx = {
            "tool_name": name,
            "args_json": json.dumps(args, ensure_ascii=False),
            "working_dir": self._working_dir,
        }
        results = self._hooks.run("PreToolUse", ctx)
        for r in results:
            if r.block:
                return {"block": True, "reason": f"hook {r.name} 阻断: {r.stderr}"}
        return {"block": False}

    def post_tool(self, name: str, args: dict, result: dict) -> None:
        """运行 PostToolUse 钩子。结果不回 LLM，仅 side effect（日志/审计）。"""
        ctx = {
            "tool_name": name,
            "args_json": json.dumps(args, ensure_ascii=False),
            "success": str(result.get("success", False)),
            "working_dir": self._working_dir,
        }
        self._hooks.run("PostToolUse", ctx)

    def post_llm(self, content: str, tool_calls: list) -> None:
        """运行 PostLLMResponse 钩子。"""
        ctx = {
            "content_len": str(len(content or "")),
            "tool_calls_count": str(len(tool_calls or [])),
            "working_dir": self._working_dir,
        }
        self._hooks.run("PostLLMResponse", ctx)

    def shutdown(self) -> None:
        """停止所有 MCP 子进程。幂等。"""
        try:
            self._mcp.stop_all()
        except Exception as e:
            logger.error(f"MCP stop_all 失败: {e}")
