//! Trust registry cache implementation.
//!
//! Uses platform-specific cache implementations:
//! - **Native**: `mini-moka` with sync/concurrent API and TTL support
//! - **WASM**: `micro-moka` with single-threaded API (manual TTL tracking since
//!   micro-moka doesn't support time-based expiration)

use std::time::Duration;

// WASM: micro-moka uses unsync module and doesn't support TTL,
// so we wrap entries with timestamps for manual expiration
#[cfg(target_arch = "wasm32")]
use {micro_moka::unsync::Cache, std::cell::RefCell, std::rc::Rc};
#[cfg(not(target_arch = "wasm32"))]
use {mini_moka::sync::Cache, std::sync::Arc};

use super::IssuerInfo;

/// In-memory TTL cache for issuer info.
///
/// Caches trust registry lookups to reduce API calls. Default TTL is 60
/// seconds.
///
/// # Platform Differences
///
/// - **Native**: Thread-safe using `mini-moka` sync cache
/// - **WASM**: Single-threaded using `micro-moka` unsync cache (WASM is
///   single-threaded)
#[cfg(not(target_arch = "wasm32"))]
pub struct TrustRegistryCache {
    cache: Arc<Cache<String, IssuerInfo>>,
    #[allow(dead_code)]
    ttl: Duration,
}

/// Cached entry with timestamp for manual TTL tracking (WASM only).
#[cfg(target_arch = "wasm32")]
struct CachedEntry {
    info: IssuerInfo,
    /// Timestamp when entry was inserted (milliseconds since epoch)
    inserted_at_ms: f64,
}

/// In-memory TTL cache for issuer info (WASM version).
///
/// Since micro-moka doesn't support time-based expiration, we manually
/// track timestamps and check expiration on `get()`.
#[cfg(target_arch = "wasm32")]
pub struct TrustRegistryCache {
    cache: Rc<RefCell<Cache<String, CachedEntry>>>,
    ttl_ms: f64,
}

// Native implementation using mini-moka (thread-safe)
#[cfg(not(target_arch = "wasm32"))]
impl TrustRegistryCache {
    /// Create a new cache with the specified TTL and max entries.
    ///
    /// # Arguments
    ///
    /// * `ttl` - Time-to-live for cache entries
    /// * `max_entries` - Maximum number of entries to cache
    #[must_use]
    pub fn new(ttl: Duration, max_entries: usize) -> Self {
        let cache = Cache::builder()
            .max_capacity(max_entries as u64)
            .time_to_live(ttl)
            .build();

        Self {
            cache: Arc::new(cache),
            ttl,
        }
    }

    /// Get an issuer from the cache.
    ///
    /// Returns `None` if the entry is not present or has expired.
    #[must_use]
    pub fn get(&self, did: &str) -> Option<IssuerInfo> {
        self.cache.get(&did.to_string())
    }

    /// Insert an issuer into the cache.
    pub fn insert(&self, did: String, info: IssuerInfo) {
        self.cache.insert(did, info);
    }

    /// Invalidate (remove) a cached issuer.
    pub fn invalidate(&self, did: &str) {
        self.cache.invalidate(&did.to_string());
    }

    /// Clear all cached entries.
    pub fn clear(&self) {
        self.cache.invalidate_all();
    }

    /// Get the number of entries in the cache.
    ///
    /// Note: This calls `sync()` internally to ensure accurate count.
    #[must_use]
    #[allow(clippy::cast_possible_truncation)]
    pub fn len(&self) -> usize {
        use mini_moka::sync::ConcurrentCacheExt;
        self.cache.sync();
        // Safe: cache max_capacity is set to max_entries which fits in usize
        self.cache.entry_count() as usize
    }

    /// Check if the cache is empty.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }
}

// WASM implementation using micro-moka (single-threaded) with manual TTL
#[cfg(target_arch = "wasm32")]
impl TrustRegistryCache {
    /// Create a new cache with the specified TTL and max entries.
    ///
    /// # Arguments
    ///
    /// * `ttl` - Time-to-live for cache entries (manually tracked since
    ///   micro-moka lacks TTL)
    /// * `max_entries` - Maximum number of entries to cache
    #[must_use]
    pub fn new(ttl: Duration, max_entries: usize) -> Self {
        // micro-moka doesn't support TTL, only capacity-based eviction
        let cache = Cache::builder().max_capacity(max_entries as u64).build();

        #[allow(clippy::cast_precision_loss)]
        let ttl_ms = ttl.as_millis() as f64;

        Self {
            cache: Rc::new(RefCell::new(cache)),
            ttl_ms,
        }
    }

    /// Get current time in milliseconds (WASM-compatible).
    fn now_ms() -> f64 {
        js_sys::Date::now()
    }

    /// Get an issuer from the cache.
    ///
    /// Returns `None` if the entry is not present or has expired.
    #[must_use]
    pub fn get(&self, did: &str) -> Option<IssuerInfo> {
        let mut cache = self.cache.borrow_mut();
        let key = did.to_string();

        // micro-moka's get() takes &mut self and returns Option<&V>
        if let Some(entry) = cache.get(&key) {
            let now = Self::now_ms();
            if now - entry.inserted_at_ms < self.ttl_ms {
                return Some(entry.info.clone());
            }
            // Entry expired - remove it
            // Entry reference goes out of scope here
        }
        cache.invalidate(&key);
        None
    }

    /// Insert an issuer into the cache.
    pub fn insert(&self, did: String, info: IssuerInfo) {
        let entry = CachedEntry {
            info,
            inserted_at_ms: Self::now_ms(),
        };
        self.cache.borrow_mut().insert(did, entry);
    }

    /// Invalidate (remove) a cached issuer.
    pub fn invalidate(&self, did: &str) {
        self.cache.borrow_mut().invalidate(&did.to_string());
    }

    /// Clear all cached entries.
    pub fn clear(&self) {
        self.cache.borrow_mut().invalidate_all();
    }

    /// Get the number of entries in the cache.
    ///
    /// Note: May include expired entries that haven't been accessed yet.
    #[must_use]
    #[allow(clippy::cast_possible_truncation)]
    pub fn len(&self) -> usize {
        // Safe: cache max_capacity is set to max_entries which fits in usize
        self.cache.borrow().entry_count() as usize
    }

    /// Check if the cache is empty.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }
}

impl Default for TrustRegistryCache {
    fn default() -> Self {
        Self::new(Duration::from_secs(60), 1000)
    }
}

#[cfg(not(target_arch = "wasm32"))]
impl Clone for TrustRegistryCache {
    fn clone(&self) -> Self {
        Self {
            cache: Arc::clone(&self.cache),
            ttl: self.ttl,
        }
    }
}

#[cfg(target_arch = "wasm32")]
impl Clone for TrustRegistryCache {
    fn clone(&self) -> Self {
        Self {
            cache: Rc::clone(&self.cache),
            ttl_ms: self.ttl_ms,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::trust_registry::{ES256PublicKey, IssuerStatus};

    fn test_issuer() -> IssuerInfo {
        IssuerInfo {
            enterprise_did: "did:lastid:test".into(),
            organization_name: "Test Org".into(),
            endpoint_url: "https://test.com".into(),
            status: IssuerStatus::Authorized,
            permitted_types: vec!["LastID.Base".into()],
            public_key: ES256PublicKey {
                kty: "EC".into(),
                crv: "P-256".into(),
                x: "WbbXWlY0Sdw6eVKiJGnFFAJnGDHVjdVRLWbZGIgn3Go".into(),
                y: "bfIuXkJHLZvVMZ96ekEqhyuSoVVbWBzWqcAzSvhp2gs".into(),
            },
        }
    }

    #[test]
    fn test_cache_insert_and_get() {
        let cache = TrustRegistryCache::default();
        let issuer = test_issuer();

        cache.insert("did:lastid:test".into(), issuer.clone());

        let cached = cache.get("did:lastid:test");
        assert!(cached.is_some());
        assert_eq!(cached.unwrap(), issuer);
    }

    #[test]
    fn test_cache_miss() {
        let cache = TrustRegistryCache::default();
        assert!(cache.get("did:lastid:nonexistent").is_none());
    }

    #[test]
    fn test_cache_invalidate() {
        let cache = TrustRegistryCache::default();
        let issuer = test_issuer();

        cache.insert("did:lastid:test".into(), issuer);
        assert!(cache.get("did:lastid:test").is_some());

        cache.invalidate("did:lastid:test");
        assert!(cache.get("did:lastid:test").is_none());
    }

    #[test]
    fn test_cache_clear() {
        let cache = TrustRegistryCache::default();
        let issuer = test_issuer();

        cache.insert("did:lastid:test1".into(), issuer.clone());
        cache.insert("did:lastid:test2".into(), issuer);

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

        cache.clear();
        assert!(cache.is_empty());
    }

    #[test]
    fn test_cache_clone_shares_data() {
        let cache1 = TrustRegistryCache::default();
        let cache2 = cache1.clone();
        let issuer = test_issuer();

        cache1.insert("did:lastid:test".into(), issuer.clone());

        // Both caches should see the same data
        assert!(cache2.get("did:lastid:test").is_some());
    }
}
