//! Search results caching functionality

use crate::constants::APP_NAME;
use crate::error::{LazyDomainError, Result};
use crate::types::CheckResult;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// Represents a cached search query with its results
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedSearch {
    /// The search query (domain name)
    pub query: String,
    /// Timestamp when the search was performed
    pub timestamp: DateTime<Utc>,
    /// The search results
    pub results: Vec<CheckResult>,
}

impl CachedSearch {
    /// Creates a new cached search
    pub fn new(query: impl Into<String>, results: Vec<CheckResult>) -> Self {
        CachedSearch {
            query: query.into(),
            timestamp: Utc::now(),
            results,
        }
    }

    /// Gets the count of results
    pub fn result_count(&self) -> usize {
        self.results.len()
    }

    /// Gets the count of available domains
    pub fn available_count(&self) -> usize {
        self.results
            .iter()
            .filter(|r| r.status == crate::types::CheckStatus::Available)
            .count()
    }
}

/// Cache manager for storing and retrieving search history
#[derive(Debug)]
pub struct CacheManager {
    /// Path to the cache file
    cache_path: PathBuf,
}

impl CacheManager {
    /// Creates a new cache manager
    pub fn new() -> Result<Self> {
        let cache_path = Self::get_cache_path()?;
        
        // Ensure cache directory exists
        if let Some(parent) = cache_path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        Ok(CacheManager { cache_path })
    }

    /// Gets the path to the cache file
    fn get_cache_path() -> Result<PathBuf> {
        dirs::config_dir()
            .map(|p| p.join(APP_NAME).join("search_cache.json"))
            .ok_or_else(|| {
                LazyDomainError::ConfigError("Could not determine config directory".to_string())
            })
    }

    /// Loads all cached searches from disk
    pub fn load(&self) -> Result<Vec<CachedSearch>> {
        if !self.cache_path.exists() {
            return Ok(Vec::new());
        }

        let content = std::fs::read_to_string(&self.cache_path)?;
        
        // Handle empty file or whitespace-only content
        if content.trim().is_empty() {
            return Ok(Vec::new());
        }

        let searches: Vec<CachedSearch> = serde_json::from_str(&content)
            .map_err(|e| LazyDomainError::ConfigError(format!("Failed to parse cache: {}", e)))?;

        Ok(searches)
    }

    /// Saves all cached searches to disk
    fn save(&self, searches: &[CachedSearch]) -> Result<()> {
        let content = serde_json::to_string_pretty(searches)
            .map_err(|e| LazyDomainError::ConfigError(format!("Failed to serialize cache: {}", e)))?;

        std::fs::write(&self.cache_path, content)?;

        Ok(())
    }

    /// Adds a new search to the cache
    pub fn add_search(&self, query: impl Into<String>, results: Vec<CheckResult>) -> Result<()> {
        let mut searches = self.load()?;
        let cached_search = CachedSearch::new(query, results);
        
        // Add to beginning of list (most recent first)
        searches.insert(0, cached_search);

        // Limit cache size to prevent unbounded growth (keep last 100 searches)
        const MAX_CACHE_SIZE: usize = 100;
        if searches.len() > MAX_CACHE_SIZE {
            searches.truncate(MAX_CACHE_SIZE);
        }

        self.save(&searches)?;

        Ok(())
    }

    /// Clears all cached searches
    pub fn clear(&self) -> Result<()> {
        if self.cache_path.exists() {
            std::fs::remove_file(&self.cache_path)?;
        }
        Ok(())
    }

    /// Gets the total number of cached searches
    #[allow(dead_code)]
    pub fn count(&self) -> Result<usize> {
        Ok(self.load()?.len())
    }

    /// Finds a cached search by query
    pub fn find_by_query(&self, query: &str) -> Result<Option<CachedSearch>> {
        let searches = self.load()?;
        Ok(searches.into_iter().find(|s| s.query.eq_ignore_ascii_case(query)))
    }

    /// Deletes a specific cached search by index
    pub fn delete_by_index(&self, index: usize) -> Result<()> {
        let mut searches = self.load()?;
        
        if index < searches.len() {
            searches.remove(index);
            self.save(&searches)?;
        }
        
        Ok(())
    }
}

impl Default for CacheManager {
    fn default() -> Self {
        Self::new().expect("Failed to create cache manager")
    }
}

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

    fn create_test_result(domain: &str, tld: &str, status: CheckStatus) -> CheckResult {
        CheckResult {
            domain: domain.to_string(),
            tld: tld.to_string(),
            status,
            registrar: None,
            expiry_date: None,
            error_message: None,
            checked_at: Utc::now(),
        }
    }

    #[test]
    fn test_cached_search_creation() {
        let results = vec![
            create_test_result("example", "com", CheckStatus::Taken),
            create_test_result("example", "io", CheckStatus::Available),
        ];

        let cached = CachedSearch::new("example", results.clone());

        assert_eq!(cached.query, "example");
        assert_eq!(cached.result_count(), 2);
        assert_eq!(cached.available_count(), 1);
    }

    #[test]
    fn test_cached_search_available_count() {
        let results = vec![
            create_test_result("test", "com", CheckStatus::Taken),
            create_test_result("test", "io", CheckStatus::Available),
            create_test_result("test", "net", CheckStatus::Available),
            create_test_result("test", "org", CheckStatus::Error),
        ];

        let cached = CachedSearch::new("test", results);

        assert_eq!(cached.result_count(), 4);
        assert_eq!(cached.available_count(), 2);
    }
}
