use std::collections::HashSet;
use std::path::{Path, PathBuf};

use edgecrab_types::Platform;

use crate::config::PluginsConfig;
use crate::error::PluginError;
use crate::hermes::{
    HermesCliCommand, discover_entrypoint_plugins, hermes_declared_hooks, hermes_declared_tools,
    introspect_runtime_surface, introspect_runtime_surface_for_entrypoint,
    looks_like_hermes_plugin, missing_required_env as missing_hermes_env, parse_hermes_manifest,
    synthesize_entrypoint_manifest, synthesize_manifest as synthesize_hermes_manifest,
};
use crate::manifest::{PluginManifest, parse_plugin_manifest};
use crate::skill::inject::build_prompt_fragment;
use crate::skill::loader::{LoadedSkill, scan_skills_dir};
use crate::types::{PluginKind, PluginStatus, SkillReadinessStatus, SkillSource, TrustLevel};

#[derive(Debug, Clone)]
pub struct DiscoveredPlugin {
    pub name: String,
    pub version: String,
    pub description: String,
    pub compatibility: Option<String>,
    pub kind: PluginKind,
    pub status: PluginStatus,
    pub path: PathBuf,
    pub manifest: Option<PluginManifest>,
    pub skill: Option<LoadedSkill>,
    pub tools: Vec<String>,
    pub hooks: Vec<String>,
    pub trust_level: TrustLevel,
    pub install_source: Option<String>,
    pub enabled: bool,
    pub source: SkillSource,
    pub missing_env: Vec<String>,
    pub related_skills: Vec<String>,
    pub cli_commands: Vec<HermesCliCommand>,
}

#[derive(Debug, Clone, Default)]
pub struct PluginDiscovery {
    pub plugins: Vec<DiscoveredPlugin>,
}

pub fn discover_plugins(
    config: &PluginsConfig,
    platform: Platform,
) -> Result<PluginDiscovery, PluginError> {
    let mut seen = HashSet::new();
    let mut plugins = Vec::new();
    for (root, source) in plugin_dirs(config) {
        if !root.exists() {
            continue;
        }
        for entry in std::fs::read_dir(&root)? {
            let entry = entry?;
            let path = entry.path();
            if !path.is_dir() {
                continue;
            }
            let manifest_path = path.join("plugin.toml");
            let plugin = if manifest_path.is_file() {
                load_plugin_from_manifest(config, &path, source, platform, &manifest_path)?
            } else if looks_like_hermes_plugin(&path) {
                load_hermes_plugin(config, &path, source, platform)?
            } else if path.join("SKILL.md").is_file() {
                load_skill_only_plugin(config, &path, source, platform)?
            } else {
                continue;
            };
            if seen.insert(plugin.name.clone()) {
                plugins.push(plugin);
            }
        }
    }
    if let Ok(entrypoints) = discover_entrypoint_plugins() {
        for entrypoint in entrypoints {
            if seen.contains(&entrypoint.name) {
                continue;
            }
            let plugin = load_hermes_entrypoint_plugin(config, entrypoint, platform)?;
            if seen.insert(plugin.name.clone()) {
                plugins.push(plugin);
            }
        }
    }
    plugins.sort_by(|left, right| left.name.cmp(&right.name));
    Ok(PluginDiscovery { plugins })
}

pub fn build_plugin_skill_prompt(discovery: &PluginDiscovery) -> Option<String> {
    let sections: Vec<String> = discovery
        .plugins
        .iter()
        .filter_map(|plugin| {
            if !plugin.enabled {
                return None;
            }
            let skill = plugin.skill.as_ref()?;
            if !skill.platform_visible {
                return None;
            }
            match &skill.readiness {
                SkillReadinessStatus::Available => Some(build_prompt_fragment(
                    &skill.manifest.name,
                    &skill.manifest.body,
                )),
                _ => None,
            }
        })
        .collect();
    if sections.is_empty() {
        None
    } else {
        Some(format!("# Plugin Skills\n\n{}", sections.join("\n\n")))
    }
}

fn load_plugin_from_manifest(
    config: &PluginsConfig,
    path: &Path,
    source: SkillSource,
    platform: Platform,
    manifest_path: &Path,
) -> Result<DiscoveredPlugin, PluginError> {
    let manifest = parse_plugin_manifest(manifest_path)?;
    if manifest.plugin.kind == PluginKind::Hermes && looks_like_hermes_plugin(path) {
        return load_hermes_plugin_with_install_metadata(config, path, source, platform, manifest);
    }
    let enabled = config.is_plugin_enabled(&manifest.plugin.name, Some(&platform.to_string()));
    let skill = if manifest.plugin.kind == PluginKind::Skill && path.join("SKILL.md").is_file() {
        scan_skills_dir(path, source, &[], platform)?
            .into_iter()
            .find(|skill| skill.path == path.join("SKILL.md"))
    } else {
        None
    };
    let status = skill.as_ref().map_or_else(
        || {
            if enabled {
                PluginStatus::Available
            } else {
                PluginStatus::Disabled
            }
        },
        |skill| status_for_skill(enabled, skill),
    );
    let compatibility = skill
        .as_ref()
        .and_then(|loaded| loaded.manifest.compatibility.clone());
    let related_skills = skill
        .as_ref()
        .map(|loaded| loaded.manifest.related_skills.clone())
        .unwrap_or_default();
    let missing_env = skill.as_ref().map(skill_missing_env).unwrap_or_default();
    Ok(DiscoveredPlugin {
        name: manifest.plugin.name.clone(),
        version: manifest.plugin.version.clone(),
        description: manifest.plugin.description.clone(),
        compatibility,
        kind: manifest.plugin.kind,
        status,
        path: path.to_path_buf(),
        tools: manifest
            .tools
            .iter()
            .map(|tool| tool.name.clone())
            .collect(),
        hooks: Vec::new(),
        trust_level: manifest
            .trust
            .as_ref()
            .map(|trust| trust.level)
            .unwrap_or_default(),
        install_source: manifest
            .trust
            .as_ref()
            .and_then(|trust| trust.source.clone()),
        manifest: Some(manifest),
        skill,
        enabled,
        source,
        missing_env,
        related_skills,
        cli_commands: Vec::new(),
    })
}

fn load_skill_only_plugin(
    config: &PluginsConfig,
    path: &Path,
    source: SkillSource,
    platform: Platform,
) -> Result<DiscoveredPlugin, PluginError> {
    let loaded = scan_skills_dir(path, source, &[], platform)?
        .into_iter()
        .find(|skill| skill.path == path.join("SKILL.md"))
        .ok_or_else(|| PluginError::InvalidSkill {
            path: path.join("SKILL.md"),
            message: "failed to load skill plugin".into(),
        })?;
    let missing_env = skill_missing_env(&loaded);
    let enabled = config.is_plugin_enabled(&loaded.manifest.name, Some(&platform.to_string()));
    let compatibility = loaded.manifest.compatibility.clone();
    let related_skills = loaded.manifest.related_skills.clone();
    Ok(DiscoveredPlugin {
        name: loaded.manifest.name.clone(),
        version: loaded
            .manifest
            .version
            .clone()
            .unwrap_or_else(|| "0.1.0".into()),
        description: loaded.manifest.description.clone(),
        compatibility,
        kind: PluginKind::Skill,
        status: status_for_skill(enabled, &loaded),
        path: path.to_path_buf(),
        manifest: None,
        skill: Some(loaded),
        tools: Vec::new(),
        hooks: Vec::new(),
        trust_level: TrustLevel::Unverified,
        install_source: None,
        enabled,
        source,
        missing_env,
        related_skills,
        cli_commands: Vec::new(),
    })
}

fn plugin_dirs(config: &PluginsConfig) -> Vec<(PathBuf, SkillSource)> {
    let mut seen = HashSet::new();
    let mut dirs = Vec::new();

    let mut push_dir = |path: PathBuf, source: SkillSource| {
        if seen.insert(path.clone()) {
            dirs.push((path, source));
        }
    };

    push_dir(config.install_dir.clone(), SkillSource::User);
    let legacy_user_plugins = dirs::home_dir()
        .map(|home| home.join(".hermes").join("plugins"))
        .unwrap_or_else(|| PathBuf::from("~/.hermes/plugins"));
    push_dir(legacy_user_plugins, SkillSource::User);
    for path in config.expanded_external_dirs() {
        push_dir(path, SkillSource::User);
    }
    push_dir(PathBuf::from(".edgecrab/plugins"), SkillSource::Project);
    if env_var_enabled("HERMES_ENABLE_PROJECT_PLUGINS") {
        push_dir(PathBuf::from(".hermes/plugins"), SkillSource::Project);
    }
    #[cfg(unix)]
    push_dir(
        PathBuf::from("/usr/share/edgecrab/plugins"),
        SkillSource::System,
    );
    dirs
}

fn status_for_skill(enabled: bool, loaded: &LoadedSkill) -> PluginStatus {
    if !enabled {
        return PluginStatus::Disabled;
    }
    if !loaded.platform_visible {
        return PluginStatus::PlatformExcluded;
    }
    match loaded.readiness {
        SkillReadinessStatus::Available => PluginStatus::Available,
        SkillReadinessStatus::SetupNeeded { .. } => PluginStatus::SetupNeeded,
        SkillReadinessStatus::Unsupported { .. } => PluginStatus::Unsupported,
    }
}

#[cfg(test)]
#[allow(clippy::items_after_test_module)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    #[test]
    fn builds_prompt_only_for_enabled_available_skills() {
        let discovery = PluginDiscovery {
            plugins: vec![DiscoveredPlugin {
                name: "demo".into(),
                version: "0.1.0".into(),
                description: "Demo".into(),
                compatibility: None,
                kind: PluginKind::Skill,
                status: PluginStatus::Available,
                path: PathBuf::from("/tmp/demo"),
                manifest: None,
                skill: Some(LoadedSkill {
                    path: PathBuf::from("/tmp/demo/SKILL.md"),
                    source: SkillSource::User,
                    manifest: crate::skill::manifest::SkillManifest {
                        name: "demo".into(),
                        description: "Demo".into(),
                        body: "Follow demo steps.".into(),
                        ..Default::default()
                    },
                    readiness: SkillReadinessStatus::Available,
                    platform_visible: true,
                }),
                tools: Vec::new(),
                hooks: Vec::new(),
                trust_level: TrustLevel::Unverified,
                install_source: None,
                enabled: true,
                source: SkillSource::User,
                missing_env: Vec::new(),
                related_skills: Vec::new(),
                cli_commands: Vec::new(),
            }],
        };

        let prompt = build_plugin_skill_prompt(&discovery).expect("prompt exists");
        assert!(prompt.contains("# Plugin Skills"));
        assert!(prompt.contains("Follow demo steps."));
    }

    #[test]
    fn hermes_plugin_with_missing_env_is_setup_needed() {
        let temp = TempDir::new().expect("tempdir");
        let plugin_dir = temp.path().join("demo");
        std::fs::create_dir_all(&plugin_dir).expect("plugin dir");
        std::fs::write(
            plugin_dir.join("plugin.yaml"),
            r#"
name: hermes-demo
version: "1.0.0"
description: Demo Hermes plugin
provides_tools:
  - hello_world
requires_env:
  - EDGECRAB_TEST_HERMES_TOKEN
"#,
        )
        .expect("manifest");
        std::fs::write(
            plugin_dir.join("__init__.py"),
            "def register(ctx):\n    pass\n",
        )
        .expect("plugin");

        let config = PluginsConfig {
            install_dir: temp.path().join("empty-install-root"),
            external_dirs: vec![
                plugin_dir
                    .parent()
                    .expect("parent")
                    .to_string_lossy()
                    .to_string(),
            ],
            ..PluginsConfig::default()
        };

        let discovery = discover_plugins(&config, Platform::Cli).expect("discovery");
        let plugin = discovery
            .plugins
            .iter()
            .find(|plugin| plugin.name == "hermes-demo")
            .expect("plugin discovered");

        assert_eq!(plugin.status, PluginStatus::SetupNeeded);
        assert_eq!(plugin.missing_env, vec!["EDGECRAB_TEST_HERMES_TOKEN"]);
    }

    #[test]
    fn hermes_plugin_loads_bundled_skill_metadata() {
        let temp = TempDir::new().expect("tempdir");
        let plugin_dir = temp.path().join("calculator");
        std::fs::create_dir_all(&plugin_dir).expect("plugin dir");
        std::fs::write(
            plugin_dir.join("plugin.yaml"),
            r#"
name: calculator
version: "1.0.0"
description: Calculator plugin
provides_tools:
  - calculate
"#,
        )
        .expect("manifest");
        std::fs::write(
            plugin_dir.join("__init__.py"),
            "def register(ctx):\n    pass\n",
        )
        .expect("plugin");
        std::fs::write(
            plugin_dir.join("SKILL.md"),
            r#"---
name: calculator-skill
description: Use calculator for exact arithmetic.
compatibility: Requires calculator plugin
metadata:
  hermes:
    related_skills: [math-notes]
---

# Calculator

Use `calculate` for exact arithmetic.
"#,
        )
        .expect("skill");

        let config = PluginsConfig {
            install_dir: temp.path().join("empty-install-root"),
            external_dirs: vec![
                plugin_dir
                    .parent()
                    .expect("parent")
                    .to_string_lossy()
                    .to_string(),
            ],
            ..PluginsConfig::default()
        };

        let discovery = discover_plugins(&config, Platform::Cli).expect("discovery");
        let plugin = discovery
            .plugins
            .iter()
            .find(|plugin| plugin.name == "calculator")
            .expect("plugin discovered");

        assert_eq!(
            plugin.compatibility.as_deref(),
            Some("Requires calculator plugin")
        );
        assert_eq!(plugin.related_skills, vec!["math-notes"]);
        assert!(plugin.skill.is_some());
    }
}

fn env_var_enabled(name: &str) -> bool {
    std::env::var(name)
        .ok()
        .map(|value| {
            matches!(
                value.trim().to_ascii_lowercase().as_str(),
                "1" | "true" | "yes" | "on"
            )
        })
        .unwrap_or(false)
}

fn load_hermes_plugin(
    config: &PluginsConfig,
    path: &Path,
    source: SkillSource,
    platform: Platform,
) -> Result<DiscoveredPlugin, PluginError> {
    let hermes_manifest = parse_hermes_manifest(path)?;
    load_hermes_plugin_with_install_metadata(
        config,
        path,
        source,
        platform,
        synthesize_hermes_manifest(path, &hermes_manifest),
    )
}

fn load_hermes_plugin_with_install_metadata(
    config: &PluginsConfig,
    path: &Path,
    source: SkillSource,
    platform: Platform,
    install_manifest: PluginManifest,
) -> Result<DiscoveredPlugin, PluginError> {
    let hermes_manifest = parse_hermes_manifest(path)?;
    let mut manifest = synthesize_hermes_manifest(path, &hermes_manifest);
    if let (Some(exec), Some(installed_exec)) =
        (manifest.exec.as_mut(), install_manifest.exec.as_ref())
    {
        exec.env = installed_exec.env.clone();
        exec.cwd = installed_exec.cwd.clone();
    }
    manifest.trust = install_manifest.trust.clone();
    manifest.integrity = install_manifest.integrity.clone();
    let runtime_surface = introspect_runtime_surface(path, &hermes_manifest);
    let mut tools = hermes_declared_tools(&hermes_manifest);
    for tool in runtime_surface.tools {
        if !tools.iter().any(|existing| existing == &tool.name) {
            tools.push(tool.name);
        }
    }
    tools.sort();
    let mut hooks = hermes_declared_hooks(&hermes_manifest);
    for hook in runtime_surface.hooks {
        if !hooks.iter().any(|existing| existing == &hook) {
            hooks.push(hook);
        }
    }
    hooks.sort();
    let enabled = config.is_plugin_enabled(&manifest.plugin.name, Some(&platform.to_string()));
    let missing_env = missing_hermes_env(&hermes_manifest);
    let skill = if path.join("SKILL.md").is_file() {
        scan_skills_dir(path, source, &[], platform)?
            .into_iter()
            .find(|loaded| loaded.path == path.join("SKILL.md"))
    } else {
        None
    };
    let compatibility = skill
        .as_ref()
        .and_then(|loaded| loaded.manifest.compatibility.clone());
    let related_skills = skill
        .as_ref()
        .map(|loaded| loaded.manifest.related_skills.clone())
        .unwrap_or_default();
    let trust_level = manifest
        .trust
        .as_ref()
        .map(|trust| trust.level)
        .unwrap_or(TrustLevel::Unverified);
    let install_source = manifest
        .trust
        .as_ref()
        .and_then(|trust| trust.source.clone());
    Ok(DiscoveredPlugin {
        name: manifest.plugin.name.clone(),
        version: manifest.plugin.version.clone(),
        description: manifest.plugin.description.clone(),
        compatibility,
        kind: manifest.plugin.kind,
        status: if !enabled {
            PluginStatus::Disabled
        } else if missing_env.is_empty() {
            PluginStatus::Available
        } else {
            PluginStatus::SetupNeeded
        },
        path: path.to_path_buf(),
        manifest: Some(manifest),
        skill,
        tools,
        hooks,
        trust_level,
        install_source,
        enabled,
        source,
        missing_env,
        related_skills,
        cli_commands: runtime_surface.cli_commands,
    })
}

fn load_hermes_entrypoint_plugin(
    config: &PluginsConfig,
    entrypoint: crate::hermes::HermesEntrypointPlugin,
    platform: Platform,
) -> Result<DiscoveredPlugin, PluginError> {
    let manifest = synthesize_entrypoint_manifest(&entrypoint, None);
    let runtime_surface = introspect_runtime_surface_for_entrypoint(&entrypoint);
    let mut tools = runtime_surface
        .tools
        .iter()
        .map(|tool| tool.name.clone())
        .collect::<Vec<_>>();
    tools.sort();
    tools.dedup();
    let mut hooks = runtime_surface.hooks.clone();
    hooks.sort();
    hooks.dedup();
    let enabled = config.is_plugin_enabled(&manifest.plugin.name, Some(&platform.to_string()));

    Ok(DiscoveredPlugin {
        name: manifest.plugin.name.clone(),
        version: manifest.plugin.version.clone(),
        description: manifest.plugin.description.clone(),
        compatibility: None,
        kind: manifest.plugin.kind,
        status: if enabled {
            PluginStatus::Available
        } else {
            PluginStatus::Disabled
        },
        path: entrypoint
            .module_path
            .clone()
            .unwrap_or_else(|| PathBuf::from(format!("entrypoint:{}", entrypoint.name))),
        manifest: Some(manifest),
        skill: None,
        tools,
        hooks,
        trust_level: TrustLevel::Unverified,
        install_source: Some(format!("entrypoint:{}", entrypoint.value)),
        enabled,
        source: SkillSource::System,
        missing_env: Vec::new(),
        related_skills: Vec::new(),
        cli_commands: runtime_surface.cli_commands,
    })
}

fn skill_missing_env(loaded: &LoadedSkill) -> Vec<String> {
    match &loaded.readiness {
        SkillReadinessStatus::SetupNeeded { missing } => missing.clone(),
        _ => Vec::new(),
    }
}
