//! Agentic 循环上下文生命周期:F-15 自动压缩(智能裁剪)。
//!
//! 从 `mod.rs::run_agentic_loop` 机械抽取(L1017-1193),行为零变更。
//!
//! 触发条件:history_tokens > budget*0.6 且 保护区外有可压缩消息 且 未在压缩中。
//!
//! 流程(对齐 ai_chat_compress_context IPC 的 read-but-don't-mutate 模式):
//! ① 读 active 克隆(不改 status / 不扣 token)→ 喂 LLM 出摘要;
//! ② LLM 成功 → compress_old_messages(标 compressed + 扣 token)+ insert_at(摘要 system);
//! ③ LLM 失败 → 消息状态完全不变(未改 status / 未扣 token),降级走原 build_for_request 裁剪。
//!
//! 口径决策(任务让"你判断"):选**延迟 mutate(成功才改)**而非"失败回滚 status"。
//!   理由:ContextManager.history_tokens 字段私有、无 set_history_tokens 公开接口;
//!         若先 compress_old_messages(扣 token)再 LLM,失败回滚需精确恢复 history_tokens,
//!         但 ChatMessage 克隆不含 token_count,无法等量加回——回滚 token 不精确。
//!         延迟 mutate 则失败时零副作用(消息状态/token 完全不变),语义最干净。
//!   注:延迟 mutate 的窗口(active_msgs 读出→LLM 出摘要期间)不持锁,但 loop 串行无并发
//!       (本函数独占 session_arc,工具执行/审批分支在 stream 之后),故此窗口内 messages 不变。
//!
//! 安全:复用 loop 顶部已 build+验证 的 provider(不再 build_provider_for 重复 resolve
//!   keyring),api_key 经 df_storage::secret 闭环;summary/error payload/日志均不含 api_key。
//!   is_compressing 防重入:set_compressing(true/false) 成对(LLM 调用前后均复位)。
//!   单轮问答(history_tokens 未超 0.6*budget)不触发,零行为变化。
//! messages 操作改 per_conv。

use std::sync::Arc;

use tauri::{AppHandle, Emitter, Manager};
use tokio::sync::Mutex;

use df_ai::context_helpers::{extract_keyword_summary, CompressedSummary};
use df_ai::provider::{ChatMessage, LlmProvider};
use df_storage::models::AiProviderRecord;

use crate::state::{AppState, LlmConcurrency};

// compress_via_llm 位于 commands::ai::compress(mod.rs 的 super::compress)。
use super::super::compress::compress_via_llm;
use super::{AiChatEvent, AiSession};

/// 压缩保护区条数(对齐 clear/compress IPC 的 PROTECT_COUNT=6)。
/// protect_start = len.saturating_sub(PROTECT_COUNT):保护区内的最近 6 条(含本轮三元组)
/// 不参与压缩,避免压缩正在使用的活跃消息。
const PROTECT_COUNT: usize = 6;

/// 改进3 B 常量开关:压缩失败时是否启用关键词摘要兜底(默认 true)。
///
/// true(默认):LLM 压缩失败 → compress_old_messages 标 compressed(释放 token)
///   + insert_at(0, system 关键词摘要),保留用户反复提及的主题词作续接锚点。
/// false(回退):回退原裸裁剪行为(不插关键词摘要,仅 build_for_request 裁剪保最近 6 条)。
///   排障/对比用:置 false 即可观察无兜底时的裁剪效果。
const KEYWORD_FALLBACK_ENABLED: bool = true;

/// 自动压缩(智能裁剪)——在 build_for_request 之前预处理。
///
/// 从 `run_agentic_loop` 抽取,行为零变更。返回值:
/// - `true`:conv 已删除,调用方应立即 `return`(退出整个 loop)。
/// - `false`:正常结束(无论是否触发压缩、压缩是否成功),调用方继续后续流程。
///
/// 参数沿用原内联代码引用的局部:
/// - `provider`:loop 顶部 build 的 provider(F-04b 切换 candidate 后由调用方传入最新)。
/// - `provider_config`:同上,compress 沿用主 candidate(非 fallback 范围)。
/// - `iteration`:当前轮次(仅用于日志)。
pub(super) async fn maybe_auto_compress(
    session_arc: &Arc<Mutex<AiSession>>,
    conv_id: &str,
    app_handle: &AppHandle,
    provider: &Box<dyn LlmProvider>,
    provider_config: &AiProviderRecord,
    llm_concurrency: &LlmConcurrency,
    iteration: usize,
    sys_tokens: u32,
) -> bool {
    let prev_compressing = session_arc.lock().await.conv_read(&conv_id).map(|c| c.messages.is_compressing()).unwrap_or(false);
    if !prev_compressing {
        // 读触发条件(history_tokens / budget / has_compressible_messages),持锁快照判断。
        let (should_compress, protect_start, pre_compress_tokens) = {
            let session = session_arc.lock().await;
            let mgr = match session.conv_read(&conv_id) {
                Some(c) => c,
                None => {
                    tracing::warn!(stale_conv = %conv_id, "[ai] conv 已删除,loop 退出(压缩段入口)");
                    return true;
                }
            };
            let mgr = &mgr.messages;
            let protect_start = mgr.len().saturating_sub(PROTECT_COUNT);
            let history_tokens = mgr.history_tokens();
            let available = mgr.budget_limit().saturating_sub(sys_tokens);
            // 触发阈值 0.6*available(可用预算=budget-sys_tokens):available*6/10 < history_tokens
            let should = protect_start > 0
                && (available as u64) * 6 / 10 < history_tokens as u64
                && mgr.has_compressible_messages(protect_start);
            (should, protect_start, history_tokens)
        };

        if should_compress {
            // emit 压缩开始 + 置位防重入 + 读 active 克隆(不改 status)。
            let ev = AiChatEvent::AiCompressing {
                conversation_id: Some(conv_id.to_string()),
            };
            let _ = app_handle.emit("ai-chat-event", ev.clone());
            // L3 emit 双写:tunnel subscriber透传 miniapp
            let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(ev);
            let (mut active_msgs, lang) = {
                let mut session = session_arc.lock().await;
                let conv = session.conv(&conv_id);
                conv.messages.set_compressing(true);
                // 读 active 克隆(不改 status):filter is_active,LLM 失败则消息状态完全不变。
                let active_msgs: Vec<ChatMessage> = conv.messages.messages_mut()
                    [..protect_start]
                    .iter()
                    .filter(|t| t.message.is_active())
                    .map(|t| t.message.clone())
                    .collect();
                let lang = conv.agent_language.clone()
                    .unwrap_or_else(|| "zh-CN".to_string());
                (active_msgs, lang)
            };

            // T2-修复(方案A):展开 namespace 引用为真实内容,再喂压缩 LLM/关键词摘要。
            // 同 agentic/mod.rs 的 build_for_request 出口展开。未展开则压缩 LLM 只看到
            // "namespace://..." 占位符,摘要基于占位符丢主题。read_only None(条目被 LRU 淘汰 /
            // 重启后 namespace 清空 / 跨会话残留引用)→ 替换为 EVICTED_PLACEHOLDER 提示文案
            // (非保留无意义 namespace:// URI)+ warn 日志。
            {
                let session = session_arc.lock().await;
                for m in &mut active_msgs {
                    if df_ai::namespace_store::is_namespace_ref(&m.content) {
                        let path = m.content.clone();
                        if let Some(original) = session.namespace_store.read_only(&path) {
                            m.content = original.to_string();
                        } else {
                            tracing::warn!(
                                conv_id = %conv_id,
                                path = %path,
                                "[namespace] 引用已淘汰,替换为提示文案"
                            );
                            m.content = df_ai::namespace_store::EVICTED_PLACEHOLDER.to_string();
                        }
                    }
                }
            }

            // 改进3 B:在 active_msgs move 进 compress_via_llm 前,先算关键词摘要兜底文本。
            // LLM 压缩失败时仍想保留用户反复提及的主题词(续接锚点),避免裸裁剪丢主题。
            // KEYWORD_FALLBACK_ENABLED=false → 跳过(回退原裸裁剪行为,排障/对比用)。
            let keyword_fallback: String = if KEYWORD_FALLBACK_ENABLED {
                extract_keyword_summary(&active_msgs)
            } else {
                String::new()
            };

            // 压缩调用(复用 loop 顶部已 build 的 provider,api_key 经 secret 闭环)。
            // 成功 → Some(summary);失败 → Err;无 active 可压缩(active_msgs 空)→ 视为 noop。
            let compress_outcome: Result<Option<CompressedSummary>, String> = if active_msgs.is_empty() {
                Ok(None)
            } else {
                compress_via_llm(
                    provider.as_ref(),
                    provider_config,
                    active_msgs,
                    &lang,
                    conv_id,
                    llm_concurrency,
                ).await.map(Some)
            };

            match compress_outcome {
                Ok(Some(summary)) => {
                    // LLM 成功 → 标 compressed(扣 token)+ 摘要 system 插首位 + set_compressing(false)。
                    {
                        let mut session = session_arc.lock().await;
                        let conv = session.conv(&conv_id);
                        let _compressed = conv.messages.compress_old_messages(protect_start);
                        // T3: 插入 NL 摘要(向前兼容),JSON 卡片可供后续 WorkingContext(T5)使用
                        conv.messages.insert_at(0, ChatMessage::system(&summary.nl_summary));
                        conv.messages.set_compressing(false);
                        // TODO(T5): 如有 WorkingContext,调用 version.reset_all(summary.json_card, turn)
                    }
                    tracing::info!(
                        conv_id = %conv_id,
                        iteration,
                        pre_tokens = pre_compress_tokens,
                        "[ai] 自动压缩成功,摘要已插首位"
                    );
                    // 治 Task#1:loop 自动压缩用 AiAutoCompressed(非手动变体),
                    // 桌面端静默(仅复位 isCompressing,不弹 toast 不 switchConversation)。
                    // miniapp 仍插摘要气泡(对端发生压缩告知用户)。
                    let ev = AiChatEvent::AiAutoCompressed {
                        conversation_id: Some(conv_id.to_string()),
                        summary: summary.nl_summary,
                    };
                    let _ = app_handle.emit("ai-chat-event", ev.clone());
                    // L3 emit 双写:tunnel subscriber透传 miniapp
                    let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(ev);
                }
                Ok(None) => {
                    // 保护区外无 active 可压缩(已全 compressed/archived)→ noop,仅复位 is_compressing。
                    session_arc.lock().await.conv(&conv_id).messages.set_compressing(false);
                }
                Err(e) => {
                    // LLM 失败 → 改进3 B:仍标 compressed 释放 token + 关键词摘要塞回首条(非裸裁剪)。
                    //
                    // 旧行为:消息状态完全不变,降级走 build_for_request 裁剪(丢主题)。
                    // 新行为(KEYWORD_FALLBACK_ENABLED=true 默认):
                    //   - compress_old_messages 标 [..protect_start] active 为 compressed(释放 token,
                    //     与成功路径一致,后续 build_for_request 不再把它们进 LLM 上下文);
                    //   - keyword_fallback 非空 → insert_at(0, system 关键词摘要)作续接锚点;
                    //   - keyword_fallback 空(无 user 消息/无可提取词)→ 不插,等价旧裁剪(保底)。
                    // 持久化语义不变:compressed 仍软删可追溯(DB 全量保留),与成功路径一致。
                    // KEYWORD_FALLBACK_ENABLED=false → 跳过兜底,等价旧行为(set_compressing(false) +
                    //   消息状态不变,降级 build_for_request 裁剪)。
                    session_arc.lock().await.conv(&conv_id).messages.set_compressing(false);
                    tracing::warn!(
                        conv_id = %conv_id,
                        error = %e,
                        keyword_fallback_len = keyword_fallback.len(),
                        "[ai] 自动压缩失败,降级走关键词摘要兜底(KEYWORD_FALLBACK_ENABLED={})",
                        KEYWORD_FALLBACK_ENABLED,
                    );
                    if KEYWORD_FALLBACK_ENABLED {
                        // 标 compressed 释放 token + 关键词摘要塞首位(若非空)。
                        let inserted = {
                            let mut session = session_arc.lock().await;
                            let conv = session.conv(&conv_id);
                            let _compressed = conv.messages.compress_old_messages(protect_start);
                            if !keyword_fallback.is_empty() {
                                conv.messages.insert_at(0, ChatMessage::system(&keyword_fallback));
                                true
                            } else {
                                false
                            }
                        };
                        if inserted {
                            tracing::info!(
                                conv_id = %conv_id,
                                pre_tokens = pre_compress_tokens,
                                "[ai] 压缩失败兜底:关键词摘要已插首位(compressed 标记已扣 token)"
                            );
                        }
                    }
                    // 压缩失败不发 AiError——前端 AiError case 无条件 setStreaming(false)
                    // 误判生成结束(后端 loop 实际继续),致"压缩后停止"。降级为 warn(上方 line 957 已有)
                    // + compress_old_messages 兜底释放 token + build_for_request 裁剪,loop 继续 stream_llm,
                    // 用户自然看到后续回复。压缩降级对用户透明(非致命错误,不该停流)。
                }
            }
        }
    }
    false
}
