use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use directories::{BaseDirs, ProjectDirs};
use serde::{Deserialize, Serialize};

pub const CONFIG_HOME_ENV: &str = "AIMGR_CONFIG_HOME";
pub const STATE_HOME_ENV: &str = "AIMGR_STATE_HOME";

/// ~/.config/aimgr/ (XDG) unless `AIMGR_CONFIG_HOME` is set.
pub fn config_home() -> Result<PathBuf> {
    if let Some(v) = env::var_os(CONFIG_HOME_ENV) {
        return Ok(PathBuf::from(v));
    }
    Ok(ProjectDirs::from("", "", "aimgr")
        .context("could not determine user config directory")?
        .config_dir()
        .to_path_buf())
}

/// ~/.local/state/aimgr/ (XDG state) unless `AIMGR_STATE_HOME` is set.
pub fn state_home() -> Result<PathBuf> {
    if let Some(v) = env::var_os(STATE_HOME_ENV) {
        return Ok(PathBuf::from(v));
    }
    let base = BaseDirs::new().context("could not determine user directories")?;
    let state = base
        .state_dir()
        .map(|p| p.to_path_buf())
        .unwrap_or_else(|| base.home_dir().join(".local/state"));
    Ok(state.join("aimgr"))
}

pub fn config_file() -> Result<PathBuf> {
    Ok(config_home()?.join("config.toml"))
}

/// Global intent — packages + tools to apply when running outside a project.
///
/// Shape:
///   - `[install]` — wish list arrays (`tools`, `packages`) the bootstrap
///     iterates over.
///   - `[tools.<name>]` / `[packages.<name>]` — per-entity config (trust,
///     future fields). Entries only appear once a user has granted trust
///     or otherwise configured the entity.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
    #[serde(default, skip_serializing_if = "Install::is_empty")]
    pub install: Install,
    #[serde(default)]
    pub tools: BTreeMap<String, ToolEntry>,
    #[serde(default)]
    pub packages: BTreeMap<String, PackageEntry>,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Install {
    #[serde(default)]
    pub tools: Vec<String>,
    #[serde(default)]
    pub packages: Vec<String>,
}

impl Install {
    fn is_empty(&self) -> bool {
        self.tools.is_empty() && self.packages.is_empty()
    }
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PackageEntry {
    /// Shell commands the user has approved for this package (trust gate).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub trusted: Vec<String>,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ToolEntry {
    /// Shell commands the user has approved for this tool (trust gate).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub trusted: Vec<String>,
}

/// Which map an entity lives in — used by the trust gate to route writes.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum EntityKind {
    Tool,
    Package,
}

impl Config {
    /// Checks both maps; trust can live wherever the entity is registered.
    pub fn is_trusted(&self, name: &str, command: &str) -> bool {
        let in_pkg = self
            .packages
            .get(name)
            .is_some_and(|p| p.trusted.iter().any(|c| c == command));
        let in_tool = self
            .tools
            .get(name)
            .is_some_and(|t| t.trusted.iter().any(|c| c == command));
        in_pkg || in_tool
    }

    pub fn trust(&mut self, kind: EntityKind, name: &str, command: &str) {
        match kind {
            EntityKind::Tool => {
                let entry = self.tools.entry(name.to_string()).or_default();
                if !entry.trusted.iter().any(|c| c == command) {
                    entry.trusted.push(command.to_string());
                }
            }
            EntityKind::Package => {
                let entry = self.packages.entry(name.to_string()).or_default();
                if !entry.trusted.iter().any(|c| c == command) {
                    entry.trusted.push(command.to_string());
                }
            }
        }
    }

    /// Add `name` to the package wish list (idempotent). Returns `true` if
    /// newly added.
    pub fn add_package(&mut self, name: &str) -> bool {
        if self.install.packages.iter().any(|p| p == name) {
            return false;
        }
        self.install.packages.push(name.to_string());
        true
    }

    /// Drop `name` from the wish list and clear any trust it carried.
    /// Returns `true` if anything was removed.
    pub fn remove_package(&mut self, name: &str) -> bool {
        let before = self.install.packages.len();
        self.install.packages.retain(|p| p != name);
        let removed_from_list = self.install.packages.len() != before;
        let removed_trust = self.packages.remove(name).is_some();
        removed_from_list || removed_trust
    }

    pub fn add_tool(&mut self, name: &str) -> bool {
        if self.install.tools.iter().any(|t| t == name) {
            return false;
        }
        self.install.tools.push(name.to_string());
        true
    }

    pub fn remove_tool(&mut self, name: &str) -> bool {
        let before = self.install.tools.len();
        self.install.tools.retain(|t| t != name);
        let removed_from_list = self.install.tools.len() != before;
        let removed_trust = self.tools.remove(name).is_some();
        removed_from_list || removed_trust
    }
}

impl Config {
    pub fn load() -> Result<Self> {
        Self::load_from(&config_file()?)
    }

    pub fn load_from(path: &Path) -> Result<Self> {
        if !path.exists() {
            return Ok(Self::default());
        }
        let text =
            fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
        toml::from_str(&text).with_context(|| format!("parsing {}", path.display()))
    }

    pub fn save(&self) -> Result<()> {
        self.save_to(&config_file()?)
    }

    pub fn save_to(&self, path: &Path) -> Result<()> {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
        }
        let text = toml::to_string_pretty(self).context("serializing config")?;
        fs::write(path, text).with_context(|| format!("writing {}", path.display()))
    }
}

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

    #[test]
    fn load_missing_returns_default() {
        let dir = tempdir().unwrap();
        let cfg = Config::load_from(&dir.path().join("missing.toml")).unwrap();
        assert!(cfg.install.tools.is_empty());
        assert!(cfg.install.packages.is_empty());
        assert!(cfg.packages.is_empty());
        assert!(cfg.tools.is_empty());
    }

    #[test]
    fn round_trip() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("nested/config.toml");
        let mut cfg = Config::default();
        cfg.add_tool("claude");
        cfg.trust(EntityKind::Tool, "claude", "curl install.sh | bash");
        cfg.add_package("seek");
        cfg.add_package("plannotator");
        cfg.trust(EntityKind::Package, "seek", "curl foo | sh");
        cfg.save_to(&path).unwrap();
        let loaded = Config::load_from(&path).unwrap();
        assert_eq!(loaded.install.tools, vec!["claude".to_string()]);
        assert_eq!(
            loaded.install.packages,
            vec!["seek".to_string(), "plannotator".to_string()]
        );
        assert!(loaded.is_trusted("seek", "curl foo | sh"));
        assert!(loaded.is_trusted("claude", "curl install.sh | bash"));
    }

    #[test]
    fn trust_creates_entry_and_dedupes() {
        let mut cfg = Config::default();
        cfg.trust(EntityKind::Package, "seek", "curl foo | sh");
        assert!(cfg.is_trusted("seek", "curl foo | sh"));
        assert!(!cfg.is_trusted("seek", "curl bar | sh"));
        assert!(!cfg.is_trusted("other", "curl foo | sh"));
        cfg.trust(EntityKind::Package, "seek", "curl foo | sh");
        assert_eq!(cfg.packages["seek"].trusted.len(), 1);
    }

    #[test]
    fn trust_routes_by_kind() {
        let mut cfg = Config::default();
        cfg.trust(EntityKind::Tool, "claude", "curl claude.sh");
        assert!(cfg.tools.contains_key("claude"));
        assert!(!cfg.packages.contains_key("claude"));
        assert!(cfg.is_trusted("claude", "curl claude.sh"));
    }

    #[test]
    fn add_package_is_idempotent_and_remove_clears_trust() {
        let mut cfg = Config::default();
        assert!(cfg.add_package("seek"));
        assert!(!cfg.add_package("seek"));
        cfg.trust(EntityKind::Package, "seek", "curl");
        assert!(cfg.remove_package("seek"));
        assert!(!cfg.is_trusted("seek", "curl"));
        assert!(!cfg.remove_package("seek"));
    }

    #[test]
    fn add_tool_is_idempotent_and_remove_clears_trust() {
        let mut cfg = Config::default();
        assert!(cfg.add_tool("claude"));
        assert!(!cfg.add_tool("claude"));
        cfg.trust(EntityKind::Tool, "claude", "curl");
        assert!(cfg.remove_tool("claude"));
        assert!(!cfg.is_trusted("claude", "curl"));
        assert!(!cfg.remove_tool("claude"));
    }

    #[test]
    fn wish_list_and_trust_coexist_in_toml() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("config.toml");
        let mut cfg = Config::default();
        cfg.add_tool("claude");
        cfg.trust(EntityKind::Tool, "claude", "curl claude.sh");
        cfg.add_package("seek");
        cfg.trust(EntityKind::Package, "seek", "curl seek.sh");
        cfg.save_to(&path).unwrap();
        let text = fs::read_to_string(&path).unwrap();
        assert!(text.contains("[install]"));
        assert!(text.contains("tools = [\"claude\"]"));
        assert!(text.contains("packages = [\"seek\"]"));
        assert!(text.contains("[tools.claude]"));
        assert!(text.contains("[packages.seek]"));
    }
}
