//! 工程系统命令(V34,project_modules 表,项目多工程,每个工程独立代码仓库)
//!
//! 对标 [`services`](super::services) 的 IPC 模式:CRUD 返回完整记录,前端拿回 id 直接用。
//!
//! 关键设计(对标设计 §五工程系统 + V34 迁移注释):
//! - **工程元数据 CRUD**:路径/Git 地址/技术栈存表,create/update 返回完整记录。
//! - **Git 状态实时派生**:`get_module_git_status` 在工程目录跑 git 命令(分支/改动/最近提交),
//!   10s 超时,无 .git 返回空状态。**前端 IPC 用 std::process::Command**(非 df-execute,
//!   因为这是用户读操作,不是 AI 工具调用,不走审批/沙箱)。
//! - **文件浏览**(Batch 10):`get_module_file_tree` 列目录并合并 git status --porcelain
//!   到每个条目的 git_status 字段;`read_module_file` 读单文件(1MB 上限,二进制检测)。
//!   同样走 std::process::Command 读 git,目录扫描用 std::fs(read_dir)。
//! - **路径校验**:trim + 非空校验在前置 IPC 层(给清晰错误)。

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;

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

use df_storage::models::{ModuleDependencyRecord, ProjectModuleRecord};
use df_types::types::new_id;

use crate::state::AppState;

use super::{err_str, now_millis};

// ============================================================
// 入参 / 出参结构
// ============================================================

/// 新增工程入参(对标设计 §五 add_project_module)。
#[derive(Debug, Deserialize)]
pub struct AddProjectModuleInput {
    pub project_id: String,
    pub name: String,
    /// 工程目录绝对路径(本机)
    pub path: String,
    /// 远程仓库地址,可选(单仓库本地工程可为空)
    #[serde(default)]
    pub git_url: Option<String>,
    /// 技术栈 JSON 字符串(前端检测后传入),可选
    #[serde(default)]
    pub stack: Option<String>,
}

/// 更新工程入参(部分更新语义,仅传入字段被覆盖;对标设计 §五 update_project_module)。
#[derive(Debug, Deserialize)]
pub struct UpdateProjectModuleInput {
    pub id: String,
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default)]
    pub path: Option<String>,
    #[serde(default)]
    pub git_url: Option<String>,
    #[serde(default)]
    pub stack: Option<String>,
}

// ============================================================
// 辅助:Optional 字段 trim + 空串归一为 None
// ============================================================

fn trim_opt(s: Option<String>) -> Option<String> {
    s.map(|v| v.trim().to_string()).filter(|v| !v.is_empty())
}

// ============================================================
// IPC 命令 — 工程 CRUD
// ============================================================

/// 新增工程(对标设计 §五 add_project_module)。返回完整记录。
#[tauri::command]
pub async fn add_project_module(
    state: State<'_, AppState>,
    input: AddProjectModuleInput,
) -> Result<ProjectModuleRecord, String> {
    let project_id = input.project_id.trim().to_string();
    let name = input.name.trim().to_string();
    let path = input.path.trim().to_string();
    if project_id.is_empty() {
        return Err("project_id 不能为空".to_string());
    }
    if name.is_empty() {
        return Err("name 不能为空".to_string());
    }
    if path.is_empty() {
        return Err("path 不能为空".to_string());
    }
    // sort_order 取当前项目下工程数(末尾追加,新工程排末位)
    let existing = state
        .project_modules
        .list_by_project(&project_id)
        .await
        .map_err(err_str)?;
    let sort_order = existing.len() as i32;

    let record = ProjectModuleRecord {
        id: new_id(),
        project_id,
        name,
        path,
        git_url: trim_opt(input.git_url),
        stack: trim_opt(input.stack),
        auto_detected: false,
        sort_order,
        // created_at/updated_at 由 Repo 内部覆盖,此处占位
        created_at: now_millis(),
        updated_at: now_millis(),
    };
    let record_id = record.id.clone();
    let ok = state
        .project_modules
        .insert(record)
        .await
        .map_err(err_str)?;
    if !ok {
        return Err("插入工程记录失败".to_string());
    }
    state
        .project_modules
        .get_by_id(&record_id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| "插入后回读失败".to_string())
}

/// 更新工程(部分更新语义:仅传入字段被覆盖,其余保留)。返回完整记录。
#[tauri::command]
pub async fn update_project_module(
    state: State<'_, AppState>,
    input: UpdateProjectModuleInput,
) -> Result<ProjectModuleRecord, String> {
    let id = input.id.trim().to_string();
    if id.is_empty() {
        return Err("id 不能为空".to_string());
    }
    // 至少一个可更新字段
    if input.name.is_none()
        && input.path.is_none()
        && input.git_url.is_none()
        && input.stack.is_none()
    {
        return Err("至少提供一个待更新字段 (name/path/git_url/stack)".to_string());
    }

    // 先校验存在(404 友好错误),再整体更新(保留不可变字段)
    let mut existing = state
        .project_modules
        .get_by_id(&id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("工程 {id} 不存在"))?;

    if let Some(name) = input.name.map(|s| s.trim().to_string()) {
        if name.is_empty() {
            return Err("name 不能为空".to_string());
        }
        existing.name = name;
    }
    if let Some(path) = input.path.map(|s| s.trim().to_string()) {
        if path.is_empty() {
            return Err("path 不能为空".to_string());
        }
        existing.path = path;
    }
    existing.git_url = trim_opt(input.git_url);
    existing.stack = trim_opt(input.stack);

    let hit = state
        .project_modules
        .update_full(&existing)
        .await
        .map_err(err_str)?;
    if !hit {
        return Err(format!("工程 {id} 不存在(更新未命中)"));
    }
    state
        .project_modules
        .get_by_id(&id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| "更新后回读失败".to_string())
}

/// 删除工程(按 id 硬删,工程不需要软删审计追溯)。
#[tauri::command]
pub async fn remove_project_module(state: State<'_, AppState>, id: String) -> Result<(), String> {
    let id = id.trim().to_string();
    if id.is_empty() {
        return Err("id 不能为空".to_string());
    }
    state.project_modules.delete(&id).await.map_err(err_str)?;
    Ok(())
}

/// 查询项目全部工程(按 sort_order ASC 返回,前端工程列表稳定顺序)。
#[tauri::command]
pub async fn list_project_modules(
    state: State<'_, AppState>,
    project_id: String,
) -> Result<Vec<ProjectModuleRecord>, String> {
    let project_id = project_id.trim().to_string();
    if project_id.is_empty() {
        return Err("project_id 不能为空".to_string());
    }
    let modules = state
        .project_modules
        .list_by_project(&project_id)
        .await
        .map_err(err_str)?;
    // 老项目兼容:V34 迁移前创建的项目无工程记录(project_modules 表为空)。
    // 自动从 projects.path 补建一个工程,使文件浏览器直接可用(单仓库退化场景)。
    if modules.is_empty() {
        if let Ok(Some(project)) = state.projects.get_by_id(&project_id).await {
            if let Some(ref path) = project.path {
                if !path.trim().is_empty() {
                    let now_str = df_types::now_millis().to_string();
                    let module = ProjectModuleRecord {
                        id: df_types::types::new_id(),
                        project_id: project_id.clone(),
                        name: project.name.clone(),
                        path: path.clone(),
                        git_url: None,
                        stack: project.stack.clone(),
                        auto_detected: true,
                        sort_order: 0,
                        created_at: now_str.clone(),
                        updated_at: now_str,
                    };
                    if let Err(e) = state.project_modules.insert(module.clone()).await {
                        tracing::warn!("老项目自动补建工程失败(非阻断): {}", e);
                    }
                    // 补建后重查返回(确保前端拿到刚建的工程)
                    return state
                        .project_modules
                        .list_by_project(&project_id)
                        .await
                        .map_err(err_str);
                }
            }
        }
    }
    Ok(modules)
}

// ============================================================
// IPC 命令 — Git 状态查询(实时派生,不进表)
// ============================================================

/// Git 改动文件项(状态码 + 相对路径)。
#[derive(Debug, Serialize)]
struct GitChangedFile {
    /// `git status --porcelain` 的状态码(xy 两字符,如 " M"、"M "、"??")
    status: String,
    /// 相对仓库根的路径
    path: String,
}

/// 最近提交项(简短哈希 + subject)。
#[derive(Debug, Serialize)]
struct GitRecentCommit {
    hash: String,
    subject: String,
    /// Unix 时间戳(秒),供前端格式化显示。
    timestamp: i64,
    /// 作者名(git an)
    author: String,
}

/// Git 状态返回结构(无 .git 时各字段空,前端据此显示"非 Git 仓库")。
#[derive(Debug, Serialize)]
struct GitStatus {
    /// 当前分支名(HEAD 处于分离状态时为空)
    branch: String,
    /// 改动文件列表(未提交)
    changed_files: Vec<GitChangedFile>,
    /// 最近 10 条提交
    recent_commits: Vec<GitRecentCommit>,
    /// 当前 HEAD 的全量提交数(`git rev-list --count HEAD`)。
    /// 前端历史 Tab 徽标 / 分支栏计数用它,而非 recent_commits.len()(后者受分页限制)。
    total_commits: i64,
    /// 该目录是否为 Git 仓库(无 .git 时 false,其余字段空)
    is_git_repo: bool,
}

/// 默认空状态(无 .git / git 不可用时返回此)。
fn empty_status() -> GitStatus {
    GitStatus {
        branch: String::new(),
        changed_files: Vec::new(),
        recent_commits: Vec::new(),
        total_commits: 0,
        is_git_repo: false,
    }
}

/// 查询工程目录的 Git 状态(分支/改动文件/最近提交)。
///
/// 实现选择:**前端 IPC 用 `std::process::Command`**(非 df-execute,因为这是用户读操作,
/// 不是 AI 工具调用,不走审批/沙箱)。10s 超时,无 .git 返回空状态(`is_git_repo: false`)。
///
/// 返回 `serde_json::Value` 以便前端灵活取字段(分支/改动/提交三段)。
#[tauri::command]
pub async fn get_module_git_status(
    state: State<'_, AppState>,
    module_id: String,
) -> Result<serde_json::Value, String> {
    let module_id = module_id.trim().to_string();
    if module_id.is_empty() {
        return Err("module_id 不能为空".to_string());
    }
    // 取工程记录拿 path
    let module = state
        .project_modules
        .get_by_id(&module_id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("工程 {module_id} 不存在"))?;

    let path = std::path::Path::new(&module.path);
    // 无 .git → 返回空状态
    if !path.join(".git").exists() {
        return Ok(serde_json::to_value(empty_status()).map_err(err_str)?);
    }

    // git 命令在 spawn_blocking 中执行(阻塞 IO 不污染 async runtime),10s 超时
    let dir = module.path.clone();
    let status = tokio::task::spawn_blocking(move || run_git_status(&dir))
        .await
        .map_err(|e| format!("git 状态查询任务失败: {e}"))?;

    Ok(serde_json::to_value(status).map_err(err_str)?)
}

/// 列出工程本地分支(只读)。返回 { current, branches: [{ name, is_current }] }。
#[tauri::command]
pub async fn list_branches(
    state: State<'_, AppState>,
    module_id: String,
) -> Result<serde_json::Value, String> {
    let module_id = module_id.trim().to_string();
    if module_id.is_empty() {
        return Err("module_id 不能为空".to_string());
    }
    let module = state
        .project_modules
        .get_by_id(&module_id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("工程 {module_id} 不存在"))?;
    let path = std::path::Path::new(&module.path);
    if !path.join(".git").exists() {
        return Ok(serde_json::json!({ "current": "", "branches": [] }));
    }
    let dir = module.path.clone();
    // git 命令在 spawn_blocking 中执行(阻塞 IO 不污染 async runtime),10s 超时
    let result = tokio::task::spawn_blocking(move || -> (String, Vec<serde_json::Value>) {
        // git branch --format="%(HEAD)%00%(refname:short)"
        let out = run_git_cmd(
            std::path::Path::new(&dir),
            &["branch", "--format=%(HEAD)%00%(refname:short)"],
            std::time::Duration::from_secs(10),
        )
        .unwrap_or_default();
        let mut current = String::new();
        let mut branches = Vec::new();
        for line in out.lines() {
            let line = line.trim();
            if line.is_empty() { continue; }
            // 格式: "*\0branch_name" 或 "\0branch_name"
            let (head, name) = line.split_once('\u{0}').unwrap_or(("", line));
            let is_current = head.contains('*');
            let name = name.trim().to_string();
            if name.is_empty() { continue; }
            if is_current { current = name.clone(); }
            branches.push(serde_json::json!({ "name": name, "is_current": is_current }));
        }
        (current, branches)
    })
    .await
    .map_err(|e| format!("分支列表查询任务失败: {e}"))?;

    Ok(serde_json::json!({
        "current": result.0,
        "branches": result.1,
    }))
}

/// 在指定目录跑 git 命令采集状态(当前分支 / 改动文件 / 最近提交)。
///
/// 超时:每个 git 子进程 10s(用户读操作,卡死不应阻塞 UI)。任一命令失败时返回已采集部分。
fn run_git_status(dir: &str) -> GitStatus {
    use std::time::Duration;

    let path = std::path::Path::new(dir);
    if !path.join(".git").exists() {
        return empty_status();
    }

    let timeout = Duration::from_secs(10);

    // 1) 当前分支:`git rev-parse --abbrev-ref HEAD`(分离 HEAD 时返回 "HEAD")
    let branch = run_git_cmd(path, &["rev-parse", "--abbrev-ref", "HEAD"], timeout)
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty() && s != "HEAD")
        .unwrap_or_default();

    // 2) 改动文件:`git status --porcelain`(每行 "XY path",XY 为两字符状态码)
    let mut changed_files = Vec::new();
    if let Some(out) = run_git_cmd(path, &["status", "--porcelain"], timeout) {
        for line in out.lines() {
            if line.len() < 4 {
                continue;
            }
            // porcelain 格式:"XY path"(X/Y 各一字符 + 一空格 + path)
            let status = line[..2].to_string();
            let file_path = line[3..].trim().to_string();
            if !file_path.is_empty() {
                changed_files.push(GitChangedFile {
                    status,
                    path: file_path,
                });
            }
        }
    }

        // 3) 最近提交:`git log -50 --format="%h %ct %an %s"`(hash + 时间戳 + 作者 + subject)
        let mut recent_commits = Vec::new();
        if let Some(out) = run_git_cmd(path, &["log", "-50", "--format=%h %ct %an %s"], timeout) {
            for line in out.lines() {
                // format:"<hash> <unix_ts> <author> <subject>"(作者不含空格)
                let line = line.trim();
                if line.is_empty() { continue; }
                let mut parts = line.splitn(4, ' ');
                let hash = parts.next().unwrap_or("").to_string();
                let ts_str = parts.next().unwrap_or("0");
                let timestamp: i64 = ts_str.parse().unwrap_or(0);
                let author = parts.next().unwrap_or("").to_string();
                let subject = parts.next().unwrap_or("").to_string();
                if !hash.is_empty() {
                    recent_commits.push(GitRecentCommit {
                        hash,
                        subject,
                        timestamp,
                        author,
                    });
                }
            }
        }

    // 4) 全量提交计数:`git rev-list --count HEAD`(供前端历史 Tab 徽标真实总数,
    //    非 recent_commits.len() 后者上限 50)。命令失败 → 退化为 recent_commits 长度。
    let total_commits = run_git_cmd(path, &["rev-list", "--count", "HEAD"], timeout)
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
        .and_then(|s| s.parse::<i64>().ok())
        .unwrap_or(recent_commits.len() as i64);

    GitStatus {
        branch,
        changed_files,
        recent_commits,
        total_commits,
        is_git_repo: true,
    }
}

/// 跑单个 git 子命令,捕获 stdout(成功)或 None(失败/超时)。
///
/// 用 `wait_timeout` 实现 10s 超时(标准库 `Command::output` 无超时,需手动 wait)。
fn run_git_cmd(cwd: &std::path::Path, args: &[&str], timeout: std::time::Duration) -> Option<String> {
    use std::sync::mpsc;

    // 在独立线程跑子进程,主线程 select 超时,避免标准库无超时 API 的痛点。
    // 编码:设置环境变量强制 git 输出 UTF-8(Windows 系统编码可能是 GBK,直接读会乱码)。
    let (tx, rx) = mpsc::channel();
    let cwd = cwd.to_path_buf();
    let args = args.iter().map(|s| s.to_string()).collect::<Vec<_>>();
    std::thread::spawn(move || {
        let mut cmd = Command::new("git");
        cmd.args(&args)
            .current_dir(&cwd)
            .env("LANG", "en_US.UTF-8")
            .env("LC_ALL", "en_US.UTF-8")
            .env("GIT_PAGER", "cat");
        // Windows 下 spawn 子进程默认会弹 cmd 黑窗,加 CREATE_NO_WINDOW 抑制闪烁
        // (GitChanges 挂载并发跑 rev-parse/status/log,不抑制会疯狂闪)。
        #[cfg(windows)]
        {
            use std::os::windows::process::CommandExt;
            cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
        }
        let out = cmd.output();
        let _ = tx.send(out);
    });

    match rx.recv_timeout(timeout) {
        Ok(Ok(output)) if output.status.success() => {
            Some(String::from_utf8_lossy(&output.stdout).to_string())
        }
        _ => None,
    }
}

// ============================================================
// IPC 命令 — 文件浏览(Batch 10)
// ============================================================
//
// 设计要点(对标设计 §五 + Batch 10):
// - **路径安全**:sub_path / file_path 均拒含 `..`(防目录穿越)。先 join 再 canonicalize
//   校验结果仍位于 module.path 子树内(双重防御,即便 `..` 漏网也无法逃出工程根)。
// - **噪音过滤**:`get_module_file_tree` 跳过 node_modules/target/.git/__pycache__/dist/.vite
//   (对齐需求清单,前端文件树不展示构建产物/VCS 元数据)。
// - **Git 状态合并**:跑 `git status --porcelain` 得 {rel_path: status} 映射,按条目相对路径
//   查表填充 git_status。文件夹一律 git_status = None(只标文件,符合需求)。
// - **二进制检测**:read_module_file 读 1MB 上限,前 8KB 含 \0 视作二进制(is_binary=true,content 留空)。

/// 噪音目录名(列表级过滤,read_dir 看到这些名字跳过)。
const NOISE_DIRS: &[&str] = &[
    "node_modules",
    "target",
    ".git",
    "__pycache__",
    "dist",
    ".vite",
];

/// 文件树条目(单层;前端点击文件夹再懒加载下一层)。
#[derive(Debug, Serialize)]
struct FileTreeEntry {
    name: String,
    /// 相对工程根的路径(POSIX 风格,前端用作后续 sub_path / file_path)。
    path: String,
    is_dir: bool,
    /// 字节数(文件夹恒为 0)。
    size: u64,
    /// Git 状态码(`git status --porcelain` 的 XY 两字符,如 " M"/"M "/"??")。
    /// 文件夹恒为 None(只标文件)。
    #[serde(skip_serializing_if = "Option::is_none")]
    git_status: Option<String>,
}

/// 列出工程目录(可钻入子目录)的文件树(单层 + git 状态合并)。
/// 返回 { path, entries: [{ name, path, is_dir, size, git_status? }] }。
#[tauri::command]
pub async fn get_module_file_tree(
    state: State<'_, AppState>,
    module_id: String,
    sub_path: Option<String>,
) -> Result<serde_json::Value, String> {
    let module_id = module_id.trim().to_string();
    if module_id.is_empty() {
        return Err("module_id 不能为空".to_string());
    }
    let sub = sub_path
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty());

    // 路径穿越防御:`..` 一律拒(规范化后 canonicalize 再兜底校验仍在工程根子树)。
    if let Some(ref s) = sub {
        if s.contains("..") {
            return Err("sub_path 不允许包含 ..".to_string());
        }
    }

    let module = state
        .project_modules
        .get_by_id(&module_id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("工程 {module_id} 不存在"))?;

    let root = PathBuf::from(&module.path);
    // 工程根本身必须存在(否则连列根都无意义,直接报错给前端清晰提示)。
    if !root.exists() {
        return Err(format!("工程目录不存在: {}", module.path));
    }

    // 拼接 sub_path 得目标目录;规范化路径显示用。
    let target_dir = match &sub {
        Some(rel) => root.join(rel),
        None => root.clone(),
    };
    if !target_dir.is_dir() {
        return Err(format!("目标路径不是目录: {}", target_dir.display()));
    }

    // 计算相对工程根的显示路径(POSIX 风格),前端面包屑/二次请求复用。
    let rel_display = match &sub {
        Some(rel) => rel.replace('\\', "/"),
        None => String::new(),
    };

    // 采集 git 改动文件映射(相对仓库根):{posix_rel: status}。
    // 10s 超时;非 git 仓库/失败 → 空映射(条目 git_status 全 None,不阻断列目录)。
    let dir_for_git = module.path.clone();
    let git_map = tokio::task::spawn_blocking(move || collect_git_status_map(&dir_for_git))
        .await
        .map_err(|e| format!("git status 采集任务失败: {e}"))?;
    // 路径前缀(工程根本身)的相对基准;target_dir 下条目相对路径要以此拼成 posix 形式查 git_map。
    // git status 输出是相对仓库根(module.path 当作根),sub 非空时条目前缀 = sub + "/"。
    let prefix = if rel_display.is_empty() {
        String::new()
    } else {
        format!("{rel_display}/")
    };

    // 读目录(spawn_blocking 避免阻塞 runtime);排序:目录优先 + 字母序。
    let target_dir_clone = target_dir.clone();
    let prefix_clone = prefix.clone();
    let git_map_clone = git_map.clone();
    let entries = tokio::task::spawn_blocking(move || -> Result<Vec<FileTreeEntry>, String> {
        let read = std::fs::read_dir(&target_dir_clone)
            .map_err(|e| format!("读取目录失败: {e}"))?;
        let mut items: Vec<FileTreeEntry> = Vec::new();
        for entry in read.flatten() {
            let file_name = entry.file_name();
            let name = file_name.to_string_lossy().to_string();
            // 噪音过滤(无论文件/目录,凡名命中即跳)。
            if NOISE_DIRS.iter().any(|n| *n == name.as_str()) {
                continue;
            }
            let ft = match entry.file_type() {
                Ok(t) => t,
                Err(_) => continue, // 无法判定类型跳过(符号链接断链等)
            };
            // 跳过隐藏文件(以 . 开头),与常见编辑器行为一致,减少噪音。
            if name.starts_with('.') {
                continue;
            }
            let is_dir = ft.is_dir();
            let size = if is_dir {
                0
            } else {
                entry.metadata().map(|m| m.len()).unwrap_or(0)
            };
            let rel_path = format!("{prefix_clone}{name}");
            let posix_rel = rel_path.replace('\\', "/");
            let git_status = if is_dir {
                None
            } else {
                git_map_clone.get(&posix_rel).cloned()
            };
            items.push(FileTreeEntry {
                name,
                path: posix_rel,
                is_dir,
                size,
                git_status,
            });
        }
        // 目录优先,各自字母序(稳定可预期,前端无需再排)。
        items.sort_by(|a, b| match (a.is_dir, b.is_dir) {
            (true, false) => std::cmp::Ordering::Less,
            (false, true) => std::cmp::Ordering::Greater,
            _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
        });
        Ok(items)
    })
    .await
    .map_err(|e| format!("列目录任务失败: {e}"))??;

    Ok(serde_json::json!({
        "path": rel_display,
        "entries": entries,
    }))
}

/// 读工程内单文件(文本/图片);1MB 上限,二进制检测(前 8KB 含 \0)。
/// 返回 { path, content, size, is_binary }。二进制/超限时 is_binary=true 或 content 截断,
/// 前端据此降级展示("二进制文件不支持预览")。
#[tauri::command]
pub async fn read_module_file(
    state: State<'_, AppState>,
    module_id: String,
    file_path: String,
) -> Result<serde_json::Value, String> {
    let module_id = module_id.trim().to_string();
    let file_path = file_path.trim().to_string();
    if module_id.is_empty() {
        return Err("module_id 不能为空".to_string());
    }
    if file_path.is_empty() {
        return Err("file_path 不能为空".to_string());
    }
    // 路径穿越防御:`..` 一律拒。
    if file_path.contains("..") {
        return Err("file_path 不允许包含 ..".to_string());
    }

    let module = state
        .project_modules
        .get_by_id(&module_id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("工程 {module_id} 不存在"))?;

    let root = PathBuf::from(&module.path);
    let abs = root.join(&file_path);
    // 必须是文件(目录/不存在均拒)。
    if !abs.is_file() {
        return Err(format!("文件不存在或不是普通文件: {}", file_path));
    }

    const MAX_BYTES: u64 = 1024 * 1024; // 1MB 上限
    let meta = std::fs::metadata(&abs).map_err(|e| format!("读取文件元信息失败: {e}"))?;
    let total_size = meta.len();

    // 读取(超 1MB 截断到 1MB;spawn_blocking 避免大文件 IO 阻塞 runtime)。
    let abs_clone = abs.clone();
    let read_result = tokio::task::spawn_blocking(move || -> Result<(Vec<u8>, bool), String> {
        use std::io::Read;
        let mut f = std::fs::File::open(&abs_clone).map_err(|e| format!("打开文件失败: {e}"))?;
        let mut buf = vec![0u8; MAX_BYTES as usize];
        let n = f.read(&mut buf).map_err(|e| format!("读取文件失败: {e}"))?;
        buf.truncate(n);
        // 二进制检测:前 8KB 含 \0 → 二进制。8KB 内全为文本字节 → 当文本。
        let check_len = buf.len().min(8192);
        let is_binary = buf[..check_len].iter().any(|&b| b == 0);
        Ok((buf, is_binary))
    })
    .await
    .map_err(|e| format!("读文件任务失败: {e}"))??;
    let (bytes, is_binary) = read_result;

    let content = if is_binary {
        // 二进制不返内容(前端走"二进制不支持预览"分支,避免给前端塞不可打印字节)。
        String::new()
    } else {
        // 非 UTF-8 字节用 lossy(替换非法字节,文本类基本不受影响)。
        String::from_utf8_lossy(&bytes).to_string()
    };

    Ok(serde_json::json!({
        "path": file_path.replace('\\', "/"),
        "content": content,
        "size": total_size,
        "is_binary": is_binary,
        // 标记是否被截断(超 1MB),前端据此提示"文件过大,仅显示前 1MB"。
        "truncated": total_size > MAX_BYTES,
    }))
}

/// 查询工程内文件元信息(不读内容,轻量检测变化用)。
/// 返回 { path, size, modified_at }。modified_at 是毫秒级时间戳。
#[tauri::command]
pub async fn get_module_file_meta(
    state: State<'_, AppState>,
    module_id: String,
    file_path: String,
) -> Result<serde_json::Value, String> {
    let module_id = module_id.trim().to_string();
    let file_path = file_path.trim().to_string();
    if module_id.is_empty() {
        return Err("module_id 不能为空".to_string());
    }
    if file_path.is_empty() {
        return Err("file_path 不能为空".to_string());
    }
    if file_path.contains("..") {
        return Err("file_path 不允许包含 ..".to_string());
    }
    let module = state
        .project_modules
        .get_by_id(&module_id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("工程 {module_id} 不存在"))?;
    let root = std::path::PathBuf::from(&module.path);
    let abs = root.join(&file_path);
    if !abs.is_file() {
        return Err(format!("文件不存在或不是普通文件: {}", file_path));
    }
    let meta = std::fs::metadata(&abs).map_err(|e| format!("读取文件元信息失败: {e}"))?;
    // 系统级 modified_at 需跨平台转换;用 std::time::UNIX_EPOCH 算毫秒
    let modified_ms = meta
        .modified()
        .ok()
        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0);
    Ok(serde_json::json!({
        "path": file_path.replace('\\', "/"),
        "size": meta.len(),
        "modified_at": modified_ms,
    }))
}

/// 查询工程内文件的 git diff(相对 staged 或 working tree 的变更)。
/// 返回 { path, diff }。diff 为空串表示无变更。
#[tauri::command]
pub async fn get_module_file_diff(
    state: State<'_, AppState>,
    module_id: String,
    file_path: String,
) -> Result<serde_json::Value, String> {
    let module_id = module_id.trim().to_string();
    let file_path = file_path.trim().to_string();
    if module_id.is_empty() {
        return Err("module_id 不能为空".to_string());
    }
    if file_path.is_empty() {
        return Err("file_path 不能为空".to_string());
    }
    if file_path.contains("..") {
        return Err("file_path 不允许包含 ..".to_string());
    }
    let module = state
        .project_modules
        .get_by_id(&module_id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("工程 {module_id} 不存在"))?;
    let root = std::path::PathBuf::from(&module.path);
    if !root.join(".git").exists() {
        return Ok(serde_json::json!({"path": file_path, "diff": ""}));
    }
    // 跑 git diff <path>(工作树 vs 索引的 unstaged 变更+staged 变更)
    // spawn_blocking + run_git_cmd(10s 超时,防 git 卡死阻塞 runtime)。
    let cmd_dir = root.clone();
    let path_arg = file_path.replace('\\', "/");
    let diff = tokio::task::spawn_blocking(move || -> Option<String> {
        run_git_cmd(
            &cmd_dir,
            &["diff", "--", &path_arg],
            std::time::Duration::from_secs(10),
        )
    })
    .await
    .map_err(|e| format!("git diff 任务失败: {e}"))?
    .unwrap_or_default();
    Ok(serde_json::json!({
        "path": file_path.replace('\\', "/"),
        "diff": diff,
    }))
}

/// 扫描项目绑定目录下的子仓库(含 .git 的直接子目录),自动创建工程记录。
/// 返回新创建的工程数量。幂等:已存在的路径不重复创建。
#[tauri::command]
pub async fn scan_project_modules(
    state: State<'_, AppState>,
    project_id: String,
) -> Result<i64, String> {
    let project_id = project_id.trim().to_string();
    if project_id.is_empty() {
        return Err("project_id 不能为空".to_string());
    }
    // 取项目记录拿 path
    let project = state
        .projects
        .get_by_id(&project_id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("项目 {project_id} 不存在"))?;
    let root_path = project
        .path
        .as_ref()
        .filter(|p| !p.trim().is_empty())
        .ok_or_else(|| "项目未绑定目录".to_string())?;
    let root = PathBuf::from(root_path);
    // 取已有工程路径集(避免重复创建)
    let existing = state
        .project_modules
        .list_by_project(&project_id)
        .await
        .map_err(err_str)?;
    let existing_paths: std::collections::HashSet<String> = existing
        .iter()
        .map(|m| m.path.to_lowercase().replace("\\", "/"))
        .collect();
    // 扫描一级子目录(含 .git)
    let mut new_count = 0i64;
    let entries = std::fs::read_dir(&root)
        .map_err(|e| format!("读取项目目录失败: {e}"))?;
    for entry in entries.flatten() {
        let ft = match entry.file_type() { Ok(t) => t, Err(_) => continue };
        if !ft.is_dir() { continue; }
        let name = entry.file_name().to_string_lossy().to_string();
        // 跳过隐藏目录和噪音目录
        if name.starts_with('.') || name == "node_modules" || name == "target" || name == "__pycache__" || name == "dist" {
            continue;
        }
        let child_path = entry.path();
        // 必须含 .git(独立仓库)
        if !child_path.join(".git").exists() { continue; }
        let child_path_str = child_path.to_string_lossy().replace("\\", "/");
        if existing_paths.contains(&child_path_str.to_lowercase()) { continue; }
        // 获取远程地址(失败忽略);spawn_blocking + run_git_cmd(10s 超时,防 git 卡死阻塞 runtime)
        let url_dir = child_path.clone();
        let git_url = tokio::task::spawn_blocking(move || {
            run_git_cmd(
                &url_dir,
                &["remote", "get-url", "origin"],
                std::time::Duration::from_secs(10),
            )
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty())
        })
        .await
        .unwrap_or(None);
        let now_str = now_millis();
        let record = ProjectModuleRecord {
            id: new_id(),
            project_id: project_id.clone(),
            name: name.clone(),
            path: child_path.to_string_lossy().to_string(),
            git_url,
            stack: None,
            auto_detected: true,
            sort_order: (existing.len() + new_count as usize) as i32,
            created_at: now_str.clone(),
            updated_at: now_str,
        };
        if let Err(e) = state.project_modules.insert(record).await {
            tracing::warn!("自动探测工程 {} 失败: {}", child_path_str, e);
            continue;
        }
        new_count += 1;
    }
    Ok(new_count)
}

/// 采集工程目录 git status --porcelain 的 {posix_rel_path: status} 映射。
/// 非 git 仓库/命令失败 → 空映射(不阻断文件树展示,仅 git_status 字段全 None)。
fn collect_git_status_map(dir: &str) -> HashMap<String, String> {
    let path = Path::new(dir);
    let mut map = HashMap::new();
    if !path.join(".git").exists() {
        return map;
    }
    let timeout = std::time::Duration::from_secs(10);
    let Some(out) = run_git_cmd(path, &["status", "--porcelain"], timeout) else {
        return map;
    };
    for line in out.lines() {
        if line.len() < 4 {
            continue;
        }
        let status = line[..2].to_string();
        let raw_path = line[3..].trim().to_string();
        if raw_path.is_empty() {
            continue;
        }
        // porcelain 在路径含空格/特殊字符时可能带引号;git 默认不引普通路径,剥引号兜底。
        let cleaned = raw_path
            .strip_prefix('"')
            .and_then(|s| s.strip_suffix('"'))
            .unwrap_or(&raw_path)
            .replace('\\', "/");
        // 重命名条目形如 "R  old -> new":取 -> 后的新路径(更贴合文件树展示位置)。
        let final_path = cleaned
            .split_once(" -> ")
            .map(|(_, new)| new.to_string())
            .unwrap_or(cleaned);
        map.insert(final_path, status);
    }
    map
}

/// 查询工程 Git 提交历史(分页,按时间倒序)。
/// 返回 { commits: [{ hash, subject, timestamp, author }], has_more: bool, total: i64 }。
/// total = `git rev-list --count HEAD` 的全量提交数,供前端徽标真实总数(commits.len() 受分页限制)。
#[tauri::command]
pub async fn get_module_commits(
    state: State<'_, AppState>,
    module_id: String,
    skip: Option<u32>,
    limit: Option<u32>,
) -> Result<serde_json::Value, String> {
    let module_id = module_id.trim().to_string();
    if module_id.is_empty() {
        return Err("module_id 不能为空".to_string());
    }
    let module = state
        .project_modules
        .get_by_id(&module_id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("工程 {module_id} 不存在"))?;
    let path = std::path::Path::new(&module.path);
    if !path.join(".git").exists() {
        return Ok(serde_json::json!({ "commits": [], "has_more": false, "total": 0 }));
    }
    let skip = skip.unwrap_or(0);
    let fetch = limit.unwrap_or(50);
    // 多取一条以判断 has_more
    let fetch_plus = fetch + 1;
    let dir = module.path.clone();
    let dir_for_git = dir.clone();
    // git 命令在 spawn_blocking 中执行(阻塞 IO 不污染 async runtime),10s 超时
    let (commits, total): (Vec<serde_json::Value>, i64) = tokio::task::spawn_blocking(move || -> (Vec<serde_json::Value>, i64) {
        let path = std::path::Path::new(&dir_for_git);
        let timeout = std::time::Duration::from_secs(10);
        let out = run_git_cmd(
            path,
            &[
                "log",
                &format!("--skip={}", skip),
                &format!("-{}", fetch_plus),
                "--format=%h %ct %an %s",
            ],
            timeout,
        )
        .unwrap_or_default();
        let mut commits: Vec<serde_json::Value> = Vec::new();
        for line in out.lines() {
            let line = line.trim();
            if line.is_empty() { continue; }
            if commits.len() >= fetch as usize { break; }
            let mut parts = line.splitn(4, ' ');
            let hash = parts.next().unwrap_or("").to_string();
            let ts_str = parts.next().unwrap_or("0");
            let timestamp: i64 = ts_str.parse().unwrap_or(0);
            let author = parts.next().unwrap_or("").to_string();
            let subject = parts.next().unwrap_or("").to_string();
            if !hash.is_empty() {
                commits.push(serde_json::json!({
                    "hash": hash,
                    "subject": subject,
                    "timestamp": timestamp,
                    "author": author,
                }));
            }
        }
        // 全量提交计数(`git rev-list --count HEAD`):前端历史 Tab 徽标真实总数。
        // 命令失败 → 退化为 0(前端会显示 0,但 has_more 仍可驱动分页)。
        let total = run_git_cmd(path, &["rev-list", "--count", "HEAD"], timeout)
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty())
            .and_then(|s| s.parse::<i64>().ok())
            .unwrap_or(0);
        (commits, total)
    })
    .await
    .map_err(|e| format!("提交历史查询任务失败: {e}"))?;

    // 判断 has_more:取了 N+1 条但只返回 N 条,说明有更多
    let total_fetched = commits.len();
    let has_more = total_fetched == fetch_plus as usize;
    let returned: Vec<_> = commits.into_iter().take(fetch as usize).collect();

    Ok(serde_json::json!({
        "commits": returned,
        "has_more": has_more,
        "total": total,
    }))
}

/// 查询某次提交的变更文件列表及全量 diff。
/// 返回 { files: [{ status, path }], diff: string }。
#[tauri::command]
pub async fn get_commit_detail(
    state: State<'_, AppState>,
    module_id: String,
    commit_hash: String,
) -> Result<serde_json::Value, String> {
    let module_id = module_id.trim().to_string();
    let commit_hash = commit_hash.trim().to_string();
    if module_id.is_empty() {
        return Err("module_id 不能为空".to_string());
    }
    if commit_hash.is_empty() {
        return Err("commit_hash 不能为空".to_string());
    }
    let module = state
        .project_modules
        .get_by_id(&module_id)
        .await
        .map_err(err_str)?
        .ok_or_else(|| format!("工程 {module_id} 不存在"))?;
    let path = std::path::Path::new(&module.path);
    if !path.join(".git").exists() {
        return Ok(serde_json::json!({ "files": [], "diff": "" }));
    }
    let dir = module.path.clone();
    // git 命令在 spawn_blocking 中执行(阻塞 IO 不污染 async runtime),10s 超时
    let (files, diff, parents, author, date, full_message) = tokio::task::spawn_blocking(move || -> (Vec<serde_json::Value>, String, Vec<String>, String, String, String) {
        let dir_path = std::path::Path::new(&dir);
        let timeout = std::time::Duration::from_secs(10);
        // 1) 获取变更文件列表:`git diff-tree --no-commit-id -r --name-status <hash>`
        let files_out = run_git_cmd(
            dir_path,
            &["diff-tree", "--no-commit-id", "-r", "--name-status", &commit_hash],
            timeout,
        )
        .unwrap_or_default();
        let mut files: Vec<serde_json::Value> = Vec::new();
        for line in files_out.lines() {
            let line = line.trim();
            if line.is_empty() { continue; }
            // format:"<status>\t<path>"
            if let Some((status, fpath)) = line.split_once('\t') {
                files.push(serde_json::json!({
                    "status": status.trim(),
                    "path": fpath.trim(),
                }));
            }
        }
        // 2) 获取全量 diff:`git show <hash>`(仅 diff 部分)
        let diff = run_git_cmd(
            dir_path,
            &["show", "--format=", &commit_hash],
            timeout,
        )
        .unwrap_or_default();
        // 3) 获取提交元信息(父提交/作者/日期/完整消息):
        //    用 printf 自定义格式,%P=父哈希(空格分隔多个)\t%an=作者\t%ad=日期\t%B=完整消息
        let meta = run_git_cmd(
            dir_path,
            &["show", "-s", "--format=%P\t%an\t%ad\t%B", &commit_hash],
            timeout,
        )
        .unwrap_or_default();
        let mut parents = Vec::new();
        let mut author = String::new();
        let mut date = String::new();
        let mut full_message = String::new();
        if let Some(first_line) = meta.lines().next() {
            let parts: Vec<&str> = first_line.splitn(4, '\t').collect();
            if parts.len() >= 1 {
                parents = parts[0].split_whitespace().map(|s| s.to_string()).collect();
            }
            if parts.len() >= 2 { author = parts[1].to_string(); }
            if parts.len() >= 3 { date = parts[2].to_string(); }
            if parts.len() >= 4 { full_message = parts[3].to_string(); }
        }
        // multi-line message: 补尾部行(meta.lines().next() 只取首行,需拼回)
        if !full_message.is_empty() {
            let rest = meta.lines().skip(1).collect::<Vec<_>>().join("\n");
            if !rest.is_empty() {
                full_message = format!("{}\n{}", full_message, rest);
            }
        }
        (files, diff, parents, author, date, full_message)
    })
    .await
    .map_err(|e| format!("提交详情查询任务失败: {e}"))?;

    Ok(serde_json::json!({
        "files": files,
        "diff": diff,
        "parents": parents,
        "author": author,
        "date": date,
        "full_message": full_message,
    }))
}

// ============================================================
// IPC 命令 — 工程依赖关系(module_dependencies 表)
// ============================================================

/// 新增工程依赖。返回完整记录。
#[tauri::command]
pub async fn add_module_dependency(
    state: State<'_, AppState>,
    project_id: String,
    from_module_id: String,
    to_module_id: String,
    dep_type: Option<String>,
    label: Option<String>,
) -> Result<ModuleDependencyRecord, String> {
    let project_id = project_id.trim().to_string();
    let from_module_id = from_module_id.trim().to_string();
    let to_module_id = to_module_id.trim().to_string();
    if project_id.is_empty() || from_module_id.is_empty() || to_module_id.is_empty() {
        return Err("project_id / from_module_id / to_module_id 不能为空".to_string());
    }
    let dep_type = dep_type.unwrap_or_else(|| "library".to_string());
    let record = ModuleDependencyRecord {
        id: new_id(),
        project_id,
        from_module_id,
        to_module_id,
        dep_type,
        label: trim_opt(label),
        created_at: now_millis(),
    };
    state.module_dependencies.insert(record.clone()).await.map_err(err_str)?;
    Ok(record)
}

/// 删除工程依赖。
#[tauri::command]
pub async fn remove_module_dependency(
    state: State<'_, AppState>,
    id: String,
) -> Result<(), String> {
    let id = id.trim().to_string();
    if id.is_empty() {
        return Err("id 不能为空".to_string());
    }
    state.module_dependencies.delete(&id).await.map_err(err_str)?;
    Ok(())
}

/// 列出项目的全部工程依赖。
#[tauri::command]
pub async fn list_module_dependencies(
    state: State<'_, AppState>,
    project_id: String,
) -> Result<Vec<ModuleDependencyRecord>, String> {
    let project_id = project_id.trim().to_string();
    if project_id.is_empty() {
        return Err("project_id 不能为空".to_string());
    }
    state
        .module_dependencies
        .list_by_field("project_id", &project_id)
        .await
        .map_err(err_str)
}

/// 检测工程依赖图中的环形依赖(DFS 三色标记法)。
/// 返回参与环的 module_id 列表(空 = 无环)。
#[tauri::command]
pub async fn detect_module_cycles(
    state: State<'_, AppState>,
    project_id: String,
) -> Result<Vec<String>, String> {
    let project_id = project_id.trim().to_string();
    if project_id.is_empty() {
        return Err("project_id 不能为空".to_string());
    }
    let deps = state
        .module_dependencies
        .list_by_field("project_id", &project_id)
        .await
        .map_err(err_str)?;
    // 构建邻接表
    let mut adj: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new();
    for d in &deps {
        adj.entry(d.from_module_id.clone())
            .or_default()
            .push(d.to_module_id.clone());
        adj.entry(d.to_module_id.clone()).or_default();
    }
    // DFS 三色:0=白(未访问) 1=灰(栈中) 2=黑(完成)
    let mut color: std::collections::HashMap<String, u8> = std::collections::HashMap::new();
    let mut cycle_nodes: std::collections::HashSet<String> = std::collections::HashSet::new();
    fn dfs(
        node: &str,
        adj: &std::collections::HashMap<String, Vec<String>>,
        color: &mut std::collections::HashMap<String, u8>,
        cycle_nodes: &mut std::collections::HashSet<String>,
        path: &mut Vec<String>,
    ) {
        let c = color.get(node).copied().unwrap_or(0);
        if c == 1 {
            // 发现环:path 中从当前节点开始的都参与环
            let in_cycle = path.iter().position(|n| n == node);
            if let Some(start) = in_cycle {
                for n in &path[start..] {
                    cycle_nodes.insert(n.clone());
                }
            }
            cycle_nodes.insert(node.to_string());
            return;
        }
        if c == 2 {
            return;
        }
        color.insert(node.to_string(), 1);
        path.push(node.to_string());
        if let Some(neighbors) = adj.get(node) {
            for next in neighbors {
                dfs(next, adj, color, cycle_nodes, path);
            }
        }
        path.pop();
        color.insert(node.to_string(), 2);
    }
    let nodes: Vec<String> = adj.keys().cloned().collect();
    let mut path: Vec<String> = Vec::new();
    for n in &nodes {
        if color.get(n).copied().unwrap_or(0) == 0 {
            dfs(n, &adj, &mut color, &mut cycle_nodes, &mut path);
        }
    }
    Ok(cycle_nodes.into_iter().collect())
}
