use crate::platform::{atomic_rename, home_dir as platform_home_dir, path_to_uri, xml_escape as platform_xml_escape};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};

#[cfg(test)]
static SKILL_TEST_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

#[cfg(test)]
pub(crate) fn acquire_skill_test_env_lock() -> std::sync::MutexGuard<'static, ()> {
    SKILL_TEST_ENV_LOCK.lock().unwrap_or_else(|err| err.into_inner())
}

use crate::config::SkillsConfig;

#[derive(Debug, Clone)]
pub struct Skill {
    pub name: String,
    pub description: String,
    pub body: String,
    pub source: SkillSource,
    pub path: PathBuf,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SkillSource {
    User,
    UserAgent,
    UserOpenclaw,
    Project,
    ProjectAgent,
    ProjectOpenclaw,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SkillScope {
    User,
    Project,
}

impl SkillScope {
    pub fn parse(value: &str) -> Option<Self> {
        match value {
            "user" => Some(Self::User),
            "project" => Some(Self::Project),
            _ => None,
        }
    }

    pub fn as_str(&self) -> &'static str {
        match self {
            Self::User => "user",
            Self::Project => "project",
        }
    }
}

impl From<SkillScope> for SkillSource {
    fn from(value: SkillScope) -> Self {
        match value {
            SkillScope::User => SkillSource::User,
            SkillScope::Project => SkillSource::Project,
        }
    }
}

#[derive(Debug)]
pub struct SkillRuntime {
    config: SkillsConfig,
    catalog: RwLock<SkillCatalog>,
}

#[derive(Debug, Clone)]
pub struct SkillAvailabilityChange {
    pub name: String,
    pub scope: SkillScope,
    pub state_path: PathBuf,
    pub changed: bool,
    pub disabled_in_scopes: Vec<SkillScope>,
    pub available: bool,
}

impl Default for SkillRuntime {
    fn default() -> Self {
        Self {
            config: SkillsConfig::default(),
            catalog: RwLock::new(SkillCatalog::default()),
        }
    }
}

impl SkillRuntime {
    pub fn from_config(config: SkillsConfig) -> Self {
        let catalog = SkillCatalog::discover(&config);
        Self {
            config,
            catalog: RwLock::new(catalog),
        }
    }

    pub fn reload(&self) -> Result<SkillCatalog, String> {
        let catalog = SkillCatalog::discover(&self.config);
        let mut guard = self.catalog.write().expect("skills rwlock poisoned");
        *guard = catalog.clone();
        Ok(catalog)
    }

    pub fn is_empty(&self) -> bool {
        self.catalog
            .read()
            .expect("skills rwlock poisoned")
            .is_empty()
    }

    pub fn len(&self) -> usize {
        self.catalog.read().expect("skills rwlock poisoned").len()
    }

    pub fn system_index_prompt(&self) -> Option<String> {
        self.catalog
            .read()
            .expect("skills rwlock poisoned")
            .system_index_prompt()
    }

    pub fn discovery_event_payload(&self) -> serde_json::Value {
        self.catalog
            .read()
            .expect("skills rwlock poisoned")
            .discovery_event_payload()
    }

    pub fn offered_event_payload(&self) -> serde_json::Value {
        self.catalog
            .read()
            .expect("skills rwlock poisoned")
            .offered_event_payload()
    }

    pub fn activation_payload(&self, name: &str) -> Result<String, String> {
        self.catalog
            .read()
            .expect("skills rwlock poisoned")
            .activation_payload(name)
    }

    pub fn activation_event_payload(&self, name: &str) -> Result<serde_json::Value, String> {
        self.catalog
            .read()
            .expect("skills rwlock poisoned")
            .activation_event_payload(name)
    }

    pub fn list_skills(&self) -> Vec<Skill> {
        self.catalog
            .read()
            .expect("skills rwlock poisoned")
            .skills
            .clone()
    }

    pub fn get_skill(&self, name: &str) -> Option<Skill> {
        self.catalog
            .read()
            .expect("skills rwlock poisoned")
            .find_skill(name)
            .cloned()
    }

    pub fn create_skill(
        &self,
        scope: SkillScope,
        name: &str,
        description: &str,
        body: &str,
        reload: bool,
    ) -> Result<Skill, String> {
        validate_skill_name(name)?;
        let path = skill_file_path(scope, name)?;
        if path.exists() {
            return Err(format!(
                "skill '{}' already exists at {}",
                name,
                path.display()
            ));
        }

        write_skill_file(&path, name, description, body)?;
        let skill = parse_skill_file(&path, scope.into())?;
        if reload {
            let _ = self.reload()?;
        }
        Ok(skill)
    }

    pub fn update_skill(
        &self,
        scope: SkillScope,
        name: &str,
        description: Option<&str>,
        body: Option<&str>,
        reload: bool,
    ) -> Result<Skill, String> {
        validate_skill_name(name)?;
        let path = skill_file_path(scope, name)?;
        if !path.exists() {
            return Err(format!("skill '{}' not found at {}", name, path.display()));
        }

        let existing = parse_skill_file(&path, scope.into())?;
        let next_description = description.unwrap_or(&existing.description);
        let next_body = body.unwrap_or(&existing.body);

        write_skill_file(&path, name, next_description, next_body)?;
        let skill = parse_skill_file(&path, scope.into())?;
        if reload {
            let _ = self.reload()?;
        }
        Ok(skill)
    }

    pub fn delete_skill(
        &self,
        scope: SkillScope,
        name: &str,
        reload: bool,
    ) -> Result<PathBuf, String> {
        validate_skill_name(name)?;
        let dir = skill_dir_path(scope, name)?;
        if !dir.exists() {
            return Err(format!("skill '{}' not found at {}", name, dir.display()));
        }

        fs::remove_dir_all(&dir)
            .map_err(|err| format!("failed to delete skill directory: {}", err))?;
        if reload {
            let _ = self.reload()?;
        }
        Ok(dir)
    }

    pub fn disable_skill(
        &self,
        scope: SkillScope,
        name: &str,
        reload: bool,
    ) -> Result<SkillAvailabilityChange, String> {
        self.set_skill_enabled(scope, name, false, reload)
    }

    pub fn enable_skill(
        &self,
        scope: SkillScope,
        name: &str,
        reload: bool,
    ) -> Result<SkillAvailabilityChange, String> {
        self.set_skill_enabled(scope, name, true, reload)
    }

    pub fn has_skill_definition(&self, name: &str) -> Result<bool, String> {
        validate_skill_name(name)?;
        let cwd = std::env::current_dir()
            .map_err(|err| format!("failed to get current dir: {}", err))?;
        Ok(SkillCatalog::discover_without_state(&self.config, &cwd)
            .find_skill(name)
            .is_some())
    }

    fn set_skill_enabled(
        &self,
        scope: SkillScope,
        name: &str,
        enabled: bool,
        reload: bool,
    ) -> Result<SkillAvailabilityChange, String> {
        validate_skill_name(name)?;
        if !self.has_skill_definition(name)? {
            return Err(format!("skill '{}' not found", name));
        }

        let state_path = skill_state_path(scope)?;
        let mut state = load_skill_state_file(&state_path)?;
        let mut disabled: HashSet<String> = state.disabled_skills.into_iter().collect();
        let changed = if enabled {
            disabled.remove(name)
        } else {
            disabled.insert(name.to_string())
        };

        let mut disabled_skills: Vec<String> = disabled.into_iter().collect();
        disabled_skills.sort();
        state.disabled_skills = disabled_skills;
        save_skill_state_file(&state_path, &state)?;

        if reload {
            let _ = self.reload()?;
        }

        let cwd = std::env::current_dir()
            .map_err(|err| format!("failed to get current dir: {}", err))?;
        let effective_state = load_skill_disable_state(&cwd);
        let disabled_in_scopes = effective_state.disabled_scopes_for(name);

        Ok(SkillAvailabilityChange {
            name: name.to_string(),
            scope,
            state_path,
            changed,
            available: disabled_in_scopes.is_empty(),
            disabled_in_scopes,
        })
    }
}

impl crate::agent::SkillProvider for SkillRuntime {
    fn system_index_prompt(&self) -> Option<String> {
        SkillRuntime::system_index_prompt(self)
    }

    fn matching_skill_summary(&self, name: &str) -> Option<String> {
        self.get_skill(name).map(|skill| skill.description)
    }
}

impl SkillSource {
    fn as_str(&self) -> &'static str {
        match self {
            SkillSource::User => "user",
            SkillSource::UserAgent => "user_agent",
            SkillSource::UserOpenclaw => "user_openclaw",
            SkillSource::Project => "project",
            SkillSource::ProjectAgent => "project_agent",
            SkillSource::ProjectOpenclaw => "project_openclaw",
        }
    }
}

#[derive(Debug, Clone)]
pub struct SkillCatalog {
    skills: Vec<Skill>,
    #[allow(dead_code)]
    max_index_chars: usize,
    #[allow(dead_code)]
    max_listed_skills: usize,
}

impl Default for SkillCatalog {
    fn default() -> Self {
        Self {
            skills: Vec::new(),
            max_index_chars: 4_000,
            max_listed_skills: 32,
        }
    }
}

impl SkillCatalog {
    pub fn discover(config: &SkillsConfig) -> Self {
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let disable_state = load_skill_disable_state(&cwd);
        Self::discover_with_state(config, &cwd, Some(&disable_state))
    }

    fn discover_without_state(config: &SkillsConfig, cwd: &Path) -> Self {
        Self::discover_with_state(config, cwd, None)
    }

    fn discover_with_state(
        config: &SkillsConfig,
        cwd: &Path,
        disable_state: Option<&SkillDisableState>,
    ) -> Self {
        if !config.enabled {
            return Self {
                max_index_chars: config.max_index_chars,
                max_listed_skills: config.max_listed_skills,
                ..Self::default()
            };
        }

        let mut merged: HashMap<String, Skill> = HashMap::new();
        let mut sources_seen = 0usize;

        // Load from least specific to most specific so later sources win on conflicts.
        for source in source_order(&config.sources) {
            sources_seen += 1;
            let root = source_root(source, &cwd);

            let Some(root) = root else { continue };
            for skill in load_skills_from_root(&root, source) {
                if let Some(existing) = merged.get(&skill.name) {
                    tracing::warn!(
                        skill = %skill.name,
                        old_source = %existing.source.as_str(),
                        new_source = %skill.source.as_str(),
                        "Duplicate skill name found; overriding with later source"
                    );
                }
                merged.insert(skill.name.clone(), skill);
            }
        }

        let mut skills: Vec<Skill> = merged.into_values().collect();
        if let Some(disable_state) = disable_state {
            skills.retain(|skill| !disable_state.is_disabled(&skill.name));
        }
        skills.sort_by(|a, b| a.name.cmp(&b.name));

        tracing::info!(
            sources_seen,
            discovered = skills.len(),
            "Skills discovery completed"
        );

        Self {
            skills,
            max_index_chars: config.max_index_chars,
            max_listed_skills: config.max_listed_skills,
        }
    }

    pub fn is_empty(&self) -> bool {
        self.skills.is_empty()
    }

    pub fn len(&self) -> usize {
        self.skills.len()
    }

    pub fn system_index_prompt(&self) -> Option<String> {
        if self.skills.is_empty() {
            return None;
        }

        let mut prompt = String::from(
            "# 技能系统（Skills）\n\n\
            技能是预定义的工作流和指令集合，用于处理特定类型的任务。当任务涉及专业化工作流时，使用技能系统获取详细的执行指导。\n\n\
            ## 何时使用技能\n\n\
            当满足以下条件时，应该使用技能：\n\
            - 当前任务与某个技能的描述相匹配\n\
            - 需要执行特定领域的专业化工作流\n\
            - 任务涉及多步骤操作，且有现成技能可用\n\n\
            ## 如何使用技能\n\n\
            1. **查看可用技能**: 浏览下方的 <available_skills> 列表，了解可用的技能\n\
            2. **匹配任务**: 判断是否有技能的描述与当前任务匹配\n\
            3. **激活技能**: 调用 `skill_activate` 工具，传入技能名称（name 参数）\n\
            4. **执行指令**: 根据 skill_activate 返回的详细说明执行任务\n\n\
            ## 注意事项\n\n\
            - 技能名称不是工具名称，不能直接作为工具调用\n\
            - 必须先调用 skill_activate 获取技能的具体指令，再按照指令执行\n\
            - 一次只能激活一个技能，激活后会返回该技能的完整说明\n\n\
            <available_skills>\n",
        );

        for skill in &self.skills {
            let entry = format!(
                "  <skill>\n    <name>{}</name>\n    <description>{}</description>\n    <location>{}</location>\n  </skill>\n",
                platform_xml_escape(&skill.name),
                platform_xml_escape(&skill.description),
                platform_xml_escape(&path_to_uri(&skill.path)),
            );
            prompt.push_str(&entry);
        }

        prompt.push_str("</available_skills>\n");

        Some(prompt)
    }

    pub fn discovery_event_payload(&self) -> serde_json::Value {
        self.catalog_event_payload()
    }

    pub fn offered_event_payload(&self) -> serde_json::Value {
        self.catalog_event_payload()
    }

    pub fn activation_payload(&self, name: &str) -> Result<String, String> {
        let skill = self
            .find_skill(name)
            .ok_or_else(|| format!("skill '{}' not found", name))?;

        if skill.body.is_empty() {
            return Ok(format!(
                "SKILL LOADED: {}\nDescription: {}\nNo additional body instructions found.",
                skill.name, skill.description
            ));
        }

        Ok(format!(
            "SKILL LOADED: {}\nDescription: {}\nSource: {}\nPath: {}\n\n{}",
            skill.name,
            skill.description,
            skill.source.as_str(),
            skill.path.display(),
            skill.body
        ))
    }

    pub fn activation_event_payload(&self, name: &str) -> Result<serde_json::Value, String> {
        let skill = self
            .find_skill(name)
            .ok_or_else(|| format!("skill '{}' not found", name))?;

        Ok(json!({
            "name": skill.name,
            "description": skill.description,
            "source": skill.source.as_str(),
            "path": skill.path.display().to_string(),
            "body_chars": skill.body.len(),
        }))
    }

    fn find_skill(&self, name: &str) -> Option<&Skill> {
        self.skills.iter().find(|s| s.name == name)
    }

    fn catalog_event_payload(&self) -> serde_json::Value {
        json!({
            "count": self.skills.len(),
            "skills": self.skills.iter().map(|skill| json!({
                "name": skill.name,
                "description": skill.description,
                "source": skill.source.as_str(),
                "path": skill.path.display().to_string(),
            })).collect::<Vec<_>>()
        })
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
struct SkillStateFile {
    #[serde(default)]
    disabled_skills: Vec<String>,
}

#[derive(Debug, Clone, Default)]
struct SkillDisableState {
    user_disabled: HashSet<String>,
    project_disabled: HashSet<String>,
}

impl SkillDisableState {
    fn is_disabled(&self, name: &str) -> bool {
        self.user_disabled.contains(name) || self.project_disabled.contains(name)
    }

    fn disabled_scopes_for(&self, name: &str) -> Vec<SkillScope> {
        let mut scopes = Vec::new();
        if self.user_disabled.contains(name) {
            scopes.push(SkillScope::User);
        }
        if self.project_disabled.contains(name) {
            scopes.push(SkillScope::Project);
        }
        scopes
    }
}

fn source_order(sources: &[String]) -> Vec<SkillSource> {
    let mut result = Vec::new();
    for source in sources {
        match source.as_str() {
            "user" => {
                if !result.contains(&SkillSource::User) {
                    result.push(SkillSource::User);
                }
            }
            "user_agent" => {
                if !result.contains(&SkillSource::UserAgent) {
                    result.push(SkillSource::UserAgent);
                }
            }
            "user_openclaw" => {
                if !result.contains(&SkillSource::UserOpenclaw) {
                    result.push(SkillSource::UserOpenclaw);
                }
            }
            "project" => {
                if !result.contains(&SkillSource::Project) {
                    result.push(SkillSource::Project);
                }
            }
            "project_agent" => {
                if !result.contains(&SkillSource::ProjectAgent) {
                    result.push(SkillSource::ProjectAgent);
                }
            }
            "project_openclaw" => {
                if !result.contains(&SkillSource::ProjectOpenclaw) {
                    result.push(SkillSource::ProjectOpenclaw);
                }
            }
            unknown => {
                tracing::warn!(source = %unknown, "Unknown skills source ignored");
            }
        }
    }

    if result.is_empty() {
        vec![
            SkillSource::User,
            SkillSource::UserAgent,
            SkillSource::UserOpenclaw,
            SkillSource::Project,
            SkillSource::ProjectAgent,
            SkillSource::ProjectOpenclaw,
        ]
    } else {
        result
    }
}

fn validate_skill_name(name: &str) -> Result<(), String> {
    if name.trim().is_empty() {
        return Err("skill name cannot be empty".to_string());
    }
    if name.contains('/') || name.contains('\\') || name.contains("..") {
        return Err("skill name must not contain path separators or '..'".to_string());
    }
    Ok(())
}

pub fn project_skills_root() -> Result<PathBuf, String> {
    let cwd =
        std::env::current_dir().map_err(|err| format!("failed to get current dir: {}", err))?;
    Ok(cwd.join(".picobot").join("skills"))
}

fn project_agent_skills_root(cwd: &Path) -> PathBuf {
    cwd.join(".agents").join("skills")
}

fn project_openclaw_skills_root(cwd: &Path) -> PathBuf {
    cwd.join(".openclaw").join("skills")
}

fn project_skill_state_path(cwd: &Path) -> PathBuf {
    cwd.join(".picobot").join("skill-state.json")
}

fn user_skills_root() -> Option<PathBuf> {
    platform_home_dir().map(|p| p.join(".picobot").join("skills"))
}

fn user_skill_state_path() -> Option<PathBuf> {
    platform_home_dir().map(|p| p.join(".picobot").join("skill-state.json"))
}

fn user_agent_skills_root() -> Option<PathBuf> {
    platform_home_dir().map(|p| p.join(".agents").join("skills"))
}

fn user_openclaw_skills_root() -> Option<PathBuf> {
    platform_home_dir().map(|p| p.join(".openclaw").join("skills"))
}

fn source_root(source: SkillSource, cwd: &Path) -> Option<PathBuf> {
    match source {
        SkillSource::User => user_skills_root(),
        SkillSource::UserAgent => user_agent_skills_root(),
        SkillSource::UserOpenclaw => user_openclaw_skills_root(),
        SkillSource::Project => Some(cwd.join(".picobot").join("skills")),
        SkillSource::ProjectAgent => Some(project_agent_skills_root(cwd)),
        SkillSource::ProjectOpenclaw => Some(project_openclaw_skills_root(cwd)),
    }
}

fn root_for_scope(scope: SkillScope) -> Result<PathBuf, String> {
    match scope {
        SkillScope::User => {
            user_skills_root().ok_or_else(|| "failed to resolve home directory".to_string())
        }
        SkillScope::Project => project_skills_root(),
    }
}

fn skill_dir_path(scope: SkillScope, name: &str) -> Result<PathBuf, String> {
    Ok(root_for_scope(scope)?.join(name))
}

fn skill_file_path(scope: SkillScope, name: &str) -> Result<PathBuf, String> {
    Ok(skill_dir_path(scope, name)?.join("SKILL.md"))
}

fn skill_state_path(scope: SkillScope) -> Result<PathBuf, String> {
    match scope {
        SkillScope::User => user_skill_state_path()
            .ok_or_else(|| "failed to resolve home directory".to_string()),
        SkillScope::Project => {
            let cwd = std::env::current_dir()
                .map_err(|err| format!("failed to get current dir: {}", err))?;
            Ok(project_skill_state_path(&cwd))
        }
    }
}

fn load_skill_disable_state(cwd: &Path) -> SkillDisableState {
    SkillDisableState {
        user_disabled: user_skill_state_path()
            .map(|path| load_disabled_skill_names(&path))
            .unwrap_or_default(),
        project_disabled: load_disabled_skill_names(&project_skill_state_path(cwd)),
    }
}

fn load_disabled_skill_names(path: &Path) -> HashSet<String> {
    match load_skill_state_file(path) {
        Ok(state) => state
            .disabled_skills
            .into_iter()
            .filter_map(|name| normalize_skill_name(name, path))
            .collect(),
        Err(err) => {
            tracing::warn!(path = %path.display(), error = %err, "Failed to load skill state file");
            HashSet::new()
        }
    }
}

fn normalize_skill_name(name: String, path: &Path) -> Option<String> {
    let trimmed = name.trim();
    match validate_skill_name(trimmed) {
        Ok(()) => Some(trimmed.to_string()),
        Err(err) => {
            tracing::warn!(path = %path.display(), skill = %name, error = %err, "Ignoring invalid disabled skill entry");
            None
        }
    }
}

fn load_skill_state_file(path: &Path) -> Result<SkillStateFile, String> {
    if !path.exists() {
        return Ok(SkillStateFile::default());
    }

    let content = fs::read_to_string(path)
        .map_err(|err| format!("failed to read skill state file: {}", err))?;
    serde_json::from_str(&content)
        .map_err(|err| format!("failed to parse skill state file: {}", err))
}

fn save_skill_state_file(path: &Path, state: &SkillStateFile) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|err| format!("failed to create skill state directory: {}", err))?;
    }

    let content = serde_json::to_string_pretty(state)
        .map_err(|err| format!("failed to render skill state file: {}", err))?;
    let tmp_path = path.with_extension("json.tmp");
    fs::write(&tmp_path, format!("{}\n", content))
        .map_err(|err| format!("failed to write temporary skill state file: {}", err))?;

    // 使用平台抽象的原子重命名
    atomic_rename(&tmp_path, path)
        .map_err(|err| format!("failed to persist skill state file: {}", err))
}

fn render_skill_file(name: &str, description: &str, body: &str) -> Result<String, String> {
    if description.trim().is_empty() {
        return Err("description is required and cannot be empty".to_string());
    }

    #[derive(serde::Serialize)]
    struct SkillFrontmatterOwned {
        name: String,
        description: String,
    }

    let yaml = serde_yaml::to_string(&SkillFrontmatterOwned {
        name: name.to_string(),
        description: description.to_string(),
    })
    .map_err(|err| format!("failed to render skill frontmatter: {}", err))?;

    let yaml = yaml.trim_start_matches("---\n");
    let body = body.trim();
    if body.is_empty() {
        Ok(format!("---\n{}---\n", yaml))
    } else {
        Ok(format!("---\n{}---\n{}\n", yaml, body))
    }
}

fn write_skill_file(path: &Path, name: &str, description: &str, body: &str) -> Result<(), String> {
    let content = render_skill_file(name, description, body)?;
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|err| format!("failed to create skill directory: {}", err))?;
    }
    fs::write(path, content).map_err(|err| format!("failed to write skill file: {}", err))
}

fn load_skills_from_root(root: &Path, source: SkillSource) -> Vec<Skill> {
    let mut out = Vec::new();
    if !root.exists() {
        return out;
    }

    let entries = match fs::read_dir(root) {
        Ok(entries) => entries,
        Err(err) => {
            tracing::warn!(path = %root.display(), error = %err, "Failed to read skills directory");
            return out;
        }
    };

    for entry in entries.flatten() {
        let path = entry.path();
        if !path.is_dir() {
            continue;
        }
        let skill_md = path.join("SKILL.md");
        if !skill_md.exists() {
            continue;
        }

        match parse_skill_file(&skill_md, source) {
            Ok(skill) => out.push(skill),
            Err(err) => {
                tracing::warn!(path = %skill_md.display(), error = %err, "Skipping invalid skill file");
            }
        }
    }

    out
}

#[derive(Debug, Deserialize)]
struct SkillFrontmatter {
    description: String,
    #[serde(default)]
    name: Option<String>,
}

fn parse_skill_file(path: &Path, source: SkillSource) -> Result<Skill, String> {
    let content = fs::read_to_string(path).map_err(|e| format!("failed to read file: {}", e))?;

    let (frontmatter_raw, body) =
        split_frontmatter(&content).ok_or_else(|| "missing YAML frontmatter block".to_string())?;

    let frontmatter: SkillFrontmatter = serde_yaml::from_str(frontmatter_raw)
        .map_err(|e| format!("invalid YAML frontmatter: {}", e))?;

    let description = frontmatter.description.trim();
    if description.is_empty() {
        return Err("description is required and cannot be empty".to_string());
    }

    let dir_name = path
        .parent()
        .and_then(|p| p.file_name())
        .map(|s| s.to_string_lossy().to_string())
        .unwrap_or_else(|| "unknown-skill".to_string());

    let name = frontmatter.name.unwrap_or(dir_name).trim().to_string();

    Ok(Skill {
        name,
        description: description.to_string(),
        body: body.trim().to_string(),
        source,
        path: path.to_path_buf(),
    })
}

fn split_frontmatter(content: &str) -> Option<(&str, &str)> {
    let rest = content.strip_prefix("---\n")?;
    let marker = "\n---\n";
    let idx = rest.find(marker)?;
    let frontmatter = &rest[..idx];
    let body = &rest[idx + marker.len()..];
    Some((frontmatter, body))
}

// 使用 platform 模块提供的 xml_escape 和 path_to_uri 函数

// SkillPromptProvider 实现
use crate::agent::{SystemPrompt, SystemPromptContext, SystemPromptProvider};

/// Skill 提示词提供者
///
/// 负责提供技能的系统索引提示词（system_index_prompt）。
pub struct SkillPromptProvider {
    skills: Arc<SkillRuntime>,
}

impl SkillPromptProvider {
    /// 创建新的 Skill 提示词提供者
    pub fn new(skills: Arc<SkillRuntime>) -> Self {
        Self { skills }
    }
}

impl SystemPromptProvider for SkillPromptProvider {
    fn build(&self, _context: &SystemPromptContext) -> Option<SystemPrompt> {
        // 调用 SkillRuntime 的 system_index_prompt 方法
        self.skills.system_index_prompt().map(|content| SystemPrompt {
            content,
            context: Some("skill_index".to_string()),
        })
    }
}

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

    struct CurrentDirGuard {
        previous: PathBuf,
    }

    struct HomeDirGuard {
        previous: Option<OsString>,
        previous_userprofile: Option<OsString>,
    }

    impl CurrentDirGuard {
        fn enter(path: &Path) -> Self {
            let previous = std::env::current_dir().unwrap();
            std::env::set_current_dir(path).unwrap();
            Self { previous }
        }
    }

    impl Drop for CurrentDirGuard {
        fn drop(&mut self) {
            let _ = std::env::set_current_dir(&self.previous);
        }
    }

    impl HomeDirGuard {
        fn enter(path: &Path) -> Self {
            let home_backup = std::env::var_os("HOME");
            let userprofile_backup = std::env::var_os("USERPROFILE");

            unsafe {
                std::env::set_var("HOME", path);
                // Windows 环境下同时设置 USERPROFILE
                std::env::set_var("USERPROFILE", path);
            }

            Self {
                previous: home_backup,
                previous_userprofile: userprofile_backup,
            }
        }
    }

    impl Drop for HomeDirGuard {
        fn drop(&mut self) {
            unsafe {
                match &self.previous {
                    Some(value) => std::env::set_var("HOME", value),
                    None => std::env::remove_var("HOME"),
                }
                match &self.previous_userprofile {
                    Some(value) => std::env::set_var("USERPROFILE", value),
                    None => std::env::remove_var("USERPROFILE"),
                }
            }
        }
    }

    fn acquire_test_lock() -> std::sync::MutexGuard<'static, ()> {
        acquire_skill_test_env_lock()
    }

    #[test]
    fn test_split_frontmatter() {
        let input = "---\ndescription: demo\n---\nhello";
        let (fm, body) = split_frontmatter(input).unwrap();
        assert!(fm.contains("description"));
        assert_eq!(body, "hello");
    }

    #[test]
    fn test_parse_skill_file_requires_description() {
        let dir = tempfile::tempdir().unwrap();
        let skill_dir = dir.path().join("demo");
        fs::create_dir_all(&skill_dir).unwrap();
        let skill_md = skill_dir.join("SKILL.md");
        fs::write(&skill_md, "---\nname: demo\n---\ncontent").unwrap();

        let err = parse_skill_file(&skill_md, SkillSource::Project).unwrap_err();
        assert!(err.contains("description"));
    }

    #[test]
    fn test_activation_payload_contains_body() {
        let dir = tempfile::tempdir().unwrap();
        let skill_dir = dir.path().join("demo");
        fs::create_dir_all(&skill_dir).unwrap();
        let skill_md = skill_dir.join("SKILL.md");
        fs::write(
            &skill_md,
            "---\nname: demo\ndescription: demo skill\n---\nStep A\nStep B",
        )
        .unwrap();

        let skill = parse_skill_file(&skill_md, SkillSource::Project).unwrap();
        let catalog = SkillCatalog {
            skills: vec![skill],
            max_index_chars: 1000,
            max_listed_skills: 10,
        };

        let payload = catalog.activation_payload("demo").unwrap();
        assert!(payload.contains("SKILL LOADED: demo"));
        assert!(payload.contains("Step A"));
    }

    #[test]
    fn test_system_index_prompt_uses_available_skills_markup() {
        // 使用临时目录创建测试路径，确保跨平台兼容
        let temp_dir = tempfile::tempdir().unwrap();
        let skill_path = temp_dir.path().join("demo-skill").join("SKILL.md");

        let catalog = SkillCatalog {
            skills: vec![Skill {
                name: "demo-skill".to_string(),
                description: "demo <skill> & usage".to_string(),
                body: String::new(),
                source: SkillSource::Project,
                path: skill_path.clone(),
            }],
            max_index_chars: 4000,
            max_listed_skills: 32,
        };

        let prompt = catalog.system_index_prompt().unwrap();
        assert!(prompt.contains("<available_skills>"));
        assert!(prompt.contains("技能是预定义的工作流和指令集合，用于处理特定类型的任务。"));
        assert!(prompt.contains("<name>demo-skill</name>"));
        assert!(prompt.contains("<description>demo &lt;skill&gt; &amp; usage</description>"));

        // 验证 location 包含正确的 file:// URI 格式
        let expected_uri = path_to_uri(&skill_path);
        assert!(prompt.contains(&format!("<location>{}</location>", platform_xml_escape(&expected_uri))));
        assert!(prompt.contains("</available_skills>"));
    }

    #[test]
    fn test_path_to_uri() {
        // Unix 路径
        let unix_path = PathBuf::from("/tmp/demo-skill/SKILL.md");
        let unix_uri = path_to_uri(&unix_path);
        if cfg!(target_os = "windows") {
            // Windows 上运行时，路径可能被转换
            assert!(unix_uri.contains("file://"));
        } else {
            assert_eq!(unix_uri, "file:///tmp/demo-skill/SKILL.md");
        }

        // Windows 路径格式测试（仅在 Windows 上）
        if cfg!(target_os = "windows") {
            let win_path = PathBuf::from("C:\\Users\\test\\.picobot\\skills\\demo\\SKILL.md");
            let win_uri = path_to_uri(&win_path);
            assert!(win_uri.starts_with("file:///C:/"));
            assert!(win_uri.contains("/SKILL.md"));
            assert!(!win_uri.contains('\\')); // 不应包含反斜杠
        }
    }

    #[test]
    fn test_runtime_create_update_delete_reload() {
        let _lock = acquire_test_lock();
        let temp_dir = tempfile::tempdir().unwrap();
        let home_dir = temp_dir.path().join("home");
        let project_dir = temp_dir.path().join("project");
        fs::create_dir_all(&home_dir).unwrap();
        fs::create_dir_all(&project_dir).unwrap();
        let _home = HomeDirGuard::enter(&home_dir);
        let _guard = CurrentDirGuard::enter(&project_dir);

        let runtime = SkillRuntime::from_config(SkillsConfig {
            enabled: true,
            sources: vec!["project".to_string()],
            max_index_chars: 4000,
            max_listed_skills: 32,
        });

        assert_eq!(runtime.len(), 0);

        let created = runtime
            .create_skill(
                SkillScope::Project,
                "demo-skill",
                "demo desc",
                "line 1",
                true,
            )
            .unwrap();
        assert_eq!(created.name, "demo-skill");
        assert_eq!(runtime.len(), 1);

        let updated = runtime
            .update_skill(
                SkillScope::Project,
                "demo-skill",
                Some("updated desc"),
                Some("line 2"),
                true,
            )
            .unwrap();
        assert_eq!(updated.description, "updated desc");
        assert!(
            runtime
                .activation_payload("demo-skill")
                .unwrap()
                .contains("line 2")
        );

        let deleted_path = runtime
            .delete_skill(SkillScope::Project, "demo-skill", true)
            .unwrap();
        assert!(!deleted_path.exists());
        assert_eq!(runtime.len(), 0);
    }

    #[test]
    fn test_source_order_supports_agent_sources() {
        let ordered = source_order(&[
            "user".to_string(),
            "user_agent".to_string(),
            "user_openclaw".to_string(),
            "project".to_string(),
            "project_agent".to_string(),
            "project_openclaw".to_string(),
            "project".to_string(),
            "unknown".to_string(),
        ]);

        assert_eq!(
            ordered,
            vec![
                SkillSource::User,
                SkillSource::UserAgent,
                SkillSource::UserOpenclaw,
                SkillSource::Project,
                SkillSource::ProjectAgent,
                SkillSource::ProjectOpenclaw,
            ]
        );
    }

    #[test]
    fn test_discover_loads_project_agent_skills() {
        let _lock = acquire_test_lock();
        let temp_dir = tempfile::tempdir().unwrap();
        let home_dir = temp_dir.path().join("home");
        let project_dir = temp_dir.path().join("project");
        fs::create_dir_all(&home_dir).unwrap();
        fs::create_dir_all(&project_dir).unwrap();
        let _home = HomeDirGuard::enter(&home_dir);
        let _guard = CurrentDirGuard::enter(&project_dir);

        let agent_skill_dir = project_dir
            .join(".agents")
            .join("skills")
            .join("demo-agent");
        fs::create_dir_all(&agent_skill_dir).unwrap();
        fs::write(
            agent_skill_dir.join("SKILL.md"),
            "---\ndescription: agent skill\n---\nUse agent",
        )
        .unwrap();

        let catalog = SkillCatalog::discover(&SkillsConfig {
            enabled: true,
            sources: vec!["project_agent".to_string()],
            max_index_chars: 4000,
            max_listed_skills: 32,
        });

        assert_eq!(catalog.len(), 1);
        let payload = catalog.activation_event_payload("demo-agent").unwrap();
        assert_eq!(payload["source"], "project_agent");
    }

    #[test]
    fn test_discover_prefers_project_agent_on_conflict() {
        let _lock = acquire_test_lock();
        let temp_dir = tempfile::tempdir().unwrap();
        let home_dir = temp_dir.path().join("home");
        let project_dir = temp_dir.path().join("project");
        fs::create_dir_all(&home_dir).unwrap();
        fs::create_dir_all(&project_dir).unwrap();
        let _home = HomeDirGuard::enter(&home_dir);
        let _guard = CurrentDirGuard::enter(&project_dir);

        let project_skill_dir = project_dir.join(".picobot").join("skills").join("demo");
        fs::create_dir_all(&project_skill_dir).unwrap();
        fs::write(
            project_skill_dir.join("SKILL.md"),
            "---\ndescription: project version\n---\nProject body",
        )
        .unwrap();

        let agent_skill_dir = project_dir.join(".agents").join("skills").join("demo");
        fs::create_dir_all(&agent_skill_dir).unwrap();
        fs::write(
            agent_skill_dir.join("SKILL.md"),
            "---\ndescription: agent version\n---\nAgent body",
        )
        .unwrap();

        let catalog = SkillCatalog::discover(&SkillsConfig {
            enabled: true,
            sources: vec!["project".to_string(), "project_agent".to_string()],
            max_index_chars: 4000,
            max_listed_skills: 32,
        });

        let payload = catalog.activation_payload("demo").unwrap();
        assert!(payload.contains("Source: project_agent"));
        assert!(payload.contains("Agent body"));
    }

    #[test]
    fn test_skill_state_file_roundtrip() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("skill-state.json");
        let state = SkillStateFile {
            disabled_skills: vec!["demo".to_string(), "other".to_string()],
        };

        save_skill_state_file(&path, &state).unwrap();

        let loaded = load_skill_state_file(&path).unwrap();
        assert_eq!(loaded, state);
    }

    #[test]
    fn test_discover_filters_disabled_skills_from_sidecar() {
        let _lock = acquire_test_lock();
        let temp_dir = tempfile::tempdir().unwrap();
        let home_dir = temp_dir.path().join("home");
        let project_dir = temp_dir.path().join("project");
        fs::create_dir_all(&home_dir).unwrap();
        fs::create_dir_all(&project_dir).unwrap();
        let _home = HomeDirGuard::enter(&home_dir);
        let _guard = CurrentDirGuard::enter(&project_dir);

        let project_skill_dir = project_dir.join(".picobot").join("skills").join("demo");
        fs::create_dir_all(&project_skill_dir).unwrap();
        fs::write(
            project_skill_dir.join("SKILL.md"),
            "---\ndescription: project version\n---\nProject body",
        )
        .unwrap();

        save_skill_state_file(
            &project_dir.join(".picobot").join("skill-state.json"),
            &SkillStateFile {
                disabled_skills: vec!["demo".to_string()],
            },
        )
        .unwrap();

        let catalog = SkillCatalog::discover(&SkillsConfig {
            enabled: true,
            sources: vec!["project".to_string()],
            max_index_chars: 4000,
            max_listed_skills: 32,
        });

        assert_eq!(catalog.len(), 0);
        assert!(catalog.activation_payload("demo").is_err());
    }

    #[test]
    fn test_runtime_disable_and_enable_skill_updates_visibility() {
        let _lock = acquire_test_lock();
        let temp_dir = tempfile::tempdir().unwrap();
        let home_dir = temp_dir.path().join("home");
        let project_dir = temp_dir.path().join("project");
        fs::create_dir_all(&home_dir).unwrap();
        fs::create_dir_all(&project_dir).unwrap();
        let _home = HomeDirGuard::enter(&home_dir);
        let _guard = CurrentDirGuard::enter(&project_dir);

        let project_skill_dir = project_dir.join(".picobot").join("skills").join("demo");
        fs::create_dir_all(&project_skill_dir).unwrap();
        fs::write(
            project_skill_dir.join("SKILL.md"),
            "---\ndescription: project version\n---\nProject body",
        )
        .unwrap();

        let runtime = SkillRuntime::from_config(SkillsConfig {
            enabled: true,
            sources: vec!["project".to_string()],
            max_index_chars: 4000,
            max_listed_skills: 32,
        });

        let disabled = runtime.disable_skill(SkillScope::Project, "demo", true).unwrap();
        assert!(disabled.changed);
        assert_eq!(disabled.disabled_in_scopes, vec![SkillScope::Project]);
        assert!(!disabled.available);
        assert!(runtime.get_skill("demo").is_none());

        let enabled = runtime.enable_skill(SkillScope::Project, "demo", true).unwrap();
        assert!(enabled.changed);
        assert!(enabled.disabled_in_scopes.is_empty());
        assert!(enabled.available);
        assert!(runtime.get_skill("demo").is_some());
    }

    #[test]
    fn test_user_scope_disable_overrides_project_scope_enable() {
        let _lock = acquire_test_lock();
        let temp_dir = tempfile::tempdir().unwrap();
        let home_dir = temp_dir.path().join("home");
        let project_dir = temp_dir.path().join("project");
        fs::create_dir_all(&home_dir).unwrap();
        fs::create_dir_all(&project_dir).unwrap();
        let _home = HomeDirGuard::enter(&home_dir);
        let _guard = CurrentDirGuard::enter(&project_dir);

        let project_skill_dir = project_dir.join(".picobot").join("skills").join("demo");
        fs::create_dir_all(&project_skill_dir).unwrap();
        fs::write(
            project_skill_dir.join("SKILL.md"),
            "---\ndescription: project version\n---\nProject body",
        )
        .unwrap();

        let runtime = SkillRuntime::from_config(SkillsConfig {
            enabled: true,
            sources: vec!["project".to_string()],
            max_index_chars: 4000,
            max_listed_skills: 32,
        });

        let user_disabled = runtime.disable_skill(SkillScope::User, "demo", true).unwrap();
        assert_eq!(user_disabled.disabled_in_scopes, vec![SkillScope::User]);
        assert!(runtime.get_skill("demo").is_none());

        let project_enabled = runtime.enable_skill(SkillScope::Project, "demo", true).unwrap();
        assert!(!project_enabled.available);
        assert_eq!(project_enabled.disabled_in_scopes, vec![SkillScope::User]);
        assert!(runtime.get_skill("demo").is_none());

        let user_enabled = runtime.enable_skill(SkillScope::User, "demo", true).unwrap();
        assert!(user_enabled.available);
        assert!(user_enabled.disabled_in_scopes.is_empty());
        assert!(runtime.get_skill("demo").is_some());
    }

    #[test]
    fn test_discover_loads_project_openclaw_skills() {
        let _lock = acquire_test_lock();
        let temp_dir = tempfile::tempdir().unwrap();
        let home_dir = temp_dir.path().join("home");
        let project_dir = temp_dir.path().join("project");
        fs::create_dir_all(&home_dir).unwrap();
        fs::create_dir_all(&project_dir).unwrap();
        let _home = HomeDirGuard::enter(&home_dir);
        let _guard = CurrentDirGuard::enter(&project_dir);

        let openclaw_skill_dir = project_dir
            .join(".openclaw")
            .join("skills")
            .join("demo-openclaw");
        fs::create_dir_all(&openclaw_skill_dir).unwrap();
        fs::write(
            openclaw_skill_dir.join("SKILL.md"),
            "---\ndescription: openclaw skill\n---\nUse openclaw",
        )
        .unwrap();

        let catalog = SkillCatalog::discover(&SkillsConfig {
            enabled: true,
            sources: vec!["project_openclaw".to_string()],
            max_index_chars: 4000,
            max_listed_skills: 32,
        });

        assert_eq!(catalog.len(), 1);
        let payload = catalog.activation_event_payload("demo-openclaw").unwrap();
        assert_eq!(payload["source"], "project_openclaw");
    }

    #[test]
    fn test_discover_loads_user_openclaw_skills() {
        let _lock = acquire_test_lock();
        let temp_dir = tempfile::tempdir().unwrap();
        let home_dir = temp_dir.path().join("home");
        let project_dir = temp_dir.path().join("project");
        fs::create_dir_all(&home_dir).unwrap();
        fs::create_dir_all(&project_dir).unwrap();
        let _home = HomeDirGuard::enter(&home_dir);
        let _guard = CurrentDirGuard::enter(&project_dir);

        let user_openclaw_skill_dir = home_dir
            .join(".openclaw")
            .join("skills")
            .join("demo-user-openclaw");
        fs::create_dir_all(&user_openclaw_skill_dir).unwrap();
        fs::write(
            user_openclaw_skill_dir.join("SKILL.md"),
            "---\ndescription: user openclaw skill\n---\nUse user openclaw",
        )
        .unwrap();

        let catalog = SkillCatalog::discover(&SkillsConfig {
            enabled: true,
            sources: vec!["user_openclaw".to_string()],
            max_index_chars: 4000,
            max_listed_skills: 32,
        });

        assert_eq!(catalog.len(), 1);
        let payload = catalog.activation_event_payload("demo-user-openclaw").unwrap();
        assert_eq!(payload["source"], "user_openclaw");
    }
}
