//! 工程系统 — project_modules 表 CRUD(V34,项目多工程,每个工程独立代码仓库)
//!
//! 对标 [`project_service_repo`] 的风格(手写 Repo,与设计 §五工程系统 IPC 对齐)。
//!
//! 关键设计:
//! - **不存 Git 状态**:分支/改动/最近提交是实时派生的(查 git 命令),不进表;
//!   表只存工程元数据(路径/Git 地址/技术栈)。
//! - **硬删**:`delete` / `delete_by_project` 物理删除(工程不需要软删审计追溯)。
//! - **sort_order 排序**:`list_by_project` 按 `sort_order ASC` 返回,前端工程列表稳定顺序。
//! - **auto_detected**:技术栈自动检测标志(扫描目录结构推断),人工覆盖时置 false。

use std::sync::Arc;

use rusqlite::{params, Row};
use tokio::sync::Mutex;

use crate::db::Database;
use crate::models::ProjectModuleRecord;

use super::{now_millis_str, storage_err};

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

fn project_module_from_row(row: &Row<'_>) -> std::result::Result<ProjectModuleRecord, rusqlite::Error> {
    Ok(ProjectModuleRecord {
        id: row.get("id")?,
        project_id: row.get("project_id")?,
        name: row.get("name")?,
        path: row.get("path")?,
        git_url: row.get("git_url")?,
        stack: row.get("stack")?,
        auto_detected: row.get("auto_detected")?,
        sort_order: row.get("sort_order")?,
        created_at: row.get("created_at")?,
        updated_at: row.get("updated_at")?,
    })
}

// ============================================================
// Repo 实现(手写,参考 project_service_repo.rs 风格)
// ============================================================

/// 工程系统 CRUD(project_modules,V34,可改非审计)。
///
/// 与 [`ProjectServiceRepo`](super::ProjectServiceRepo) 同款手写 Repo:
/// 表有 `updated_at`,但工程无字段级白名单收口需求(无敏感字段、无枚举类型),
/// 故不强制走 `impl_repo!` 宏的 `update_field` 路径——`update_full` 整体替换足够。
pub struct ProjectModuleRepo {
    conn: Arc<Mutex<rusqlite::Connection>>,
}

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

    /// 插入一条工程记录。`created_at` / `updated_at` 由本方法内部取当前毫秒覆盖。
    /// 返回插入是否成功(affected > 0)。
    pub async fn insert(&self, record: ProjectModuleRecord) -> Result<bool, df_types::error::Error> {
        let conn = self.conn.clone();
        let now = now_millis_str();
        let mut rec = record;
        rec.created_at = now.clone();
        rec.updated_at = now;
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let conn = &*guard;
            let r = &rec;
            let affected = conn
                .execute(
                    "INSERT INTO project_modules \
                     (id, project_id, name, path, git_url, stack, auto_detected, sort_order, created_at, updated_at) \
                     VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
                    params![
                        r.id, r.project_id, r.name, r.path,
                        r.git_url, r.stack, r.auto_detected, r.sort_order,
                        r.created_at, r.updated_at,
                    ],
                )
                .map_err(storage_err)?;
            Ok(affected > 0)
        })
        .await
        .map_err(storage_err)?
    }

    /// 按 id 查询工程记录。
    pub async fn get_by_id(&self, id: &str) -> Result<Option<ProjectModuleRecord>, df_types::error::Error> {
        let conn = self.conn.clone();
        let id = id.to_owned();
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let mut stmt = guard
                .prepare("SELECT * FROM project_modules WHERE id = ?1")
                .map_err(storage_err)?;
            let row = stmt
                .query_row(params![id], project_module_from_row)
                .optional()
                .map_err(storage_err)?;
            Ok(row)
        })
        .await
        .map_err(storage_err)?
    }

    /// 按项目查询全部工程(命中 idx_project_modules_project,按 sort_order ASC 稳定排序)。
    pub async fn list_by_project(
        &self,
        project_id: &str,
    ) -> Result<Vec<ProjectModuleRecord>, df_types::error::Error> {
        let conn = self.conn.clone();
        let project_id = project_id.to_owned();
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let mut stmt = guard
                .prepare(
                    "SELECT id, project_id, name, path, git_url, stack, auto_detected, sort_order, created_at, updated_at \
                     FROM project_modules WHERE project_id = ?1 ORDER BY sort_order ASC",
                )
                .map_err(storage_err)?;
            let rows = stmt
                .query_map(params![project_id], project_module_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)?
    }

    /// 整体更新工程记录(全可变字段,保留 id 与 created_at,updated_at 内部覆盖)。
    /// 返回是否命中(affected > 0)。
    pub async fn update_full(&self, record: &ProjectModuleRecord) -> Result<bool, df_types::error::Error> {
        let conn = self.conn.clone();
        let mut rec = record.clone();
        rec.updated_at = now_millis_str();
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let conn = &*guard;
            let r = &rec;
            let affected = conn
                .execute(
                    "UPDATE project_modules SET \
                     project_id = ?1, name = ?2, path = ?3, git_url = ?4, stack = ?5, \
                     auto_detected = ?6, sort_order = ?7, updated_at = ?8 \
                     WHERE id = ?9",
                    params![
                        r.project_id, r.name, r.path,
                        r.git_url, r.stack, r.auto_detected, r.sort_order,
                        r.updated_at, r.id,
                    ],
                )
                .map_err(storage_err)?;
            Ok(affected > 0)
        })
        .await
        .map_err(storage_err)?
    }

    /// 按 id 硬删工程(工程不需要软删审计追溯)。
    pub async fn delete(&self, id: &str) -> Result<bool, df_types::error::Error> {
        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 project_modules WHERE id = ?1",
                    params![id],
                )
                .map_err(storage_err)?;
            Ok(affected > 0)
        })
        .await
        .map_err(storage_err)?
    }

    /// 按项目删除全部工程(删项目时级联清理,硬删)。
    pub async fn delete_by_project(&self, project_id: &str) -> Result<bool, df_types::error::Error> {
        let conn = self.conn.clone();
        let project_id = project_id.to_owned();
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let affected = guard
                .execute(
                    "DELETE FROM project_modules WHERE project_id = ?1",
                    params![project_id],
                )
                .map_err(storage_err)?;
            Ok(affected > 0)
        })
        .await
        .map_err(storage_err)?
    }
}

// 引入 OptionalExtension 用于 query_row().optional()(避免每次写完整路径)
use rusqlite::OptionalExtension;

// ============================================================
// 单元测试 — ProjectModuleRepo CRUD(内存 DB,对标 project_service_repo 测试风格)
// ============================================================

#[cfg(test)]
mod tests {
    use super::*;
    use crate::db::Database;

    /// 建库 + 建占位 project 满足 FK 约束 + 返回 repo(对标 project_service_repo::setup)。
    async fn setup() -> (Database, ProjectModuleRepo, String) {
        let db = Database::open_in_memory().await.expect("open_in_memory");
        let project_id = "proj-test".to_string();
        db.conn()
            .blocking_lock()
            .execute(
                "INSERT INTO projects (id, name, status, path, stack, created_at, updated_at) \
                 VALUES (?1, ?2, 'active', '/tmp', 'rust', '0', '0')",
                params![project_id, "Test Project"],
            )
            .expect("insert placeholder project");
        let repo = ProjectModuleRepo::new(&db);
        (db, repo, project_id)
    }

    fn mrec(project_id: &str, name: &str, path: &str) -> ProjectModuleRecord {
        ProjectModuleRecord {
            id: format!("mod-{name}"),
            project_id: project_id.to_string(),
            name: name.to_string(),
            path: path.to_string(),
            git_url: None,
            stack: None,
            auto_detected: false,
            sort_order: 0,
            created_at: "0".to_string(),
            updated_at: "0".to_string(),
        }
    }

    #[tokio::test]
    async fn insert_and_get_by_id_reads_back_full_fields() {
        let (_db, repo, pid) = setup().await;
        let mut rec = mrec(&pid, "frontend", "/p/frontend");
        rec.git_url = Some("https://example.com/f.git".to_string());
        rec.stack = Some(r#"{"lang":"ts"}"#.to_string());
        rec.auto_detected = true;
        rec.sort_order = 2;
        let ok = repo.insert(rec.clone()).await.expect("insert");
        assert!(ok);
        let got = repo.get_by_id(&rec.id).await.expect("get").expect("found");
        assert_eq!(got.name, "frontend");
        assert_eq!(got.path, "/p/frontend");
        assert_eq!(got.git_url.as_deref(), Some("https://example.com/f.git"));
        assert_eq!(got.stack.as_deref(), Some(r#"{"lang":"ts"}"#));
        assert!(got.auto_detected);
        assert_eq!(got.sort_order, 2);
    }

    #[tokio::test]
    async fn list_by_project_orders_by_sort_order_asc() {
        let (_db, repo, pid) = setup().await;
        let mut a = mrec(&pid, "a", "/p/a");
        a.sort_order = 5;
        let mut b = mrec(&pid, "b", "/p/b");
        b.sort_order = 1;
        let mut c = mrec(&pid, "c", "/p/c");
        c.sort_order = 3;
        repo.insert(a).await.unwrap();
        repo.insert(b).await.unwrap();
        repo.insert(c).await.unwrap();
        let list = repo.list_by_project(&pid).await.expect("list");
        assert_eq!(list.len(), 3);
        // sort_order ASC:b(1) → c(3) → a(5)
        assert_eq!(list[0].name, "b");
        assert_eq!(list[1].name, "c");
        assert_eq!(list[2].name, "a");
    }

    #[tokio::test]
    async fn update_full_changes_fields_and_preserves_created_at() {
        let (_db, repo, pid) = setup().await;
        let rec = mrec(&pid, "frontend", "/p/old");
        repo.insert(rec.clone()).await.unwrap();
        let original = repo.get_by_id(&rec.id).await.unwrap().unwrap();
        // 整体更新:path 变了,sort_order 变了
        let mut updated = original.clone();
        updated.path = "/p/new".to_string();
        updated.sort_order = 9;
        let hit = repo.update_full(&updated).await.expect("update");
        assert!(hit);
        let got = repo.get_by_id(&rec.id).await.unwrap().unwrap();
        assert_eq!(got.path, "/p/new");
        assert_eq!(got.sort_order, 9);
        // created_at 保留不变(update_full 不覆盖 created_at)
        assert_eq!(got.created_at, original.created_at);
    }

    #[tokio::test]
    async fn delete_removes_record() {
        let (_db, repo, pid) = setup().await;
        let rec = mrec(&pid, "frontend", "/p/f");
        repo.insert(rec.clone()).await.unwrap();
        let hit = repo.delete(&rec.id).await.expect("delete");
        assert!(hit);
        assert!(repo.get_by_id(&rec.id).await.unwrap().is_none());
    }

    #[tokio::test]
    async fn delete_by_project_removes_all() {
        let (_db, repo, pid) = setup().await;
        repo.insert(mrec(&pid, "a", "/p/a")).await.unwrap();
        repo.insert(mrec(&pid, "b", "/p/b")).await.unwrap();
        let hit = repo.delete_by_project(&pid).await.expect("delete_by_project");
        assert!(hit);
        assert!(repo.list_by_project(&pid).await.unwrap().is_empty());
    }
}
