//! AI 聊天命令模块 — 流式对话、工具调用、审批门控、提供商管理
//!
//! 由原单文件 ai.rs(2663 行)按职责拆分为 11 个子模块。
//!
//! 模块布局：
//! - [`commands`] — 所有 `#[tauri::command]` IPC 函数
//! - [`agentic`] — run_agentic_loop / try_continue_agent_loop / DEFAULT_MAX_AGENT_ITERATIONS
//! - [`stream_recv`] — stream_llm 流式接收
//! - [`conversation`] — save_conversation / TokenAccumulator / accumulate_tokens
//! - [`title`] — 对话标题生成
//! - [`audit`] — 工具调用审计 + pending 审批恢复 + 工具调用处理
//! - [`skills`] — 本机 Claude 技能扫描
//! - [`prompt`] — 系统提示词构建
//! - [`provider_pool`] — F-260614-04 多 Provider 负载均衡池选择(纯逻辑)
//! - [`compress`] — F-15 上下文压缩 LLM 摘要(compress_via_llm 公共函数)
//! - [`tool_registry`] — AI 工具注册表构建 + 文件路径校验
//! - [`knowledge_inject`] — 知识库注入 + 提炼
//!
//! 事件协议：通过 app.emit("ai-chat-event", payload) 流式推送到前端
//! - AiTextDelta: 流式文本片段
//! - AiToolCallStarted/Completed: 工具调用生命周期
//! - AiApprovalRequired: 需要人工审批
//! - AiCompleted/AiError: 完成/错误

pub mod agentic;
pub mod audit;
pub mod commands;
pub mod compress;
pub mod conversation;
pub mod http;
pub mod knowledge_inject;
pub mod prompt;
pub mod provider_pool;
pub mod secret;
pub mod skills;
pub mod stream_recv;
pub mod title;
pub mod tool_registry;

use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;

use serde::{Deserialize, Serialize};

// RiskLevel 不在此 import:PendingApproval.risk_level 删后 mod.rs 不再用(audit 流程在 audit.rs 自 import)
use df_ai::context::ContextManager;
use df_ai::context::ContextConfig;

// ============================================================
// 重导出 — 保路径不变(state.rs / lib.rs / knowledge.rs 在用)
// ============================================================

// commands 子模块(glob 重导出) — `#[tauri::command]` 宏生成的命令函数 +
// 内部符号(__cmd__xxx / __tauri_command_name_xxx)同源同模块定义,
// glob 把它们全部拉到 commands::ai 路径,使 generate_handler! 能解析到。
// 静默 unused_imports:glob 重导出用于跨模块路径解析,本文件不引用这些符号。
#[allow(unused_imports)]
pub use self::commands::*;

// 子模块的非命令 pub 项,逐个保路径
#[allow(unused_imports)]
pub use self::audit::restore_pending_approvals;
#[allow(unused_imports)]
pub use self::knowledge_inject::{spawn_embedding_for_knowledge, trigger_extraction_now};
#[allow(unused_imports)]
pub use self::tool_registry::build_ai_tool_registry;

// ============================================================
// 事件载荷类型(放 mod.rs,各子文件经 use super::* 拿到)
// ============================================================

/// AI 错误分类(B-260615-42)：供前端按错误源差异化提示(如 auth 引导填 key,
/// network 提示检查连接,provider_config 引导检查 base_url/model)。
///
/// 设计：`Option<ErrorType>` 向后兼容——旧 emit 点若难精确分类填 `None`,
/// 旧前端消费方忽略未填值;新点尽量精确填并在注释标注映射理由。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ErrorType {
    /// 鉴权失败:API key 缺失/无效/钥匙串损坏(resolve_provider_secret / ensure_resolved_key 失败)
    Auth,
    /// 网络错误:连接失败/DNS 解析失败/流中途断(SSE 传输断)
    Network,
    /// 超时:连接超时/idle timeout(120s 无 chunk)
    Timeout,
    /// Provider 配置错误:base_url/model/provider_type 错或无可用 provider(配置丢失/全删)
    ProviderConfig,
    /// 未分类(达最大轮次/provider 流式错误事件等难精确归类的旧点)
    Unknown,
}

/// AI 聊天事件（推送到前端）
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type")]
pub enum AiChatEvent {
    /// 流式文本片段
    AiTextDelta { delta: String, conversation_id: Option<String> },
    /// 工具调用开始
    AiToolCallStarted { id: String, name: String, args: serde_json::Value, conversation_id: Option<String> },
    /// 工具调用完成
    AiToolCallCompleted { id: String, result: serde_json::Value, conversation_id: Option<String> },
    /// 会话级信任自动放行（AE-2025-04 Session Trust）：
    /// 同会话已批准过同类操作（同工具+同目录），下次命中自动放行，前端显示轻量 toast。
    /// 与 AiToolCallStarted/Completed 正交——这三个事件描述一次信任放行调用生命周期：
    /// Started → AutoApproved（toast 提示用户已自动放行）→ Completed。
    AiToolAutoApproved { id: String, tool: String, dir: String, conversation_id: Option<String> },
    /// 需要人工审批
    /// diff（AE-2025-03，路径 B）：仅 write_file 注入（旧文件 vs 新内容行级 unified diff），
    /// 供审批卡即时预览。旧文件不存在（新建）或非 write_file 工具为 None。
    AiApprovalRequired { id: String, name: String, args: serde_json::Value, reason: String, diff: Option<String>, conversation_id: Option<String> },
    /// 审批结果
    AiApprovalResult { id: String, approved: bool, conversation_id: Option<String> },
    /// AI 响应完成
    ///
    /// `incomplete`(可选,默认 None=false):UX-2025-04 / CR-30-2 / 决策 F-260616-07 a1——
    /// 流中途失败(MidStream)保文路径置 Some(true),标记此回复不完整。前端可据此差异化
    /// 展示(如附系统提示「⚠ 响应因网络中断不完整」)。正常完成/用户停止均 None(完整)。
    AiCompleted {
        total_tokens: u32,
        prompt_tokens: u32,
        completion_tokens: u32,
        /// 不完整标记(可选):Some(true)=网络中断保文,None 或 Some(false)=完整回复
        incomplete: Option<bool>,
        conversation_id: Option<String>,
    },
    /// 错误
    ///
    /// `error_type` 为错误源分类(B-260615-42),`None` 表示未分类(向后兼容旧 emit 点)。
    AiError {
        error: String,
        /// 错误源分类(可选,便于前端差异化提示);旧点/难精确分类填 None
        error_type: Option<ErrorType>,
        conversation_id: Option<String>,
    },
    /// Agent 循环新一轮（前端需新建 assistant 消息）
    AiAgentRound { round: u32, conversation_id: Option<String> },
    /// 流式心跳（静默期报活，前端 watchdog reset，区分「LLM 在跑」与「真断」）
    AiHeartbeat { conversation_id: Option<String> },
    /// 达最大轮次仍未收敛（F-260616-03：转暂停态询问用户继续/停止）
    ///
    /// 触发时 generating 仍为 true（仿审批等待），前端展示独立操作卡：
    /// - 点继续 → ai_continue_loop → try_continue_agent_loop 再跑 max_iterations 轮
    /// - 点停止 → ai_stop_loop → 走完成流程（save + AiCompleted + generating=false）
    AiMaxRoundsReached { conversation_id: Option<String> },
    /// 流式调用自动重试提示（F-260616-07：流前失败重试中）
    ///
    /// 每次重试前 emit，前端可在错误气泡内显示「重试 n/m」。
    /// attempt 从 1 开始(首次失败后第一次重试=attempt 1)，max_attempts = max_retries + 1。
    AiStreamRetry { attempt: u32, max_attempts: u32, conversation_id: Option<String> },
    /// F-15 阶段2 手动上下文分段完成:会话归档不删(archived_segment 标记),
    /// 前端可据此刷新消息视图(归档段折叠/隐藏)。
    AiContextCleared { conversation_id: Option<String> },
    /// F-15 阶段2 手动 LLM 压缩开始:前端展示压缩中态(spinner/禁用按钮防重入)。
    AiCompressing { conversation_id: Option<String> },
    /// F-15 阶段2 手动 LLM 压缩完成:摘要已插入消息首位,前端可展示摘要 + 移除压缩中态。
    AiCompressed { conversation_id: Option<String>, summary: String },
}

// ============================================================
// 会话状态(放 mod.rs,各子文件经 use super::* 拿到)
// ============================================================

/// AI 会话读侧状态视图（只读枚举，由 [`AiSession::session_state`] 推导）
///
/// 这是 **只读视图**，不替代 `AiSession` 上 `generating` / `pending_approvals` /
/// `stop_flag` 等字段定义——字段仍是唯一真相源，调用方写状态时继续写字段，
/// 读状态时统一走 `session_state()` 收敛判断逻辑（避免散布的 `if generating` /
/// `if !pending_approvals.is_empty()` 三路组合在各调用点各自实现）。
///
/// 优先级：`pending_approvals` 非空（有挂起审批优先报 AwaitingApproval，
/// 即使同时在流式）> `generating`（Streaming）> 都不满足（Idle）。
#[allow(dead_code)] // SW-260618-21 b:预留读视图(SW-02 类终态化复用·当前 0 消费者·保留扩展点)
pub enum SessionState {
    /// 空闲：既未生成、也无挂起审批
    Idle,
    /// 流式生成中：generating 为 true 且无挂起审批
    Streaming,
    /// 等待审批：pending_approvals 非空
    AwaitingApproval,
}

// ============================================================
// AE-2025-04 会话级信任（Session Trust）
//
// 核心：信任上下文相关。同会话批准过同类操作（同工具+同目录）后自动放行；
// 换会话（new/switch）清空重审（session_trust 随会话销毁，不落库）。
//
// TrustKey 按 操作 + 目录 粒度（非文件级——文件级粒度过细，LLM 每写一个文件都要重审，
// 信任面太窄失去意义；目录级让"批准过同目录写入"覆盖该目录后续写入，对齐"写→跑→看→改"
// 闭环最高频场景）。首批：write_file + run_command。
//
// FR-S1 安全边界：TrustKey 只含 tool + dir，**绝不**含 api_key / 文件内容 / 命令串等敏感
// 参数。dir 经 resolve_workspace_path/canonicalize 规范化（去 .. / symlink 逃逸）。
// ============================================================

/// 会话级信任键：按「操作类型 + 目录」粒度唯一标识一类已批准操作。
///
/// - `Write { dir }`：write_file 在 `dir` 目录下（路径父目录归一化）
/// - `Execute { dir }`：run_command 在 `dir` 工作目录下
///
/// 命中即自动放行，跳过 pending 审批 + 二次确认（AE-05），不写 audit 仍记一条
/// `decided_by=auto_trust` 的审计记录留痕。
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum TrustKey {
    Write { dir: String },
    Execute { dir: String },
}

/// 由「工具名 + args」推算 TrustKey（若该工具不在首批信任名单内返 None，走原审批流程）。
///
/// 目录归一化策略（与 tool_registry.rs 各 handler 的路径锚定一致）：
/// - write_file：取 args.path，resolve_workspace_path → 父目录；解析失败回退 workspace_root
/// - run_command：取 args.working_dir；缺省回退 workspace_root；不做越界校验
///   （run_command handler 自身仅 validate_path 不限定 workspace，信任目录用 LLM 传入或
///   workspace_root 默认值作为 key 字面量，与 handler 实际 working_dir 对齐）
///
/// FR-S1：不读 api_key / content / command 等敏感字段，只取 path/working_dir 归一化为 dir。
pub fn trust_key_for(tool: &str, args: &serde_json::Value) -> Option<TrustKey> {
    match tool {
        "write_file" => {
            let path = args.get("path").and_then(|v| v.as_str())?;
            let dir = dir_of_path_normalized(path);
            Some(TrustKey::Write { dir })
        }
        "run_command" => {
            let dir = match args.get("working_dir").and_then(|v| v.as_str()) {
                Some(d) => normalize_dir_key(d),
                None => workspace_root_str(),
            };
            Some(TrustKey::Execute { dir })
        }
        _ => None,
    }
}

/// write_file 路径 → 父目录规范化字符串（作 HashSet key 用）。
///
/// resolve_workspace_path 失败（路径越界/含 .. ）回退 workspace_root —— 失败的工具调用本身
/// 不会被执行（handler 内校验拦截），但 trust_key 计算不应 panic，失败回退保守默认。
fn dir_of_path_normalized(path: &str) -> String {
    match self::tool_registry::resolve_workspace_path_pub(path) {
        Ok(resolved) => {
            let parent = resolved
                .parent()
                .map(|p| p.to_path_buf())
                .unwrap_or_else(|| std::path::PathBuf::from("."));
            normalize_dir_key(&parent.to_string_lossy())
        }
        Err(_) => workspace_root_str(),
    }
}

/// run_command / write_file 共用的目录 key 规范化：
/// 尝试 canonicalize（去 symlink / .. / 大小写归一），失败回退原字面量 trim。
fn normalize_dir_key(dir: &str) -> String {
    let p = std::path::Path::new(dir);
    match p.canonicalize() {
        Ok(canon) => canon.to_string_lossy().to_string(),
        Err(_) => dir.trim_end_matches(['/', '\\']).to_string(),
    }
}

/// workspace_root 字符串（作 run_command working_dir 缺省时的 trust key 回退）。
///
/// 与 tool_registry.rs workspace_root() 同源（CARGO_MANIFEST_DIR 上两级），canonicalize 后
/// 保证与 run_command handler 默认 working_dir = workspace_root().to_string() 生成的 key 一致。
fn workspace_root_str() -> String {
    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(|p| p.parent())
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    root.canonicalize()
        .map(|c| c.to_string_lossy().to_string())
        .unwrap_or_else(|_| root.to_string_lossy().to_string())
}

// 引入 PathBuf 供 workspace_root_str / dir_of_path_normalized 使用
use std::path::PathBuf;

/// AI 会话内状态（Mutex 保护）
///
/// F-260616-09 B 批4(决策 e 真并发上线):会话级状态全部迁入 [`per_conv`](Self::per_conv)
/// HashMap(按 conversation_id 切分),顶层单例字段(messages/generating/stop_flag/notify/
/// iteration_used/agent_language/model_override/session_trust)已退役删除。批2 双写桥接代码
/// (IPC 写路径的顶层双写行 + 各文件 fallback 读)在批4 全部清理完毕,per_conv 是唯一真相源。
pub struct AiSession {
    /// 当前提供商 ID
    pub active_provider_id: Option<String>,
    /// 当前活跃对话 ID(应用级路由键:当前展示的 conv;切走即变,不影响后台 loop)
    pub active_conversation_id: Option<String>,
    /// 活跃对话创建时间(懒创建:首条消息落库前仅存内存,upsert 时用作 created_at)
    pub active_conv_created_at: Option<String>,
    /// 挂起的审批。
    ///
    /// **结构**：单层 `HashMap<tool_call_id, PendingApproval>`，按 `tool_call_id`
    /// 路由审批结果（`tool_call_id` 是路由键）。`PendingApproval.conversation_id`
    /// 是业务语义（哪个对话产生的审批）而非路由键——IPC 端按 `tool_call_id` 精确命中。
    ///
    /// **现状**：`ai_pending_tool_calls` 等读取路径按 `conversation_id` 做 O(n) 线性
    /// 过滤。在典型场景（单对话、少量并发审批）下 n 极小，O(n) 可接受，无需二级索引。
    ///
    /// **未来**：若多会话并发、审批量显著增大，可考虑加二级索引
    /// （`conversation_id → Vec<tool_call_id>`）把按会话过滤降到 O(1)。
    pub pending_approvals: HashMap<String, PendingApproval>,
    /// F-260616-09 B 阶段 多会话并发架构 — 会话级状态按 conversation_id 切分。
    ///
    /// **批4 状态(per_conv 唯一真相源)**:批2-批4 已迁移所有调用方(agentic.rs loop +
    /// GeneratingGuard + try_continue + commands.rs IPC 写路径 + audit.rs process_tool_calls +
    /// conversation.rs save + title.rs + knowledge_inject.rs + lib.rs L0)读写
    /// `session.conv(conv_id).*` / `session.conv_read(conv_id)`。顶层单例会话级字段已全部删除。
    pub per_conv: HashMap<String, PerConvState>,
}

impl AiSession {
    pub fn new() -> Self {
        Self {
            active_provider_id: None,
            active_conversation_id: None,
            active_conv_created_at: None,
            pending_approvals: HashMap::new(),
            per_conv: HashMap::new(),
        }
    }

    /// 推导指定会话读侧状态视图（只读，不写任何字段）。
    ///
    /// 优先级：该 conv 有 pending 审批 → [`SessionState::AwaitingApproval`]；
    /// 否则 per_conv.generating → [`SessionState::Streaming`]；否则 → [`SessionState::Idle`]。
    ///
    /// 收敛点：调用方读「某会话处于什么阶段」时统一走本方法，替代散布的
    /// `if pending / if generating` 三路组合。
    ///
    /// F-260616-09 B 批4:per_conv 化后接 conv_id 参数(顶层 generating 已删除);
    ///   pending 判断改为按 conv_id 过滤(决策 e 真并发下各 conv 独立)。
    ///
    /// # 待替换调用点（本次仅新增视图，不替换调用点）
    ///
    /// - `agentic.rs` — agent loop 入口/恢复处对 generating + 审批的组合判断
    /// - `commands.rs` — `ai_pending_tool_calls` 等读取前的状态门控
    /// - `commands.rs` — 审批提交/会话复位路径的状态判断
    ///
    /// 上述三处替换由 P0 任务承接，本方法先就位供其切换。
    #[allow(dead_code)] // SW-260618-21 b:预留(P0 终态化任务承接·当前 0 调用·保留扩展点)
    pub fn session_state(&self, conv_id: &str) -> SessionState {
        let has_pending = self.pending_approvals.values()
            .any(|a| a.conversation_id.as_deref() == Some(conv_id));
        if has_pending {
            SessionState::AwaitingApproval
        } else if self.conv_read(conv_id).map(|c| c.generating).unwrap_or(false) {
            SessionState::Streaming
        } else {
            SessionState::Idle
        }
    }

    // ============================================================
    // F-260616-09 B 阶段批1 PerConvState 访问器(设计 §2.1.2)
    //
    // 批1 仅定义访问器,**不迁移调用方**(批2 承接)。当前 0 调用方,行为完全不变。
    // 批2 迁移:所有 `session.messages` / `session.generating` 等读写改走
    //          `session.conv(conv_id).*`(写)/ `session.conv_read(conv_id)`(读)。
    // ============================================================

    /// 取或惰性创建指定会话的可变状态(设计 §2.1.2)。
    ///
    /// 不存在则 `entry().or_insert_with(PerConvState::new)` 创建一份默认状态,
    /// 初值对齐 [`AiSession::new`] / [`PerConvState::new`]。
    /// 已存在则返回同一实例引用(同 conv_id 复用)。
    pub fn conv(&mut self, conv_id: &str) -> &mut PerConvState {
        self.per_conv
            .entry(conv_id.to_string())
            .or_insert_with(PerConvState::new)
    }

    /// 只读取指定会话状态(不创建)。
    ///
    /// 未创建的 conv_id 返 `None`,已创建返 `Some(&PerConvState)`。用于读取路径(如
    /// 列举/查询)避免误触发惰性创建。
    pub fn conv_read(&self, conv_id: &str) -> Option<&PerConvState> {
        self.per_conv.get(conv_id)
    }
}

#[cfg(test)]
mod tests_f09_per_conv {
    use super::*;

    /// 验证 PerConvState::new() 各字段初值正确(对齐 AiSession::new 基线)。
    #[test]
    fn test_per_conv_state_new() {
        let s = PerConvState::new();
        assert!(!s.generating, "generating 初值应为 false");
        assert_eq!(s.iteration_used, 0, "iteration_used 初值应为 0");
        assert!(s.agent_language.is_none(), "agent_language 初值应为 None");
        assert!(s.model_override.is_none(), "model_override 初值应为 None");
        assert!(s.session_trust.is_empty(), "session_trust 初值应为空 HashSet");
        assert!(s.created_at.is_none(), "created_at 初值应为 None");
        // stop_flag 初值 false(与 AiSession::new 对齐)
        assert!(
            !s.stop_flag.load(std::sync::atomic::Ordering::SeqCst),
            "stop_flag 初值应为 false"
        );
        // notify 是 Arc<Notify>,无可见状态,仅验证不 panic(构造即视为对齐)
        // messages 是 ContextManager,内部状态;仅验证能取到引用(初值对齐由 ContextManager::new 保证)
        let _messages_ref: &ContextManager = &s.messages;
    }

    /// 验证 conv() 首次创建 + 二次复用同一实例。
    #[test]
    fn test_conv_lazy_create() {
        let mut session = AiSession::new();
        assert!(session.per_conv.is_empty(), "新建 session 的 per_conv 应为空");

        // 首次 conv() 创建
        let ptr_first = {
            let s = session.conv("conv-a");
            assert!(!s.generating);
            assert_eq!(s.iteration_used, 0);
            s as *const PerConvState
        };
        assert_eq!(session.per_conv.len(), 1, "首次 conv() 后 per_conv 应有 1 项");

        // 二次 conv(同 conv_id) 返回同一实例(指针相等)
        let ptr_second = session.conv("conv-a") as *const PerConvState;
        assert_eq!(
            ptr_first, ptr_second,
            "同 conv_id 二次 conv() 应返回同一实例"
        );
        assert_eq!(session.per_conv.len(), 1, "二次 conv(同 id) 不应新增项");

        // 不同 conv_id 创建独立实例
        let _s_b = session.conv("conv-b");
        assert_eq!(session.per_conv.len(), 2, "不同 conv_id 应新建独立实例");
    }

    /// 验证 conv_read() 未创建的 conv_id 返 None(只读,不惰性创建)。
    #[test]
    fn test_conv_read_none() {
        let session = AiSession::new();
        // 未创建的 conv_id
        assert!(
            session.conv_read("never-created").is_none(),
            "未创建的 conv_id conv_read() 应返 None"
        );
        // conv_read 不触发创建(per_conv 仍空)
        assert!(
            session.per_conv.is_empty(),
            "conv_read 不应触发惰性创建"
        );

        // 已创建的 conv_id 返 Some(只读访问)
        let mut session = session;
        let _ = session.conv("exists");
        assert!(
            session.conv_read("exists").is_some(),
            "已创建的 conv_id conv_read() 应返 Some"
        );
    }
}

// ============================================================
// F-260616-09 B 阶段 多会话并发架构 — PerConvState(批1 纯新增)
//
// 设计文档: docs/02-架构设计/已编号方案/F-09-多会话并发架构设计-2026-06-19.md §2.1.2
//
// 批1 范围(纯新增无行为变化):
//   - 仅新增 PerConvState struct + new() + AiSession.per_conv 字段 + conv()/conv_read() 访问器
//   - **不迁移**现有单例字段(messages/generating/stop_flag/notify/iteration_used/
//     agent_language/model_override/session_trust 保留原样),不迁移现有调用方(批2 承接)
//   - per_conv HashMap 初始空,旧调用路径继续读写 AiSession 顶层单例字段,完全无感
//
// 共存期:批1 后顶层单例字段(唯一真相源)与 per_conv(全空,无写入)并存,行为不变。
// 批2 迁移:把所有调用方从 session.messages / session.generating 等改为 session.conv(conv_id).*,
//          迁移完成后顶层单例字段废弃删除(批3)。
// ============================================================

/// 单会话级状态(F-260616-09 B 阶段批1 纯新增,设计 §2.1.2)。
///
/// 持有当前会话(按 conversation_id 切分)私有的可变状态。批1 仅定义 + 惰性创建访问器,
/// **不迁移** AiSession 顶层单例字段(共存期顶层字段仍是唯一真相源)。
///
/// 字段初值逐字对齐 [`AiSession::new`](#method.new),保证批2 迁移调用方时行为不变。
#[allow(dead_code)] // F-09 B 批1 共存期:0 调用方(批2 迁移承接),字段待迁移后消费
pub struct PerConvState {
    /// 对话历史(ContextManager:会话级消息真相源,裁剪仅影响发送视图)
    pub messages: ContextManager,
    /// 是否正在生成
    pub generating: bool,
    /// 停止信号(会话级):ai_chat_stop 置位,agentic loop / stream_llm 检测后尽快退出
    pub stop_flag: Arc<AtomicBool>,
    /// 即时停止唤醒(会话级):阻塞在 stream.next() 时 notify_one() 立即唤醒跳出 select!
    pub notify: Arc<tokio::sync::Notify>,
    /// agentic loop 累计已跑 iteration 计数(语义同 AiSession.iteration_used)
    pub iteration_used: usize,
    /// 当前 agent 循环的语言设置(用于审批后恢复循环)
    pub agent_language: Option<String>,
    /// 用户指定模型 override(主对话专用,语义同 AiSession.model_override)
    pub model_override: Option<String>,
    /// 会话级信任(Session Trust):随会话销毁,不落库
    pub session_trust: HashSet<TrustKey>,
    /// 会话创建时间(懒创建:首条消息落库前仅存内存,upsert 时用作 created_at)
    pub created_at: Option<String>,
}

impl PerConvState {
    /// 新建默认会话级状态。
    ///
    /// 各字段初值**逐字对齐 [`AiSession::new`]** —— 批2 迁移调用方时行为不变。
    /// - messages: ContextManager::new(ContextConfig::default())
    /// - generating: false
    /// - stop_flag: Arc::new(AtomicBool::new(false))
    /// - notify: Arc::new(tokio::sync::Notify::new())
    /// - iteration_used: 0
    /// - agent_language: None
    /// - model_override: None
    /// - session_trust: HashSet::new()
    /// - created_at: None(批1 新增字段,AiSession 现有 active_conv_created_at 同语义)
    pub fn new() -> Self {
        Self {
            messages: ContextManager::new(ContextConfig::default()),
            generating: false,
            stop_flag: Arc::new(AtomicBool::new(false)),
            notify: Arc::new(tokio::sync::Notify::new()),
            iteration_used: 0,
            agent_language: None,
            model_override: None,
            session_trust: HashSet::new(),
            created_at: None,
        }
    }
}

impl Default for PerConvState {
    fn default() -> Self {
        Self::new()
    }
}

/// 待审批的工具调用
#[derive(Debug, Clone)]
pub struct PendingApproval {
    pub tool_call_id: String,
    pub tool_name: String,
    pub arguments: serde_json::Value,
    pub conversation_id: Option<String>,
    /// 重启恢复的积压审批:无 live loop 持有 session.messages,审批后不 save(防空 messages 污染老对话)、不续跑
    pub recovered: bool,
    /// AE-2025-03（路径 B）：write_file 的行级 unified diff（旧文件 vs 新内容）。
    /// 仅挂起审批前预读注入，供前端审批卡预览；旧文件不存在（新建）或非 write_file 为 None。
    /// recovered 审批（启动重建）无 diff（文件可能已变，重读无意义）。
    #[allow(dead_code)] // IPC 活跃(useAiEvents:252+ToolCard·UX-260618-06)·cargo Rust never-read 误报
    pub diff: Option<String>,
}

/// 工具调用草稿（流式收集时的临时结构）
#[derive(Debug, Clone, Default)]
pub(crate) struct ToolCallDraft {
    pub(crate) id: String,
    pub(crate) name: String,
    pub(crate) args: String,
}

// ============================================================
// B-260617-01 决策 a(方案 a2)：run_workflow 工具经审批后的真实执行入口
//
// 背景：run_workflow handler(tool_registry.rs)仅持 db:Arc<Database>,无 AppHandle/State
// (registry/event_bus/workflows Repo),经 ai_tools.execute 必然 Err。ai_approve 识别 run_workflow
// 后改调本函数(同 invoke('run_workflow') IPC 入口 workflow.rs::run_workflow,持有完整 State),
// 真正触发工作流引擎执行 + 联动任务推进,把 execution_id 回填 tool_result 让 LLM 收成功结果停止重试。
//
// 选后端分支(a2)而非前端拦截(a1):ai_approve 已持 state+app,后端统一更简,无需新增前端 IPC +
// 不把工作流知识泄漏到前端。本函数仅在 ai_approve 内被调用(单一调用方)。
// ============================================================

/// 把 run_workflow 工具调用的 AI args 转调工作流执行函数,返回 execution_id(供回填 tool_result)。
///
/// 从 args 取 task_id + target_status(两者必填),传空 DagDef(由 workflow.rs 按 target_status
/// 自动选推进链模板,见 task_workflow_templates::template_for),config 传空 Object。
/// 任一参数缺失返 Err(回填 failed tool_result,LLM 据此修参重发)。
///
/// 不在此推进任务状态 —— workflow.rs::run_workflow 内部已按 task_id+target_status 自动联动
/// (成功推进到 target / 失败按 regression_target 退回),此处不重复。
pub(crate) async fn execute_run_workflow_for_tool(
    app: &tauri::AppHandle,
    state: &crate::state::AppState,
    args: &serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
    let task_id = args
        .get("task_id")
        .and_then(|v| v.as_str())
        .ok_or_else(|| anyhow::anyhow!("缺少 task_id 参数"))?
        .to_string();
    let target_status = args
        .get("target_status")
        .and_then(|v| v.as_str())
        .ok_or_else(|| anyhow::anyhow!("缺少 target_status 参数"))?
        .to_string();

    // 空 DagDef + target_status:workflow.rs:84-92 自动按 target_status 选推进链模板
    // (AiNode 自审 / HumanNode 核对闸门),无需本函数感知模板知识。
    let dag = df_workflow::dag_def::DagDef::new();
    let config = serde_json::Value::Object(serde_json::Map::new());
    let name = format!("AI 推进任务 {} → {}", task_id, target_status);

    // 转调工作流执行核心(B-260617-01:run_workflow_inner 是 run_workflow 命令的共用核心,
    // 同 invoke('run_workflow') IPC 入口的执行逻辑,持 &AppState 非 tauri::State,绕开
    // tauri::State 在非命令上下文不可构造的限制)。
    // CR-52: 本路径是 AI 工具经 ai_approve 审批后执行 → triggered_by="ai"(区别命令层 manual)。
    // 返回 execution_id 字符串;失败(模板不存在/DAG 校验失败/DB 写入失败)上抛 Err。
    let execution_id = crate::commands::workflow::run_workflow_inner(
        app,
        state,
        name,
        dag,
        config,
        Some(task_id.clone()),
        Some(target_status.clone()),
        "ai",
    )
    .await
    .map_err(|e| anyhow::anyhow!("工作流执行失败: {}", e))?;

    // 回填 tool_result:返回 execution_id + 联动语义说明,让 LLM 知工作流已触发(后台异步执行,
    // 完成后自动推进任务状态),不再重试同 tool_call。
    Ok(serde_json::json!({
        "task_id": task_id,
        "target_status": target_status,
        "execution_id": execution_id,
        "status": "running",
        "note": "工作流已触发(后台异步执行,完成后自动推进任务状态,失败按退回态回滚)。前端 workflow-event 跟踪进度。"
    }))
}
