//! Bounded LRU entity cache for near-instant detail view reopens.
//!
//! Caches `IssueDetail` / `MrDetail` payloads keyed on [`EntityKey`].
//! Tick-based LRU eviction keeps the most-recently-accessed entries alive
//! while bounding memory usage. Selective invalidation removes only
//! stale entries after a sync, rather than flushing the whole cache.
//!
//! Single-threaded (TUI event loop) — no `Arc`/`Mutex` needed.

use std::collections::HashMap;

use crate::message::EntityKey;

/// Default entity cache capacity (sufficient for drill-in/out workflows).
const DEFAULT_CAPACITY: usize = 64;

/// Bounded LRU cache keyed on [`EntityKey`].
///
/// Each entry stores its value alongside a monotonic tick recording the
/// last access time. On capacity overflow, the entry with the lowest
/// tick (least recently used) is evicted.
pub struct EntityCache<V> {
    entries: HashMap<EntityKey, (V, u64)>,
    capacity: usize,
    tick: u64,
}

impl<V> EntityCache<V> {
    /// Create a new cache with the default capacity (64).
    #[must_use]
    pub fn new() -> Self {
        Self {
            entries: HashMap::with_capacity(DEFAULT_CAPACITY),
            capacity: DEFAULT_CAPACITY,
            tick: 0,
        }
    }

    /// Create a new cache with the given capacity.
    ///
    /// # Panics
    /// Panics if `capacity` is zero.
    #[must_use]
    pub fn with_capacity(capacity: usize) -> Self {
        assert!(capacity > 0, "EntityCache capacity must be > 0");
        Self {
            entries: HashMap::with_capacity(capacity),
            capacity,
            tick: 0,
        }
    }

    /// Look up an entry, bumping its access tick to keep it alive.
    pub fn get(&mut self, key: &EntityKey) -> Option<&V> {
        self.tick += 1;
        let tick = self.tick;
        self.entries.get_mut(key).map(|(val, t)| {
            *t = tick;
            &*val
        })
    }

    /// Insert an entry, evicting the least-recently-accessed entry if at capacity.
    pub fn put(&mut self, key: EntityKey, value: V) {
        self.tick += 1;
        let tick = self.tick;

        // If key already exists, just update in place.
        if let Some(entry) = self.entries.get_mut(&key) {
            *entry = (value, tick);
            return;
        }

        // Evict LRU if at capacity.
        if self.entries.len() >= self.capacity
            && let Some(lru_key) = self
                .entries
                .iter()
                .min_by_key(|(_, (_, t))| *t)
                .map(|(k, _)| k.clone())
        {
            self.entries.remove(&lru_key);
        }

        self.entries.insert(key, (value, tick));
    }

    /// Remove only the specified keys, leaving all other entries intact.
    pub fn invalidate(&mut self, keys: &[EntityKey]) {
        for key in keys {
            self.entries.remove(key);
        }
    }

    /// Number of entries currently cached.
    #[must_use]
    pub fn len(&self) -> usize {
        self.entries.len()
    }

    /// Whether the cache is empty.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }
}

impl<V> Default for EntityCache<V> {
    fn default() -> Self {
        Self::new()
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    fn issue(iid: i64) -> EntityKey {
        EntityKey::issue(1, iid)
    }

    fn mr(iid: i64) -> EntityKey {
        EntityKey::mr(1, iid)
    }

    #[test]
    fn test_get_returns_recently_put_item() {
        let mut cache = EntityCache::with_capacity(4);
        cache.put(issue(1), "issue-1");
        assert_eq!(cache.get(&issue(1)), Some(&"issue-1"));
    }

    #[test]
    fn test_get_returns_none_for_missing_key() {
        let mut cache: EntityCache<&str> = EntityCache::with_capacity(4);
        assert_eq!(cache.get(&issue(99)), None);
    }

    #[test]
    fn test_lru_eviction_removes_least_recently_used() {
        let mut cache = EntityCache::with_capacity(3);
        cache.put(issue(1), "a"); // tick 1
        cache.put(issue(2), "b"); // tick 2
        cache.put(issue(3), "c"); // tick 3

        // Access issue(1) to bump its tick above issue(2).
        cache.get(&issue(1)); // tick 4 -> issue(1) now most recent

        // Insert a 4th item: should evict issue(2) (tick 2, lowest).
        cache.put(issue(4), "d"); // tick 5

        assert_eq!(
            cache.get(&issue(1)),
            Some(&"a"),
            "issue(1) should survive (recently accessed)"
        );
        assert_eq!(
            cache.get(&issue(2)),
            None,
            "issue(2) should be evicted (LRU)"
        );
        assert_eq!(cache.get(&issue(3)), Some(&"c"), "issue(3) should survive");
        assert_eq!(cache.get(&issue(4)), Some(&"d"), "issue(4) just inserted");
    }

    #[test]
    fn test_put_overwrites_existing_key() {
        let mut cache = EntityCache::with_capacity(4);
        cache.put(issue(1), "v1");
        cache.put(issue(1), "v2");
        assert_eq!(cache.get(&issue(1)), Some(&"v2"));
        assert_eq!(cache.len(), 1);
    }

    #[test]
    fn test_invalidate_removes_only_specified_keys() {
        let mut cache = EntityCache::with_capacity(8);
        cache.put(issue(1), "a");
        cache.put(issue(2), "b");
        cache.put(mr(3), "c");
        cache.put(mr(4), "d");

        cache.invalidate(&[issue(2), mr(4)]);

        assert_eq!(cache.get(&issue(1)), Some(&"a"), "issue(1) not invalidated");
        assert_eq!(cache.get(&issue(2)), None, "issue(2) was invalidated");
        assert_eq!(cache.get(&mr(3)), Some(&"c"), "mr(3) not invalidated");
        assert_eq!(cache.get(&mr(4)), None, "mr(4) was invalidated");
    }

    #[test]
    fn test_invalidate_with_nonexistent_keys_is_noop() {
        let mut cache = EntityCache::with_capacity(4);
        cache.put(issue(1), "a");
        cache.invalidate(&[issue(99), mr(99)]);
        assert_eq!(cache.len(), 1);
    }

    #[test]
    fn test_default_capacity_is_64() {
        let cache: EntityCache<String> = EntityCache::new();
        assert_eq!(cache.capacity, DEFAULT_CAPACITY);
        assert_eq!(cache.capacity, 64);
    }

    #[test]
    fn test_len_and_is_empty() {
        let mut cache = EntityCache::with_capacity(4);
        assert!(cache.is_empty());
        assert_eq!(cache.len(), 0);

        cache.put(issue(1), "a");
        assert!(!cache.is_empty());
        assert_eq!(cache.len(), 1);
    }

    #[test]
    #[should_panic(expected = "capacity must be > 0")]
    fn test_zero_capacity_panics() {
        let _: EntityCache<String> = EntityCache::with_capacity(0);
    }

    #[test]
    fn test_mixed_entity_kinds() {
        let mut cache = EntityCache::with_capacity(4);
        // Same iid, different kinds — should be separate entries.
        cache.put(issue(42), "issue-42");
        cache.put(mr(42), "mr-42");

        assert_eq!(cache.get(&issue(42)), Some(&"issue-42"));
        assert_eq!(cache.get(&mr(42)), Some(&"mr-42"));
        assert_eq!(cache.len(), 2);
    }
}
