"""
Agent Orchestrator for Mid Platform.
[AC-MARH-07] ReAct loop with iteration limit (3-5 iterations).

Supports two execution modes:
1. ReAct (Text-based): Traditional Thought/Action/Observation loop
2. Function Calling: Uses LLM's native function calling capability

ReAct Flow:
1. Thought: Agent thinks about what to do
2. Action: Agent decides to use a tool
3. Observation: Tool execution result
4. Repeat until final answer or max iterations reached
"""

import asyncio
import json
import logging
import re
import time
import uuid
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Any

from app.models.mid.schemas import (
    ExecutionMode,
    ReActContext,
    ToolCallStatus,
    ToolCallTrace,
    ToolType,
    TraceInfo,
)
from app.services.llm.base import ToolDefinition
from app.services.mid.tool_guide_registry import ToolGuideRegistry, get_tool_guide_registry
from app.services.mid.timeout_governor import TimeoutGovernor
from app.services.mid.tool_converter import convert_tools_to_llm_format, build_tool_result_message
from app.services.prompt.template_service import PromptTemplateService
from app.services.prompt.variable_resolver import VariableResolver

logger = logging.getLogger(__name__)

DEFAULT_MAX_ITERATIONS = 5
MIN_ITERATIONS = 3


class AgentMode(str, Enum):
    """Agent execution mode."""
    REACT = "react"
    FUNCTION_CALLING = "function_calling"


@dataclass
class ToolResult:
    """Tool execution result."""
    success: bool
    output: Any = None
    error: str | None = None
    duration_ms: int = 0


@dataclass
class AgentThought:
    """Agent thought in ReAct loop."""
    content: str
    action: str | None = None
    action_input: dict[str, Any] | None = None


class AgentOrchestrator:
    """
    [AC-MARH-07] Agent orchestrator with ReAct loop control.

    Features:
    - ReAct loop with max 5 iterations (min 3)
    - Function Calling mode for supported LLMs (OpenAI, DeepSeek, etc.)
    - Per-tool timeout (2s) and end-to-end timeout (8s)
    - Automatic fallback on iteration limit or timeout
    - Template-based prompt with variable injection
    """

    def __init__(
        self,
        max_iterations: int = DEFAULT_MAX_ITERATIONS,
        timeout_governor: TimeoutGovernor | None = None,
        llm_client: Any = None,
        tool_registry: Any = None,
        guide_registry: ToolGuideRegistry | None = None,
        template_service: PromptTemplateService | None = None,
        variable_resolver: VariableResolver | None = None,
        tenant_id: str | None = None,
        user_id: str | None = None,
        session_id: str | None = None,
        scene: str | None = None,
        mode: AgentMode = AgentMode.FUNCTION_CALLING,
    ):
        self._max_iterations = max(min(max_iterations, 5), MIN_ITERATIONS)
        self._timeout_governor = timeout_governor or TimeoutGovernor()
        self._llm_client = llm_client
        self._tool_registry = tool_registry
        self._guide_registry = guide_registry
        self._template_service = template_service
        self._variable_resolver = variable_resolver or VariableResolver()
        self._tenant_id = tenant_id
        self._user_id = user_id
        self._session_id = session_id
        self._scene = scene
        self._mode = mode
        self._tools_cache: list[ToolDefinition] | None = None

    def _get_tools_definition(self) -> list[ToolDefinition]:
        """Get cached tools definition for Function Calling."""
        if self._tools_cache is None and self._tool_registry:
            tools = self._tool_registry.get_all_tools()
            self._tools_cache = convert_tools_to_llm_format(tools)
        return self._tools_cache or []

    async def _get_tools_definition_async(self) -> list[ToolDefinition]:
        """Get tools definition for Function Calling with dynamic schema support."""
        if self._tools_cache is not None:
            return self._tools_cache
        
        if not self._tool_registry:
            return []
        
        tools = self._tool_registry.get_all_tools()
        result = []
        
        for tool in tools:
            if tool.name == "kb_search_dynamic" and self._tenant_id:
                from app.services.mid.kb_search_dynamic_tool import (
                    _TOOL_SCHEMA_CACHE,
                    _TOOL_SCHEMA_CACHE_TTL_SECONDS,
                )
                import time
                
                cache_key = f"tool_schema:{self._tenant_id}"
                current_time = time.time()
                
                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:
                        result.append(ToolDefinition(
                            name=cached_schema["name"],
                            description=cached_schema["description"],
                            parameters=cached_schema["parameters"],
                        ))
                        continue
                
                result.append(convert_tool_to_llm_format(tool))
            else:
                result.append(convert_tool_to_llm_format(tool))
        
        self._tools_cache = result
        return result

    async def execute(
        self,
        user_message: str,
        context: dict[str, Any] | None = None,
        on_thought: Any = None,
        on_action: Any = None,
    ) -> tuple[str, ReActContext, TraceInfo]:
        """
        [AC-MARH-07] Execute agent loop with iteration control.

        Args:
            user_message: User input message
            context: Execution context (history, retrieval results, etc.)
            on_thought: Callback for thought events
            on_action: Callback for action events

        Returns:
            Tuple of (final_answer, react_context, trace_info)
        """
        if self._mode == AgentMode.FUNCTION_CALLING:
            return await self._execute_function_calling(user_message, context, on_action)
        else:
            return await self._execute_react(user_message, context, on_thought, on_action)

    async def _execute_function_calling(
        self,
        user_message: str,
        context: dict[str, Any] | None = None,
        on_action: Any = None,
    ) -> tuple[str, ReActContext, TraceInfo]:
        """
        Execute using Function Calling mode.
        
        This mode uses the LLM's native function calling capability,
        which is more reliable and token-efficient than text-based ReAct.
        """
        react_ctx = ReActContext(max_iterations=self._max_iterations)
        tool_calls: list[ToolCallTrace] = []
        start_time = time.time()

        logger.info(
            f"[AC-MARH-07] Starting Function Calling loop: max_iterations={self._max_iterations}, "
            f"llm_client={self._llm_client is not None}, tool_registry={self._tool_registry is not None}"
        )

        if not self._llm_client:
            logger.error("[DEBUG-ORCH] LLM client is None, returning error response")
            return "抱歉，服务配置错误，请联系管理员。", react_ctx, TraceInfo(
                mode=ExecutionMode.AGENT,
                request_id=str(uuid.uuid4()),
                generation_id=str(uuid.uuid4()),
            )

        try:
            messages: list[dict[str, Any]] = [
                {"role": "system", "content": await self._build_system_prompt()},
                {"role": "user", "content": user_message},
            ]
            
            tools = await self._get_tools_definition_async()
            
            overall_start = time.time()
            end_to_end_timeout = self._timeout_governor.end_to_end_timeout_seconds
            llm_timeout = getattr(self._timeout_governor, 'llm_timeout_seconds', 15.0)

            while react_ctx.should_continue and react_ctx.iteration < react_ctx.max_iterations:
                react_ctx.iteration += 1

                elapsed = time.time() - overall_start
                remaining_time = end_to_end_timeout - elapsed
                if remaining_time <= 0:
                    logger.warning("[AC-MARH-09] Function Calling loop exceeded end-to-end timeout")
                    react_ctx.final_answer = "抱歉，处理超时，请稍后重试或联系人工客服。"
                    break

                logger.info(
                    f"[AC-MARH-07] Function Calling iteration {react_ctx.iteration}/"
                    f"{react_ctx.max_iterations}, remaining_time={remaining_time:.1f}s"
                )

                logger.info(
                    f"[DEBUG-ORCH] Calling LLM generate with messages_count={len(messages)}, "
                    f"tools_count={len(tools) if tools else 0}"
                )

                response = await asyncio.wait_for(
                    self._llm_client.generate(
                        messages=messages,
                        tools=tools if tools else None,
                        tool_choice="auto" if tools else None,
                    ),
                    timeout=min(llm_timeout, remaining_time)
                )

                logger.info(
                    f"[DEBUG-ORCH] LLM response received: has_tool_calls={response.has_tool_calls}, "
                    f"content_length={len(response.content) if response.content else 0}, "
                    f"tool_calls_count={len(response.tool_calls) if response.tool_calls else 0}"
                )

                if response.has_tool_calls:
                    for tool_call in response.tool_calls:
                        tool_name = tool_call.name
                        tool_args = tool_call.arguments
                        
                        logger.info(f"[AC-MARH-07] Tool call: {tool_name}, args={tool_args}")
                        
                        messages.append({
                            "role": "assistant",
                            "content": response.content,
                            "tool_calls": [tool_call.to_dict()],
                        })
                        
                        tool_result, tool_trace = await self._act_fc(
                            tool_call.id, tool_name, tool_args, react_ctx
                        )
                        tool_calls.append(tool_trace)
                        react_ctx.tool_calls.append(tool_trace)
                        
                        if on_action:
                            await on_action(tool_name, tool_result)
                        
                        called_tools = {tc.tool_name for tc in react_ctx.tool_calls[:-1]}
                        is_first_call = tool_name not in called_tools
                        
                        # Extract tool_guide from output if present (added by _act_fc)
                        result_output = tool_result.output if tool_result.success else {"error": tool_result.error}
                        tool_guide = None
                        if isinstance(result_output, dict) and "_tool_guide" in result_output:
                            result_output = dict(result_output)
                            tool_guide = result_output.pop("_tool_guide")
                        
                        messages.append(build_tool_result_message(
                            tool_call_id=tool_call.id,
                            tool_name=tool_name,
                            result=result_output,
                            tool_guide=tool_guide,
                        ))
                        
                        if not tool_result.success:
                            if tool_trace.status == ToolCallStatus.TIMEOUT:
                                react_ctx.final_answer = "抱歉，操作超时，请稍后重试或联系人工客服。"
                                react_ctx.should_continue = False
                                break
                else:
                    react_ctx.final_answer = response.content or "抱歉，我无法处理您的请求。"
                    react_ctx.should_continue = False
                    break

            if react_ctx.should_continue and not react_ctx.final_answer:
                logger.warning(f"[AC-MARH-07] Function Calling reached max iterations: {react_ctx.iteration}")
                react_ctx.final_answer = await self._force_final_answer_fc(messages)

        except asyncio.TimeoutError:
            logger.error("[AC-MARH-09] Function Calling loop timed out (end-to-end)")
            react_ctx.final_answer = "抱歉，处理超时，请稍后重试或联系人工客服。"
            tool_calls.append(ToolCallTrace(
                tool_name="fc_loop",
                tool_type=ToolType.INTERNAL,
                duration_ms=int((time.time() - start_time) * 1000),
                status=ToolCallStatus.TIMEOUT,
                error_code="E2E_TIMEOUT",
            ))
        except Exception as e:
            logger.error(f"[AC-MARH-07] Function Calling error: {e}", exc_info=True)
            react_ctx.final_answer = f"抱歉，处理过程中发生错误：{str(e)}"

        total_duration_ms = int((time.time() - start_time) * 1000)
        trace = TraceInfo(
            mode=ExecutionMode.AGENT,
            request_id=str(uuid.uuid4()),
            generation_id=str(uuid.uuid4()),
            react_iterations=react_ctx.iteration,
            tools_used=[tc.tool_name for tc in tool_calls if tc.tool_name not in ("fc_loop", "react_loop")],
            tool_calls=tool_calls if tool_calls else None,
        )

        logger.info(
            f"[AC-MARH-07] Function Calling completed: iterations={react_ctx.iteration}, "
            f"duration_ms={total_duration_ms}"
        )

        return react_ctx.final_answer or "抱歉，我暂时无法处理您的请求。", react_ctx, trace

    async def _act_fc(
        self,
        tool_call_id: str,
        tool_name: str,
        tool_args: dict[str, Any],
        react_ctx: ReActContext,
    ) -> tuple[ToolResult, ToolCallTrace]:
        """Execute tool in Function Calling mode."""
        start_time = time.time()

        if not self._tool_registry:
            duration_ms = int((time.time() - start_time) * 1000)
            return ToolResult(
                success=False,
                error="Tool registry not configured",
                duration_ms=duration_ms,
            ), ToolCallTrace(
                tool_name=tool_name,
                tool_type=ToolType.INTERNAL,
                duration_ms=duration_ms,
                status=ToolCallStatus.ERROR,
                error_code="NO_REGISTRY",
            )

        try:
            final_args = dict(tool_args)
            if self._tenant_id:
                final_args["tenant_id"] = self._tenant_id
            if self._user_id:
                final_args["user_id"] = self._user_id
            if self._session_id:
                final_args["session_id"] = self._session_id

            if tool_name == "kb_search_dynamic":
                # 确保 context 存在，供 AI 传入动态过滤条件
                if "context" not in final_args:
                    final_args["context"] = {}
                # scene 参数由 AI 从元数据中选择，系统不强制覆盖

            logger.info(
                f"[AC-MARH-07] FC Tool call starting: tool={tool_name}, "
                f"args={tool_args}, final_args={final_args}"
            )

            result = await asyncio.wait_for(
                self._tool_registry.execute(
                    name=tool_name,
                    **final_args,
                ),
                timeout=self._timeout_governor.per_tool_timeout_seconds
            )

            duration_ms = int((time.time() - start_time) * 1000)
            
            called_tools = {tc.tool_name for tc in react_ctx.tool_calls}
            is_first_call = tool_name not in called_tools
            
            output = result.output
            if is_first_call and result.success:
                usage_guide = self._build_tool_usage_guide(tool_name)
                if usage_guide:
                    if isinstance(output, dict):
                        output = dict(output)
                        output["_tool_guide"] = usage_guide
                    elif isinstance(output, str):
                        output = f"{output}\n\n---\n{usage_guide}"
                    else:
                        output = {"result": output, "_tool_guide": usage_guide}

            return ToolResult(
                success=result.success,
                output=output,
                error=result.error,
                duration_ms=duration_ms,
            ), ToolCallTrace(
                tool_name=tool_name,
                tool_type=ToolType.INTERNAL,
                duration_ms=duration_ms,
                status=ToolCallStatus.OK if result.success else ToolCallStatus.ERROR,
                args_digest=str(tool_args)[:100] if tool_args else None,
                result_digest=str(result.output)[:100] if result.output else None,
                arguments=tool_args,
                result=output,
            )

        except asyncio.TimeoutError:
            duration_ms = int((time.time() - start_time) * 1000)
            logger.warning(f"[AC-MARH-08] FC Tool timeout: {tool_name}, duration={duration_ms}ms")
            return ToolResult(
                success=False,
                error="Tool timeout",
                duration_ms=duration_ms,
            ), ToolCallTrace(
                tool_name=tool_name,
                tool_type=ToolType.INTERNAL,
                duration_ms=duration_ms,
                status=ToolCallStatus.TIMEOUT,
                error_code="TOOL_TIMEOUT",
                arguments=tool_args,
            )

        except Exception as e:
            duration_ms = int((time.time() - start_time) * 1000)
            logger.error(f"[AC-MARH-07] FC Tool error: {tool_name}, error={e}")
            return ToolResult(
                success=False,
                error=str(e),
                duration_ms=duration_ms,
            ), ToolCallTrace(
                tool_name=tool_name,
                tool_type=ToolType.INTERNAL,
                duration_ms=duration_ms,
                status=ToolCallStatus.ERROR,
                error_code="TOOL_ERROR",
                arguments=tool_args,
            )

    async def _build_system_prompt(self) -> str:
        """Build system prompt for Function Calling mode with template support."""
        default_prompt = """你是一个智能客服助手，正在处理用户请求。

## 决策协议

1. 优先使用已有观察信息，避免重复调用同类工具。
2. 当问题需要外部事实或结构化状态时再调用工具；如果可直接回答则不要调用。
3. 缺少关键参数时，优先向用户追问，不要使用空参数调用工具。
4. 工具失败时，先说明已尝试，再给出降级方案或下一步引导。
5. 对用户输出必须拟人、自然、有同理心，不暴露"工具调用/路由/策略"等内部术语。

## 知识库查询强制流程

当用户问题需要进行知识库查询（kb_search_dynamic）时，必须遵循以下步骤：

**步骤1：先调用 list_document_metadata_fields**
- 在任何知识库搜索之前，必须先调用 `list_document_metadata_fields` 工具
- 获取可用的元数据字段（如 grade, subject, kb_scene 等）及其常见取值

**步骤2：分析用户意图，选择合适的过滤条件**
- 根据用户问题和返回的元数据字段，确定合适的过滤条件
- 从元数据字段的 common_values 中选择合适的值

**步骤3：调用 kb_search_dynamic 进行搜索**
- 使用步骤1获取的元数据字段构造 context 参数
- scene 参数必须从元数据字段的 kb_scene 常见值中选择，不要硬编码

**示例流程：**
1. 调用 `list_document_metadata_fields` 获取字段信息
2. 根据返回结果，发现可用字段：grade（年级）、subject（学科）、kb_scene（场景）
3. 分析用户问题"三年级语文怎么学"，确定过滤条件：grade="三年级", subject="语文"
4. 从 kb_scene 的常见值中选择合适的 scene（如"学习方案"）
5. 调用 `kb_search_dynamic`，传入构造好的 context 和 scene

## 注意事项
- **严禁**在调用 kb_search_dynamic 之前不调用 list_document_metadata_fields。
"""

        if not self._template_service or not self._tenant_id:
            return default_prompt

        try:
            from app.core.database import get_session
            from app.core.prompts import SYSTEM_PROMPT
            
            async with get_session() as session:
                template_service = PromptTemplateService(session)
                
                base_prompt = await template_service.get_published_template(
                    tenant_id=self._tenant_id,
                    scene="agent_fc",
                    resolver=self._variable_resolver,
                )
                
                if not base_prompt or base_prompt == SYSTEM_PROMPT:
                    base_prompt = await template_service.get_published_template(
                        tenant_id=self._tenant_id,
                        scene="default",
                        resolver=self._variable_resolver,
                    )
                
                if not base_prompt or base_prompt == SYSTEM_PROMPT:
                    logger.info("[AC-MARH-07] No published template found for agent_fc or default, using default prompt")
                    return default_prompt

                agent_protocol = """

## 智能体决策协议

1. 优先使用已有观察信息，避免重复调用同类工具。
2. 当问题需要外部事实或结构化状态时再调用工具；如果可直接回答则不要调用。
3. 缺少关键参数时，优先向用户追问，不要使用空参数调用工具。
4. 工具失败时，先说明已尝试，再给出降级方案或下一步引导。

## 知识库查询强制流程

当用户问题需要进行知识库查询（kb_search_dynamic）时，必须遵循以下步骤：

**步骤1：先调用 list_document_metadata_fields**
- 在任何知识库搜索之前，必须先调用 `list_document_metadata_fields` 工具
- 获取可用的元数据字段（如 grade, subject, kb_scene 等）及其常见取值

**步骤2：分析用户意图，选择合适的过滤条件**
- 根据用户问题和返回的元数据字段，确定合适的过滤条件
- 从元数据字段的 common_values 中选择合适的值

**步骤3：调用 kb_search_dynamic 进行搜索**
- 使用步骤1获取的元数据字段构造 context 参数
- scene 参数必须从元数据字段的 kb_scene 常见值中选择，不要硬编码

## 注意事项
- **严禁**在调用 kb_search_dynamic 之前不调用 list_document_metadata_fields。
"""
                
                final_prompt = base_prompt + agent_protocol
                
                logger.info(f"[AC-MARH-07] Loaded template for tenant={self._tenant_id}")
                return final_prompt

        except Exception as e:
            logger.warning(f"[AC-MARH-07] Failed to load template, using default: {e}")
            return default_prompt

    async def _force_final_answer_fc(self, messages: list[dict[str, Any]]) -> str:
        """Force final answer when max iterations reached in Function Calling mode."""
        try:
            response = await self._llm_client.generate(
                messages=messages + [{"role": "user", "content": "请基于以上信息给出最终回答，不要再调用工具。"}],
                tools=None,
            )
            return response.content or "抱歉，我已经尽力处理您的请求，但可能需要更多信息。"
        except Exception as e:
            logger.error(f"[AC-MARH-07] Force final answer FC failed: {e}")
            return "抱歉，我已经尽力处理您的请求，但可能需要更多信息。请稍后重试或联系人工客服。"

    async def _execute_react(
        self,
        user_message: str,
        context: dict[str, Any] | None = None,
        on_thought: Any = None,
        on_action: Any = None,
    ) -> tuple[str, ReActContext, TraceInfo]:
        """
        Execute using traditional ReAct mode (text-based).
        
        This is the original implementation for backward compatibility.
        """
        react_ctx = ReActContext(max_iterations=self._max_iterations)
        tool_calls: list[ToolCallTrace] = []
        start_time = time.time()

        logger.info(
            f"[AC-MARH-07] Starting ReAct loop: max_iterations={self._max_iterations}"
        )

        try:
            overall_start = time.time()
            end_to_end_timeout = self._timeout_governor.end_to_end_timeout_seconds
            llm_timeout = (
                self._timeout_governor.llm_timeout_seconds
                if hasattr(self._timeout_governor, 'llm_timeout_seconds')
                else 15.0
            )

            while react_ctx.should_continue and react_ctx.iteration < react_ctx.max_iterations:
                react_ctx.iteration += 1

                elapsed = time.time() - overall_start
                remaining_time = end_to_end_timeout - elapsed
                if remaining_time <= 0:
                    logger.warning(
                        "[AC-MARH-09] ReAct loop exceeded end-to-end timeout"
                    )
                    react_ctx.final_answer = "抱歉，处理超时，请稍后重试或联系人工客服。"
                    break

                logger.info(
                    f"[AC-MARH-07] ReAct iteration {react_ctx.iteration}/"
                    f"{react_ctx.max_iterations}, remaining_time={remaining_time:.1f}s"
                )

                thought = await asyncio.wait_for(
                    self._think(user_message, context, react_ctx),
                    timeout=min(llm_timeout, remaining_time)
                )
                if on_thought:
                    await on_thought(thought)

                if not thought.action:
                    logger.info(
                        f"[AC-MARH-07] No action, setting final_answer: "
                        f"{thought.content[:200] if thought.content else 'None'}"
                    )
                    react_ctx.final_answer = thought.content
                    react_ctx.should_continue = False
                    break

                tool_result, tool_trace = await self._act(thought, react_ctx)
                tool_calls.append(tool_trace)
                react_ctx.tool_calls.append(tool_trace)

                if on_action:
                    await on_action(thought.action, tool_result)

                if tool_result.success:
                    context = context or {}
                    context["last_observation"] = tool_result.output
                else:
                    if tool_trace.status == ToolCallStatus.TIMEOUT:
                        logger.warning(f"[AC-MARH-08] Tool timeout: {thought.action}")
                        react_ctx.final_answer = "抱歉，操作超时，请稍后重试或联系人工客服。"
                        react_ctx.should_continue = False
                        break

            if react_ctx.should_continue and not react_ctx.final_answer:
                logger.warning(f"[AC-MARH-07] ReAct reached max iterations: {react_ctx.iteration}")
                react_ctx.final_answer = await self._force_final_answer(user_message, context, react_ctx)

        except asyncio.TimeoutError:
            logger.error("[AC-MARH-09] ReAct loop timed out (end-to-end)")
            react_ctx.final_answer = "抱歉，处理超时，请稍后重试或联系人工客服。"
            tool_calls.append(ToolCallTrace(
                tool_name="react_loop",
                tool_type=ToolType.INTERNAL,
                duration_ms=int((time.time() - start_time) * 1000),
                status=ToolCallStatus.TIMEOUT,
                error_code="E2E_TIMEOUT",
            ))

        total_duration_ms = int((time.time() - start_time) * 1000)
        trace = TraceInfo(
            mode=ExecutionMode.AGENT,
            request_id=str(uuid.uuid4()),
            generation_id=str(uuid.uuid4()),
            react_iterations=react_ctx.iteration,
            tools_used=[tc.tool_name for tc in tool_calls if tc.tool_name != "react_loop"],
            tool_calls=tool_calls if tool_calls else None,
        )

        logger.info(
            f"[AC-MARH-07] ReAct completed: iterations={react_ctx.iteration}, "
            f"duration_ms={total_duration_ms}"
        )

        return react_ctx.final_answer or "抱歉，我暂时无法处理您的请求。", react_ctx, trace

    async def _think(
        self,
        user_message: str,
        context: dict[str, Any] | None,
        react_ctx: ReActContext,
    ) -> AgentThought:
        """
        [AC-MARH-07] Agent thinks about next action.

        In real implementation, this would call LLM with ReAct prompt.
        For now, returns a simple thought without action.
        """
        if not self._llm_client:
            return AgentThought(content=f"思考中... 用户消息: {user_message}")

        try:
            observations = []
            if context and "last_observation" in context:
                observations.append(f"上一步结果: {context['last_observation']}")

            for tc in react_ctx.tool_calls[-3:]:
                observations.append(f"工具 {tc.tool_name}: {tc.result_digest or '无结果'}")

            prompt = await self._build_react_prompt(user_message, observations)
            response = await self._llm_client.generate([{"role": "user", "content": prompt}])

            logger.info(f"[AC-MARH-07] LLM response content: {response.content[:500] if response.content else 'None'}")

            return self._parse_thought(response.content)

        except Exception as e:
            logger.error(f"[AC-MARH-07] Think failed: {e}")
            return AgentThought(content=f"思考失败: {str(e)}")

    async def _build_react_prompt(self, user_message: str, observations: list[str]) -> str:
        """Build ReAct prompt for LLM with template support."""
        obs_text = "\n".join(observations) if observations else "无"
        tools_text = self._build_tools_section()

        internal_protocol = f"""你必须遵循以下决策协议：
1. 优先使用已有观察信息（历史观察、上一步工具结果），避免重复调用同类工具。
2. 当问题需要外部事实或结构化状态时再调用工具；如果可直接回答则不要调用。
3. 缺少关键参数时，优先向用户追问，不要使用空参数调用工具。
4. 工具失败时，先说明已尝试，再给出降级方案或下一步引导。
5. 只能调用可用工具列表中的工具，工具名必须完全匹配（区分大小写）。
6. tenant_id 由系统自动注入，绝不能由你填写、猜测或修改。
7. 对用户输出必须拟人、自然、有同理心，不暴露“工具调用/路由/策略”等内部术语。
"""

        output_contract = """输出格式（二选一）：
A) 直接回答用户：
Final Answer: [给用户的最终回答]

B) 调用工具：
Thought: [你的思考]
Action: [工具名称]
Action Input:
```json
{"param1": "value1"}
```

要求：
- Action Input 必须是合法 JSON 对象。
- 不要输出不存在的工具名。
- 如果要调用工具，Action 和 Action Input 必须同时出现。
"""

        default_template = f"""你是一个智能客服助手，正在使用 ReAct 模式处理用户请求。

{tools_text}

用户消息: {user_message}

历史观察:
{obs_text}

{internal_protocol}

{output_contract}
"""

        if not self._template_service or not self._tenant_id:
            return default_template

        try:
            template_version = await self._template_service.get_published_template(
                tenant_id=self._tenant_id,
                scene="agent_react",
            )

            if not template_version:
                return default_template

            extra_context = {
                "available_tools": tools_text,
                "query": user_message,
                "history": obs_text,
                "internal_protocol": internal_protocol,
                "output_contract": output_contract,
            }

            resolved_template = self._variable_resolver.resolve(
                template=template_version.system_instruction,
                variables=template_version.variables,
                extra_context=extra_context,
            )

            final_prompt = (
                f"{resolved_template}\n\n"
                f"【系统强制规则】\n{internal_protocol}\n"
                f"【输出契约】\n{output_contract}"
            )

            logger.info(f"[AC-MARH-07] Using template: scene=agent_react, version={template_version.version}")
            return final_prompt

        except Exception as e:
            logger.warning(f"[AC-MARH-07] Failed to load template, using default: {e}")
            return default_template
    
    def _build_tools_section(self) -> str:
        """
        Build compact tools section for ReAct prompt.
        
        Only includes tool name and brief description for initial scanning.
        Detailed usage guides are disclosed on-demand when tool is called.
        """
        if not self._tool_registry:
            return "当前没有可用的工具。"

        tools = self._tool_registry.get_all_tools()
        if not tools:
            return "当前没有可用的工具。"

        lines = ["## 可用工具列表", "", "以下是你可以使用的工具：", ""]

        for tool in tools:
            tool_guide = self._guide_registry.get_tool_guide(tool.name) if self._guide_registry else None
            description = tool_guide.description if tool_guide else tool.description
            lines.append(f"- **{tool.name}**: {description}")

        lines.append("")
        lines.append("调用工具时，系统会提供该工具的详细使用说明。")

        return "\n".join(lines)
    
    def _build_tool_usage_guide(self, tool_name: str) -> str:
        """
        Build detailed usage guide for a specific tool.
        Called when the tool is executed to provide on-demand guidance.
        """
        tool_guide = self._guide_registry.get_tool_guide(tool_name) if self._guide_registry else None
        tool = self._tool_registry.get_tool(tool_name) if self._tool_registry else None
        
        if not tool_guide and not tool:
            return ""
        
        lines = [f"## {tool_name} 使用说明", ""]
        
        if tool_guide:
            lines.append(f"**用途**: {tool_guide.description}")
            lines.append("")
            
            if tool_guide.triggers:
                lines.append("**适用场景**:")
                for trigger in tool_guide.triggers:
                    lines.append(f"- {trigger}")
                lines.append("")
            
            if tool_guide.anti_triggers:
                lines.append("**不适用场景**:")
                for anti in tool_guide.anti_triggers:
                    lines.append(f"- {anti}")
                lines.append("")
            
            if tool_guide.content:
                lines.append(tool_guide.content)
                lines.append("")
        
        if tool:
            meta = tool.metadata or {}
            params = meta.get("parameters")
            if isinstance(params, dict):
                properties = params.get("properties", {})
                required = params.get("required", [])
                if properties:
                    lines.append("**参数说明**:")
                    for param_name, param_info in properties.items():
                        param_desc = param_info.get("description", "") if isinstance(param_info, dict) else ""
                        req_mark = " (必填)" if param_name in required else ""
                        if param_name == "tenant_id":
                            req_mark = " (系统注入)"
                        lines.append(f"- `{param_name}`: {param_desc}{req_mark}")
                    lines.append("")

            if meta.get("example_action_input"):
                lines.append("**调用示例**:")
                try:
                    example_text = json.dumps(meta["example_action_input"], ensure_ascii=False, indent=2)
                except Exception:
                    example_text = str(meta["example_action_input"])
                lines.append(f"```json\n{example_text}\n```")
                lines.append("")

            if meta.get("result_interpretation"):
                lines.append(f"**结果说明**: {meta['result_interpretation']}")
                lines.append("")
        
        return "\n".join(lines)
    
    def _build_tools_guide_section(self, tool_names: list[str] | None = None) -> str:
        """
        Build detailed tools guide section for ReAct prompt.
        
        This provides comprehensive usage guides from ToolRegistry.
        Called separately from _build_tools_section for flexibility.
        
        Args:
            tool_names: If provided, only include tools for these names.
                       If None, include all tools.
        """
        return self._guide_registry.build_tools_prompt_section(tool_names)

    def _extract_json_object(self, text: str) -> dict[str, Any] | None:
        """Extract the first valid JSON object from free text."""
        candidates = []

        code_block_match = re.search(r"```json\s*([\s\S]*?)\s*```", text, re.IGNORECASE)
        if code_block_match:
            candidates.append(code_block_match.group(1).strip())

        fence_match = re.search(r"```\s*([\s\S]*?)\s*```", text)
        if fence_match:
            candidates.append(fence_match.group(1).strip())

        brace_match = re.search(r"\{[\s\S]*\}", text)
        if brace_match:
            candidates.append(brace_match.group(0).strip())

        for candidate in candidates:
            if not candidate:
                continue
            try:
                obj = json.loads(candidate)
                if isinstance(obj, dict):
                    return obj
            except json.JSONDecodeError:
                fixed = candidate.replace("'", '"')
                try:
                    obj = json.loads(fixed)
                    if isinstance(obj, dict):
                        return obj
                except json.JSONDecodeError:
                    continue

        return None

    def _parse_thought(self, content: str) -> AgentThought:
        """Parse LLM response into AgentThought with robust format handling."""
        action = None
        action_input: dict[str, Any] | None = None

        action_match = re.search(r"^Action:\s*(.+)$", content, re.MULTILINE)
        if action_match:
            action = action_match.group(1).strip()

        action_input_match = re.search(
            r"Action Input:\s*([\s\S]*)$",
            content,
            re.IGNORECASE,
        )
        if action_input_match:
            raw_input_text = action_input_match.group(1).strip()
            parsed = self._extract_json_object(raw_input_text)
            action_input = parsed if parsed is not None else {}

        if action and action_input is None:
            action_input = {}

        return AgentThought(content=content, action=action, action_input=action_input)

    async def _act(
        self,
        thought: AgentThought,
        react_ctx: ReActContext,
    ) -> tuple[ToolResult, ToolCallTrace]:
        """
        [AC-MARH-07, AC-MARH-08] Execute tool action with timeout.
        
        On first call to a tool, appends detailed usage guide to observation.
        """
        tool_name = thought.action or "unknown"
        start_time = time.time()

        if not self._tool_registry:
            duration_ms = int((time.time() - start_time) * 1000)
            return ToolResult(
                success=False,
                error="Tool registry not configured",
                duration_ms=duration_ms,
            ), ToolCallTrace(
                tool_name=tool_name,
                tool_type=ToolType.INTERNAL,
                duration_ms=duration_ms,
                status=ToolCallStatus.ERROR,
                error_code="NO_REGISTRY",
            )

        try:
            tool_args = dict(thought.action_input or {})
            if self._tenant_id:
                tool_args["tenant_id"] = self._tenant_id
            
            if self._user_id:
                tool_args["user_id"] = self._user_id
            
            if self._session_id:
                tool_args["session_id"] = self._session_id

            if tool_name == "kb_search_dynamic":
                # 确保 context 存在，供 AI 传入动态过滤条件
                if "context" not in tool_args:
                    tool_args["context"] = {}
                # scene 参数由 AI 从元数据中选择，系统不强制覆盖

            logger.info(
                f"[AC-MARH-07] Tool call starting: tool={tool_name}, "
                f"action_input={thought.action_input}, final_args={tool_args}"
            )

            result = await asyncio.wait_for(
                self._tool_registry.execute(
                    tool_name=tool_name,
                    args=tool_args,
                ),
                timeout=self._timeout_governor.per_tool_timeout_seconds
            )

            duration_ms = int((time.time() - start_time) * 1000)
            
            called_tools = {tc.tool_name for tc in react_ctx.tool_calls}
            is_first_call = tool_name not in called_tools
            
            output = result.output
            if is_first_call and result.success:
                usage_guide = self._build_tool_usage_guide(tool_name)
                if usage_guide:
                    if isinstance(output, dict):
                        output = dict(output)
                        output["_tool_guide"] = usage_guide
                    elif isinstance(output, str):
                        output = f"{output}\n\n---\n{usage_guide}"
                    else:
                        output = {"result": output, "_tool_guide": usage_guide}

            return ToolResult(
                success=result.success,
                output=output,
                error=result.error,
                duration_ms=duration_ms,
            ), ToolCallTrace(
                tool_name=tool_name,
                tool_type=ToolType.INTERNAL,
                duration_ms=duration_ms,
                status=ToolCallStatus.OK if result.success else ToolCallStatus.ERROR,
                args_digest=str(thought.action_input)[:100] if thought.action_input else None,
                result_digest=str(result.output)[:100] if result.output else None,
                arguments=thought.action_input,
                result=output,
            )

        except asyncio.TimeoutError:
            duration_ms = int((time.time() - start_time) * 1000)
            logger.warning(f"[AC-MARH-08] Tool timeout: {tool_name}, duration={duration_ms}ms")
            return ToolResult(
                success=False,
                error="Tool timeout",
                duration_ms=duration_ms,
            ), ToolCallTrace(
                tool_name=tool_name,
                tool_type=ToolType.INTERNAL,
                duration_ms=duration_ms,
                status=ToolCallStatus.TIMEOUT,
                error_code="TOOL_TIMEOUT",
                arguments=thought.action_input,
            )

        except Exception as e:
            duration_ms = int((time.time() - start_time) * 1000)
            logger.error(f"[AC-MARH-07] Tool error: {tool_name}, error={e}")
            return ToolResult(
                success=False,
                error=str(e),
                duration_ms=duration_ms,
            ), ToolCallTrace(
                tool_name=tool_name,
                tool_type=ToolType.INTERNAL,
                duration_ms=duration_ms,
                status=ToolCallStatus.ERROR,
                error_code="TOOL_ERROR",
                arguments=thought.action_input,
            )

    async def _force_final_answer(
        self,
        user_message: str,
        context: dict[str, Any] | None,
        react_ctx: ReActContext,
    ) -> str:
        """Force final answer when max iterations reached."""
        observations = []
        for tc in react_ctx.tool_calls:
            if tc.result_digest:
                observations.append(f"- {tc.tool_name}: {tc.result_digest}")

        obs_text = "\n".join(observations) if observations else "无"

        if self._llm_client:
            try:
                prompt = f"""基于以下信息，请给出最终回答:

用户消息: {user_message}

收集到的信息:
{obs_text}

请直接给出回答，不要再调用工具。"""
                response = await self._llm_client.generate([{"role": "user", "content": prompt}])
                return response.content
            except Exception as e:
                logger.error(f"[AC-MARH-07] Force final answer failed: {e}")

        return "抱歉，我已经尽力处理您的请求，但可能需要更多信息。请稍后重试或联系人工客服。"
