//! In-memory cache for recently generated merkle proofs.

use crate::tree::LeafProof;
// `sync::Cache` (not `future::Cache`) — StateTree operations are synchronous.
use moka::sync::Cache;
use std::{
    sync::{
        atomic::{AtomicU64, Ordering},
        Arc,
    },
    time::Duration,
};

const DEFAULT_TTL: Duration = Duration::from_secs(300);

/// In-memory proof cache backed by [`moka::sync::Cache`].
///
/// Keyed by `[u8; 32]` (the JMT key hash), valued by `(version, LeafProof)`.
/// On reads the stored version is compared against the current version counter —
/// stale entries (written before the last `invalidate()`) are discarded.
/// TTL eviction handles natural cleanup.
///
/// Spec §4.4 frames the cache key as `(namespace, leaf_key, state_root)`. Here
/// `namespace + leaf_key` is already absorbed into `key_hash` via the prefix
/// byte, and `version` is the state-root proxy: bumped after every successful
/// redb commit so a stable version equates to a stable root.
#[derive(Clone)]
pub struct ProofCache {
    version: Arc<AtomicU64>,
    inner: Cache<[u8; 32], (u64, LeafProof)>,
}

impl std::fmt::Debug for ProofCache {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ProofCache")
            .field("version", &self.version.load(Ordering::Relaxed))
            .field("entry_count", &self.inner.entry_count())
            .finish_non_exhaustive()
    }
}

impl ProofCache {
    /// Create a new cache with the given maximum capacity and default TTL (5 min).
    pub fn new(max_capacity: u64) -> Self {
        Self::with_ttl(max_capacity, DEFAULT_TTL)
    }

    /// Create a new cache with explicit capacity and TTL.
    pub fn with_ttl(max_capacity: u64, ttl: Duration) -> Self {
        let inner = Cache::builder().max_capacity(max_capacity).time_to_live(ttl).build();
        Self {
            version: Arc::new(AtomicU64::new(0)),
            inner,
        }
    }

    /// Insert a proof into the cache, tagged with the current version.
    ///
    /// Crate-private — callers in `tree::proof()` use `try_insert` instead so
    /// they can detect a concurrent `invalidate()` between proof generation
    /// and insertion. Direct `insert` would silently cache stale bytes against
    /// a since-bumped version.
    pub(crate) fn insert(&self, key_hash: [u8; 32], proof: LeafProof) {
        let v = self.version.load(Ordering::Acquire);
        self.inner.insert(key_hash, (v, proof));
    }

    /// Insert only if the cache version hasn't changed since `expected_version`.
    /// Returns `true` if the entry was inserted.
    pub fn try_insert(&self, key_hash: [u8; 32], proof: LeafProof, expected_version: u64) -> bool {
        let current = self.version.load(Ordering::Acquire);
        if current == expected_version {
            self.inner.insert(key_hash, (current, proof));
            true
        } else {
            false
        }
    }

    /// Retrieve a cached proof if it exists and is not stale.
    ///
    /// Emits `proof_cache_outcomes_total{outcome=...}` with one of:
    /// `hit_fresh`, `hit_stale_evicted`, `miss_empty`. Label values match the
    /// `proof_cache_outcome` module in `crates/metrics/src/lib.rs`.
    pub fn get(&self, key_hash: &[u8; 32]) -> Option<LeafProof> {
        let current_version = self.version.load(Ordering::Acquire);
        let Some((stored_version, proof)) = self.inner.get(key_hash) else {
            crate::metrics::cache_outcome(crate::metrics::OUTCOME_MISS_EMPTY);
            return None;
        };
        if stored_version == current_version {
            crate::metrics::cache_outcome(crate::metrics::OUTCOME_HIT_FRESH);
            Some(proof)
        } else {
            self.inner.invalidate(key_hash);
            crate::metrics::cache_outcome(crate::metrics::OUTCOME_HIT_STALE_EVICTED);
            None
        }
    }

    /// Invalidate all cached proofs by advancing the version.
    ///
    /// Existing entries are lazily evicted on the next `get()` — no eager flush.
    /// Emits `proof_cache_invalidations_total`.
    pub fn invalidate(&self) {
        self.version.fetch_add(1, Ordering::Release);
        crate::metrics::cache_invalidation();
    }

    /// Current cache version (exposed for testing).
    pub fn version(&self) -> u64 {
        self.version.load(Ordering::Acquire)
    }

    /// Number of entries currently in the cache (including stale).
    pub fn entry_count(&self) -> u64 {
        self.inner.entry_count()
    }
}

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

    #[test]
    fn invalidate_advances_version() {
        let cache = ProofCache::new(100);
        assert_eq!(cache.version(), 0);
        cache.invalidate();
        assert_eq!(cache.version(), 1);
        cache.invalidate();
        assert_eq!(cache.version(), 2);
    }
}
