//! Agent 友好基础设施生成（接入包 v2）。
//!
//! 卡 A：扫描项目框架层技术栈，推断外部契约路径，为后续生成三件套做准备。
//! 与 project_scan.rs（工具层）互补：本模块专注框架/ORM/API 层识别。

use serde::{Deserialize, Serialize};
use std::path::Path;

// ── 公共数据结构 ──────────────────────────────────────────────────────────────

/// 推断出的外部契约条目（由用户确认后传给 generate_agent_infra_files）。
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DetectedContract {
    /// 人读标签，如"数据库 schema（Drizzle）"
    pub label: String,
    /// 推断的路径列表（可含 glob，如 "src/routes/**"）
    pub paths: Vec<String>,
    /// 变更时的文档同步提示
    pub doc: String,
    /// "high"：路径文件在磁盘上存在；"medium"：仅从依赖推断；"low"：弱信号
    pub confidence: String,
}

/// scan_agent_infra_stack 的完整返回值。
#[derive(Debug, Serialize, Deserialize)]
pub struct AgentInfraStack {
    /// 识别到的契约列表（已去重，按 label 排序）
    pub detected_contracts: Vec<DetectedContract>,
    /// AGENTS.md 是否已含"## 文档防腐元规则"段落
    pub meta_rules_present: bool,
    /// docs/ai-context/project-context.yaml 是否已存在
    pub context_yaml_exists: bool,
}

// ── package.json 解析 ─────────────────────────────────────────────────────────

/// 从指定 package.json 文件提取 dependencies + devDependencies 的所有 key。
fn pkg_json_dep_keys_from_file(path: &Path) -> Vec<String> {
    let content = match std::fs::read_to_string(path) {
        Ok(c) => c,
        Err(_) => return vec![],
    };
    let v: serde_json::Value = match serde_json::from_str(&content) {
        Ok(v) => v,
        Err(_) => return vec![],
    };
    let mut keys = Vec::new();
    for section in &["dependencies", "devDependencies"] {
        if let Some(obj) = v.get(section).and_then(|d| d.as_object()) {
            keys.extend(obj.keys().cloned());
        }
    }
    keys
}

/// 收集根目录 + monorepo 子包（apps/*、packages/*）的所有依赖 key。
fn collect_all_pkg_deps(root: &Path) -> Vec<String> {
    let mut keys = pkg_json_dep_keys_from_file(&root.join("package.json"));
    if root.join("pnpm-workspace.yaml").exists() {
        for dir in &["apps", "packages"] {
            if let Ok(entries) = std::fs::read_dir(root.join(dir)) {
                for entry in entries.flatten() {
                    if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
                        let pkg = entry.path().join("package.json");
                        if pkg.exists() {
                            keys.extend(pkg_json_dep_keys_from_file(&pkg));
                        }
                    }
                }
            }
        }
        keys.sort();
        keys.dedup();
    }
    keys
}

/// monorepo 中在 apps/* 子包里找第一个含 sub_path 的目录，返回相对根的路径。
fn find_in_apps(root: &Path, sub_path: &str) -> Option<String> {
    let apps_dir = root.join("apps");
    if !apps_dir.is_dir() {
        return None;
    }
    let mut names: Vec<_> = std::fs::read_dir(&apps_dir)
        .ok()?
        .flatten()
        .filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
        .map(|e| e.file_name().to_string_lossy().to_string())
        .collect();
    names.sort();
    for name in names {
        let rel = format!("apps/{name}/{sub_path}");
        if root.join(&rel).exists() {
            return Some(rel);
        }
    }
    None
}

/// 从 Cargo.toml 内容字符串中粗略判断某 crate 是否在依赖中。
fn cargo_has(content: &str, crate_name: &str) -> bool {
    // 匹配 `crate_name = ...` 或 `crate_name = { ... }` 形式
    content.contains(&format!("\n{crate_name} "))
        || content.contains(&format!("\n{crate_name}="))
        || content.contains(&format!("\"{crate_name}\""))
}

// ── 框架识别核心 ──────────────────────────────────────────────────────────────

fn path_exists(root: &Path, rel: &str) -> bool {
    root.join(rel).exists()
}

fn confidence(root: &Path, path: &str) -> String {
    if path.contains('*') {
        "medium".into()
    } else if path_exists(root, path) {
        "high".into()
    } else {
        "medium".into()
    }
}

fn detect_contracts(root: &Path) -> Vec<DetectedContract> {
    let mut contracts: Vec<DetectedContract> = Vec::new();
    let pkg_deps = collect_all_pkg_deps(root);
    // 纯 Rust 项目：Cargo.toml 在根；Tauri 项目：Cargo.toml 在 src-tauri/
    let cargo_content = std::fs::read_to_string(root.join("Cargo.toml"))
        .or_else(|_| std::fs::read_to_string(root.join("src-tauri/Cargo.toml")))
        .unwrap_or_default();
    // Tauri 项目：commands 目录在 src-tauri/src/commands/
    let tauri_commands_path = if path_exists(root, "src-tauri/src/commands") {
        "src-tauri/src/commands"
    } else {
        "src/commands"
    };

    // ── Node.js 框架层 ────────────────────────────────────────────────────────

    // Drizzle ORM
    if pkg_deps.iter().any(|k| k == "drizzle-orm") {
        // monorepo：优先在 apps/* 子包里找 drizzle 目录
        let p: String = find_in_apps(root, "src/drizzle")
            .map(|d| format!("{d}/**"))
            .or_else(|| find_in_apps(root, "drizzle").map(|d| format!("{d}/**")))
            .unwrap_or_else(|| {
                if path_exists(root, "drizzle/schema.ts") {
                    "drizzle/schema.ts".into()
                } else if path_exists(root, "src/db/schema.ts") {
                    "src/db/schema.ts".into()
                } else {
                    "drizzle/schema.ts".into()
                }
            });
        contracts.push(DetectedContract {
            label: "数据库 schema（Drizzle）".into(),
            paths: vec![p.clone()],
            doc: "schema 变更需同步更新数据库文档，检查 project-context.yaml".into(),
            confidence: confidence(root, &p),
        });
    }

    // Prisma
    if pkg_deps.iter().any(|k| k == "prisma" || k == "@prisma/client") {
        let p = "prisma/schema.prisma";
        contracts.push(DetectedContract {
            label: "数据库 schema（Prisma）".into(),
            paths: vec![p.into()],
            doc: "schema 变更需同步更新数据库文档，检查 project-context.yaml".into(),
            confidence: confidence(root, p),
        });
    }

    // Hono
    if pkg_deps.iter().any(|k| k == "hono") {
        // monorepo：优先在 apps/* 子包里找 routes 目录
        let p: String = find_in_apps(root, "src/routes")
            .map(|d| format!("{d}/**"))
            .unwrap_or_else(|| {
                if path_exists(root, "src/routes") {
                    "src/routes/**".into()
                } else {
                    "src/index.ts".into()
                }
            });
        contracts.push(DetectedContract {
            label: "API 接口（Hono）".into(),
            paths: vec![p],
            doc: "接口路由变更需同步更新 API 文档或 openapi.yaml".into(),
            confidence: "medium".into(),
        });
    }

    // Express
    if pkg_deps.iter().any(|k| k == "express") {
        contracts.push(DetectedContract {
            label: "API 接口（Express）".into(),
            paths: vec!["src/routes/**".into()],
            doc: "接口路由变更需同步更新 API 文档".into(),
            confidence: "medium".into(),
        });
    }

    // Fastify
    if pkg_deps.iter().any(|k| k == "fastify") {
        contracts.push(DetectedContract {
            label: "API 接口（Fastify）".into(),
            paths: vec!["src/routes/**".into()],
            doc: "接口路由变更需同步更新 API 文档".into(),
            confidence: "medium".into(),
        });
    }

    // tRPC
    if pkg_deps.iter().any(|k| k == "@trpc/server") {
        let p = if path_exists(root, "src/router") {
            "src/router/**"
        } else {
            "src/trpc/**"
        };
        contracts.push(DetectedContract {
            label: "API 接口（tRPC）".into(),
            paths: vec![p.into()],
            doc: "router 变更需同步 client 类型，检查 project-context.yaml".into(),
            confidence: "medium".into(),
        });
    }

    // pnpm monorepo 共享类型
    if path_exists(root, "pnpm-workspace.yaml") {
        // 优先已知命名约定；否则列出 packages/ 下实际存在的子包目录
        let paths: Vec<String> = if path_exists(root, "packages/types") {
            vec!["packages/types/**".into()]
        } else if path_exists(root, "packages/shared") {
            vec!["packages/shared/**".into()]
        } else if let Ok(entries) = std::fs::read_dir(root.join("packages")) {
            let mut dirs: Vec<String> = entries
                .flatten()
                .filter(|e| {
                    e.file_type().map(|t| t.is_dir()).unwrap_or(false)
                        && e.path().join("package.json").exists()
                })
                .map(|e| format!("packages/{}/**", e.file_name().to_string_lossy()))
                .collect();
            dirs.sort();
            dirs.truncate(3);
            if dirs.is_empty() { vec!["packages/**".into()] } else { dirs }
        } else {
            vec!["packages/**".into()]
        };
        contracts.push(DetectedContract {
            label: "共享包（pnpm monorepo）".into(),
            paths,
            doc: "共享包变更影响所有 apps，禁止在各 app 内手抄类型".into(),
            confidence: "medium".into(),
        });
    }

    // .env.example → 环境变量声明
    if path_exists(root, ".env.example") {
        contracts.push(DetectedContract {
            label: "环境变量声明".into(),
            paths: vec![".env.example".into()],
            doc: "新增环境变量必须在 .env.example 中同步声明".into(),
            confidence: "high".into(),
        });
    }

    // ── Rust 框架层 ───────────────────────────────────────────────────────────

    if !cargo_content.is_empty() {
        // Axum
        if cargo_has(&cargo_content, "axum") {
            // Tauri 项目：Axum 通常用于嵌入式 MCP/HTTP 服务器，路由在 src-tauri/src/
            let p = if path_exists(root, "src-tauri/src/mcp_server.rs") {
                "src-tauri/src/mcp_server.rs"
            } else if path_exists(root, "src/routes.rs") {
                "src/routes.rs"
            } else if path_exists(root, "src/routes") {
                "src/routes/**"
            } else {
                "src/routes/**"
            };
            contracts.push(DetectedContract {
                label: "API 接口（Axum）".into(),
                paths: vec![p.into()],
                doc: "路由变更需同步 API 文档".into(),
                confidence: if p.contains('*') { "medium".into() } else { "high".into() },
            });
        }

        // SeaORM
        if cargo_has(&cargo_content, "sea-orm") {
            let p = "src/entities";
            contracts.push(DetectedContract {
                label: "数据库 schema（SeaORM）".into(),
                paths: vec![format!("{p}/**")],
                doc: "entity 变更需同步数据库文档".into(),
                confidence: if path_exists(root, p) { "high".into() } else { "medium".into() },
            });
        }

        // Tauri（自身的 command 层）
        let tauri_frontend_ts = if path_exists(root, "src/lib/commands.ts") {
            "src/lib/commands.ts"
        } else {
            "src/commands.ts"
        };
        if cargo_has(&cargo_content, "tauri") && path_exists(root, tauri_commands_path) {
            contracts.push(DetectedContract {
                label: "Tauri command 接口（前后端契约）".into(),
                paths: vec![
                    format!("{tauri_commands_path}/**/*.rs"),
                    tauri_frontend_ts.into(),
                ],
                doc: "新增 command 时前后端必须同步".into(),
                confidence: "high".into(),
            });
        }
    }

    contracts
}

// ── Tauri Command ─────────────────────────────────────────────────────────────

/// 扫描项目框架层技术栈，推断外部契约路径。
/// project_path 为 Windows 本地项目根目录（绝对路径）。
#[tauri::command]
pub fn scan_agent_infra_stack(
    project_path: String,
) -> Result<AgentInfraStack, String> {
    let root = Path::new(project_path.trim_end_matches(['/', '\\']));
    if !root.exists() {
        return Err(format!("目录不存在: {}", root.display()));
    }

    let detected_contracts = detect_contracts(root);

    let meta_rules_present = std::fs::read_to_string(root.join("AGENTS.md"))
        .map(|c| c.contains("## 文档防腐元规则"))
        .unwrap_or(false);

    let context_yaml_exists = root.join("docs/ai-context/project-context.yaml").exists();

    Ok(AgentInfraStack {
        detected_contracts,
        meta_rules_present,
        context_yaml_exists,
    })
}

// ── 三件套生成 ────────────────────────────────────────────────────────────────

/// 用户确认后的契约条目（前端传入）。
#[derive(Debug, Deserialize, Clone)]
pub struct ContractInput {
    pub label: String,
    pub paths: Vec<String>,
    pub doc: String,
}

/// 单个文件的生成结果。
#[derive(Debug, Serialize)]
pub struct FileResult {
    /// 相对项目根的路径
    pub path: String,
    /// "written" | "updated" | "skipped" | "failed"
    pub action: String,
    pub detail: String,
}

/// generate_agent_infra_files 的完整返回值。
#[derive(Debug, Serialize)]
pub struct GenerateReport {
    pub files: Vec<FileResult>,
}

// 元规则段落内容（技术栈无关，编译期内嵌）
const META_RULES_SECTION: &str = r#"
---

## Agent 友好工程四支柱

修改代码前，确认这个项目满足四支柱：

**可导航**：凭文件名/目录名可在 30 秒内定位某功能的代码。通用能力有独立家，不寄生在业务模块下。

**可推理**：公共类型只在一处定义。改一个接口，类型检查能报出所有受影响处。

**可验证**：typecheck / lint / test 各有一条命令真能跑。关键路径有冒烟测试。

**可记忆**：规则按场景注入（不是一份巨型文档）。决策只写"为什么"，代码自己回答"是什么"。

---

## 文档防腐元规则

做任何改动时，判断它属于哪类变更：

### T1 外部契约变更（最高优先级）
其他代码、服务、或 agent 依赖的定义——接口、共享类型、数据结构、配置键名、环境变量名。
→ 查 `docs/ai-context/project-context.yaml` 找到记录该契约的文档，检查并同步更新。

### T2 架构边界变更
模块划分、目录结构、依赖方向、包边界调整。
→ 检查架构文档是否需要同步。

### T3 工具链变更
引入或替换库、框架、构建工具。
→ 更新 AGENTS.md 技术栈声明；通知炼境重新扫描并更新 `project-context.yaml`。

### T4 常驻上下文过期
AGENTS.md 或 `docs/ai-context/project-brief.md` 描述的现实已经改变。
→ 立即更新。这两份文档常驻 agent 上下文，过期危害最大。

### 不触发检查
内部重构、性能优化、bug fix（外部行为不变）、测试文件、临时脚本。

---

## 文档防腐铁律

- 文档只写稳定的：为什么、铁律、意图、架构决策。
- 易变的（端口、路由、表名、命令清单）交给工具自省，不写死进常驻文档。写死 = 定时炸弹。
- 单一事实来源：两处说同一事时，删掉派生的那处。
- 两份文档打架时，信代码，不信文档。

---

## 定期防腐自查

每隔一阵或大改后，对照以下清单：

- [ ] AGENTS.md 里的技术栈声明还和代码一致吗？
- [ ] `project-brief.md` 最近 30 天有没有随架构改动更新？
- [ ] 有没有两份文档说同一件事且打架？
- [ ] `project-context.yaml` 的契约路径还准确吗？
"#;

fn write_file(path: &std::path::PathBuf, content: &str, rel: &str) -> FileResult {
    if let Some(parent) = path.parent() {
        if let Err(e) = std::fs::create_dir_all(parent) {
            return FileResult {
                path: rel.into(),
                action: "failed".into(),
                detail: format!("建目录失败: {e}"),
            };
        }
    }
    match std::fs::write(path, content) {
        Ok(_) => FileResult { path: rel.into(), action: "written".into(), detail: "已写入".into() },
        Err(e) => FileResult { path: rel.into(), action: "failed".into(), detail: format!("写入失败: {e}") },
    }
}

/// 生成 docs/ai-context/project-context.yaml
fn gen_context_yaml(root: &Path, contracts: &[ContractInput]) -> FileResult {
    let rel = "docs/ai-context/project-context.yaml";
    let target = root.join(rel);
    let existed = target.exists();

    let mut yaml = String::from(
        "# 项目契约路径映射\n\
         # 由炼境根据技术栈扫描生成，技术栈变更后重新运行接入包生成\n\
         # 供 AGENTS.md 元规则 T1 查询：「哪个文件是这个项目的外部契约？」\n\n\
         contracts:\n",
    );
    for c in contracts {
        yaml.push_str(&format!("  - label: {}\n    paths:\n", c.label));
        for p in &c.paths {
            yaml.push_str(&format!("      - \"{p}\"\n"));
        }
        yaml.push_str(&format!("    doc: \"{}\"\n\n", c.doc.replace('"', "'")));
    }
    yaml.push_str(
        "architecture_docs:\n  - \"AGENTS.md\"\n\n\
         agent_docs:\n  - \"AGENTS.md\"\n  - \"docs/ai-context/project-brief.md\"\n",
    );

    let mut result = write_file(&target, &yaml, rel);
    if result.action == "written" && existed {
        result.action = "updated".into();
        result.detail = "已更新（覆盖上次生成版本）".into();
    }
    result
}

/// 向 AGENTS.md 注入元规则段落（已有则跳过，幂等）
fn inject_meta_rules(root: &Path) -> FileResult {
    let rel = "AGENTS.md";
    let target = root.join(rel);

    match std::fs::read_to_string(&target) {
        Ok(content) if content.contains("## 文档防腐元规则") => FileResult {
            path: rel.into(),
            action: "skipped".into(),
            detail: "元规则段落已存在，未修改".into(),
        },
        Ok(mut content) => {
            content.push_str(META_RULES_SECTION);
            match std::fs::write(&target, content) {
                Ok(_) => FileResult {
                    path: rel.into(),
                    action: "updated".into(),
                    detail: "已追加元规则段落".into(),
                },
                Err(e) => FileResult {
                    path: rel.into(),
                    action: "failed".into(),
                    detail: format!("写入失败: {e}"),
                },
            }
        }
        Err(_) => FileResult {
            path: rel.into(),
            action: "skipped".into(),
            detail: "AGENTS.md 不存在，请先应用接入包（v1）".into(),
        },
    }
}

/// 生成 .claude/rules/doc-freshness.md（含项目特定 globs）
fn gen_doc_freshness_rule(root: &Path, contracts: &[ContractInput]) -> FileResult {
    let rel = ".claude/rules/doc-freshness.md";
    let target = root.join(rel);

    // 收集所有非 glob 的具体文件路径作为 globs frontmatter
    let globs: Vec<String> = contracts
        .iter()
        .flat_map(|c| c.paths.iter().cloned())
        .collect();
    let globs_str = globs.join(", ");

    let content = format!(
        "---\nglobs: {globs_str}\n---\n\n\
         这些是本项目的外部契约文件。修改时检查 `docs/ai-context/project-context.yaml` 是否需要同步更新。\n\n\
         参考 AGENTS.md 元规则 **T1 外部契约变更**。\n"
    );

    write_file(&target, &content, rel)
}

/// 根据用户确认的契约列表生成三件套文件。
#[tauri::command]
pub fn generate_agent_infra_files(
    project_path: String,
    contracts: Vec<ContractInput>,
) -> Result<GenerateReport, String> {
    let root = Path::new(project_path.trim_end_matches(['/', '\\']));
    if !root.exists() {
        return Err(format!("目录不存在: {}", root.display()));
    }

    let files = vec![
        gen_context_yaml(root, &contracts),
        inject_meta_rules(root),
        gen_doc_freshness_rule(root, &contracts),
    ];

    Ok(GenerateReport { files })
}

// ── 文档腐化评分 ──────────────────────────────────────────────────────────────

/// 单条契约的腐化评估。
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FreshnessDetail {
    /// 契约路径（来自 project-context.yaml）
    pub contract_path: String,
    /// 契约文件最后一次 commit 的 Unix 时间戳；None = 从未提交
    pub contract_ts: Option<i64>,
    /// project-context.yaml 最后一次 commit 的 Unix 时间戳
    pub yaml_ts: Option<i64>,
    /// 契约比 yaml 新了多少天（负数 = yaml 更新）
    pub gap_days: Option<i64>,
    /// "green" | "yellow" | "red"
    pub level: String,
}

/// scan_doc_freshness 的完整返回值。
#[derive(Debug, Serialize)]
pub struct FreshnessResult {
    /// 综合评分（所有 detail 中最差的）
    pub level: String,
    pub details: Vec<FreshnessDetail>,
    /// ISO 8601 检查时间
    pub checked_at: String,
}

/// 对 git log 路径做简化：glob 路径取目录部分供 git log 使用。
fn git_query_path(path: &str) -> &str {
    if let Some(pos) = path.find("/**") {
        &path[..pos]
    } else if path.ends_with("**") {
        &path[..path.len() - 2]
    } else {
        path
    }
}

/// 获取某文件（或目录）在本地 git 仓库的最后 commit 时间戳（Unix 秒）。
/// 返回 None 表示路径从未被提交。
fn git_last_commit_ts(project_root: &Path, rel_path: &str) -> Option<i64> {
    let query = git_query_path(rel_path);
    let output = crate::silent_cmd("git")
        .args(["log", "--format=%at", "-1", "--", query])
        .current_dir(project_root)
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let s = String::from_utf8_lossy(&output.stdout).trim().to_string();
    s.parse::<i64>().ok()
}

/// 从生成的 project-context.yaml 中提取 contracts.*.paths 列表。
fn extract_yaml_contract_paths(content: &str) -> Vec<String> {
    let mut paths = Vec::new();
    let mut in_paths_block = false;

    for line in content.lines() {
        // "    paths:" 在 contracts 条目内（4 空格缩进）
        if line.starts_with("    paths:") {
            in_paths_block = true;
            continue;
        }
        if in_paths_block {
            // 路径条目：6 空格 + "- "
            if line.starts_with("      - ") {
                let raw = line
                    .trim_start_matches(|c: char| c.is_whitespace())
                    .trim_start_matches("- ")
                    .trim_matches('"');
                if !raw.is_empty() {
                    paths.push(raw.to_string());
                }
            } else {
                in_paths_block = false;
            }
        }
    }
    paths
}

fn level_from_gap(gap_days: Option<i64>) -> &'static str {
    match gap_days {
        Some(d) if d > 30 => "red",
        Some(d) if d > 14 => "yellow",
        _ => "green",
    }
}

fn worst_level(a: &str, b: &str) -> String {
    match (a, b) {
        ("red", _) | (_, "red") => "red".into(),
        ("yellow", _) | (_, "yellow") => "yellow".into(),
        _ => "green".into(),
    }
}

/// 扫描文档腐化风险，结果写入 doc_freshness_cache。
#[tauri::command]
pub fn scan_doc_freshness(
    project_path: String,
    project_id: String,
) -> Result<FreshnessResult, String> {
    let root = Path::new(project_path.trim_end_matches(['/', '\\']));
    if !root.exists() {
        return Err(format!("目录不存在: {}", root.display()));
    }

    let yaml_rel = "docs/ai-context/project-context.yaml";
    let yaml_path = root.join(yaml_rel);

    // project-context.yaml 不存在 → 直接 RED
    if !yaml_path.exists() {
        let result = FreshnessResult {
            level: "red".into(),
            details: vec![FreshnessDetail {
                contract_path: yaml_rel.into(),
                contract_ts: None,
                yaml_ts: None,
                gap_days: None,
                level: "red".into(),
            }],
            checked_at: chrono::Local::now().to_rfc3339(),
        };
        cache_result(&project_id, &result);
        return Ok(result);
    }

    let yaml_content = std::fs::read_to_string(&yaml_path).map_err(|e| e.to_string())?;
    let contract_paths = extract_yaml_contract_paths(&yaml_content);
    let yaml_ts = git_last_commit_ts(root, yaml_rel);

    let mut details: Vec<FreshnessDetail> = Vec::new();
    let mut overall = String::from("green");

    if contract_paths.is_empty() {
        // yaml 存在但无契约 → green（基础设施已就绪，只是项目没有外部契约）
        details.push(FreshnessDetail {
            contract_path: "(无契约路径)".into(),
            contract_ts: None,
            yaml_ts,
            gap_days: None,
            level: "green".into(),
        });
    } else {
        for cp in &contract_paths {
            let contract_ts = git_last_commit_ts(root, cp);
            let gap_days = match (contract_ts, yaml_ts) {
                (Some(ct), Some(yt)) => {
                    let diff = ct - yt;
                    Some(diff / 86400) // 秒转天
                }
                (Some(_), None) => Some(999), // 有契约提交但 yaml 从未提交 → 高风险
                _ => None,
            };
            let level = level_from_gap(gap_days).to_string();
            overall = worst_level(&overall, &level);
            details.push(FreshnessDetail {
                contract_path: cp.clone(),
                contract_ts,
                yaml_ts,
                gap_days,
                level,
            });
        }
    }

    let checked_at = chrono::Local::now().to_rfc3339();
    let result = FreshnessResult {
        level: overall,
        details,
        checked_at,
    };

    cache_result(&project_id, &result);
    Ok(result)
}

fn cache_result(project_id: &str, result: &FreshnessResult) {
    let details_json = serde_json::to_string(&result.details).unwrap_or_default();
    if let Some(Ok(conn)) = crate::db::try_pool().map(|p| p.get()) {
        let _ = conn.execute(
            "INSERT INTO doc_freshness_cache (project_id, score, details, checked_at)
             VALUES (?1, ?2, ?3, ?4)
             ON CONFLICT(project_id) DO UPDATE SET
               score = excluded.score,
               details = excluded.details,
               checked_at = excluded.checked_at",
            rusqlite::params![project_id, result.level, details_json, result.checked_at],
        );
    }
}

/// 读取缓存的腐化评分（供看板 E 卡使用，不触发 git 扫描）。
#[tauri::command]
pub fn get_doc_freshness_cache(project_id: String) -> Option<CachedFreshness> {
    let conn = crate::db::try_pool()?.get().ok()?;
    conn.query_row(
        "SELECT score, details, checked_at FROM doc_freshness_cache WHERE project_id = ?1",
        rusqlite::params![project_id],
        |row| {
            Ok(CachedFreshness {
                score: row.get(0)?,
                details: row.get(1)?,
                checked_at: row.get(2)?,
            })
        },
    ).ok()
}

/// 看板 E 卡读取的缓存结构。
#[derive(Debug, Serialize)]
pub struct CachedFreshness {
    pub score: String,
    pub details: String, // JSON 字符串，前端自行解析
    pub checked_at: String,
}

// ── Agent 健康诊断 ─────────────────────────────────────────────────────────────

#[derive(Debug, Serialize)]
pub struct AgentHealthIssue {
    pub level: String,  // "error" | "warn"
    pub title: String,
    pub fix: String,
}

#[derive(Debug, Serialize)]
pub struct AgentHealthReport {
    pub overall: String,       // "green" | "yellow" | "red"
    pub issues: Vec<AgentHealthIssue>,
    /// 可直接复制给子项目 agent 的修正指令文本（Markdown）
    pub fix_prompt: String,
    pub checked_at: String,
}

/// 诊断项目 agent 友好度，生成可复制的修正指令。
/// 内部调用 scan_agent_infra_stack + scan_doc_freshness，同时刷新 doc_freshness_cache。
#[tauri::command]
pub fn get_agent_health(
    project_path: String,
    project_id: String,
) -> Result<AgentHealthReport, String> {
    let root = Path::new(project_path.trim_end_matches(['/', '\\']));
    if !root.exists() {
        return Err(format!("目录不存在: {}", root.display()));
    }

    let stack = scan_agent_infra_stack(project_path.clone())?;
    let freshness = scan_doc_freshness(project_path.clone(), project_id.clone())?;

    let mut issues: Vec<AgentHealthIssue> = Vec::new();
    let mut overall = "green".to_string();

    if !stack.context_yaml_exists {
        issues.push(AgentHealthIssue {
            level: "error".into(),
            title: "缺少 docs/ai-context/project-context.yaml".into(),
            fix: "在炼境「Agent 基础设施」面板点击「扫描技术栈」→「确认并生成三件套」".into(),
        });
        overall = "red".to_string();
    }

    if !stack.meta_rules_present {
        issues.push(AgentHealthIssue {
            level: "warn".into(),
            title: "AGENTS.md 缺少「文档防腐元规则」段落".into(),
            fix: "在炼境「Agent 基础设施」面板生成三件套，或手动在 AGENTS.md 末尾添加 ## 文档防腐元规则 章节".into(),
        });
        if overall != "red" {
            overall = "yellow".to_string();
        }
    }

    match freshness.level.as_str() {
        "red" if !stack.context_yaml_exists => {
            // yaml 不存在导致的 red 已在上面记录，不重复
        }
        "red" => {
            let stale_count = freshness.details.iter().filter(|d| d.level == "red").count();
            issues.push(AgentHealthIssue {
                level: "error".into(),
                title: format!("文档严重腐化：{stale_count} 个契约文件更新超 30 天未同步 project-context.yaml"),
                fix: "更新（或重新生成）docs/ai-context/project-context.yaml 后【必须 git commit 该文件】——评分基于 git 提交时间而非文件修改时间，只改不提交无法清除本项".into(),
            });
            overall = "red".to_string();
        }
        "yellow" => {
            let stale_count = freshness.details.iter()
                .filter(|d| d.level == "yellow" || d.level == "red")
                .count();
            issues.push(AgentHealthIssue {
                level: "warn".into(),
                title: format!("文档腐化风险：{stale_count} 个契约文件更新后 14-30 天未同步"),
                fix: "更新 docs/ai-context/project-context.yaml 中的契约描述并 git commit（评分基于提交时间）".into(),
            });
            if overall != "red" {
                overall = "yellow".to_string();
            }
        }
        _ => {}
    }

    let proj_name = root
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("未知项目");
    let fix_prompt = build_fix_prompt(proj_name, &overall, &issues, &stack, &freshness);

    Ok(AgentHealthReport {
        overall,
        issues,
        fix_prompt,
        checked_at: freshness.checked_at,
    })
}

fn build_fix_prompt(
    project_name: &str,
    overall: &str,
    issues: &[AgentHealthIssue],
    stack: &AgentInfraStack,
    freshness: &FreshnessResult,
) -> String {
    let status_emoji = match overall {
        "red" => "🔴",
        "yellow" => "🟡",
        _ => "🟢",
    };
    let status_label = match overall {
        "red" => "需要修复",
        "yellow" => "存在风险",
        _ => "良好",
    };
    let date_label = freshness.checked_at.get(..10).unwrap_or(&freshness.checked_at);

    let mut lines = vec![
        format!("# Agent 友好度诊断 — {project_name}"),
        format!("整体状态：{status_emoji} {status_label}（检测时间：{date_label}）"),
        String::new(),
    ];

    if issues.is_empty() {
        lines.push("✅ 所有检查通过，项目 agent 友好度良好，无需修复。".into());
    } else {
        lines.push("## 待修复项".into());
        lines.push(String::new());
        for issue in issues {
            let icon = if issue.level == "error" { "❌" } else { "⚠️" };
            lines.push(format!("{icon} **{}**", issue.title));
            lines.push(format!("   修复方法：{}", issue.fix));
            lines.push(String::new());
        }
    }

    let mut passed = Vec::new();
    if stack.context_yaml_exists {
        passed.push("docs/ai-context/project-context.yaml 已存在".to_string());
    }
    if stack.meta_rules_present {
        passed.push("AGENTS.md 已包含文档防腐元规则".to_string());
    }
    if !stack.detected_contracts.is_empty() && freshness.level == "green" {
        passed.push(format!(
            "{} 个契约路径均在 14 天内同步",
            stack.detected_contracts.len()
        ));
    }

    if !passed.is_empty() {
        lines.push("## 已通过项".into());
        lines.push(String::new());
        for p in &passed {
            lines.push(format!("✅ {p}"));
        }
        lines.push(String::new());
    }

    lines.push("---".into());
    lines.push("*由炼境 dev-manager 自动生成。请根据上述修复项逐项处理，完成后在炼境中点击「重新检测」刷新状态。*".into());

    lines.join("\n")
}

// ── 单元测试 ──────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;

    fn setup(dir: &std::path::Path) {
        fs::create_dir_all(dir).unwrap();
    }

    fn write(dir: &std::path::Path, rel: &str, content: &str) {
        let p = dir.join(rel);
        if let Some(parent) = p.parent() {
            fs::create_dir_all(parent).unwrap();
        }
        fs::write(p, content).unwrap();
    }

    #[test]
    fn detects_drizzle() {
        let dir = std::env::temp_dir().join(format!("ai_infra_test_drizzle_{}", std::process::id()));
        setup(&dir);
        write(&dir, "package.json", r#"{"dependencies":{"drizzle-orm":"^0.30.0","hono":"^4.0.0"}}"#);
        write(&dir, "drizzle/schema.ts", "export const users = {};");

        let contracts = detect_contracts(&dir);
        let labels: Vec<&str> = contracts.iter().map(|c| c.label.as_str()).collect();
        assert!(labels.contains(&"数据库 schema（Drizzle）"), "应识别 Drizzle");
        assert!(labels.contains(&"API 接口（Hono）"), "应识别 Hono");

        let drizzle = contracts.iter().find(|c| c.label.contains("Drizzle")).unwrap();
        assert_eq!(drizzle.confidence, "high", "schema.ts 存在应为 high");

        fs::remove_dir_all(&dir).unwrap();
    }

    #[test]
    fn detects_prisma() {
        let dir = std::env::temp_dir().join(format!("ai_infra_test_prisma_{}", std::process::id()));
        setup(&dir);
        write(&dir, "package.json", r#"{"devDependencies":{"prisma":"^5.0.0"},"dependencies":{"@prisma/client":"^5.0.0"}}"#);

        let contracts = detect_contracts(&dir);
        let labels: Vec<&str> = contracts.iter().map(|c| c.label.as_str()).collect();
        assert!(labels.contains(&"数据库 schema（Prisma）"), "应识别 Prisma");

        let prisma = contracts.iter().find(|c| c.label.contains("Prisma")).unwrap();
        // schema.prisma 不存在，confidence 应为 medium
        assert_eq!(prisma.confidence, "medium");

        fs::remove_dir_all(&dir).unwrap();
    }

    #[test]
    fn detects_axum_from_cargo() {
        let dir = std::env::temp_dir().join(format!("ai_infra_test_axum_{}", std::process::id()));
        setup(&dir);
        write(&dir, "Cargo.toml", "[dependencies]\naxum = \"0.7\"\ntokio = { version = \"1\" }\n");

        let contracts = detect_contracts(&dir);
        let labels: Vec<&str> = contracts.iter().map(|c| c.label.as_str()).collect();
        assert!(labels.contains(&"API 接口（Axum）"), "应识别 Axum");

        fs::remove_dir_all(&dir).unwrap();
    }

    #[test]
    fn detects_tauri_contract() {
        let dir = std::env::temp_dir().join(format!("ai_infra_test_tauri_{}", std::process::id()));
        setup(&dir);
        write(&dir, "Cargo.toml", "[dependencies]\ntauri = \"2\"\n");
        fs::create_dir_all(dir.join("src-tauri/src/commands")).unwrap();

        let contracts = detect_contracts(&dir);
        let found = contracts.iter().any(|c| c.label.contains("Tauri command"));
        assert!(found, "应识别 Tauri command 契约");

        fs::remove_dir_all(&dir).unwrap();
    }

    #[test]
    fn empty_project_returns_empty() {
        let dir = std::env::temp_dir().join(format!("ai_infra_test_empty_{}", std::process::id()));
        setup(&dir);
        let contracts = detect_contracts(&dir);
        assert!(contracts.is_empty(), "无框架文件应返回空列表");
        fs::remove_dir_all(&dir).unwrap();
    }

    #[test]
    fn generate_creates_three_files() {
        let dir = std::env::temp_dir().join(format!("ai_infra_test_gen_{}", std::process::id()));
        setup(&dir);
        // AGENTS.md 存在但无元规则
        write(&dir, "AGENTS.md", "# My Project\n\n## 项目概况\n\nsome content\n");

        let contracts = vec![
            ContractInput {
                label: "数据库 schema（Drizzle）".into(),
                paths: vec!["drizzle/schema.ts".into()],
                doc: "schema 变更需同步".into(),
            },
            ContractInput {
                label: "API 接口（Hono）".into(),
                paths: vec!["src/routes/**".into()],
                doc: "接口变更需同步".into(),
            },
        ];

        let report = generate_agent_infra_files(dir.to_str().unwrap().to_string(), contracts).unwrap();
        let actions: Vec<(&str, &str)> = report.files.iter()
            .map(|f| (f.path.as_str(), f.action.as_str()))
            .collect();

        // project-context.yaml 写入
        assert!(actions.iter().any(|(p, a)| p.contains("project-context.yaml") && *a == "written"),
            "应写入 project-context.yaml，实际: {actions:?}");
        // AGENTS.md 注入元规则
        assert!(actions.iter().any(|(p, a)| p.contains("AGENTS.md") && *a == "updated"),
            "应追加元规则到 AGENTS.md，实际: {actions:?}");
        // .claude/rules/doc-freshness.md 写入
        assert!(actions.iter().any(|(p, a)| p.contains("doc-freshness.md") && *a == "written"),
            "应写入 doc-freshness.md，实际: {actions:?}");

        // 验证 doc-freshness.md 含 globs
        let freshness = fs::read_to_string(dir.join(".claude/rules/doc-freshness.md")).unwrap();
        assert!(freshness.contains("drizzle/schema.ts"), "doc-freshness 应含契约路径");
        assert!(freshness.contains("globs:"), "doc-freshness 应有 globs frontmatter");

        // 验证幂等：重复生成 AGENTS.md 应 skipped
        let contracts2 = vec![ContractInput {
            label: "L".into(), paths: vec!["x.ts".into()], doc: "d".into(),
        }];
        let report2 = generate_agent_infra_files(dir.to_str().unwrap().to_string(), contracts2).unwrap();
        let agents_result = report2.files.iter().find(|f| f.path.contains("AGENTS.md")).unwrap();
        assert_eq!(agents_result.action, "skipped", "元规则已存在，第二次应跳过");

        fs::remove_dir_all(&dir).unwrap();
    }

    #[test]
    fn extract_yaml_paths_works() {
        let yaml = "\
contracts:\n  - label: \"数据库 schema\"\n    paths:\n      - \"drizzle/schema.ts\"\n      - \"src/db.ts\"\n    doc: \"xxx\"\n  - label: \"API\"\n    paths:\n      - \"src/routes/**\"\n    doc: \"yyy\"\narchitecture_docs:\n  - \"AGENTS.md\"\n";
        let paths = extract_yaml_contract_paths(yaml);
        assert_eq!(paths, vec!["drizzle/schema.ts", "src/db.ts", "src/routes/**"]);
    }

    #[test]
    fn freshness_red_when_no_yaml() {
        let dir = std::env::temp_dir().join(format!("ai_infra_test_fresh_{}", std::process::id()));
        setup(&dir);
        // 初始化一个假 git 仓库（让 git log 不报错）
        let _ = std::process::Command::new("git").args(["init"]).current_dir(&dir).output();

        let result = scan_doc_freshness(
            dir.to_str().unwrap().to_string(),
            "test-proj-id".into(),
        ).unwrap();
        assert_eq!(result.level, "red", "无 project-context.yaml 应返回 RED");

        fs::remove_dir_all(&dir).unwrap();
    }

    #[test]
    fn git_query_path_strips_glob() {
        assert_eq!(git_query_path("src/routes/**"), "src/routes");
        assert_eq!(git_query_path("drizzle/schema.ts"), "drizzle/schema.ts");
        assert_eq!(git_query_path("packages/types/**"), "packages/types");
    }

    #[test]
    fn meta_rules_detection() {
        let dir = std::env::temp_dir().join(format!("ai_infra_test_meta_{}", std::process::id()));
        setup(&dir);
        write(&dir, "AGENTS.md", "# Title\n## 文档防腐元规则\nsome content");
        write(&dir, "package.json", "{}");

        let result = scan_agent_infra_stack(dir.to_str().unwrap().to_string()).unwrap();
        assert!(result.meta_rules_present);
        assert!(!result.context_yaml_exists);

        fs::remove_dir_all(&dir).unwrap();
    }
}
