//! 系统提示词构建 + 活跃提供商获取

use df_storage::models::AiProviderRecord;

use crate::commands::err_str;
use crate::state::AppState;

/// 获取当前活跃提供商配置
pub(crate) async fn get_active_provider(state: &AppState) -> Result<AiProviderRecord, String> {
    let session = state.ai_session.lock().await;
    if let Some(ref pid) = session.active_provider_id {
        let provider = state
            .ai_providers
            .get_by_id(pid)
            .await
            .map_err(err_str)?
            .ok_or_else(|| format!("活跃提供商不存在: {}", pid))?;
        Ok(provider)
    } else {
        // 查找默认提供商
        drop(session);
        let providers = state
            .ai_providers
            .list_all()
            .await
            .map_err(err_str)?;
        let default = providers.iter().find(|p| p.is_default).cloned();
        default
            .or_else(|| providers.into_iter().next())
            .ok_or_else(|| "未配置 AI 提供商，请先在设置中添加".to_string())
    }
}

/// 当前运行环境画像(注入 system prompt):日期 + OS + shell 执行姿势 + 可用解释器。
/// L1 环境感知(治 kms seq26-52 引号地狱):教 AI 避命令行 -c 内联,复杂脚本写文件执行。
///
/// - 日期:LLM 无法自行获取,不注入则写文件日期全靠猜
/// - OS + shell:平台对应 shell(Windows=PowerShell / Linux·macOS=bash),AI 写命令需对齐
/// - 执行姿势:复杂脚本(多行/含引号/$变量)写 .ps1/.py 文件用 `powershell -File`/`python` 执行,
///   避免命令行 -c 内联(cmd/PowerShell 引号转义易出错,kms 实证 20+ 次失败)
/// - 可用解释器:Windows=python(无 python3);Linux·macOS=python3;node 均有
///
/// 跨设备:cfg! 分支,平台各自正确姿势。补充探测见 detect_environment 工具(失败自愈)。
fn env_profile_line() -> String {
    let today = chrono::Local::now().format("%Y-%m-%d");
    let (os, shell, interp) = if cfg!(target_os = "windows") {
        ("Windows", "PowerShell", "python(无 python3)、node")
    } else if cfg!(target_os = "macos") {
        ("macOS", "bash/zsh", "python3、node")
    } else if cfg!(target_os = "linux") {
        ("Linux", "bash", "python3、node")
    } else {
        ("Unknown", "sh", "python3、node")
    };
    format!(
        "当前日期: {today} | 运行环境: {os} | shell: {shell}\n\
         执行姿势: 复杂脚本(多行/含引号/$变量)写成 .ps1 或 .py 文件,用 `powershell -File x.ps1` 或 `python x.py` 执行,避免命令行 -c 内联(引号转义易出错)。可用解释器: {interp}。\n\n"
    )
}

/// 按语言返回系统提示词的 (固定前缀, 项目上下文标题, 任务上下文标题)
fn system_prompt_parts(lang: &str) -> (&'static str, &'static str, &'static str) {
    match lang {
        "en" => (
            "You are DevFlow's AI assistant. You help users manage projects, tasks, ideas, and workflows.\n\
             Please respond in English.\n\n\
             ## Capabilities\n\
             You can perform the following actions via tool calls:\n\
             - Create/query projects, tasks, and ideas\n\
             - Run workflows\n\
             - Read file contents, list directories, create/write files\n\
             - To understand/locate a code symbol (function/struct/class) prefer read_symbol(path, symbol): returns a skeleton (signature + line range + internal calls, minimal and high-density), avoiding read_file dumping the whole file into the prompt; pass full=true for the full definition body; read_symbol supports .rs/.ts/.tsx/.js/.jsx/.vue — fall back to read_file only for other languages or when the symbol is not found\n\
             - Get file metadata (size, line count, binary detection)\n\
             - Append content to files, search files by name pattern\n\n\
             ## Guidelines\n\
             - Briefly explain your intent before executing actions\n\
             - Ask for clarification if the user's intent is unclear\n\
             - Prefer using tools to complete actions rather than just describing steps\n\
             - When a tool call fails, clearly tell the user it failed and why. Never disguise a fallback action as the original intent's success (e.g. don't write to description to fake a directory binding), and never falsely report success\n\
             ## Focus\n\
             - Always center your response on the core goal of the user's current request; the previous round's topic is only background, not the current task.\n\
             - When the user switches topics (a clear new intent), follow the latest request; do not drag the old topic into the new answer.\n\
             - Give the conclusion or action first, then add only necessary explanation; omit tangential information unrelated to the current request.\n\
             - Do not proactively expand context (files/data) that is irrelevant to the current request.\n",
            "\n## Current Projects\n",
            "\n## Current Tasks\n",
        ),
        _ => (
            "你是 DevFlow 的 AI 助手。你帮助用户管理项目、任务、灵感和工作流。\n\
             必须使用简体中文回复，禁止使用繁体中文字符。\n\n\
             ## 当前能力\n\
             你可以通过工具调用执行以下操作：\n\
             - 创建/查询项目、任务、灵感\n\
             - 运行工作流\n\
             - 读取文件内容、列出目录、创建/写入文件\n\
             - 理解/定位代码符号（函数/结构体/类等）优先用 read_symbol(path, symbol)：返回骨架（签名+行范围+内部调用，极小高密度），避免 read_file 全文回灌撑爆 prompt；需完整定义体传 full=true；read_symbol 支持 .rs/.ts/.tsx/.js/.jsx/.vue，其他语言或未找到符号时再退回 read_file\n\
             - 获取文件元信息（大小、行数、二进制检测）\n\
             - 追加写入文件、按名称模式搜索文件\n\n\
             ## 行为准则\n\
             - 执行操作前简要说明你的意图\n\
             - 如果不确定用户意图，先提问\n\
             - 优先使用工具完成操作，而不是只描述步骤\n\
             - 工具调用失败时必须明确告知用户失败原因，严禁用替代操作冒充原意图成功（如绑定目录失败不得改写描述冒充已绑定），也绝不谎报成功\n\
             ## 聚焦准则\n\
             - 始终围绕用户当前请求的核心目标回答；上一轮的主题只是背景，不是当前任务。\n\
             - 用户切换话题（明显的新意图）时，以最新请求为准，不要把旧话题带进新回答。\n\
             - 回答先给结论/动作，再补必要的解释；与当前请求无关的扩展信息省略。\n\
             - 需要的上下文（文件/数据）若与当前请求无关，不要主动展开。\n",
            "\n## 当前项目\n",
            "\n## 当前任务\n",
        ),
    }
}

/// 构建系统提示词（环境信息 + 固定前缀 + 当前项目/任务**全局清单**）
///
/// 本函数注入"全貌"清单:最近 20 项目 + 20 任务的 name/status/description(无 path),
/// 供 LLM 知道当前存在哪些实体(非精准引用)。
///
/// **被@实体的精准投影**(含 path 脱敏、剥 frontmatter 的 skill 正文)由 chat.rs 的
/// augmentation 层(`ResolverRegistry::resolve_all` + `build_augmentation_segment`)处理,
/// 非 build_system_prompt 职责 —— 两套并行:全局清单(全貌)+ augmentation(用户本次引用)。
///
/// 注入克制:仅各取最近 20 条,防 context 膨胀。
///
/// 保留旧签名供 agentic/mod.rs(无 mention 场景)等零改动调用,内部委托给
/// [`build_system_prompt_with_excluded`] 传两个空排除列表(无去重)。
pub(crate) async fn build_system_prompt(state: &AppState, lang: &str) -> String {
    build_system_prompt_with_excluded(state, lang, &[], &[]).await
}

/// 构建系统提示词(可去重版本)——主路径 chat.rs send / force_send 调用。
///
/// 与 [`build_system_prompt`] 的差异(机制而非说教):
/// 1. **去重**:被 @ 的 project/task 已由 augmentation 层精准投影(含 path/正文),清单中
///    再现会造成同一实体两次入 prompt(实测 build f64dee94 会话 list_projects 8 次 /
///    list_tasks 20 次)。此处把 `excluded_project_ids`/`excluded_task_ids` 命中的条目
///    从全局清单循环中跳过,被@实体只保留 augmentation 一次。
/// 2. **注明语**:清单末尾按语言追加一行,告知 LLM 「项目已全部列出无需再调 list_projects」/
///    「任务仅最近 20 条,全量查询请用 list_tasks」,从机制层降低工具与清单重复调用
///    (而非在行为准则里反复说教)。
///
/// - 空排除列表(无 mention 的调用)与 [`build_system_prompt`] 行为完全等价(仅多一行注明语)。
/// - 注入克制策略不变:仍各 take(20)。
pub(crate) async fn build_system_prompt_with_excluded(
    state: &AppState,
    lang: &str,
    excluded_project_ids: &[String],
    excluded_task_ids: &[String],
) -> String {
    let (prefix, proj_label, task_label) = system_prompt_parts(lang);
    let mut prompt = env_profile_line();
    prompt.push_str(prefix);

    // 附加当前数据上下文
    if let Ok(projects) = state.projects.list_active().await {
        if !projects.is_empty() {
            prompt.push_str(proj_label);
            // system prompt 前缀克制：仅最近 20 个项目，防 context 膨胀
            // 去重:被 @ 的项目跳过(已有 augmentation 精准投影,清单再现致同一实体两次入 prompt)
            for p in projects.iter().take(20) {
                if excluded_project_ids.iter().any(|id| id == &p.id) {
                    continue;
                }
                prompt.push_str(&format!("- {} ({}): {}\n", p.name, p.status, p.description));
            }
            // 机制层注明语(中/英):项目已全部列出,降 list_projects 重复调用
            prompt.push_str(&projects_listed_note(lang, 20));
        }
    }
    // 任务全貌清单(供 LLM 知道存在哪些任务;被@任务的精准投影走 augmentation 层)
    // 仅最近 20 条未删除任务,按 created_at DESC(同 list_active 顺序)。
    if let Ok(tasks) = state.tasks.list_active().await {
        if !tasks.is_empty() {
            prompt.push_str(task_label);
            // 去重:被 @ 的任务跳过(同项目去重机理)
            for tk in tasks.iter().take(20) {
                if excluded_task_ids.iter().any(|id| id == &tk.id) {
                    continue;
                }
                prompt.push_str(&format!("- {} ({}): {}\n", tk.title, tk.status, tk.description));
            }
            // 机制层注明语(中/英):仅最近 20 条,全量/按项目查询走 list_tasks
            prompt.push_str(&tasks_listed_note(lang));
        }
    }

    prompt
}

/// 项目清单尾部注明语(中/英)。
///
/// 机制层降重复:直接告知「已全部列出」,LLM 知道再调 list_projects 是冗余。
/// `shown` 取实际注入条数上限 20(避免传运态 list_active 长度,只描述清单本身)。
fn projects_listed_note(lang: &str, shown: usize) -> String {
    match lang {
        "en" => format!(
            "(Listed {} active projects above; the active set is shown, no need to call list_projects again.)\n",
            shown
        ),
        _ => format!(
            "(以上为活跃项目清单,无需重复调用 list_projects)\n"
        ),
    }
}

/// 任务清单尾部注明语(中/英)。
///
/// 机制层降重复:明确「仅最近 20 条 + 全量查询走 list_tasks」,避免 LLM 误以为清单即全量
/// 而反复调 list_tasks 校验(实测单会话 20 次)。
fn tasks_listed_note(lang: &str) -> String {
    match lang {
        "en" => "(Only the 20 most recent tasks are shown. For the full list or filtered by project, use the list_tasks tool.)\n".to_string(),
        _ => "(仅显示最近 20 条任务,全量或按项目查询请用 list_tasks 工具)\n".to_string(),
    }
}

/// 上下文压缩 system prompt 模板（F-15 §4.2，纯函数无 IO）。
///
/// 引导 LLM 把对话压缩为四段式结构化摘要（意图/决策/文件/约束），最大化保留
/// 旧消息语义。调用方把返回的 system prompt 放在消息列表首位，后接被压缩的
/// active 消息原文（user/assistant/tool）喂给 `provider.complete()`：
///
/// ```ignore
/// let mut msgs = vec![ChatMessage::system(compress_prompt(lang))];
/// msgs.extend(active_msgs);
/// let resp = provider.complete(CompletionRequest { messages: msgs, .. }).await?;
/// ```
///
/// `lang` 与系统提示词保持一致取值集合（"en" 英文，其余一律中文）。
//
// 调用方:compress.rs:85(compress_via_llm 内构造 system 消息)。
pub(crate) fn compress_prompt(lang: &str) -> &'static str {
    match lang {
        "en" => "You are a conversation summarizer. Compress the following conversation \
                 into a structured summary that preserves the essential context for \
                 continuing the work. Output exactly four sections:\n\
                 \n\
                 ## Intent\n\
                 The user's core intent and requirements (what they are trying to achieve).\n\
                 \n\
                 ## Decisions\n\
                 Key decisions already made (which approach/option was chosen, why alternatives \
                 were rejected).\n\
                 \n\
                 ## Files\n\
                 Files modified/created/inspected, with a one-line description of the change in each.\n\
                 \n\
                 ## Constraints\n\
                 Important constraints, preferences, or open issues to respect going forward.\n\
                 \n\
                 Rules:\n\
                 - Be concise; prefer bullet points.\n\
                 - Drop small talk and transient pleasantries; keep only technically load-bearing facts.\n\
                 - Preserve file paths, identifiers, and error messages verbatim.\n\
                 - Must preserve core topic words, entity names, and technical terms the user \
                   repeatedly mentions; they are anchors for continuing the conversation and losing \
                   them breaks the context thread.\n\
                 - Do NOT invent facts not present in the conversation.\n\
                 - Output the four sections only, no preamble or extra commentary.",
        _ => "你是对话总结器。请把以下对话压缩为结构化摘要,保留继续推进工作所必需的关键上下文。\
               严格按以下四段输出:\n\
               \n\
               ## 意图\n\
               用户的核心意图和需求(用户想要达成什么)。\n\
               \n\
               ## 决策\n\
               已做出的关键决策(选了哪种方案/选项,为何排除其他方案)。\n\
               \n\
               ## 文件\n\
               已修改/创建/查看过的文件,每条一行简述其变更内容。\n\
               \n\
               ## 约束\n\
               重要约束、偏好或待解决的问题,后续需要继续遵守或留意。\n\
               \n\
               要求:\n\
               - 简洁,优先用要点。\n\
               - 去掉寒暄、过渡性客套,只保留技术上有价值的事实。\n\
               - 文件路径、标识符、错误信息等照原样保留。\n\
               - 必须保留用户反复提及的核心主题词、实体名、技术名词(它们是对话续接的锚点,丢失会致上下文断裂)。\n\
               - 不要编造对话中没有的事实。\n\
               - 只输出上述四段内容,不要前言、解释或额外评论。",
    }
}

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

    // 改进1 系统提示聚焦段:中文版含「聚焦准则」独立段
    #[test]
    fn system_prompt_zh_has_focus_section() {
        let (prefix, _, _) = system_prompt_parts("zh-CN");
        assert!(
            prefix.contains("聚焦准则"),
            "中文 system prompt 应含聚焦准则段,实际: {}",
            prefix
        );
        // 独立段标题,非稀释在行为准则里
        assert!(prefix.contains("## 聚焦准则"));
        // 核心条款抽样
        assert!(prefix.contains("始终围绕用户当前请求的核心目标"));
        assert!(prefix.contains("也绝不谎报成功"));
    }

    // 改进1 系统提示聚焦段:英文版含「Focus」独立段
    #[test]
    fn system_prompt_en_has_focus_section() {
        let (prefix, _, _) = system_prompt_parts("en");
        assert!(
            prefix.contains("Focus"),
            "英文 system prompt 应含 Focus 段,实际: {}",
            prefix
        );
        assert!(prefix.contains("## Focus"));
        assert!(prefix.contains("never falsely report success"));
        assert!(prefix.contains("core goal of the user's current request"));
    }

    // 兜底:lang 未匹配回落中文,聚焦段仍存在
    #[test]
    fn system_prompt_unknown_lang_falls_back_zh_with_focus() {
        let (prefix, _, _) = system_prompt_parts("fr");
        assert!(prefix.contains("聚焦准则"));
    }

    #[test]
    fn compress_prompt_unaffected_by_focus_addition() {
        // 压缩 prompt 是独立函数,聚焦段改动不应波及
        let zh = compress_prompt("zh-CN");
        assert!(zh.contains("意图"));
        let en = compress_prompt("en");
        assert!(en.contains("Intent"));
    }

    // 改进3 A: 压缩 prompt 主题保留段(锚点词防上下文断裂)
    #[test]
    fn compress_prompt_zh_keeps_topic_anchor_clause() {
        let zh = compress_prompt("zh-CN");
        assert!(
            zh.contains("主题词"),
            "中文 compress_prompt 应含主题词/锚点保留要求,实际: {}",
            zh
        );
        assert!(zh.contains("锚点"));
        assert!(zh.contains("上下文断裂"));
    }

    #[test]
    fn compress_prompt_en_keeps_topic_anchor_clause() {
        let en = compress_prompt("en");
        assert!(
            en.contains("anchors"),
            "英文 compress_prompt 应含 anchors 保留要求,实际: {}",
            en
        );
        assert!(en.contains("topic words"));
        assert!(en.contains("breaks the context"));
    }

    // 项目/任务清单注明语(中/英)存在
    #[test]
    fn listed_notes_present_zh_en() {
        let zh_proj = projects_listed_note("zh-CN", 20);
        assert!(zh_proj.contains("无需重复调用 list_projects"));
        let en_proj = projects_listed_note("en", 20);
        assert!(en_proj.contains("no need to call list_projects"));

        let zh_task = tasks_listed_note("zh-CN");
        assert!(zh_task.contains("仅显示最近 20 条任务"));
        assert!(zh_task.contains("list_tasks"));
        let en_task = tasks_listed_note("en");
        assert!(en_task.contains("20 most recent tasks"));
        assert!(en_task.contains("list_tasks"));
    }
}
