//! Smart build cache system for incremental compilation
//!
//! Provides intelligent caching of build-time generated files to avoid
//! regenerating unchanged content. Uses manifest cache_build_path as canonical
//! source for all generated build artifacts.

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

use serde::{Deserialize, Serialize};

/// Cache metadata for a set of generated files
#[derive(Serialize, Deserialize, Default)]
pub struct CacheMetadata {
    pub files: HashMap<String, FileMeta>,
}

/// Metadata for a single cached file
#[derive(Serialize, Deserialize)]
pub struct FileMeta {
    pub source_hash: String,
    pub timestamp: String,
    pub dependencies: Vec<String>,
}

/// Result of cache freshness check
#[derive(Debug)]
pub enum CacheStatus {
    Fresh(PathBuf),   // File exists and is up to date
    Stale(PathBuf),   // File exists but needs regeneration
    Missing(PathBuf), // File doesn't exist, needs generation
}

/// Smart cache for build artifacts
pub struct SmartCache {
    cache_root: PathBuf,
}

impl SmartCache {
    /// Create new SmartCache instance
    pub fn new(cache_type: &str) -> Result<Self, Box<dyn std::error::Error>> {
        let cache_root = get_project_cache_path(cache_type, None)?;

        // Ensure cache directory exists
        fs::create_dir_all(&cache_root)?;

        Ok(SmartCache { cache_root })
    }

    /// Create target-specific cache (for tools like config_constants.rs)
    pub fn new_with_target(
        cache_type: &str,
        target: &str,
    ) -> Result<Self, Box<dyn std::error::Error>> {
        let cache_root = get_project_cache_path(cache_type, Some(target))?;

        // Ensure cache directory exists
        fs::create_dir_all(&cache_root)?;

        Ok(SmartCache { cache_root })
    }

    /// Check if cached file is fresh (up to date)
    pub fn check_file_cache(
        &self,
        filename: &str,
        source_hash: &str,
    ) -> Result<CacheStatus, Box<dyn std::error::Error>> {
        let cache_file_path = self.cache_root.join(filename);

        if !cache_file_path.exists() {
            return Ok(CacheStatus::Missing(cache_file_path));
        }

        // Load metadata
        let metadata = self.load_metadata()?;

        match metadata.files.get(filename) {
            Some(file_meta) if file_meta.source_hash == source_hash => {
                Ok(CacheStatus::Fresh(cache_file_path))
            }
            Some(_) => Ok(CacheStatus::Stale(cache_file_path)),
            None => Ok(CacheStatus::Missing(cache_file_path)),
        }
    }

    /// Copy file from cache to destination
    pub fn copy_from_cache(
        &self,
        filename: &str,
        dest_dir: &Path,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let cache_file = self.cache_root.join(filename);
        let dest_file = dest_dir.join(filename);

        if let Some(parent) = dest_file.parent() {
            fs::create_dir_all(parent)?;
        }

        fs::copy(&cache_file, &dest_file)?;
        Ok(())
    }

    /// Update cache with new content and copy to destination
    pub fn update_cache(
        &self,
        filename: &str,
        content: &str,
        source_hash: &str,
        dest_dir: &Path,
        dependencies: Vec<String>,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let cache_file = self.cache_root.join(filename);
        let dest_file = dest_dir.join(filename);

        // Write to cache (canonical location)
        fs::write(&cache_file, content)?;

        // Copy to destination (OUT_DIR for compilation)
        if let Some(parent) = dest_file.parent() {
            fs::create_dir_all(parent)?;
        }
        fs::write(&dest_file, content)?;

        // Update metadata
        let mut metadata = self.load_metadata()?;
        metadata.files.insert(
            filename.to_string(),
            FileMeta {
                source_hash: source_hash.to_string(),
                timestamp: format_timestamp(SystemTime::now())?,
                dependencies,
            },
        );
        self.save_metadata(&metadata)?;

        Ok(())
    }

    /// Load cache metadata
    fn load_metadata(&self) -> Result<CacheMetadata, Box<dyn std::error::Error>> {
        let meta_file = self.cache_root.join("meta.json");

        if !meta_file.exists() {
            return Ok(CacheMetadata::default());
        }

        let content = fs::read_to_string(&meta_file)?;
        let metadata: CacheMetadata = serde_json::from_str(&content)?;
        Ok(metadata)
    }

    /// Save cache metadata
    fn save_metadata(&self, metadata: &CacheMetadata) -> Result<(), Box<dyn std::error::Error>> {
        let meta_file = self.cache_root.join("meta.json");
        let content = serde_json::to_string_pretty(metadata)?;
        fs::write(&meta_file, content)?;
        Ok(())
    }

    /// Get cache root directory
    pub fn cache_root(&self) -> &Path {
        &self.cache_root
    }
}

/// Generate hash for content
pub fn hash_content(content: &str) -> String {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};

    let mut hasher = DefaultHasher::new();
    content.hash(&mut hasher);
    format!("{:x}", hasher.finish())
}

/// Generate hash for route configuration
pub fn hash_route_config(route_content: &str, dependencies: &[String]) -> String {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};

    let mut hasher = DefaultHasher::new();
    route_content.hash(&mut hasher);

    // Include dependency file contents in hash
    for dep_path in dependencies {
        if let Ok(dep_content) = fs::read_to_string(dep_path) {
            dep_content.hash(&mut hasher);
        }
    }

    format!("{:x}", hasher.finish())
}

/// Format system time as ISO string
fn format_timestamp(time: SystemTime) -> Result<String, Box<dyn std::error::Error>> {
    let duration = time.duration_since(SystemTime::UNIX_EPOCH)?;
    let secs = duration.as_secs();

    // Simple timestamp format - could use chrono for better formatting
    Ok(format!("unix_{}", secs))
}

/// Get project cache path, PAP-compliant using manifest configuration
fn get_project_cache_path(
    cache_type: &str,
    target: Option<&str>,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
    // PAP-compliant: Use manifest resolver for cache path configuration
    // let resolver = rustelo_utils::manifest::get_manifest();
    let cache_base = rustelo_utils::manifest::cache_build_path();

    let mut cache_path = cache_base.join(cache_type);

    if let Some(target) = target {
        cache_path = cache_path.join(target);
    }

    Ok(cache_path)
}

/// Find project root by looking for workspace Cargo.toml
fn find_project_root(start_path: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
    let mut current = start_path;

    loop {
        let cargo_toml = current.join("Cargo.toml");
        if cargo_toml.exists() {
            // Check if this is a workspace root (contains [workspace])
            if let Ok(content) = fs::read_to_string(&cargo_toml) {
                if content.contains("[workspace]") {
                    return Ok(current.to_path_buf());
                }
            }
        }

        // Go up one level
        if let Some(parent) = current.parent() {
            current = parent;
        } else {
            // If we can't find workspace root, use the original manifest dir's grandparent
            // This handles the typical crates/{crate_name} structure
            return Ok(start_path
                .parent()
                .and_then(|p| p.parent())
                .unwrap_or(start_path)
                .to_path_buf());
        }
    }
}

/// Check if cache is available (PAP-compliant)
pub fn is_cache_available() -> bool {
    // Always return true - we use manifest-based cache path resolution
    // which provides proper fallback configuration
    true
}

/// Foundation item cache management for pages and components
impl SmartCache {
    /// Cache foundation pages discovery results
    pub fn cache_foundation_pages(
        &self,
        pages: &[crate::build::foundation_discovery::FoundationItem],
    ) -> Result<(), Box<dyn std::error::Error>> {
        let content = serde_json::to_string_pretty(pages)?;
        let source_hash = hash_content(&content);

        let dest_dir = std::path::PathBuf::from("/tmp"); // Not used for foundation cache
        self.update_cache(
            "foundation_pages.json",
            &content,
            &source_hash,
            &dest_dir,
            vec!["foundation_discovery".to_string()],
        )?;

        Ok(())
    }

    /// Cache foundation components discovery results
    pub fn cache_foundation_components(
        &self,
        components: &[crate::build::foundation_discovery::FoundationItem],
    ) -> Result<(), Box<dyn std::error::Error>> {
        let content = serde_json::to_string_pretty(components)?;
        let source_hash = hash_content(&content);

        let dest_dir = std::path::PathBuf::from("/tmp"); // Not used for foundation cache
        self.update_cache(
            "foundation_components.json",
            &content,
            &source_hash,
            &dest_dir,
            vec!["foundation_discovery".to_string()],
        )?;

        Ok(())
    }

    /// Load cached foundation pages
    pub fn load_foundation_pages(
        &self,
    ) -> Result<Vec<crate::build::foundation_discovery::FoundationItem>, Box<dyn std::error::Error>>
    {
        let cache_file = self.cache_root.join("foundation_pages.json");
        if !cache_file.exists() {
            return Ok(Vec::new());
        }

        let content = fs::read_to_string(&cache_file)?;
        let pages: Vec<crate::build::foundation_discovery::FoundationItem> =
            serde_json::from_str(&content)?;
        Ok(pages)
    }

    /// Load cached foundation components
    pub fn load_foundation_components(
        &self,
    ) -> Result<Vec<crate::build::foundation_discovery::FoundationItem>, Box<dyn std::error::Error>>
    {
        let cache_file = self.cache_root.join("foundation_components.json");
        if !cache_file.exists() {
            return Ok(Vec::new());
        }

        let content = fs::read_to_string(&cache_file)?;
        let components: Vec<crate::build::foundation_discovery::FoundationItem> =
            serde_json::from_str(&content)?;
        Ok(components)
    }

    /// Check if foundation discovery cache is fresh
    pub fn is_foundation_cache_fresh(
        &self,
        discovery_hash: &str,
    ) -> Result<bool, Box<dyn std::error::Error>> {
        match self.check_file_cache("foundation_pages.json", discovery_hash)? {
            CacheStatus::Fresh(_) => {
                match self.check_file_cache("foundation_components.json", discovery_hash)? {
                    CacheStatus::Fresh(_) => Ok(true),
                    _ => Ok(false),
                }
            }
            _ => Ok(false),
        }
    }
}

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

    use tempfile::TempDir;

    use super::*;

    #[test]
    fn test_hash_content() {
        let content1 = "hello world";
        let content2 = "hello world";
        let content3 = "different content";

        assert_eq!(hash_content(content1), hash_content(content2));
        assert_ne!(hash_content(content1), hash_content(content3));
    }

    #[test]
    fn test_project_root_resolution() {
        // Test that we can find a project root (may fail in some test environments)
        let temp_dir = TempDir::new().unwrap();
        env::set_var("CARGO_MANIFEST_DIR", temp_dir.path());
        env::set_var("SITE_DEVTOOLS_PATH", "target/site_build/devtools");

        // This should not panic even if project root detection fails
        let result = SmartCache::new("test");
        // In test environment, this might fail, so we just check it doesn't panic
        // Both Ok and Err are acceptable outcomes
        let _ = result;
    }
}
