//! 启动恢复:从审计表重建 pending_approvals(重启前未审批的工具调用,内存态已丢)
//!
//! 第二批从 audit/mod.rs 抽出(行为零变更,纯 fn 搬迁)。re-export 经 audit/mod.rs
//! `pub use restore::restore_pending_approvals;` 保持 `commands::ai::audit::*` 路径透明。

use std::collections::HashSet;

use crate::state::AppState;

use super::super::{ApprovalKind, PendingApproval};
use super::risk_from_str;

/// 启动恢复:从审计表重建 pending_approvals(重启前未审批的工具调用,内存态已丢)
///
/// pending_approvals 是 AiSession 内存 HashMap,重启必丢。ai_tool_executions 表已存
/// status=pending 的行(持久化真相源),此处读回重建内存态,使重启后待审批不丢。
/// 前端经 ai_pending_tool_calls 查询 + switchConversation 恢复 toolCard 的 pending_approval 态。
///
/// **多 conv 适配**:DB pending 审批按
/// `conversation_id` 分配到各 conv 的 per_conv state——对每个含 pending 审批的 conv
/// **惰性建 `PerConvState`**(不依赖 `active_conversation_id` 单值),使后续 ai_approve →
/// try_continue_agent_loop 的 `conv_read(conv_id)` 命中各自 per_conv(各归各,不串)。
/// `pending_approvals` 本身保持单层 HashMap(设计 §2.1.2,已带 conversation_id 是业务
/// 语义而非路由键)。conversation_id=None 的无主审批(R-9)不建 per_conv(无 conv_id 可挂),
/// 仍进 pending_approvals 单层表,后续审批按 tool_call_id 路由,不影响正确性。
pub async fn restore_pending_approvals(state: &AppState) {
    // 清理超 24 小时的残留 pending(旧会话遗留,不再有意义)
    if let Err(e) = state.ai_tool_executions.cleanup_stale_pending(86400).await {
        tracing::warn!("清理过期 pending 审批失败(非阻断): {}", e);
    }

    let pending = match state.ai_tool_executions.list_pending().await {
        Ok(v) => v,
        Err(e) => {
            tracing::warn!("启动恢复 pending 审批失败(非阻断): {}", e);
            return;
        }
    };
    if pending.is_empty() {
        return;
    }
    let mut session = state.ai_session.lock().await;
    // 多 conv 分配:先建 conv → per_conv 索引(惰性建 PerConvState::new),再 insert pending。
    // conv() 已自处理「已存在则复用」(同 conv_id 多条 pending 复用同一 PerConvState),
    // 故对每条 pending 都调一次 conv() 是幂等的(后续命中 entry().or_insert_with 跳过新建)。
    let mut convs_restored: HashSet<String> = HashSet::new();
    for rec in pending {
        // 损坏 arguments 不还原(语义保留:数据异常不恢复)——
        // unwrap_or_default() 会吞错误置空 {},污染 pending 还原成无参数 tool_call(语义错);
        // 改为解析失败 warn! + continue 跳过该条,与下方 risk_level 解析失败的损坏过滤语义对齐。
        let args: serde_json::Value = match serde_json::from_str(&rec.arguments) {
            Ok(v) => v,
            Err(e) => {
                tracing::warn!(
                    "跳过 pending 审批恢复(tool_call_id={}):arguments 解析失败(不还原以免污染无参数语义): {}",
                    rec.tool_call_id,
                    e
                );
                continue;
            }
        };
        // 过滤 risk_level 解析失败的损坏记录(语义保留:数据异常不恢复)·不再绑定 risk(PendingApproval.risk_level 已删)
        if risk_from_str(&rec.risk_level).is_none() {
            continue;
        }
        // 多 conv 分配:有 conversation_id 的审批惰性建/复用对应 conv 的 per_conv(设计 §3 batch8)。
        // 无 conversation_id 的无主审批(R-9)不建 per_conv——无 conv_id 可挂,仍进 pending_approvals
        // 单层表按 tool_call_id 路由,后续 ai_approve 路径不依赖其 per_conv。
        if let Some(cid) = rec.conversation_id.as_deref() {
            if !cid.is_empty() && convs_restored.insert(cid.to_string()) {
                // 惰性建:无则建 PerConvState::new,有则 or_insert_with 跳过(幂等,无副作用)。
                // 不在此恢复 messages——messages 由 switchConversation 从 DB reload 独立路径恢复,
                // 此处仅保证 conv 在 per_conv 中存在(供 conv_read 命中)。
                let _ = session.conv(cid);
            }
        }
        session.pending_approvals.insert(
            rec.tool_call_id.clone(),
            PendingApproval {
                tool_call_id: rec.tool_call_id,
                tool_name: rec.tool_name,
                arguments: args,
                conversation_id: rec.conversation_id,
                recovered: true,
                // 单真相源合并:恢复的审批一律 kind=Risk(diff=None,与原顶层 diff 字段同语义)。
                //
                // **path 审批不恢复的决策(语义保留)**:路径授权挂起是会话级状态,重启后 session
                // 重建,无法恢复挂起语义;且 path 的"always"决策已写入持久白名单(Settings KV),
                // 重启后白名单仍生效(文件工具路径预校验会直接 Authorized,不再挂起)。故 path 审批
                // 无需恢复 —— 恢复 risk 即可覆盖所有需人工决策的积压。
                //
                // 重启恢复的审批不重读旧文件——审批可能跨重启，
                // 期间文件可能已被外部改动，重读生成 diff 反映的不是当初决策时的状态，
                // 且恢复路径在 session.lock 内做 async IO 复杂度高，预览价值低。
                // 前端见 diff=None 时回退显新 content。
                kind: ApprovalKind::Risk { diff: None },
                // 重启恢复的审批 retry_count=0(恢复语义即"待用户首次决策",非重试)。
                // 即便审计表已有 pending 记录,恢复后用户审批执行属首次正常执行,不断路。
                retry_count: 0,
                // 恢复审批 created_at=重启时刻(无原挂起时间记录,按当前系统时间计)。
                // 超时取消计数仍从此刻起,避免恢复后立即被误判超时。
                created_at: Some(std::time::SystemTime::now()),
            },
        );
    }
    tracing::info!(
        "启动恢复: {} 条 pending 工具审批重建到内存(pending_approvals), 分布 {} 个 conv 的 per_conv",
        session.pending_approvals.len(),
        convs_restored.len()
    );
}

#[cfg(test)]
mod tests_multi_conv_restore {
    use super::super::{ApprovalKind, PendingApproval};
    use super::*;
    use crate::commands::ai::AiSession;

    /// 验证 restore_pending_approvals 的多 conv 分配不变量。
    ///
    /// restore_pending_approvals 受限于 AppState(需 DB),无法直接单测。但其核心分配逻辑
    /// 「对每个 conversation_id=Some 的恢复审批,惰性建/复用对应 conv 的 PerConvState」依赖
    /// 的契约是 AiSession::conv 的幂等惰性创建 + conv_read 可达。本测试用同一契约重现分配
    /// 逻辑,验证各 conv 各归各(不串)、无主审批(None)不建 per_conv。
    #[test]
    fn test_restore_multi_conv_distribution_invariant() {
        let mut session = AiSession::new();

        // 模拟 DB pending 行:3 条分属 2 个 conv(conv-a 两条 + conv-b 一条)+ 1 条 None 无主。
        let pending_rows: Vec<(String, String, Option<String>)> = vec![
            ("tc-1".to_string(), "write_file".to_string(), Some("conv-a".to_string())),
            ("tc-2".to_string(), "run_command".to_string(), Some("conv-a".to_string())),
            ("tc-3".to_string(), "delete_project".to_string(), Some("conv-b".to_string())),
            ("tc-4".to_string(), "create_task".to_string(), None), // 无主(R-9)
        ];

        // 复现 restore_pending_approvals 的分配逻辑(同名变量,逐字对齐实现)。
        let mut convs_restored: HashSet<String> = HashSet::new();
        for (tool_call_id, tool_name, conversation_id) in &pending_rows {
            if let Some(cid) = conversation_id.as_deref() {
                if !cid.is_empty() && convs_restored.insert(cid.to_string()) {
                    let _ = session.conv(cid); // 惰性建
                }
            }
            // 单真相源合并:恢复的 pending 全部进 pending_approvals,kind=Risk。
            session.pending_approvals.insert(
                tool_call_id.clone(),
                PendingApproval {
                    tool_call_id: tool_call_id.clone(),
                    tool_name: tool_name.clone(),
                    arguments: serde_json::Value::Null,
                    conversation_id: conversation_id.clone(),
                    recovered: true,
                    kind: ApprovalKind::Risk { diff: None },
                    // 恢复的审批 retry_count=0(首次用户决策,非重试)。
                    retry_count: 0,
                    created_at: None,
                },
            );
        }

        // 不变量 1:per_conv 含 conv-a + conv-b 各一份(惰性建,无主 None 不建)。
        assert_eq!(session.per_conv.len(), 2, "2 个 conv 各惰性建 1 份 per_conv");
        assert!(session.conv_read("conv-a").is_some(), "conv-a 恢复后 conv_read 应可达");
        assert!(session.conv_read("conv-b").is_some(), "conv-b 恢复后 conv_read 应可达");
        assert!(
            session.conv_read("never-restored").is_none(),
            "未恢复的 conv 不应被惰性建"
        );

        // 不变量 2:pending_approvals HashMap 4 条(含无主),conversation_id 保留(业务语义)。
        // 全部进单表 pending_approvals(原 risk_pending 合一,path 审批不恢复)。
        assert_eq!(session.pending_approvals.len(), 4, "全部 pending 入 pending_approvals");
        // 构造期恒等式:上方 pending_rows 构造的 4 条 PendingApproval 全部 kind=Risk(本测试构造
        // 时未插入任何 Path(_)),故 filter Path(_) 计数必为 0。这是构造恒等式而非外部不变量——
        // 对齐真实 restore_pending_approvals 的构造(实现 L78 一律 kind=Risk,无 Path 分支)。
        // 语义:首次 restore 无历史(Path 审批是会话级状态不恢复,恢复侧构造时本就不产出 Path)。
        let path_restored = session.pending_approvals.values()
            .filter(|a| matches!(a.kind, ApprovalKind::Path(_)))
            .count();
        assert_eq!(path_restored, 0, "构造期恒等式:恢复侧构造全部 kind=Risk,无 Path(首次 restore 无历史)");
        let conv_a_pending: Vec<_> = session
            .pending_approvals
            .values()
            .filter(|a| a.conversation_id.as_deref() == Some("conv-a"))
            .collect();
        assert_eq!(conv_a_pending.len(), 2, "conv-a 2 条 pending 各归各");
        let conv_b_pending: Vec<_> = session
            .pending_approvals
            .values()
            .filter(|a| a.conversation_id.as_deref() == Some("conv-b"))
            .collect();
        assert_eq!(conv_b_pending.len(), 1, "conv-b 1 条 pending");
        let ownerless: Vec<_> = session
            .pending_approvals
            .values()
            .filter(|a| a.conversation_id.is_none())
            .collect();
        assert_eq!(ownerless.len(), 1, "无主审批 1 条仍入 pending_approvals(R-9)");

        // 不变量 3:同 conv 多条 pending 复用同一 PerConvState(conv() 幂等,不重复建)。
        let conv_a_ptr = session.conv("conv-a") as *const _;
        let _ = session.conv("conv-a"); // 再次访问(模拟同 conv 二次 pending)
        let conv_a_ptr_again = session.conv("conv-a") as *const _;
        assert_eq!(conv_a_ptr, conv_a_ptr_again, "同 conv 复用同一 PerConvState(幂等)");
        assert_eq!(session.per_conv.len(), 2, "同 conv 二次访问不新增 per_conv 条目");
    }

    /// 验证 restore 仅含无主审批(None)时不建任何 per_conv(R-9 边界)。
    #[test]
    fn test_restore_only_ownerless_no_per_conv() {
        let mut session = AiSession::new();
        let mut convs_restored: HashSet<String> = HashSet::new();
        // 全部 None 的 pending 行:无主,不应触发任何 conv() 惰性建。
        let pending_rows: Vec<Option<String>> = vec![None, None, None];
        for conversation_id in &pending_rows {
            if let Some(cid) = conversation_id.as_deref() {
                if !cid.is_empty() && convs_restored.insert(cid.to_string()) {
                    let _ = session.conv(cid);
                }
            }
        }
        assert!(session.per_conv.is_empty(), "仅无主审批不应建任何 per_conv");
        assert!(convs_restored.is_empty(), "无 conv 被记录为已恢复");
    }
}
