//! Caching layer for Synor storage.
//!
//! Provides LRU caches for frequently accessed data to reduce disk I/O.
//!
//! OPTIMIZATION: Uses the `lru` crate for O(1) get/put operations instead of
//! the O(n) Vec-based implementation. This significantly improves performance
//! for high-throughput scenarios.

use parking_lot::RwLock;
use std::hash::Hash;
use std::num::NonZeroUsize;
use synor_types::{BlockHeader, BlockId, Hash256, Transaction, TransactionId};

use crate::stores::{StoredGhostdagData, StoredRelations, StoredUtxo};

/// A wrapper around the `lru` crate's LruCache with our API.
///
/// This provides O(1) get and put operations using a doubly-linked list
/// with a HashMap for fast lookups.
pub struct LruCache<K, V> {
    inner: lru::LruCache<K, V>,
}

impl<K: Hash + Eq, V: Clone> LruCache<K, V> {
    /// Creates a new LRU cache with the given capacity.
    pub fn new(capacity: usize) -> Self {
        let cap = NonZeroUsize::new(capacity.max(1)).unwrap();
        LruCache {
            inner: lru::LruCache::new(cap),
        }
    }

    /// Gets a value from the cache, updating access order.
    /// O(1) operation.
    pub fn get(&mut self, key: &K) -> Option<V> {
        self.inner.get(key).cloned()
    }

    /// Peeks at a value without updating access order.
    /// O(1) operation.
    pub fn peek(&self, key: &K) -> Option<&V> {
        self.inner.peek(key)
    }

    /// Inserts a value into the cache.
    /// O(1) operation. Returns the evicted value if cache was at capacity.
    pub fn insert(&mut self, key: K, value: V) -> Option<V> {
        self.inner.push(key, value).map(|(_, v)| v)
    }

    /// Removes a value from the cache.
    /// O(1) operation.
    pub fn remove(&mut self, key: &K) -> Option<V> {
        self.inner.pop(key)
    }

    /// Returns the number of entries in the cache.
    pub fn len(&self) -> usize {
        self.inner.len()
    }

    /// Returns true if the cache is empty.
    pub fn is_empty(&self) -> bool {
        self.inner.is_empty()
    }

    /// Clears the cache.
    pub fn clear(&mut self) {
        self.inner.clear();
    }

    /// Returns true if the key is in the cache.
    pub fn contains(&self, key: &K) -> bool {
        self.inner.contains(key)
    }

    /// Returns the capacity of the cache.
    pub fn capacity(&self) -> usize {
        self.inner.cap().get()
    }
}

/// Configuration for the storage cache.
#[derive(Clone, Debug)]
pub struct CacheConfig {
    /// Maximum number of headers to cache.
    pub header_cache_size: usize,
    /// Maximum number of block bodies to cache.
    pub block_cache_size: usize,
    /// Maximum number of transactions to cache.
    pub tx_cache_size: usize,
    /// Maximum number of UTXOs to cache.
    pub utxo_cache_size: usize,
    /// Maximum number of GHOSTDAG entries to cache.
    pub ghostdag_cache_size: usize,
    /// Maximum number of relations to cache.
    pub relations_cache_size: usize,
}

impl Default for CacheConfig {
    fn default() -> Self {
        CacheConfig {
            header_cache_size: 10_000,
            block_cache_size: 1_000,
            tx_cache_size: 50_000,
            utxo_cache_size: 100_000,
            ghostdag_cache_size: 50_000,
            relations_cache_size: 50_000,
        }
    }
}

impl CacheConfig {
    /// Creates a configuration for low-memory environments.
    pub fn low_memory() -> Self {
        CacheConfig {
            header_cache_size: 1_000,
            block_cache_size: 100,
            tx_cache_size: 5_000,
            utxo_cache_size: 10_000,
            ghostdag_cache_size: 5_000,
            relations_cache_size: 5_000,
        }
    }

    /// Creates a configuration for high-memory environments.
    pub fn high_memory() -> Self {
        CacheConfig {
            header_cache_size: 100_000,
            block_cache_size: 10_000,
            tx_cache_size: 500_000,
            utxo_cache_size: 1_000_000,
            ghostdag_cache_size: 500_000,
            relations_cache_size: 500_000,
        }
    }
}

/// UTXO cache key (txid + index).
#[derive(Clone, Hash, Eq, PartialEq)]
pub struct UtxoKey {
    pub txid: TransactionId,
    pub index: u32,
}

impl UtxoKey {
    pub fn new(txid: TransactionId, index: u32) -> Self {
        UtxoKey { txid, index }
    }
}

/// Cache statistics.
#[derive(Clone, Debug, Default)]
pub struct CacheStats {
    /// Number of cache hits.
    pub hits: u64,
    /// Number of cache misses.
    pub misses: u64,
    /// Number of insertions.
    pub insertions: u64,
    /// Number of evictions.
    pub evictions: u64,
}

impl CacheStats {
    /// Returns the hit ratio (0.0 to 1.0).
    pub fn hit_ratio(&self) -> f64 {
        let total = self.hits + self.misses;
        if total == 0 {
            0.0
        } else {
            self.hits as f64 / total as f64
        }
    }
}

/// Combined storage cache for all data types.
pub struct StorageCache {
    /// Header cache.
    headers: RwLock<LruCache<Hash256, BlockHeader>>,
    /// Transaction cache.
    transactions: RwLock<LruCache<TransactionId, Transaction>>,
    /// UTXO cache.
    utxos: RwLock<LruCache<UtxoKey, StoredUtxo>>,
    /// GHOSTDAG data cache.
    ghostdag: RwLock<LruCache<BlockId, StoredGhostdagData>>,
    /// Relations cache.
    relations: RwLock<LruCache<BlockId, StoredRelations>>,
    /// Cache statistics.
    stats: RwLock<CacheStats>,
    /// Configuration.
    config: CacheConfig,
}

impl StorageCache {
    /// Creates a new storage cache with the given configuration.
    pub fn new(config: CacheConfig) -> Self {
        StorageCache {
            headers: RwLock::new(LruCache::new(config.header_cache_size)),
            transactions: RwLock::new(LruCache::new(config.tx_cache_size)),
            utxos: RwLock::new(LruCache::new(config.utxo_cache_size)),
            ghostdag: RwLock::new(LruCache::new(config.ghostdag_cache_size)),
            relations: RwLock::new(LruCache::new(config.relations_cache_size)),
            stats: RwLock::new(CacheStats::default()),
            config,
        }
    }

    /// Creates a new storage cache with default configuration.
    pub fn default_config() -> Self {
        Self::new(CacheConfig::default())
    }

    // Header cache operations

    /// Gets a header from the cache.
    pub fn get_header(&self, hash: &Hash256) -> Option<BlockHeader> {
        let mut cache = self.headers.write();
        let result = cache.get(hash);
        let mut stats = self.stats.write();
        if result.is_some() {
            stats.hits += 1;
        } else {
            stats.misses += 1;
        }
        result
    }

    /// Inserts a header into the cache.
    pub fn insert_header(&self, header: BlockHeader) {
        let hash = header.block_id();
        let mut cache = self.headers.write();
        let was_full = cache.len() >= cache.capacity();
        cache.insert(hash, header);
        let mut stats = self.stats.write();
        stats.insertions += 1;
        if was_full {
            stats.evictions += 1;
        }
    }

    /// Removes a header from the cache.
    pub fn remove_header(&self, hash: &Hash256) {
        let mut cache = self.headers.write();
        cache.remove(hash);
    }

    // Transaction cache operations

    /// Gets a transaction from the cache.
    pub fn get_transaction(&self, txid: &TransactionId) -> Option<Transaction> {
        let mut cache = self.transactions.write();
        let result = cache.get(txid);
        let mut stats = self.stats.write();
        if result.is_some() {
            stats.hits += 1;
        } else {
            stats.misses += 1;
        }
        result
    }

    /// Inserts a transaction into the cache.
    pub fn insert_transaction(&self, tx: Transaction) {
        let txid = tx.txid();
        let mut cache = self.transactions.write();
        cache.insert(txid, tx);
        self.stats.write().insertions += 1;
    }

    /// Removes a transaction from the cache.
    pub fn remove_transaction(&self, txid: &TransactionId) {
        let mut cache = self.transactions.write();
        cache.remove(txid);
    }

    // UTXO cache operations

    /// Gets a UTXO from the cache.
    pub fn get_utxo(&self, txid: &TransactionId, index: u32) -> Option<StoredUtxo> {
        let key = UtxoKey::new(*txid, index);
        let mut cache = self.utxos.write();
        let result = cache.get(&key);
        let mut stats = self.stats.write();
        if result.is_some() {
            stats.hits += 1;
        } else {
            stats.misses += 1;
        }
        result
    }

    /// Inserts a UTXO into the cache.
    pub fn insert_utxo(&self, txid: TransactionId, index: u32, utxo: StoredUtxo) {
        let key = UtxoKey::new(txid, index);
        let mut cache = self.utxos.write();
        cache.insert(key, utxo);
        self.stats.write().insertions += 1;
    }

    /// Removes a UTXO from the cache.
    pub fn remove_utxo(&self, txid: &TransactionId, index: u32) {
        let key = UtxoKey::new(*txid, index);
        let mut cache = self.utxos.write();
        cache.remove(&key);
    }

    // GHOSTDAG cache operations

    /// Gets GHOSTDAG data from the cache.
    pub fn get_ghostdag(&self, block_id: &BlockId) -> Option<StoredGhostdagData> {
        let mut cache = self.ghostdag.write();
        let result = cache.get(block_id);
        let mut stats = self.stats.write();
        if result.is_some() {
            stats.hits += 1;
        } else {
            stats.misses += 1;
        }
        result
    }

    /// Inserts GHOSTDAG data into the cache.
    pub fn insert_ghostdag(&self, block_id: BlockId, data: StoredGhostdagData) {
        let mut cache = self.ghostdag.write();
        cache.insert(block_id, data);
        self.stats.write().insertions += 1;
    }

    /// Removes GHOSTDAG data from the cache.
    pub fn remove_ghostdag(&self, block_id: &BlockId) {
        let mut cache = self.ghostdag.write();
        cache.remove(block_id);
    }

    // Relations cache operations

    /// Gets relations from the cache.
    pub fn get_relations(&self, block_id: &BlockId) -> Option<StoredRelations> {
        let mut cache = self.relations.write();
        let result = cache.get(block_id);
        let mut stats = self.stats.write();
        if result.is_some() {
            stats.hits += 1;
        } else {
            stats.misses += 1;
        }
        result
    }

    /// Inserts relations into the cache.
    pub fn insert_relations(&self, block_id: BlockId, relations: StoredRelations) {
        let mut cache = self.relations.write();
        cache.insert(block_id, relations);
        self.stats.write().insertions += 1;
    }

    /// Removes relations from the cache.
    pub fn remove_relations(&self, block_id: &BlockId) {
        let mut cache = self.relations.write();
        cache.remove(block_id);
    }

    // Statistics and management

    /// Returns cache statistics.
    pub fn stats(&self) -> CacheStats {
        self.stats.read().clone()
    }

    /// Resets cache statistics.
    pub fn reset_stats(&self) {
        *self.stats.write() = CacheStats::default();
    }

    /// Returns the total number of cached entries.
    pub fn total_entries(&self) -> usize {
        self.headers.read().len()
            + self.transactions.read().len()
            + self.utxos.read().len()
            + self.ghostdag.read().len()
            + self.relations.read().len()
    }

    /// Clears all caches.
    pub fn clear(&self) {
        self.headers.write().clear();
        self.transactions.write().clear();
        self.utxos.write().clear();
        self.ghostdag.write().clear();
        self.relations.write().clear();
    }

    /// Returns the configuration.
    pub fn config(&self) -> &CacheConfig {
        &self.config
    }

    /// Returns cache size information.
    pub fn size_info(&self) -> CacheSizeInfo {
        CacheSizeInfo {
            headers: self.headers.read().len(),
            headers_capacity: self.config.header_cache_size,
            transactions: self.transactions.read().len(),
            transactions_capacity: self.config.tx_cache_size,
            utxos: self.utxos.read().len(),
            utxos_capacity: self.config.utxo_cache_size,
            ghostdag: self.ghostdag.read().len(),
            ghostdag_capacity: self.config.ghostdag_cache_size,
            relations: self.relations.read().len(),
            relations_capacity: self.config.relations_cache_size,
        }
    }
}

impl Default for StorageCache {
    fn default() -> Self {
        Self::default_config()
    }
}

/// Cache size information.
#[derive(Clone, Debug)]
pub struct CacheSizeInfo {
    pub headers: usize,
    pub headers_capacity: usize,
    pub transactions: usize,
    pub transactions_capacity: usize,
    pub utxos: usize,
    pub utxos_capacity: usize,
    pub ghostdag: usize,
    pub ghostdag_capacity: usize,
    pub relations: usize,
    pub relations_capacity: usize,
}

impl CacheSizeInfo {
    /// Returns the total number of cached entries.
    pub fn total_entries(&self) -> usize {
        self.headers + self.transactions + self.utxos + self.ghostdag + self.relations
    }

    /// Returns the total capacity.
    pub fn total_capacity(&self) -> usize {
        self.headers_capacity
            + self.transactions_capacity
            + self.utxos_capacity
            + self.ghostdag_capacity
            + self.relations_capacity
    }

    /// Returns the overall fill ratio.
    pub fn fill_ratio(&self) -> f64 {
        self.total_entries() as f64 / self.total_capacity() as f64
    }
}

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

    #[test]
    fn test_lru_cache_basic() {
        let mut cache: LruCache<i32, String> = LruCache::new(3);

        cache.insert(1, "one".to_string());
        cache.insert(2, "two".to_string());
        cache.insert(3, "three".to_string());

        assert_eq!(cache.get(&1), Some("one".to_string()));
        assert_eq!(cache.get(&2), Some("two".to_string()));
        assert_eq!(cache.len(), 3);
    }

    #[test]
    fn test_lru_cache_eviction() {
        let mut cache: LruCache<i32, String> = LruCache::new(3);

        cache.insert(1, "one".to_string());
        cache.insert(2, "two".to_string());
        cache.insert(3, "three".to_string());
        cache.insert(4, "four".to_string()); // Should evict 1

        assert_eq!(cache.get(&1), None);
        assert_eq!(cache.get(&4), Some("four".to_string()));
        assert_eq!(cache.len(), 3);
    }

    #[test]
    fn test_lru_cache_access_order() {
        let mut cache: LruCache<i32, String> = LruCache::new(3);

        cache.insert(1, "one".to_string());
        cache.insert(2, "two".to_string());
        cache.insert(3, "three".to_string());

        // Access 1, making it most recently used
        cache.get(&1);

        // Insert 4, should evict 2 (least recently used)
        cache.insert(4, "four".to_string());

        assert_eq!(cache.get(&1), Some("one".to_string()));
        assert_eq!(cache.get(&2), None);
        assert_eq!(cache.get(&3), Some("three".to_string()));
        assert_eq!(cache.get(&4), Some("four".to_string()));
    }

    #[test]
    fn test_storage_cache_stats() {
        let cache = StorageCache::new(CacheConfig {
            header_cache_size: 10,
            block_cache_size: 10,
            tx_cache_size: 10,
            utxo_cache_size: 10,
            ghostdag_cache_size: 10,
            relations_cache_size: 10,
        });

        // Miss
        let _ = cache.get_utxo(&TransactionId::from_bytes([1u8; 32]), 0);
        assert_eq!(cache.stats().misses, 1);

        // Insert and hit
        cache.insert_utxo(
            TransactionId::from_bytes([1u8; 32]),
            0,
            StoredUtxo {
                amount: 1000,
                script_pubkey: vec![],
                block_daa_score: 0,
                is_coinbase: false,
            },
        );
        let _ = cache.get_utxo(&TransactionId::from_bytes([1u8; 32]), 0);

        let stats = cache.stats();
        assert_eq!(stats.hits, 1);
        assert_eq!(stats.misses, 1);
        assert_eq!(stats.insertions, 1);
    }

    #[test]
    fn test_cache_size_info() {
        let cache = StorageCache::default_config();
        let info = cache.size_info();

        assert_eq!(info.total_entries(), 0);
        assert!(info.total_capacity() > 0);
        assert_eq!(info.fill_ratio(), 0.0);
    }
}
