//! 知识图谱 / 工程系统类 AI 工具声明式注册(register_task_graph_tools 10 个迁入)。
//!
//! 迁自 `tool_registry.rs::register_task_graph_tools`(原 10 个:task_link CRUD + 父子树
//! + 跨池移动 + content 更新 + 项目事件流 + 项目基础设施配置 + 工程列表),改用 `declare_tool!` 宏。
//!
//! 迁移策略(handler 逻辑零变更):
//! - handler body 逐字照搬原 `register_task_graph_tools` 内 async move 块(逻辑等价),
//!   仅闭包包装(`{ let db = db.clone(); Box::new(move |args| { let db = db.clone();
//!   Box::pin(async move { ... }) }) }`)改由 `declare_tool!` 宏生成。
//! - name/desc/schema/risk 与原手写定义逐字一致。
//! - 复用类型:new_id/ProjectServiceRecord/now_millis(各 pub(crate) 来源,与原同源)。
//!
//! 等价性验证:基线测试 `test_build_ai_tool_registry_baseline_tool_count` 仍断言 48 总量 +
//! 工具名集合稳定(防 rename / 漏注册)。

use std::sync::Arc;

use df_ai::ai_tools::{object_schema, AiToolRegistry, RiskLevel};
use df_ai::declare_tool;
use df_storage::db::Database;
use df_storage::models::ProjectServiceRecord;
use df_types::types::new_id;

use crate::commands::now_millis;

/// 注册 10 个知识图谱 / 工程系统类工具到 `$registry`。
///
/// 与原手写 register(name, desc, schema, risk, handler) 语义 1:1:
/// - name/desc/schema 字符串与 JSON Schema 逐字照搬原定义
/// - risk 与原一致(link 写/queue 移动/content 更新/add_service=Medium,只读=Low)
/// - handler body 与原 async move 块逐字一致(逻辑零变更)
///
/// 唯一差异:闭包包装改由 `declare_tool!` 宏生成,handler body 直接写业务逻辑。
pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
    // ── create_task_link (Medium,设计 §2.2 + §五) ──
    // 建立任务横向关联。TaskLinkRepo::create_link 内含:
    // link_type 白名单(depends_on/blocks/relates_to)+ 自环拒绝 + depends_on BFS 循环依赖检测。
    // handler 仅转发参数 + new_id,业务校验全部下沉 Repo 层(单一真相源,与 IPC create_task_link 同源)。
    declare_tool!(
        registry,
        db: Arc<Database>,
        "create_task_link",
        "建立任务横向关联(AI 拓扑排序编排调度基础)。参数:task_id(source 任务,关联发起方)、target_id(target 任务,关联指向方)、link_type(depends_on 依赖/blocks 阻塞/relates_to 弱关联)、remark(可选备注)。约束:link_type 白名单校验;source==target 自环拒绝;depends_on 链 BFS 循环依赖检测(A→B→A 拒绝);跨项目允许。返回 link id",
        RiskLevel::Medium,
        schema: object_schema(vec![
            ("task_id", "string", true), ("target_id", "string", true),
            ("link_type", "string", true), ("remark", "string", false),
        ]),
        args => {
            let task_id = args["task_id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 task_id"))?;
            let target_id = args["target_id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 target_id"))?;
            let link_type = args["link_type"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 link_type"))?;
            let remark = args.get("remark").and_then(|v| v.as_str()).filter(|s| !s.is_empty());
            let repo = df_storage::crud::TaskLinkRepo::new(&db);
            let id = new_id();
            let link_id = repo.create_link(&id, task_id, target_id, link_type, remark).await?;
            Ok(serde_json::json!({ "id": link_id, "task_id": task_id, "target_id": target_id, "link_type": link_type }))
        }
    );

    // ── remove_task_link (Medium,设计 §2.2 + §五) ──
    // 解除任务关联(按 link id)。返回是否命中(deleted=false=link 不存在)。
    declare_tool!(
        registry,
        db: Arc<Database>,
        "remove_task_link",
        "解除任务关联(按 link id)。返回 deleted 布尔(是否命中,link 不存在则 false)",
        RiskLevel::Medium,
        schema: object_schema(vec![("id", "string", true)]),
        args => {
            let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?;
            let repo = df_storage::crud::TaskLinkRepo::new(&db);
            let deleted = repo.delete(id).await?;
            Ok(serde_json::json!({ "deleted": deleted, "id": id }))
        }
    );

    // ── list_task_links (Low 只读,设计 §2.2 + §五) ──
    // 查询任务关联(方向:outgoing 作为 source / incoming 作为 target / both 双向合并去重)。
    // AI 编排调度用:取某任务的全部依赖/阻塞/关联,做拓扑排序。
    declare_tool!(
        registry,
        db: Arc<Database>,
        "list_task_links",
        "查询任务关联(AI 编排调度用)。参数:task_id(目标任务)、direction(查询方向 outgoing=作为 source 查我依赖谁 / incoming=作为 target 查谁依赖我 / both=双向合并去重,默认 both)。返回关联记录列表(按 created_at 升序)",
        RiskLevel::Low,
        schema: object_schema(vec![("task_id", "string", true), ("direction", "string", false)]),
        args => {
            let task_id = args["task_id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 task_id"))?;
            // 方向默认 both(对齐 commands::task::LinkDirection::default)。空串/未传走 both。
            let direction = args.get("direction").and_then(|v| v.as_str()).unwrap_or("both").to_lowercase();
            let repo = df_storage::crud::TaskLinkRepo::new(&db);
            let links = match direction.as_str() {
                "outgoing" => repo.get_by_source(task_id).await?,
                "incoming" => repo.get_by_target(task_id).await?,
                // both:Outgoing + Incoming 合并后按 id 去重(同一 link 双向查询可能重复)
                _ => {
                    let mut outgoing = repo.get_by_source(task_id).await?;
                    let incoming = repo.get_by_target(task_id).await?;
                    let mut seen: std::collections::HashSet<String> =
                        outgoing.iter().map(|l| l.id.clone()).collect();
                    for l in incoming {
                        if seen.insert(l.id.clone()) {
                            outgoing.push(l);
                        }
                    }
                    outgoing.sort_by(|a, b| a.created_at.cmp(&b.created_at));
                    outgoing
                }
            };
            let total = links.len();
            Ok(serde_json::json!({ "items": links, "total": total, "task_id": task_id, "direction": direction }))
        }
    );

    // ── get_task_tree (Low 只读,设计 §2.1 + §五) ──
    // 获取父子任务树(限 1 级嵌套,设计 D2:无孙任务)。返回 parent + 直接 children 列表。
    // AI 用途:查看需求分解结构、子任务进度聚合。
    declare_tool!(
        registry,
        db: Arc<Database>,
        "get_task_tree",
        "获取任务父子树(限 1 级嵌套,设计 D2 无孙任务)。参数 task_id(任务 ID,无论叶子还是父任务)。返回 { parent: 父任务记录, children: 直接子任务列表(parent_id 指向 task_id,按 created_at 升序) }。叶子任务 children 为空。AI 用途:查看需求分解结构、子任务进度聚合",
        RiskLevel::Low,
        schema: object_schema(vec![("task_id", "string", true)]),
        args => {
            let task_id = args["task_id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 task_id"))?;
            let repo = df_storage::crud::TaskRepo::new(&db);
            let parent = repo
                .get_by_id(task_id)
                .await?
                .ok_or_else(|| anyhow::anyhow!("任务 {task_id} 不存在"))?;
            let children = repo.get_children(task_id).await?;
            let child_count = children.len();
            Ok(serde_json::json!({ "parent": parent, "children": children, "child_count": child_count }))
        }
    );

    // ── move_task_queue (Medium,设计 §2.1 + §五) ──
    // 跨池移动任务(backlog/todo/decision/active/done),按一致性约束联动 status。
    // 对齐 commands::task::move_task_queue 语义:
    // - done → status 强制 done / backlog → status 强制 todo
    // - active → status 若不在执行中三态则强制 in_progress / todo → status 强制 todo
    // - decision → status 不变(待决策池保留执行态)
    // status 写入走专用 set_status_for_aggregation(绕过 status 收口,move_task_queue 是合法非状态机路径)。
    declare_tool!(
        registry,
        db: Arc<Database>,
        "move_task_queue",
        "跨池移动任务(管理维度池 backlog/todo/decision/active/done,与 status 执行维度正交)。参数 id(任务 ID)、new_queue(目标池)。一致性约束联动 status:done→status=done / backlog→status=todo / active→status 若非执行中三态强制 in_progress / todo→status=todo / decision→status 不变。返回移动后的最新 TaskRecord",
        RiskLevel::Medium,
        schema: object_schema(vec![("id", "string", true), ("new_queue", "string", true)]),
        args => {
            let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?;
            let new_queue = args["new_queue"].as_str()
                .ok_or_else(|| anyhow::anyhow!("缺少 new_queue"))?
                .trim()
                .to_string();
            // queue 白名单校验(对标 commands::task::validate_queue)
            const TASK_QUEUE_VALUES: &[&str] = &["backlog", "todo", "decision", "active", "done"];
            if !TASK_QUEUE_VALUES.contains(&new_queue.as_str()) {
                anyhow::bail!("非法 new_queue 值 {:?},合法值: {:?}", new_queue, TASK_QUEUE_VALUES);
            }

            let repo = df_storage::crud::TaskRepo::new(&db);
            let current = repo
                .get_by_id(id)
                .await?
                .ok_or_else(|| anyhow::anyhow!("任务 {id} 不存在"))?;

            // 一致性约束联动:根据 new_queue 决定 status 是否需调整
            // (对标 commands::task::move_task_queue 同源逻辑,单一真相源)
            const ACTIVE_OK_STATUSES: &[&str] = &["in_progress", "in_review", "testing"];
            let new_status = match new_queue.as_str() {
                "done" => "done".to_string(),
                "backlog" => "todo".to_string(),
                "active" => {
                    if ACTIVE_OK_STATUSES.contains(&current.status.as_str()) {
                        current.status.as_str().to_string()
                    } else {
                        "in_progress".to_string()
                    }
                }
                "todo" => "todo".to_string(),
                "decision" => current.status.as_str().to_string(),
                _ => unreachable!("queue 白名单已收口"),
            };

            // 写 queue(queue 已在 tasks 白名单);与 current 不同才写(避免无谓 updated_at 抖动)
            if current.queue != new_queue {
                repo.update_field(id, "queue", &new_queue).await?;
            }
            // 写 status(专用 set_status_for_aggregation 绕过 status 收口,合法非状态机路径)
            if current.status.as_str() != new_status {
                repo.set_status_for_aggregation(id, &new_status).await?;
            }

            // 回读最新记录返回
            let updated = repo
                .get_by_id(id)
                .await?
                .ok_or_else(|| anyhow::anyhow!("任务 {id} 不存在(移动后回读失败)"))?;
            Ok(serde_json::to_value(&updated)?)
        }
    );

    // ── update_content (Medium,设计 §五) ──
    // 更新 content_json 结构化需求规格({background,acceptance_criteria[],scope[],technical_design})。
    // 走通用 update_field(content_json 已在 tasks 白名单) + JSON 合法性校验(对齐 create_task 校验)。
    // AI 从对话中提取信息填充 content_json,人确认后 AI 执行中用 acceptance_criteria 自检。
    declare_tool!(
        registry,
        db: Arc<Database>,
        "update_content",
        "更新任务的结构化需求规格 content_json(JSON 字符串 {background,acceptance_criteria[],scope[],technical_design,custom_fields})。参数 id(任务 ID)、content_json(合法 JSON 字符串,空串清空 content_json)。校验:须是合法 JSON,否则拒。AI 用途:从对话提取信息填充需求规格,供后续 acceptance_criteria 自检",
        RiskLevel::Medium,
        schema: object_schema(vec![("id", "string", true), ("content_json", "string", true)]),
        args => {
            let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?;
            let content_json = args["content_json"].as_str()
                .ok_or_else(|| anyhow::anyhow!("缺少 content_json"))?;
            // JSON 合法性校验(对齐 commands::task::create_task 的 content_json 校验)
            // 非空串须是合法 JSON;空串视为清空(写 NULL)——但 update_field 收 string,
            // 用空串写库后会留空字符串而非 NULL(可接受,content_json 为 Option<String> 但
            // 通用 update_field 走 TEXT 列接受空串)。这里对非空串做校验,空串直接放行清空。
            if !content_json.trim().is_empty() {
                serde_json::from_str::<serde_json::Value>(content_json)
                    .map_err(|e| anyhow::anyhow!("content_json 不是合法 JSON: {e}"))?;
            }
            let repo = df_storage::crud::TaskRepo::new(&db);
            // 透传 affected:0 行(id 不存在 / 已软删)→ updated=false + title 空,
            // 让 AI 知道 id 不对而非假成功(对齐 update_idea / update_task)。
            let updated = repo.update_field(id, "content_json", content_json).await?;
            let title = repo.get_by_id(id).await?.map(|t| t.title).unwrap_or_default();
            Ok(serde_json::json!({ "id": id, "title": title, "field": "content_json", "updated": updated }))
        }
    );

    // ── get_project_timeline (Low 只读,设计 §五 + §2.4) ──
    // 查询项目事件流(project_events 统一事件流)。AI 精准检索基础:
    // 回答"上周做了什么 / 这个任务为何 blocked / 决策何时做出"。
    // 参数 project_id(必填) + event_type(可选过滤) + limit(默认 50,上限 200)。
    // source 标 "ai"(AI 工具路径,区别于 IPC 层的 "human")。
    declare_tool!(
        registry,
        db: Arc<Database>,
        "get_project_timeline",
        "查询项目事件流(AI 精准检索基础,回答\"上周做了什么/这个任务为何 blocked/决策何时做出\")。参数 project_id(必填,项目 ID)、event_type(可选,按事件类型精确过滤,如 task_created/task_advanced/idea_promoted/idea_evaluated/decision_made 等,空=不过滤)、limit(可选,返回条数上限,默认 50,内部钳制 ≤ 200)。返回 { items: 事件列表(时间倒序), total, project_id }。事件含 event_type/entity_type/entity_id/from_state/to_state/source/created_at",
        RiskLevel::Low,
        schema: object_schema(vec![
            ("project_id", "string", true),
            ("event_type", "string", false),
            ("limit", "integer", false),
        ]),
        args => {
            let project_id = args["project_id"].as_str()
                .ok_or_else(|| anyhow::anyhow!("缺少 project_id"))?
                .trim()
                .to_string();
            if project_id.is_empty() {
                anyhow::bail!("project_id 不能为空");
            }
            let event_filter = args.get("event_type")
                .and_then(|v| v.as_str())
                .map(str::trim)
                .filter(|s| !s.is_empty())
                .map(|s| s.to_string());
            // limit 默认 50(对齐 commands::events::default_limit);负数/异常值兜底 50。
            let limit = args.get("limit")
                .and_then(|v| v.as_u64())
                .map(|n| n as u32)
                .unwrap_or(50);

            let repo = df_storage::crud::ProjectEventRepo::new(&db);
            let mut events = repo.get_by_project(&project_id, limit).await?;
            // 应用层按 event_type 过滤(单用户事件量小,保持 Repo 方法精简)。
            if let Some(et) = &event_filter {
                events.retain(|e| e.event_type == *et);
            }
            let total = events.len();
            Ok(serde_json::json!({
                "items": events,
                "total": total,
                "project_id": project_id,
            }))
        }
    );

    // ── add_project_service (Medium,设计 §2.3 + §五 + D10) ──
    // 添加项目依赖的基础设施配置(数据库/缓存/MQ/API 等)。为 AI 执行任务时提供基础设施上下文:
    // "这项目用了什么数据库、Redis 在哪、有没有 MQ"。
    // service_type 白名单 + D10 凭证审查下沉 Repo::insert_validated(单一真相源,与 IPC
    // add_project_service 同源)。
    //
    // ⚠️ D10 安全边界:config_json / endpoint / remark 不含敏感凭证(密码/密钥/Token)。
    // Repo 层审查 password/secret/token/api_key 子串,命中即拒。凭证走环境变量。
    declare_tool!(
        registry,
        db: Arc<Database>,
        "add_project_service",
        "为项目添加基础设施配置(为 AI 执行任务时提供\"用了什么数据库/缓存/MQ/API\"上下文)。参数:project_id(项目 ID)、name(服务名,如 主库/Redis 缓存)、service_type(类型,白名单 mysql/postgresql/sqlite/redis/mongodb/mq/api/other)、environment(环境 development/staging/production,默认 development)、endpoint(可选,连接地址 localhost:3306 或 URL,不含凭证)、config_json(可选,类型相关配置 JSON 字符串,如 {\"pool_size\":10},⚠️不含密码字段,凭证走环境变量)、remark(可选,备注)。返回新增完整记录。约束:service_type 白名单;D10 安全边界 config_json/endpoint/remark 含 password/secret/token/api_key 子串拒绝",
        RiskLevel::Medium,
        schema: object_schema(vec![
            ("project_id", "string", true),
            ("name", "string", true),
            ("service_type", "string", true),
            ("environment", "string", false),
            ("endpoint", "string", false),
            ("config_json", "string", false),
            ("remark", "string", false),
        ]),
        args => {
            let project_id = args["project_id"].as_str()
                .ok_or_else(|| anyhow::anyhow!("缺少 project_id"))?
                .trim()
                .to_string();
            let name = args["name"].as_str()
                .ok_or_else(|| anyhow::anyhow!("缺少 name"))?
                .trim()
                .to_string();
            let service_type = args["service_type"].as_str()
                .ok_or_else(|| anyhow::anyhow!("缺少 service_type"))?
                .trim()
                .to_string();
            if project_id.is_empty() {
                anyhow::bail!("project_id 不能为空");
            }
            if name.is_empty() {
                anyhow::bail!("name 不能为空");
            }
            if service_type.is_empty() {
                anyhow::bail!("service_type 不能为空");
            }
            let endpoint = args.get("endpoint")
                .and_then(|v| v.as_str())
                .map(str::trim)
                .filter(|s| !s.is_empty())
                .map(|s| s.to_string());
            let config_json = args.get("config_json")
                .and_then(|v| v.as_str())
                .map(str::trim)
                .filter(|s| !s.is_empty())
                .map(|s| s.to_string());
            let environment = args.get("environment")
                .and_then(|v| v.as_str())
                .map(str::trim)
                .filter(|s| !s.is_empty())
                .unwrap_or("development")
                .to_string();
            let remark = args.get("remark")
                .and_then(|v| v.as_str())
                .map(str::trim)
                .filter(|s| !s.is_empty())
                .map(|s| s.to_string());

            let record = ProjectServiceRecord {
                id: new_id(),
                project_id: project_id.clone(),
                name,
                service_type,
                endpoint,
                config_json,
                environment,
                remark,
                created_at: now_millis(),
                updated_at: now_millis(),
            };
            let repo = df_storage::crud::ProjectServiceRepo::new(&db);
            // insert_validated 内含 service_type 白名单 + D10 凭证审查(密码/token/api_key 子串拒绝)。
            let id = repo.insert_validated(record).await?;
            let inserted = repo.get_by_id(&id).await?
                .ok_or_else(|| anyhow::anyhow!("插入后回读失败"))?;
            Ok(serde_json::to_value(&inserted)?)
        }
    );

    // ── list_project_services (Low 只读,设计 §2.3 + §五) ──
    // 查询项目基础设施配置。AI 执行任务时按需取基础设施上下文:
    // 部署 production 任务只取 production 服务,开发调试取 development(环境隔离避免误连生产库)。
    declare_tool!(
        registry,
        db: Arc<Database>,
        "list_project_services",
        "查询项目基础设施配置(AI 执行任务时取\"用了什么数据库/缓存/MQ/API\"上下文)。参数:project_id(项目 ID)、environment(可选,按环境过滤 development/staging/production,空=返回所有环境)。返回 { items: 配置列表(时间倒序), total, project_id }。环境隔离:production 部署任务取 production 服务,开发调试取 development,避免误连生产库",
        RiskLevel::Low,
        schema: object_schema(vec![
            ("project_id", "string", true),
            ("environment", "string", false),
        ]),
        args => {
            let project_id = args["project_id"].as_str()
                .ok_or_else(|| anyhow::anyhow!("缺少 project_id"))?
                .trim()
                .to_string();
            if project_id.is_empty() {
                anyhow::bail!("project_id 不能为空");
            }
            let env_filter = args.get("environment")
                .and_then(|v| v.as_str())
                .map(str::trim)
                .filter(|s| !s.is_empty())
                .map(|s| s.to_string());

            let repo = df_storage::crud::ProjectServiceRepo::new(&db);
            let items = if let Some(env) = &env_filter {
                repo.list_by_project_env(&project_id, env).await?
            } else {
                repo.list_by_project(&project_id).await?
            };
            let total = items.len();
            Ok(serde_json::json!({
                "items": items,
                "total": total,
                "project_id": project_id,
            }))
        }
    );

    // ── list_project_modules (Low 只读,工程系统) ──
    // 查询项目工程列表(AI 取项目结构上下文:有哪些代码仓库、各自目录/技术栈/Git 地址)。
    // 多工程场景(Monorepo/微服务/前后端分离)AI 据此决定在哪个工程执行 git/构建操作。
    declare_tool!(
        registry,
        db: Arc<Database>,
        "list_project_modules",
        "查询项目工程列表(每个工程是独立代码仓库)。参数:project_id(项目 ID)。返回 { modules: 工程列表(含 name/path/git_url/stack), total }。AI 据此了解项目有哪些代码仓库及各自技术栈",
        RiskLevel::Low,
        schema: object_schema(vec![
            ("project_id", "string", true),
        ]),
        args => {
            let project_id = args["project_id"].as_str()
                .ok_or_else(|| anyhow::anyhow!("缺少 project_id"))?
                .trim()
                .to_string();
            if project_id.is_empty() {
                anyhow::bail!("project_id 不能为空");
            }
            let repo = df_storage::crud::ProjectModuleRepo::new(&db);
            let modules = repo.list_by_project(&project_id).await?;
            let total = modules.len();
            Ok(serde_json::json!({
                "modules": modules,
                "total": total,
                "project_id": project_id,
            }))
        }
    );
}
