use std::fs;
use std::path::PathBuf;

use crate::error::{ConfigError, Result};

/// Manages cached extension schemas from OCI registry
#[derive(Debug, Clone)]
pub struct ExtensionSchemaCache {
    cache_dir: PathBuf,
}

#[derive(Debug, Clone)]
pub struct CachedExtension {
    pub name: String,
    pub version: String,
    pub schema_path: PathBuf,
}

impl ExtensionSchemaCache {
    /// Creates a new extension schema cache manager
    ///
    /// # Arguments
    /// - `base_cache_dir`: Base cache directory (typically
    ///   ~/.cache/provisioning/extensions)
    pub fn new(base_cache_dir: PathBuf) -> Result<Self> {
        // Ensure cache directory exists
        fs::create_dir_all(&base_cache_dir).map_err(|e| {
            ConfigError::io_error(format!(
                "Failed to create extension cache directory {:?}: {}",
                base_cache_dir, e
            ))
        })?;

        Ok(Self {
            cache_dir: base_cache_dir,
        })
    }

    /// Creates a cache at the default location
    /// (~/.cache/provisioning/extensions)
    pub fn new_default() -> Result<Self> {
        let home = std::env::var("HOME")
            .map_err(|_| ConfigError::io_error("HOME environment variable not set"))?;

        let cache_dir = PathBuf::from(format!("{}/.cache/provisioning/extensions", home));
        Self::new(cache_dir)
    }

    /// Lists all cached extensions
    ///
    /// Returns a vector of cached extensions with their paths
    /// Expected structure: {cache_dir}/{name}@{version}/schemas/
    pub fn list_cached_extensions(&self) -> Result<Vec<CachedExtension>> {
        if !self.cache_dir.exists() {
            tracing::warn!(
                "Extension cache directory does not exist: {:?}",
                self.cache_dir
            );
            return Ok(Vec::new());
        }

        let mut extensions = Vec::new();

        match fs::read_dir(&self.cache_dir) {
            Ok(entries) => {
                for entry in entries.flatten() {
                    let path = entry.path();

                    if !path.is_dir() {
                        continue;
                    }

                    // Extract name and version from directory name (format: {name}@{version})
                    let Some(dir_str) = path.file_name().and_then(|n| n.to_str()) else {
                        continue;
                    };
                    let Some((name, version)) = dir_str.split_once('@') else {
                        continue;
                    };
                    let schema_path = path.join("schemas");
                    if !schema_path.exists() {
                        continue;
                    }
                    tracing::debug!(
                        "Found cached extension: {}@{} at {:?}",
                        name,
                        version,
                        schema_path
                    );
                    extensions.push(CachedExtension {
                        name: name.to_string(),
                        version: version.to_string(),
                        schema_path,
                    });
                }
            }
            Err(e) => {
                tracing::warn!(
                    "Failed to read extension cache directory {:?}: {}",
                    self.cache_dir,
                    e
                );
                return Err(ConfigError::io_error(format!(
                    "Failed to read extension cache directory: {}",
                    e
                )));
            }
        }

        Ok(extensions)
    }

    /// Gets the schema path for a specific extension
    pub fn get_extension_path(&self, name: &str, version: Option<&str>) -> Option<PathBuf> {
        match self.list_cached_extensions() {
            Ok(extensions) => {
                if let Some(version) = version {
                    // Find exact version match
                    extensions
                        .iter()
                        .find(|e| e.name == name && e.version == version)
                        .map(|e| e.schema_path.clone())
                } else {
                    // Find latest version (first one found, assuming sorted by version)
                    extensions
                        .iter()
                        .find(|e| e.name == name)
                        .map(|e| e.schema_path.clone())
                }
            }
            Err(e) => {
                tracing::warn!(
                    "Failed to get extension path for {}:{:?}: {}",
                    name,
                    version,
                    e
                );
                None
            }
        }
    }

    /// Prepares paths for NICKEL_IMPORT_PATH by getting all extension schema
    /// directories
    pub fn build_import_paths(&self) -> Result<Vec<PathBuf>> {
        self.list_cached_extensions()
            .map(|extensions| extensions.into_iter().map(|e| e.schema_path).collect())
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::TempDir;

    use super::*;

    #[test]
    fn test_extension_schema_cache_new() {
        let temp_dir = TempDir::new().unwrap();
        let cache = ExtensionSchemaCache::new(temp_dir.path().to_path_buf()).unwrap();

        assert!(temp_dir.path().exists());
        assert_eq!(cache.cache_dir, temp_dir.path());
    }

    #[test]
    fn test_list_cached_extensions_empty() {
        let temp_dir = TempDir::new().unwrap();
        let cache = ExtensionSchemaCache::new(temp_dir.path().to_path_buf()).unwrap();

        let extensions = cache.list_cached_extensions().unwrap();
        assert!(extensions.is_empty());
    }

    #[test]
    fn test_list_cached_extensions_with_content() {
        let temp_dir = TempDir::new().unwrap();
        let cache_path = temp_dir.path();

        // Create mock extension structure
        let ext_dir = cache_path.join("hetzner@1.0.0");
        let schemas_dir = ext_dir.join("schemas");
        fs::create_dir_all(&schemas_dir).unwrap();
        fs::write(schemas_dir.join("main.ncl"), "{}").unwrap();

        let cache = ExtensionSchemaCache::new(cache_path.to_path_buf()).unwrap();
        let extensions = cache.list_cached_extensions().unwrap();

        assert_eq!(extensions.len(), 1);
        assert_eq!(extensions[0].name, "hetzner");
        assert_eq!(extensions[0].version, "1.0.0");
        assert!(extensions[0].schema_path.exists());
    }

    #[test]
    fn test_get_extension_path() {
        let temp_dir = TempDir::new().unwrap();
        let cache_path = temp_dir.path();

        // Create multiple extension versions
        for version in &["1.0.0", "1.1.0", "2.0.0"] {
            let ext_dir = cache_path.join(format!("hetzner@{}", version));
            let schemas_dir = ext_dir.join("schemas");
            fs::create_dir_all(&schemas_dir).unwrap();
            fs::write(schemas_dir.join("main.ncl"), "{}").unwrap();
        }

        let cache = ExtensionSchemaCache::new(cache_path.to_path_buf()).unwrap();

        // Test exact version match
        let path = cache.get_extension_path("hetzner", Some("1.1.0"));
        assert!(path.is_some());
        assert!(path.unwrap().join("main.ncl").exists());

        // Test no version specified (should return first found)
        let path = cache.get_extension_path("hetzner", None);
        assert!(path.is_some());

        // Test non-existent extension
        let path = cache.get_extension_path("nonexistent", None);
        assert!(path.is_none());
    }

    #[test]
    fn test_build_import_paths() {
        let temp_dir = TempDir::new().unwrap();
        let cache_path = temp_dir.path();

        // Create multiple extensions
        for (name, version) in &[("hetzner", "1.0.0"), ("aws", "2.1.0")] {
            let ext_dir = cache_path.join(format!("{}@{}", name, version));
            let schemas_dir = ext_dir.join("schemas");
            fs::create_dir_all(&schemas_dir).unwrap();
        }

        let cache = ExtensionSchemaCache::new(cache_path.to_path_buf()).unwrap();
        let paths = cache.build_import_paths().unwrap();

        assert_eq!(paths.len(), 2);
        for path in paths {
            assert!(path.exists());
        }
    }
}
