//! provider 域 IPC — AI 提供商管理 / 模型拉取探测
//!
//! 由原 `commands.rs`(单文件 1829 行 God 文件)按域拆分,本文件聚焦 provider 域 7 个 IPC:
//! - 提供商 CRUD:`ai_list_providers` / `ai_save_provider` / `ai_set_provider` /
//!   `ai_delete_provider` / `ai_update_provider_pool`
//! - 模型拉取探测:`ai_fetch_models` / `ai_probe_model`
//! + provider 专用 helper:`mask_api_key`(api_key 脱敏 FR-S1)、
//!   `normalize_provider_type_for_fetch`(provider_type 归一)。
//!
//! 其他域(skills/config)仍留在 `commands/mod.rs`,后续批拆分。
//!
//! re-export:由 `commands/mod.rs` 经 `pub use self::provider::*;` 拉到 `commands::*`,
//! 再经 `ai/mod.rs` 的 `pub use self::commands::*;` 透传到 `commands::ai::*`,
//! 保 `lib.rs` invoke_handler + 前端 `api/ai.ts` 零改动。
//!
//! 纯搬迁:不改 IPC 签名/行为(FR-S1 密钥迁移 / F-260614-04c 池配置编辑保留 / F-01 阶段5 探测
//! 等逻辑原样保留)。

use tauri::State;

// df-ai 重导出 df_ai_core(供下游直接引用 trait/类型);src-tauri 不直接依赖 df-ai-core crate。
use df_ai::df_ai_core::model::ModelConfig;
use df_types::types::new_id;
use df_storage::models::AiProviderRecord;

use crate::state::AppState;
use crate::commands::{err_str, now_millis};

// provider.rs 的 super = commands,super::super = ai(与原 commands.rs 的 super=ai 等价)。
// 使用 async 版本(`*_async`):IPC 在 async 上下文,同步 keyring 调用会卡 tokio runtime
// (Tauri 单线程 runtime,Windows COM / Linux D-Bus 慢场景累加尾延迟)。
use super::super::secret::{
    get_provider_secret_async, set_provider_secret_async, delete_provider_secret_async,
};

// ============================================================
// 提供商管理
// ============================================================

/// api_key 脱敏:IPC 不传明文给前端(FR-S1),保留首尾各 4 字符便于辨识。
///
/// `pub(crate)` 供 get_app_config AI 工具复用(查 DevFlow 自身 provider 配置时同样脱敏,
/// 不向 LLM 回灌明文 key,与 ai_list_providers IPC 走同一套脱敏规则,单点维护)。
pub(crate) fn mask_api_key(key: &str) -> String {
    let chars: Vec<char> = key.chars().collect();
    if chars.len() <= 8 {
        return "•".repeat(chars.len());
    }
    let prefix: String = chars[..4].iter().collect();
    let suffix: String = chars[chars.len() - 4..].iter().collect();
    format!("{}••••{}", prefix, suffix)
}

/// 列出所有已配置的 AI 提供商（is_default 真相源为 DB，重启不丢）
#[tauri::command]
pub async fn ai_list_providers(state: State<'_, AppState>) -> Result<Vec<AiProviderRecord>, String> {
    let mut providers = state.ai_providers.list_all().await.map_err(err_str)?;
    // 收敛 is_default 唯一性自愈:历史脏数据(早期 ai_set_provider 未互斥/F-04 前
    // ai_save_provider 曾直接置默认)可能让多条 provider 同为 is_default=true,
    // UI 会显示多个"默认"徽章且 get_active_provider 取首条不可预期。此处检测到
    // 多于 1 条时,保留 created_at 最新的一条(与 ai_set_provider 语义一致:后设优先),
    // 其余清零并落库。Settings 每次打开必经此 IPC,作为单点收敛入口。
    let defaults: Vec<&AiProviderRecord> = providers.iter().filter(|p| p.is_default).collect();
    if defaults.len() > 1 {
        // created_at 最大者保留为默认(list_all 已按 created_at DESC 排序,首条即最新)
        let keep_id = defaults[0].id.clone();
        tracing::warn!(
            "[is_default] 检测到 {} 条 provider 同为默认,收敛保留 {} 清零其余",
            defaults.len(), keep_id
        );
        for p in &providers {
            if p.is_default && p.id != keep_id {
                let mut updated = p.clone();
                updated.is_default = false;
                updated.updated_at = now_millis();
                state.ai_providers.update_full(&updated).await.map_err(err_str)?;
            }
        }
        // 同步内存副本,供下方 mask 循环使用
        for p in &mut providers {
            if p.id != keep_id {
                p.is_default = false;
            }
        }
    }
    // IPC 不传明文 api_key(FR-S1):前端编辑用空 apiKey 表示不改,mask 后前端 realm 不持有明文。
    // 迁移后 DB api_key 空 → 从 keyring 取真实密钥再 mask(前端看到 mask 但不持有明文)
    for p in &mut providers {
        let real = if !p.api_key.is_empty() {
            p.api_key.clone() // 未迁移(老明文)
        } else {
            get_provider_secret_async(p.id.clone()).await.unwrap_or_default() // 迁移后从 keyring
        };
        p.api_key = if real.is_empty() { String::new() } else { mask_api_key(&real) };
    }
    Ok(providers)
}

/// 保存/更新 AI 提供商配置
#[tauri::command]
pub async fn ai_save_provider(
    state: State<'_, AppState>,
    id: Option<String>,
    name: String,
    base_url: String,
    api_key: String,
    default_model: String,
    provider_type: String,
    model_configs: Vec<ModelConfig>,
) -> Result<String, String> {
    // F-260618-06:接收前端传入的 model_configs 落库(含用户在 Settings 调的 weight/enabled/label),
    // 不再硬塞 Vec::new() 丢弃用户配置。新建传 []、编辑传回填+改动后的 providerForm.models。
    // 编辑已有提供商时保留原 created_at，避免被覆盖
    // F-260614-04c: 编辑路径同时保留原 enabled/weight(负载均衡池可编辑层)。
    // 前端经此 IPC 改 enabled/weight 落库;新建走默认 enabled=true/weight=50。
    let existing = match &id {
        Some(pid) => state.ai_providers.get_by_id(pid).await.map_err(err_str)?,
        None => None,
    };
    let created_at = existing.as_ref().map(|p| p.created_at.clone()).unwrap_or_else(now_millis);
    // is_default：编辑保留原值；新建时若全表尚无默认则设为默认（首个自动默认，避免无默认可用）
    let is_default = match &existing {
        Some(p) => p.is_default,
        None => !state.ai_providers.list_all().await
            .map_err(err_str)?
            .iter().any(|p| p.is_default),
    };
    // F-260614-04c: enabled/weight 编辑保留原值(前端 Settings 改值经此落库);
    // 新建默认进池(enabled=true,weight=50)。
    let (enabled, weight) = match &existing {
        Some(p) => (p.enabled, p.weight),
        None => (true, 50),
    };
    // FR-S1:密钥存 OS keyring,DB api_key 列恒空(不入明文)。
    // api_key 非空 = 新/改密钥 → 写 keyring;空 = 编辑不改 → 保留原 keyring 密钥不动。
    let provider_id = id.clone().unwrap_or_else(new_id);
    if !api_key.is_empty() {
        // 显式改/填密钥 → 写 keyring(现状不变)
        if let Err(e) = set_provider_secret_async(provider_id.clone(), api_key.clone()).await {
            return Err(format!("密钥保存到系统钥匙串失败: {}", e));
        }
    } else if let Some(pid) = &id {
        // 空 key 编辑:保住密钥,防未迁移态静默丢失(R-PD-1)。
        // 未迁移态(DB 有明文 + keyring 空)下,下方 INSERT OR REPLACE 会无条件清 DB api_key,
        // 唯一密钥副本被覆盖成空 → keyring 也空 → resolve 返空 → provider 报废密钥永久丢失。
        // 兜底:发现未迁移态先即时迁移补密钥,迁移成功后再让下方清 DB 明文(收敛到迁移完成态);
        // 迁移失败则 Err 阻断保存且 INSERT OR REPLACE 不执行 → DB 明文保留,绝不劣化现状。
        let old = state.ai_providers.get_by_id(pid).await
            .map_err(err_str)?;
        if let Some(old) = old {
            if !old.api_key.is_empty()
                && get_provider_secret_async(pid.clone()).await.is_none()
            {
                // DB 有明文 且 keyring 无 → 即时迁移补密钥
                if let Err(e) = set_provider_secret_async(pid.clone(), old.api_key.clone()).await {
                    return Err(format!(
                        "检测到该提供商密钥尚未迁移至系统钥匙串,本次保存尝试即时迁移失败({})。\
                         已保留原密钥未改动——请检查系统钥匙串权限后再次保存。",
                        e
                    ));
                }
                tracing::info!(
                    "[FR-S1] 编辑路径即时迁移 provider {} 密钥至 keyring(R-PD-1 兜底)",
                    pid
                );
            }
            // else: keyring 已有 / DB 已空 → 下方 INSERT OR REPLACE 清 DB 明文安全
        }
    }
    let api_key = String::new(); // DB 恒空(真实密钥在 keyring)
    let record = AiProviderRecord {
        id: provider_id,
        name,
        provider_type: if provider_type.is_empty() { "openai_compat".to_string() } else { provider_type },
        api_key,
        base_url,
        default_model,
        models: None,
        model_configs,
        is_default,
        config: None,
        created_at,
        updated_at: now_millis(),
        // F-260614-04c: enabled/weight 编辑保留原值(负载均衡池可编辑层),新建走默认。
        enabled,
        weight,
    };
    let id = record.id.clone();
    state
        .ai_providers
        .insert(record)
        .await
        .map_err(err_str)?;
    Ok(id)
}

/// F-260614-04c: 轻量更新 provider 池配置(enabled/weight),不改其它字段、不触密钥迁移。
///
/// 与 `ai_save_provider` 的区别:
/// - ai_save_provider 是全量保存(name/base_url/api_key/model...),编辑路径会走 R-PD-1 密钥
///   迁移 + INSERT OR REPLACE 全字段;前端 Settings「负载均衡池」开关/权重滑块仅需改这俩字段,
///   不应重发整张表(尤其避免空 api_key 触发密钥迁移分支)。
/// - 本 IPC 仅 UPDATE enabled/weight(经 update_field 或 update_full),重建 caps 表。
///
/// 落库后立即重建 per_provider caps(set_provider_caps),保证开关/权重变更对 agentic loop
/// 即时生效(下条消息即按新配置 acquire)。caps 重建非强一致(软收敛:已持 permit 不回收)。
#[tauri::command]
pub async fn ai_update_provider_pool(
    state: State<'_, AppState>,
    provider_id: String,
    enabled: bool,
    weight: u32,
) -> Result<(), String> {
    // 验证 provider 存在(防前端传错 id 静默无操作)
    let mut record = state
        .ai_providers
        .get_by_id(&provider_id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("提供商不存在: {}", provider_id))?;
    // weight 落库前 clamp 到 [0,100](对齐 crud update 的 weight.min(100),防越界)。
    let weight = weight.min(100);
    if record.enabled == enabled && record.weight == weight {
        // 无变化:跳过 DB 写 + caps 重建(幂等,防前端重复点击触发不必要的 IO)。
        return Ok(());
    }
    record.enabled = enabled;
    record.weight = weight;
    record.updated_at = now_millis();
    // update_full 走 UPDATE 全字段(含 enabled/weight,波12已加);api_key 不变(DB 恒空)。
    state
        .ai_providers
        .update_full(&record)
        .await
        .map_err(err_str)?;
    // 重建 per_provider caps:本 provider 被禁用/weight=0 → 不入新表 → acquire_for_provider
    // 对其返 None(无限流,但 provider_pool::select 已把它移出候选,实际不会被 acquire)。
    // caps 重建逻辑收敛到 AppState::reload_provider_caps(单点真理,启动 + 变更共用)。
    state.reload_provider_caps().await;
    Ok(())
}

#[tauri::command]
pub async fn ai_set_provider(
    state: State<'_, AppState>,
    provider_id: String,
) -> Result<(), String> {
    // 验证提供商存在
    let provider = state
        .ai_providers
        .get_by_id(&provider_id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("提供商不存在: {}", provider_id))?;

    // 互斥写 DB：目标 is_default=true，其余=false。仅写变化的记录。
    let providers = state.ai_providers.list_all().await.map_err(err_str)?;
    for p in &providers {
        let should = p.id == provider_id;
        if p.is_default != should {
            let mut updated = p.clone();
            updated.is_default = should;
            updated.updated_at = now_millis();
            state.ai_providers.update_full(&updated).await.map_err(err_str)?;
        }
    }

    let mut session = state.ai_session.lock().await;
    session.active_provider_id = Some(provider.id);
    Ok(())
}

/// 删除 AI 提供商
#[tauri::command]
pub async fn ai_delete_provider(
    state: State<'_, AppState>,
    provider_id: String,
) -> Result<(), String> {
    state.ai_providers.delete(&provider_id).await.map_err(err_str)?;
    // CR-260615-01:DB 已删则清 keyring 残留密钥(失败仅 warn 不阻断——无 DB 消费方,
    // 残留 keyring 不可复活;同 id 复用也不会读到旧密钥,因 set 覆盖写)
    if let Err(e) = delete_provider_secret_async(provider_id.clone()).await {
        tracing::warn!("[FR-S1] keyring 清理失败 {} (残留但无消费方,不阻断删除): {}", provider_id, e);
    }
    // 删除的若是当前默认，清空 active 指向，避免悬空
    let mut session = state.ai_session.lock().await;
    if session.active_provider_id.as_deref() == Some(&provider_id) {
        session.active_provider_id = None;
    }
    Ok(())
}

// ============================================================
// 模型列表拉取 + 单模型探测(F-01 阶段5 IPC)
// ============================================================

/// 将前端 provider_type 规范化为 fetch_and_probe 接受的类型。
///
/// Settings.vue 存的 provider_type 是 "openai_compat" / "anthropic"(对齐 build_provider 工厂),
/// 而 model_fetch::fetch_and_probe 分派用 "openai_compat" / "anthropic_compat"。
/// 两个工厂入口类型语义一致(anthropic 协议),仅命名不同,这里收敛归一。
fn normalize_provider_type_for_fetch(provider_type: &str) -> String {
    match provider_type {
        "anthropic" => "anthropic_compat".to_string(),
        other => other.to_string(),
    }
}

/// 测试连接并拉取厂商模型列表(F-01 阶段5)
///
/// 流程:
/// 1. DB 取 AiProviderRecord(get_by_id)
/// 2. FR-S1:经 resolve_provider_secret 内存解析真实 api_key(keyring 优先 fallback DB,
///    绝不进日志/返回值/错误信息)
/// 3. provider_type 归一(anthropic→anthropic_compat)+ base_url + api_key 调
///    df_ai::model_fetch::fetch_and_probe → Vec<ModelConfig>(每个模型名已探测出 4 维度)
/// 4. 写回 AiProviderRecord.model_configs(update_full)→ 返回 Vec<ModelConfig>
///
/// ModelConfig 本就无 api_key 字段,返回值天然不含密钥(FR-S1 闭环)。
#[tauri::command]
pub async fn ai_fetch_models(
    state: State<'_, AppState>,
    provider_id: String,
) -> Result<Vec<ModelConfig>, String> {
    let provider = state
        .ai_providers
        .get_by_id(&provider_id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("提供商不存在: {}", provider_id))?;

    // FR-S1:内存解析密钥,绝不外泄(不入日志/返回值/错误信息)
    let api_key = df_storage::secret::resolve_provider_secret(&provider);
    let fetch_type = normalize_provider_type_for_fetch(&provider.provider_type);

    let probed = df_ai::model_fetch::fetch_and_probe(&fetch_type, &provider.base_url, &api_key)
        .await
        .map_err(err_str)?;

    // 合并:新探测 configs 为主(更新能力维度 modalities/capabilities/cost_tier/intelligence/
    // context_window/probe_source),按 model_id 保留用户在 Settings 调过的 weight/enabled/label。
    // 否则每次「测试连接/拉取模型」覆盖回探测默认 weight(50),权重失效致 ProviderPool/router
    // 排序摇摆。对齐 provider 级 enabled/weight 编辑保留逻辑(commands.rs:1041 match existing)。
    // 新模型(旧池无同 model_id)用探测默认值。
    let merged: Vec<ModelConfig> = probed
        .iter()
        .map(|c| match provider.model_configs.iter().find(|o| o.model_id == c.model_id) {
            Some(old) => ModelConfig {
                weight: old.weight,
                enabled: old.enabled,
                label: old.label.clone(),
                ..c.clone()
            },
            None => c.clone(),
        })
        .collect();

    // 写回 model_configs(更新 updated_at)
    let mut updated = provider.clone();
    updated.model_configs = merged.clone();
    updated.updated_at = now_millis();
    state
        .ai_providers
        .update_full(&updated)
        .await
        .map_err(err_str)?;

    Ok(merged)
}

/// 一次性测试连接(P0-1):用**表单当前值**而非 DB 值测试,不写库。
///
/// 解决:`ai_fetch_models` 从 DB 读 provider,用户改了表单未保存就点「测试连接」时
/// 测的是旧值(base_url/api_key 已改但 DB 未落),给出误导性「成功/失败」。
///
/// 行为:
/// - api_key 非空 → 直接用表单值(base_url + api_key)测,完全不读 DB。
/// - api_key 空 + provider_id 给出 → 编辑态用户没重填密钥(留空=不改),
///   从 DB 取该 provider 记录经 FR-S1 内存解析出真实 key,base_url 仍用表单值
///   (用户可能改了 URL,密钥沿用旧)。
/// - api_key 空 + provider_id 空 → 新建态没填 key,直接报错(无来源可解析)。
///
/// 一次性:不写回 model_configs(测试≠落库);成功后用户可继续编辑或保存。
/// 保留 `ai_fetch_models`(从 DB 读 + 写回,用于「已保存配置重测」场景),本 IPC 不破坏它。
#[tauri::command]
pub async fn ai_test_connection(
    state: State<'_, AppState>,
    provider_type: String,
    base_url: String,
    api_key: String,
    provider_id: Option<String>,
) -> Result<Vec<ModelConfig>, String> {
    // 解析测试用 api_key:表单非空用表单;空则从 DB provider 沿用旧密钥(编辑不改 key 场景)
    let api_key = if !api_key.is_empty() {
        api_key
    } else {
        let pid = provider_id.ok_or_else(|| {
            "测试连接失败:api_key 为空且无已保存配置可沿用,请填写 api_key".to_string()
        })?;
        let provider = state
            .ai_providers
            .get_by_id(&pid)
            .await
            .map_err(err_str)?
            .ok_or_else(|| format!("提供商不存在: {}", pid))?;
        // FR-S1:内存解析,绝不外泄(不入日志/返回值/错误信息)
        df_storage::secret::resolve_provider_secret(&provider)
    };

    let fetch_type = normalize_provider_type_for_fetch(&provider_type);
    let probed = df_ai::model_fetch::fetch_and_probe(&fetch_type, &base_url, &api_key)
        .await
        .map_err(err_str)?;

    Ok(probed)
}

/// 单模型探测(F-01 阶段5):纯 CPU 启发式 + 预设表,无网络。
///
/// 用途:拉取后用户手动补一个模型名、或想重探某模型的能力维度。
/// 直接调 df_ai::model_probe::probe(&model_id),返回填充了 probe_source 的 ModelConfig。
#[tauri::command]
pub async fn ai_probe_model(
    _state: State<'_, AppState>,
    model_id: String,
) -> Result<ModelConfig, String> {
    Ok(df_ai::model_probe::probe(&model_id))
}
