use async_trait::async_trait;
use serde_json::json;
use std::sync::Arc;

use crate::skills::{SkillRuntime, SkillScope};
use crate::tools::traits::{Tool, ToolResult};
use crate::tools::{extract_bool, extract_string_array};

pub struct SkillManageTool {
    skills: Arc<SkillRuntime>,
}

impl SkillManageTool {
    pub fn new(skills: Arc<SkillRuntime>) -> Self {
        Self { skills }
    }
}

#[async_trait]
impl Tool for SkillManageTool {
    fn name(&self) -> &str {
        "skill_manage"
    }

    fn description(&self) -> &str {
        "Manage PicoBot skills. Actions: list, get, create, update, delete, disable, reload.\n\n\
        IMPORTANT: To create or modify skills, ALWAYS use this tool (skill_manage), NOT the write tool.\n\n\
        Skill Structure:\n\
        - Folder name: kebab-case (lowercase with hyphens, e.g., 'my-cool-skill')\n\
        - Required: SKILL.md with YAML frontmatter + Markdown body\n\
        - Optional folders: scripts/, references/, assets/\n\
        - Storage paths (created automatically by this tool):\n\
          - Project scope: {current-dir}/.picobot/skills/{name}/SKILL.md\n\
          - User scope: ~/.picobot/skills/{name}/SKILL.md\n\n\
        Installing from Zip:\n\
        - Extract skill folders to .picobot/skills/ directory (NOT skills/)\n\
        - If zip contains multiple skills, extract each subfolder separately\n\
        - Final structure: .picobot/skills/{skill-name}/SKILL.md"
    }

    fn parameters_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["list", "get", "create", "update", "delete", "disable", "reload"],
                    "description": "Management action to perform"
                },
                "scope": {
                    "type": "string",
                    "enum": ["project", "user"],
                    "description": "Writable skill scope for create/update/delete/disable. Defaults to project. .agents discovery sources are read-only here, but can still be disabled via sidecar state."
                },
                "name": {
                    "type": "string",
                    "description": "Skill folder name in kebab-case (e.g., 'my-cool-skill', 'code-review'). The skill_manage tool automatically creates files at .picobot/skills/{name}/SKILL.md (project scope) or ~/.picobot/skills/{name}/SKILL.md (user scope)."
                },
                "names": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    },
                    "description": "Skill names for batch disable; pass a single-item array to disable one skill"
                },
                "description": {
                    "type": "string",
                    "description": "Skill description used for discovery"
                },
                "body": {
                    "type": "string",
                    "description": "Skill body instructions"
                },
                "reload": {
                    "type": "boolean",
                    "description": "Whether to reload the runtime catalog after mutation. Defaults to true.",
                    "default": true
                }
            },
            "required": ["action"]
        })
    }

    async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
        let action = match args.get("action").and_then(|v| v.as_str()) {
            Some(value) => value,
            None => {
                return Ok(error_result("Missing required parameter: action"));
            }
        };

        let reload = extract_bool(&args, "reload").unwrap_or(true);
        let scope = match args.get("scope").and_then(|v| v.as_str()) {
            Some(value) => match SkillScope::parse(value) {
                Some(scope) => scope,
                None => {
                    return Ok(error_result(
                        "scope must be 'project' or 'user'; .agents sources are discovery-only",
                    ));
                }
            },
            None => SkillScope::Project,
        };

        let name = args.get("name").and_then(|v| v.as_str());

        let result = match action {
            "list" => list_skills_payload(&self.skills),
            "get" => {
                let name = match name {
                    Some(name) => name,
                    None => return Ok(error_result("Missing required parameter: name")),
                };

                match self.skills.get_skill(name) {
                    Some(skill) => json!({
                        "name": skill.name,
                        "description": skill.description,
                        "body": skill.body,
                        "source": match skill.source {
                            crate::skills::SkillSource::User => "user",
                            crate::skills::SkillSource::UserAgent => "user_agent",
                            crate::skills::SkillSource::UserOpenclaw => "user_openclaw",
                            crate::skills::SkillSource::Project => "project",
                            crate::skills::SkillSource::ProjectAgent => "project_agent",
                            crate::skills::SkillSource::ProjectOpenclaw => "project_openclaw",
                        },
                        "path": skill.path.display().to_string(),
                    }),
                    None => return Ok(error_result(&format!("skill '{}' not found", name))),
                }
            }
            "create" => {
                let name = match name {
                    Some(name) => name,
                    None => return Ok(error_result("Missing required parameter: name")),
                };
                let description = match args.get("description").and_then(|v| v.as_str()) {
                    Some(value) => value,
                    None => return Ok(error_result("Missing required parameter: description")),
                };
                let body = args.get("body").and_then(|v| v.as_str()).unwrap_or("");

                match self
                    .skills
                    .create_skill(scope, name, description, body, reload)
                {
                    Ok(skill) => json!({
                        "status": "created",
                        "name": skill.name,
                        "path": skill.path.display().to_string(),
                        "scope": scope.as_str(),
                        "reloaded": reload,
                    }),
                    Err(err) => return Ok(error_result(&err)),
                }
            }
            "update" => {
                let name = match name {
                    Some(name) => name,
                    None => return Ok(error_result("Missing required parameter: name")),
                };
                let description = args.get("description").and_then(|v| v.as_str());
                let body = args.get("body").and_then(|v| v.as_str());
                if description.is_none() && body.is_none() {
                    return Ok(error_result("update requires description or body"));
                }

                match self
                    .skills
                    .update_skill(scope, name, description, body, reload)
                {
                    Ok(skill) => json!({
                        "status": "updated",
                        "name": skill.name,
                        "path": skill.path.display().to_string(),
                        "scope": scope.as_str(),
                        "reloaded": reload,
                    }),
                    Err(err) => return Ok(error_result(&err)),
                }
            }
            "delete" => {
                let name = match name {
                    Some(name) => name,
                    None => return Ok(error_result("Missing required parameter: name")),
                };

                match self.skills.delete_skill(scope, name, reload) {
                    Ok(path) => json!({
                        "status": "deleted",
                        "name": name,
                        "path": path.display().to_string(),
                        "scope": scope.as_str(),
                        "reloaded": reload,
                    }),
                    Err(err) => return Ok(error_result(&err)),
                }
            }
            "reload" => match self.skills.reload() {
                Ok(catalog) => json!({
                    "status": "reloaded",
                    "count": catalog.len(),
                }),
                Err(err) => return Ok(error_result(&err)),
            },
            "disable" => {
                let names = match parse_disable_names(&args) {
                    Ok(names) => names,
                    Err(err) => return Ok(error_result(&err)),
                };
                let targets = &names;

                let mut changes = Vec::new();
                for target in targets {
                    match self.skills.disable_skill(scope, target, false) {
                        Ok(change) => changes.push(change),
                        Err(err) => return Ok(error_result(&err)),
                    }
                }
                if reload {
                    if let Err(err) = self.skills.reload() {
                        return Ok(error_result(&err));
                    }
                }

                json!({
                    "status": "disabled",
                    "scope": scope.as_str(),
                    "count": changes.len(),
                    "reloaded": reload,
                    "changes": changes.into_iter().map(skill_change_payload).collect::<Vec<_>>(),
                })
            }
            _ => return Ok(error_result("Unsupported action")),
        };

        Ok(ToolResult {
            success: true,
            output: serde_json::to_string_pretty(&result)?,
            error: None,
        })
    }
}

fn error_result(message: &str) -> ToolResult {
    ToolResult {
        success: false,
        output: String::new(),
        error: Some(message.to_string()),
    }
}

fn parse_disable_names(args: &serde_json::Value) -> Result<Vec<String>, String> {
    // 支持两种格式：实际数组 或 字符串化的 JSON 数组
    extract_string_array(args, "names")
        .filter(|arr| !arr.is_empty())
        .ok_or_else(|| "disable requires names (array of strings)".to_string())
}

fn skill_change_payload(change: crate::skills::SkillAvailabilityChange) -> serde_json::Value {
    json!({
        "name": change.name,
        "scope": change.scope.as_str(),
        "path": change.state_path.display().to_string(),
        "changed": change.changed,
        "available": change.available,
        "disabled_in_scopes": change.disabled_in_scopes.into_iter().map(|scope| scope.as_str()).collect::<Vec<_>>(),
    })
}

fn list_skills_payload(skills: &Arc<SkillRuntime>) -> serde_json::Value {
    let skills = skills.list_skills();
    json!({
        "count": skills.len(),
        "skills": skills.into_iter().map(|skill| json!({
            "name": skill.name,
            "description": skill.description,
            "source": match skill.source {
                crate::skills::SkillSource::User => "user",
                crate::skills::SkillSource::UserAgent => "user_agent",
                crate::skills::SkillSource::UserOpenclaw => "user_openclaw",
                crate::skills::SkillSource::Project => "project",
                crate::skills::SkillSource::ProjectAgent => "project_agent",
                crate::skills::SkillSource::ProjectOpenclaw => "project_openclaw",
            },
            "path": skill.path.display().to_string(),
        })).collect::<Vec<_>>()
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::SkillsConfig;
    use crate::skills::acquire_skill_test_env_lock;
    use std::ffi::OsString;
    use std::path::{Path, PathBuf};

    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);
                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()
    }

    #[tokio::test]
    async fn test_skill_manage_disable_updates_runtime() {
        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");
        std::fs::create_dir_all(&home_dir).unwrap();
        std::fs::create_dir_all(&project_dir).unwrap();
        let _home = HomeDirGuard::enter(&home_dir);
        let _guard = CurrentDirGuard::enter(&project_dir);

        let runtime = Arc::new(SkillRuntime::from_config(SkillsConfig {
            enabled: true,
            sources: vec!["project".to_string()],
            max_index_chars: 4000,
            max_listed_skills: 32,
        }));
        runtime
            .create_skill(SkillScope::Project, "demo", "demo skill", "body", true)
            .unwrap();

        let tool = SkillManageTool::new(runtime.clone());

        let disabled = tool
            .execute(json!({
                "action": "disable",
                "names": ["demo"],
                "scope": "project"
            }))
            .await
            .unwrap();
        assert!(disabled.success);
        assert!(disabled.output.contains("disabled"));
        assert!(runtime.get_skill("demo").is_none());
    }

    #[tokio::test]
    async fn test_skill_manage_batch_disable_uses_names_array() {
        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");
        std::fs::create_dir_all(&home_dir).unwrap();
        std::fs::create_dir_all(&project_dir).unwrap();
        let _home = HomeDirGuard::enter(&home_dir);
        let _guard = CurrentDirGuard::enter(&project_dir);

        let runtime = Arc::new(SkillRuntime::from_config(SkillsConfig {
            enabled: true,
            sources: vec!["project".to_string()],
            max_index_chars: 4000,
            max_listed_skills: 32,
        }));
        runtime
            .create_skill(SkillScope::Project, "demo-a", "demo skill a", "body", true)
            .unwrap();
        runtime
            .create_skill(SkillScope::Project, "demo-b", "demo skill b", "body", true)
            .unwrap();

        let tool = SkillManageTool::new(runtime.clone());

        let disabled = tool
            .execute(json!({
                "action": "disable",
                "names": ["demo-a", "demo-b"],
                "scope": "project"
            }))
            .await
            .unwrap();

        assert!(disabled.success);
        assert!(disabled.output.contains("\"count\": 2"));
        assert!(runtime.get_skill("demo-a").is_none());
        assert!(runtime.get_skill("demo-b").is_none());
    }

    #[tokio::test]
    async fn test_skill_manage_disable_requires_names_array() {
        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");
        std::fs::create_dir_all(&home_dir).unwrap();
        std::fs::create_dir_all(&project_dir).unwrap();
        let _home = HomeDirGuard::enter(&home_dir);
        let _guard = CurrentDirGuard::enter(&project_dir);

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

        let result = tool
            .execute(json!({
                "action": "disable",
                "name": "demo",
                "scope": "project"
            }))
            .await
            .unwrap();

        assert!(!result.success);
        assert!(result.error.unwrap().contains("disable requires names"));
    }
}
