//! AI 工具实体参数 name→id 自动解析(机制层)
//!
//! 背景:DevFlow aichat 的 AI 工具要求 project_id 传 UUID,但用户/模型常用 name
//! (如 "moyu"/"u-talk"/"DevFlow"),`repo.query("project_id", pid)` 硬匹配 UUID 全返空,
//! 模型被迫绕 list_projects 找 UUID 浪费轮次。
//!
//! 本模块在工具执行前统一做 name→id 解析:
//! - 声明式映射表 [`RESOLVE_MAP`]:新工具需按 name 解析,只需在表里登记一行即生效
//!   (工具名 + 参数键名 + 实体类型),无框架/无配置。
//! - UUID 形态跳过:已是 id 的值直接透传,不查库(零额外开销)。
//! - 按 name 精确查 active 项目(list_active 排软删 + 内存过滤 `p.name == value`),
//!   命中唯一则替换为项目 id;0 命中 / 重名(≥2)返可行动错误,让 LLM 拿提示自修。
//!
//! 调用点:audit/mod.rs process_tool_calls 单点漏斗(auto + 审批 + 目录授权全部执行路径
//! 统一拿到已解析 id)。

use std::sync::Arc;

use df_storage::crud::ProjectRepo;
use df_storage::db::Database;

/// 实体类型:后续扩展 Task/Idea 时在此加枚举变体即可。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum EntityType {
    Project,
}

/// 声明式映射表:工具名 → [(参数键名, 实体类型)]。
/// 需按 name 解析的工具在此登记一行即生效。
pub(crate) static RESOLVE_MAP: &[(&str, &[(&str, EntityType)])] = &[
    ("list_tasks",           &[("project_id", EntityType::Project)]),
    ("create_task",          &[("project_id", EntityType::Project)]),
    ("get_task_count",       &[("project_id", EntityType::Project)]),
    ("get_project_timeline", &[("project_id", EntityType::Project)]),
    ("add_project_service",  &[("project_id", EntityType::Project)]),
    ("list_project_services",&[("project_id", EntityType::Project)]),
    ("list_project_modules", &[("project_id", EntityType::Project)]),
    ("update_project",       &[("id", EntityType::Project)]),
    ("bind_directory",       &[("id", EntityType::Project)]),
    ("delete_project",       &[("id", EntityType::Project)]),
    ("restore_project",      &[("id", EntityType::Project)]),
    ("purge_project",        &[("id", EntityType::Project)]),
];

/// 判定字符串是否为 UUID 形态(`Uuid::parse_str` 支持 simple/hyphenated/urn/braced 四格式)。
///
/// **严禁用 `contains('-')` 判定**——"u-talk"/"df-relay" 是真实项目名带连字符,会被误判为 UUID。
fn is_uuid_shape(s: &str) -> bool {
    uuid::Uuid::parse_str(s).is_ok()
}

/// 解析工具参数中的实体 name→id。
///
/// 规则(与调用点协商的契约):
/// - 工具未在 [`RESOLVE_MAP`] 登记 / 参数缺失 / 值非字符串 → 原样 `Ok(args.clone())`
///   (不拦截,交给工具 handler 正常处理)。
/// - 值是 UUID 形态 → 已是 id,跳过(不查库)。
/// - 非 UUID → 按 name 精确查 active 项目(list_active 排软删 + 内存过滤 `p.name == value`)。
///   命中唯一 → 替换该键为项目 id(保持其余字段);0 命中 / 重名(≥2)→ Err(结构化错误,
///   由调用点包 failed envelope 回传 LLM,让模型拿可行动提示自修)。
pub(crate) async fn resolve_entity_ids(
    db: &Arc<Database>,
    tool_name: &str,
    args: &serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
    // 映射表未登记该工具 → 原样透传(零侵入)
    let Some((_, key_entities)) = RESOLVE_MAP.iter().find(|(name, _)| *name == tool_name) else {
        return Ok(args.clone());
    };

    let mut resolved = args.clone();
    // 遍历该工具登记的所有实体参数键(当前每个工具 1 键,通用支持多键)
    for (key, entity_type) in *key_entities {
        // 参数缺失 / 值非字符串 → 跳过(工具 handler 自有缺参语义,不在此拦截)
        let Some(value) = resolved.get(*key).and_then(|v| v.as_str()) else {
            continue;
        };
        // UUID 形态 → 已是 id,跳过(不查库,零额外开销)
        if is_uuid_shape(value) {
            continue;
        }
        // 按 name 精确解析:list_active 排软删 + 内存过滤 p.name == value
        let id = match *entity_type {
            EntityType::Project => resolve_project_name(db, tool_name, key, value).await?,
        };
        // 替换该键为项目 id,保持其余字段
        if let Some(obj) = resolved.as_object_mut() {
            obj.insert(key.to_string(), serde_json::Value::String(id));
        }
    }
    Ok(resolved)
}

/// 按 name 精确解析 active 项目(list_active 排软删),返回唯一命中的项目 id。
/// 0 命中 / 重名(≥2)返可行动错误(含工具名 + 参数键 + 值),让 LLM 自修。
async fn resolve_project_name(
    db: &Arc<Database>,
    tool_name: &str,
    key: &str,
    value: &str,
) -> anyhow::Result<String> {
    let projects = ProjectRepo::new(db).list_active().await?;
    let matches: Vec<_> = projects.iter().filter(|p| p.name == value).collect();
    match matches.len() {
        0 => Err(anyhow::anyhow!(
            "工具 {} 的参数 {}={} 解析失败:未找到名为「{}」的项目(精确匹配,含空格/大小写)。可先调用 list_projects 核对项目名",
            tool_name, key, value, value
        )),
        1 => Ok(matches[0].id.clone()),
        _ => {
            // 重名:列出前 2 个(含 id+path),引导用 list_projects 区分后传具体项目 id
            let a = matches[0];
            let b = matches[1];
            Err(anyhow::anyhow!(
                "工具 {} 的参数 {}={} 命中多个同名项目:「{}」(id={}, path={})、「{}」(id={}, path={})。请用 list_projects 区分后传具体项目 id",
                tool_name, key, value,
                a.name, a.id, a.path.clone().unwrap_or_default(),
                b.name, b.id, b.path.clone().unwrap_or_default(),
            ))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use df_storage::models::ProjectRecord;
    use df_types::types::{ProjectStatus, new_id};

    /// 内存 DB + 插一条项目,返回 (db, project_id)
    async fn seed_project(db: &Arc<Database>, name: &str) -> String {
        let now = df_types::now_millis().to_string();
        let rec = ProjectRecord {
            id: new_id(),
            name: name.to_owned(),
            description: String::new(),
            status: ProjectStatus::Planning,
            idea_id: None,
            path: Some(format!("C:/projects/{}", name)),
            stack: None,
            created_at: now.clone(),
            updated_at: now,
        };
        ProjectRepo::new(db).insert(rec).await.unwrap()
    }

    #[test]
    fn is_uuid_shape_table() {
        let real_uuid = df_types::types::new_id();
        assert!(is_uuid_shape(&real_uuid), "真 UUID v4 应为 true");
        for s in ["u-talk", "moyu", "proj-svc-1", ""] {
            assert!(!is_uuid_shape(s), "「{}」不应判为 UUID", s);
        }
    }

    #[tokio::test]
    async fn resolve_unknown_tool_passthrough() {
        let db = Arc::new(Database::open_in_memory().await.unwrap());
        let args = serde_json::json!({ "path": "/tmp/x.rs" });
        let out = resolve_entity_ids(&db, "read_file", &args).await.unwrap();
        assert_eq!(out, args, "未登记工具应原样透传");
    }

    #[tokio::test]
    async fn resolve_unique_name_to_id() {
        let db = Arc::new(Database::open_in_memory().await.unwrap());
        let pid = seed_project(&db, "moyu").await;
        let args = serde_json::json!({ "project_id": "moyu", "status": "todo" });
        let out = resolve_entity_ids(&db, "list_tasks", &args).await.unwrap();
        assert_eq!(out["project_id"], serde_json::Value::String(pid));
        assert_eq!(out["status"], "todo", "其余字段应保持不变");
    }

    #[tokio::test]
    async fn resolve_uuid_passthrough() {
        let db = Arc::new(Database::open_in_memory().await.unwrap());
        let uuid = df_types::types::new_id();
        let args = serde_json::json!({ "project_id": uuid });
        let out = resolve_entity_ids(&db, "list_tasks", &args).await.unwrap();
        assert_eq!(out, args, "已是 UUID 应原样透传(不查库)");
    }

    #[tokio::test]
    async fn resolve_missing_param_passthrough() {
        let db = Arc::new(Database::open_in_memory().await.unwrap());
        let args = serde_json::json!({ "status": "todo" });
        let out = resolve_entity_ids(&db, "list_tasks", &args).await.unwrap();
        assert_eq!(out, args, "参数缺失应原样透传");
    }

    #[tokio::test]
    async fn resolve_zero_hit_errors() {
        let db = Arc::new(Database::open_in_memory().await.unwrap());
        let args = serde_json::json!({ "project_id": "不存在" });
        let err = resolve_entity_ids(&db, "list_tasks", &args).await.unwrap_err();
        let msg = err.to_string();
        assert!(msg.contains("未找到"), "0 命中应报「未找到」,实际: {}", msg);
    }

    #[tokio::test]
    async fn resolve_duplicate_name_errors() {
        let db = Arc::new(Database::open_in_memory().await.unwrap());
        let pid1 = seed_project(&db, "DevFlow").await;
        let pid2 = seed_project(&db, "DevFlow").await;
        let args = serde_json::json!({ "project_id": "DevFlow" });
        let err = resolve_entity_ids(&db, "list_tasks", &args).await.unwrap_err();
        let msg = err.to_string();
        assert!(msg.contains("命中多个同名项目"), "重名应报,实际: {}", msg);
        assert!(msg.contains(&pid1), "错误应含第一个 id,实际: {}", msg);
        assert!(msg.contains(&pid2), "错误应含第二个 id,实际: {}", msg);
    }

    #[tokio::test]
    async fn resolve_soft_deleted_excluded() {
        let db = Arc::new(Database::open_in_memory().await.unwrap());
        let pid = seed_project(&db, "回收站项目").await;
        ProjectRepo::new(&db).soft_delete(&pid).await.unwrap();
        let args = serde_json::json!({ "project_id": "回收站项目" });
        let err = resolve_entity_ids(&db, "list_tasks", &args).await.unwrap_err();
        assert!(
            err.to_string().contains("未找到"),
            "软删项目不应解析到,实际: {}",
            err
        );
    }
}
