"""
KB Search Dynamic Tool for Mid Platform.
[AC-MARH-05] Agent 默认 KB 检索工具，支持元数据驱动参数。
[AC-MARH-06] KB 失败时可观测降级。

核心特性：
- 通过元数据配置动态生成检索参数/过滤器
- 必填字段缺失时返回 missing_required_slots
- 工具执行可观测（tool_call/tool_result）
- 超时降级返回 fallback_reason_code
"""

from __future__ import annotations

import asyncio
import logging
import time
import uuid
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Literal

from sqlalchemy.ext.asyncio import AsyncSession

from app.models.mid.schemas import ToolCallStatus, ToolCallTrace, ToolType
from app.services.mid.metadata_filter_builder import (
    FilterBuildResult,
    FilterFieldInfo,
    MetadataFilterBuilder,
)
from app.services.mid.slot_state_aggregator import (
    SlotState,
    SlotStateAggregator,
)
from app.services.mid.timeout_governor import TimeoutGovernor

if TYPE_CHECKING:
    from app.services.mid.tool_registry import ToolRegistry

logger = logging.getLogger(__name__)

DEFAULT_TOP_K = 5
DEFAULT_TIMEOUT_MS = 2000
KB_SEARCH_DYNAMIC_TOOL_NAME = "kb_search_dynamic"

_TOOL_SCHEMA_CACHE: dict[str, tuple[float, dict[str, Any]]] = {}
_TOOL_SCHEMA_CACHE_TTL_SECONDS = 300  # 5 minutes


@dataclass
class KbSearchDynamicResult:
    """KB 动态检索结果。"""
    success: bool = True
    hits: list[dict[str, Any]] = field(default_factory=list)
    applied_filter: dict[str, Any] = field(default_factory=dict)
    missing_required_slots: list[dict[str, str]] = field(default_factory=list)
    filter_debug: dict[str, Any] = field(default_factory=dict)
    filter_sources: dict[str, str] = field(default_factory=dict)  # [AC-SCENE-SLOT-02] 过滤条件来源
    fallback_reason_code: str | None = None
    duration_ms: int = 0
    tool_trace: ToolCallTrace | None = None
    step_kb_binding: dict[str, Any] | None = None  # [Step-KB-Binding] 步骤知识库绑定信息


@dataclass
class StepKbConfig:
    """[Step-KB-Binding] 步骤级别的知识库配置。"""
    allowed_kb_ids: list[str] | None = None
    preferred_kb_ids: list[str] | None = None
    kb_query_hint: str | None = None
    max_kb_calls: int = 1
    step_id: str | None = None


@dataclass
class KbSearchDynamicConfig:
    """KB 动态检索配置。"""
    enabled: bool = True
    top_k: int = DEFAULT_TOP_K
    timeout_ms: int = DEFAULT_TIMEOUT_MS
    min_score_threshold: float = 0.5


class KbSearchDynamicTool:
    """
    [AC-MARH-05] KB 动态检索工具。

    支持通过元数据配置动态生成检索参数/过滤器，而不是固定入参写死。

    固定外壳入参：
    - query: 检索查询
    - scene: 场景标识
    - tenant_id: 租户 ID
    - top_k: 返回数量
    - context: 上下文（包含动态过滤值）

    返回结构：
    - hits: 检索结果
    - applied_filter: 已应用的过滤条件
    - missing_required_slots: 缺失的必填字段
    - filter_debug: 过滤器调试信息
    - fallback_reason_code: 降级原因码
    """

    def __init__(
        self,
        session: AsyncSession,
        timeout_governor: TimeoutGovernor | None = None,
        config: KbSearchDynamicConfig | None = None,
        slot_state_aggregator: SlotStateAggregator | None = None,
    ):
        self._session = session
        self._timeout_governor = timeout_governor or TimeoutGovernor()
        self._config = config or KbSearchDynamicConfig()
        self._vector_retriever = None
        self._filter_builder: MetadataFilterBuilder | None = None
        self._slot_state_aggregator = slot_state_aggregator

    @property
    def name(self) -> str:
        """工具名称。"""
        return KB_SEARCH_DYNAMIC_TOOL_NAME

    @property
    def description(self) -> str:
        """工具描述。"""
        return (
            "知识库动态检索工具。"
            "根据租户配置的元数据字段定义，动态构建检索过滤器。"
            "支持必填字段检测和可观测降级。"
        )

    def get_tool_schema(self) -> dict[str, Any]:
        """
        获取工具 Schema，用于 Agent 工具描述。
        动态生成基于租户配置的过滤字段。
        """
        return {
            "name": self.name,
            "description": self.description,
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "检索查询文本",
                    },
                    "scene": {
                        "type": "string",
                        "description": "场景标识，如 'open_consult', 'intent_match'",
                    },
                    "top_k": {
                        "type": "integer",
                        "description": "返回结果数量",
                        "default": DEFAULT_TOP_K,
                    },
                    "context": {
                        "type": "object",
                        "description": "上下文信息，包含动态过滤字段值",
                    },
                },
                "required": ["query"],
            },
        }

    async def get_dynamic_tool_schema(self, tenant_id: str) -> dict[str, Any]:
        """
        获取动态生成的工具 Schema，包含租户的元数据过滤字段。
        
        使用缓存机制，避免每次都查询数据库。
        只显示关联知识库的元数据过滤字段（field_roles 包含 resource_filter）。
        
        Args:
            tenant_id: 租户 ID
            
        Returns:
            动态生成的工具 Schema
        """
        import time
        current_time = time.time()
        
        cache_key = f"tool_schema:{tenant_id}"
        if cache_key in _TOOL_SCHEMA_CACHE:
            cached_time, cached_schema = _TOOL_SCHEMA_CACHE[cache_key]
            if current_time - cached_time < _TOOL_SCHEMA_CACHE_TTL_SECONDS:
                logger.debug(f"[AC-MARH-05] Tool schema cache hit for tenant={tenant_id}")
                return cached_schema
        
        logger.info(f"[AC-MARH-05] Building dynamic tool schema for tenant={tenant_id}")
        
        base_properties = {
            "query": {
                "type": "string",
                "description": "检索查询文本",
            },
            "top_k": {
                "type": "integer",
                "description": "返回结果数量",
                "default": DEFAULT_TOP_K,
            },
        }
        
        required_fields = ["query"]
        context_properties = {}
        
        try:
            if self._filter_builder is None:
                self._filter_builder = MetadataFilterBuilder(self._session)
            
            filterable_fields = await self._filter_builder._get_filterable_fields(tenant_id)
            
            for field_info in filterable_fields:
                field_schema = self._build_field_schema(field_info)
                context_properties[field_info.field_key] = field_schema
                
                if field_info.required:
                    required_fields.append(field_info.field_key)
            
            logger.info(
                f"[AC-MARH-05] Dynamic schema built: tenant={tenant_id}, "
                f"context_fields={len(context_properties)}, required={required_fields}"
            )
        except Exception as e:
            logger.warning(f"[AC-MARH-05] Failed to get filterable fields: {e}, using base schema")
        
        if context_properties:
            base_properties["context"] = {
                "type": "object",
                "description": "过滤条件，根据用户意图选择合适的字段传递",
                "properties": context_properties,
            }
        else:
            base_properties["context"] = {
                "type": "object",
                "description": "过滤条件（当前租户未配置元数据字段）",
            }
        
        schema = {
            "name": self.name,
            "description": self.description,
            "parameters": {
                "type": "object",
                "properties": base_properties,
                "required": required_fields,
            },
        }
        
        _TOOL_SCHEMA_CACHE[cache_key] = (current_time, schema)
        
        return schema

    def _build_field_schema(self, field_info: "FilterFieldInfo") -> dict[str, Any]:
        """
        根据字段信息构建 JSON Schema。
        
        Args:
            field_info: 字段信息
            
        Returns:
            字段的 JSON Schema
        """
        schema: dict[str, Any] = {
            "description": field_info.label or field_info.field_key,
        }
        
        if field_info.required:
            schema["description"] += "（必填）"
        
        field_type = field_info.field_type.lower() if field_info.field_type else "string"
        
        if field_type in ("enum", "select", "array_enum", "multi_select"):
            schema["type"] = "string"
            if field_info.options:
                schema["enum"] = field_info.options
                schema["description"] += f"，可选值：{', '.join(field_info.options)}"
        elif field_type in ("number", "integer", "float"):
            schema["type"] = "number"
        elif field_type == "boolean":
            schema["type"] = "boolean"
        else:
            schema["type"] = "string"
        
        if field_info.default_value is not None:
            schema["default"] = field_info.default_value
        
        return schema

    async def execute(
        self,
        query: str,
        tenant_id: str,
        scene: str = "open_consult",
        top_k: int | None = None,
        context: dict[str, Any] | None = None,
        slot_state: SlotState | None = None,
        step_kb_config: StepKbConfig | None = None,
        slot_policy: Literal["flow_strict", "agent_relaxed"] = "flow_strict",
    ) -> KbSearchDynamicResult:
        """
        [AC-MARH-05] 执行 KB 动态检索。
        [AC-MRS-SLOT-META-02] 支持槽位状态聚合和过滤构建优先级
        [Step-KB-Binding] 支持步骤级别的知识库约束

        Args:
            query: 检索查询
            tenant_id: 租户 ID
            scene: 场景标识（默认值，会被 context 中的 scene 覆盖）
            top_k: 返回数量
            context: 上下文（包含动态过滤值，包括 scene）
            slot_state: 预聚合的槽位状态（可选，优先使用）
            step_kb_config: 步骤级别的知识库配置（可选）
            slot_policy: 槽位策略（flow_strict=流程严格模式，agent_relaxed=通用问答宽松模式）

        Returns:
            KbSearchDynamicResult 包含检索结果和追踪信息
        """
        if not self._config.enabled:
            logger.info(f"[AC-MARH-05] KB search dynamic disabled for tenant={tenant_id}")
            return KbSearchDynamicResult(
                success=False,
                fallback_reason_code="KB_DISABLED",
            )

        start_time = time.time()
        top_k = top_k or self._config.top_k

        effective_context = dict(context) if context else {}
        effective_scene = effective_context.get("scene", scene)

        # [Step-KB-Binding] 记录步骤知识库约束
        step_kb_binding_info: dict[str, Any] = {}
        if step_kb_config:
            step_kb_binding_info = {
                "step_id": step_kb_config.step_id,
                "allowed_kb_ids": step_kb_config.allowed_kb_ids,
                "preferred_kb_ids": step_kb_config.preferred_kb_ids,
                "kb_query_hint": step_kb_config.kb_query_hint,
                "max_kb_calls": step_kb_config.max_kb_calls,
            }
            logger.info(
                f"[Step-KB-Binding] Step KB config applied: "
                f"step_id={step_kb_config.step_id}, "
                f"allowed_kb_ids={step_kb_config.allowed_kb_ids}, "
                f"preferred_kb_ids={step_kb_config.preferred_kb_ids}"
            )

        logger.info(
            f"[AC-MARH-05] 开始执行KB动态检索: tenant={tenant_id}, "
            f"query={query[:50]}..., scene={effective_scene}, top_k={top_k}, "
            f"slot_policy={slot_policy}, context_keys={list(effective_context.keys())}"
        )

        # [AC-MRS-SLOT-META-02] 如果提供了 slot_state，优先使用
        if slot_state is not None:
            logger.info(
                f"[AC-MRS-SLOT-META-02] Using provided slot_state: "
                f"filled={len(slot_state.filled_slots)}, "
                f"missing={len(slot_state.missing_required_slots)}"
            )
            
            # 检查是否有缺失的必填槽位（仅在流程严格模式下阻断）
            if slot_state.missing_required_slots:
                if slot_policy == "flow_strict":
                    duration_ms = int((time.time() - start_time) * 1000)

                    logger.info(
                        f"[AC-MRS-SLOT-META-03] 流程严格模式命中缺失必填槽位，触发追问: "
                        f"tenant={tenant_id}, missing={len(slot_state.missing_required_slots)}"
                    )

                    tool_trace = ToolCallTrace(
                        tool_name=self.name,
                        tool_type=ToolType.INTERNAL,
                        duration_ms=duration_ms,
                        status=ToolCallStatus.ERROR,
                        error_code="MISSING_REQUIRED_SLOTS",
                        args_digest=f"query={query[:50]}, scene={effective_scene}",
                        result_digest=f"missing={len(slot_state.missing_required_slots)}",
                        arguments={"query": query, "scene": effective_scene, "context": context},
                        result={"missing_required_slots": slot_state.missing_required_slots},
                    )

                    return KbSearchDynamicResult(
                        success=False,
                        applied_filter={},
                        missing_required_slots=slot_state.missing_required_slots,
                        filter_debug={
                            "source": "slot_state",
                            "filled_slots": slot_state.filled_slots,
                            "slot_to_field_map": slot_state.slot_to_field_map,
                        },
                        fallback_reason_code="MISSING_REQUIRED_SLOTS",
                        duration_ms=duration_ms,
                        tool_trace=tool_trace,
                    )

                logger.info(
                    f"[AC-MRS-SLOT-META-03] 通用问答宽松模式检测到缺失槽位但不阻断检索: "
                    f"tenant={tenant_id}, missing={len(slot_state.missing_required_slots)}"
                )
            
            # 使用 slot_state 构建 filter
            metadata_filter, filter_sources = await self._build_filter_from_slot_state(
                tenant_id=tenant_id,
                slot_state=slot_state,
                context=effective_context,
                scene_slot_context=effective_context.get("scene_slot_context"),  # [AC-SCENE-SLOT-02]
            )
        else:
            # 原有逻辑：构建元数据 filter
            # 如果 context 简单（只有键值对），直接构造 filter，跳过 MetadataFilterBuilder
            metadata_filter = await self._build_filter_legacy(
                tenant_id=tenant_id,
                context=effective_context,
                query=query,
                effective_scene=effective_scene,
                start_time=start_time,
            )
            
            if isinstance(metadata_filter, KbSearchDynamicResult):
                # 有错误，直接返回
                return metadata_filter

        try:
            hits = await self._retrieve_with_timeout(
                tenant_id=tenant_id,
                query=query,
                metadata_filter=metadata_filter,
                top_k=top_k,
                step_kb_config=step_kb_config,
            )

            duration_ms = int((time.time() - start_time) * 1000)
            kb_hit = len(hits) > 0

            # [Step-KB-Binding] 记录命中的知识库
            hit_kb_ids = list(set(hit.get("kb_id") for hit in hits if hit.get("kb_id")))
            if step_kb_binding_info:
                step_kb_binding_info["used_kb_ids"] = hit_kb_ids
                step_kb_binding_info["kb_hit"] = kb_hit

            tool_trace = ToolCallTrace(
                tool_name=self.name,
                tool_type=ToolType.INTERNAL,
                duration_ms=duration_ms,
                status=ToolCallStatus.OK,
                args_digest=f"query={query[:50]}, scene={effective_scene}",
                result_digest=f"hits={len(hits)}",
                arguments={"query": query, "scene": effective_scene, "context": context},
                result={"hits_count": len(hits), "kb_hit": kb_hit},
            )

            logger.info(
                f"[AC-MARH-05] KB dynamic search completed: tenant={tenant_id}, "
                f"hits={len(hits)}, duration_ms={duration_ms}, kb_hit={kb_hit}, "
                f"hit_kb_ids={hit_kb_ids}"
            )

            # 确定 filter 来源用于调试
            filter_source = "slot_state" if slot_state is not None else "builder"
            kb_filter_sources = filter_sources if slot_state is not None else {}
            
            return KbSearchDynamicResult(
                success=True,
                hits=hits,
                applied_filter=metadata_filter or {},
                filter_debug={"source": filter_source, "filter_sources": kb_filter_sources},
                duration_ms=duration_ms,
                tool_trace=tool_trace,
                step_kb_binding=step_kb_binding_info if step_kb_binding_info else None,
            )

        except asyncio.TimeoutError:
            duration_ms = int((time.time() - start_time) * 1000)
            logger.warning(
                f"[AC-MARH-06] KB dynamic search timeout: tenant={tenant_id}, "
                f"duration_ms={duration_ms}"
            )

            tool_trace = ToolCallTrace(
                tool_name=self.name,
                tool_type=ToolType.INTERNAL,
                duration_ms=duration_ms,
                status=ToolCallStatus.TIMEOUT,
                error_code="KB_TIMEOUT",
                arguments={"query": query, "scene": effective_scene, "context": context},
            )

            return KbSearchDynamicResult(
                success=False,
                applied_filter=metadata_filter or {},
                missing_required_slots=[],
                filter_debug={"error": "timeout"},
                fallback_reason_code="KB_TIMEOUT",
                duration_ms=duration_ms,
                tool_trace=tool_trace,
                step_kb_binding=step_kb_binding_info if step_kb_binding_info else None,
            )

        except Exception as e:
            duration_ms = int((time.time() - start_time) * 1000)
            logger.error(
                f"[AC-MARH-06] KB dynamic search failed: tenant={tenant_id}, "
                f"error={e}"
            )

            tool_trace = ToolCallTrace(
                tool_name=self.name,
                tool_type=ToolType.INTERNAL,
                duration_ms=duration_ms,
                status=ToolCallStatus.ERROR,
                error_code="KB_ERROR",
                arguments={"query": query, "scene": effective_scene, "context": context},
            )

            return KbSearchDynamicResult(
                success=False,
                applied_filter=metadata_filter or {},
                missing_required_slots=[],
                filter_debug={"error": str(e)},
                fallback_reason_code="KB_ERROR",
                duration_ms=duration_ms,
                tool_trace=tool_trace,
            )

    async def _build_filter_legacy(
        self,
        tenant_id: str,
        context: dict[str, Any],
        query: str,
        effective_scene: str,
        start_time: float,
    ) -> dict[str, Any] | KbSearchDynamicResult:
        """
        [AC-MRS-SLOT-META-02] 原有逻辑：构建元数据 filter
        
        Returns:
            dict: 构建成功的 filter
            KbSearchDynamicResult: 构建失败时的错误结果
        """
        metadata_filter: dict[str, Any] | None = None
        
        # 简单 context：直接构造 filter（信任 AI 传入的值）
        # 复杂场景：使用 MetadataFilterBuilder 进行严格验证
        is_simple_context = all(
            isinstance(v, (str, int, float, bool))
            for v in context.values()
        )
        
        if is_simple_context:
            # 直接构造 filter，不查询数据库
            metadata_filter = context
            logger.info(
                f"[AC-MARH-05] Using simple context as filter directly: "
                f"{metadata_filter}"
            )
        else:
            # 复杂 context，使用 MetadataFilterBuilder
            filter_result = await self._build_filter_with_builder(
                tenant_id=tenant_id,
                context=context,
                query=query,
                effective_scene=effective_scene,
                start_time=start_time,
            )
            if isinstance(filter_result, KbSearchDynamicResult):
                # 有错误，直接返回
                return filter_result
            metadata_filter = filter_result
        
        return metadata_filter or {}

    async def _build_filter_from_slot_state(
        self,
        tenant_id: str,
        slot_state: SlotState,
        context: dict[str, Any],
        scene_slot_context: Any = None,  # [AC-SCENE-SLOT-02] 场景槽位上下文
    ) -> tuple[dict[str, Any], dict[str, str]]:
        """
        [AC-MRS-SLOT-META-02] 基于槽位状态构建过滤条件
        [AC-SCENE-SLOT-02] 支持场景槽位包配置的优先级
        
        过滤值来源优先级：
        1. 已确认槽位值（slot_state.filled_slots）
        2. 当前请求 context 显式值
        3. 元数据默认值
        
        Args:
            tenant_id: 租户 ID
            slot_state: 槽位状态
            context: 上下文
            scene_slot_context: 场景槽位上下文
            
        Returns:
            (过滤条件字典, 过滤来源映射)
        """
        if self._filter_builder is None:
            self._filter_builder = MetadataFilterBuilder(self._session)
        
        # 获取可过滤字段定义
        filterable_fields = await self._filter_builder._get_filterable_fields(tenant_id)
        
        applied_filter: dict[str, Any] = {}
        filter_debug_sources: dict[str, str] = {}
        
        # [AC-SCENE-SLOT-02] 如果有场景槽位上下文，优先处理场景定义的槽位
        scene_slot_keys = set()
        if scene_slot_context:
            scene_slot_keys = set(scene_slot_context.get_all_slot_keys())
            logger.debug(
                f"[AC-SCENE-SLOT-02] Processing scene slots: "
                f"scene={scene_slot_context.scene_key}, slots={scene_slot_keys}"
            )
        
        for field_info in filterable_fields:
            field_key = field_info.field_key
            value = None
            source = None
            
            # 优先级 1: 已确认槽位值（通过 slot_to_field_map 映射）
            if slot_state.slot_to_field_map:
                # 查找哪个 slot 映射到这个 field
                for slot_key, mapped_field_key in slot_state.slot_to_field_map.items():
                    if mapped_field_key == field_key and slot_key in slot_state.filled_slots:
                        value = slot_state.filled_slots[slot_key]
                        source = "slot"
                        break
            
            # 如果 slot 映射没有命中，直接检查 slot_key 是否等于 field_key
            if value is None and field_key in slot_state.filled_slots:
                value = slot_state.filled_slots[field_key]
                source = "slot"
            
            # 优先级 2: 当前请求 context 显式值
            if value is None and field_key in context:
                value = context[field_key]
                source = "context"
            
            # 优先级 3: 元数据默认值
            if value is None and field_info.default_value is not None:
                value = field_info.default_value
                source = "default"
            
            # 构建过滤条件
            if value is not None:
                filter_value = self._filter_builder._build_field_filter(
                    field_info, value
                )
                if filter_value is not None:
                    applied_filter[field_key] = filter_value
                    filter_debug_sources[field_key] = source
        
        logger.info(
            f"[AC-MRS-SLOT-META-02] Filter built from slot_state: "
            f"fields={len(applied_filter)}, sources={filter_debug_sources}"
        )
        
        return applied_filter, filter_debug_sources

    async def _build_filter_with_builder(
        self,
        tenant_id: str,
        context: dict[str, Any],
        query: str,
        effective_scene: str,
        start_time: float,
    ) -> dict[str, Any] | KbSearchDynamicResult:
        """
        使用 MetadataFilterBuilder 构建 filter（复杂场景）。
        
        Returns:
            dict: 构建成功的 filter
            KbSearchDynamicResult: 构建失败时的错误结果
        """
        from app.services.mid.metadata_filter_builder import FilterBuildResult
        
        if self._filter_builder is None:
            self._filter_builder = MetadataFilterBuilder(self._session)

        filter_result = await self._filter_builder.build_filter(
            tenant_id=tenant_id,
            context=context,
        )

        if filter_result.missing_required_slots:
            logger.warning(
                f"[AC-MARH-05] Missing required slots: "
                f"{filter_result.missing_required_slots}"
            )
            duration_ms = int((time.time() - start_time) * 1000)

            tool_trace = ToolCallTrace(
                tool_name=self.name,
                tool_type=ToolType.INTERNAL,
                duration_ms=duration_ms,
                status=ToolCallStatus.ERROR,
                error_code="MISSING_REQUIRED_SLOTS",
                args_digest=f"query={query[:50]}, scene={effective_scene}",
                result_digest=f"missing={len(filter_result.missing_required_slots)}",
                arguments={"query": query, "scene": effective_scene, "context": context},
                result={"missing_required_slots": filter_result.missing_required_slots},
            )

            return KbSearchDynamicResult(
                success=False,
                applied_filter=filter_result.applied_filter,
                missing_required_slots=filter_result.missing_required_slots,
                filter_debug=filter_result.debug_info,
                fallback_reason_code="MISSING_REQUIRED_SLOTS",
                duration_ms=duration_ms,
                tool_trace=tool_trace,
            )

        return filter_result.applied_filter if filter_result.success else {}

    async def _retrieve_with_timeout(
        self,
        tenant_id: str,
        query: str,
        metadata_filter: dict[str, Any] | None = None,
        top_k: int = DEFAULT_TOP_K,
        step_kb_config: StepKbConfig | None = None,
    ) -> list[dict[str, Any]]:
        """带超时控制的检索。"""
        timeout_seconds = self._config.timeout_ms / 1000.0

        try:
            return await asyncio.wait_for(
                self._do_retrieve(tenant_id, query, metadata_filter, top_k, step_kb_config),
                timeout=timeout_seconds,
            )
        except asyncio.TimeoutError:
            raise asyncio.TimeoutError("KB dynamic search timeout")

    async def _do_retrieve(
        self,
        tenant_id: str,
        query: str,
        metadata_filter: dict[str, Any] | None = None,
        top_k: int = DEFAULT_TOP_K,
        step_kb_config: StepKbConfig | None = None,
    ) -> list[dict[str, Any]]:
        """执行实际检索。[Step-KB-Binding] 支持步骤级别的知识库约束。"""
        if self._vector_retriever is None:
            from app.services.retrieval.vector_retriever import get_vector_retriever
            self._vector_retriever = await get_vector_retriever()

        from app.services.retrieval.base import RetrievalContext

        # [Step-KB-Binding] 确定要检索的知识库范围
        kb_ids = None
        if step_kb_config:
            # 如果配置了 allowed_kb_ids，则只检索这些知识库
            if step_kb_config.allowed_kb_ids:
                kb_ids = step_kb_config.allowed_kb_ids
                logger.info(
                    f"[Step-KB-Binding] Restricting KB search to: {kb_ids}"
                )
            # 如果只配置了 preferred_kb_ids，优先检索这些知识库
            elif step_kb_config.preferred_kb_ids:
                kb_ids = step_kb_config.preferred_kb_ids
                logger.info(
                    f"[Step-KB-Binding] Preferring KB search in: {kb_ids}"
                )

        ctx = RetrievalContext(
            tenant_id=tenant_id,
            query=query,
            metadata_filter=metadata_filter,
            kb_ids=kb_ids,
        )

        result = await self._vector_retriever.retrieve(ctx)

        hits = []
        for hit in result.hits:
            if hit.score >= self._config.min_score_threshold:
                hits.append({
                    "id": hit.metadata.get("chunk_id", str(uuid.uuid4())),
                    "content": hit.text,
                    "score": hit.score,
                    "metadata": hit.metadata,
                    "kb_id": hit.metadata.get("kb_id"),
                })

        return hits[:top_k]

    def get_config(self) -> KbSearchDynamicConfig:
        """获取当前配置。"""
        return self._config


async def create_kb_search_dynamic_handler(
    session: AsyncSession,
    timeout_governor: TimeoutGovernor | None = None,
    config: KbSearchDynamicConfig | None = None,
) -> callable:
    """
    创建 kb_search_dynamic 工具的 handler 函数，用于注册到 ToolRegistry。

    Args:
        session: 数据库会话
        timeout_governor: 超时治理器
        config: 工具配置

    Returns:
        异步 handler 函数
    """
    tool = KbSearchDynamicTool(
        session=session,
        timeout_governor=timeout_governor,
        config=config,
    )

    async def handler(
        query: str,
        tenant_id: str = "",
        scene: str = "open_consult",
        top_k: int = DEFAULT_TOP_K,
        context: dict[str, Any] | None = None,
        **kwargs,  # 接受系统注入的额外参数（user_id, session_id 等）
    ) -> dict[str, Any]:
        """
        KB 动态检索 handler。

        Args:
            query: 检索查询
            tenant_id: 租户 ID
            scene: 场景标识（默认值，会被 context 中的 scene 覆盖）
            top_k: 返回数量
            context: 上下文（包含 scene 等过滤字段）

        Returns:
            检索结果字典
        """
        result = await tool.execute(
            query=query,
            tenant_id=tenant_id,
            scene=scene,
            top_k=top_k,
            context=context,
        )

        return {
            "success": result.success,
            "hits": result.hits,
            "applied_filter": result.applied_filter,
            "missing_required_slots": result.missing_required_slots,
            "filter_debug": result.filter_debug,
            "fallback_reason_code": result.fallback_reason_code,
            "duration_ms": result.duration_ms,
        }

    return handler


def register_kb_search_dynamic_tool(
    registry: ToolRegistry,
    session: AsyncSession,
    timeout_governor: TimeoutGovernor | None = None,
    config: KbSearchDynamicConfig | None = None,
) -> None:
    """
    [AC-MARH-05] 将 kb_search_dynamic 注册到 ToolRegistry。

    Args:
        registry: ToolRegistry 实例
        session: 数据库会话
        timeout_governor: 超时治理器
        config: 工具配置
    """
    from app.services.mid.tool_registry import ToolType as RegistryToolType

    async def handler(
        query: str,
        tenant_id: str = "",
        scene: str = "open_consult",
        top_k: int = DEFAULT_TOP_K,
        context: dict[str, Any] | None = None,
        **kwargs,  # 接受系统注入的额外参数（user_id, session_id 等）
    ) -> dict[str, Any]:
        tool = KbSearchDynamicTool(
            session=session,
            timeout_governor=timeout_governor,
            config=config,
        )

        result = await tool.execute(
            query=query,
            tenant_id=tenant_id,
            scene=scene,
            top_k=top_k,
            context=context,
        )

        return {
            "success": result.success,
            "hits": result.hits,
            "applied_filter": result.applied_filter,
            "missing_required_slots": result.missing_required_slots,
            "filter_debug": result.filter_debug,
            "fallback_reason_code": result.fallback_reason_code,
            "duration_ms": result.duration_ms,
        }

    registry.register(
        name=KB_SEARCH_DYNAMIC_TOOL_NAME,
        description="知识库动态检索工具，支持元数据驱动过滤",
        handler=handler,
        tool_type=RegistryToolType.INTERNAL,
        version="1.0.0",
        auth_required=False,
        timeout_ms=config.timeout_ms if config else DEFAULT_TIMEOUT_MS,
        enabled=True,
        metadata={
            "supports_dynamic_filter": True,
            "min_score_threshold": config.min_score_threshold if config else 0.5,
            "when_to_use": "当需要知识库事实支撑回答，且需按租户元数据动态过滤时使用。",
            "when_not_to_use": "当用户问题不依赖知识库（纯闲聊/仅流程确认）或已有充分 KB 结果时不重复调用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "检索查询文本"},
                    "tenant_id": {"type": "string", "description": "租户 ID"},
                    "scene": {"type": "string", "description": "场景标识，如 open_consult"},
                    "top_k": {"type": "integer", "description": "返回条数"},
                    "context": {"type": "object", "description": "上下文，用于动态过滤字段"}
                },
                "required": ["query", "tenant_id"]
            },
            "example_action_input": {
                "query": "退款到账一般要多久",
                "tenant_id": "default",
                "scene": "open_consult",
                "top_k": 5,
                "context": {"product_line": "vip_course", "region": "beijing"}
            },
            "result_interpretation": "success=true 且 hits 非空表示命中知识；missing_required_slots 非空时应先向用户补采信息。"
        },
    )

    logger.info(
        f"[AC-MARH-05] Tool registered: {KB_SEARCH_DYNAMIC_TOOL_NAME}"
    )
