"""搜索 Agent 工具调度器 — 工具描述与 dispatch 分发。

实现 ``core/agent/protocols.ToolDispatcher`` Protocol。
连接 TreeEnvironment（数据）、summarizer（LLM 摘要）、
vision（VLM 观察）和 skills（策略加载）。

与 TRM4 ``core/tree/tools.py`` 的差异:
- 自由函数 ``dispatch()`` → ``SearchToolDispatcher`` 类（依赖注入）;
- 同步 → 全异步;
- view_node / search_similar 内部拆分为 env 数据读取 + summarizer LLM 摘要。
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from app.search.summarizer import summarize_children, summarize_node, summarize_nodes_batch
from app.search.vision import observe_frame
from app.tree.environment import _LEVEL_LABEL, TreeEnvironment, _node_level

if TYPE_CHECKING:
    from collections.abc import Callable
    from pathlib import Path

    import numpy as np

    from app.ports import OCRProvider
    from app.search.skills import SkillRegistry
    from core.protocols import LLMProvider, VLMProvider

# ── 工具描述文本（与 TRM4 core/tree/tools.py 完全一致） ─────────────────

_BASE_DESCRIPTIONS = """\
## 可用工具

在 action 中指定 tool 和 args 来调用工具。

### view_node
查看节点信息，获取与问题相关的内容摘要和子节点概览。
- args: {"node_id": "节点 ID", "question": "当前关注的具体问题"}

### search_similar
语义检索最相关的节点，返回与问题相关的内容摘要。
- args: {"query": "搜索关键词（2-4 词）", "question": "当前关注的具体问题", "k": 返回数量（可选，默认 5）}

### observe_frame
调用视觉模型查看关键帧图像，回答针对性的视觉问题。
- args: {"node_ids": ["L3 节点 ID 列表（1-4 个），或单个 L2 节点 ID"], "question": "针对帧内容的具体视觉问题"}

### submit_answer
提交最终答案。
- args: {"answer": "选项字母 A/B/C/D", "evidence": "关键证据摘要", "reasoning": "每个选项的判断理由"}"""

_SKILL_DESCRIPTION = """

### read_skill
加载指定题型技能的详细搜索策略。
- args: {"name": "技能名称"}"""


def get_tool_descriptions(include_read_skill: bool = False) -> str:
    """返回工具描述文本，用于写入 system prompt。

    参数:
        include_read_skill: 是否包含 read_skill 工具（manual 模式用）。

    返回:
        Markdown 格式的工具描述文本。
    """
    text = _BASE_DESCRIPTIONS
    if include_read_skill:
        text += _SKILL_DESCRIPTION
    return text


# ── SearchToolDispatcher ──────────────────────────────────────────────


class SearchToolDispatcher:
    """搜索 Agent 工具调度器，实现 ToolDispatcher Protocol。

    按工具名路由到对应私有处理方法。未知工具抛 ValueError
    （AgentLoop 捕获后不计步数）；节点不存在等运行时错误
    捕获后返回错误文本。

    参数:
        env: 视频树运行时环境（纯数据访问）。
        tool_llm: 摘要用 LLM 端口。
        vlm: 视觉模型端口。
        ocr: 帧文字转录端口（None 不启用）。
        prompts_dir: prompt 文件目录。
        skills: 技能注册表（None 不启用 read_skill）。
        embed_fn: 文本嵌入函数（search_similar 用）。
        verify_vision: observe_frame 是否执行验证轮。
        anchor: view_node 是否启用行号锚模式。
        assemble_mode: 锚模式装配形态（"ids"/"ids_expand"/"expand_only"）。
        stats_sink: 统计回调（None 不收集）。
    """

    def __init__(
        self,
        env: TreeEnvironment,
        tool_llm: LLMProvider,
        vlm: VLMProvider,
        ocr: OCRProvider | None,
        prompts_dir: Path,
        skills: SkillRegistry | None,
        *,
        embed_fn: Callable[[str | list[str]], np.ndarray],
        verify_vision: bool,
        anchor: bool,
        assemble_mode: str,
        stats_sink: Callable[[dict[str, Any]], None] | None = None,
    ) -> None:
        self._env = env
        self._tool_llm = tool_llm
        self._vlm = vlm
        self._ocr = ocr
        self._prompts_dir = prompts_dir
        self._skills = skills
        self._embed_fn = embed_fn
        self._verify_vision = verify_vision
        self._anchor = anchor
        self._assemble_mode = assemble_mode
        self._stats_sink = stats_sink

    # ── ToolDispatcher Protocol 实现 ──────────────────────────────────

    async def dispatch(
        self, tool_name: str, args: dict[str, Any], *, context: dict[str, Any]
    ) -> str:
        """按工具名分发到对应处理方法。

        参数:
            tool_name: 工具名称。
            args: 工具参数字典。
            context: 调用上下文（含 session_id、parent_call_id 等遥测字段）。

        返回:
            工具执行结果文本。

        异常:
            ValueError: 未知工具名——上抛给 AgentLoop，不计步数。
        """
        try:
            if tool_name == "view_node":
                return await self._handle_view_node(args, context)
            if tool_name == "search_similar":
                return await self._handle_search_similar(args, context)
            if tool_name == "observe_frame":
                return await self._handle_observe_frame(args, context)
            if tool_name == "submit_answer":
                return f"[ok] 答案已提交: {args['answer']}"
            if tool_name == "read_skill":
                return self._handle_read_skill(args)
        except (KeyError, FileNotFoundError) as e:
            return f"工具执行错误: {e}"

        raise ValueError(f"未知工具: {tool_name}")

    # ── 私有处理方法 ──────────────────────────────────────────────────

    async def _handle_view_node(self, args: dict[str, Any], context: dict[str, Any]) -> str:
        """view_node：节点摘要 + 子节点概览。

        参数:
            args: {"node_id": str, "question": str}。
            context: 遥测上下文。

        返回:
            "[节点] {id} | {level} | {time}\\n\\n{summary}\\n\\n[子节点概览] ..."
        """
        node_id: str = args["node_id"]
        question: str = args["question"]
        session_id = context.get("session_id")
        parent_call_id = context.get("parent_call_id")

        # Phase 1: 节点元数据（头部格式化）
        node = self._env._id_to_node[node_id]
        level = _node_level(node)
        level_label = _LEVEL_LABEL[level]
        time_str = TreeEnvironment._format_time_range(node)

        # Phase 2: 节点内容摘要
        raw_text, anchor_map = self._env.get_node_text(node_id, anchor=self._anchor)
        summary = await summarize_node(
            self._tool_llm,
            raw_text,
            question,
            self._prompts_dir,
            anchor_map=anchor_map,
            assemble_mode=self._assemble_mode,
            stats_sink=self._stats_sink,
            session_id=session_id,
            parent_call_id=parent_call_id,
        )

        parts: list[str] = [
            f"[节点] {node_id} | {level_label} | {time_str}",
            "",
            summary,
        ]

        # Phase 2.5: 确定性追加实体/画面文字原文（防按题摘要吞噬，Spec-1 B）
        for label, text in self._env.node_entity_fields(node_id).items():
            parts.append(f"[{label}] {text}")

        # Phase 3: 子节点概览
        children_info = self._env.get_children_info(node_id)
        if children_info:
            children_text = await summarize_children(
                self._tool_llm,
                children_info,
                question,
                self._prompts_dir,
                session_id=session_id,
                parent_call_id=parent_call_id,
            )
            parts.append(f"\n[子节点概览] {len(children_info)} 个子节点\n{children_text}")

        return "\n".join(parts)

    async def _handle_search_similar(self, args: dict[str, Any], context: dict[str, Any]) -> str:
        """search_similar：语义检索 + 批量摘要。

        参数:
            args: {"query": str, "question": str, "k": int (可选)}。
            context: 遥测上下文。

        返回:
            "[搜索结果] 查询 \\"{query}\\" → N 个相关节点\\n\\n1. ..."
        """
        query: str = args["query"]
        question: str = args["question"]
        top_k: int = args.get("k", 5)
        session_id = context.get("session_id")
        parent_call_id = context.get("parent_call_id")

        # Phase 1: 语义检索
        results = self._env.search_similar(query, top_k=top_k, embed_fn=self._embed_fn)

        if not results:
            return f'[搜索结果] 查询 "{query}" → 0 个相关节点'

        # Phase 2: 构建摘要输入
        items: list[tuple[str, str, str]] = []
        for nid, score in results:
            node = self._env._id_to_node[nid]
            raw_text, _ = self._env.get_node_text(nid)
            level = _node_level(node)
            time_str = TreeEnvironment._format_time_range(node)
            extra = f"{level}  score={score:.4f}  [{time_str}]"
            items.append((nid, raw_text, extra))

        # Phase 3: 并发批量摘要
        summaries = await summarize_nodes_batch(
            self._tool_llm,
            items,
            question,
            self._prompts_dir,
            session_id=session_id,
            parent_call_id=parent_call_id,
        )

        # Phase 4: 格式化输出
        lines: list[str] = []
        for i, (nid, summary_text) in enumerate(summaries):
            _, _, extra = items[i]
            lines.append(f"{i + 1}. {nid} | {extra}\n   {summary_text}")

        header = f'[搜索结果] 查询 "{query}" → {len(results)} 个相关节点'
        return header + "\n\n" + "\n\n".join(lines)

    async def _handle_observe_frame(self, args: dict[str, Any], context: dict[str, Any]) -> str:
        """observe_frame：VLM 帧观察 + 字幕前置。

        参数:
            args: {"node_ids": list[str], "question": str}。
            context: 遥测上下文。

        返回:
            "[字幕上下文] ...\\n[视觉观察] ..." 或 "[视觉观察] ..."
        """
        node_ids: list[str] = args["node_ids"]
        question: str = args.get("question", "")
        session_id = context.get("session_id")
        parent_call_id = context.get("parent_call_id")

        if not question.strip():
            return "工具执行错误: question 不能为空"

        # Phase 1: 解析帧路径和字幕
        frame_paths = self._env.resolve_frame_paths(node_ids)
        subtitle = self._env.get_subtitle(node_ids[0])

        # Phase 2: VLM 调用
        result = await observe_frame(
            self._vlm,
            frame_paths,
            question,
            self._prompts_dir,
            ocr=self._ocr,
            verify=self._verify_vision,
            stats_sink=self._stats_sink,
            session_id=session_id,
            parent_call_id=parent_call_id,
        )

        # Phase 3: 字幕前置拼接
        if subtitle:
            return f"[字幕上下文] {subtitle}\n{result}"
        return result

    def _handle_read_skill(self, args: dict[str, Any]) -> str:
        """read_skill：加载指定技能的搜索策略正文。

        参数:
            args: {"name": str}。

        返回:
            技能正文或错误提示。
        """
        if self._skills is None:
            return "错误: skills 未启用"
        return self._skills.read(args["name"])
