//! 本机 Claude 技能扫描（skills / commands / plugins 三类）
//!
//! 核心设计6(/ skill 修复):
//! - `strip_frontmatter` + `read_skill_content_stripped`:注入正文剥首个 `---...---` 块
//!   (原 `read_skill_content` 保留兼容,返全文)
//! - `OnceLock<Vec>` → `RwLock<Option<Vec>>` + 快路径(读锁命中 clone)/ 慢路径(重扫)
//!   + `invalidate_skills` 写锁置 None,支持进程内 `ai_reload_skills` 热重载
//! - `scan_skills` 返 `ScanResult{skills, conflicts}`:同名收集所有 path 入 conflicts,
//!   保留首份(优先级 skills>commands>plugins);`SkillInfo.duplicates` 仅冲突时填。

use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{RwLock, RwLockReadGuard};

use serde::Serialize;

/// 技能元信息（前端 `/` 联想 + 后端注入用）
#[derive(Debug, Clone, Serialize)]
pub struct SkillInfo {
    pub name: String,
    pub description: String,
    pub argument_hint: Option<String>,
    /// skill | command | plugin
    pub source: String,
    /// SKILL.md 绝对路径（注入时读全文）
    pub path: String,
    /// 同名冲突时的其它来源路径（仅冲突时填 Some；首份为 None）。
    ///
    /// 注入始终取首份(优先级 skills>commands>plugins)，前端据非空 duplicates 提示用户
    /// "存在同名技能 N 份，已用 {source} 来源"。无冲突为 None，向后兼容旧前端（字段缺省）。
    #[serde(skip_serializing_if = "Option::is_none")]
    pub duplicates: Option<Vec<String>>,
}

/// 扫描结果：去重后的 skills 列表 + 同名冲突明细（前端提示用）。
///
/// `conflicts`: `(name, 所有同名的 path 列表)` —— 注入取 skills[0]（即 ScanResult.skills 中
/// 首份，优先级 skills>commands>plugins），冲突列表仅做提示，不影响注入。
#[derive(Debug, Clone, Serialize)]
pub struct ScanResult {
    pub skills: Vec<SkillInfo>,
    pub conflicts: Vec<(String, Vec<String>)>,
}

/// ~/.claude 目录（跨平台：USERPROFILE / HOME）
fn claude_home() -> Option<PathBuf> {
    std::env::var_os("USERPROFILE")
        .or_else(|| std::env::var_os("HOME"))
        .map(PathBuf::from)
        .map(|h| h.join(".claude"))
}

/// 剥离 markdown 首个 `---...---` frontmatter 块，返回正文。
///
/// 状态机：用行索引推进；首个 `---`(trim 后)进入 in_fm，遇到下个 `---` 退出，余为正文。
/// 无 frontmatter（首行非 ---）直接返原文；frontmatter 未闭合（只有起始 --- 无收尾）
/// 按容错返空串（避免把整篇当 frontmatter 误剥，此分支极罕见 —— SKILL.md 都闭合）。
///
/// 注入用：原 `read_skill_content` 返全文（含 frontmatter）会让 LLM 看到 YAML 头噪声，
/// 改用 `read_skill_content_stripped` 后注入正文干净。
pub(crate) fn strip_frontmatter(md: &str) -> String {
    let mut lines = md.lines().enumerate().peekable();
    // 空串 / 首行非 --- → 无 frontmatter，返原文
    let first = match lines.next() {
        Some((_, l)) => l,
        None => return String::new(),
    };
    if first.trim() != "---" {
        // 无 frontmatter：返原文（首行 + 余下，用 \n 拼回）
        let mut out = first.to_string();
        for (_, l) in lines {
            out.push('\n');
            out.push_str(l);
        }
        return out;
    }
    // in_fm：跳过直到下个 ---
    let mut body_start: Option<usize> = None;
    for (i, l) in lines {
        if l.trim() == "---" {
            body_start = Some(i + 1);
            break;
        }
    }
    match body_start {
        Some(start) => {
            // 正文 = 原文第 start 行（0-based）起所有行
            md.lines().skip(start).collect::<Vec<_>>().join("\n")
        }
        None => String::new(), // frontmatter 未闭合
    }
}

/// 剥离 YAML 标量值两侧的引号（`"..."` / `'...'`），简易 frontmatter 解析用
fn unquote(s: &str) -> &str {
    s.strip_prefix('"')
        .and_then(|x| x.strip_suffix('"'))
        .or_else(|| s.strip_prefix('\'').and_then(|x| x.strip_suffix('\'')))
        .unwrap_or(s)
}

/// 解析 markdown frontmatter 的 name / description / argument-hint / user_invocable
/// （简易，按行匹配，容错缩进与 CRLF；仅扫描 frontmatter 区段）
fn parse_frontmatter(md: &str) -> Option<(String, String, Option<String>, bool)> {
    let mut lines = md.lines();
    if lines.next()?.trim() != "---" {
        return None;
    }
    let mut name = None;
    let mut desc = None;
    let mut hint = None;
    let mut invocable = true;
    for line in lines {
        if line.trim() == "---" {
            break;
        }
        let l = line.trim_start();
        if let Some(v) = l.strip_prefix("name:") {
            name = Some(unquote(v.trim()).to_string());
        } else if let Some(v) = l.strip_prefix("description:") {
            desc = Some(unquote(v.trim()).to_string());
        } else if let Some(v) = l.strip_prefix("argument-hint:") {
            hint = Some(unquote(v.trim()).to_string());
        } else if let Some(v) = l.strip_prefix("user_invocable:") {
            invocable = v.trim() != "false";
        }
    }
    name.map(|n| (n, desc.unwrap_or_default(), hint, invocable))
}

/// 解析单个 SKILL.md / command md 为 SkillInfo（排除 user_invocable: false）
fn parse_skill_file(path: &Path, source: &str) -> Option<SkillInfo> {
    let md = fs::read_to_string(path).ok()?;
    let (name, description, argument_hint, invocable) = parse_frontmatter(&md).unwrap_or_else(|| {
        // 无 frontmatter（部分 commands）：用文件名兜底，默认可调用
        let stem = path
            .file_stem()
            .map(|s| s.to_string_lossy().to_string())
            .unwrap_or_default();
        (stem, String::new(), None, true)
    });
    if !invocable {
        return None;
    }
    Some(SkillInfo {
        name,
        description,
        argument_hint,
        source: source.to_string(),
        path: path.to_string_lossy().to_string(),
        duplicates: None,
    })
}

/// 递归收集目录下所有 SKILL.md（用于 plugins/marketplaces 多层嵌套）
fn collect_skill_files(dir: &Path, out: &mut Vec<PathBuf>) {
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let p = entry.path();
            if p.is_dir() {
                // 跳过依赖/版本目录，避免递归爆炸
                let name = p.file_name().and_then(|n| n.to_str()).unwrap_or("");
                if name == "node_modules" || name == ".git" {
                    continue;
                }
                collect_skill_files(&p, out);
            } else if p.file_name().and_then(|n| n.to_str()) == Some("SKILL.md") {
                out.push(p);
            }
        }
    }
}

/// 扫描三类来源，按 name 去重（skills 优先 > commands > plugins）。
///
/// 返 `ScanResult`：`skills` 为去重后首份（优先级保留），`conflicts` 收集所有同名 path
/// （仅当同名 > 1 时入列，前端提示用）。
fn scan_skills() -> ScanResult {
    let home = match claude_home() {
        Some(h) => h,
        None => {
            return ScanResult {
                skills: Vec::new(),
                conflicts: Vec::new(),
            }
        }
    };

    // 三类来源按优先级顺序收集（skills > commands > plugins）
    let mut all: Vec<Vec<SkillInfo>> = Vec::with_capacity(3);

    // 1. ~/.claude/skills/*/SKILL.md
    let mut batch_skill = Vec::new();
    if let Ok(entries) = fs::read_dir(home.join("skills")) {
        for entry in entries.flatten() {
            if let Some(info) = parse_skill_file(&entry.path().join("SKILL.md"), "skill") {
                batch_skill.push(info);
            }
        }
    }
    all.push(batch_skill);

    // 2. ~/.claude/commands/*.md
    let mut batch_cmd = Vec::new();
    if let Ok(entries) = fs::read_dir(home.join("commands")) {
        for entry in entries.flatten() {
            let p = entry.path();
            if p.extension().and_then(|e| e.to_str()) == Some("md") {
                if let Some(info) = parse_skill_file(&p, "command") {
                    batch_cmd.push(info);
                }
            }
        }
    }
    all.push(batch_cmd);

    // 3. ~/.claude/plugins/marketplaces/**/skills/*/SKILL.md（递归；cache 不在此路径下）
    let mut files = Vec::new();
    collect_skill_files(&home.join("plugins").join("marketplaces"), &mut files);
    let mut batch_plugin = Vec::new();
    for f in files {
        if let Some(info) = parse_skill_file(&f, "plugin") {
            batch_plugin.push(info);
        }
    }
    all.push(batch_plugin);

    // 按 name 去重（首份优先级保留）+ 收集冲突
    // name -> (首份 index in skills, 所有 path)
    let mut seen: HashSet<String> = HashSet::new();
    let mut skills: Vec<SkillInfo> = Vec::new();
    // name -> Vec<path>（按来源顺序，用于冲突判定 + duplicates 回填）
    let mut name_paths: std::collections::HashMap<String, Vec<String>> =
        std::collections::HashMap::new();

    for batch in &all {
        for info in batch {
            name_paths
                .entry(info.name.clone())
                .or_default()
                .push(info.path.clone());
            if seen.insert(info.name.clone()) {
                skills.push(info.clone());
            }
        }
    }

    // 回填 duplicates + 构造 conflicts
    let mut conflicts: Vec<(String, Vec<String>)> = Vec::new();
    for skill in skills.iter_mut() {
        if let Some(paths) = name_paths.get(&skill.name) {
            if paths.len() > 1 {
                // 首份 path 是 skill.path 本身，duplicates 填其余（按收集顺序）
                let dups: Vec<String> = paths
                    .iter()
                    .filter(|p| **p != skill.path)
                    .cloned()
                    .collect();
                if !dups.is_empty() {
                    skill.duplicates = Some(dups);
                }
                conflicts.push((skill.name.clone(), paths.clone()));
            }
        }
    }

    ScanResult { skills, conflicts }
}

/// 进程内技能缓存（RwLock + Option 懒初始化）。
///
/// 设计（核心设计6）:
/// - 快路径:`skills_cached()` 读锁命中 → clone 返回（扫盘零开销）
/// - 慢路径:读锁 None → 释放后 `scan_skills()` 填充（双检锁，避免持写锁扫盘阻塞读）
/// - `invalidate_skills()` 写锁置 None，下次 `skills_cached()` 触发重扫
/// - `ai_reload_skills` IPC:invalidate + 重扫，进程内热重载（不重启生效）
static SKILLS: RwLock<Option<Vec<SkillInfo>>> = RwLock::new(None);

/// 读锁快路径 guard（供 `read_skill_content_stripped` 持引用迭代读缓存）。
///
/// 生命周期 `'static`:SKILLS 是 static 项,对其借用可标注 `'static`(Rust 对 static 的保证),
/// 使函数能返回 guard 跨作用域传递。guard Drop 时释放读锁。
type SkillsGuard = RwLockReadGuard<'static, Option<Vec<SkillInfo>>>;

/// 取读锁快照（懒初始化：None 时先释放锁扫盘填回，再读锁取引用）。
///
/// 返 `RwLockReadGuard<Option<Vec<SkillInfo>>>`，调用方解 `*guard` 得 `&Vec<SkillInfo>`。
/// 懒初始化走双检锁：先读锁查 Some（快），None 时释放 → 扫盘 → 写锁填回 → 读锁重取。
///
/// P1-260617-3:`scan_skills` 同步递归 `fs::read_dir` + `read_to_string`(plugins/marketplaces
/// 多层嵌套,Windows 文件多时同步阻塞 tokio runtime)。本函数改 async,慢路径扫盘包
/// `spawn_blocking` 隔离(对齐 commands/project.rs detect_stack 模式)。快路径(读锁命中)仍同步无 fs。
///
/// 锁中毒(P1-260617-3 加固):读写锁 expect 中毒会 panic,技能加载热路径 panic 不可接受。
/// 中毒 → tracing::error! 记录 + 返 None(调用方 `skills_cached` 得空 Vec / `read_skill_content_stripped`
/// 返 None),不再 panic。中毒通常因持锁 panicking 线程(早期改 *g 时 unwrap)残留,缓存本身可重建,
/// 返空后下次 `invalidate_skills` 或进程重启自愈。
async fn skills_lock_async() -> Option<SkillsGuard> {
    // 快路径：读锁命中(无 fs,纯内存)
    {
        // 锁中毒不 panic:PoisonError 携 guard 仍可恢复数据,但缓存一致性不保 → 记录后返 None 降级空。
        let g = match RwLock::read(&SKILLS) {
            Ok(g) => g,
            Err(_) => {
                tracing::error!("SKILLS 读锁中毒,返空技能列表");
                return None;
            }
        };
        if g.is_some() {
            return Some(g);
        }
    }
    // 慢路径：扫盘(spawn_blocking 隔离同步 fs 递归,防阻塞 tokio runtime)
    let scanned = tokio::task::spawn_blocking(scan_skills)
        .await
        .map(|res| res.skills)
        .unwrap_or_default();
    {
        let mut g = match RwLock::write(&SKILLS) {
            Ok(g) => g,
            Err(_) => {
                tracing::error!("SKILLS 写锁中毒,返空技能列表");
                return None;
            }
        };
        // 另一线程可能已填，二次检查（双检锁）
        if g.is_none() {
            *g = Some(scanned);
        }
    }
    // 再取读锁返回（此时必 Some）
    let g = match RwLock::read(&SKILLS) {
        Ok(g) => g,
        Err(_) => {
            tracing::error!("SKILLS 读锁(慢路径后)中毒,返空技能列表");
            return None;
        }
    };
    debug_assert!(g.is_some(), "skills_lock 慢路径后必 Some");
    Some(g)
}

/// 技能扫描结果缓存（进程内；命中即 clone，不重复扫盘）。
///
/// 替代原 `OnceLock::get_or_init` 路径：返 owned `Vec<SkillInfo>`（clone），
/// 因 RwLock 不能返 `&'static`。调用方(config.rs:30 / read_skill_content_stripped)已同步适配。
///
/// P1-260617-3:改 async,慢路径(首次/重扫)走 `skills_lock_async` → spawn_blocking
/// 隔离同步 fs 防阻塞 tokio runtime(Tauri 单线程 runtime)。快路径(读锁命中)无 fs。
pub(crate) async fn skills_cached() -> Vec<SkillInfo> {
    let g = skills_lock_async().await;
    // 锁中毒 → None → unwrap_or_default() 得空 Vec(对齐 P1-260617-3 中毒降级)。
    g.and_then(|g| g.clone()).unwrap_or_default()
}

/// 置缓存为 None，下次 `skills_cached()` 触发重扫。
///
/// `ai_reload_skills` IPC 调用：写锁置 None → 紧接 `skills_cached()` 重扫，
/// 实现"改技能不重启即生效"。
pub(crate) fn invalidate_skills() {
    // 锁中毒:本就是想置 None 清缓存重建,但中毒时持锁线程已 panic,此处无法恢复一致状态。
    // 记录错误并提前返回(下次加载由 skills_lock_async 中毒降级返空,进程重启自愈)。
    let mut g = match RwLock::write(&SKILLS) {
        Ok(g) => g,
        Err(_) => {
            tracing::error!("SKILLS 写锁中毒(invalidate_skills),跳过置 None");
            return;
        }
    };
    *g = None;
}

/// 按 name 读技能正文（剥首个 `---...---` frontmatter 块）。
///
/// 核心设计6:注入用正文，避免 YAML 头噪声污染 system prompt。
/// 缓存未命中返 None；文件读失败返 None。
///
/// P1-260617-3:改 async,缓存懒初始化(可能触发扫盘)走 spawn_blocking 防阻塞 runtime。
///
/// 注:guard(std::sync::RwLockReadGuard 非 Send)必须在 spawn_blocking 的 .await 前 drop,
/// 否则非 Send 跨 await 点致 future 不 Send(MentionResolver 要求 Send)。guard 用 { } 限作用域。
pub(crate) async fn read_skill_content_stripped(name: String) -> Option<String> {
    // 用 skills_cached(扫盘填充 + clone)替代 skills_lock_async(只读):防御 SKILLS 为 None
    // (首次/invalidate 后)时 ? 早返 None 致技能不注入。skills_cached 触发 get_or_init 扫盘确保填充。
    let path: Option<String> = {
        let skills = skills_cached().await;
        skills.iter().find(|s| s.name == name).map(|s| s.path.clone())
    };
    let path = match path {
        Some(p) => p,
        None => {
            tracing::warn!(skill = %name, "[read_skill_content_stripped] 缓存中未找到该技能(name 不匹配/未扫到/user_invocable:false)");
            return None;
        }
    };
    // 单文件读(spawn_blocking 防 Windows fs 慢),细化诊断各失败步便于定位真断点。
    // path move 进 spawn_blocking,先 clone 留诊断用(tracing 借用 move 后值会 E0382)。
    let path_for_log = path.clone();
    let md = match tokio::task::spawn_blocking(move || fs::read_to_string(&path)).await {
        Ok(Ok(content)) => content,
        Ok(Err(e)) => {
            tracing::warn!(skill = %name, path = %path_for_log, error = %e, "[read_skill_content_stripped] 读 SKILL.md 失败(路径/权限/编码)");
            return None;
        }
        Err(e) => {
            tracing::warn!(skill = %name, error = %e, "[read_skill_content_stripped] spawn_blocking join 失败");
            return None;
        }
    };
    Some(strip_frontmatter(&md))
}

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

    #[test]
    fn strip_frontmatter_normal() {
        let md = "---\nname: foo\ndescription: bar\n---\n# Body\ncontent";
        assert_eq!(strip_frontmatter(md), "# Body\ncontent");
    }

    #[test]
    fn strip_frontmatter_no_fm() {
        let md = "# Title\nbody line";
        assert_eq!(strip_frontmatter(md), "# Title\nbody line");
    }

    #[test]
    fn strip_frontmatter_empty() {
        assert_eq!(strip_frontmatter(""), "");
    }

    #[test]
    fn strip_frontmatter_only_fm() {
        // 只有 frontmatter 无正文
        let md = "---\nname: foo\n---\n";
        assert_eq!(strip_frontmatter(md), "");
    }

    #[test]
    fn strip_frontmatter_crlf() {
        let md = "---\r\nname: foo\r\n---\r\nbody\r\n";
        let out = strip_frontmatter(md);
        assert!(out.contains("body"), "CRLF 正文应保留: {:?}", out);
    }

    #[test]
    fn scan_result_dedup_and_conflicts() {
        // 仅测去重逻辑（不依赖 ~/.claude 存在）；通过 parse_skill_file 间接覆盖
        // scan_skills 本身依赖文件系统，此处不集成测
        let _ = ScanResult {
            skills: vec![],
            conflicts: vec![],
        };
    }
}
