//! 高危工具去重缓存。
//!
//! 从 audit/mod.rs 搬迁:`find_cached_high_risk_result` + `canonical_args_key`
//! + `sort_object_keys` + `PENDING_APPROVAL_PLACEHOLDER`(去重与 process_tool_calls
//! 共用占位常量)。三函数内部闭环(find_cached 调 canonical→sort),无外部 helper 依赖。

use std::sync::Arc;

use df_ai::provider::ChatMessage;
use df_storage::crud::AiToolExecutionRepo;
use tokio::sync::Mutex;

use super::super::AiSession;

/// Med/High 工具挂起审批时回填的占位 tool_result 内容(基础文本,不含 tc_id 标记)。
///
/// 单一常量避免「写入处」(process_tool_calls) 与「去重排查处」
/// (find_cached_high_risk_result) 各持一份字面量致耦合——若两者漂移,
/// 去重会把 pending 占位误判为已落定结果命中缓存,污染 LLM 上下文。
///
/// **占位配对完整性**:实际写入 messages 时用 [`pending_placeholder_for`] 生成带
/// `__PENDING__:tc_id` 标记的完整占位文本,供 sanitize 识别"占位不可裁 + 出口断言自愈补头"。
/// 本常量保留为基础文本(向前兼容 + 去重匹配基准)。
pub(crate) const PENDING_APPROVAL_PLACEHOLDER: &str = "需要用户审批，等待确认";

/// 构造带唯一标记(`__PENDING__:tc_id`)的审批挂起占位 tool_result 内容。
///
/// 解 400 orphan:占位 tool_result 内嵌 tc_id 标记,sanitize step3.5(反向 orphan 检测)
/// 据此把占位豁免保留(不丢),出口断言 `assert_placeholder_pairing` 据此补 TOOL_MISSING_PREFIX
/// 占位头自愈,使占位 result 与(可能被裁掉的)tool_call 头闭合配对,防 provider 400 orphan。
///
/// 标记格式:`{PENDING_APPROVAL_PLACEHOLDER}{PENDING_MARKER_PREFIX}{tc_id}`,
/// 如 "需要用户审批，等待确认__PENDING__:call_abc123"。tc_id 为空时退回基础文本(老格式,向前兼容)。
pub(crate) fn pending_placeholder_for(tc_id: &str) -> String {
    if tc_id.is_empty() {
        return PENDING_APPROVAL_PLACEHOLDER.to_string();
    }
    format!("{}{}{}", PENDING_APPROVAL_PLACEHOLDER, df_ai::context_helpers::PENDING_MARKER_PREFIX, tc_id)
}

/// 判定 tool_result content 是否为审批挂起占位(含基础文本或带 __PENDING__ 标记)。
///
/// 替代原裸 `msg.content == PENDING_APPROVAL_PLACEHOLDER` 精确匹配(占位带 tc_id 标记后
/// 不再精确等于基础文本,需用子串/标记匹配)。兼容老占位(纯文本)与新占位(带标记)。
pub(crate) fn is_pending_placeholder(content: &str) -> bool {
    df_ai::context_helpers::is_pending_placeholder(content)
}

/// 高危工具去重（根治 run_command 超时→重试→重新审批循环）。
///
/// ⚠ 性能注记:本函数在 session lock 持有期间对每个 high risk 工具
/// 串行查 AiToolExecutionRepo::find_by_tool_call_id,工具数多时锁持有线性增长。
/// 批量预取(改签名传 tool_call_ids 批量查)属架构改暂未做,后续 High risk 工具增多时优先处理。
///
/// **根因链**（F-04 batch42 已做超时标注）：LLM 调 run_command 超时 → F-04 把超时标注成
/// tool_result 回传 LLM → LLM（不可靠）仍重试同命令 → 新 tool_call_id（provider 每轮新 id）
/// → `process_tool_calls` 重新 insert pending(audit.rs:418) → 用户被迫重新审批，循环。
///
/// **治本（F-05）**：即使 LLM 仍重试，同 (tool_name, args) 不重复审批。本函数扫描会话历史，
/// 若发现**已落定**（executed/failed/rejected，非 pending）的同类同参高危调用，返回其
/// tool_result 内容，调用方把缓存结果作为新 tool_call_id 的 tool_result 回传 LLM，跳过审批。
///
/// **安全边界**：
/// - 仅 High risk（循环源头）；Low/Med 不去重（Low 直行无审批，Med 重试场景少且去重易误伤）。
/// - 仅匹配**已落定**结果（pending 的不命中——pending 已有独立流程，不会触发循环；
///   且避免两个 pending 互相吞掉审批）。LLM 只有在收到 tool_result 后才会重试，
///   故循环必然是「上一条已落定 → 重试」形态，pending 不命中不影响治本。
/// - args 走 JSON 规范化比较（键序无关），避免 LLM 两次生成键序不同误判为不同命令。
/// - 返回 None 表示无缓存命中（走原审批流程）。
///
/// `session` 只读扫描 messages（不写），调用方据返回值决定是否跳过 insert pending。
///
/// 经`conv_id` 索引 per_conv.messages(顶层 messages 在拆分存储后是死字段)。
///   conv_id 来源:process_tool_calls 入参 → 由 agentic/mod.rs run_agentic_loop 入参透传。
pub(crate) async fn find_cached_high_risk_result(
    session_arc: &Arc<Mutex<AiSession>>,
    conv_id: &str,
    audit_repo: &AiToolExecutionRepo,
    tool_name: &str,
    args: &serde_json::Value,
) -> Option<(String, String)> {
    use df_ai::provider::MessageRole;

    // 规范化新调用的 args 为可比字符串（排序键，键序无关）
    let new_args_key = canonical_args_key(args);

    // 短 lock 段读 messages + 找旧 tool_call_id + 旧 tool_result content,
    // drop 锁后再锁外 await DB 查 status(原代码持锁 await audit_repo,违反持锁 await 慢操作禁令)。
    // 第一步:锁内(async block 包裹,出 block 自动 drop guard)反向扫描,定位旧 tool_call_id 与对应 tool_result content
    let cached: Option<(String, String)> = (async {
        let session = session_arc.lock().await;
        // 读 per_conv.messages。process_tool_calls 调用前 loop 入口已桥接建立 per_conv,
        // 故 conv_read 必命中;防御性 None 时返 None(无缓存命中,走原审批流程)。
        let Some(conv) = session.conv_read(conv_id) else { return None };
        // ContextManager::iter 返回 impl Iterator（非 DoubleEnded），collect 成 Vec 再反向遍历。
        // 单对话消息量小（百级），collect 开销可忽略。
        let msgs: Vec<&ChatMessage> = conv.messages.iter().collect();

        // 1) 反向扫描 assistant tool_calls，找最近一条同名同参的 High 工具调用 → 拿到旧 tool_call_id
        //    反向：循环是「最近一次超时→重试」，命中通常是末尾附近，反向先停省全扫。
        let mut prev_tool_call_id: Option<String> = None;
        for msg in msgs.iter().rev() {
            if !matches!(msg.role, MessageRole::Assistant) {
                continue;
            }
            let Some(tcs) = msg.tool_calls.as_ref() else { continue };
            for tc in tcs {
                if tc.function.name != tool_name {
                    continue;
                }
                // 旧调用的 args 是流式拼接的 JSON 字符串，解析失败跳过（不误判为命中）
                let Ok(old_args) = serde_json::from_str::<serde_json::Value>(&tc.function.arguments) else {
                    continue;
                };
                if canonical_args_key(&old_args) == new_args_key {
                    prev_tool_call_id = Some(tc.id.clone());
                    break;
                }
            }
            if prev_tool_call_id.is_some() {
                break;
            }
        }

        // 2) 用旧 tool_call_id 找对应 tool_result。注意：审批拒绝/超时失败也属「已落定」，
        //    其 tool_result 内容同样回传（LLM 看到原反馈自行决定，不再逼用户二次审批）。
        //    pending 占位（「需要用户审批，等待确认」）不命中——仍在审批中，走原流程。
        let Some(old_id) = prev_tool_call_id else { return None };
        for msg in msgs.iter().rev() {
            if !matches!(msg.role, MessageRole::Tool) {
                continue;
            }
            if msg.tool_call_id.as_deref() != Some(old_id.as_str()) {
                continue;
            }
            // 命中旧 tool_result：排除 pending 占位（基础文本或带 __PENDING__:tc_id 标记）
            // 占位带标记后不再精确等于基础文本,改用 is_pending_placeholder 匹配。
            // 加固(子串误伤):`__PENDING__` 标记是 audit/cache.rs 占位模板独占信号(权威判定);
            // 老占位分支已收紧为精确全文等值(非 starts_with 前缀),杜绝用户真实 tool_result 内容
            // 恰以"需要用户审批..."开头被误判占位致去重误吞(详见 context_helpers::is_pending_placeholder)。
            if is_pending_placeholder(&msg.content) {
                return None;
            }
            return Some((old_id, msg.content.clone()));
        }
        None
    }).await;
    // 锁外:查审计表拿缓存来源真实 status(completed/rejected/failed),透传给
    // audit_tool_call 而非固定 completed(审计语义与结果内容一致,防"rejected/failed 结果
    // 记 completed"误导安全追溯)。审计记录缺失/查询失败 fallback completed(不阻塞去重,降级原行为)。
    let (old_id, content) = cached?;
    let status = audit_repo
        .find_by_tool_call_id(old_id.as_str())
        .await
        .ok()
        .and_then(|opt| opt.map(|rec| rec.status))
        .unwrap_or_else(|| "completed".to_string());
    Some((content, status))
}

/// 把 JSON args 规范化为可比字符串：对象键按字典序排序后序列化，
/// 键序不同的等价参数生成同一 key（防 LLM 两次生成键序不同误判为不同命令）。
fn canonical_args_key(args: &serde_json::Value) -> String {
    let mut v = args.clone();
    sort_object_keys(&mut v);
    // 紧凑序列化（无空白），保证稳定可比
    serde_json::to_string(&v).unwrap_or_default()
}

/// 只读幂等工具结果缓存(治 LLM 死循环重调)。
///
/// **根因链(实测 9357c27c)**:LLM 无"已调用过"记忆,对同参只读工具反复触发:
/// - `read_file(application.xml)` 读了 17 次(两次返回完全相同的 8457c 全文)
/// - `search_files(pattern='.java')` 连调 13 次(每次返回 items=0/has_more=True 几乎一致)
/// 每次都真执行 + 全文回灌 tool_result,是 prompt 64 万的直接元凶。
///
/// **治本**:对幂等只读工具,扫描会话历史,若发现**已成功完成**的同类同参调用,
/// 返回其 tool_result 内容,调用方把缓存结果作为新 tool_call_id 的 tool_result 回传 LLM,
/// 跳过真实执行(也跳过 emit Started/Completed——直接走缓存回填路径)。
///
/// **安全边界**(对照 find_cached_high_risk_result 严格收紧):
/// - 仅匹配白名单只读幂等工具(READONLY_CACHE_TOOLS):read_file/list_directory/
///   search_files/grep/read_symbol。写工具(write_file/patch_file/run_command 等)
///   永不缓存,避免误伤。
/// - 仅匹配 **status=completed** 的成功结果(失败/拒绝不缓存,可能重试成功)。
/// - 仅匹配 **非占位** 结果(排除 pending_placeholder 占位)。
/// - args 走 JSON 规范化比较(键序无关),与 find_cached_high_risk_result 一致。
/// - 返回 None 表示无缓存命中(走原执行流程)。
///
/// 与 find_cached_high_risk_result 区别:后者治"超时→重试→重新审批"循环( High risk),
/// 本函数治"LLM 失忆→同参重调"死循环( Low risk 只读),问题形态相似但触发原因不同。
pub(crate) async fn find_cached_readonly_result(
    session_arc: &Arc<Mutex<AiSession>>,
    conv_id: &str,
    audit_repo: &AiToolExecutionRepo,
    tool_name: &str,
    args: &serde_json::Value,
) -> Option<String> {
    use df_ai::provider::MessageRole;

    // 白名单:只读幂等工具才缓存。写工具/有副作用工具永不缓存。
    if !READONLY_CACHE_TOOLS.contains(&tool_name) {
        return None;
    }

    let new_args_key = canonical_args_key(args);

    // 短 lock 段读 messages + 锁外 await DB 查 status(原代码持锁 await)
    let cached: Option<(String, String)> = (async {
        let session = session_arc.lock().await;
        let Some(conv) = session.conv_read(conv_id) else { return None };
        let msgs: Vec<&ChatMessage> = conv.messages.iter().collect();

        // 1) 反向扫描 assistant tool_calls,找最近一条同名同参调用 → 拿到旧 tool_call_id
        let mut prev_tool_call_id: Option<String> = None;
        for msg in msgs.iter().rev() {
            if !matches!(msg.role, MessageRole::Assistant) {
                continue;
            }
            let Some(tcs) = msg.tool_calls.as_ref() else { continue };
            for tc in tcs {
                if tc.function.name != tool_name {
                    continue;
                }
                let Ok(old_args) = serde_json::from_str::<serde_json::Value>(&tc.function.arguments) else {
                    continue;
                };
                if canonical_args_key(&old_args) == new_args_key {
                    prev_tool_call_id = Some(tc.id.clone());
                    break;
                }
            }
            if prev_tool_call_id.is_some() {
                break;
            }
        }

        // 2) 用旧 tool_call_id 找对应 tool_result
        let Some(old_id) = prev_tool_call_id else { return None };
        for msg in msgs.iter().rev() {
            if !matches!(msg.role, MessageRole::Tool) {
                continue;
            }
            if msg.tool_call_id.as_deref() != Some(old_id.as_str()) {
                continue;
            }
            // 排除 pending 占位(仍在审批中,未真执行)
            if is_pending_placeholder(&msg.content) {
                return None;
            }
            return Some((old_id, msg.content.clone()));
        }
        None
    }).await;
    // 锁外:查审计表确认是 completed 成功结果(失败/拒绝不缓存,给 LLM 重试机会)
    let (old_id, content) = cached?;
    let status = audit_repo
        .find_by_tool_call_id(old_id.as_str())
        .await
        .ok()
        .and_then(|opt| opt.map(|rec| rec.status))
        .unwrap_or_else(|| "completed".to_string());
    if status != "completed" {
        return None;
    }
    Some(content)
}

/// 可缓存的只读幂等工具白名单。
/// 仅含纯读取/查询类工具:同参多次调用结果一致,缓存命中安全。
/// 写工具/有副作用工具(write_file/patch_file/run_command/create_*/update_*/delete_* 等)
/// 永不进此白名单——它们每次执行都可能改变状态,缓存会掩盖变化。
const READONLY_CACHE_TOOLS: &[&str] = &[
    "read_file",       // 读文件内容(同 path+offset+limit+search 参数 → 同结果)
    "read_symbol",     // AST 符号读取(同 path+symbol+full → 同结果)
    "list_directory",  // 列目录(同 path+recursive+max_depth → 同结果)
    "search_files",    // 搜文件名(同 path+pattern+offset+limit → 同结果)
    "grep",            // 搜文件内容(同 path+pattern+mode → 同结果)
];

/// 递归对 JSON 对象的键做字典序排序（就地），数组成员也递归排序。
fn sort_object_keys(v: &mut serde_json::Value) {
    match v {
        serde_json::Value::Object(map) => {
            // BTreeMap 按键排序，重建 Object
            let mut entries: Vec<(String, serde_json::Value)> = map
                .iter()
                .map(|(k, val)| (k.clone(), val.clone()))
                .collect();
            entries.sort_by(|a, b| a.0.cmp(&b.0));
            map.clear();
            for (k, mut val) in entries {
                sort_object_keys(&mut val);
                map.insert(k, val);
            }
        }
        serde_json::Value::Array(arr) => {
            for item in arr {
                sort_object_keys(item);
            }
        }
        _ => {}
    }
}
