use std::time::{Duration, Instant};

use dashmap::DashMap;
use tracing::{debug, trace};

use crate::types::{SearchRequest, SearchResponse};

/// A cached search result with expiry metadata.
#[derive(Debug, Clone)]
pub struct CachedResult {
    pub response: SearchResponse,
    pub cached_at: Instant,
    pub ttl: Duration,
}

impl CachedResult {
    /// Returns true if this cache entry has not yet expired.
    pub fn is_valid(&self) -> bool {
        self.cached_at.elapsed() < self.ttl
    }
}

/// Thread-safe, TTL-aware search result cache backed by DashMap.
pub struct SearchCache {
    entries: DashMap<String, CachedResult>,
    default_ttl: Duration,
    max_entries: usize,
}

impl SearchCache {
    /// Create a new SearchCache with the given TTL and capacity.
    pub fn new(default_ttl: Duration, max_entries: usize) -> Self {
        debug!(
            ttl_secs = default_ttl.as_secs(),
            max_entries,
            "Initializing SearchCache"
        );
        Self {
            entries: DashMap::new(),
            default_ttl,
            max_entries,
        }
    }

    /// Look up a cached result for the given request.
    /// Returns `None` if missing or expired (expired entries are evicted on access).
    pub fn get(&self, request: &SearchRequest) -> Option<SearchResponse> {
        let key = self.compute_cache_key(request);
        trace!(cache_key = %key, "Cache lookup");

        if let Some(entry) = self.entries.get(&key) {
            if entry.is_valid() {
                debug!(cache_key = %key, "Cache HIT");
                return Some(entry.response.clone());
            }
            // Drop the read reference before removing
            drop(entry);
            debug!(cache_key = %key, "Cache entry expired, evicting");
            self.entries.remove(&key);
        }

        debug!(cache_key = %key, "Cache MISS");
        None
    }

    /// Insert a search response into the cache, using the default TTL.
    /// If the cache is at capacity, the oldest entry is evicted first.
    pub fn put(&self, request: &SearchRequest, response: SearchResponse) {
        let key = self.compute_cache_key(request);
        self.put_with_ttl(request, response, self.default_ttl);
        trace!(cache_key = %key, "Stored in cache with default TTL");
    }

    /// Insert a search response with a custom TTL.
    pub fn put_with_ttl(&self, request: &SearchRequest, response: SearchResponse, ttl: Duration) {
        // Evict if at capacity
        if self.entries.len() >= self.max_entries {
            self.evict_oldest();
        }

        let key = self.compute_cache_key(request);
        let cached = CachedResult {
            response,
            cached_at: Instant::now(),
            ttl,
        };
        self.entries.insert(key, cached);
    }

    /// Remove a specific entry from the cache.
    pub fn invalidate(&self, request: &SearchRequest) {
        let key = self.compute_cache_key(request);
        if self.entries.remove(&key).is_some() {
            debug!(cache_key = %key, "Cache entry invalidated");
        }
    }

    /// Remove all entries from the cache.
    pub fn clear(&self) {
        let count = self.entries.len();
        self.entries.clear();
        debug!(evicted = count, "Cache cleared");
    }

    /// Return the number of entries currently stored (including potentially expired ones).
    pub fn len(&self) -> usize {
        self.entries.len()
    }

    /// Returns true if the cache contains no entries.
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    /// Purge all expired entries. Useful for periodic maintenance.
    pub fn purge_expired(&self) {
        let before = self.entries.len();
        self.entries.retain(|_, v| v.is_valid());
        let evicted = before - self.entries.len();
        if evicted > 0 {
            debug!(evicted, "Purged expired cache entries");
        }
    }

    /// Compute a deterministic cache key from a SearchRequest.
    /// Uses the query text, max_results, file_types, and exclude_paths to form a
    /// unique key that is stable across identical requests.
    pub fn compute_cache_key(&self, request: &SearchRequest) -> String {
        // Build a canonical string from all request fields that affect results.
        let mut key_parts: Vec<String> = Vec::with_capacity(4);
        key_parts.push(request.query.to_lowercase());
        key_parts.push(format!("max:{}", request.max_results));

        if !request.file_types.is_empty() {
            let mut ft = request.file_types.clone();
            ft.sort();
            key_parts.push(format!("ft:{}", ft.join(",")));
        }

        if !request.exclude_paths.is_empty() {
            let mut ep = request.exclude_paths.clone();
            ep.sort();
            key_parts.push(format!("ex:{}", ep.join(",")));
        }

        let canonical = key_parts.join("|");

        // Use a simple hash so keys stay compact in memory.
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};
        let mut hasher = DefaultHasher::new();
        canonical.hash(&mut hasher);
        format!("sc:{:016x}", hasher.finish())
    }

    // ---- internal helpers ----

    /// Evict the single oldest (earliest `cached_at`) entry.
    fn evict_oldest(&self) {
        let mut oldest_key: Option<String> = None;
        let mut oldest_time: Option<Instant> = None;

        for entry in self.entries.iter() {
            match oldest_time {
                None => {
                    oldest_key = Some(entry.key().clone());
                    oldest_time = Some(entry.value().cached_at);
                }
                Some(t) if entry.value().cached_at < t => {
                    oldest_key = Some(entry.key().clone());
                    oldest_time = Some(entry.value().cached_at);
                }
                _ => {}
            }
        }

        if let Some(key) = oldest_key {
            self.entries.remove(&key);
            trace!(cache_key = %key, "Evicted oldest cache entry");
        }
    }
}

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

    fn make_request(query: &str) -> SearchRequest {
        SearchRequest {
            query: query.to_string(),
            ..Default::default()
        }
    }

    fn make_response() -> SearchResponse {
        SearchResponse {
            query_id: String::new(),
            results: vec![],
            source: SearchSource::Cache,
            latency: Duration::ZERO,
            cost: 0.0,
            tier_breakdown: TierBreakdown::default(),
        }
    }

    #[test]
    fn test_cache_hit_and_miss() {
        let cache = SearchCache::new(Duration::from_secs(60), 100);
        let req = make_request("hello world");
        assert!(cache.get(&req).is_none());

        cache.put(&req, make_response());
        assert!(cache.get(&req).is_some());
    }

    #[test]
    fn test_cache_invalidate() {
        let cache = SearchCache::new(Duration::from_secs(60), 100);
        let req = make_request("test query");
        cache.put(&req, make_response());
        assert!(cache.get(&req).is_some());

        cache.invalidate(&req);
        assert!(cache.get(&req).is_none());
    }

    #[test]
    fn test_cache_clear() {
        let cache = SearchCache::new(Duration::from_secs(60), 100);
        for i in 0..5 {
            let req = make_request(&format!("query {i}"));
            cache.put(&req, make_response());
        }
        assert_eq!(cache.len(), 5);
        cache.clear();
        assert!(cache.is_empty());
    }

    #[test]
    fn test_cache_key_deterministic() {
        let cache = SearchCache::new(Duration::from_secs(60), 100);
        let req = make_request("some query");
        let k1 = cache.compute_cache_key(&req);
        let k2 = cache.compute_cache_key(&req);
        assert_eq!(k1, k2);
    }

    #[test]
    fn test_cache_key_case_insensitive() {
        let cache = SearchCache::new(Duration::from_secs(60), 100);
        let r1 = make_request("Hello World");
        let r2 = make_request("hello world");
        assert_eq!(
            cache.compute_cache_key(&r1),
            cache.compute_cache_key(&r2)
        );
    }

    #[test]
    fn test_max_entries_eviction() {
        let cache = SearchCache::new(Duration::from_secs(60), 3);
        for i in 0..5 {
            let req = make_request(&format!("q{i}"));
            cache.put(&req, make_response());
        }
        // Should never exceed max_entries
        assert!(cache.len() <= 3);
    }
}
