pub mod builtin;

mod embedded {
    include!(concat!(env!("OUT_DIR"), "/embedded_skills.rs"));
}

use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;

/// Skill definition
#[derive(Debug, Clone)]
pub struct Skill {
    pub name: String,
    pub description: String,
    pub content: String,
    pub always: bool,
    pub path: Option<PathBuf>,
}

#[derive(Default)]
struct SkillMarkdownMeta {
    name: Option<String>,
    description: Option<String>,
    always: Option<bool>,
}

#[derive(Clone)]
struct SkillsState {
    loaded_skills: Vec<Skill>,
    /// Skill names explicitly disabled by the user; everything else is
    /// enabled by default.
    disabled_skills: HashSet<String>,
    last_picobot_mtime: Option<SystemTime>,
    last_agent_mtime: Option<SystemTime>,
    last_workspace_mtime: Option<SystemTime>,
    last_state_mtime: Option<SystemTime>,
    last_load_time: SystemTime,
}

impl Default for SkillsState {
    fn default() -> Self {
        Self {
            loaded_skills: Vec::new(),
            disabled_skills: HashSet::new(),
            last_picobot_mtime: None,
            last_agent_mtime: None,
            last_workspace_mtime: None,
            last_state_mtime: None,
            last_load_time: SystemTime::now(),
        }
    }
}

/// Skills loader - loads skills from multiple directories
#[derive(Clone)]
pub struct SkillsLoader {
    picobot_skills_dir: PathBuf,
    agent_skills_dir: PathBuf,
    workspace_skills_dir: Option<PathBuf>,
    /// Path of the JSON state file recording user-disabled skills
    /// (`<config_dir>/skills_state.json`).
    state_path: PathBuf,
    state: Arc<Mutex<SkillsState>>,
}

impl SkillsLoader {
    /// Create a new loader with default paths
    pub fn new() -> Self {
        let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
        let picobot_skills_dir = home.join(".picobot/skills");
        let state_path = picobot_skills_dir
            .parent()
            .unwrap_or(home.as_path())
            .join("skills_state.json");

        builtin::install_builtin_skills(&picobot_skills_dir);

        Self {
            picobot_skills_dir,
            agent_skills_dir: home.join(".agents/skills"),
            workspace_skills_dir: None,
            state_path,
            state: Arc::new(Mutex::new(SkillsState::default())),
        }
    }

    #[cfg(test)]
    pub(crate) fn new_for_testing(picobot_dir: PathBuf, agent_dir: PathBuf) -> Self {
        let state_path = picobot_dir
            .parent()
            .unwrap_or(picobot_dir.as_path())
            .join("skills_state.json");
        Self {
            picobot_skills_dir: picobot_dir,
            agent_skills_dir: agent_dir,
            workspace_skills_dir: None,
            state_path,
            state: Arc::new(Mutex::new(SkillsState::default())),
        }
    }

    /// Set the workspace skills directory (./skills under workspace root)
    pub fn set_workspace_skills_dir(&mut self, workspace_path: PathBuf) {
        self.workspace_skills_dir = Some(workspace_path.join("skills"));
    }

    /// Load all skills from all directories and record modification times.
    /// Priority: workspace > ~/.picobot/skills > ~/.agents/skills.
    /// Same-name skills from higher-priority directories replace lower-priority ones.
    /// User-disabled skills (see `set_enabled`) are excluded from the result.
    pub fn load_skills(&self) {
        self.load_state();
        let mut state = self.state.lock().unwrap();
        state.loaded_skills.clear();

        // Ensure ~/.picobot/skills directory exists
        if !self.picobot_skills_dir.exists() {
            if let Err(e) = std::fs::create_dir_all(&self.picobot_skills_dir) {
                tracing::warn!(dir = %self.picobot_skills_dir.display(), error = %e, "Failed to create skills directory");
            } else {
                tracing::info!(dir = %self.picobot_skills_dir.display(), "Created skills directory");
            }
        }

        // Load from ~/.agents/skills (lowest priority)
        if self.agent_skills_dir.exists() {
            let loaded = self.load_skills_from_dir(&self.agent_skills_dir);
            tracing::debug!(
                dir = %self.agent_skills_dir.display(),
                count = loaded.len(),
                "Loaded skills from agent directory"
            );
            state.loaded_skills.extend(loaded);
            state.last_agent_mtime = Self::get_dir_mtime(&self.agent_skills_dir);
        }

        // Load from ~/.picobot/skills (medium priority) — replace same-name skills
        if self.picobot_skills_dir.exists() {
            let loaded = self.load_skills_from_dir(&self.picobot_skills_dir);
            let count = loaded.len();
            let mut replaced = 0usize;
            for skill in loaded {
                if let Some(existing) = state
                    .loaded_skills
                    .iter_mut()
                    .find(|s| s.name == skill.name)
                {
                    *existing = skill;
                    replaced += 1;
                } else {
                    state.loaded_skills.push(skill);
                }
            }
            tracing::debug!(
                dir = %self.picobot_skills_dir.display(),
                count = count,
                replaced = replaced,
                "Loaded skills from picobot directory"
            );
            state.last_picobot_mtime = Self::get_dir_mtime(&self.picobot_skills_dir);
        }

        // Load from workspace skills dir (highest priority) — replace same-name skills
        if let Some(ref ws_dir) = self.workspace_skills_dir
            && ws_dir.exists()
        {
            let loaded = self.load_skills_from_dir(ws_dir);
            let count = loaded.len();
            let mut replaced = 0usize;
            for skill in loaded {
                if let Some(existing) = state
                    .loaded_skills
                    .iter_mut()
                    .find(|s| s.name == skill.name)
                {
                    *existing = skill;
                    replaced += 1;
                } else {
                    state.loaded_skills.push(skill);
                }
            }
            tracing::debug!(
                dir = %ws_dir.display(),
                count = count,
                replaced = replaced,
                "Loaded skills from workspace directory"
            );
            state.last_workspace_mtime = Self::get_dir_mtime(ws_dir);
        }

        state.last_load_time = SystemTime::now();

        let disabled_skills = state.disabled_skills.clone();
        state
            .loaded_skills
            .retain(|skill| !disabled_skills.contains(&skill.name));

        if state.loaded_skills.is_empty() {
            tracing::debug!("No skills found in any skills directory");
        } else {
            tracing::info!(
                count = state.loaded_skills.len(),
                "Loaded {} skills total",
                state.loaded_skills.len()
            );
        }
    }

    /// Check if skills directories have been modified since last load
    fn has_changed(&self) -> bool {
        let state = self.state.lock().unwrap();
        let picobot_changed = if self.picobot_skills_dir.exists() {
            let current_mtime = Self::get_dir_mtime(&self.picobot_skills_dir);
            current_mtime != state.last_picobot_mtime
        } else {
            false
        };

        let agent_changed = if self.agent_skills_dir.exists() {
            let current_mtime = Self::get_dir_mtime(&self.agent_skills_dir);
            current_mtime != state.last_agent_mtime
        } else {
            false
        };

        let workspace_changed = if let Some(ref ws_dir) = self.workspace_skills_dir {
            if ws_dir.exists() {
                let current_mtime = Self::get_dir_mtime(ws_dir);
                current_mtime != state.last_workspace_mtime
            } else {
                false
            }
        } else {
            false
        };

        let state_changed = Self::get_file_mtime(&self.state_path) != state.last_state_mtime;

        picobot_changed || agent_changed || workspace_changed || state_changed
    }

    /// Reload skills if changes are detected
    pub fn reload_if_changed(&self) -> bool {
        if self.has_changed() {
            tracing::info!("Skills directories changed, reloading...");
            self.load_skills();
            true
        } else {
            false
        }
    }

    /// Get the latest modification time of a directory or any of its children
    fn get_dir_mtime(dir: &Path) -> Option<SystemTime> {
        let mut max_mtime = None;

        if let Ok(metadata) = std::fs::metadata(dir)
            && let Ok(mtime) = metadata.modified()
        {
            max_mtime = Some(mtime);
        }

        if let Ok(entries) = std::fs::read_dir(dir) {
            for entry in entries.flatten() {
                let path = entry.path();
                if let Ok(metadata) = std::fs::metadata(&path)
                    && let Ok(mtime) = metadata.modified()
                    && max_mtime.is_none_or(|current| mtime > current)
                {
                    max_mtime = Some(mtime);
                }
            }
        }

        max_mtime
    }

    /// Get the modification time of a single file (missing file -> None).
    fn get_file_mtime(path: &Path) -> Option<SystemTime> {
        std::fs::metadata(path)
            .and_then(|metadata| metadata.modified())
            .ok()
    }

    /// Read the disabled-skills state file into `state.disabled_skills`.
    /// A missing or malformed file resets to "everything enabled".
    fn load_state(&self) {
        let mut state = self.state.lock().unwrap();
        match std::fs::read_to_string(&self.state_path) {
            Ok(content) => {
                let mut disabled = HashSet::new();
                if let Ok(value) = serde_json::from_str::<serde_json::Value>(&content)
                    && let Some(items) = value.get("disabled").and_then(|v| v.as_array())
                {
                    disabled = items
                        .iter()
                        .filter_map(serde_json::Value::as_str)
                        .map(str::to_string)
                        .collect();
                }
                state.disabled_skills = disabled;
            }
            Err(_) => {
                state.disabled_skills.clear();
            }
        }
        state.last_state_mtime = Self::get_file_mtime(&self.state_path);
    }

    /// Persist the disabled-skills state atomically.
    fn save_state(&self) -> Result<(), String> {
        let mut disabled: Vec<String> = {
            let state = self.state.lock().unwrap();
            state.disabled_skills.iter().cloned().collect()
        };
        disabled.sort();
        let content = serde_json::json!({ "disabled": disabled }).to_string();

        let parent = self.state_path.parent().unwrap_or_else(|| Path::new("."));
        std::fs::create_dir_all(parent).map_err(|e| format!("create state dir: {e}"))?;
        let temp = parent.join(".skills_state.json.tmp");
        std::fs::write(&temp, &content).map_err(|e| format!("write state: {e}"))?;
        std::fs::rename(&temp, &self.state_path).map_err(|e| format!("rename state: {e}"))
    }

    /// Whether a skill is enabled. Skills are enabled by default; only names
    /// explicitly disabled by the user return false.
    pub fn is_enabled(&self, name: &str) -> bool {
        let state = self.state.lock().unwrap();
        !state.disabled_skills.contains(name)
    }

    /// Enable or disable a skill and persist the change. Disabled skills are
    /// excluded from prompts, listings, and `get_skill`.
    pub fn set_enabled(&self, name: &str, enabled: bool) -> Result<(), String> {
        {
            let mut state = self.state.lock().unwrap();
            if enabled {
                state.disabled_skills.remove(name);
            } else {
                state.disabled_skills.insert(name.to_string());
            }
        }
        self.save_state()?;
        self.load_skills();
        Ok(())
    }

    pub fn source_of(&self, path: Option<&Path>) -> &'static str {
        let Some(path) = path else {
            return "unknown";
        };
        if let Some(ws) = &self.workspace_skills_dir
            && path.starts_with(ws)
        {
            return "workspace";
        }
        if path.starts_with(&self.picobot_skills_dir) {
            return "picobot";
        }
        if path.starts_with(&self.agent_skills_dir) {
            return "agent";
        }
        "other"
    }

    /// Get a copy of loaded skills (checks for changes first)
    pub fn get_loaded_skills(&self) -> Vec<Skill> {
        self.reload_if_changed();
        let state = self.state.lock().unwrap();
        Self::sort_skills(state.loaded_skills.clone())
    }

    /// Get skills marked as always (checks for changes first)
    pub fn get_always_skills(&self) -> Vec<Skill> {
        self.reload_if_changed();
        let state = self.state.lock().unwrap();
        Self::sort_skills(
            state
                .loaded_skills
                .iter()
                .filter(|s| s.always)
                .cloned()
                .collect(),
        )
    }

    /// Get a specific skill by name (checks for changes first)
    pub fn get_skill(&self, name: &str) -> Option<Skill> {
        self.reload_if_changed();
        let state = self.state.lock().unwrap();
        state.loaded_skills.iter().find(|s| s.name == name).cloned()
    }

    /// List all skills (name + description) (checks for changes first)
    pub fn list_skills(&self) -> Vec<(String, String)> {
        self.reload_if_changed();
        let state = self.state.lock().unwrap();
        Self::sort_skills(state.loaded_skills.clone())
            .into_iter()
            .map(|s| (s.name.clone(), s.description.clone()))
            .collect()
    }

    /// Build full skills prompt: directory conventions, always-skill summary, always-skill content
    pub fn build_skills_prompt(&self) -> String {
        self.reload_if_changed();
        let state = self.state.lock().unwrap();

        if state.loaded_skills.is_empty() {
            return String::new();
        }

        let loaded_skills = Self::sort_skills(state.loaded_skills.clone());
        let mut prompt = String::from("## Skills\n\n");

        // Directory conventions
        prompt.push_str("### 目录说明\n\n");
        prompt.push_str("- `~/.agents/skills/` — 外部共享 skill 目录（第三方、系统级 skill）\n");
        prompt.push_str("- `~/.picobot/skills/` — 安装 skill 的默认目录\n");
        prompt.push_str(
            "- `{workspace}/skills/` — 工作目录下的 skill，picobot 自行创建的 skill 存放于此\n\n",
        );
        prompt.push_str(
            "安装或创建 skill 时请按上述目录规范存放，创建skill时不要和已有skill同名。\n\n",
        );

        // Always skills summary
        let always_skills: Vec<_> = loaded_skills.iter().filter(|s| s.always).collect();
        if !always_skills.is_empty() {
            prompt.push_str("### 常用技能\n\n");
            for skill in &always_skills {
                let path_str = skill
                    .path
                    .as_ref()
                    .map(|p| p.to_string_lossy().to_string())
                    .unwrap_or_else(|| "—".to_string());
                prompt.push_str(&format!(
                    "- **{}**: {}  [路径: `{}`]\n",
                    skill.name, skill.description, path_str
                ));
            }
            prompt.push('\n');
        }

        // Usage instructions
        prompt.push_str("### 使用方法\n\n");
        prompt.push_str(
            "- 使用 `get_skill` 工具 action=\"list\" 列出所有可用 skill 及其名称、简介、路径\n",
        );
        prompt.push_str(
            "- 使用 `get_skill` 工具 action=\"get\" 并提供 `skill_name` 获取指定 skill 完整内容\n",
        );
        prompt.push_str("- 当用户询问已安装的 skill 相关信息时，需重新调用 get_skill 工具查询最新内容，避免 skill 已变更导致信息过时\n");

        // Always skills full content
        if !always_skills.is_empty() {
            prompt.push_str("\n---\n\n");
            let mut parts = Vec::new();
            for skill in &always_skills {
                parts.push(format!("## Skill: {}\n\n{}", skill.name, skill.content));
            }
            prompt.push_str(&parts.join("\n\n---\n\n"));
        }

        prompt
    }

    /// Build the minimal prompt exposed to a named Agent. The Agent can only
    /// discover the allowlisted skills through its scoped get_skill tool.
    pub fn build_scoped_skills_prompt(&self, allowed: &[String]) -> String {
        if allowed.is_empty() {
            return String::new();
        }
        let allowed: std::collections::HashSet<_> = allowed.iter().map(String::as_str).collect();
        let skills: Vec<_> = self
            .get_loaded_skills()
            .into_iter()
            .filter(|skill| allowed.contains(skill.name.as_str()))
            .collect();
        if skills.is_empty() {
            return String::new();
        }
        let mut prompt = String::from("## 可用 Skills\n\n");
        for skill in skills {
            prompt.push_str(&format!("- **{}**: {}\n", skill.name, skill.description));
        }
        prompt.push_str("\n需要详细说明时，使用 `get_skill` 读取上述 allowlist 中的 skill。");
        prompt
    }

    fn sort_skills(mut skills: Vec<Skill>) -> Vec<Skill> {
        skills.sort_by(|a, b| {
            b.always
                .cmp(&a.always)
                .then_with(|| a.name.cmp(&b.name))
                .then_with(|| a.description.cmp(&b.description))
                .then_with(|| {
                    a.path
                        .as_ref()
                        .map(|p| p.to_string_lossy().to_string())
                        .cmp(&b.path.as_ref().map(|p| p.to_string_lossy().to_string()))
                })
        });
        skills
    }

    /// Load skills from a specific directory
    fn load_skills_from_dir(&self, dir: &Path) -> Vec<Skill> {
        let mut skills = Vec::new();

        let Ok(entries) = std::fs::read_dir(dir) else {
            tracing::warn!(dir = %dir.display(), "Failed to read skills directory");
            return skills;
        };

        for entry in entries.flatten() {
            let path = entry.path();
            if !path.is_dir() {
                continue;
            }

            let skill_file = path.join("SKILL.md");
            if !skill_file.exists() {
                continue;
            }

            match std::fs::read_to_string(&skill_file) {
                Ok(content) => match self.parse_skill(&path, &content) {
                    Some(skill) => {
                        tracing::debug!(
                            skill = %skill.name,
                            path = %skill_file.display(),
                            always = skill.always,
                            "Loaded skill"
                        );
                        skills.push(skill);
                    }
                    None => {
                        tracing::warn!(
                            path = %skill_file.display(),
                            "Failed to parse skill"
                        );
                    }
                },
                Err(e) => {
                    tracing::warn!(
                        path = %skill_file.display(),
                        error = %e,
                        "Failed to read skill file"
                    );
                }
            }
        }

        skills
    }

    /// Parse a skill from markdown content
    fn parse_skill(&self, dir: &Path, content: &str) -> Option<Skill> {
        let (meta, body) = self.parse_skill_markdown(content);

        let name = meta.name.or_else(|| {
            dir.file_name()
                .and_then(|n| n.to_str())
                .map(|s| s.to_string())
        })?;

        let description = meta
            .description
            .unwrap_or_else(|| extract_description(&body));

        Some(Skill {
            name,
            description,
            content: body,
            always: meta.always.unwrap_or(false),
            path: Some(dir.to_path_buf()),
        })
    }

    /// Parse skill markdown, extracting frontmatter and body
    fn parse_skill_markdown(&self, content: &str) -> (SkillMarkdownMeta, String) {
        let normalized = content.replace("\r\n", "\n");

        if let Some(stripped) = normalized.strip_prefix("---\n") {
            if let Some(idx) = stripped.find("\n---\n") {
                let frontmatter = stripped[..idx].to_string();
                let body = stripped[idx + 5..].trim().to_string();
                let meta = self.parse_frontmatter(&frontmatter);
                return (meta, body);
            }
            if let Some(frontmatter) = stripped.strip_suffix("\n---") {
                return (self.parse_frontmatter(frontmatter), String::new());
            }
        }

        (SkillMarkdownMeta::default(), normalized)
    }

    /// Parse simple YAML-like frontmatter
    fn parse_frontmatter(&self, content: &str) -> SkillMarkdownMeta {
        let mut meta = SkillMarkdownMeta::default();

        for line in content.lines() {
            let Some((key, val)) = line.split_once(':') else {
                continue;
            };
            let key = key.trim();
            let val = val.trim().trim_matches('"').trim_matches('\'');

            match key {
                "name" => meta.name = Some(val.to_string()),
                "description" => meta.description = Some(val.to_string()),
                "always" => {
                    meta.always = match val.to_lowercase().as_str() {
                        "true" | "1" | "yes" | "on" => Some(true),
                        "false" | "0" | "no" | "off" => Some(false),
                        _ => None,
                    };
                }
                _ => {}
            }
        }

        meta
    }
}

impl Default for SkillsLoader {
    fn default() -> Self {
        Self::new()
    }
}

/// Extract first non-empty, non-heading line as description
fn extract_description(content: &str) -> String {
    content
        .lines()
        .find(|line| !line.starts_with('#') && !line.trim().is_empty())
        .map(|l| l.trim().to_string())
        .unwrap_or_else(|| "No description".to_string())
}

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

    #[test]
    fn test_parse_skill_without_frontmatter() {
        let loader = SkillsLoader::new();
        let content = "# My Skill\n\nThis is the content.";
        let (meta, body) = loader.parse_skill_markdown(content);

        assert!(meta.name.is_none());
        assert!(meta.description.is_none());
        assert!(body.contains("My Skill"));
    }

    #[test]
    fn test_parse_skill_with_frontmatter() {
        let loader = SkillsLoader::new();
        let content = r#"---
name: test-skill
description: A test skill
always: true
---
# Test Skill

This is the content.
"#;

        let (meta, body) = loader.parse_skill_markdown(content);

        assert_eq!(meta.name, Some("test-skill".to_string()));
        assert_eq!(meta.description, Some("A test skill".to_string()));
        assert_eq!(meta.always, Some(true));
        assert!(body.contains("Test Skill"));
    }

    #[test]
    fn test_extract_description() {
        assert_eq!(
            extract_description("# Title\n\nFirst line of content."),
            "First line of content."
        );
        assert_eq!(extract_description("# Title"), "No description");
    }

    #[test]
    fn test_build_skills_prompt_is_sorted() {
        let loader = SkillsLoader::new_for_testing(
            PathBuf::from("/tmp/picobot"),
            PathBuf::from("/tmp/agents"),
        );
        {
            let mut state = loader.state.lock().unwrap();
            state.loaded_skills = vec![
                Skill {
                    name: "zeta".to_string(),
                    description: "Z".to_string(),
                    content: "Z".to_string(),
                    always: false,
                    path: None,
                },
                Skill {
                    name: "beta".to_string(),
                    description: "B".to_string(),
                    content: "B".to_string(),
                    always: true,
                    path: None,
                },
                Skill {
                    name: "alpha".to_string(),
                    description: "A".to_string(),
                    content: "A".to_string(),
                    always: true,
                    path: None,
                },
            ];
        }

        let prompt = loader.build_skills_prompt();
        let alpha_pos = prompt.find("**alpha**").unwrap();
        let beta_pos = prompt.find("**beta**").unwrap();
        assert!(alpha_pos < beta_pos);
    }

    #[test]
    fn test_source_of() {
        let mut loader = SkillsLoader::new_for_testing(
            PathBuf::from("/home/user/.picobot/skills"),
            PathBuf::from("/home/user/.agents/skills"),
        );
        loader.set_workspace_skills_dir(PathBuf::from("/workspace"));

        assert_eq!(loader.source_of(None), "unknown");
        assert_eq!(
            loader.source_of(Some(Path::new("/workspace/skills/my-skill"))),
            "workspace"
        );
        assert_eq!(
            loader.source_of(Some(Path::new("/home/user/.picobot/skills/foo"))),
            "picobot"
        );
        assert_eq!(
            loader.source_of(Some(Path::new("/home/user/.agents/skills/bar"))),
            "agent"
        );
        assert_eq!(
            loader.source_of(Some(Path::new("/opt/other/skill"))),
            "other"
        );
    }

    #[test]
    fn test_set_enabled_persists_and_filters() {
        let temp = tempfile::tempdir().unwrap();
        let picobot_dir = temp.path().join("picobot");
        let agent_dir = temp.path().join("agents");
        for name in ["alpha", "beta"] {
            let skill_dir = picobot_dir.join(name);
            std::fs::create_dir_all(&skill_dir).unwrap();
            std::fs::write(
                skill_dir.join("SKILL.md"),
                format!("---\nname: {name}\ndescription: {name}\n---\ncontent"),
            )
            .unwrap();
        }

        let loader = SkillsLoader::new_for_testing(picobot_dir.clone(), agent_dir.clone());
        loader.load_skills();
        assert_eq!(loader.get_loaded_skills().len(), 2);
        assert!(loader.is_enabled("beta"));

        loader.set_enabled("beta", false).unwrap();
        let loaded = loader.get_loaded_skills();
        assert_eq!(loaded.len(), 1);
        assert_eq!(loaded[0].name, "alpha");
        assert!(!loader.is_enabled("beta"));
        assert!(loader.is_enabled("alpha"));

        // State survives a fresh loader instance reading the same state file.
        let loader2 = SkillsLoader::new_for_testing(picobot_dir, agent_dir);
        loader2.load_skills();
        assert_eq!(loader2.get_loaded_skills().len(), 1);
        assert!(!loader2.is_enabled("beta"));

        loader2.set_enabled("beta", true).unwrap();
        assert_eq!(loader2.get_loaded_skills().len(), 2);
        assert!(loader2.is_enabled("beta"));
    }
}
