"""
通用 LLM 回调处理器 (LlmCallback)
=================================

作为 `langchain_agentx` 的库能力使用。

特性：
- 记录工具调用与 LLM 调用的详细信息
- 使用 `PerformanceMonitor` 收集性能数据
- 将结构化日志写入 `workspace/` 目录下（通过内部 `write_to_workspace`）

注意：
- 不依赖项目中的 `tools.*` 或 `api.*` 模块
- 日志格式与原实现保持兼容，便于沿用现有分析脚本
"""

from __future__ import annotations

import json
import logging
import random
import re
import time
from datetime import datetime
from typing import Any, Dict, List

from langchain_core.callbacks import BaseCallbackHandler

from .performance_monitor import OutputMode, PerformanceMonitor
from ..tools import write_to_workspace

logger = logging.getLogger(__name__)


class LlmCallback(BaseCallbackHandler):
    """
    统一回调处理器，用于记录工具调用和 LLM 调用的详细信息。

    记录内容：
    - 工具名称和参数（on_tool_start）
    - 工具返回结果（on_tool_end）
    - 工具调用耗时
    - 工具调用异常（on_tool_error）
    - LLM 模型和提示（on_llm_start / on_chat_model_start）
    - LLM 返回结果（on_llm_end）
    - LLM 调用耗时
    - LLM 调用异常（on_llm_error）

    文件保存位置：`workspace/{log_dir}/`
    文件名格式：
      - 工具调用：{YYYYMMDDHHMMSS}-{order}-{random_id}-tool-callback-{event_type}.txt
      - LLM 调用：{YYYYMMDDHHMMSS}-{order}-{random_id}-llm-callback-{event_type}.txt
    """

    def __init__(self, log_dir: str = "intelligent", enable_performance_monitor: bool = True):
        """
        初始化回调处理器。

        Args:
            log_dir: 日志目录名称（相对于 workspace/），默认 'intelligent'
            enable_performance_monitor: 是否启用性能监控（默认：True）
        """
        super().__init__()
        self.log_dir = log_dir

        # 文件序号（从0开始，每次保存文件时递增）
        self.file_order = 0

        # 工具调用相关状态（按 run_id 隔离，支持并发/交错调用）
        # 结构: { run_id: {
        #     "start_time": float,
        #     "tool_name": str,
        #     "tool_input": Any,
        #     "session_id": str,
        #     "random_id": int,
        #     "record": PerformanceRecord | None,
        # } }
        self.tool_states: Dict[Any, Dict[str, Any]] = {}

        # LLM 调用相关状态
        self.llm_start_time = None
        self.llm_model_name = None
        self.llm_prompts = None
        self.llm_session_id = None
        self.llm_random_id = None  # 9位随机数编号，用于关联 start/end/error 事件

        # 性能监控器
        self.performance_monitor: PerformanceMonitor | None = None
        self.current_tool_record = None  # 兼容字段
        self.current_llm_record = None
        if enable_performance_monitor:
            self.performance_monitor = PerformanceMonitor()
            self.performance_monitor.start_session()
            logger.info("PerformanceMonitor initialized")

    # ========= 工具回调 =========

    def on_tool_start(self, serialized: Dict[str, Any], input_str: str, **kwargs: Any) -> None:
        """工具调用开始时触发。"""
        logger.debug(
            "LlmCallback.on_tool_start | serialized keys: %s",
            list(serialized.keys()) if isinstance(serialized, dict) else "not dict",
        )

        run_id = kwargs.get("run_id")
        if run_id is None:
            run_id = f"tool-{id(self)}-{int(time.time() * 1000)}"
            logger.warning("LlmCallback.on_tool_start: 未提供 run_id，使用临时ID: %s", run_id)

        start_time = time.time()

        # 获取工具名称
        tool_name = "未知工具"
        if isinstance(serialized, dict):
            if "name" in serialized:
                tool_name = serialized["name"]
            elif "id" in serialized:
                if isinstance(serialized["id"], list) and serialized["id"]:
                    last_id = serialized["id"][-1]
                    if isinstance(last_id, str):
                        tool_name = last_id
                    elif isinstance(last_id, list) and last_id and isinstance(last_id[-1], str):
                        tool_name = last_id[-1]
                elif isinstance(serialized["id"], str):
                    tool_name = serialized["id"]

        # 解析输入参数
        try:
            if isinstance(input_str, str):
                try:
                    tool_input = json.loads(input_str)
                except Exception:
                    tool_input = input_str
            else:
                tool_input = input_str
        except Exception as exc:  # noqa: BLE001
            logger.warning("解析工具输入参数失败: %s", exc)
            tool_input = str(input_str)

        tool_random_id = random.randint(100000000, 999999999)
        tool_session_id = datetime.now().strftime("%Y%m%d%H%M%S%f")[:-3]

        state: Dict[str, Any] = {
            "start_time": start_time,
            "tool_name": tool_name,
            "tool_input": tool_input,
            "session_id": tool_session_id,
            "random_id": tool_random_id,
            "record": None,
        }
        self.tool_states[run_id] = state

        log_data = {
            "event": "tool_start",
            "session_id": tool_session_id,
            "random_id": tool_random_id,
            "timestamp": datetime.now().isoformat(),
            "tool_name": tool_name,
            "input": tool_input,
            "input_str": (
                str(input_str)[:500] if len(str(input_str)) > 500 else str(input_str)
            ),
        }
        self._save_tool_log("tool_start", log_data)

        # 性能监控记录
        if self.performance_monitor:
            if start_time is None:
                start_time = time.time()
            record = self.performance_monitor.add_tool_call(
                tool_name=tool_name,
                start_time=start_time,
                input_args=tool_input if isinstance(tool_input, dict) else None,
            )
            state["record"] = record
            self.current_tool_record = record

        logger.debug(
            "LlmCallback: 工具调用开始 | 工具: %s | 会话ID: %s | 随机编号: %s | run_id: %s",
            tool_name,
            tool_session_id,
            tool_random_id,
            run_id,
        )

    def on_tool_end(self, output: Any, **kwargs: Any) -> None:
        """工具调用成功结束时触发。"""
        run_id = kwargs.get("run_id")
        state = self.tool_states.get(run_id) if run_id is not None else None

        if state is None:
            end_time = time.time()
            logger.error(
                "LlmCallback.on_tool_end: 未找到 run_id=%s 对应的工具状态，可能存在并发或回调顺序问题。",
                run_id,
            )
            tool_name = "未知工具"
            tool_input = None
            session_id = None
            random_id = None
            start_time = None
            record = None
        else:
            end_time = time.time()
            start_time = state.get("start_time")
            tool_name = state.get("tool_name", "未知工具")
            tool_input = state.get("tool_input")
            session_id = state.get("session_id")
            random_id = state.get("random_id")
            record = state.get("record")

        cost_time = round(end_time - start_time, 2) if start_time else 0
        output_str = str(output)
        output_length = len(output_str)

        output_preview = output_str
        if output_length > 1000:
            output_preview = (
                output_str[:1000]
                + f"\n... (输出过长，已截断，总长度: {output_length} 字符)"
            )

        log_data = {
            "event": "tool_end",
            "session_id": session_id,
            "random_id": random_id,
            "timestamp": datetime.now().isoformat(),
            "tool_name": tool_name,
            "input": tool_input,
            "output": output_str,
            "output_preview": output_preview,
            "output_length": output_length,
            "cost_time_seconds": cost_time,
        }
        self._save_tool_log("tool_end", log_data)

        if self.performance_monitor and record:
            record.finalize(end_time=end_time, output_length=output_length)
            self.current_tool_record = None

        logger.info(
            "工具调用成功 | 工具: %s | 耗时: %ss | 输出长度: %s",
            tool_name,
            cost_time,
            output_length,
        )

        if run_id is not None and run_id in self.tool_states:
            del self.tool_states[run_id]

    def on_tool_error(self, error: Exception, **kwargs: Any) -> None:
        """工具调用异常时触发。"""
        run_id = kwargs.get("run_id")
        state = self.tool_states.get(run_id) if run_id is not None else None

        if state is None:
            end_time = time.time()
            logger.error(
                "LlmCallback.on_tool_error: 未找到 run_id=%s 对应的工具状态，可能存在并发或回调顺序问题。",
                run_id,
            )
            tool_name = "未知工具"
            tool_input = None
            session_id = None
            random_id = None
            start_time = None
            record = None
        else:
            end_time = time.time()
            start_time = state.get("start_time")
            tool_name = state.get("tool_name", "未知工具")
            tool_input = state.get("tool_input")
            session_id = state.get("session_id")
            random_id = state.get("random_id")
            record = state.get("record")

        cost_time = round(end_time - start_time, 2) if start_time else 0

        log_data = {
            "event": "tool_error",
            "session_id": session_id,
            "random_id": random_id,
            "timestamp": datetime.now().isoformat(),
            "tool_name": tool_name,
            "input": tool_input,
            "error": str(error),
            "error_type": type(error).__name__,
            "cost_time_seconds": cost_time,
        }
        self._save_tool_log("tool_error", log_data)

        if self.performance_monitor and record:
            record.end_time = end_time
            record.cost_time_ms = cost_time * 1000
            record.error = str(error)
            self.current_tool_record = None

        logger.error(
            "工具调用失败 | 工具: %s | 耗时: %ss | 错误: %s",
            tool_name,
            cost_time,
            error,
        )

        if run_id is not None and run_id in self.tool_states:
            del self.tool_states[run_id]

    def get_performance_report(self, mode: str = "debug") -> str:
        """获取性能报告。"""
        if not self.performance_monitor:
            return ""

        mode_map = {
            "production": OutputMode.PRODUCTION,
            "debug": OutputMode.DEBUG,
            "benchmark": OutputMode.BENCHMARK,
        }
        output_mode = mode_map.get(mode, OutputMode.DEBUG)
        return self.performance_monitor.generate_report(mode=output_mode)

    def get_tool_stats_summary(self) -> str:
        """获取工具统计摘要，例如：`3次 (bound_search_code → grep → read_file)`。"""
        if not self.performance_monitor:
            return "N/A"
        return self.performance_monitor.get_tool_stats_summary()

    def _save_tool_log(self, event_type: str, log_data: Dict[str, Any]) -> None:
        """保存工具调用日志到文件。"""
        try:
            formatted_content: List[str] = []
            formatted_content.append("=" * 80)
            formatted_content.append(f"事件类型: {log_data.get('event', 'unknown')}")
            formatted_content.append(
                f"随机编号: {log_data.get('random_id', 'N/A')} (用于关联 start/end/error 事件)"
            )
            formatted_content.append(f"会话ID: {log_data.get('session_id', 'N/A')}")
            formatted_content.append(f"时间戳: {log_data.get('timestamp', 'N/A')}")
            formatted_content.append(f"工具名称: {log_data.get('tool_name', 'N/A')}")
            formatted_content.append("=" * 80)
            formatted_content.append("")

            if "input" in log_data and log_data["input"]:
                input_data = log_data["input"]
                formatted_content.append("📥 工具输入 (Input):")
                formatted_content.append("-" * 80)
                if isinstance(input_data, dict):
                    formatted_content.append(
                        json.dumps(input_data, ensure_ascii=False, indent=2)
                    )
                else:
                    formatted_content.append(str(input_data))
                formatted_content.append("-" * 80)
                formatted_content.append("")

            if "output" in log_data and log_data["output"]:
                output = log_data["output"]
                output_length = log_data.get("output_length", len(str(output)))
                formatted_content.append("📤 工具输出 (Output):")
                formatted_content.append("-" * 80)
                if isinstance(output, str):
                    output_display = (
                        output.replace("\\n", "\n")
                        .replace("\\t", "\t")
                        .replace("\\r", "\r")
                    )
                    formatted_content.append(output_display)
                else:
                    formatted_content.append(str(output))
                formatted_content.append("-" * 80)
                formatted_content.append(f"输出长度: {output_length} 字符")
                if output_length > 10000:
                    formatted_content.append(
                        f"⚠️  注意: 输出内容较长（{output_length} 字符），完整内容已保存"
                    )
                formatted_content.append("")

            if "error" in log_data:
                formatted_content.append("❌ 错误信息 (Error):")
                formatted_content.append("-" * 80)
                formatted_content.append(
                    f"错误类型: {log_data.get('error_type', 'Unknown')}"
                )
                formatted_content.append(f"错误内容: {log_data.get('error', 'N/A')}")
                formatted_content.append("-" * 80)
                formatted_content.append("")

            if "cost_time_seconds" in log_data:
                formatted_content.append(f"⏱️  耗时: {log_data['cost_time_seconds']} 秒")
                formatted_content.append("")

            formatted_content.append("=" * 80)
            formatted_content.append("")

            log_content = "\n".join(formatted_content)

            random_id = log_data.get("random_id", "000000000")
            timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
            order = self.file_order
            self.file_order += 1

            file_name = f"{timestamp}-{order}-{random_id}-tool-callback-{event_type}"
            write_to_workspace(
                prompt=log_content,
                path_name=self.log_dir,
                file_name=file_name,
            )
        except Exception as exc:  # noqa: BLE001
            logger.error("LlmCallback: 保存工具日志失败 | 事件: %s | 错误: %s", event_type, exc)

    # ========= LLM 回调 =========

    def _extract_tools_from_kwargs(self, kwargs: Dict[str, Any]) -> list:
        """从 LLM 调用 kwargs 中尝试提取工具列表。"""
        tools: list = []
        inv = kwargs.get("invocation_params")
        if isinstance(inv, dict):
            tools = inv.get("tools")
        if not isinstance(tools, list):
            tools = []
        if not tools:
            tools = kwargs.get("tools") or []
        if not isinstance(tools, list):
            tools = []
        return tools

    def _format_tools_for_log(self, tools: list) -> str:
        """将工具列表格式化为可读的「工具描述」文本。"""
        if not tools:
            return ""
        lines: List[str] = []
        for t in tools:
            name = ""
            desc = ""
            if hasattr(t, "name"):
                name = getattr(t, "name", "") or ""
            if hasattr(t, "description"):
                desc = (getattr(t, "description", "") or "").strip()
            if isinstance(t, dict):
                name = t.get("name") or t.get("function", {}).get("name") or ""
                desc = (
                    t.get("description")
                    or t.get("function", {}).get("description")
                    or ""
                ).strip()
            if not name and isinstance(t, dict) and "function" in t:
                name = t["function"].get("name", "")
            name = name or "(未命名)"
            desc_preview = (desc[:500] + "…") if len(desc) > 500 else desc
            lines.append(f"- **{name}**: {desc_preview}")
        return "\n".join(lines)

    def on_llm_start(self, serialized: Dict[str, Any], prompts: list, **kwargs: Any) -> None:
        """LLM 调用开始时触发。"""
        logger.debug(
            "LlmCallback.on_llm_start | serialized keys: %s",
            list(serialized.keys()) if isinstance(serialized, dict) else "not dict",
        )

        self.llm_start_time = time.time()
        self.llm_model_name = "未知模型"
        if isinstance(serialized, dict):
            if "name" in serialized:
                self.llm_model_name = serialized["name"]
            elif "id" in serialized:
                if isinstance(serialized["id"], list) and serialized["id"]:
                    self.llm_model_name = serialized["id"][-1]
                elif isinstance(serialized["id"], str):
                    self.llm_model_name = serialized["id"]

        self.llm_prompts = prompts
        self.llm_random_id = random.randint(100000000, 999999999)
        self.llm_session_id = datetime.now().strftime("%Y%m%d%H%M%S%f")[:-3]

        prompt_length = sum(len(str(p)) for p in prompts)
        tools = self._extract_tools_from_kwargs(kwargs)
        tool_descriptions = self._format_tools_for_log(tools)
        if tool_descriptions:
            prompt_length += len(tool_descriptions)

        log_data = {
            "event": "llm_start",
            "session_id": self.llm_session_id,
            "random_id": self.llm_random_id,
            "timestamp": datetime.now().isoformat(),
            "model_name": self.llm_model_name,
            "prompt_count": len(prompts),
            "prompt_length": prompt_length,
            "prompts": [str(p) for p in prompts],
            "prompts_preview": [str(p)[:200] for p in prompts[:3]],
        }
        if tool_descriptions:
            log_data["tool_descriptions"] = tool_descriptions
            log_data["tool_count"] = len(tools)

        self._save_llm_log("llm_start", log_data)

        if self.performance_monitor:
            if self.llm_start_time is None:
                self.llm_start_time = time.time()
            self.current_llm_record = self.performance_monitor.add_llm_call(
                model_name=self.llm_model_name,
                start_time=self.llm_start_time,
                prompt_length=prompt_length,
            )

        logger.debug(
            "LlmCallback: LLM 调用开始 | 模型: %s | 会话ID: %s | 随机编号: %s",
            self.llm_model_name,
            self.llm_session_id,
            self.llm_random_id,
        )

    def on_chat_model_start(self, serialized: Dict[str, Any], messages: list, **kwargs: Any) -> None:
        """Chat 模型调用开始时触发，复用 on_llm_start 逻辑。"""
        self.on_llm_start(serialized=serialized, prompts=messages, **kwargs)

    # on_llm_end/on_llm_error 实现较长，这里保持与原逻辑一致，只做最小依赖调整

    def on_llm_end(self, response: Any, **kwargs: Any) -> None:  # noqa: C901
        """LLM 调用成功结束时触发。"""
        end_time = time.time()
        cost_time = round(end_time - self.llm_start_time, 2) if self.llm_start_time else 0

        response_type = type(response).__name__
        try:
            if hasattr(response, "generations"):
                output_texts: List[str] = []
                for generation_list in response.generations:
                    if not generation_list:
                        continue
                    for generation in generation_list:
                        if hasattr(generation, "message") and generation.message:
                            msg = generation.message
                            content_str = ""
                            if hasattr(msg, "content"):
                                content = msg.content
                                if content is not None:
                                    if isinstance(content, list):
                                        content_str = " ".join(str(item) for item in content)
                                    else:
                                        content_str = str(content)
                            tool_calls_str = ""
                            if hasattr(msg, "tool_calls") and msg.tool_calls:
                                try:
                                    tool_calls_list: List[str] = []
                                    for tc in msg.tool_calls:
                                        tool_name = "unknown"
                                        tool_args: Dict[str, Any] = {}
                                        if isinstance(tc, dict):
                                            tool_name = tc.get(
                                                "name", tc.get("function", {}).get("name", "unknown")
                                            )
                                            tool_args = tc.get(
                                                "args",
                                                tc.get("function", {}).get("arguments", {}),
                                            )
                                            if isinstance(tool_args, str):
                                                try:
                                                    tool_args = json.loads(tool_args)
                                                except Exception:
                                                    pass
                                        else:
                                            tool_name = getattr(tc, "name", None)
                                            if tool_name is None:
                                                func = getattr(tc, "function", None)
                                                if func:
                                                    tool_name = getattr(func, "name", "unknown")
                                                    args_str = getattr(func, "arguments", "{}")
                                                    try:
                                                        tool_args = (
                                                            json.loads(args_str)
                                                            if isinstance(args_str, str)
                                                            else args_str
                                                        )
                                                    except Exception:
                                                        tool_args = {}
                                            else:
                                                tool_args = getattr(
                                                    tc, "args", getattr(tc, "arguments", {})
                                                )
                                        tool_calls_list.append(
                                            f"{tool_name}({json.dumps(tool_args, ensure_ascii=False)})"
                                        )
                                    tool_calls_str = (
                                        f"[工具调用 ({len(msg.tool_calls)} 个): "
                                        f"{', '.join(tool_calls_list)}]"
                                    )
                                except Exception as exc:  # noqa: BLE001
                                    logger.debug("提取 tool_calls 失败: %s", exc)
                                    tool_calls_str = f"[工具调用: {str(msg.tool_calls)[:500]}]"

                            invalid_tool_calls_str = ""
                            if hasattr(msg, "invalid_tool_calls") and msg.invalid_tool_calls:
                                try:
                                    inv_list: List[str] = []
                                    for itc in msg.invalid_tool_calls:
                                        if isinstance(itc, dict):
                                            name = itc.get("name", "unknown")
                                            args = itc.get("args", "")
                                            if isinstance(args, str) and len(args) > 200:
                                                args = args[:200] + "..."
                                            inv_list.append(f"{name}({args})")
                                        else:
                                            inv_list.append(str(itc)[:150])
                                    invalid_tool_calls_str = (
                                        f"[无效工具调用 ({len(msg.invalid_tool_calls)} 个): "
                                        f"{', '.join(inv_list)}]"
                                    )
                                except Exception as exc:  # noqa: BLE001
                                    logger.debug("提取 invalid_tool_calls 失败: %s", exc)
                                    invalid_tool_calls_str = (
                                        f"[无效工具调用: {str(msg.invalid_tool_calls)[:500]}]"
                                    )

                            if content_str.strip():
                                output_texts.append(content_str)
                                if tool_calls_str:
                                    output_texts.append(tool_calls_str)
                                if invalid_tool_calls_str:
                                    output_texts.append(invalid_tool_calls_str)
                                continue
                            if tool_calls_str:
                                output_texts.append(tool_calls_str)
                                if invalid_tool_calls_str:
                                    output_texts.append(invalid_tool_calls_str)
                                continue
                            if invalid_tool_calls_str:
                                output_texts.append(invalid_tool_calls_str)
                                continue
                        if hasattr(generation, "text"):
                            text = generation.text
                            text_str = str(text) if text else ""
                            if text_str:
                                output_texts.append(text_str)
                output_str = "\n".join(output_texts) if output_texts else ""
            elif hasattr(response, "content"):
                content_str = str(response.content) if response.content else ""
                tool_calls_str = ""
                if hasattr(response, "tool_calls") and response.tool_calls:
                    try:
                        tool_calls_list = []
                        for tc in response.tool_calls:
                            tool_name = "unknown"
                            tool_args: Dict[str, Any] = {}
                            if isinstance(tc, dict):
                                tool_name = tc.get(
                                    "name", tc.get("function", {}).get("name", "unknown")
                                )
                                tool_args = tc.get(
                                    "args", tc.get("function", {}).get("arguments", {})
                                )
                                if isinstance(tool_args, str):
                                    try:
                                        tool_args = json.loads(tool_args)
                                    except Exception:
                                        pass
                            else:
                                tool_name = getattr(tc, "name", None)
                                if tool_name is None:
                                    func = getattr(tc, "function", None)
                                    if func:
                                        tool_name = getattr(func, "name", "unknown")
                                        args_str = getattr(func, "arguments", "{}")
                                        try:
                                            tool_args = (
                                                json.loads(args_str)
                                                if isinstance(args_str, str)
                                                else args_str
                                            )
                                        except Exception:
                                            tool_args = {}
                                else:
                                    tool_args = getattr(
                                        tc, "args", getattr(tc, "arguments", {})
                                    )
                            tool_calls_list.append(
                                f"{tool_name}({json.dumps(tool_args, ensure_ascii=False)})"
                            )
                        tool_calls_str = (
                            f"[工具调用 ({len(response.tool_calls)} 个): "
                            f"{', '.join(tool_calls_list)}]"
                        )
                    except Exception as exc:  # noqa: BLE001
                        logger.debug("提取 tool_calls 失败: %s", exc)
                        tool_calls_str = f"[工具调用: {str(response.tool_calls)[:500]}]"

                if content_str.strip():
                    output_str = content_str
                    if tool_calls_str:
                        output_str = f"{content_str}\n{tool_calls_str}"
                elif tool_calls_str:
                    output_str = tool_calls_str
                else:
                    output_str = ""
            else:
                logger.debug("未知响应类型: %s，尝试多种方式提取", response_type)
                output_str = str(response)
                if not output_str or (output_str.startswith("<") and output_str.endswith(">")):
                    if hasattr(response, "__dict__"):
                        output_str = f"[响应对象: {response_type}] {response.__dict__}"
                    else:
                        output_str = f"[响应对象: {response_type}] {repr(response)}"
        except Exception as exc:  # noqa: BLE001
            logger.error("解析 LLM 响应失败: %s | 响应类型: %s", exc, response_type, exc_info=True)
            try:
                output_str = str(response)
            except Exception:
                output_str = f"[无法解析响应对象: {response_type}]"

        output_length = len(output_str)
        if not output_str or output_length == 0:
            # 若为 tool_calls 回合（无正文）或含 invalid_tool_calls，用友好说明替代“结构不匹配”警告
            friendly_note = ""
            if hasattr(response, "generations") and response.generations:
                for gen_list in response.generations:
                    if not gen_list:
                        continue
                    for gen in gen_list:
                        if not hasattr(gen, "message") or not gen.message:
                            continue
                        msg = gen.message
                        finish_reason = None
                        if hasattr(msg, "response_metadata") and msg.response_metadata:
                            finish_reason = msg.response_metadata.get("finish_reason")
                        if finish_reason in ("tool_calls", "tool-calls"):
                            friendly_note = "[本次为工具调用回合，无正文内容]"
                            if hasattr(msg, "invalid_tool_calls") and msg.invalid_tool_calls:
                                friendly_note += (
                                    f" [无效工具调用: {len(msg.invalid_tool_calls)} 个]"
                                )
                            break
                        if hasattr(msg, "invalid_tool_calls") and msg.invalid_tool_calls:
                            friendly_note = (
                                f"[无正文，含无效工具调用 {len(msg.invalid_tool_calls)} 个]"
                            )
                            break
                    if friendly_note:
                        break

            if friendly_note:
                output_str = friendly_note
                output_length = len(output_str)
            else:
                diagnostic_info: List[str] = []
                diagnostic_info.append(f"响应类型: {response_type}")
                diagnostic_info.append(f"响应对象字符串表示: {str(response)[:500]}")
                if hasattr(response, "__dict__"):
                    diagnostic_info.append(f"响应对象属性: {list(response.__dict__.keys())}")
                if hasattr(response, "generations"):
                    diagnostic_info.append(f"generations 类型: {type(response.generations)}")
                    if response.generations:
                        diagnostic_info.append(f"generations 长度: {len(response.generations)}")
                diagnostic_msg = "\n".join(diagnostic_info)
                logger.debug("LLM 响应为空，诊断信息:\n%s", diagnostic_msg)
                output_str = (
                    "[⚠️ 警告: LLM 响应为空，可能是响应对象结构不匹配]\n\n诊断信息:\n"
                    f"{diagnostic_msg}"
                )
                output_length = len(output_str)

        output_preview = output_str
        if output_length > 1000:
            output_preview = (
                output_str[:1000]
                + f"\n... (输出过长，已截断，总长度: {output_length} 字符)"
            )

        log_data = {
            "event": "llm_end",
            "session_id": self.llm_session_id,
            "random_id": self.llm_random_id,
            "timestamp": datetime.now().isoformat(),
            "model_name": self.llm_model_name,
            "output": output_str,
            "output_preview": output_preview,
            "output_length": output_length,
            "cost_time_seconds": cost_time,
        }
        self._save_llm_log("llm_end", log_data)

        # 退化检测统计（不做纠偏，仅记录）
        degraded_tool_names: List[str] = []
        degradation_detected = False
        try:
            if isinstance(output_str, str) and "<tool_call>" in output_str:
                has_structured_tool_calls = False
                if hasattr(response, "tool_calls") and getattr(response, "tool_calls"):
                    has_structured_tool_calls = True
                if not has_structured_tool_calls and hasattr(response, "generations"):
                    for gen_list in getattr(response, "generations", []) or []:
                        for gen in gen_list or []:
                            msg = getattr(gen, "message", None)
                            if msg is not None and hasattr(msg, "tool_calls") and getattr(
                                msg, "tool_calls"
                            ):
                                has_structured_tool_calls = True
                                break
                        if has_structured_tool_calls:
                            break
                if not has_structured_tool_calls:
                    degradation_detected = True
                    degraded_tool_names = self._parse_degraded_tool_names_from_text(
                        output_str
                    )
        except Exception as exc:  # noqa: BLE001
            logger.debug("退化检测失败（忽略）：%s", exc)

        if self.performance_monitor and self.current_llm_record:
            prompt_tokens = None
            output_tokens = None

            if hasattr(response, "llm_output") and response.llm_output:
                token_usage = response.llm_output.get("token_usage", {})
                if token_usage:
                    prompt_tokens = token_usage.get("prompt_tokens")
                    output_tokens = token_usage.get("completion_tokens")

            if prompt_tokens is None and hasattr(response, "response_metadata"):
                token_usage = response.response_metadata.get("token_usage", {})
                if token_usage:
                    prompt_tokens = token_usage.get("prompt_tokens")
                    output_tokens = token_usage.get("completion_tokens")

            if prompt_tokens is None and hasattr(response, "generations"):
                for gen_list in response.generations:
                    if gen_list:
                        for gen in gen_list:
                            if hasattr(gen, "message") and hasattr(
                                gen.message, "response_metadata"
                            ):
                                token_usage = gen.message.response_metadata.get(
                                    "token_usage", {}
                                )
                                if token_usage:
                                    prompt_tokens = token_usage.get("prompt_tokens")
                                    output_tokens = token_usage.get("completion_tokens")
                                    break
                        if prompt_tokens is not None:
                            break

            self.current_llm_record.finalize(
                end_time=end_time,
                output_length=output_length,
                prompt_tokens=prompt_tokens,
                output_tokens=output_tokens,
            )

            if degradation_detected:
                self.current_llm_record.degradation_detected = True
                self.current_llm_record.degraded_tool_names = degraded_tool_names

        logger.info(
            "LLM 调用成功 | 模型: %s | 耗时: %ss | 输出长度: %s",
            self.llm_model_name,
            cost_time,
            output_length,
        )

        self.llm_start_time = None
        self.llm_model_name = None
        self.llm_prompts = None
        self.llm_session_id = None
        self.llm_random_id = None
        self.current_llm_record = None

    @staticmethod
    def _parse_degraded_tool_names_from_text(text: str) -> list[str]:
        """从 `<tool_call>...</tool_call>` 文本中解析工具名（用于统计）。"""
        tool_names: list[str] = []
        if not text:
            return tool_names
        pattern = re.compile(r"<tool_call>(.*?)</tool_call>", re.DOTALL)
        for raw in pattern.findall(text):
            raw = (raw or "").strip()
            if not raw:
                continue
            try:
                payload = json.loads(raw)
            except Exception:
                continue
            if isinstance(payload, dict):
                name = payload.get("name")
                if isinstance(name, str) and name:
                    tool_names.append(name)
        return tool_names

    def on_llm_error(self, error: Exception, **kwargs: Any) -> None:
        """LLM 调用异常时触发。"""
        end_time = time.time()
        cost_time = round(end_time - self.llm_start_time, 2) if self.llm_start_time else 0

        log_data = {
            "event": "llm_error",
            "session_id": self.llm_session_id,
            "random_id": self.llm_random_id,
            "timestamp": datetime.now().isoformat(),
            "model_name": self.llm_model_name,
            "error": str(error),
            "error_type": type(error).__name__,
            "cost_time_seconds": cost_time,
        }
        self._save_llm_log("llm_error", log_data)

        if self.performance_monitor and self.current_llm_record:
            self.current_llm_record.end_time = end_time
            self.current_llm_record.cost_time_ms = cost_time * 1000
            self.current_llm_record.error = str(error)

        logger.error(
            "LLM 调用失败 | 模型: %s | 耗时: %ss | 错误: %s",
            self.llm_model_name,
            cost_time,
            error,
        )

        self.llm_start_time = None
        self.llm_model_name = None
        self.llm_prompts = None
        self.llm_session_id = None
        self.llm_random_id = None
        self.current_llm_record = None

    def _save_llm_log(self, event_type: str, log_data: Dict[str, Any]) -> None:
        """保存 LLM 调用日志到文件。"""
        try:
            formatted_content: List[str] = []
            formatted_content.append("=" * 80)
            formatted_content.append(f"事件类型: {log_data.get('event', 'unknown')}")
            formatted_content.append(
                f"随机编号: {log_data.get('random_id', 'N/A')} (用于关联 start/end/error 事件)"
            )
            formatted_content.append(f"会话ID: {log_data.get('session_id', 'N/A')}")
            formatted_content.append(f"时间戳: {log_data.get('timestamp', 'N/A')}")
            formatted_content.append(f"模型名称: {log_data.get('model_name', 'N/A')}")
            formatted_content.append("=" * 80)
            formatted_content.append("")

            prompts_to_display = log_data.get("prompts", log_data.get("prompts_preview", []))
            if prompts_to_display:
                formatted_content.append("📥 提示词 (Prompts):")
                formatted_content.append("-" * 80)
                formatted_content.append(
                    f"提示词数量: {log_data.get('prompt_count', len(prompts_to_display))}"
                )
                formatted_content.append(
                    f"提示词总长度: {log_data.get('prompt_length', sum(len(str(p)) for p in prompts_to_display))} 字符"
                )
                formatted_content.append("")
                for i, prompt in enumerate(prompts_to_display, 1):
                    formatted_content.append(f"提示词 {i}:")
                    if isinstance(prompt, str):
                        prompt_formatted = (
                            prompt.replace("\\n", "\n")
                            .replace("\\t", "\t")
                            .replace("\\r", "\r")
                        )
                        formatted_content.append(prompt_formatted)
                    else:
                        prompt_str = str(prompt)
                        prompt_formatted = (
                            prompt_str.replace("\\n", "\n")
                            .replace("\\t", "\t")
                            .replace("\\r", "\r")
                        )
                        formatted_content.append(prompt_formatted)
                    formatted_content.append("")
                formatted_content.append("-" * 80)
                formatted_content.append("")

            tool_descriptions = log_data.get("tool_descriptions")
            if tool_descriptions:
                formatted_content.append("📥 工具描述 (Tool Descriptions):")
                formatted_content.append("-" * 80)
                formatted_content.append(f"工具数量: {log_data.get('tool_count', 'N/A')}")
                formatted_content.append("")
                formatted_content.append(tool_descriptions)
                formatted_content.append("")
                formatted_content.append("-" * 80)
                formatted_content.append("")

            output = log_data.get("output") or log_data.get("output_preview", "")
            output_length = log_data.get("output_length", len(str(output)))
            if output:
                formatted_content.append("📤 LLM 输出 (Output):")
                formatted_content.append("-" * 80)
                if isinstance(output, str):
                    output_display = (
                        output.replace("\\n", "\n")
                        .replace("\\t", "\t")
                        .replace("\\r", "\r")
                    )
                    formatted_content.append(output_display)
                else:
                    formatted_content.append(str(output))
                formatted_content.append("-" * 80)
                formatted_content.append(f"输出长度: {output_length} 字符")
                if output_length > 10000:
                    formatted_content.append(
                        f"⚠️  注意: 输出内容较长（{output_length} 字符），完整内容已保存"
                    )
                formatted_content.append("")
            else:
                formatted_content.append("📤 LLM 输出 (Output):")
                formatted_content.append("-" * 80)
                output_text = log_data.get("output", "")
                if output_text and "[⚠️ 警告" in output_text and "诊断信息" in output_text:
                    formatted_content.append(output_text)
                else:
                    formatted_content.append("[⚠️ 警告: LLM 响应为空]")
                formatted_content.append("-" * 80)
                formatted_content.append("")

            if "error" in log_data:
                formatted_content.append("❌ 错误信息 (Error):")
                formatted_content.append("-" * 80)
                formatted_content.append(
                    f"错误类型: {log_data.get('error_type', 'Unknown')}"
                )
                formatted_content.append(f"错误内容: {log_data.get('error', 'N/A')}")
                formatted_content.append("-" * 80)
                formatted_content.append("")

            if "cost_time_seconds" in log_data:
                formatted_content.append(f"⏱️  耗时: {log_data['cost_time_seconds']} 秒")
                formatted_content.append("")

            formatted_content.append("=" * 80)
            formatted_content.append("")

            log_content = "\n".join(formatted_content)

            random_id = log_data.get("random_id", "000000000")
            timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
            order = self.file_order
            self.file_order += 1

            file_name = f"{timestamp}-{order}-{random_id}-llm-callback-{event_type}"
            write_to_workspace(
                prompt=log_content,
                path_name=self.log_dir,
                file_name=file_name,
            )
        except Exception as exc:  # noqa: BLE001
            logger.error("LlmCallback: 保存 LLM 日志失败 | 事件: %s | 错误: %s", event_type, exc)

