//! 项目基础设施配置相关命令(知识图谱 Phase 3 V31,对标设计 §2.3 + §五 + D10 安全边界)
//!
//! 记录项目依赖的基础设施(数据库/缓存/MQ/API 等),为 AI 执行任务时提供基础设施上下文:
//! "这项目用了什么数据库、Redis 在哪、有没有 MQ"。
//!
//! 关键设计(对标 events.rs Phase 2 模式 + project.rs IPC 模式):
//! - **service_type 白名单**:mysql/postgresql/sqlite/redis/mongodb/mq/api/other,
//!   校验下沉 ProjectServiceRepo::insert_validated / update_full_validated(单一真相源)。
//! - **D10 安全边界**:不存敏感凭证。ProjectServiceRepo 在 insert/update 时审查
//!   config_json / endpoint / remark 是否含 password/secret/token/api_key 子串,命中即拒。
//!   IPC 入参 schema 在此注明,config_json 不含密码 —— 凭证走环境变量。
//! - create/update IPC 返回完整记录(对标 create_project),前端拿回 id 直接用。

use serde::{Deserialize, Serialize};
use tauri::State;

use df_storage::crud::PROJECT_SERVICE_TYPES;
use df_storage::models::ProjectServiceRecord;
use df_types::types::new_id;

use crate::state::AppState;

use super::{err_str, now_millis};

// ============================================================
// 入参 / 出参结构
// ============================================================

/// 新增基础设施配置入参(对标设计 §五 add_project_service + D10 边界)。
///
/// ⚠️ **D10 安全边界**:不存敏感凭证(密码/密钥/Token)。config_json / endpoint / remark
/// 含 password/secret/token/api_key 子串会被 Repo 层审查拒绝。凭证走环境变量,此处只存
/// 连接信息(host/port/数据库名/连接池配置等元数据)。
#[derive(Debug, Deserialize)]
pub struct AddProjectServiceInput {
    pub project_id: String,
    pub name: String,
    /// 基础设施类型,白名单 mysql/postgresql/sqlite/redis/mongodb/mq/api/other
    pub service_type: String,
    /// 连接地址(localhost:3306 / URL),可选。不含凭证(query 带 token= 会被拒)
    #[serde(default)]
    pub endpoint: Option<String>,
    /// 类型相关配置 JSON 字符串(如 {"pool_size":10}),可选。不含密码字段
    #[serde(default)]
    pub config_json: Option<String>,
    /// 环境 development/staging/production,默认 development
    #[serde(default = "default_environment")]
    pub environment: String,
    #[serde(default)]
    pub remark: Option<String>,
}

/// 整体更新入参(对标 Repo::update_full_validated,保留 id 与 created_at)。
/// 全字段必填(整体替换语义,与 IdeaRepo::update_full 一致)。
#[derive(Debug, Deserialize)]
pub struct UpdateProjectServiceInput {
    pub id: String,
    pub project_id: String,
    pub name: String,
    pub service_type: String,
    #[serde(default)]
    pub endpoint: Option<String>,
    #[serde(default)]
    pub config_json: Option<String>,
    #[serde(default = "default_environment")]
    pub environment: String,
    #[serde(default)]
    pub remark: Option<String>,
}

/// 列表查询入参(对标设计 §五 list_project_services:按项目过滤,可选按环境过滤)。
#[derive(Debug, Deserialize)]
pub struct ListProjectServicesInput {
    pub project_id: String,
    /// 可选,按环境过滤(development/staging/production)。空/None = 返回所有环境。
    #[serde(default)]
    pub environment: Option<String>,
}

/// 列表返回(对标 events.rs TimelineResult,外层补 total 便于前端计数)。
#[derive(Debug, Serialize)]
pub struct ListProjectServicesResult {
    pub items: Vec<ProjectServiceRecord>,
    pub total: usize,
    pub project_id: String,
}

fn default_environment() -> String {
    "development".to_string()
}

// ============================================================
// IPC 命令
// ============================================================

/// 新增基础设施配置(对标设计 §五 add_project_service)。
///
/// service_type 白名单 + D10 凭证审查下沉 Repo::insert_validated(单一真相源,与 AI 工具
/// add_project_service 同源)。返回完整记录。
#[tauri::command]
pub async fn add_project_service(
    state: State<'_, AppState>,
    input: AddProjectServiceInput,
) -> Result<ProjectServiceRecord, String> {
    let project_id = input.project_id.trim().to_string();
    let name = input.name.trim().to_string();
    let service_type = input.service_type.trim().to_string();
    if project_id.is_empty() {
        return Err("project_id 不能为空".to_string());
    }
    if name.is_empty() {
        return Err("name 不能为空".to_string());
    }
    if service_type.is_empty() {
        return Err("service_type 不能为空".to_string());
    }
    // service_type 白名单 fail-fast(给清晰错误,虽 Repo 层也会校验)
    if !PROJECT_SERVICE_TYPES.contains(&service_type.as_str()) {
        return Err(format!(
            "非法 service_type: {service_type},合法值: {:?}",
            PROJECT_SERVICE_TYPES
        ));
    }

    let record = ProjectServiceRecord {
        id: new_id(),
        project_id,
        name,
        service_type,
        endpoint: input
            .endpoint
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty()),
        config_json: input
            .config_json
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty()),
        environment: input.environment.trim().to_string(),
        remark: input
            .remark
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty()),
        // created_at/updated_at 由 Repo 内部覆盖,此处占位
        created_at: now_millis(),
        updated_at: now_millis(),
    };
    // insert_validated 内含 service_type 白名单 + D10 凭证审查(密码/token/api_key 子串拒绝)。
    let id = state
        .project_services
        .insert_validated(record)
        .await
        .map_err(err_str)?;
    state
        .project_services
        .get_by_id(&id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| "插入后回读失败".to_string())
}

/// 整体更新基础设施配置(保留 id 与 created_at,updated_at 由 Repo 内部覆盖)。
///
/// service_type 变更须走此整体更新路径(对标 Repo 注释:update_field 白名单不含 service_type,
/// 防旁路 update_field 改类型)。D10 凭证审查下沉 update_full_validated。
#[tauri::command]
pub async fn update_project_service(
    state: State<'_, AppState>,
    input: UpdateProjectServiceInput,
) -> Result<ProjectServiceRecord, String> {
    let id = input.id.trim().to_string();
    let project_id = input.project_id.trim().to_string();
    let name = input.name.trim().to_string();
    let service_type = input.service_type.trim().to_string();
    if id.is_empty() {
        return Err("id 不能为空".to_string());
    }
    if project_id.is_empty() || name.is_empty() || service_type.is_empty() {
        return Err("project_id / name / service_type 不能为空".to_string());
    }
    if !PROJECT_SERVICE_TYPES.contains(&service_type.as_str()) {
        return Err(format!(
            "非法 service_type: {service_type},合法值: {:?}",
            PROJECT_SERVICE_TYPES
        ));
    }

    // 先校验存在(404 友好错误),再整体更新
    let existing = state
        .project_services
        .get_by_id(&id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("基础设施配置 {id} 不存在"))?;

    let record = ProjectServiceRecord {
        id: id.clone(),
        project_id,
        name,
        service_type,
        endpoint: input
            .endpoint
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty()),
        config_json: input
            .config_json
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty()),
        environment: input.environment.trim().to_string(),
        remark: input
            .remark
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty()),
        // 保留原 created_at(update_full_validated 不覆盖 created_at)
        created_at: existing.created_at,
        updated_at: now_millis(),
    };
    let hit = state
        .project_services
        .update_full_validated(&record)
        .await
        .map_err(err_str)?;
    if !hit {
        return Err(format!("基础设施配置 {id} 不存在(更新未命中)"));
    }
    state
        .project_services
        .get_by_id(&id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| "更新后回读失败".to_string())
}

/// 删除基础设施配置(按 id 物理删,对标 Repo::delete)。
#[tauri::command]
pub async fn remove_project_service(
    state: State<'_, AppState>,
    id: String,
) -> Result<bool, String> {
    let id = id.trim().to_string();
    if id.is_empty() {
        return Err("id 不能为空".to_string());
    }
    state.project_services.delete(&id).await.map_err(err_str)
}

/// 查询项目基础设施配置(对标设计 §五 list_project_services)。
///
/// 按项目过滤,可选按环境过滤(production 部署任务只取 production 服务,开发调试取 development,
/// 环境隔离避免误连生产库)。返回时间倒序列表。
#[tauri::command]
pub async fn list_project_services(
    state: State<'_, AppState>,
    input: ListProjectServicesInput,
) -> Result<ListProjectServicesResult, String> {
    let project_id = input.project_id.trim().to_string();
    if project_id.is_empty() {
        return Err("project_id 不能为空".to_string());
    }
    let env_filter = input
        .environment
        .as_deref()
        .map(str::trim)
        .filter(|s| !s.is_empty())
        .map(|s| s.to_string());

    let items = if let Some(env) = &env_filter {
        state
            .project_services
            .list_by_project_env(&project_id, env)
            .await
            .map_err(err_str)?
    } else {
        state
            .project_services
            .list_by_project(&project_id)
            .await
            .map_err(err_str)?
    };
    let total = items.len();
    Ok(ListProjectServicesResult {
        items,
        total,
        project_id,
    })
}
