//! 任务横向关联 Repo:TaskLinkRepo(task_links 表,知识图谱 Phase 1 V29)
//!
//! 表达任务间 depends_on / blocks / relates_to 关系,AI 拓扑排序编排调度的基础。
//! 对标设计 docs/02-架构设计/专项设计/项目知识图谱与任务队列系统-2026-06-26.md §2.2。
//!
//! 关键设计:
//! - `link_type` 白名单(depends_on/blocks/relates_to)应用层校验,非 DB 约束。
//! - 循环依赖(`depends_on` 链 A→B→A)在 `create_link` BFS 检测拒绝(非 DB 约束,对标 D8)。
//! - 跨项目依赖允许(现实中有跨项目依赖)。
//! - 软删除语义:Task 软删不级联删 link(恢复后关系还在)。

use std::collections::HashSet;

use rusqlite::{params, OptionalExtension, Row};

use df_types::error::Result;

use crate::db::Database;
use crate::models::TaskLinkRecord;

use super::{now_millis_str, storage_err};

// ============================================================
// link_type 白名单 + 校验
// ============================================================

/// `link_type` 白名单:只允许这三种关联类型(防拼写漂移 / 非法值进库)。
///
/// 对标设计 §2.2 link_type 语义:
/// - `depends_on`:source 依赖 target(target 完成后 source 才能开始)→ 拓扑排序调度
/// - `blocks`:source 阻塞 target(source 不完成则 target 无法推进)→ 依赖的反向声明
/// - `relates_to`:弱关联,无执行约束 → 上下文提示
pub const TASK_LINK_TYPES: &[&str] = &["depends_on", "blocks", "relates_to"];

/// 校验 `link_type` 在白名单内,否则返回 Err。
fn validate_link_type(link_type: &str) -> Result<()> {
    if TASK_LINK_TYPES.contains(&link_type) {
        Ok(())
    } else {
        Err(df_types::error::Error::Storage(format!(
            "非法 link_type: {link_type}，合法值: {:?}",
            TASK_LINK_TYPES
        )))
    }
}

// ============================================================
// from_row 辅助函数
// ============================================================

fn task_link_from_row(row: &Row<'_>) -> std::result::Result<TaskLinkRecord, rusqlite::Error> {
    Ok(TaskLinkRecord {
        id: row.get("id")?,
        source_id: row.get("source_id")?,
        target_id: row.get("target_id")?,
        link_type: row.get("link_type")?,
        remark: row.get("remark")?,
        created_at: row.get("created_at")?,
    })
}

// ============================================================
// TaskLinkRepo
// ============================================================

/// 任务横向关联表 Repo(task_links,V29)。
///
/// 不走 `impl_repo!` 宏:① 表无 `updated_at`/`deleted_at`(link 不可改只增删,审计简单);
/// ② `create_link` 需在 INSERT 前做 BFS 循环依赖检测(宏生成的 insert 无业务前置逻辑)。
/// 故手写专用方法,对标 SettingsRepo 全专用路径。
pub struct TaskLinkRepo {
    conn: std::sync::Arc<tokio::sync::Mutex<rusqlite::Connection>>,
}

impl TaskLinkRepo {
    pub fn new(db: &Database) -> Self {
        Self { conn: db.conn() }
    }

    /// 创建任务关联(应用层校验 link_type 白名单 + depends_on 链 BFS 循环依赖检测)。
    ///
    /// 对标设计 §2.2 边界约束:
    /// - `link_type` 必须在白名单(depends_on/blocks/relates_to),否则 Err。
    /// - 自环(source_id == target_id)直接拒绝(无意义的自依赖)。
    /// - 循环依赖检测:**仅对 depends_on 链**做 BFS。blocks 是 depends_on 的反向声明,
    ///   relates_to 无执行约束,二者不参与环检测(否则弱关联也会触发拒绝,过度约束)。
    ///   BFS 思路:新增 source→target 后,从 target 出发沿 depends_on 链向下遍历,
    ///   若能回到 source 则成环,拒绝。数据量小(单项目 ~50 任务),全表遍历无压力(对标 D8)。
    /// - 跨项目允许(不做 project_id 一致性校验,现实有跨项目依赖)。
    ///
    /// 返回插入的 link id。
    pub async fn create_link(
        &self,
        id: &str,
        source_id: &str,
        target_id: &str,
        link_type: &str,
        remark: Option<&str>,
    ) -> Result<String> {
        // 应用层校验先行(fail-fast,非法值不进 DB 层)
        validate_link_type(link_type)?;
        if source_id == target_id {
            return Err(df_types::error::Error::Storage(format!(
                "非法 task_link: source_id 与 target_id 相同({source_id}),自环无意义"
            )));
        }

        let conn = self.conn.clone();
        let id = id.to_owned();
        let source_id_o = source_id.to_owned();
        let target_id_o = target_id.to_owned();
        let link_type_o = link_type.to_owned();
        let remark_o = remark.map(|s| s.to_owned());
        let now = now_millis_str();

        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();

            // depends_on 链 BFS 环检测:新增 source→target 边后,从 target 沿 depends_on
            // 向下走,若能回到 source 即成环。在 INSERT 前检测,避免脏数据(检测与插入非原子,
            // 但单用户桌面应用无并发,够用)。
            if link_type_o == "depends_on" {
                if bfs_reaches(&guard, &target_id_o, &source_id_o)? {
                    return Err(df_types::error::Error::Storage(format!(
                        "非法 task_link: {} depends_on {} 会形成循环依赖",
                        source_id_o, target_id_o
                    )));
                }
            }

            guard
                .execute(
                    "INSERT INTO task_links (id, source_id, target_id, link_type, remark, created_at)
                     VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
                    params![id, source_id_o, target_id_o, link_type_o, remark_o, now],
                )
                .map_err(storage_err)?;
            Ok(id)
        })
        .await
        .map_err(storage_err)?
    }

    /// 删除关联(按 id),返回是否命中。
    pub async fn delete(&self, id: &str) -> Result<bool> {
        let conn = self.conn.clone();
        let id = id.to_owned();
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let affected = guard
                .execute("DELETE FROM task_links WHERE id = ?1", params![id])
                .map_err(storage_err)?;
            Ok(affected > 0)
        })
        .await
        .map_err(storage_err)?
    }

    /// 按 id 取单条(对标 IdeaRepo::get_by_id 模式)。
    pub async fn get_by_id(&self, id: &str) -> Result<Option<TaskLinkRecord>> {
        let conn = self.conn.clone();
        let id = id.to_owned();
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let row = guard
                .query_row(
                    "SELECT id, source_id, target_id, link_type, remark, created_at \
                     FROM task_links WHERE id = ?1",
                    params![id],
                    task_link_from_row,
                )
                .optional()
                .map_err(storage_err)?;
            Ok(row)
        })
        .await
        .map_err(storage_err)?
    }

    /// 按 source_id 查询(source 主动声明的全部关联),命中 idx_task_links_source。
    /// AI 编排调度用:取某任务的全部依赖 / 阻塞 / 关联。
    pub async fn get_by_source(&self, source_id: &str) -> Result<Vec<TaskLinkRecord>> {
        let conn = self.conn.clone();
        let source_id = source_id.to_owned();
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let mut stmt = guard
                .prepare(
                    "SELECT id, source_id, target_id, link_type, remark, created_at \
                     FROM task_links WHERE source_id = ?1 ORDER BY created_at ASC",
                )
                .map_err(storage_err)?;
            let rows = stmt
                .query_map(params![source_id], task_link_from_row)
                .map_err(storage_err)?;
            let mut results = Vec::new();
            for r in rows {
                results.push(r.map_err(storage_err)?);
            }
            Ok(results)
        })
        .await
        .map_err(storage_err)?
    }

    /// 按 target_id 查询(谁指向了 target),命中 idx_task_links_target。
    /// AI 编排调度用:「谁依赖了我」「谁被我阻塞」反向查询(JSON 列方案无法高效反查的痛点)。
    pub async fn get_by_target(&self, target_id: &str) -> Result<Vec<TaskLinkRecord>> {
        let conn = self.conn.clone();
        let target_id = target_id.to_owned();
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let mut stmt = guard
                .prepare(
                    "SELECT id, source_id, target_id, link_type, remark, created_at \
                     FROM task_links WHERE target_id = ?1 ORDER BY created_at ASC",
                )
                .map_err(storage_err)?;
            let rows = stmt
                .query_map(params![target_id], task_link_from_row)
                .map_err(storage_err)?;
            let mut results = Vec::new();
            for r in rows {
                results.push(r.map_err(storage_err)?);
            }
            Ok(results)
        })
        .await
        .map_err(storage_err)?
    }

    /// 列出全部关联(调试/管理用,按创建时间升序)。
    pub async fn list_all(&self) -> Result<Vec<TaskLinkRecord>> {
        let conn = self.conn.clone();
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let mut stmt = guard
                .prepare(
                    "SELECT id, source_id, target_id, link_type, remark, created_at \
                     FROM task_links ORDER BY created_at ASC",
                )
                .map_err(storage_err)?;
            let rows = stmt.query_map([], task_link_from_row).map_err(storage_err)?;
            let mut results = Vec::new();
            for r in rows {
                results.push(r.map_err(storage_err)?);
            }
            Ok(results)
        })
        .await
        .map_err(storage_err)?
    }
}

// ============================================================
// BFS 循环依赖检测(仅 depends_on 链)
// ============================================================

/// BFS:从 `start` 出发沿 depends_on 链(source→target 方向)向下遍历,判断能否到达 `target_node`。
///
/// 用于 `create_link(source, depends_on, target)` 前的环检测:新增 source→target 边后,
/// 若 target 能经 depends_on 链回到 source,则形成 source→target→...→source 闭环。
/// 故调用 `bfs_reaches(conn, start=target, target_node=source)`:从 target 出发看能否到 source。
///
/// 实现细节:
/// - 遍历 `WHERE source_id = ? AND link_type = 'depends_on'` 取下一跳(对标设计 D8:数据量小,
///   全表遍历无压力,不走 SQL 递归 CTE,纯 Rust BFS)。
/// - `visited` HashSet 防重复访问(菱形依赖 A→B,A→C,B→D,C→D 不应死循环)。
/// - 防御性深度上限(10000 跳):万一数据异常成环未被拦截,BFS 不致无限循环。
fn bfs_reaches(
    conn: &rusqlite::Connection,
    start: &str,
    target_node: &str,
) -> Result<bool> {
    use std::collections::VecDeque;

    let mut visited: HashSet<String> = HashSet::new();
    let mut queue: VecDeque<String> = VecDeque::new();
    queue.push_back(start.to_owned());
    visited.insert(start.to_owned());

    let mut depth = 0u32;
    const MAX_DEPTH: u32 = 10_000;

    while let Some(node) = queue.pop_front() {
        depth += 1;
        if depth > MAX_DEPTH {
            // 防御性兜底:正常依赖链不可能这么深,到这要么数据异常要么已有环未被拦截。
            // 保守视为成环(拒绝创建),避免无限循环 + 暴露异常数据。
            tracing::warn!(
                "task_links BFS 超过 {MAX_DEPTH} 跳,疑似数据异常,保守拒绝创建"
            );
            return Ok(true);
        }

        if node == target_node {
            return Ok(true);
        }

        // 取该节点作为 source 的全部 depends_on 边的 target(下一跳)
        let mut stmt = conn
            .prepare(
                "SELECT target_id FROM task_links \
                 WHERE source_id = ?1 AND link_type = 'depends_on'",
            )
            .map_err(storage_err)?;
        let nexts: Vec<String> = stmt
            .query_map(params![node], |row| row.get::<_, String>(0))
            .map_err(storage_err)?
            .filter_map(|r| r.ok())
            .collect();

        for next in nexts {
            if visited.insert(next.clone()) {
                queue.push_back(next);
            }
        }
    }
    Ok(false)
}

// ============================================================
// 单元测试 — TaskLinkRepo CRUD + 循环依赖拒绝(内存 DB,对标 idea_repo 测试)
// ============================================================

#[cfg(test)]
mod tests {
    use super::*;
    use crate::crud::TaskRepo;
    use crate::models::TaskRecord;
    use df_types::types::{ProjectStatus, TaskStatus};

    /// 构造一条 TaskRecord fixture(18 字段全填,queue 默认 todo)。
    fn trec(id: &str, project_id: &str) -> TaskRecord {
        TaskRecord {
            id: id.to_string(),
            project_id: project_id.to_string(),
            title: format!("task-{id}"),
            description: String::new(),
            status: TaskStatus::Todo,
            priority: 1,
            branch_name: None,
            assignee: None,
            workflow_def_id: None,
            base_branch: None,
            review_rounds: 0,
            output_json: None,
            idea_id: None,
            queue: "todo".to_string(),
            parent_id: None,
            content_json: None,
            created_at: "1700000000000".to_string(),
            updated_at: "1700000000000".to_string(),
        }
    }

    /// 构造内存 DB + 注入若干任务(task_links FK 要求 tasks 存在,
    /// tasks.project_id FK 要求 projects 存在 → 先建占位 project 满足 FK 约束,
    /// 对标 tests/project_soft_delete.rs 集成测试的 setup 模式)。
    async fn setup_with_tasks(ids: &[&str]) -> (crate::db::Database, TaskLinkRepo, TaskRepo) {
        let db = crate::db::Database::open_in_memory()
            .await
            .expect("open_in_memory");
        let link_repo = TaskLinkRepo::new(&db);
        let task_repo = TaskRepo::new(&db);
        // 先建占位 project 满足 tasks.project_id FK(PRAGMA foreign_keys=ON,db.rs:37/37)
        let project_repo = crate::crud::ProjectRepo::new(&db);
        project_repo
            .insert(crate::models::ProjectRecord {
                id: "proj-1".to_string(),
                name: "proj-1".to_string(),
                description: String::new(),
                status: ProjectStatus::Planning,
                idea_id: None,
                path: None,
                stack: None,
                created_at: "1700000000000".to_string(),
                updated_at: "1700000000000".to_string(),
            })
            .await
            .unwrap();
        for id in ids {
            task_repo.insert(trec(id, "proj-1")).await.unwrap();
        }
        (db, link_repo, task_repo)
    }

    #[tokio::test]
    async fn validate_link_type_rejects_unknown() {
        assert!(validate_link_type("depends_on").is_ok());
        assert!(validate_link_type("blocks").is_ok());
        assert!(validate_link_type("relates_to").is_ok());
        assert!(validate_link_type("unknown").is_err());
        assert!(validate_link_type("").is_err());
    }

    #[tokio::test]
    async fn create_link_basic_depends_on() {
        let (_db, repo, _task_repo) = setup_with_tasks(&["a", "b"]).await;
        let id = repo
            .create_link("l1", "a", "b", "depends_on", None)
            .await
            .unwrap();
        assert_eq!(id, "l1");

        let got = repo.get_by_id("l1").await.unwrap().expect("link 存在");
        assert_eq!(got.source_id, "a");
        assert_eq!(got.target_id, "b");
        assert_eq!(got.link_type, "depends_on");
        assert!(got.remark.is_none());
    }

    #[tokio::test]
    async fn create_link_with_remark() {
        let (_db, repo, _) = setup_with_tasks(&["a", "b"]).await;
        repo.create_link("l1", "a", "b", "blocks", Some("阻塞说明"))
            .await
            .unwrap();
        let got = repo.get_by_id("l1").await.unwrap().unwrap();
        assert_eq!(got.link_type, "blocks");
        assert_eq!(got.remark.as_deref(), Some("阻塞说明"));
    }

    #[tokio::test]
    async fn create_link_rejects_invalid_type() {
        let (_db, repo, _) = setup_with_tasks(&["a", "b"]).await;
        let err = repo
            .create_link("l1", "a", "b", "invalid_type", None)
            .await;
        assert!(err.is_err(), "非法 link_type 应被拒绝");
        // 未入库
        assert!(repo.get_by_id("l1").await.unwrap().is_none());
    }

    #[tokio::test]
    async fn create_link_rejects_self_loop() {
        let (_db, repo, _) = setup_with_tasks(&["a"]).await;
        let err = repo.create_link("l1", "a", "a", "depends_on", None).await;
        assert!(err.is_err(), "自环 source==target 应被拒绝");
    }

    #[tokio::test]
    async fn get_by_source_and_target() {
        let (_db, repo, _) = setup_with_tasks(&["a", "b", "c"]).await;
        repo.create_link("l1", "a", "b", "depends_on", None)
            .await
            .unwrap();
        repo.create_link("l2", "a", "c", "relates_to", None)
            .await
            .unwrap();
        repo.create_link("l3", "c", "b", "blocks", None)
            .await
            .unwrap();

        // a 作为 source 声明了 2 条(l1/l2)
        let from_a = repo.get_by_source("a").await.unwrap();
        let ids: Vec<_> = from_a.iter().map(|l| l.id.as_str()).collect();
        assert_eq!(ids, vec!["l1", "l2"]);

        // b 作为 target 被 2 条指向(l1/l3)
        let to_b = repo.get_by_target("b").await.unwrap();
        let ids: Vec<_> = to_b.iter().map(|l| l.id.as_str()).collect();
        assert_eq!(ids, vec!["l1", "l3"]);
    }

    #[tokio::test]
    async fn delete_link() {
        let (_db, repo, _) = setup_with_tasks(&["a", "b"]).await;
        repo.create_link("l1", "a", "b", "depends_on", None)
            .await
            .unwrap();
        assert!(repo.delete("l1").await.unwrap());
        assert!(repo.get_by_id("l1").await.unwrap().is_none());
        // 再删返回 false
        assert!(!repo.delete("l1").await.unwrap());
    }

    // ---------- 循环依赖 BFS 检测(仅 depends_on 链)----------

    #[tokio::test]
    async fn cycle_direct_a_depends_b_then_b_depends_a_rejected() {
        // A→B 合法;再 B→A 应形成 A→B→A 闭环,拒绝
        let (_db, repo, _) = setup_with_tasks(&["a", "b"]).await;
        repo.create_link("l1", "a", "b", "depends_on", None)
            .await
            .unwrap();
        let err = repo
            .create_link("l2", "b", "a", "depends_on", None)
            .await;
        assert!(err.is_err(), "A→B→A 循环依赖应被拒绝");
        // l2 未入库
        assert!(repo.get_by_id("l2").await.unwrap().is_none());
    }

    #[tokio::test]
    async fn cycle_three_nodes_rejected() {
        // A→B→C 合法;再 C→A 形成 A→B→C→A 闭环,拒绝
        let (_db, repo, _) = setup_with_tasks(&["a", "b", "c"]).await;
        repo.create_link("l1", "a", "b", "depends_on", None)
            .await
            .unwrap();
        repo.create_link("l2", "b", "c", "depends_on", None)
            .await
            .unwrap();
        let err = repo
            .create_link("l3", "c", "a", "depends_on", None)
            .await;
        assert!(err.is_err(), "A→B→C→A 三节点循环依赖应被拒绝");
    }

    #[tokio::test]
    async fn diamond_dependency_not_cycle() {
        // 菱形依赖 A→B, A→C, B→D, C→D 是 DAG 非环,D 不应再指向 A/B/C
        // 关键:BFS 遇菱形不误判(D 被两条路径到达,visited 去重不死循环)
        let (_db, repo, _) = setup_with_tasks(&["a", "b", "c", "d"]).await;
        repo.create_link("l1", "a", "b", "depends_on", None)
            .await
            .unwrap();
        repo.create_link("l2", "a", "c", "depends_on", None)
            .await
            .unwrap();
        repo.create_link("l3", "b", "d", "depends_on", None)
            .await
            .unwrap();
        repo.create_link("l4", "c", "d", "depends_on", None)
            .await
            .unwrap();
        // 全部成功(菱形合法)
        assert_eq!(repo.list_all().await.unwrap().len(), 4);
    }

    #[tokio::test]
    async fn blocks_link_does_not_trigger_cycle_check() {
        // blocks 是 depends_on 的反向声明,不参与环检测:A blocks B + B blocks A 应都合法
        // (虽然语义重复,但环检测只管 depends_on 链,blocks/relates_to 弱约束不强拦)
        let (_db, repo, _) = setup_with_tasks(&["a", "b"]).await;
        repo.create_link("l1", "a", "b", "blocks", None)
            .await
            .unwrap();
        // B blocks A 不触发 depends_on 环检测,合法
        repo.create_link("l2", "b", "a", "blocks", None)
            .await
            .unwrap();
        assert_eq!(repo.list_all().await.unwrap().len(), 2);
    }

    #[tokio::test]
    async fn relates_to_link_does_not_trigger_cycle_check() {
        // relates_to 弱关联无执行约束,不参与环检测
        let (_db, repo, _) = setup_with_tasks(&["a", "b"]).await;
        repo.create_link("l1", "a", "b", "relates_to", None)
            .await
            .unwrap();
        repo.create_link("l2", "b", "a", "relates_to", None)
            .await
            .unwrap();
        assert_eq!(repo.list_all().await.unwrap().len(), 2);
    }

    #[tokio::test]
    async fn cross_project_link_allowed() {
        // 跨项目依赖允许(设计 §2.2 边界):不做 project_id 一致性校验
        let (_db, repo, _) = setup_with_tasks(&["a", "b"]).await; // fixture 同 proj-1,但 repo 不校验
        let res = repo.create_link("l1", "a", "b", "depends_on", None).await;
        assert!(res.is_ok(), "跨项目依赖应允许(repo 层不校验 project 一致性)");
    }

    #[tokio::test]
    async fn cycle_check_mixed_chain_only_depends_on_matters() {
        // 混合链:A relates_to B(弱关联),B depends_on A 应合法(relates_to 不构成环路径)
        let (_db, repo, _) = setup_with_tasks(&["a", "b"]).await;
        repo.create_link("l1", "a", "b", "relates_to", None)
            .await
            .unwrap();
        // B depends_on A:BFS 从 A 出发沿 depends_on 找 B,A 无 depends_on 出边 → 不到 B → 合法
        repo.create_link("l2", "b", "a", "depends_on", None)
            .await
            .unwrap();
        assert_eq!(repo.list_all().await.unwrap().len(), 2);
    }
}
