//! MemTable implementation for read-your-writes consistency.
//!
//! The MemTable provides immediate visibility of assertions after WAL commit,
//! before the IngestWorker has processed them into KVStore indexes.
//!
//! # Design
//!
//! - Thread-safe via DashMap for concurrent reads/writes
//! - Three indexes for efficient lookup:
//!   - by_hash: O(1) hash lookup
//!   - by_subject: subject → list of hashes
//!   - by_subject_predicate: (subject, predicate) → list of hashes
//! - Eviction based on WAL offset watermark
//! - Safety valve: age-based eviction for stale entries

use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::time::{Duration, Instant};

use dashmap::DashMap;
use stemedb_core::types::Assertion;
use tracing::{debug, warn};

use super::MemTableEntry;

/// In-memory buffer for assertions pending KVStore indexing.
///
/// Provides read-your-writes consistency: queries merge MemTable with KVStore
/// to ensure recently written assertions are immediately visible.
pub struct MemTable {
    /// Primary index: hash → entry
    by_hash: DashMap<[u8; 32], MemTableEntry>,

    /// Subject index: subject → list of hashes
    by_subject: DashMap<String, Vec<[u8; 32]>>,

    /// Compound index: (subject, predicate) → list of hashes
    by_subject_predicate: DashMap<(String, String), Vec<[u8; 32]>>,

    /// WAL offset up to which assertions have been indexed in KVStore.
    /// Entries with wal_offset <= indexed_offset are safe to evict.
    indexed_offset: AtomicU64,

    /// Maximum entries before triggering aggressive eviction (soft limit).
    max_entries: usize,

    /// Eviction age threshold for safety valve (default: 30 seconds).
    max_age: Duration,

    /// Count of entries (for metrics without locking).
    entry_count: AtomicUsize,
}

impl MemTable {
    /// Create a new MemTable with the specified capacity limit.
    pub fn new(max_entries: usize) -> Self {
        Self {
            by_hash: DashMap::new(),
            by_subject: DashMap::new(),
            by_subject_predicate: DashMap::new(),
            indexed_offset: AtomicU64::new(0),
            max_entries,
            max_age: Duration::from_secs(30),
            entry_count: AtomicUsize::new(0),
        }
    }

    /// Create a MemTable with custom max age for testing.
    #[cfg(test)]
    pub fn with_max_age(max_entries: usize, max_age: Duration) -> Self {
        Self {
            by_hash: DashMap::new(),
            by_subject: DashMap::new(),
            by_subject_predicate: DashMap::new(),
            indexed_offset: AtomicU64::new(0),
            max_entries,
            max_age,
            entry_count: AtomicUsize::new(0),
        }
    }

    /// Insert an entry into the MemTable.
    ///
    /// Updates all indexes atomically. If the hash already exists, the entry
    /// is replaced (idempotent on retry).
    pub fn insert(&self, entry: MemTableEntry) {
        let hash = entry.hash;
        let subject = entry.assertion.subject.clone();
        let predicate = entry.assertion.predicate.clone();

        // Insert into primary index
        let was_new = self.by_hash.insert(hash, entry).is_none();

        if was_new {
            // Update subject index
            self.by_subject.entry(subject.clone()).or_default().push(hash);

            // Update compound index
            self.by_subject_predicate.entry((subject, predicate)).or_default().push(hash);

            self.entry_count.fetch_add(1, Ordering::Relaxed);
        }

        // Check if we need to evict
        if self.len() > self.max_entries {
            self.evict_stale_entries();
        }
    }

    /// Get an entry by its hash.
    pub fn get_by_hash(&self, hash: &[u8; 32]) -> Option<Assertion> {
        self.by_hash.get(hash).map(|entry| entry.assertion.clone())
    }

    /// Get all assertions for a subject.
    pub fn get_by_subject(&self, subject: &str) -> Vec<Assertion> {
        let hashes = match self.by_subject.get(subject) {
            Some(ref_multi) => ref_multi.clone(),
            None => return Vec::new(),
        };

        let mut results = Vec::with_capacity(hashes.len());
        for hash in hashes {
            if let Some(entry) = self.by_hash.get(&hash) {
                results.push(entry.assertion.clone());
            }
        }
        results
    }

    /// Get all assertions for a subject and predicate.
    pub fn get_by_subject_predicate(&self, subject: &str, predicate: &str) -> Vec<Assertion> {
        let key = (subject.to_string(), predicate.to_string());
        let hashes = match self.by_subject_predicate.get(&key) {
            Some(ref_multi) => ref_multi.clone(),
            None => return Vec::new(),
        };

        let mut results = Vec::with_capacity(hashes.len());
        for hash in hashes {
            if let Some(entry) = self.by_hash.get(&hash) {
                results.push(entry.assertion.clone());
            }
        }
        results
    }

    /// Advance the indexed offset watermark.
    ///
    /// Called by IngestWorker after processing records. Entries with
    /// wal_offset <= this value are now in KVStore and can be evicted.
    pub fn advance_indexed_offset(&self, offset: u64) {
        self.indexed_offset.fetch_max(offset, Ordering::Release);
        debug!(offset, "Advanced indexed offset");

        // Trigger eviction after advancing
        self.evict_indexed_entries();
    }

    /// Get current indexed offset.
    pub fn indexed_offset(&self) -> u64 {
        self.indexed_offset.load(Ordering::Acquire)
    }

    /// Evict entries that have been indexed in KVStore.
    ///
    /// Entries with wal_offset <= indexed_offset are safe to remove because
    /// queries will find them via KVStore indexes.
    pub fn evict_indexed_entries(&self) {
        let indexed_up_to = self.indexed_offset.load(Ordering::Acquire);
        if indexed_up_to == 0 {
            return;
        }

        // Collect hashes to evict
        let to_evict: Vec<[u8; 32]> = self
            .by_hash
            .iter()
            .filter(|entry| entry.wal_offset <= indexed_up_to)
            .map(|entry| entry.hash)
            .collect();

        if !to_evict.is_empty() {
            debug!(count = to_evict.len(), indexed_up_to, "Evicting indexed entries");
        }

        for hash in to_evict {
            self.remove_by_hash(&hash);
        }
    }

    /// Evict entries older than max_age (safety valve).
    ///
    /// This prevents unbounded memory growth if IngestWorker is slow or stuck.
    fn evict_stale_entries(&self) {
        let threshold = Instant::now() - self.max_age;

        let to_evict: Vec<[u8; 32]> = self
            .by_hash
            .iter()
            .filter(|entry| entry.inserted_at < threshold)
            .map(|entry| entry.hash)
            .collect();

        if !to_evict.is_empty() {
            warn!(
                count = to_evict.len(),
                max_age_secs = self.max_age.as_secs(),
                "Safety evicting stale entries"
            );
        }

        for hash in to_evict {
            self.remove_by_hash(&hash);
        }
    }

    /// Remove an entry by hash, updating all indexes.
    fn remove_by_hash(&self, hash: &[u8; 32]) {
        if let Some((_, entry)) = self.by_hash.remove(hash) {
            let subject = &entry.assertion.subject;
            let predicate = &entry.assertion.predicate;

            // Update subject index
            if let Some(mut hashes) = self.by_subject.get_mut(subject) {
                hashes.retain(|h| h != hash);
            }

            // Update compound index
            let key = (subject.clone(), predicate.clone());
            if let Some(mut hashes) = self.by_subject_predicate.get_mut(&key) {
                hashes.retain(|h| h != hash);
            }

            self.entry_count.fetch_sub(1, Ordering::Relaxed);
        }
    }

    /// Get current entry count.
    pub fn len(&self) -> usize {
        self.entry_count.load(Ordering::Relaxed)
    }

    /// Check if empty.
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// Clear all entries (for testing).
    #[cfg(test)]
    pub fn clear(&self) {
        self.by_hash.clear();
        self.by_subject.clear();
        self.by_subject_predicate.clear();
        self.entry_count.store(0, Ordering::Relaxed);
    }
}

impl Default for MemTable {
    fn default() -> Self {
        Self::new(10_000)
    }
}
