# -*- coding: utf-8 -*-
"""
工具调用追踪器

借鉴 zhikuncode-main 的 engine/tracking/ToolCallTracker.java，
记录每次工具调用的元数据（名称、参数哈希、成功/失败、错误信息），
提供滑动窗口全失败检测和相同错误重复检测能力。

是 TerminationStrategy 和 ToolRecoveryFramework 的数据基础：
- 滑动窗口最近 N 次全失败 → 触发 SWITCH_STRATEGY / REQUEST_USER_INPUT
- 相同错误重复 M 次 → 触发 ESCALATE_TO_USER / ABORT_WITH_SUMMARY

线程安全：单进程 CLI 通常单线程，但 subagent 并发场景下需线程安全，
故用 threading.Lock 保护 _history 与 _consecutive_errors。
"""
import hashlib
import json
import threading
import time
from dataclasses import dataclass, field
from typing import List, Optional


@dataclass
class ToolCallRecord:
    """单次工具调用记录。"""
    tool_name: str
    args_hash: str          # sha256(json.dumps(args, sort_keys=True))，用于去重判定
    success: bool
    error_message: str = ""
    error_type: str = ""   # 异常类型名（TypeError / ValueError / 等）
    timestamp: float = field(default_factory=time.time)
    recovery_relevant: bool = True
    # 权限拒绝等预期终态设 False，不参与"需要恢复"判定

    @classmethod
    def create(
        cls,
        tool_name: str,
        args: dict,
        success: bool,
        error: str = "",
        error_type: str = "",
        recovery_relevant: bool = True,
    ) -> "ToolCallRecord":
        """工厂：自动计算 args_hash。"""
        try:
            args_str = json.dumps(args, sort_keys=True, ensure_ascii=False)
            args_hash = hashlib.sha256(args_str.encode("utf-8")).hexdigest()[:16]
        except (TypeError, ValueError):
            args_hash = "unhashable"
        return cls(
            tool_name=tool_name,
            args_hash=args_hash,
            success=success,
            error_message=error,
            error_type=error_type,
            recovery_relevant=recovery_relevant,
        )


class ToolCallTracker:
    """工具调用历史追踪器。

    维护三层状态：
    - history：按时间顺序的工具调用记录列表（FIFO 淘汰）
    - consecutive_errors：连续失败计数（成功则归零）
    - recovery_relevant 标志位：区分"权限拒绝等预期终态"与"真正需要恢复的系统错误"

    提供两类检测：
    - is_window_all_failed(window_size)：最近 N 次是否全部失败
    - is_same_error_repeated(threshold)：相同 errorMessage 是否连续重复超阈值
    """

    def __init__(self, max_history: int = 100):
        """初始化，指定历史记录上限。超过上限 FIFO 淘汰最旧记录。"""
        self._history: List[ToolCallRecord] = []
        self._max_history = max_history
        self._consecutive_errors = 0
        self._lock = threading.Lock()

    def record(
        self,
        tool_name: str,
        args: dict,
        success: bool,
        error: str = "",
        error_type: str = "",
        recovery_relevant: bool = True,
    ) -> None:
        """记录一次工具调用。

        Args:
            tool_name: 工具名（如 "file_edit" / "shell_exec"）
            args: 工具参数 dict，用于计算 args_hash
            success: 是否成功
            error: 失败时的错误消息（成功时为空串）
            error_type: 失败时的异常类型名
            recovery_relevant: 是否与恢复决策相关。
                权限拒绝等预期终态设 False，不参与"需要恢复"判定。
        """
        record = ToolCallRecord.create(
            tool_name=tool_name,
            args=args,
            success=success,
            error=error,
            error_type=error_type,
            recovery_relevant=recovery_relevant,
        )
        with self._lock:
            self._history.append(record)
            if len(self._history) > self._max_history:
                # FIFO 淘汰最旧
                self._history = self._history[-self._max_history:]
            if success:
                self._consecutive_errors = 0
            else:
                self._consecutive_errors += 1

    def is_window_all_failed(self, window_size: int = 3) -> bool:
        """检测最近 window_size 次调用是否全部失败。

        仅统计 recovery_relevant=True 的记录（权限拒绝等预期终态不计）。
        历史不足 window_size 时返回 False（无法判定）。
        """
        with self._lock:
            if len(self._history) < window_size:
                return False
            recent = self._history[-window_size:]
        for r in recent:
            if r.success or not r.recovery_relevant:
                return False
        return True

    def is_same_error_repeated(self, threshold: int = 3) -> bool:
        """检测最近是否有相同 errorMessage 连续重复超阈值次。

        "连续重复"指从最新记录往前数，相同 errorMessage 的连续条数 >= threshold。
        成功记录会打断连续性。
        """
        if threshold <= 0:
            return False
        with self._lock:
            if not self._history:
                return False
            latest = self._history[-1]
            if latest.success or not latest.recovery_relevant:
                return False
            count = 1
            for r in reversed(self._history[:-1]):
                if r.success:
                    break
                if (not r.recovery_relevant) or r.error_message != latest.error_message:
                    break
                count += 1
        return count >= threshold

    def consecutive_errors(self) -> int:
        """返回当前连续失败次数（成功则归零）。"""
        with self._lock:
            return self._consecutive_errors

    def recent(self, n: int = 10) -> List[ToolCallRecord]:
        """返回最近 n 条记录的副本（不影响内部状态）。"""
        with self._lock:
            return list(self._history[-n:])

    def total_count(self) -> int:
        """返回历史记录总数。"""
        with self._lock:
            return len(self._history)

    def reset(self) -> None:
        """清空所有历史记录与计数器（用于会话切换）。"""
        with self._lock:
            self._history.clear()
            self._consecutive_errors = 0

    def get_status(self) -> dict:
        """返回当前状态摘要，供日志或 UI 显示。"""
        with self._lock:
            total = len(self._history)
            success_count = sum(1 for r in self._history if r.success)
            fail_count = total - success_count
        return {
            "total_calls": total,
            "success_count": success_count,
            "fail_count": fail_count,
            "consecutive_errors": self._consecutive_errors,
            "window_all_failed": self.is_window_all_failed(3),
            "same_error_repeated": self.is_same_error_repeated(3),
        }


# 模块级默认实例（与 SensitiveDataFilter / ThinkingBudgetCalculator 一致）
# 让 agent_loop / main 无需传参即可访问，同时允许测试通过依赖注入替换。
_default_tracker: Optional[ToolCallTracker] = None
_default_lock = threading.Lock()


def get_default_tracker() -> ToolCallTracker:
    """获取默认的 ToolCallTracker 单例（线程安全懒加载）。"""
    global _default_tracker
    if _default_tracker is None:
        with _default_lock:
            if _default_tracker is None:
                _default_tracker = ToolCallTracker()
    return _default_tracker


def reset_default_tracker() -> None:
    """重置默认单例（主要用于测试隔离）。"""
    global _default_tracker
    with _default_lock:
        _default_tracker = None
