# -*- coding: utf-8 -*-
"""
Agent 工具执行循环（从 main.py 提取）

包含 execute_tool、execute_tool_batch、process_tool_calls，
是 LLM agent 的工具调用分派和执行循环。display/UI 逻辑留在 main.py 中。

设计借鉴 Hermes Agent 的 agent/tool_executor.py：
- 函数以显式参数接收依赖（registry、host、esc、engine），而非模块级全局
- 提取后 main.py 薄包装，加上 display 逻辑
"""

import json
import os
import inspect
import time
from typing import Any, Dict, List, Optional

from .tools.registry import (
    get_tc_field,
    merge_tool_calls,
    format_tool_result,
)
from .tools.tracking import ToolCallTracker, get_default_tracker
from .concurrency import AbortError, AbortSignal
from . import logger


# Phase 8 Tier 2 CG：文件历史事务集成
# 这些函数在 history 模块通过 threading.local 维护当前事务 id，
# 仅在 main.py 注入 global service 后才生效，未注入时静默跳过。
def _record_file_change_if_active(path: str) -> None:
    """若当前线程有活跃事务，记录文件原内容。

    file_edit/file_write/file_read 工具执行前调用。
    文件不存在时记录 None（表示新建）。
    """
    try:
        from .history import record_in_current_transaction
    except ImportError:
        return
    try:
        if path and os.path.exists(path):
            try:
                with open(path, "r", encoding="utf-8-sig") as f:
                    old_content = f.read()
            except (OSError, UnicodeDecodeError):
                # 二进制文件或读取失败，记录存在但内容为 None
                old_content = None
        else:
            old_content = None
        record_in_current_transaction(path, old_content)
    except Exception:
        # 记录失败不阻塞工具执行
        pass


# 需要记录原文件内容的工具名集合
_FILE_EDITING_TOOLS = {"file_edit", "file_write"}


class TaskInterrupted(Exception):
    """用户按 Esc 中断当前任务时抛出，主循环捕获后返回提示符。"""
    pass


class SelfCorrectionLoop:
    """工具失败自纠错循环。

    借鉴 zhikuncode-main 的 SelfCorrectionLoop，工具执行失败后生成
    结构化再提示供 LLM 修正参数，最多重试 max_attempts 次。
    成功后立即停止；达到上限后返回最后失败结果 + 耗尽提示。
    """

    def __init__(self, max_attempts: int = 3):
        """初始化，指定最大重试次数。"""
        self.max_attempts = max_attempts
        self.attempts = 0
        self._failure_history = []  # 记录每次失败的工具名+原因

    def should_retry(self, tool_call, result) -> bool:
        """判断是否应该重试：工具失败且未达上限。"""
        if self.attempts >= self.max_attempts:
            return False
        # 判断 result 是否表示失败
        if self._is_failure(result):
            self.attempts += 1
            self._record_failure(tool_call, result)
            return True
        return False

    def _is_failure(self, result) -> bool:
        """判断工具结果是否为失败。"""
        if isinstance(result, dict):
            return not result.get("success", True)
        return False

    def _record_failure(self, tool_call, result):
        """记录失败历史供 build_correction_prompt 使用。"""
        tool_name = get_tc_field(tool_call, "function", {})
        tool_name = get_tc_field(tool_name, "name", "unknown")
        error = result.get("error", "未知错误") if isinstance(result, dict) else str(result)
        self._failure_history.append({"tool": tool_name, "error": error})

    def build_correction_prompt(self, tool_call, result, attempt: int) -> str:
        """生成结构化再提示供 LLM 修正。"""
        func = get_tc_field(tool_call, "function", {})
        tool_name = get_tc_field(func, "name", "unknown")
        error = result.get("error", "未知错误") if isinstance(result, dict) else str(result)
        suggestion = self._get_suggestion(tool_name, error)
        return (f"[自纠错提示 第{attempt}次] 工具 {tool_name} 失败：{error}\n"
                f"建议：{suggestion}")

    def _get_suggestion(self, tool_name: str, error: str) -> str:
        """根据工具名和错误生成修正建议。"""
        if tool_name == "file_edit":
            return "先 file_read 确认文件内容，再调整 old_string 参数使其精确匹配"
        if tool_name == "shell_exec":
            return "检查命令语法，确认命令存在且参数正确"
        if tool_name == "file_write":
            return "检查路径是否有写入权限，确认父目录存在"
        return "根据错误信息调整参数后重试"

    def build_exhausted_message(self) -> str:
        """生成重试耗尽提示。"""
        return f"自纠错循环已耗尽 {self.max_attempts} 次重试，放弃修正"


def _extract_missing_param(exc: TypeError, func) -> str:
    """从 TypeError 消息中提取缺失的参数名。找不到时返回空串。

    Python 的 TypeError 形如：
      "FileEditTool.execute() missing 1 required positional argument: 'path'"
    对多余参数（unexpected keyword argument）也能识别。
    """
    msg = str(exc)
    if "missing 1 required positional argument:" in msg:
        try:
            return msg.split("argument: '", 1)[1].split("'", 1)[0]
        except IndexError:
            return ""
    if "missing" in msg and "required" in msg:
        try:
            sig = inspect.signature(func)
            required = [
                p.name for p in sig.parameters.values()
                if p.default is inspect.Parameter.empty
                and p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD,
                               inspect.Parameter.KEYWORD_ONLY)
            ]
            return ", ".join(required)
        except (ValueError, TypeError):
            return ""
    return ""


def execute_tool(
    tool_call,
    registry,
    *,
    host=None,
    esc=None,
    tracker=None,
    signal: Optional[AbortSignal] = None,
) -> str:
    """执行单个工具调用，返回给 AI 的结果 JSON 字符串。

    Args:
        tool_call: OpenAI 格式的 tool_call dict（含 function.name / function.arguments）
        registry: ToolRegistry 实例
        host: 可选 LifecycleHost（提供 pre_tool / post_tool 钩子）
        esc: 可选 EscInterrupt（提供取消检查）
        tracker: 可选 ToolCallTracker（记录调用历史用于终止决策）。
            默认 None 时使用模块级单例 get_default_tracker()。
        signal: 可选 AbortSignal（Phase 6 AM 引入，层级化中断信号）。
            若 controller 已被 abort，抛出 TaskInterrupted。与 esc 并行检查，
            支持主流程 abort 时级联取消子 Agent 工具调用。

    Returns:
        JSON 字符串，成功时含工具返回数据，失败时含 error 字段。
    """
    func = get_tc_field(tool_call, "function", {})
    name = get_tc_field(func, "name", "")
    args_str = get_tc_field(func, "arguments", "")
    try:
        arguments = json.loads(args_str) if args_str else {}
    except json.JSONDecodeError:
        # 参数解析失败也记录到 tracker（arguments 此时未定义，传空 dict）
        _record_failure(tracker, name, {}, f"参数解析失败: {args_str}", "JSONDecodeError")
        return json.dumps({"success": False, "error": f"参数解析失败: {args_str}"})

    if name not in registry:
        # 未知工具也记录到 tracker（预期终态，recovery_relevant=False）
        _record_failure(
            tracker, name, arguments, f"未知工具: {name}", "UnknownTool",
            recovery_relevant=False,
        )
        return json.dumps({"success": False, "error": f"未知工具: {name}"})

    # PreToolUse 钩子：可阻断工具调用
    if host is not None:
        guard = host.pre_tool(name, arguments)
        if guard["block"]:
            err = {"success": False, "error": f"被钩子阻断: {guard.get('reason', '')}"}
            # 钩子阻断是预期终态，recovery_relevant=False
            _record_failure(
                tracker, name, arguments, err.get("error", ""), "HookBlocked",
                recovery_relevant=False,
            )
            return format_tool_result(name, err)

    # Esc 中断检查点
    if esc is not None and esc.is_cancelled():
        raise TaskInterrupted()

    # AbortSignal 中断检查点（Phase 6 AM：层级化中断）
    # AbortError 转为 TaskInterrupted，保持调用方处理逻辑统一
    if signal is not None:
        try:
            signal.throw_if_aborted()
        except AbortError:
            raise TaskInterrupted()

    logger.tool_start(name, args=args_str)
    t0 = time.time()
    tool = registry.get(name)

    # Phase 8 Tier 2 CG：file_edit/file_write 工具执行前记录原内容
    # 供 /undo 命令批量回退。无活跃事务时静默跳过。
    if name in _FILE_EDITING_TOOLS:
        file_path = arguments.get("path", "")
        if file_path:
            _record_file_change_if_active(file_path)

    try:
        result = tool.execute(**arguments)
    except TypeError as e:
        missing = _extract_missing_param(e, tool.execute)
        hint = f"（缺失必要参数: {missing}）" if missing else ""
        result = {"success": False, "error": f"工具参数不合法: {e} {hint}"}
        _record_failure(
            tracker, name, arguments, result.get("error", ""), "TypeError",
        )
    except Exception as e:
        # 兜底捕获未预期异常，记录到 tracker
        result = {"success": False, "error": f"工具执行异常: {e}"}
        _record_failure(
            tracker, name, arguments, result.get("error", ""), type(e).__name__,
        )

    elapsed_ms = int((time.time() - t0) * 1000)
    logger.tool_end(name, success=result.get("success", False), duration_ms=elapsed_ms,
                    out_len=len(format_tool_result(name, result)))

    # 成功调用也记录到 tracker
    if result.get("success", False):
        _record_success(tracker, name, arguments)

    # PostToolUse 钩子
    if host is not None:
        host.post_tool(name, arguments, result)

    return format_tool_result(name, result)


def _record_success(tracker, tool_name: str, args: dict) -> None:
    """记录成功调用到 tracker（None 时用默认单例）。"""
    if tracker is None:
        tracker = get_default_tracker()
    tracker.record(tool_name, args, success=True)


def _record_failure(
    tracker,
    tool_name: str,
    args: dict,
    error: str,
    error_type: str,
    recovery_relevant: bool = True,
) -> None:
    """记录失败调用到 tracker（None 时用默认单例）。"""
    if tracker is None:
        tracker = get_default_tracker()
    tracker.record(
        tool_name=tool_name,
        args=args,
        success=False,
        error=error,
        error_type=error_type,
        recovery_relevant=recovery_relevant,
    )


def execute_tool_batch(
    round_num: int,
    tool_calls: list,
    registry,
    *,
    host=None,
    esc=None,
    tracker=None,
    signal: Optional[AbortSignal] = None,
) -> list:
    """执行一批工具调用。返回每个 tool_call 对应的 result JSON 字符串列表。

    Args:
        round_num: 当前轮次编号（仅用于日志上下文，不在此模块输出 UI）
        tool_calls: tool_call 列表
        registry: ToolRegistry 实例
        host: 可选 LifecycleHost
        esc: 可选 EscInterrupt
        tracker: 可选 ToolCallTracker（透传给 execute_tool）
        signal: 可选 AbortSignal（Phase 6 AM：层级化中断信号，透传给 execute_tool）
    """
    logger.batch_header(round_num, len(tool_calls), 100)
    results = []
    for tc in tool_calls:
        result = execute_tool(
            tc, registry, host=host, esc=esc, tracker=tracker, signal=signal
        )
        results.append(result)
    return results


# 工具调用往返轮数安全网（仅防 AI 死循环，正常任务不会达到）
MAX_TOOL_ROUNDS = 100


def process_tool_calls(
    session,
    client,
    tools_schemas,
    tool_calls,
    cfg,
    registry,
    *,
    host=None,
    esc=None,
    engine=None,
    stream_handler=None,
    show_reasoning=False,
    execute_tool_batch_fn=None,
    self_correction=None,
    tracker=None,
    signal: Optional[AbortSignal] = None,
) -> str:
    """执行工具调用并获取后续回复，支持多轮循环。

    首批 = 第 1 轮，for 循环从第 2 轮开始。

    Args:
        session: Session 实例
        client: API 客户端（需实现 chat(messages, tools, stream, model, ...) 方法）
        tools_schemas: OpenAI tool schema 列表
        tool_calls: 首批 tool_call 列表
        cfg: Config 实例
        registry: ToolRegistry 实例
        host: 可选 LifecycleHost
        esc: 可选 EscInterrupt
        engine: 可选 ContextEngine
        stream_handler: 可选的流处理回调，签名为
            (client, messages, tools, cfg, show_reasoning) -> (content, reasoning, tool_calls, usage)
            若为 None 则跳过 AI 调用，仅执行首批工具。
        show_reasoning: 是否显示推理过程
        execute_tool_batch_fn: 可选的带UI批次执行回调，签名为
            (round_num, tool_calls) -> list of result JSON strings
            若为 None 则使用默认无UI版本。
        self_correction: 可选的 SelfCorrectionLoop 实例。工具失败时
            注入结构化修正提示 user 消息，让 LLM 调整参数重试。
        tracker: 可选 ToolCallTracker。None 时使用模块级单例。
            用于检测连续失败 / 同错误重复，触发警告注入。
        signal: 可选 AbortSignal（Phase 6 AM：层级化中断信号，透传给 execute_tool_batch）。
            主流程 abort 时通过此信号级联取消工具调用。

    Returns:
        最终文本内容（content），为 "" 表示无文本响应或被中断。
    """
    if tracker is None:
        tracker = get_default_tracker()
    _execute_batch = execute_tool_batch_fn if execute_tool_batch_fn else (
        lambda rn, tcs: execute_tool_batch(
            rn, tcs, registry, host=host, esc=esc, tracker=tracker, signal=signal
        )
    )

    session.add_assistant(tool_calls=tool_calls)
    max_chars = cfg.get("tool_result_max_chars") or None

    # 第 1 轮：执行首批 tool_calls
    try:
        results = _execute_batch(1, tool_calls)
    except TaskInterrupted:
        # 工具执行被中断：回滚刚写入的 assistant(tool_calls) 消息，
        # 避免孤儿消息残留导致下一次 API 调用 400。
        session.rollback_last_assistant()
        return ""
    for tc, result in zip(tool_calls, results):
        session.add_tool_result(get_tc_field(tc, "id"), result, max_chars=max_chars)

    # 自纠错循环：首批工具失败时注入修正提示
    if self_correction is not None:
        _inject_correction_if_failed(session, tool_calls, results, self_correction)

    # ToolCallTracker：检测连续失败 / 同错误重复，注入警告
    _inject_tracker_warning_if_failed(session, tracker)

    if stream_handler is None:
        return ""

    for round_num in range(2, MAX_TOOL_ROUNDS + 1):
        try:
            content_buffer, reasoning_buffer, new_tool_calls, usage_info = stream_handler(
                client,
                session.get_messages_for_api(),
                tools_schemas,
                cfg,
                show_reasoning,
            )
        except TaskInterrupted:
            return ""
        except Exception as e:
            # 此前完全静默，导致工具轮次中的 API 异常无法排查。
            # 补日志：main.py 的 _stream_handler 已捕获并打印用户可见错误，
            # 这里兜底记录未预期的异常路径。
            logger.api_error(f"stream_handler round {round_num}: {e}")
            return ""

        # 更新上下文引擎
        if engine is not None and usage_info:
            if hasattr(usage_info, 'model_dump'):
                engine.update_from_response(usage_info.model_dump())
            elif isinstance(usage_info, dict):
                engine.update_from_response(usage_info)

        # PostLLMResponse 钩子
        if host is not None:
            host.post_llm(content_buffer, new_tool_calls)

        if not content_buffer and not new_tool_calls:
            return ""

        if new_tool_calls:
            merged_new = merge_tool_calls(new_tool_calls)
            session.add_assistant(
                tool_calls=merged_new,
                reasoning_content=reasoning_buffer if reasoning_buffer else None
            )
            try:
                results = _execute_batch(round_num, merged_new)
            except TaskInterrupted:
                return ""
            for tc, result in zip(merged_new, results):
                session.add_tool_result(
                    get_tc_field(tc, "id"), result, max_chars=max_chars
                )
            # 自纠错循环：后续每轮工具失败也注入修正提示
            if self_correction is not None:
                _inject_correction_if_failed(session, merged_new, results, self_correction)
            # ToolCallTracker：每轮后检测警告
            _inject_tracker_warning_if_failed(session, tracker)
            continue

        session.add_assistant(
            content=content_buffer if content_buffer else None,
            reasoning_content=reasoning_buffer if reasoning_buffer else None
        )
        return content_buffer

    return ""


def _inject_tracker_warning_if_failed(session, tracker) -> None:
    """检测 ToolCallTracker 状态，若连续失败或同错误重复超阈值则注入警告 user 消息。

    阈值：window_all_failed(3) 或 same_error_repeated(3)。
    每会话最多注入一次，避免重复警告（通过状态标志位判断）。
    """
    if tracker is None:
        return
    # 用 session 实例上的标志位避免重复注入
    if getattr(session, "_tracker_warned", False):
        return
    window_failed = tracker.is_window_all_failed(3)
    same_error = tracker.is_same_error_repeated(3)
    if not (window_failed or same_error):
        return
    status = tracker.get_status()
    parts = ["[ToolCallTracker 警告]"]
    if window_failed:
        parts.append(
            f"最近 3 次工具调用全部失败（连续失败 {status['consecutive_errors']} 次）。"
        )
    if same_error:
        parts.append("相同错误已连续重复 3 次以上。")
    parts.append(
        "建议：① 用 file_read 确认目标状态；② 检查参数与命令是否正确；"
        "③ 若任务无法继续，向用户报告失败原因并请求指导。"
    )
    session.add_user("\n".join(parts))
    session._tracker_warned = True


def _inject_correction_if_failed(session, tool_calls, results, loop) -> None:
    """检测工具失败并注入结构化修正提示 user 消息。

    遍历 (tool_call, result_str) 对，解析 result JSON 找出失败项。
    若 loop.should_retry 返回 True，用 build_correction_prompt 生成提示
    并 session.add_user 注入，让下一轮 LLM 看到修正建议。
    达到 max_attempts 上限后不再注入。
    """
    for tc, result_str in zip(tool_calls, results):
        try:
            result_dict = json.loads(result_str) if isinstance(result_str, str) else result_str
        except (json.JSONDecodeError, TypeError):
            continue
        if not isinstance(result_dict, dict):
            continue
        if result_dict.get("success", True):
            continue
        if loop.should_retry(tc, result_dict):
            prompt = loop.build_correction_prompt(tc, result_dict, loop.attempts)
            session.add_user(prompt)
