use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;

use anyhow::Result;
use rusqlite::{params, Connection};

use crate::plugin::{CleanAction, CleanTarget, Safety};

pub struct Cache {
    conn: Connection,
}

/// JSON-serializable representation of a RunCommand action.
#[derive(serde::Serialize, serde::Deserialize)]
struct RunCommandData {
    command: String,
    args: Vec<String>,
    description: String,
}

pub struct LifetimeStats {
    pub total_freed: u64,
    pub total_runs: u64,
    pub total_targets: u64,
}

impl Cache {
    pub fn open() -> Result<Self> {
        let dir = cache_dir();
        fs::create_dir_all(&dir)?;
        let db_path = dir.join("index.db");
        let conn = Connection::open(&db_path)?;
        Self::init_schema(&conn)?;
        Ok(Cache { conn })
    }

    fn init_schema(conn: &Connection) -> Result<()> {
        conn.execute_batch(
            "CREATE TABLE IF NOT EXISTS targets (
                path        TEXT PRIMARY KEY,
                label       TEXT NOT NULL,
                size_bytes  INTEGER NOT NULL,
                plugin_name TEXT NOT NULL,
                safety      TEXT NOT NULL,
                action_type TEXT NOT NULL,
                action_data TEXT NOT NULL,
                mtime_secs  INTEGER NOT NULL,
                scanned_at  INTEGER NOT NULL,
                explanation TEXT NOT NULL DEFAULT ''
            );
            CREATE INDEX IF NOT EXISTS idx_plugin ON targets(plugin_name);
            CREATE TABLE IF NOT EXISTS history (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                freed_bytes  INTEGER NOT NULL,
                target_count INTEGER NOT NULL,
                error_count  INTEGER NOT NULL,
                timestamp    INTEGER NOT NULL
            );",
        )?;
        // Migration: add explanation column to existing databases
        let _ = conn
            .execute_batch("ALTER TABLE targets ADD COLUMN explanation TEXT NOT NULL DEFAULT ''");
        Ok(())
    }

    /// Load all cached targets for the given plugin names.
    /// Validates each entry: path must still exist.
    /// Stale entries are removed. Returns valid targets.
    pub fn load_targets(&self, plugin_names: &[&str], min_size: u64) -> Vec<CleanTarget> {
        let mut targets = Vec::new();
        let mut stale_paths = Vec::new();

        let placeholders: String = plugin_names
            .iter()
            .map(|_| "?")
            .collect::<Vec<_>>()
            .join(",");
        let query = format!(
            "SELECT path, label, size_bytes, plugin_name, safety, action_type, action_data, mtime_secs, explanation
             FROM targets WHERE plugin_name IN ({}) AND size_bytes >= ?",
            placeholders
        );

        let mut stmt = match self.conn.prepare(&query) {
            Ok(s) => s,
            Err(_) => return targets,
        };

        let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = plugin_names
            .iter()
            .map(|s| Box::new(s.to_string()) as Box<dyn rusqlite::types::ToSql>)
            .collect();
        param_values.push(Box::new(min_size as i64));

        let params_ref: Vec<&dyn rusqlite::types::ToSql> =
            param_values.iter().map(|p| p.as_ref()).collect();

        let rows = match stmt.query_map(params_ref.as_slice(), |row| {
            Ok(CachedRow {
                path: row.get(0)?,
                label: row.get(1)?,
                size_bytes: row.get(2)?,
                plugin_name: row.get(3)?,
                safety: row.get(4)?,
                action_type: row.get(5)?,
                action_data: row.get(6)?,
                mtime_secs: row.get(7)?,
                explanation: row.get(8)?,
            })
        }) {
            Ok(r) => r,
            Err(_) => return targets,
        };

        for row in rows.flatten() {
            // Validate DeletePath/DeleteContents entries.
            if row.action_type == "delete" || row.action_type == "delete_contents" {
                let path = Path::new(&row.action_data);
                if !path.exists() {
                    stale_paths.push(row.path.clone());
                    continue;
                }
                // Mtime freshness: if the directory has been modified since caching,
                // discard the entry so the scanner re-discovers it with a fresh size.
                if let Some(current_mtime) = mtime_secs(path) {
                    if current_mtime != row.mtime_secs {
                        stale_paths.push(row.path.clone());
                        continue;
                    }
                }
            }

            if let Some(target) = row.into_clean_target() {
                targets.push(target);
            }
        }

        // Clean up stale entries
        for path_str in &stale_paths {
            let _ = self
                .conn
                .execute("DELETE FROM targets WHERE path = ?1", params![path_str]);
        }

        targets
    }

    /// Returns true if the cache has any entries for the given plugins.
    pub fn has_entries(&self, plugin_names: &[&str]) -> bool {
        let placeholders: String = plugin_names
            .iter()
            .map(|_| "?")
            .collect::<Vec<_>>()
            .join(",");
        let query = format!(
            "SELECT COUNT(*) FROM targets WHERE plugin_name IN ({})",
            placeholders
        );
        let mut stmt = match self.conn.prepare(&query) {
            Ok(s) => s,
            Err(_) => return false,
        };
        let params: Vec<Box<dyn rusqlite::types::ToSql>> = plugin_names
            .iter()
            .map(|s| Box::new(s.to_string()) as Box<dyn rusqlite::types::ToSql>)
            .collect();
        let params_ref: Vec<&dyn rusqlite::types::ToSql> =
            params.iter().map(|p| p.as_ref()).collect();
        stmt.query_row(params_ref.as_slice(), |row| row.get::<_, i64>(0))
            .unwrap_or(0)
            > 0
    }

    /// Store a target in the cache.
    pub fn put(&self, target: &CleanTarget) {
        let (action_type, action_data, path_key) = match &target.action {
            CleanAction::DeletePath(p) => (
                "delete".to_string(),
                p.to_string_lossy().to_string(),
                p.clone(),
            ),
            CleanAction::DeleteContents(p) => (
                "delete_contents".to_string(),
                p.to_string_lossy().to_string(),
                p.clone(),
            ),
            CleanAction::RunCommand {
                command,
                args,
                description,
            } => {
                let data = serde_json::to_string(&RunCommandData {
                    command: command.clone(),
                    args: args.clone(),
                    description: description.clone(),
                })
                .unwrap_or_default();
                let key = PathBuf::from(format!("__cmd__:{}", description));
                ("command".to_string(), data, key)
            }
        };

        let path_str = path_key.to_string_lossy().to_string();
        let mtime = match &target.action {
            CleanAction::DeletePath(p) | CleanAction::DeleteContents(p) => {
                mtime_secs(p).unwrap_or(0)
            }
            CleanAction::RunCommand { .. } => 0,
        };
        let safety_str = match target.safety {
            Safety::Recoverable => "safe",
            Safety::Unsafe => "unsafe",
        };
        let now = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .unwrap()
            .as_secs() as i64;

        let _ = self.conn.execute(
            "INSERT OR REPLACE INTO targets (path, label, size_bytes, plugin_name, safety, action_type, action_data, mtime_secs, scanned_at, explanation)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
            params![path_str, target.label, target.size_bytes as i64, target.plugin_name, safety_str, action_type, action_data, mtime, now, target.explanation],
        );
    }

    /// Clear the entire cache (for --rescan).
    pub fn clear(&self) -> Result<()> {
        self.conn.execute("DELETE FROM targets", [])?;
        Ok(())
    }

    /// Remove a path from the cache (e.g. after cleaning).
    pub fn remove(&self, path: &Path) -> Result<()> {
        let path_str = path.to_string_lossy();
        self.conn.execute(
            "DELETE FROM targets WHERE path = ?1",
            params![path_str.as_ref()],
        )?;
        Ok(())
    }

    /// Remove entries older than 90 days.
    pub fn prune_stale(&self) {
        let cutoff = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .unwrap()
            .as_secs() as i64
            - 90 * 24 * 3600;

        let _ = self
            .conn
            .execute("DELETE FROM targets WHERE scanned_at < ?1", params![cutoff]);
    }

    /// Record a completed cleanup run in the history table.
    pub fn record_run(&self, freed_bytes: u64, target_count: usize, error_count: usize) {
        let now = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .unwrap()
            .as_secs() as i64;
        let _ = self.conn.execute(
            "INSERT INTO history (freed_bytes, target_count, error_count, timestamp) VALUES (?1, ?2, ?3, ?4)",
            params![freed_bytes as i64, target_count as i64, error_count as i64, now],
        );
    }

    /// Return lifetime statistics across all recorded runs.
    pub fn lifetime_stats(&self) -> Option<LifetimeStats> {
        let result = self.conn.query_row(
            "SELECT COALESCE(SUM(freed_bytes), 0), COUNT(*), COALESCE(SUM(target_count), 0) FROM history",
            [],
            |row| {
                Ok(LifetimeStats {
                    total_freed: row.get::<_, i64>(0)? as u64,
                    total_runs: row.get::<_, i64>(1)? as u64,
                    total_targets: row.get::<_, i64>(2)? as u64,
                })
            },
        );
        match result {
            Ok(stats) if stats.total_runs > 0 => Some(stats),
            _ => None,
        }
    }
}

struct CachedRow {
    path: String,
    label: String,
    size_bytes: i64,
    plugin_name: String,
    safety: String,
    action_type: String,
    action_data: String,
    mtime_secs: i64,
    explanation: String,
}

impl CachedRow {
    fn into_clean_target(self) -> Option<CleanTarget> {
        let safety = match self.safety.as_str() {
            "safe" => Safety::Recoverable,
            "unsafe" | "permanent" => Safety::Unsafe,
            _ => return None,
        };

        let action = match self.action_type.as_str() {
            "delete" => CleanAction::DeletePath(PathBuf::from(&self.action_data)),
            "delete_contents" => CleanAction::DeleteContents(PathBuf::from(&self.action_data)),
            "command" => {
                // Try JSON first, fall back to legacy pipe-delimited format
                if let Ok(data) = serde_json::from_str::<RunCommandData>(&self.action_data) {
                    CleanAction::RunCommand {
                        command: data.command,
                        args: data.args,
                        description: data.description,
                    }
                } else {
                    let parts: Vec<&str> = self.action_data.splitn(3, '|').collect();
                    if parts.len() != 3 {
                        return None;
                    }
                    CleanAction::RunCommand {
                        command: parts[0].to_string(),
                        args: parts[1].split('\x1f').map(|s| s.to_string()).collect(),
                        description: parts[2].to_string(),
                    }
                }
            }
            _ => return None,
        };

        Some(CleanTarget {
            label: self.label,
            size_bytes: self.size_bytes as u64,
            action,
            plugin_name: self.plugin_name,
            safety,
            explanation: self.explanation,
        })
    }
}

fn cache_dir() -> PathBuf {
    dirs::home_dir()
        .expect("could not determine home directory")
        .join(".rasa")
}

fn mtime_secs(path: &Path) -> Option<i64> {
    let meta = fs::metadata(path).ok()?;
    let mtime = meta.modified().ok()?;
    let secs = mtime.duration_since(SystemTime::UNIX_EPOCH).ok()?.as_secs();
    Some(secs as i64)
}

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

    fn test_cache() -> Cache {
        let conn = Connection::open_in_memory().unwrap();
        Cache::init_schema(&conn).unwrap();
        Cache { conn }
    }

    #[test]
    fn put_and_has_entries() {
        let cache = test_cache();
        assert!(!cache.has_entries(&["cargo"]));

        let target = CleanTarget {
            label: "test target".to_string(),
            size_bytes: 1_000_000,
            action: CleanAction::DeletePath(PathBuf::from("/tmp/test-rasa-nonexistent")),
            plugin_name: "cargo".to_string(),
            safety: Safety::Recoverable,
            explanation: "test".to_string(),
        };
        cache.put(&target);
        assert!(cache.has_entries(&["cargo"]));
        assert!(!cache.has_entries(&["node"]));
    }

    #[test]
    fn put_and_load_command_target() {
        let cache = test_cache();

        let target = CleanTarget {
            label: "docker prune".to_string(),
            size_bytes: 5_000_000,
            action: CleanAction::RunCommand {
                command: "docker".to_string(),
                args: vec!["system".to_string(), "prune".to_string()],
                description: "docker system prune".to_string(),
            },
            plugin_name: "docker".to_string(),
            safety: Safety::Unsafe,
            explanation: "test".to_string(),
        };
        cache.put(&target);

        let loaded = cache.load_targets(&["docker"], 0);
        assert_eq!(loaded.len(), 1);
        assert_eq!(loaded[0].label, "docker prune");
        assert_eq!(loaded[0].size_bytes, 5_000_000);
        assert_eq!(loaded[0].safety, Safety::Unsafe);
        match &loaded[0].action {
            CleanAction::RunCommand {
                command,
                args,
                description,
            } => {
                assert_eq!(command, "docker");
                assert_eq!(args, &["system", "prune"]);
                assert_eq!(description, "docker system prune");
            }
            _ => panic!("expected RunCommand"),
        }
    }

    #[test]
    fn remove_entry() {
        let cache = test_cache();

        let target = CleanTarget {
            label: "test".to_string(),
            size_bytes: 1000,
            action: CleanAction::DeletePath(PathBuf::from("/tmp/test-rasa-rm")),
            plugin_name: "cargo".to_string(),
            safety: Safety::Recoverable,
            explanation: "test".to_string(),
        };
        cache.put(&target);
        assert!(cache.has_entries(&["cargo"]));

        cache.remove(Path::new("/tmp/test-rasa-rm")).unwrap();
        assert!(!cache.has_entries(&["cargo"]));
    }

    #[test]
    fn clear_cache() {
        let cache = test_cache();

        let target = CleanTarget {
            label: "test".to_string(),
            size_bytes: 1000,
            action: CleanAction::DeletePath(PathBuf::from("/tmp/test-rasa-clear")),
            plugin_name: "node".to_string(),
            safety: Safety::Recoverable,
            explanation: "test".to_string(),
        };
        cache.put(&target);
        cache.clear().unwrap();
        assert!(!cache.has_entries(&["node"]));
    }

    #[test]
    fn record_and_read_history() {
        let cache = test_cache();

        assert!(cache.lifetime_stats().is_none());

        cache.record_run(1_000_000, 5, 1);
        cache.record_run(2_000_000, 3, 0);

        let stats = cache.lifetime_stats().unwrap();
        assert_eq!(stats.total_freed, 3_000_000);
        assert_eq!(stats.total_runs, 2);
        assert_eq!(stats.total_targets, 8);
    }

    #[test]
    fn min_size_filter() {
        let cache = test_cache();

        let small = CleanTarget {
            label: "small".to_string(),
            size_bytes: 100,
            action: CleanAction::RunCommand {
                command: "echo".to_string(),
                args: vec![],
                description: "small cmd".to_string(),
            },
            plugin_name: "test".to_string(),
            safety: Safety::Recoverable,
            explanation: "test".to_string(),
        };
        let big = CleanTarget {
            label: "big".to_string(),
            size_bytes: 10_000,
            action: CleanAction::RunCommand {
                command: "echo".to_string(),
                args: vec![],
                description: "big cmd".to_string(),
            },
            plugin_name: "test".to_string(),
            safety: Safety::Recoverable,
            explanation: "test".to_string(),
        };
        cache.put(&small);
        cache.put(&big);

        let loaded = cache.load_targets(&["test"], 1000);
        assert_eq!(loaded.len(), 1);
        assert_eq!(loaded[0].label, "big");
    }
}
