//! 对话标题生成

use std::sync::Arc;

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

use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider, MessageRole};
// F-01 阶段5: 标题生成路由 — TaskRequirements(无需工具)。
// 标题是短文本摘要任务;池空/无匹配兜底 default_model(行为不变)。
// 注:路由已解耦(B-260618-03),原 Lite/Low 智力成本约束已删除,纯 weight 选模型。
use df_ai::router::{
    select_model_id, Modality, TaskRequirements,
};
use df_storage::crud::AiConversationRepo;
use df_storage::db::Database;
use df_storage::models::AiProviderRecord;

use crate::state::LlmConcurrency;

use super::AiSession;

/// 对话完成后按需生成智能标题（仅 title 为空时触发一次，不覆盖用户改名）
///
/// - 已有 title（用户改名或已生成）→ 跳过
/// - 否则调 LLM 非流式总结生成 ≤15 字标题；LLM 失败回退 extract_title 截断
/// - 完成后 emit ai-conversation-changed 通知前端侧栏刷新标题
pub(crate) async fn ensure_conversation_title(
    provider_config: &AiProviderRecord,
    db: &Arc<Database>,
    conv_id: &str,
    app_handle: &AppHandle,
    session_arc: &Arc<Mutex<AiSession>>,
    llm_concurrency: LlmConcurrency,
) {
    let conv_repo = AiConversationRepo::new(db);

    // 已有标题（用户改名或已生成）→ 不覆盖。
    // 但 "新对话" 是未生成占位（默认/LLM 返空兜底/清理后空回退）,视为未生成可重新生成,
    // 否则一旦 title 落"新对话"便 is_some()=true 永远跳过 → 标题卡死"新对话"。
    if let Ok(Some(rec)) = conv_repo.get_by_id(conv_id).await {
        if let Some(t) = &rec.title {
            if !t.is_empty() && t != "新对话" {
                return;
            }
        }
    }

    // 取对话文本（仅 user/assistant，跳过 tool 噪音），取前 6 条供 LLM 总结
    // F-260616-09 B 批4:per_conv.messages 唯一真相源(conv_id 来源:本函数入参)。
    //   conv() 惰性建:switch 触发的 spawn_ensure_title 前序 switch 已 conv();若极端未建,
    //   conv() 建空 PerConvState → summary_msgs 空 → 下方 return(无素材不生成标题)。
    let (summary_msgs, all_msgs) = {
        let mut session = session_arc.lock().await;
        let conv = session.conv(conv_id);
        let summary: Vec<ChatMessage> = conv.messages.iter()
            .filter(|m| matches!(m.role, MessageRole::User | MessageRole::Assistant))
            .take(6)
            .map(|m| ChatMessage {
                id: None, // 标题摘要派生消息:不落库不溯源,无需分配 ID(对齐老消息 None 语义)
                role: m.role.clone(),
                content: m.content.clone(),
                parts: None,
                tool_call_id: None,
                tool_calls: None,
                model: None,
                status: None,
                reasoning_content: m.reasoning_content.clone(),
                prompt_tokens: None,
                completion_tokens: None,
                timestamp: m.timestamp,
            })
            .collect();
        let all_msgs = conv.messages.all_messages_clone();
        (summary, all_msgs)
    };
    if summary_msgs.is_empty() {
        return;
    }

    // 任务3: 合并相邻同 role 消息(防 Anthropic 协议 1214 错误)。
    // session.messages 历史可能存在连续同 role(如 user 失败重提、助手多轮中断后续接、tool_result 被过滤后
    // user 连续),Anthropic API 严格校验「messages 必须 user/assistant 交替」,连续同 role 直接拒 1214。
    // 在传给 LLM 前合并连续同 role(拼接 content),不影响其他逻辑(本函数 summary 派生数据)。
    let summary_msgs = merge_consecutive_roles(summary_msgs);

    // B-260617-17 修复:先用 extract 兜底 emit 让侧栏即时更新（不落库），
    // 下方 LLM 生成成功后覆盖。这样 LLM 失败时有重试机会，
    // 不会永久锁定一个机械截取的差标题。
    let _fallback_title = extract_title(&all_msgs).unwrap_or_else(|| "新对话".to_string());
    let _ = app_handle.emit("ai-conversation-changed", ());

    // 标题生成是独立一次 LLM 调用，自建 provider（便于后台 spawn，不借主 loop 的 &dyn LlmProvider）
    let provider: Box<dyn LlmProvider> = match super::secret::build_provider_for(provider_config) {
        Ok(p) => p,
        Err(e) => {
            // 兜底已落库 + emit,LLM 跳过(密钥不可用等)
            tracing::warn!("标题 LLM 生成跳过(provider 密钥不可用,保留 extract_title 兜底): {}", e);
            return;
        }
    };
    // F-01 阶段5: 标题生成路由 — select_model_id None(池空/无匹配)→ 兜底 default_model。
    let title_req = TaskRequirements {
        modalities: vec![Modality::Text],
        needs_tool_use: false,
        estimated_context: 0,
        tier: None,
    };
    let title_model = select_model_id(&title_req, &provider_config.model_configs)
        .unwrap_or_else(|| provider_config.default_model.clone());
    // B-260617-17: LLM 调用包 20s 超时,防信号量/网络挂起致后台 task 永久堆积;
    // 超时或返回 None 均保留上方已落的 extract 兜底,不覆盖。
    let title = match tokio::time::timeout(
        std::time::Duration::from_secs(20),
        generate_title_via_llm(&*provider, &title_model, summary_msgs, conv_id, &llm_concurrency),
    ).await {
        Ok(Some(t)) => t,
        Ok(None) => {
            tracing::warn!("标题 LLM 生成返回空,保留 extract_title 兜底(conv_id={})", conv_id);
            return;
        }
        Err(_) => {
            tracing::warn!("标题 LLM 生成超时(20s),保留 extract_title 兜底(conv_id={})", conv_id);
            return;
        }
    };

    // LLM 生成成功且有效(非空非"新对话")→ 覆盖 extract 兜底。
    // LLM 返空/无效(clean_title 兜底"新对话")→ 保留上方已落的 extract 兜底,不覆盖(防弹回"新对话")。
    if !title.is_empty() && title != "新对话" {
        if let Err(e) = conv_repo.set_title(conv_id, &title).await {
            tracing::error!("对话标题 LLM 落库失败(conv_id={}, title={}): {}", conv_id, title, e);
        }
        let _ = app_handle.emit("ai-conversation-changed", ());
    }
}

/// 后台生成对话标题（不阻塞主流程；失败有 extract_title 兜底）
pub(crate) fn spawn_ensure_title(
    provider_config: &AiProviderRecord,
    db: &Arc<Database>,
    conv_id: &str,
    app_handle: &AppHandle,
    session_arc: &Arc<Mutex<AiSession>>,
    llm_concurrency: &LlmConcurrency,
) {
    let provider_config = provider_config.clone();
    let db = db.clone();
    let conv_id = conv_id.to_string();
    let app_handle = app_handle.clone();
    let session_arc = session_arc.clone();
    let llm_concurrency = llm_concurrency.clone();
    tauri::async_runtime::spawn(async move {
        ensure_conversation_title(&provider_config, &db, &conv_id, &app_handle, &session_arc, llm_concurrency).await;
    });
}

/// 调 LLM 非流式生成对话标题
async fn generate_title_via_llm(
    provider: &dyn LlmProvider,
    model: &str,
    msgs: Vec<ChatMessage>,
    conv_id: &str,
    llm_concurrency: &LlmConcurrency,
) -> Option<String> {
    let mut prompt = vec![ChatMessage::system(
        "你是标题生成器。根据用户与助手的对话，生成一个简短中文标题。要求：不超过15字，纯文本，不加引号不加书名号不加标点不加 emoji，只输出标题本身，不要任何前缀或解释。"
    )];
    prompt.extend(msgs);
    let request = CompletionRequest {
        model: model.to_string(),
        messages: prompt,
        temperature: Some(0.3),
        max_tokens: Some(30),
        stream: false,
        tools: None,
        tool_choice: None,
        reasoning_content: None,
    };
    // LLM 并发限流（标题生成属独立调用，纳入双层 Semaphore）
    // F-09 B 批5: per_conv 改 HashMap<conv_id>,标题针对本对话,用 conv_id 共享限流槽。
    let _global_permit = llm_concurrency.acquire_global().await;
    let _per_conv_permit = llm_concurrency.acquire_per_conv(conv_id).await;
    let resp = match provider.complete(request).await {
        Ok(r) => r,
        Err(e) => {
            // 诊断:标题 LLM complete 失败原因(网络/模型/超时)。ensure :121 防御已保留 extract 兜底,
            // 此日志助定位"为何标题未 LLM 精炼生成"(complete 失败 vs resp.text 空走 clean 兜底"新对话")。
            tracing::warn!("标题 LLM complete 失败(conv_id={}, model={}): {}", conv_id, model, e);
            return None;
        }
    };
    Some(clean_title(&resp.text))
}

/// 清理 LLM 返回的标题：去首尾引号/书名号/空白/末尾标点，截 15 字，空则兜底"新对话"
fn clean_title(raw: &str) -> String {
    let t = raw.trim()
        .trim_matches(|c: char| matches!(c, '"' | '\'' | '「' | '」' | '《' | '》' | ' ' | '\n' | '\r'));
    let t = t.trim_end_matches(|c: char| matches!(c, '。' | '.' | ',' | '，' | '！' | '!' | '？' | '?' | ':' | '：'));
    let cleaned: String = t.chars().take(15).collect();
    if cleaned.is_empty() { "新对话".to_string() } else { cleaned }
}

/// 从消息历史中提取对话标题。策略：取前 5 条 user 消息中字符数最多的那条。
///
/// 开头常出现测试/问候（"1"、"你好"等），取最长消息自然跳过这些噪音。
/// 极端情况全部过短时（仅测试消息），用第一条非空 user 兜底。
pub(crate) fn extract_title(messages: &[ChatMessage]) -> Option<String> {
    const MAX_TITLE_CHARS: usize = 60;
    const SCAN_LIMIT: usize = 5;

    let user_msgs: Vec<&str> = messages.iter()
        .filter(|m| matches!(m.role, MessageRole::User))
        .take(SCAN_LIMIT)
        .map(|m| m.content.trim())
        .filter(|s| !s.is_empty())
        .collect();

    // 取字符数最多的
    let best = user_msgs.iter()
        .max_by_key(|s| s.chars().count())
        .copied();

    match best {
        Some(text) => {
            let mut chars = text.chars();
            let t: String = chars.by_ref().take(MAX_TITLE_CHARS).collect();
            if chars.next().is_some() { Some(format!("{}...", t)) } else { Some(t) }
        }
        None => {
            // 极端情况全空 → 第一条非空 user（兜底）
            messages.iter()
                .find(|m| matches!(m.role, MessageRole::User) && !m.content.trim().is_empty())
                .map(|m| {
                    let mut chars = m.content.trim().chars();
                    let t: String = chars.by_ref().take(MAX_TITLE_CHARS).collect();
                    if chars.next().is_some() { format!("{}...", t) } else { t }
                })
        }
    }
}

/// 合并相邻同 role 消息,拼接 content(防 Anthropic 1214 连续同 role 错误)。
/// role 比较: System/User/Assistant/Tool 中,User 和 Assistant 交替校验最严,
/// 不同 role 不合并(保留边界),仅合并连续同 role。
fn merge_consecutive_roles(msgs: Vec<ChatMessage>) -> Vec<ChatMessage> {
    if msgs.is_empty() {
        return msgs;
    }
    let mut out: Vec<ChatMessage> = Vec::with_capacity(msgs.len());
    for m in msgs {
        if let Some(last) = out.last_mut() {
            // role 比较(MessageRole 未派生 PartialEq,用 matches!按变体逐个比)。
            let same_role = match (&last.role, &m.role) {
                (MessageRole::System, MessageRole::System)
                | (MessageRole::User, MessageRole::User)
                | (MessageRole::Assistant, MessageRole::Assistant)
                | (MessageRole::Tool, MessageRole::Tool) => true,
                _ => false,
            };
            if same_role {
                // 连续同 role: 拼接 content(中间补换行,避免语义粘连),保留首条元数据(id/timestamp)。
                // 不拼接 reasoning_content(标题摘要场景该字段无用,且不同 turn reasoning 串一起无意义)。
                if !m.content.is_empty() {
                    last.content.push('\n');
                    last.content.push_str(&m.content);
                }
                continue;
            }
        }
        out.push(m);
    }
    out
}
