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

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

use crate::config;
use crate::tools::Tool;

pub const PROJECT_STATE_DIR: &str = ".aimgr";
pub const PROJECT_INTENT_FILE: &str = "aimgr.toml";
pub const PROJECT_STATE_FILE: &str = "state.toml";
pub const MACHINE_STATE_FILE: &str = "state.toml";

// ───────────────────────────── project intent ─────────────────────────────

/// `.aimgr/aimgr.toml` — checked in. Declares what this project wants.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProjectIntent {
    #[serde(default)]
    pub tools: Vec<Tool>,
    #[serde(default)]
    pub packages: Vec<String>,
}

impl ProjectIntent {
    pub fn path(project_root: &Path) -> PathBuf {
        project_root
            .join(PROJECT_STATE_DIR)
            .join(PROJECT_INTENT_FILE)
    }

    pub fn exists(project_root: &Path) -> bool {
        Self::path(project_root).exists()
    }

    pub fn load(project_root: &Path) -> Result<Self> {
        let path = Self::path(project_root);
        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, project_root: &Path) -> Result<()> {
        let dir = project_root.join(PROJECT_STATE_DIR);
        fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
        let path = dir.join(PROJECT_INTENT_FILE);
        let text = toml::to_string_pretty(self).context("serializing project intent")?;
        fs::write(&path, text).with_context(|| format!("writing {}", path.display()))
    }
}

// ───────────────────────────── project reality ─────────────────────────────

/// `.aimgr/state.toml` — gitignored. Records what was wired into this repo.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProjectState {
    #[serde(default)]
    pub packages: BTreeMap<String, ProjectPackage>,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProjectPackage {
    pub installed_at: String,
    /// Pristine block text recorded per file written; used for drift detection.
    #[serde(default)]
    pub blocks: Vec<BlockRecord>,
    /// Permission grants applied for this package; replayed on sync and
    /// subtracted on unuse/purge.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub permissions: Vec<PermissionRecord>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BlockRecord {
    pub tool: Tool,
    pub path: PathBuf,
    /// Exact block text we rendered the last time this package was synced.
    pub pristine: String,
}

/// Records a single application of permission rules from a package into a
/// tool's settings file. `applied` captures exactly what was merged in so
/// `unuse` can subtract the same JSON later, even if the registry has changed.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PermissionRecord {
    pub tool: Tool,
    /// Absolute path to the settings file we wrote to.
    pub path: PathBuf,
    /// JSON object literally merged into the settings file
    /// (`{"permissions": {"allow": [...], "ask": [...], "deny": [...]}}`).
    pub applied: serde_json::Value,
}

impl ProjectState {
    pub fn path(project_root: &Path) -> PathBuf {
        project_root
            .join(PROJECT_STATE_DIR)
            .join(PROJECT_STATE_FILE)
    }

    pub fn load(project_root: &Path) -> Result<Self> {
        let path = Self::path(project_root);
        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, project_root: &Path) -> Result<()> {
        let dir = project_root.join(PROJECT_STATE_DIR);
        fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
        let path = dir.join(PROJECT_STATE_FILE);
        let text = toml::to_string_pretty(self).context("serializing project state")?;
        fs::write(&path, text).with_context(|| format!("writing {}", path.display()))
    }

    pub fn record(&mut self, name: &str, blocks: Vec<BlockRecord>) {
        let permissions = self
            .packages
            .get(name)
            .map(|p| p.permissions.clone())
            .unwrap_or_default();
        self.packages.insert(
            name.to_string(),
            ProjectPackage {
                installed_at: now_rfc3339(),
                blocks,
                permissions,
            },
        );
    }
}

// ───────────────────────────── machine reality ─────────────────────────────

/// `~/.local/state/aimgr/state.toml` — machine-wide install record + projects index.
///
/// Unknown fields are silently dropped on load so pre-0.1.0 state files that
/// carried a `trusted = [...]` list (now in `Config`) still parse; the key is
/// removed on the next save.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct MachineState {
    #[serde(default)]
    pub projects: Vec<PathBuf>,
    #[serde(default)]
    pub tools: BTreeMap<String, MachineInstall>,
    #[serde(default)]
    pub packages: BTreeMap<String, MachineInstall>,
}

/// Shared install record — same shape for both tools and packages, distinguished
/// only by which map they live in.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MachineInstall {
    pub installed_at: String,
    pub method: String,
    #[serde(default)]
    pub version: Option<String>,
    /// Permission grants applied at the user/global scope for this entity.
    /// Empty for tools — currently only packages declare permission grants.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub permissions: Vec<PermissionRecord>,
}

impl MachineState {
    pub fn path() -> Result<PathBuf> {
        Ok(config::state_home()?.join(MACHINE_STATE_FILE))
    }

    pub fn load() -> Result<Self> {
        let path = Self::path()?;
        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<()> {
        let path = Self::path()?;
        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 machine state")?;
        fs::write(&path, text).with_context(|| format!("writing {}", path.display()))
    }

    pub fn record_package(&mut self, name: &str, method: &str, version: Option<String>) {
        let permissions = self
            .packages
            .get(name)
            .map(|m| m.permissions.clone())
            .unwrap_or_default();
        self.packages.insert(
            name.to_string(),
            MachineInstall {
                installed_at: now_rfc3339(),
                method: method.to_string(),
                version,
                permissions,
            },
        );
    }

    pub fn forget_package(&mut self, name: &str) {
        self.packages.remove(name);
    }

    pub fn record_tool(&mut self, name: &str, method: &str, version: Option<String>) {
        let permissions = self
            .tools
            .get(name)
            .map(|m| m.permissions.clone())
            .unwrap_or_default();
        self.tools.insert(
            name.to_string(),
            MachineInstall {
                installed_at: now_rfc3339(),
                method: method.to_string(),
                version,
                permissions,
            },
        );
    }

    pub fn forget_tool(&mut self, name: &str) {
        self.tools.remove(name);
    }

    pub fn register_project(&mut self, path: &Path) {
        let canonical = path.to_path_buf();
        if !self.projects.iter().any(|p| p == &canonical) {
            self.projects.push(canonical);
            self.projects.sort();
        }
    }

    /// Drop project paths whose `.aimgr/` dir is gone or which no longer exist.
    pub fn prune_projects(&mut self) -> Vec<PathBuf> {
        let mut removed = Vec::new();
        self.projects.retain(|p| {
            let keep = p.join(PROJECT_STATE_DIR).exists();
            if !keep {
                removed.push(p.clone());
            }
            keep
        });
        removed
    }
}

fn now_rfc3339() -> String {
    chrono::Utc::now().to_rfc3339()
}

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

    #[test]
    fn project_intent_round_trip() {
        let dir = tempdir().unwrap();
        let intent = ProjectIntent {
            tools: vec![Tool::Claude],
            packages: vec!["seek".into()],
        };
        intent.save(dir.path()).unwrap();
        let loaded = ProjectIntent::load(dir.path()).unwrap();
        assert_eq!(loaded.packages, vec!["seek"]);
    }

    #[test]
    fn project_state_round_trip() {
        let dir = tempdir().unwrap();
        let mut s = ProjectState::default();
        s.record(
            "seek",
            vec![BlockRecord {
                tool: Tool::Claude,
                path: PathBuf::from("CLAUDE.md"),
                pristine: "<!-- BEGIN aimgr: seek -->\nbody\n<!-- END aimgr: seek -->".into(),
            }],
        );
        s.save(dir.path()).unwrap();
        let loaded = ProjectState::load(dir.path()).unwrap();
        let package = &loaded.packages["seek"];
        assert_eq!(package.blocks.len(), 1);
        assert_eq!(package.blocks[0].path, PathBuf::from("CLAUDE.md"));
    }

    fn with_state_home<F: FnOnce()>(home: &Path, f: F) {
        let _guard = crate::test_util::env_lock();
        let key = config::STATE_HOME_ENV;
        let prev = std::env::var_os(key);
        std::env::set_var(key, home);
        f();
        match prev {
            Some(v) => std::env::set_var(key, v),
            None => std::env::remove_var(key),
        }
    }

    #[test]
    fn machine_state_projects_dedupe_and_prune() {
        let home = tempdir().unwrap();
        let proj1 = tempdir().unwrap();
        fs::create_dir_all(proj1.path().join(PROJECT_STATE_DIR)).unwrap();
        let proj2 = tempdir().unwrap();
        // proj2 intentionally missing .aimgr/ — should be pruned.

        with_state_home(home.path(), || {
            let mut m = MachineState::default();
            m.register_project(proj1.path());
            m.register_project(proj1.path()); // dedupe
            m.register_project(proj2.path());
            m.save().unwrap();

            let mut reloaded = MachineState::load().unwrap();
            assert_eq!(reloaded.projects.len(), 2);
            let removed = reloaded.prune_projects();
            assert_eq!(removed.len(), 1);
            assert_eq!(removed[0], proj2.path());
            assert_eq!(reloaded.projects.len(), 1);
        });
    }

    #[test]
    fn machine_state_drops_legacy_trusted_field_on_load() {
        let home = tempdir().unwrap();
        let path = home.path().join(MACHINE_STATE_FILE);
        fs::write(
            &path,
            "[[trusted]]\npackage = \"seek\"\ncommand = \"curl\"\n",
        )
        .unwrap();
        with_state_home(home.path(), || {
            let m = MachineState::load().unwrap();
            assert!(m.packages.is_empty());
            m.save().unwrap();
            let reloaded = fs::read_to_string(&path).unwrap();
            assert!(!reloaded.contains("trusted"));
        });
    }

    #[test]
    fn machine_state_tools_round_trip() {
        let home = tempdir().unwrap();
        with_state_home(home.path(), || {
            let mut m = MachineState::default();
            m.record_tool("claude", "script", Some("1.2.3".into()));
            m.record_package("seek", "curl", None);
            m.save().unwrap();
            let reloaded = MachineState::load().unwrap();
            assert!(reloaded.tools.contains_key("claude"));
            assert_eq!(reloaded.tools["claude"].method, "script");
            assert!(reloaded.packages.contains_key("seek"));
        });
    }
}
