//! Agents 管理（轻量版）：扫描全局/项目级 agent 定义文件，读取与修改 frontmatter 的 model 字段
//!
//! - 全局：~/.claude/agents/*.md；项目：{project}/.claude/agents/*.md
//! - 行级解析 frontmatter（不引 YAML 库）：只识别顶级 name/description/model 键
//! - set_model 只改写/插入/删除 model 行，其余字节原样保留，原子落盘
//! - 改写/删除命令均有路径白名单：目标必须位于全局或已配置项目的 .claude/agents 内
//! - 内置类型（Explore/Plan/general-purpose 等）无文件定义、默认继承主会话模型，
//!   指派模型 = 生成同名覆盖文件（整体替换内置定义，含提示词），恢复内置 = 删除覆盖文件

use serde::Serialize;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentInfo {
    /// 定义文件绝对路径
    pub path: String,
    /// global（用户级） | project（项目级）
    pub scope: String,
    pub name: String,
    pub description: String,
    /// frontmatter 的 model；空 = 未设置（继承主会话模型）
    pub model: String,
}

/// 内置 agent 类型（无文件定义，默认继承主会话模型）：
/// (类型名, 默认描述, 覆盖文件的默认提示词 —— 近似内置行为的模板，用户可再编辑)
/// 注意：同名文件会整体替换内置定义（含提示词），这是 Claude Code 的覆盖语义。
const BUILTIN_AGENTS: [(&str, &str, &str); 3] = [
    (
        "Explore",
        "只读代码探索：搜索/阅读/分析并回答，结论附 文件:行号",
        "你是 Explore，只读代码探索 agent。任务：在代码库中搜索、阅读与分析，回答问题并给出精确的 文件路径:行号 引用。\n\n工作原则：\n- 只做只读操作，绝不创建/修改/删除任何文件\n- 先广撒网定位（符号/关键字/文件名），再精读关键片段\n- 结论附出处；不确定就明说，不编造\n",
    ),
    (
        "Plan",
        "规划实现方案：调研代码库并产出分步实施计划",
        "你是 Plan，规划 agent。任务：调研代码库，产出分步实现计划，不写实现代码。\n\n工作原则：\n- 先读懂相关子系统与既有模式再规划\n- 计划列出涉及文件、改动要点与实施顺序\n- 标注风险点与可选替代方案\n",
    ),
    (
        "general-purpose",
        "通用多步任务：复杂问题的搜索、分析与执行",
        "你是 general-purpose，通用协作 agent，处理复杂多步任务。\n\n工作原则：\n- 先读取相关代码/上下文再动手\n- 改动保持最小、风格与现有代码一致\n- 完成后简要汇报做了什么、还有什么没做\n",
    ),
];

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BuiltinAgentInfo {
    /// 内置类型名（subagent_type 引用名）
    pub name: String,
    /// 内置默认描述
    pub description: String,
    /// 生效的覆盖文件路径（项目级优先于全局）；空 = 未覆盖（纯内置，继承主会话模型）
    pub override_path: String,
    /// 覆盖文件作用域：'' | 'global' | 'project'
    pub override_scope: String,
    /// 覆盖文件中的 model；空 = 继承主会话模型
    pub model: String,
}

fn global_agents_dir() -> Option<PathBuf> {
    dirs::home_dir().map(|h| h.join(".claude").join("agents"))
}

/// 扫描全局 + 指定项目的 agents 目录
pub fn list_agents(project_path: Option<&str>) -> Vec<AgentInfo> {
    let mut out = Vec::new();
    if let Some(dir) = global_agents_dir() {
        scan_dir(&dir, "global", &mut out);
    }
    if let Some(p) = project_path.filter(|s| !s.is_empty()) {
        scan_dir(&Path::new(p).join(".claude").join("agents"), "project", &mut out);
    }
    out.sort_by(|a, b| a.name.cmp(&b.name));
    out
}

/// 内置类型的覆盖状态：同名文件项目级优先（与会话内生效规则一致）
pub fn list_builtins(project_path: Option<&str>) -> Vec<BuiltinAgentInfo> {
    BUILTIN_AGENTS
        .iter()
        .map(|(name, desc, _)| {
            let proj = project_path
                .filter(|s| !s.is_empty())
                .map(|p| Path::new(p).join(".claude").join("agents").join(format!("{name}.md")))
                .filter(|p| p.exists());
            let glob = global_agents_dir()
                .map(|d| d.join(format!("{name}.md")))
                .filter(|p| p.exists());
            let (path, scope) = match (proj, glob) {
                (Some(p), _) => (Some(p), "project"),
                (None, Some(g)) => (Some(g), "global"),
                (None, None) => (None, ""),
            };
            let model = path
                .as_ref()
                .and_then(|p| fs::read_to_string(p).ok())
                .map(|raw| parse_frontmatter(&raw).model)
                .unwrap_or_default();
            BuiltinAgentInfo {
                name: name.to_string(),
                description: desc.to_string(),
                override_path: path.map(|p| p.to_string_lossy().into_owned()).unwrap_or_default(),
                override_scope: scope.into(),
                model,
            }
        })
        .collect()
}

/// 路径形状校验：必须是 …/.claude/agents/*.md（纯函数）
fn has_agents_shape(p: &Path) -> bool {
    let is_md = p.extension().and_then(|e| e.to_str()) == Some("md");
    let parent = p
        .parent()
        .and_then(|d| d.file_name())
        .and_then(|n| n.to_str())
        == Some("agents");
    let grand = p
        .parent()
        .and_then(|d| d.parent())
        .and_then(|d| d.file_name())
        .and_then(|n| n.to_str())
        == Some(".claude");
    is_md && parent && grand
}

/// 归属白名单：目标必须位于全局 agents 目录或某个已配置项目的 .claude/agents 内（防任意文件改写）
fn ensure_agents_path(path: &str) -> Result<(), String> {
    let p = Path::new(path);
    if !has_agents_shape(p) {
        return Err("目标不是 .claude/agents 目录下的 .md 文件，已拒绝".into());
    }
    let mut allowed: Vec<PathBuf> = global_agents_dir().into_iter().collect();
    for proj in crate::store::load().projects {
        allowed.push(Path::new(&proj.path).join(".claude").join("agents"));
    }
    let dir = p.parent().unwrap_or(p);
    // canonicalize 消解符号链接与 ..；目录不存在时回退原始路径比较
    let canon = |d: &Path| fs::canonicalize(d).unwrap_or_else(|_| d.to_path_buf());
    let dir_c = canon(dir);
    if allowed.iter().any(|d| canon(d) == dir_c) {
        Ok(())
    } else {
        Err("目标不在全局或已配置项目的 agents 目录内，已拒绝".into())
    }
}

fn scan_dir(dir: &Path, scope: &str, out: &mut Vec<AgentInfo>) {
    let Ok(rd) = fs::read_dir(dir) else { return };
    for entry in rd.flatten() {
        let path = entry.path();
        if path.extension().and_then(|e| e.to_str()) != Some("md") {
            continue;
        }
        let Ok(raw) = fs::read_to_string(&path) else { continue };
        let fm = parse_frontmatter(&raw);
        let fallback_name = path
            .file_stem()
            .map(|s| s.to_string_lossy().into_owned())
            .unwrap_or_default();
        out.push(AgentInfo {
            path: path.to_string_lossy().into_owned(),
            scope: scope.into(),
            name: if fm.name.is_empty() { fallback_name } else { fm.name },
            description: fm.description,
            model: fm.model,
        });
    }
}

#[derive(Default)]
struct Frontmatter {
    name: String,
    description: String,
    model: String,
}

/// 行级解析 frontmatter 的 name/description/model（值可带单双引号）
fn parse_frontmatter(raw: &str) -> Frontmatter {
    let mut fm = Frontmatter::default();
    let mut lines = raw.lines();
    if lines.next().map(|l| l.trim()) != Some("---") {
        return fm;
    }
    for line in lines {
        let t = line.trim();
        if t == "---" {
            break;
        }
        // 只取顶级键（行首无缩进），跳过 tools: 列表等嵌套内容
        if line.starts_with(' ') || line.starts_with('\t') {
            continue;
        }
        if let Some((key, value)) = t.split_once(':') {
            let v = value
                .trim()
                .trim_matches('"')
                .trim_matches('\'')
                .to_string();
            match key.trim() {
                "name" => fm.name = v,
                "description" => fm.description = v,
                "model" => fm.model = v,
                _ => {}
            }
        }
    }
    fm
}

/// 修改 agent 定义文件的 model 字段：
/// - 已有 model 行 → 替换；无 → 在 frontmatter 尾部插入；model 为空 → 删除该行（恢复继承）
/// - 文件无 frontmatter 时报错（不擅自创建，避免破坏正文）
pub fn set_model(path: &str, model: &str) -> Result<(), String> {
    ensure_agents_path(path)?;
    let p = Path::new(path);
    let raw = fs::read_to_string(p).map_err(|e| format!("读取失败: {e}"))?;
    let new = rewrite_model(&raw, model)?;
    crate::fsx::write_atomic(p, new.as_bytes())
}

/// 删除 agent 定义文件（含内置类型的覆盖文件 → 恢复内置）；同样有路径白名单
pub fn delete(path: &str) -> Result<(), String> {
    ensure_agents_path(path)?;
    fs::remove_file(path).map_err(|e| format!("删除失败: {e}"))
}

/// 为内置类型指派模型：已有生效覆盖 → 改其 model；无 → 生成同名覆盖文件
/// scope 为 project 且传了 project_path 时写到项目级，否则写全局
pub fn set_builtin_model(
    scope: &str,
    project_path: Option<&str>,
    name: &str,
    model: &str,
) -> Result<(), String> {
    let Some((_, desc, prompt)) = BUILTIN_AGENTS.iter().find(|(n, _, _)| *n == name) else {
        return Err(format!("未知内置类型「{name}」"));
    };
    // 已有生效覆盖（项目级优先）→ 直接改 model
    let proj_file = project_path
        .filter(|s| !s.is_empty())
        .map(|p| Path::new(p).join(".claude").join("agents").join(format!("{name}.md")))
        .filter(|p| p.exists());
    let glob_file = global_agents_dir()
        .map(|d| d.join(format!("{name}.md")))
        .filter(|p| p.exists());
    if let Some(p) = proj_file.or(glob_file) {
        return set_model(&p.to_string_lossy(), model);
    }
    let scope = if scope == "project" && project_path.filter(|s| !s.is_empty()).is_some() {
        "project"
    } else {
        "global"
    };
    let dir = resolve_dir(scope, project_path)?;
    write_agent_file(&dir, name, desc, model, prompt).map(|_| ())
}

fn rewrite_model(raw: &str, model: &str) -> Result<String, String> {
    let model = model.trim();
    let mut lines: Vec<String> = raw.lines().map(str::to_string).collect();
    if lines.first().map(|l| l.trim()) != Some("---") {
        return Err("该文件不是标准 agent 定义（缺少 --- frontmatter），请手动补上 frontmatter 后再指派模型".into());
    }
    // 找 frontmatter 结束行
    let Some(end) = lines[1..].iter().position(|l| l.trim() == "---").map(|i| i + 1) else {
        return Err("frontmatter 未闭合（缺少第二个 ---）".into());
    };
    // str::lines() 会剥离行尾 \r：重建时按原文件换行风格统一选择分隔符
    let sep = if raw.contains("\r\n") { "\r\n" } else { "\n" };
    // 找已有 model 行（frontmatter 段内、顶级键）
    let mut model_line: Option<usize> = None;
    for (i, line) in lines.iter().enumerate().take(end).skip(1) {
        if line.starts_with(' ') || line.starts_with('\t') {
            continue;
        }
        if line.trim_start().starts_with("model:") {
            model_line = Some(i);
            break;
        }
    }
    match (model_line, model.is_empty()) {
        (Some(i), true) => {
            lines.remove(i); // 删除 → 恢复继承
        }
        (Some(i), false) => {
            lines[i] = match extract_quote_style(&lines[i]) {
                Some(q) => format!("model: {q}{model}{q}"),
                None => format!("model: {model}"),
            };
        }
        (None, true) => { /* 本来就无 model，无需改动 */ }
        (None, false) => {
            lines.insert(end, format!("model: {model}"));
        }
    }
    // 保留原始结尾换行习惯
    let mut out = lines.join(sep);
    if raw.ends_with('\n') {
        out.push_str(sep);
    }
    Ok(out)
}

/// 提取原 model 行的引号风格（" 或 '），用于替换时保持一致
fn extract_quote_style(line: &str) -> Option<char> {
    let v = line.split_once(':')?.1.trim();
    v.chars().next().filter(|c| *c == '"' || *c == '\'')
}

/// 解析写入目录：global → 用户级 agents 目录；project → 指定项目的 .claude/agents
fn resolve_dir(scope: &str, project_path: Option<&str>) -> Result<PathBuf, String> {
    match scope {
        "global" => global_agents_dir().ok_or("无法定位用户目录".into()),
        "project" => {
            let p = project_path.filter(|s| !s.is_empty()).ok_or("未选择项目")?;
            Ok(Path::new(p).join(".claude").join("agents"))
        }
        _ => Err("未知作用域".into()),
    }
}

/// YAML plain scalar 防断句：含 ": " / " #" / 双引号，或首字符为 YAML 特殊字符时加双引号包裹
/// （Claude Code 用真 YAML 解析 frontmatter，裸值含这些字符会直接报错）
fn yaml_safe_scalar(v: &str) -> String {
    let need_quote = v.contains(": ")
        || v.contains(" #")
        || v.contains('"')
        || v.ends_with(' ')
        || v.starts_with(
            ['-', '?', ':', '!', '&', '*', '#', '[', ']', '{', '}', ',', '|', '>', '@', '`', '\'', '%', ' '],
        );
    if need_quote {
        format!("\"{}\"", v.replace('\\', "\\\\").replace('"', "\\\""))
    } else {
        v.to_string()
    }
}

/// 落盘一个 agent 定义文件：已存在则报错不覆盖；description 换行压平并做 YAML 防断句
fn write_agent_file(
    dir: &Path,
    name: &str,
    description: &str,
    model: &str,
    prompt: &str,
) -> Result<String, String> {
    fs::create_dir_all(dir).map_err(|e| format!("创建目录失败: {e}"))?;
    let path = dir.join(format!("{name}.md"));
    if path.exists() {
        return Err(format!("agent「{name}」已存在（{}）", path.to_string_lossy()));
    }
    let desc_plain = description.trim().replace('\n', " ");
    let desc_yaml = yaml_safe_scalar(&desc_plain);
    let model = model.trim();
    let body = if prompt.trim().is_empty() {
        format!(
            "你是 {name}，专责{desc_plain}。\n\n工作原则：\n- 先读取相关代码/上下文再动手\n- 改动保持最小、风格与现有代码一致\n- 完成后简要汇报做了什么、还有什么没做\n"
        )
    } else {
        format!("{}\n", prompt.trim())
    };
    let model_line = if model.is_empty() {
        String::new()
    } else {
        format!("model: {model}\n")
    };
    let raw = format!("---\nname: {name}\ndescription: {desc_yaml}\n{model_line}---\n\n{body}");
    crate::fsx::write_atomic(&path, raw.as_bytes())?;
    Ok(path.to_string_lossy().into_owned())
}

/// 新建 agent 定义文件（/agents 向导已被官方移除，CCGUI 提供最小创建能力）
/// 校验名称（kebab-case，作为 subagent_type 引用名）；文件已存在则报错，不覆盖
pub fn create(
    scope: &str,
    project_path: Option<&str>,
    name: &str,
    description: &str,
    model: &str,
    prompt: &str,
) -> Result<String, String> {
    let name = name.trim();
    if name.is_empty()
        || !name
            .chars()
            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
        || name.starts_with('-')
        || name.ends_with('-')
    {
        return Err("名称需为小写字母/数字/连字符（kebab-case），且不以连字符开头结尾".into());
    }
    let dir = resolve_dir(scope, project_path)?;
    write_agent_file(&dir, name, description, model, prompt)
}

// ---------- Tauri commands ----------

#[tauri::command]
pub fn agents_list(project_path: Option<String>) -> Vec<AgentInfo> {
    list_agents(project_path.as_deref())
}

#[tauri::command]
pub fn agents_set_model(path: String, model: String) -> Result<(), String> {
    set_model(&path, &model)
}

#[tauri::command]
pub fn agents_create(
    scope: String,
    project_path: Option<String>,
    name: String,
    description: String,
    model: String,
    prompt: String,
) -> Result<String, String> {
    create(&scope, project_path.as_deref(), &name, &description, &model, &prompt)
}

#[tauri::command]
pub fn agents_builtin_list(project_path: Option<String>) -> Vec<BuiltinAgentInfo> {
    list_builtins(project_path.as_deref())
}

#[tauri::command]
pub fn agents_set_builtin_model(
    scope: String,
    project_path: Option<String>,
    name: String,
    model: String,
) -> Result<(), String> {
    set_builtin_model(&scope, project_path.as_deref(), &name, &model)
}

#[tauri::command]
pub fn agents_delete(path: String) -> Result<(), String> {
    delete(&path)
}

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

    #[test]
    fn parse_basic_frontmatter() {
        let raw = "---\nname: reviewer\ndescription: 审查代码\nmodel: glm-cheap\ntools:\n  - Read\n  - Edit\n---\n正文\n";
        let fm = parse_frontmatter(raw);
        assert_eq!(fm.name, "reviewer");
        assert_eq!(fm.description, "审查代码");
        assert_eq!(fm.model, "glm-cheap");
    }

    #[test]
    fn parse_quoted_and_no_frontmatter() {
        let fm = parse_frontmatter("---\nmodel: \"sonnet\"\n---\n");
        assert_eq!(fm.model, "sonnet");
        let none = parse_frontmatter("# 没有 frontmatter\nmodel: x\n");
        assert_eq!(none.model, "");
    }

    #[test]
    fn rewrite_replace_insert_delete() {
        let raw = "---\nname: a\nmodel: old\n---\n正文\n";
        // 替换
        let out = rewrite_model(raw, "new-model").unwrap();
        assert_eq!(out, "---\nname: a\nmodel: new-model\n---\n正文\n");
        // 删除（空值）
        let out = rewrite_model(raw, "").unwrap();
        assert_eq!(out, "---\nname: a\n---\n正文\n");
        // 插入
        let raw2 = "---\nname: a\n---\n正文\n";
        let out = rewrite_model(raw2, "ins").unwrap();
        assert_eq!(out, "---\nname: a\nmodel: ins\n---\n正文\n");
        // 引号风格保持
        let raw3 = "---\nmodel: \"old\"\n---\n";
        let out = rewrite_model(raw3, "new").unwrap();
        assert_eq!(out, "---\nmodel: \"new\"\n---\n");
        // 无 frontmatter 报错
        assert!(rewrite_model("# 正文\n", "x").is_err());
    }

    #[test]
    fn rewrite_preserves_tools_block() {
        let raw = "---\nname: a\ntools:\n  - Read\n  - Edit\nmodel: old\n---\n正文\n";
        let out = rewrite_model(raw, "new").unwrap();
        assert_eq!(out, "---\nname: a\ntools:\n  - Read\n  - Edit\nmodel: new\n---\n正文\n");
    }

    #[test]
    fn rewrite_crlf_new_lines_follow_crlf() {
        // CRLF 文件：插入/替换的行必须带 \r，否则会混入 LF-only 行
        let raw = "---\r\nname: a\r\n---\r\n正文\r\n";
        let out = rewrite_model(raw, "ins").unwrap();
        assert_eq!(out, "---\r\nname: a\r\nmodel: ins\r\n---\r\n正文\r\n");

        let raw2 = "---\r\nmodel: old\r\n---\r\n";
        let out = rewrite_model(raw2, "new").unwrap();
        assert_eq!(out, "---\r\nmodel: new\r\n---\r\n");
    }

    #[test]
    fn yaml_safe_scalar_quotes_when_needed() {
        // 普通值不动
        assert_eq!(yaml_safe_scalar("审查代码改动"), "审查代码改动");
        // 含 ": " 会破坏 YAML 解析 → 加引号
        assert_eq!(yaml_safe_scalar("用法: 审查"), "\"用法: 审查\"");
        // 含 " #"（行内注释起始）→ 加引号
        assert_eq!(yaml_safe_scalar("备注 #1"), "\"备注 #1\"");
        // 首字符为 YAML 特殊字符 → 加引号
        assert_eq!(yaml_safe_scalar("- 列表项"), "\"- 列表项\"");
        // 内嵌双引号需转义
        assert_eq!(yaml_safe_scalar("说 \"你好\""), "\"说 \\\"你好\\\"\"");
    }

    #[test]
    fn agents_path_shape() {
        assert!(has_agents_shape(Path::new(r"C:\Users\x\.claude\agents\a.md")));
        assert!(has_agents_shape(Path::new("/home/x/.claude/agents/a.md")));
        assert!(has_agents_shape(Path::new(r"F:\proj\.claude\agents\Explore.md")));
        // 非 agents 目录 / 非 .md / 缺 .claude 层级 → 拒绝
        assert!(!has_agents_shape(Path::new(r"C:\Users\x\.claude\agents\a.txt")));
        assert!(!has_agents_shape(Path::new(r"C:\Users\x\.claude\other\a.md")));
        assert!(!has_agents_shape(Path::new(r"C:\tmp\agents\a.md")));
    }

    #[test]
    fn builtin_definitions_are_wellformed() {
        // 内置类型的默认定义必须能生成合法 frontmatter 文件（描述经 yaml_safe_scalar 处理）
        for (name, desc, prompt) in BUILTIN_AGENTS {
            assert!(!name.is_empty() && !desc.is_empty() && !prompt.is_empty());
            let raw = format!(
                "---\nname: {name}\ndescription: {}\nmodel: m\n---\n\n{prompt}",
                yaml_safe_scalar(desc)
            );
            let fm = parse_frontmatter(&raw);
            assert_eq!(fm.name, name);
            assert!(!fm.description.is_empty());
            assert_eq!(fm.model, "m");
        }
    }

    #[test]
    fn list_builtins_detects_project_override() {
        // 造一个临时项目目录，放入 Explore.md 覆盖文件，验证项目级覆盖被识别（项目级优先于全局）
        let dir = std::env::temp_dir().join(format!("ccgui-agents-test-{}", std::process::id()));
        let agents_dir = dir.join(".claude").join("agents");
        fs::create_dir_all(&agents_dir).unwrap();
        fs::write(
            agents_dir.join("Explore.md"),
            "---\nname: Explore\ndescription: d\nmodel: qwen\n---\n正文\n",
        )
        .unwrap();
        let list = list_builtins(Some(&dir.to_string_lossy()));
        let explore = list.iter().find(|b| b.name == "Explore").unwrap();
        assert_eq!(explore.override_scope, "project");
        assert_eq!(explore.model, "qwen");
        assert!(explore.override_path.ends_with("Explore.md"));
        // 三个内置类型都应列出
        assert_eq!(list.len(), BUILTIN_AGENTS.len());
        fs::remove_dir_all(&dir).ok();
    }
}
