//! 任务相关命令

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

use df_types::types::{new_id, TaskStatus};
use df_storage::crud::TaskQuery;
use df_storage::models::{ProjectEventRecord, TaskLinkRecord, TaskRecord};

use crate::state::AppState;

use super::{err_str, now_millis};

// ============================================================
// 知识图谱 Phase 2:事件流埋点辅助(best-effort,对标设计 §2.4 hook/after + §10.1)
// ============================================================

/// 追加一条项目事件到 project_events(best-effort)。
///
/// 埋点策略(设计 §2.4):事件写入失败**不阻断主操作**,仅 `tracing::warn` 记录。
/// 设计 §10.1「事件流写入失败 — 影响事件完整性 — 事件写入失败不阻断主操作(best-effort)」。
///
/// `source` 语义:本辅助仅由 IPC 层调用,标 `"human"`(AI 工具路径在 tool_registry 内自行标
/// `"ai"`,不经本 IPC)。`project_id` / `entity_type` / `entity_id` / `event_type` 由调用方
/// 提供。`from_state` / `to_state` / `context_json` 可选。
async fn emit_event(
    state: &State<'_, AppState>,
    project_id: &str,
    event_type: &str,
    entity_type: Option<&str>,
    entity_id: Option<&str>,
    from_state: Option<&str>,
    to_state: Option<&str>,
) {
    let record = ProjectEventRecord {
        id: new_id(),
        project_id: project_id.to_string(),
        event_type: event_type.to_string(),
        entity_type: entity_type.map(|s| s.to_string()),
        entity_id: entity_id.map(|s| s.to_string()),
        from_state: from_state.map(|s| s.to_string()),
        to_state: to_state.map(|s| s.to_string()),
        context_json: None,
        source: Some("human".to_string()),
        conversation_id: None,
        created_at: now_millis(),
    };
    if let Err(e) = state.project_events.insert(record).await {
        tracing::warn!(
            event_type = event_type,
            project_id = project_id,
            error = %e,
            "[事件流] 埋点写入失败(不阻断主操作)"
        );
    }
}

/// 创建任务入参
#[derive(Debug, Deserialize)]
pub struct CreateTaskInput {
    pub project_id: String,
    pub title: String,
    #[serde(default)]
    pub description: String,
    #[serde(default = "default_priority")]
    pub priority: i32,
    pub branch_name: Option<String>,
    pub assignee: Option<String>,
    /// 关联灵感 ID(F-260619-01,1对1 单向,可空=不关联)。
    /// 空字符串视为不关联(与 AI 工具层一致)。
    pub idea_id: Option<String>,
    /// 管理维度池(知识图谱 Phase 1 V29,对标设计 §2.1)。默认 "todo"(待办池)。
    /// 合法值:backlog / todo / decision / active / done。非法值在 IPC 层兜底校验。
    /// 空字符串视为默认 todo(向后兼容,与 idea_id 一致处理)。
    #[serde(default = "default_queue")]
    pub queue: String,
    /// 父任务 ID(知识图谱 Phase 1 V29)。默认 None = 叶子任务。
    /// 非空 = 子任务(限制 1 级嵌套,无孙任务:父任务自身不能有 parent_id,由 IPC 层校验)。
    /// 空字符串视为 None(向后兼容)。
    #[serde(default)]
    pub parent_id: Option<String>,
    /// 结构化需求规格 JSON 字符串(知识图谱 Phase 1 V29)。
    /// 结构 { background, acceptance_criteria[], scope[], technical_design, custom_fields }。
    /// None = 无结构化规格(纯文本 description)。
    #[serde(default)]
    pub content_json: Option<String>,
}

fn default_priority() -> i32 {
    2 // medium — 新任务默认中优先级(非 high),符合常识
}

/// queue 默认值(serde default,对标 DB DEFAULT 'todo')
fn default_queue() -> String {
    "todo".to_string()
}

// ============================================================
// 知识图谱 Phase 1:queue 白名单 + queue/status 一致性约束(IPC 层校验,对标设计 §2.1)
// ============================================================

/// queue 合法值白名单(对标设计 §2.1 queue 字段语义)。
/// 不走 TaskStatus 枚举(queue 是独立的管理维度,与 status 执行维度正交)。
const TASK_QUEUE_VALUES: &[&str] = &["backlog", "todo", "decision", "active", "done"];

/// queue 执行中池(active 时 status 必须属于执行中三态之一,对标设计 §2.1 一致性约束)
const ACTIVE_OK_STATUSES: &[&str] = &["in_progress", "in_review", "testing"];

/// 校验 queue 值在白名单内,否则返回 Err(防拼写漂移/非法值进库)。
fn validate_queue(queue: &str) -> Result<(), String> {
    if TASK_QUEUE_VALUES.contains(&queue) {
        Ok(())
    } else {
        Err(format!(
            "非法 queue 值 {:?},合法值: {:?}",
            queue, TASK_QUEUE_VALUES
        ))
    }
}

/// queue/status 一致性约束校验(对标设计 §2.1,IPC 层校验不进状态机)。
///
/// 规则(设计 §2.1「queue 与 status 的关系」一致性约束):
/// - queue=done 时 status 必须=done
/// - queue=backlog 时 status 必须=todo
/// - queue=active 时 status ∈ {in_progress, in_review, testing}
/// - status=blocked 时 queue 可为 decision 或 active(本规则约束 queue 赋值场景,不在此单独拦)
/// - queue=todo 时 status=todo(默认);queue=decision 时无 status 强约束(待决策池可任意 status)
///
/// 注:create_task 仅校验 queue(新建任务 status 恒 todo),完整约束在 move_task_queue 落实。
fn assert_queue_status_consistent(queue: &str, status: &str) -> Result<(), String> {
    match queue {
        "done" => {
            if status != "done" {
                return Err(format!(
                    "一致性约束违反:queue=done 要求 status=done,当前 status={status:?}"
                ));
            }
        }
        "backlog" => {
            if status != "todo" {
                return Err(format!(
                    "一致性约束违反:queue=backlog 要求 status=todo,当前 status={status:?}"
                ));
            }
        }
        "active" => {
            if !ACTIVE_OK_STATUSES.contains(&status) {
                return Err(format!(
                    "一致性约束违反:queue=active 要求 status ∈ {:?},当前 status={status:?}",
                    ACTIVE_OK_STATUSES
                ));
            }
        }
        _ => {} // todo / decision 无 status 强约束
    }
    Ok(())
}

/// 列出未删除任务(deleted_at IS NULL)。
///
/// **向后兼容铁律**:`project_id` 与 `query` 两参都可选,旧调用方不传(或只传 project_id)
/// 必须等价改造前的全量行为,零破坏。
///
/// F-260621-02 查询维度补全(机制优先 prompt 说教):
/// - 优先走 `query`(`TaskQuery` 多维动态 WHERE):status(P1 下沉)/ keyword(P2 LIKE)
///   /project_id/priority/assignee/order_by/limit/offset 任意组合。
/// - 旧调用方仍可直接传 `project_id`(单维度),此时走 list_active_by_project
///   (SQL 下推,命中 idx_tasks_project_id),保持等价行为。
/// - query 与 project_id 同时传时:query 优先(其内含 project_id 维度,更全),project_id 忽略。
/// - 均不传时:全量未删任务(等价改造前 list_active 行为)。
///
/// status 值合法性兜底:TaskStatus::is_valid 拦截非法值(拼写错/越界),非法值返回 Err
/// (与 update_task 的 status 校验一致),不静默返回空结果误导调用方。
#[tauri::command]
pub async fn list_tasks(
    state: State<'_, AppState>,
    project_id: Option<String>,
    query: Option<TaskQuery>,
) -> Result<Vec<TaskRecord>, String> {
    if let Some(q) = &query {
        // status 值兜底校验(非法值早 Err,不进 DB 层)
        if let Some(status) = &q.status {
            if !TaskStatus::is_valid(status) {
                return Err(format!(
                    "非法 status 值 {:?}，合法值: {:?}",
                    status,
                    TaskStatus::valid_values()
                ));
            }
        }
        return state.tasks.list_by_query(q).await.map_err(err_str);
    }
    // 旧调用方:单维度 project_id(SQL 下推,fallback 等价改造前行为)
    let tasks = match project_id {
        Some(pid) => state
            .tasks
            .list_active_by_project(&pid)
            .await
            .map_err(err_str)?,
        None => state.tasks.list_active().await.map_err(err_str)?,
    };
    Ok(tasks)
}

/// 按条件计数任务(分页 total 用)。
#[tauri::command]
pub async fn count_tasks(
    state: State<'_, AppState>,
    query: Option<TaskQuery>,
) -> Result<i64, String> {
    let q = query.unwrap_or_default();
    state.tasks.count_by_query(&q).await.map_err(err_str)
}

/// 按 id 查任务，找不到返回 Err(供前端详情页)
#[tauri::command]
pub async fn get_task_by_id(
    state: State<'_, AppState>,
    id: String,
) -> Result<TaskRecord, String> {
    state
        .tasks
        .get_by_id(&id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("任务 {} 不存在", id))
}

/// 创建任务，返回完整记录
///
/// 知识图谱 Phase 1 V29(对标设计 §2.1):扩展 queue/parent_id/content_json 可选参数(向后兼容,
/// 旧调用方不传等价改造前行为)。三个新参数的 IPC 层校验:
/// - `queue`:白名单校验(validate_queue)+ queue/status 一致性(create_task 时 status 恒 todo,
///   仅 backlog/todo/decision 合法;active/done 需经 move_task_queue 或 advance_task 流转)。
/// - `parent_id`:1 级嵌套铁律(对标设计 §2.1 D2)。父任务自身不能有 parent_id(拒绝创建孙任务);
///   父任务不存在则拒(防悬空 parent_id)。空字符串视为 None(向后兼容)。
/// - `content_json`:仅做轻量 JSON 合法性校验(非空时必须是合法 JSON),结构细节由 AI 消费层负责。
#[tauri::command]
pub async fn create_task(
    state: State<'_, AppState>,
    input: CreateTaskInput,
) -> Result<TaskRecord, String> {
    // ── queue 校验(白名单 + 空串默认 todo)──
    // 空字符串视为默认 todo(向后兼容,与 idea_id 空串处理一致)
    let queue = if input.queue.trim().is_empty() {
        "todo".to_string()
    } else {
        let q = input.queue.trim();
        validate_queue(q)?;
        q.to_string()
    };
    // queue/status 一致性:create_task 时 status 恒 todo,仅 backlog/todo/decision 合法。
    // active/done 必须经 move_task_queue 流转,新建直接落 active/done 违反一致性约束。
    assert_queue_status_consistent(&queue, "todo")?;

    // ── parent_id 校验(1 级嵌套铁律,对标设计 §2.1 D2)──
    // 空字符串/纯空白视为 None(向后兼容)
    let parent_id = input
        .parent_id
        .as_ref()
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty());
    if let Some(pid) = &parent_id {
        let parent = state
            .tasks
            .get_by_id(pid)
            .await
            .map_err(err_str)?
            .ok_or_else(|| format!("父任务 {pid} 不存在,无法创建子任务"))?;
        // 1 级嵌套铁律:父任务自身有 parent_id → 它是子任务 → 拒绝在其下创建孙任务。
        if parent.parent_id.is_some() {
            return Err(format!(
                "违反 1 级嵌套铁律:父任务 {pid} 自身是子任务(parent_id={:?}),不允许在其下创建孙任务",
                parent.parent_id
            ));
        }
    }

    // ── content_json 轻量校验(非空时须是合法 JSON)──
    let content_json = match &input.content_json {
        Some(c) if !c.trim().is_empty() => {
            // 校验合法 JSON(防脏数据/截断串进库);结构细节由 AI 消费层负责
            serde_json::from_str::<serde_json::Value>(c)
                .map_err(|e| format!("content_json 不是合法 JSON: {e}"))?;
            Some(c.clone())
        }
        _ => None, // 空串/None 视为无结构化规格
    };

    let now = now_millis();
    let record = TaskRecord {
        id: new_id(),
        project_id: input.project_id,
        title: input.title,
        description: input.description,
        status: TaskStatus::Todo,
        priority: input.priority,
        branch_name: input.branch_name,
        assignee: input.assignee,
        workflow_def_id: None,
        base_branch: None,
        review_rounds: 0,
        output_json: None,
        // F-260619-01:空字符串视为不关联(与 AI 工具层一致)
        idea_id: input.idea_id.filter(|s| !s.is_empty()),
        // 知识图谱 Phase 1 V29 三列:经上方校验的 queue / parent_id / content_json
        queue,
        parent_id,
        content_json,
        created_at: now.clone(),
        updated_at: now,
    };
    state
        .tasks
        .insert(record.clone())
        .await
        .map_err(err_str)?;
    // 知识图谱 Phase 2(对标设计 §2.4 hook/after):task_created 事件。best-effort 不阻断。
    emit_event(
        &state,
        &record.project_id,
        "task_created",
        Some("task"),
        Some(&record.id),
        None,
        Some(record.status.as_str()),
    )
    .await;
    Ok(record)
}

/// 更新任务单个字段（字段名走 df-storage 白名单校验；status 值走枚举校验）。
///
/// B-260801-01(P0-1):0 行(id 不存在 / 已软删)→ 返 Err 而非 false,杜绝「假成功」
/// (前端 updateTask 仅 await 不读 bool,但 store.runWithCatch 把 Err 转 toast,用户能感知;
/// 若返 false 则 store 静默忽略,本地状态前端手动改但库无变更,造成 UI 与 DB 不一致)。
///
/// 返回值保持 bool(成功恒 true):前端 api 包装 `Promise<boolean>` 契约不变零破坏,
/// title 不在 IPC 返回值——IPC 唯一调用点(Tasks.vue 优先级改)不展示带返回值的 toast,
/// title 仅供 AI 工具卡片路径(已修见 ai/tools/task.rs),IPC 路径返 title 为冗余。
#[tauri::command]
pub async fn update_task(
    state: State<'_, AppState>,
    id: String,
    field: String,
    value: String,
) -> Result<bool, String> {
    // status 值校验保留：仅对「非法值」(拼写错 in-progess / "in progress" / 大小写错 / 越界)
    // 给出友好早错误(先于 crud.rs 白名单那串冷冰冰的「字段不在白名单」拒)。合法 status 值
    // 不可经本 IPC 写入——crud.rs tasks 白名单已收口移除 status(F-03 batch64 b94e74a /
    // D-260616-04)，所有 status 改动须走 advance_task_atomic 状态机(CAS + can_transition +
    // review_rounds 累加,唯一 status 写入路径)。即此 is_valid 校验的「通过」分支永不会触达
    // update_field 的 status 写入(白名单会先拒)；它只为非法值兜底 UX，不承担合法值写入职责。
    // priority 值校验同理补在下方：拦截 "abc" / 999 等脏数据静默落库。
    if field == "status" && !TaskStatus::is_valid(&value) {
        return Err(format!(
            "非法 status 值 {:?}，合法值: {:?}",
            value,
            TaskStatus::valid_values()
        ));
    }
    // priority 值域 0..=3（0=critical, 1=high, 2=medium, 3=low），与前端 <select> 及
    // constants/project.ts 的 PRIORITY_LABELS 一致。拦截 "abc" / 999 等脏数据。
    if field == "priority" {
        match value.parse::<i32>() {
            Ok(p) if (0..=3).contains(&p) => {}
            _ => {
                return Err(format!(
                    "非法 priority 值 {:?}，合法值: 整数 0..=3（0=critical, 1=high, 2=medium, 3=low）",
                    value
                ));
            }
        }
    }
    // project_id 跨表存在性校验(B-260616-16 收尾)。
    // crud.rs tasks 白名单保留 project_id(支持跨项目移动),跨表约束由本命令层兜底。
    // 拦截移动到不存在的项目(手输脏 id / 已物理删除的项目),避免 tasks.project_id 悬空。
    if field == "project_id" {
        let exists = state.projects.get_by_id(&value).await.map_err(err_str)?;
        if exists.is_none() {
            return Err(format!("非法 project_id 值 {:?}，目标项目不存在", value));
        }
    }
    // parent_id 1 级嵌套铁律(对标 create_task L203-221,verify agent 发现的 P1:update_task 绕过)。
    // parent_id 在 tasks 白名单(设计 §五 move/update 路径),但 1 级铁律须 IPC 层校验,否则
    // update_task(id,"parent_id",X) 可创建孙任务(X 自身有 parent_id)或悬空(X 不存在)或自环。
    if field == "parent_id" {
        let new_pid = value.trim();
        if !new_pid.is_empty() {
            if new_pid == id {
                return Err("非法 parent_id:不能将任务设为自身的父任务(自环)".to_string());
            }
            let parent = state
                .tasks
                .get_by_id(new_pid)
                .await
                .map_err(err_str)?
                .ok_or_else(|| format!("非法 parent_id 值 {:?},父任务不存在", value))?;
            if parent.parent_id.is_some() {
                return Err(format!(
                    "违反 1 级嵌套铁律:父任务 {new_pid} 自身是子任务(parent_id={:?}),不允许挂孙任务",
                    parent.parent_id
                ));
            }
        }
        // 空字符串 = None(解除父),合法放行
    }
    // B-260801-01(P0-1):update_field 返 affected>0;false = id 不存在或已软删(0 行)。
    // 不可静默返 false——前端 store.runWithCatch 把 Err 转 toast,而 false 会被忽略致假成功。
    //
    // 任务字段更新是高频「项目活跃」信号(改 title/priority/assignee 等),埋点 task_updated
    // 推动项目最近活跃排序反映真实业务(问题3)。best-effort 不阻断。
    // 读当前 project_id(一次轻量读):update_field 返 bool 不带 project_id,无法直接埋点。
    let current = state
        .tasks
        .get_by_id(&id)
        .await
        .map_err(err_str)?;
    let updated = state
        .tasks
        .update_field(&id, &field, &value)
        .await
        .map_err(err_str)?;
    if !updated {
        return Err(format!("任务 ID {id} 不存在或已删除"));
    }
    if let Some(rec) = current {
        emit_event(
            &state,
            &rec.project_id,
            "task_updated",
            Some("task"),
            Some(&rec.id),
            None,
            None,
        )
        .await;
    }
    Ok(true)
}

/// 删除任务(软删 → 回收站,可恢复)。对标 delete_project(SET deleted_at=now)。
///
/// 埋点 task_deleted(问题3 项目最近活跃排序):删除是业务事件,推动项目活跃时间。
/// best-effort 不阻断。读 project_id 一次轻量读(soft_delete 返 bool 不带 project_id)。
#[tauri::command]
pub async fn delete_task(state: State<'_, AppState>, id: String) -> Result<bool, String> {
    let rec = state.tasks.get_by_id(&id).await.map_err(err_str)?;
    let ok = state.tasks.soft_delete(&id).await.map_err(err_str)?;
    if ok {
        if let Some(r) = rec {
            emit_event(
                &state,
                &r.project_id,
                "task_deleted",
                Some("task"),
                Some(&r.id),
                None,
                None,
            )
            .await;
        }
    }
    Ok(ok)
}

/// 恢复任务(从回收站还原,清 deleted_at)。对标 restore_project。
///
/// 埋点 task_restored(问题3 项目最近活跃排序):恢复是业务事件。best-effort 不阻断。
#[tauri::command]
pub async fn restore_task(state: State<'_, AppState>, id: String) -> Result<bool, String> {
    let rec = state.tasks.get_by_id(&id).await.map_err(err_str)?;
    let ok = state.tasks.restore(&id).await.map_err(err_str)?;
    if ok {
        if let Some(r) = rec {
            emit_event(
                &state,
                &r.project_id,
                "task_restored",
                Some("task"),
                Some(&r.id),
                None,
                None,
            )
            .await;
        }
    }
    Ok(ok)
}

/// 推进任务状态(任务推进链 F-260616-02,推进链唯一 status 写入路径)。
///
/// thin 入口(D-260616-03):业务逻辑(状态机校验 + 原子 CAS + review_rounds 累加)
/// 落 df-nodes::task_advance_node::advance_task_atomic,本命令只做参数转发与错误串化。
///
/// 流程:读当前态 → can_transition 校验 → 下沉 SQL `WHERE id AND status=expected`
/// 防 TOCTOU → 退回转换一并 review_rounds+=1。失败均返回 Err(状态机/TOCTOU/任务不存在)。
///
/// **知识图谱 Phase 1 V29 父聚合(对标设计 §2.1)**:推进完成后,若推进的任务有 parent_id,
/// 触发父任务 status 重算(recompute_parent_status)。父任务=容器模型,status 不走状态机,
/// 由子任务聚合计算(聚合规则见 recompute_parent_status)。聚合失败不阻断推进(best-effort,
/// 对标设计 §十一「事件流写入失败不阻断主操作」同类宽容语义)。
///
/// 返回:推进成功后的最新 TaskRecord(含新 status / 累加后的 review_rounds)。
#[tauri::command]
pub async fn advance_task(
    state: State<'_, AppState>,
    id: String,
    target_status: String,
) -> Result<TaskRecord, String> {
    // 知识图谱 Phase 2:推进前读当前态,作 task_advanced 事件 from_state(仅一次轻量读,
    // advance 低频无压力)。失败(任务不存在)不阻断——后续 atomic 会用 NotFound 拒绝,from 留空。
    let from_state = state
        .tasks
        .get_by_id(&id)
        .await
        .map_err(err_str)?
        .map(|t| t.status);

    let updated = df_nodes::task_advance_node::advance_task_atomic(
        &state.tasks,
        &id,
        &target_status,
    )
    .await
    .map_err(err_str)?;

    // 父聚合(知识图谱 Phase 1 V29):推进的子任务有 parent_id → 重算父 status。
    // best-effort:聚合失败不阻断推进(子任务已成功推进是主结果,父 status 漂移可后续修正),
    // 仅 warn 日志记录。父任务 status 不走状态机,经专用方法 set_status_for_aggregation
    // 直接写(绕过 D-260616-04 status 收口:父任务=容器模型,聚合规则是唯一非状态机写入路径)。
    if let Some(pid) = &updated.parent_id {
        if let Err(e) = recompute_parent_status(&state, pid).await {
            tracing::warn!(
                task_id = %id,
                parent_id = %pid,
                error = %e,
                "[父聚合] 重算父任务 status 失败(不阻断子任务推进)"
            );
        }
    }

    // 知识图谱 Phase 2(对标设计 §2.4 hook/after):task_advanced 事件。best-effort 不阻断。
    emit_event(
        &state,
        &updated.project_id,
        "task_advanced",
        Some("task"),
        Some(&updated.id),
        from_state.as_ref().map(TaskStatus::as_str),
        Some(updated.status.as_str()),
    )
    .await;

    Ok(updated)
}

// ============================================================
// 知识图谱 Phase 1:父任务聚合(对标设计 §2.1 父聚合规则)
// ============================================================

/// 父任务 status 重算(对标设计 §2.1 聚合规则,D3 父任务=容器模型)。
///
/// 聚合规则(设计 §2.1「父任务推导 status」表,优先级从高到低):
/// 1. 任一子 blocked → 父 blocked(阻塞优先,避免掩盖卡点)
/// 2. 任一子 in_progress → 父 in_progress(执行中)
/// 3. 全子 done/cancelled → 父 done(全部完成/取消)
/// 4. 全子 todo → 父 todo(尚未开始)
/// 5. 其他混合态(如 todo+done) → 父 in_progress(进行中,有进展未全完)
///
/// 触发时机:advance_task 子任务推进成功后,若子任务有 parent_id 则调本函数。
/// 数据源:count_children_by_status(一次 GROUP BY 查询,数据量小无压力)。
/// 写入:set_status_for_aggregation(父任务 status 唯一非状态机写入路径)。
///
/// 返回:重算后的父任务最新 status(若与当前相同则不写,返当前值)。
async fn recompute_parent_status(state: &State<'_, AppState>, parent_id: &str) -> Result<String, String> {
    let counts = state
        .tasks
        .count_children_by_status(parent_id)
        .await
        .map_err(err_str)?;
    // 无子任务(parent_id 悬空,理论上不该发生):不重算,返当前 status
    if counts.is_empty() {
        return state
            .tasks
            .get_by_id(parent_id)
            .await
            .map_err(err_str)?
            .map(|t| t.status.as_str().to_string())
            .ok_or_else(|| format!("父任务 {parent_id} 不存在"));
    }

    // 转 HashMap<status, count> 便于按规则判定
    let map: std::collections::HashMap<String, i64> = counts.into_iter().collect();
    let total: i64 = map.values().sum();
    let blocked = map.get("blocked").copied().unwrap_or(0);
    let in_progress = map.get("in_progress").copied().unwrap_or(0);
    let todo = map.get("todo").copied().unwrap_or(0);
    let done = map.get("done").copied().unwrap_or(0);
    let cancelled = map.get("cancelled").copied().unwrap_or(0);

    // 聚合规则判定(优先级从高到低,首个命中即定)
    let new_status = if blocked > 0 {
        "blocked".to_string()
    } else if in_progress > 0 {
        "in_progress".to_string()
    } else if (done + cancelled) == total {
        // 全 done/cancelled → done(终端态聚合为 done)
        "done".to_string()
    } else if todo == total {
        // 全 todo → todo(尚未开始)
        "todo".to_string()
    } else {
        // 其他混合态(如 todo+done, in_review+done 等)→ in_progress(进行中)
        "in_progress".to_string()
    };

    // 读当前父 status,相同则不写(避免无谓 updated_at 抖动)
    let current = state
        .tasks
        .get_by_id(parent_id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("父任务 {parent_id} 不存在"))?;
    if current.status.as_str() == new_status {
        return Ok(new_status);
    }
    state
        .tasks
        .set_status_for_aggregation(parent_id, &new_status)
        .await
        .map_err(err_str)?;
    Ok(new_status)
}

// ============================================================
// 知识图谱 Phase 1:task_link CRUD IPC(对标设计 §2.2 + §五 AI 工具表)
// ============================================================

/// 创建任务横向关联(对标设计 §2.2,AI 拓扑排序编排调度的基础)。
///
/// 调 TaskLinkRepo::create_link(应用层校验 link_type 白名单 + depends_on 链 BFS 循环依赖检测)。
/// 循环依赖 / 自环 / 非法 link_type 在 repo 层拒绝(对标 D8)。
///
/// 参数:
/// - `task_id`:source 任务 ID(关联发起方)
/// - `target_id`:target 任务 ID(关联指向方)
/// - `link_type`:depends_on / blocks / relates_to(白名单校验)
/// - `remark`:可选备注
///
/// 返回:插入的 link id。
#[tauri::command]
pub async fn create_task_link(
    state: State<'_, AppState>,
    task_id: String,
    target_id: String,
    link_type: String,
    remark: Option<String>,
) -> Result<String, String> {
    let id = new_id();
    state
        .task_links
        .create_link(&id, &task_id, &target_id, &link_type, remark.as_deref())
        .await
        .map_err(err_str)
}

/// 删除任务关联(按 link id,对标设计 §2.2)。返回是否命中。
#[tauri::command]
pub async fn remove_task_link(state: State<'_, AppState>, id: String) -> Result<bool, String> {
    state.task_links.delete(&id).await.map_err(err_str)
}

/// 任务关联查询入参方向(对标设计 §2.2 + §五 list_task_links)。
///
/// - `Outgoing`:task_id 作为 source 查其声明的全部关联(我依赖谁/我阻塞谁)
/// - `Incoming`:task_id 作为 target 查谁指向它(谁依赖我/谁被我阻塞,反向查询)
/// - `Both`:双向合并(去重),全量关联视图
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LinkDirection {
    Outgoing,
    Incoming,
    Both,
}

impl Default for LinkDirection {
    fn default() -> Self {
        LinkDirection::Both
    }
}

/// 查询任务关联(AI 编排调度用,对标设计 §2.2 + §五 list_task_links)。
///
/// 参数:
/// - `task_id`:目标任务 ID
/// - `direction`:查询方向(Outgoing=作为 source / Incoming=作为 target / Both=双向合并),
///   默认 Both(全量关联视图)。空字符串/未传走默认 Both(向后兼容)。
///
/// 返回:关联记录列表(TaskLinkRecord,按 created_at 升序)。
#[tauri::command]
pub async fn list_task_links(
    state: State<'_, AppState>,
    task_id: String,
    direction: Option<LinkDirection>,
) -> Result<Vec<TaskLinkRecord>, String> {
    let dir = direction.unwrap_or_default();
    match dir {
        LinkDirection::Outgoing => state.task_links.get_by_source(&task_id).await.map_err(err_str),
        LinkDirection::Incoming => state.task_links.get_by_target(&task_id).await.map_err(err_str),
        // 双向合并:取 Outgoing + Incoming 后按 id 去重(同一 link 在双向查询中可能出现两次)
        LinkDirection::Both => {
            let mut outgoing = state.task_links.get_by_source(&task_id).await.map_err(err_str)?;
            let incoming = state.task_links.get_by_target(&task_id).await.map_err(err_str)?;
            // 按 id 去重(Outgoing 优先保留,incoming 中 id 未出现的才追加)
            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);
                }
            }
            // 按 created_at 升序(合并后重排,与单向查询排序一致)
            outgoing.sort_by(|a, b| a.created_at.cmp(&b.created_at));
            Ok(outgoing)
        }
    }
}

// ============================================================
// 知识图谱 Phase 1:move_task_queue(跨池移动 + 一致性约束,对标设计 §2.1 + §五)
// ============================================================

/// 跨池移动任务(对标设计 §2.1 queue 字段语义 + §五 move_task_queue)。
///
/// queue 是管理维度池(backlog/todo/decision/active/done),与 status(执行维度)正交。
/// move_task_queue 改 queue,同时按一致性约束联动调整 status(对标设计 §2.1):
/// - queue=done → status 强制=done(池完成即任务完成)
/// - queue=backlog → status 强制=todo(需求池任务尚未开始)
/// - queue=active → status 若不在 {in_progress,in_review,testing} 则强制=in_progress
/// - queue=todo → status 若非 todo 则强制=todo(待办池任务尚未开始)
/// - queue=decision → status 不变(待决策池可保留任意执行态,暂停推进但执行态保留)
///
/// 父任务(容器模型)也可 move_task_queue(其 status 由聚合规则管,本命令仅联动改 status
/// 以满足一致性约束,聚合规则在子任务推进时仍会重算)。
///
/// queue/status 均走白名单校验(status 写入用专用 set_status_for_aggregation 绕过 status 收口,
/// 因 move_task_queue 是合法的非状态机 status 联动路径,非 advance_task 状态机路径)。
#[tauri::command]
pub async fn move_task_queue(
    state: State<'_, AppState>,
    id: String,
    new_queue: String,
) -> Result<TaskRecord, String> {
    let new_queue = new_queue.trim().to_string();
    validate_queue(&new_queue)?;

    // 读当前任务(取当前 status 做一致性联动决策)
    let current = state
        .tasks
        .get_by_id(&id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("任务 {id} 不存在"))?;

    // 一致性约束联动:根据 new_queue 决定 status 是否需调整
    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() // 否则强制进 in_progress(执行中池默认执行态)
            }
        }
        "todo" => "todo".to_string(), // 待办池任务 status 强制=todo(从 active 退回 todo 池即重置执行态)
        "decision" => current.status.as_str().to_string(), // 待决策池保留当前 status(暂停推进不重置执行态)
        _ => unreachable!("validate_queue 已收口"),
    };

    // 写 queue:走通用 update_field(queue 已在 tasks 白名单登记,知识图谱 Phase 1 V29 新增)。
    // 与 current.queue 不同才写(避免无谓 updated_at 抖动)。
    if current.queue != new_queue {
        state
            .tasks
            .update_field(&id, "queue", &new_queue)
            .await
            .map_err(err_str)?;
    }
    // 写 status(专用 set_status_for_aggregation 绕过 status 收口,move_task_queue 是合法非状态机路径)
    if current.status.as_str() != new_status {
        state
            .tasks
            .set_status_for_aggregation(&id, &new_status)
            .await
            .map_err(err_str)?;
    }

    // 知识图谱 Phase 2(对标设计 §2.4 hook/after):queue 变化事件。best-effort 不阻断。
    // 仅在 queue 实际变化时埋点(避免 no-op 移动产噪音事件)。
    //
    // M17:用独立 event_type "task_queue_moved"(非复用 "task_advanced"),与 status 推进事件
    // 语义解耦——advance_task 触发的 task_advanced 用 from_state/to_state 装**执行态**值
    // (todo→in_progress 等),而 move_task_queue 改的是**管理维度池**(backlog/todo/decision/
    // active/done),两者正交。混用同一 event_type + 同样的 from/to 字段会让消费者无法区分
    // 「任务执行推进」与「跨池移动」,过滤 task_advanced 的事件流会混入 queue 漂移噪音。
    // 独立 type 后:status 推进走 task_advanced,queue 移动走 task_queue_moved,语义自洽,
    // from/to 字段各自装对应维度的值(queue_moved 装 queue 值),不冲突。
    if current.queue != new_queue {
        emit_event(
            &state,
            &current.project_id,
            "task_queue_moved",
            Some("task"),
            Some(&current.id),
            Some(&current.queue),
            Some(&new_queue),
        )
        .await;
    }

    // 回读最新记录返回
    state
        .tasks
        .get_by_id(&id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("任务 {id} 不存在(移动后回读失败)"))
}

// ============================================================
// 知识图谱 Phase 1:get_task_tree(父子任务树,对标设计 §2.1 + §五)
// ============================================================

/// 任务树节点(父 + 子任务列表,对标设计 §2.1 限 1 级嵌套)。
///
/// 设计限 1 级嵌套(无孙任务,D2),故树结构是扁平的「父 + 直接子任务列表」,无需递归。
#[derive(Debug, Serialize)]
pub struct TaskTreeNode {
    /// 父任务(根节点,可能是叶子任务无子)
    pub parent: TaskRecord,
    /// 直接子任务列表(parent_id 指向 parent 的任务,按 created_at 升序)。叶子任务时为空。
    pub children: Vec<TaskRecord>,
}

/// 获取任务父子树(对标设计 §2.1 + §五 get_task_tree)。
///
/// 限 1 级嵌套(设计 D2:无孙任务),故本方法取「指定任务 + 其直接子任务」,不递归。
///
/// 参数 `parent_id`:任务 ID(无论它是叶子还是父任务,都返回其自身 + 子任务列表)。
/// AI 用途:查看需求分解结构、子任务进度聚合(设计 §六 ⑥⑦ AI 自检 + 父聚合)。
#[tauri::command]
pub async fn get_task_tree(
    state: State<'_, AppState>,
    parent_id: String,
) -> Result<TaskTreeNode, String> {
    let parent = state
        .tasks
        .get_by_id(&parent_id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("任务 {parent_id} 不存在"))?;
    let children = state.tasks.get_children(&parent_id).await.map_err(err_str)?;
    Ok(TaskTreeNode { parent, children })
}
