//! 上下文管理纯函数与数据类型 — 从 context.rs 抽离的无 `self` 依赖部分
//!
//! 职责：
//! - Token 粗估器（字符级近似，无 tokenizer 依赖）
//! - 上下文窗口配置与预算计算
//! - 消息分组（淘汰时保持工具调用三元组原子性）的数据类型与分类纯函数
//! - 淘汰单元 / 跟踪消息条目等数据类型
//!
//! 抽离原因：context.rs 的 `ContextManager` impl 块不可跨文件，但其中依赖的
//! 纯函数 / 数据类型 / 常量无 `self` 依赖，可独立成模块供 context.rs `use` 复用，
//! 降低单文件体积。零行为变化：仅搬迁，逻辑原样保留。
//!
//! context.rs 通过 `pub use` 重导出本模块的 `TokenEstimator` / `ContextConfig` 等，
//! 保持 `df_ai::context::TokenEstimator` 等历史路径对外可见（零调用方变更）。

use serde::{Deserialize, Serialize};

use crate::provider::{ChatMessage, MessageRole};

// ============================================================
// Token 估算器（零依赖粗估）
// ============================================================

/// Token 粗估器 — 字符级近似计数，无 tokenizer 依赖
///
/// 用于发送前预算控制，误差 ±15% 完全可接受（保守估计，宁可多算）。
#[derive(Debug, Clone)]
pub struct TokenEstimator {
    /// 字符 → token 转换系数（默认 0.35，即 ~2.8 字符/token，中英混合偏保守）
    pub chars_ratio: f32,
    /// 每条消息固定开销（role 标记 + 格式）
    pub per_message_overhead: u32,
    /// 每个 tool_call 的额外开销（name + arguments JSON 结构）
    pub per_tool_call_overhead: u32,
}

impl Default for TokenEstimator {
    fn default() -> Self {
        Self {
            chars_ratio: 0.35,
            per_message_overhead: 4,
            per_tool_call_overhead: 30,
        }
    }
}

impl TokenEstimator {
    /// 估算单条消息的 token 数（保守估计）
    ///
    /// F-260614-05 多模态回归修正：`msg.parts` 中的 Image.base64 与 Text.text 同样计入预算。
    /// 此前只算 `content`，含图消息的大段 base64(可达 25 万 tokens)被完全忽略，致
    /// `history_tokens` 严重低估 → build_for_request 误判未超预算 → provider 超限 400/500。
    /// 这里把 parts 的文本/base64 按同一 chars_ratio 粗估累加(base64 视为密集字符，0.35 偏保守)。
    pub fn estimate_message(&self, msg: &ChatMessage) -> u32 {
        let mut char_count = msg.content.chars().count();
        if let Some(parts) = &msg.parts {
            for p in parts {
                match p {
                    crate::provider::ContentPart::Text { text } => char_count += text.chars().count(),
                    crate::provider::ContentPart::Image { base64, url, .. } => {
                        // CR-260618-11#2:0.35 按 base64 字节数粗估,显著高于厂商实际(OpenAI 按像素非字节)。
                        // 偏保守致含图消息 token 高估、过度裁剪;降值(如 0.10~0.15)需独立评估裁剪边界,本次不改值仅标注。
                        // base64 优先（多模态主载荷），url 次之；url 模式无字节，仅按 URL 长度估
                        if let Some(b) = base64 {
                            char_count += b.chars().count();
                        } else if let Some(u) = url {
                            char_count += u.chars().count();
                        }
                    }
                }
            }
        }
        let content_tokens = (char_count as f32 * self.chars_ratio).ceil() as u32;
        let mut total = content_tokens + self.per_message_overhead;

        // tool_calls 的 JSON 结构开销（role=Assistant 时可能有）
        if let Some(ref calls) = msg.tool_calls {
            for call in calls {
                total += self.per_tool_call_overhead;
                total += (call.function.name.chars().count() as f32 * self.chars_ratio).ceil() as u32;
                total += (call.function.arguments.chars().count() as f32 * self.chars_ratio).ceil() as u32;
            }
        }

        // tool_call_id 开销（role=Tool 时有）
        if msg.tool_call_id.is_some() {
            total += 3;
        }

        total
    }

    /// 估算纯文本字符串的 token 数（用于 system prompt）
    pub fn estimate_text(&self, text: &str) -> u32 {
        (text.chars().count() as f32 * self.chars_ratio).ceil() as u32
    }
}

// ============================================================
// 上下文窗口配置
// ============================================================

/// 上下文窗口配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextConfig {
    /// 窗口上限 token 数（默认 128_000）
    pub max_tokens: u32,
    /// 输出预留 token 数（窗口中留给模型生成的部分，默认 8_192）
    pub output_reserve: u32,
    /// 安全系数 0.0~1.0（默认 0.85，留 15% 余量）
    pub safety_ratio: f32,
}

impl Default for ContextConfig {
    fn default() -> Self {
        Self {
            max_tokens: 128_000,
            output_reserve: 8_192,
            safety_ratio: 0.85,
        }
    }
}

impl ContextConfig {
    /// 预算上限 = (max_tokens - output_reserve) × safety_ratio
    pub fn budget_limit(&self) -> u32 {
        (self.max_tokens.saturating_sub(self.output_reserve) as f32 * self.safety_ratio) as u32
    }
}

// ============================================================
// 消息分组（淘汰时保持工具调用三元组原子性）
// ============================================================

/// 消息在逻辑上的分组标签，用于淘汰时保持原子性
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessageGroup {
    /// 普通 User / Assistant 文本消息（可独立淘汰）
    Standalone,
    /// Assistant 带 tool_calls，是三元组的头
    ToolCallHead,
    /// Tool 结果消息，是三元组的尾
    ToolResultTail,
}

/// 带有 token 缓存和分组信息的消息条目
///
/// 字段 `pub`：供阶段2 IPC 经 `ContextManager::messages_mut()` 拿到可变切片后,
/// 直接改 `message.status` / 读 `token_count` 做 token 重算(Mutex 单线程访问,
/// 同 crate 内安全)。结构体本身也 `pub`(返回类型对外可见)。
pub struct TrackedMessage {
    pub message: ChatMessage,
    pub token_count: u32,
    pub group: MessageGroup,
}

/// 按消息角色与 tool_calls 判定其逻辑分组（淘汰单元的原子性基础）
///
/// 纯函数：无 `self` 依赖，仅依据 `msg.role` 与 `msg.tool_calls` 分类。
pub fn classify_group(msg: &ChatMessage) -> MessageGroup {
    match msg.role {
        MessageRole::Tool => MessageGroup::ToolResultTail,
        MessageRole::Assistant => {
            if msg.tool_calls.as_ref().is_some_and(|c| !c.is_empty()) {
                MessageGroup::ToolCallHead
            } else {
                MessageGroup::Standalone
            }
        }
        _ => MessageGroup::Standalone,
    }
}

// ============================================================
// 淘汰单元
// ============================================================

/// 淘汰单元：连续消息范围 [..end) + token 总和
///
/// `pub` 供 `build_eviction_units` 的返回类型对外可见(阶段2/3 调用方读 `end` / `token_sum`)。
pub struct EvictionUnit {
    pub end: usize,
    pub token_sum: u32,
}

// ============================================================
// 常量
// ============================================================

/// 保护区大小：最后 N 条消息永不淘汰（≈ 最近 2 个完整用户轮次）
pub const PROTECT_COUNT: usize = 6;

/// Anthropic 流式 tool_use 缺 id 时的占位 id 前缀（见 anthropic_compat.rs 170-176）。
/// 此类 id 必然无匹配 tool_result，是历史中毒的标志，sanitize 时据此剔除畸形三元组。
pub const TOOL_MISSING_PREFIX: &str = "tool_missing_";
