//! Decision cache with TTL and scope support.
//!
//! **Status**: Implemented (TOOLS-PERM-02)
//!
//! This module provides thread-safe caching of permission decisions with:
//! - TTL (time-to-live) for cached decisions
//! - Scope support (per-connector or per-session)
//! - Automatic expiration of stale entries
//! - Hash-based cache keys for efficient lookups

use crate::config::DecisionScope;
use std::collections::HashMap;
use std::hash::Hash;
use std::time::{Duration, Instant};

/// Permission decision outcome.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PermissionDecision {
    Allowed,
    Denied,
    Cancelled,
}

/// Cache key for permission decisions.
///
/// Keys are constructed from:
/// - Operation kind (read, write, execute)
/// - Normalized path or command
/// - Scope identifier (connector_id or session_id)
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey {
    /// Operation discriminant
    operation_kind: OperationKind,
    /// Normalized path or command string
    target: String,
    /// Scope identifier (connector or session)
    scope_id: String,
}

/// Operation kind for cache key discrimination.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum OperationKind {
    Read,
    Write,
    Execute,
}

impl CacheKey {
    /// Create a cache key for a read operation.
    pub fn read(path: &str, connector_id: &str, scope: DecisionScope) -> Self {
        Self {
            operation_kind: OperationKind::Read,
            target: path.to_string(),
            scope_id: Self::scope_id(connector_id, None, scope),
        }
    }

    /// Create a cache key for a write operation.
    pub fn write(path: &str, connector_id: &str, session_id: Option<&str>, scope: DecisionScope) -> Self {
        Self {
            operation_kind: OperationKind::Write,
            target: path.to_string(),
            scope_id: Self::scope_id(connector_id, session_id, scope),
        }
    }

    /// Create a cache key for an execute operation.
    pub fn execute(command: &str, connector_id: &str, session_id: Option<&str>, scope: DecisionScope) -> Self {
        Self {
            operation_kind: OperationKind::Execute,
            target: command.to_string(),
            scope_id: Self::scope_id(connector_id, session_id, scope),
        }
    }

    /// Construct scope identifier based on scope policy.
    fn scope_id(connector_id: &str, session_id: Option<&str>, scope: DecisionScope) -> String {
        match scope {
            DecisionScope::PerConnector => connector_id.to_string(),
            DecisionScope::PerSession => {
                format!("{}:{}", connector_id, session_id.unwrap_or("default"))
            }
        }
    }
}

/// Cached permission decision with expiration.
#[derive(Debug, Clone)]
struct CachedDecision {
    /// The permission decision
    decision: PermissionDecision,
    /// When this entry expires
    expires_at: Instant,
}

impl CachedDecision {
    /// Create a new cached decision with TTL.
    fn new(decision: PermissionDecision, ttl: Duration) -> Self {
        Self {
            decision,
            expires_at: Instant::now() + ttl,
        }
    }

    /// Check if this entry has expired.
    fn is_expired(&self) -> bool {
        Instant::now() >= self.expires_at
    }
}

/// Thread-safe decision cache with TTL support.
///
/// The cache stores permission decisions keyed by operation, path/command, and scope.
/// Entries automatically expire after their TTL, and expired entries are pruned on access.
///
/// ## Thread Safety
///
/// The cache is designed to be wrapped in `Arc<Mutex<DecisionCache>>` for thread-safe access.
/// Individual operations (get, insert, clear) should acquire the lock briefly.
///
/// ## Example
///
/// ```rust
/// use dirigent_tools::permission::cache::{DecisionCache, PermissionDecision, CacheKey};
/// use dirigent_tools::config::DecisionScope;
/// use std::time::Duration;
///
/// let mut cache = DecisionCache::new();
/// let key = CacheKey::write("/path/to/file", "connector-1", None, DecisionScope::PerConnector);
///
/// // Cache a decision
/// cache.insert(key.clone(), PermissionDecision::Allowed, Duration::from_secs(300));
///
/// // Retrieve it
/// assert_eq!(cache.get(&key), Some(PermissionDecision::Allowed));
/// ```
#[derive(Debug)]
pub struct DecisionCache {
    entries: HashMap<CacheKey, CachedDecision>,
}

impl DecisionCache {
    /// Create a new empty decision cache.
    pub fn new() -> Self {
        Self {
            entries: HashMap::new(),
        }
    }

    /// Get a cached decision if it exists and hasn't expired.
    ///
    /// Expired entries are automatically removed.
    ///
    /// Returns `Some(decision)` if a valid cached entry exists, `None` otherwise.
    pub fn get(&mut self, key: &CacheKey) -> Option<PermissionDecision> {
        // Check if entry exists
        if let Some(cached) = self.entries.get(key) {
            // Check if expired
            if cached.is_expired() {
                // Remove expired entry
                self.entries.remove(key);
                None
            } else {
                // Return valid decision
                Some(cached.decision)
            }
        } else {
            None
        }
    }

    /// Insert a new decision into the cache with the given TTL.
    ///
    /// If an entry already exists for this key, it will be replaced.
    pub fn insert(&mut self, key: CacheKey, decision: PermissionDecision, ttl: Duration) {
        self.entries.insert(key, CachedDecision::new(decision, ttl));
    }

    /// Clear all cached decisions.
    ///
    /// Useful for manual cache invalidation or testing.
    pub fn clear(&mut self) {
        self.entries.clear();
    }

    /// Remove all expired entries from the cache.
    ///
    /// This is automatically done during `get()` operations, but can be called
    /// periodically to prune the cache and free memory.
    ///
    /// Returns the number of expired entries removed.
    pub fn clear_expired(&mut self) -> usize {
        let before = self.entries.len();
        self.entries.retain(|_, cached| !cached.is_expired());
        before - self.entries.len()
    }

    /// Get the number of cached entries (including expired).
    ///
    /// For testing and monitoring purposes.
    pub fn len(&self) -> usize {
        self.entries.len()
    }

    /// Check if the cache is empty.
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }
}

impl Default for DecisionCache {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn test_cache_key_creation() {
        let key1 = CacheKey::write("/path/to/file", "conn-1", None, DecisionScope::PerConnector);
        let key2 = CacheKey::write("/path/to/file", "conn-1", None, DecisionScope::PerConnector);
        let key3 = CacheKey::write("/path/to/file", "conn-2", None, DecisionScope::PerConnector);

        // Same connector and path should produce equal keys
        assert_eq!(key1, key2);

        // Different connector should produce different keys
        assert_ne!(key1, key3);
    }

    #[test]
    fn test_cache_key_scope() {
        let key_connector = CacheKey::write(
            "/path",
            "conn-1",
            Some("session-1"),
            DecisionScope::PerConnector,
        );
        let key_session = CacheKey::write(
            "/path",
            "conn-1",
            Some("session-1"),
            DecisionScope::PerSession,
        );

        // Different scopes should produce different keys
        assert_ne!(key_connector, key_session);
    }

    #[test]
    fn test_cache_key_operation_kind() {
        let key_read = CacheKey::read("/path", "conn-1", DecisionScope::PerConnector);
        let key_write = CacheKey::write("/path", "conn-1", None, DecisionScope::PerConnector);

        // Different operations should produce different keys
        assert_ne!(key_read, key_write);
    }

    #[test]
    fn test_cache_insert_and_get() {
        let mut cache = DecisionCache::new();
        let key = CacheKey::write("/path", "conn-1", None, DecisionScope::PerConnector);

        // Initially empty
        assert_eq!(cache.get(&key), None);

        // Insert decision
        cache.insert(key.clone(), PermissionDecision::Allowed, Duration::from_secs(300));

        // Should retrieve it
        assert_eq!(cache.get(&key), Some(PermissionDecision::Allowed));
    }

    #[test]
    fn test_cache_ttl_expiration() {
        let mut cache = DecisionCache::new();
        let key = CacheKey::write("/path", "conn-1", None, DecisionScope::PerConnector);

        // Insert with very short TTL
        cache.insert(key.clone(), PermissionDecision::Allowed, Duration::from_millis(1));

        // Wait for expiration
        std::thread::sleep(Duration::from_millis(10));

        // Should not retrieve expired entry
        assert_eq!(cache.get(&key), None);

        // Entry should be removed from cache
        assert_eq!(cache.len(), 0);
    }

    #[test]
    fn test_cache_clear() {
        let mut cache = DecisionCache::new();
        let key1 = CacheKey::write("/path1", "conn-1", None, DecisionScope::PerConnector);
        let key2 = CacheKey::write("/path2", "conn-1", None, DecisionScope::PerConnector);

        cache.insert(key1.clone(), PermissionDecision::Allowed, Duration::from_secs(300));
        cache.insert(key2.clone(), PermissionDecision::Denied, Duration::from_secs(300));

        assert_eq!(cache.len(), 2);

        // Clear all entries
        cache.clear();

        assert_eq!(cache.len(), 0);
        assert_eq!(cache.get(&key1), None);
        assert_eq!(cache.get(&key2), None);
    }

    #[test]
    fn test_cache_clear_expired() {
        let mut cache = DecisionCache::new();
        let key1 = CacheKey::write("/path1", "conn-1", None, DecisionScope::PerConnector);
        let key2 = CacheKey::write("/path2", "conn-1", None, DecisionScope::PerConnector);

        // Insert one short-lived and one long-lived entry
        cache.insert(key1.clone(), PermissionDecision::Allowed, Duration::from_millis(1));
        cache.insert(key2.clone(), PermissionDecision::Denied, Duration::from_secs(300));

        assert_eq!(cache.len(), 2);

        // Wait for first to expire
        std::thread::sleep(Duration::from_millis(10));

        // Clear expired entries
        let removed = cache.clear_expired();
        assert_eq!(removed, 1);
        assert_eq!(cache.len(), 1);

        // Second entry should still be accessible
        assert_eq!(cache.get(&key2), Some(PermissionDecision::Denied));
    }

    #[test]
    fn test_cache_replace_entry() {
        let mut cache = DecisionCache::new();
        let key = CacheKey::write("/path", "conn-1", None, DecisionScope::PerConnector);

        // Insert initial decision
        cache.insert(key.clone(), PermissionDecision::Allowed, Duration::from_secs(300));
        assert_eq!(cache.get(&key), Some(PermissionDecision::Allowed));

        // Replace with different decision
        cache.insert(key.clone(), PermissionDecision::Denied, Duration::from_secs(300));
        assert_eq!(cache.get(&key), Some(PermissionDecision::Denied));

        // Should only have one entry
        assert_eq!(cache.len(), 1);
    }

    #[test]
    fn test_cache_different_sessions() {
        let mut cache = DecisionCache::new();
        let key_session1 = CacheKey::write(
            "/path",
            "conn-1",
            Some("session-1"),
            DecisionScope::PerSession,
        );
        let key_session2 = CacheKey::write(
            "/path",
            "conn-1",
            Some("session-2"),
            DecisionScope::PerSession,
        );

        // Insert different decisions for different sessions
        cache.insert(key_session1.clone(), PermissionDecision::Allowed, Duration::from_secs(300));
        cache.insert(key_session2.clone(), PermissionDecision::Denied, Duration::from_secs(300));

        // Each session should have its own decision
        assert_eq!(cache.get(&key_session1), Some(PermissionDecision::Allowed));
        assert_eq!(cache.get(&key_session2), Some(PermissionDecision::Denied));
    }

    #[test]
    fn test_cache_cancelled_decision() {
        let mut cache = DecisionCache::new();
        let key = CacheKey::write("/path", "conn-1", None, DecisionScope::PerConnector);

        // Cancelled decisions can also be cached
        cache.insert(key.clone(), PermissionDecision::Cancelled, Duration::from_secs(300));
        assert_eq!(cache.get(&key), Some(PermissionDecision::Cancelled));
    }
}
