//! 灵感来源(create_idea)消息级溯源补全。
//!
//! ## 背景
//! P1 把知识/审计的 `source_ref` 升级到消息级(`conv_msg:{id}`),但**灵感 `create_idea`
//! 漏了**:create_idea handler 是 `Fn(Value)→result` 无 message_id 上下文,source 由 AI
//! 自由填(`args["source"]`)。AI 多数场景不填(空),致灵感丢失溯源——查不到从哪条对话长出来。
//!
//! ## 方案 B(最小,低侵入 — 不改 AiToolHandler 接口)
//! 工具执行后(process_tool_calls / 审批执行路径)拿到 handler 返回的 result + AI 入参 args,
//! 检测 `tool_name == "create_idea"`:
//!   - 仅当 AI 未填 source(空 / null / 缺字段)且 message_id 可得 → 补 `source = conv_msg:{message_id}`
//!   - AI 已填(非空)→ **不覆盖**(保留 AI 有意义的自由文本,如"用户口述"/"xxx 文档")
//!   - message_id 缺失(None,异常路径/老数据)→ **不补**(无源可溯,保持原值,不伪造)
//!
//! ## 保守原则
//! 仅 create_idea 专用(其他工具不碰) / 仅 source 空时补(不覆盖 AI 填) / 无 message_id 不补。
//! 常量开关 `IDEA_SOURCE_AUTO_FILL_ENABLED` 可一键关闭(易迭代,出问题降级)。

use std::sync::Arc;

use df_storage::crud::IdeaRepo;
use df_storage::db::Database;

/// 一键开关:是否启用 create_idea source 自动补全。出问题改 false 即降级为 noop(不补)。
const IDEA_SOURCE_AUTO_FILL_ENABLED: bool = true;

/// 从 AI 入参 args 取 source,返回是否「AI 未填」(空 / null / 缺字段)。
///
/// 独立纯函数(便于单测):判定逻辑收敛一处,调用方与测试共享同一语义。
/// - `{"source": null}` / `{"source": ""}` / 缺 source 键 → true(未填,应补)
/// - `{"source": "用户口述"}` → false(AI 已填,不覆盖)
fn is_source_unfilled(args: &serde_json::Value) -> bool {
    match args.get("source") {
        None => true,
        Some(serde_json::Value::Null) => true,
        Some(serde_json::Value::String(s)) if s.trim().is_empty() => true,
        _ => false,
    }
}

/// 从 create_idea handler 返回的 result 取 idea_id(定位刚创建的灵感行)。
///
/// handler 返回 `{ "id": "...", "title": "...", "status": "draft" }`(tool_registry.rs create_idea)。
/// id 字段即 idea 表主键,用于 `IdeaRepo.update_field(id, "source", ...)`。
fn extract_idea_id(result: &serde_json::Value) -> Option<String> {
    result.get("id").and_then(|v| v.as_str()).map(|s| s.to_string())
}

/// 灵感来源消息级溯源补全(方案 B 核心)。
///
/// **保守**:仅在以下条件全部满足时补 source = `conv_msg:{message_id}`:
/// 1. `IDEA_SOURCE_AUTO_FILL_ENABLED == true`(一键开关)
/// 2. `tool_name == "create_idea"`(专用,其他工具不碰)
/// 3. `args.source` 未填(空 / null / 缺字段)—— AI 已填则**不覆盖**
/// 4. `message_id` 为 `Some`(无源可溯的异常/老数据不补,不伪造)
/// 5. result 含 `id`(idea_id,定位刚创建的灵感行)
///
/// 补全失败(库写失败等)仅 `tracing::warn` 不传播——溯源补全是 best-effort 增益,
/// 不应阻断工具已成功的执行流程(灵感已创建,source 缺失只是少了溯源,非业务失败)。
///
/// 单点逻辑(本函数)+ 多调用点(process_tool_calls 各执行路径 + 审批执行 chat.rs):
/// 调用点只管传 (tool_name, args, result, message_id),判定/补全/容错全在本函数。
pub async fn maybe_fill_idea_source(
    db: &Arc<Database>,
    tool_name: &str,
    args: &serde_json::Value,
    result: &serde_json::Value,
    message_id: Option<&str>,
) {
    // 条件 1: 一键开关
    if !IDEA_SOURCE_AUTO_FILL_ENABLED {
        return;
    }
    // 条件 2: 仅 create_idea 专用
    if tool_name != "create_idea" {
        return;
    }
    // 条件 3: AI 未填 source 才补(AI 已填不覆盖)
    if !is_source_unfilled(args) {
        return;
    }
    // 条件 4: message_id 缺失不补(无源可溯,不伪造)
    let msg_id = match message_id {
        Some(id) if !id.is_empty() => id,
        _ => return,
    };
    // 条件 5: result 含 idea_id(定位刚创建的灵感行)
    let idea_id = match extract_idea_id(result) {
        Some(id) => id,
        None => {
            tracing::warn!(
                tool = tool_name,
                "[idea-source] create_idea result 缺 id 字段,无法定位灵感行补 source(降级跳过)"
            );
            return;
        }
    };

    let source_val = format!("conv_msg:{}", msg_id);
    let repo = IdeaRepo::new(db);
    match repo.update_field(&idea_id, "source", &source_val).await {
        Ok(true) => {
            tracing::info!(
                idea_id = %idea_id,
                message_id = %msg_id,
                "[idea-source] 补全灵感来源消息级溯源: source = conv_msg:{}",
                msg_id
            );
        }
        Ok(false) => {
            // update_field 返 false = 影响行数 0(idea_id 不存在,异常,但灵感刚创建理论上必存在)
            tracing::warn!(
                idea_id = %idea_id,
                "[idea-source] update_field 影响 0 行(idea_id 不存在?);source 未补,灵感已创建不受影响"
            );
        }
        Err(e) => {
            // best-effort:溯源补全失败不阻断工具成功流程,仅 warn
            tracing::warn!(
                idea_id = %idea_id,
                error = %e,
                "[idea-source] 补全灵感来源失败(降级跳过,灵感已创建不受影响)"
            );
        }
    }
}

// ============================================================
// 单元测试 — 判定纯函数 + 补全全链路(内存 DB)
// ============================================================

// ============================================================
// 测试辅助(仅 cfg(test))— 内存 DB + 插入 IdeaRecord fixture
// ============================================================
// 独立 #[cfg(test)] 子模块:供本文件 tests 与同 crate 其他测试复用(防 DRY)。
// 不暴露到非测试构建(避免误用内存 DB 做 prod 路径)。

#[cfg(test)]
mod idea_source_test_helpers {
    use std::sync::Arc;

    use df_storage::crud::IdeaRepo;
    use df_storage::db::Database;
    use df_storage::models::IdeaRecord;
    use df_types::types::IdeaStatus;

    /// 建内存 DB(已跑 migrations,含 ideas 表)+ 插一条 IdeaRecord fixture,返回 db 句柄。
    ///
    /// `source`: None → 插 source=NULL(模拟 AI 未填);Some(s) → 插已填值。
    /// 其余字段用合理默认(title/description/status=draft/priority=1)。
    pub async fn idea_source_fixture_record(id: &str, source: Option<&str>) -> Arc<Database> {
        let db = Database::open_in_memory().await.expect("open_in_memory");
        let repo = IdeaRepo::new(&db);
        let now = df_types::now_millis().to_string();
        repo.insert(IdeaRecord {
            id: id.to_string(),
            title: format!("fixture-{}", id),
            description: String::new(),
            status: IdeaStatus::Draft,
            priority: 1,
            score: None,
            tags: None,
            source: source.map(|s| s.to_string()),
            promoted_to: None,
            ai_analysis: None,
            scores: None,
            related_ids: None,
            created_at: now.clone(),
            updated_at: now,
        })
        .await
        .expect("insert fixture idea");
        Arc::new(db)
    }
}

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

    // ---------- is_source_unfilled 纯函数 ----------

    #[test]
    fn source_missing_key_is_unfilled() {
        let args = serde_json::json!({ "title": "t" });
        assert!(is_source_unfilled(&args));
    }

    #[test]
    fn source_null_is_unfilled() {
        let args = serde_json::json!({ "title": "t", "source": null });
        assert!(is_source_unfilled(&args));
    }

    #[test]
    fn source_empty_string_is_unfilled() {
        let args = serde_json::json!({ "title": "t", "source": "" });
        assert!(is_source_unfilled(&args));
    }

    #[test]
    fn source_whitespace_only_is_unfilled() {
        let args = serde_json::json!({ "title": "t", "source": "   " });
        assert!(is_source_unfilled(&args));
    }

    #[test]
    fn source_non_empty_is_filled() {
        let args = serde_json::json!({ "title": "t", "source": "用户口述" });
        assert!(!is_source_unfilled(&args));
    }

    // ---------- extract_idea_id ----------

    #[test]
    fn extract_idea_id_from_result() {
        let result = serde_json::json!({ "id": "idea-1", "title": "t", "status": "draft" });
        assert_eq!(extract_idea_id(&result).as_deref(), Some("idea-1"));
    }

    #[test]
    fn extract_idea_id_missing_returns_none() {
        let result = serde_json::json!({ "title": "t" });
        assert!(extract_idea_id(&result).is_none());
    }

    // ---------- maybe_fill_idea_source 全链路(内存 DB)----------

    /// create_idea + source 空 + message_id 有 → 补 conv_msg:{id}
    #[tokio::test]
    async fn fills_source_when_unfilled_and_message_id_present() {
        let db = idea_source_fixture_record("idea-1", None).await;
        let args = serde_json::json!({ "title": "t" }); // source 缺
        let result = serde_json::json!({ "id": "idea-1", "title": "t", "status": "draft" });

        maybe_fill_idea_source(&db, "create_idea", &args, &result, Some("msg-42")).await;

        let repo = IdeaRepo::new(&db);
        let rec = repo.get_by_id("idea-1").await.unwrap().expect("idea-1 存在");
        assert_eq!(rec.source.as_deref(), Some("conv_msg:msg-42"));
    }

    /// create_idea + AI 已填 source → 不覆盖
    #[tokio::test]
    async fn does_not_overwrite_when_ai_filled_source() {
        let db = idea_source_fixture_record("idea-2", Some("用户口述")).await;
        let args = serde_json::json!({ "title": "t", "source": "用户口述" });
        let result = serde_json::json!({ "id": "idea-2", "title": "t", "status": "draft" });

        maybe_fill_idea_source(&db, "create_idea", &args, &result, Some("msg-42")).await;

        let repo = IdeaRepo::new(&db);
        let rec = repo.get_by_id("idea-2").await.unwrap().expect("idea-2 存在");
        assert_eq!(rec.source.as_deref(), Some("用户口述"), "AI 已填 source 不被覆盖");
    }

    /// message_id 缺失(None)→ 不补
    #[tokio::test]
    async fn does_not_fill_when_message_id_none() {
        let db = idea_source_fixture_record("idea-3", None).await;
        let args = serde_json::json!({ "title": "t" });
        let result = serde_json::json!({ "id": "idea-3", "title": "t", "status": "draft" });

        maybe_fill_idea_source(&db, "create_idea", &args, &result, None).await;

        let repo = IdeaRepo::new(&db);
        let rec = repo.get_by_id("idea-3").await.unwrap().expect("idea-3 存在");
        assert!(rec.source.is_none(), "无 message_id 不补 source");
    }

    /// message_id 空串("")→ 不补(等同 None 语义,防伪造)
    #[tokio::test]
    async fn does_not_fill_when_message_id_empty_string() {
        let db = idea_source_fixture_record("idea-3b", None).await;
        let args = serde_json::json!({ "title": "t" });
        let result = serde_json::json!({ "id": "idea-3b", "title": "t", "status": "draft" });

        maybe_fill_idea_source(&db, "create_idea", &args, &result, Some("")).await;

        let repo = IdeaRepo::new(&db);
        let rec = repo.get_by_id("idea-3b").await.unwrap().expect("idea-3b 存在");
        assert!(rec.source.is_none(), "空串 message_id 不补 source");
    }

    /// 非 create_idea 工具 → 不碰(其他工具不受影响)
    #[tokio::test]
    async fn does_not_touch_other_tools() {
        let db = idea_source_fixture_record("idea-4", None).await;
        let args = serde_json::json!({ "title": "t" });
        let result = serde_json::json!({ "id": "idea-4" });

        maybe_fill_idea_source(&db, "write_file", &args, &result, Some("msg-42")).await;

        let repo = IdeaRepo::new(&db);
        let rec = repo.get_by_id("idea-4").await.unwrap().expect("idea-4 存在");
        assert!(rec.source.is_none(), "非 create_idea 不碰 source");
    }

    /// result 缺 id 字段 → 不补(降级 warn,不 panic)
    #[tokio::test]
    async fn does_not_fill_when_result_missing_id() {
        let db = idea_source_fixture_record("idea-5", None).await;
        let args = serde_json::json!({ "title": "t" });
        let result = serde_json::json!({ "title": "t", "status": "draft" }); // 无 id

        maybe_fill_idea_source(&db, "create_idea", &args, &result, Some("msg-42")).await;

        let repo = IdeaRepo::new(&db);
        let rec = repo.get_by_id("idea-5").await.unwrap().expect("idea-5 存在");
        assert!(rec.source.is_none(), "result 缺 id 时不补,保持原值");
    }

    /// idea_id 不存在于库 → 不 panic(0 行影响 warn)
    #[tokio::test]
    async fn does_not_panic_when_idea_id_not_in_db() {
        let db = idea_source_fixture_record("idea-6", None).await;
        let args = serde_json::json!({ "title": "t" });
        // result 指向不存在的 idea-x
        let result = serde_json::json!({ "id": "idea-nonexistent", "title": "t", "status": "draft" });

        // 不应 panic,仅 warn
        maybe_fill_idea_source(&db, "create_idea", &args, &result, Some("msg-42")).await;

        // idea-6 原值不变
        let repo = IdeaRepo::new(&db);
        let rec = repo.get_by_id("idea-6").await.unwrap().expect("idea-6 存在");
        assert!(rec.source.is_none());
    }
}
