// Package cache provides in-memory L1 cache with true O(1) LRU eviction.
//
// Implementation uses a doubly-linked list + hash-map, giving O(1) for Get, Set,
// Delete and eviction — compared to the previous O(n) slice-scan approach which
// became a bottleneck under high concurrency (10 000-item cache, 1 000+ QPS).
//
// Thread-safety: a single sync.RWMutex guards the whole structure.
// Reads (Get) promote the entry to MRU and therefore must take a write lock.
// If read-heavy workloads dominate, consider a sharded variant.
package cache

import (
	"container/list"
	"sync"
	"time"
)

const (
	// defaultMaxItems is the maximum number of entries held in L1Cache.
	// Entries beyond this limit are evicted using LRU policy (O(1)).
	defaultMaxItems = 10000
)

// CacheItem holds a cached value together with its expiry timestamp.
type CacheItem struct {
	Value      interface{}
	Expiration int64 // UnixNano; 0 means no expiration
}

// Expired reports whether this item has passed its TTL.
func (item *CacheItem) Expired() bool {
	return item.Expiration > 0 && time.Now().UnixNano() > item.Expiration
}

// lruEntry is the value stored inside the doubly-linked list element.
type lruEntry struct {
	key  string
	item *CacheItem
}

// L1Cache is an in-process LRU cache backed by a hash-map and a doubly-linked
// list.  All exported methods are safe for concurrent use.
type L1Cache struct {
	mu       sync.RWMutex
	items    map[string]*list.Element // key → list element
	lruList  *list.List               // front = MRU, back = LRU
	maxItems int
}

// NewL1Cache creates a new L1Cache with the default capacity (10 000 items).
func NewL1Cache() *L1Cache {
	return &L1Cache{
		items:    make(map[string]*list.Element, defaultMaxItems),
		lruList:  list.New(),
		maxItems: defaultMaxItems,
	}
}

// NewL1CacheWithSize creates a new L1Cache with a custom capacity.
func NewL1CacheWithSize(maxItems int) *L1Cache {
	if maxItems <= 0 {
		maxItems = defaultMaxItems
	}
	return &L1Cache{
		items:    make(map[string]*list.Element, maxItems),
		lruList:  list.New(),
		maxItems: maxItems,
	}
}

// Set inserts or updates key with the given value and TTL.
// A zero or negative TTL means the entry never expires.
// O(1) amortised.
func (c *L1Cache) Set(key string, value interface{}, ttl time.Duration) {
	var expiration int64
	if ttl > 0 {
		expiration = time.Now().Add(ttl).UnixNano()
	}

	c.mu.Lock()
	defer c.mu.Unlock()

	if elem, ok := c.items[key]; ok {
		// Update existing entry and move to front (MRU).
		c.lruList.MoveToFront(elem)
		entry := elem.Value.(*lruEntry)
		entry.item = &CacheItem{Value: value, Expiration: expiration}
		return
	}

	// Evict LRU entry if at capacity.
	if c.lruList.Len() >= c.maxItems {
		c.evictLRU()
	}

	// Insert new entry at front.
	entry := &lruEntry{
		key:  key,
		item: &CacheItem{Value: value, Expiration: expiration},
	}
	elem := c.lruList.PushFront(entry)
	c.items[key] = elem
}

// evictLRU removes the least-recently-used entry.  Must be called with c.mu held.
func (c *L1Cache) evictLRU() {
	back := c.lruList.Back()
	if back == nil {
		return
	}
	entry := back.Value.(*lruEntry)
	delete(c.items, entry.key)
	c.lruList.Remove(back)
}

// Get retrieves a value from the cache.
// On a hit the entry is promoted to MRU (requires write lock).
// On expiry the entry is removed and (nil, false) is returned.
// O(1).
func (c *L1Cache) Get(key string) (interface{}, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()

	elem, ok := c.items[key]
	if !ok {
		return nil, false
	}

	entry := elem.Value.(*lruEntry)
	if entry.item.Expired() {
		c.lruList.Remove(elem)
		delete(c.items, key)
		return nil, false
	}

	// Promote to MRU.
	c.lruList.MoveToFront(elem)
	return entry.item.Value, true
}

// Delete removes a key from the cache.  No-op if the key is absent.
// O(1).
func (c *L1Cache) Delete(key string) {
	c.mu.Lock()
	defer c.mu.Unlock()

	if elem, ok := c.items[key]; ok {
		c.lruList.Remove(elem)
		delete(c.items, key)
	}
}

// Clear removes all entries from the cache.
func (c *L1Cache) Clear() {
	c.mu.Lock()
	defer c.mu.Unlock()

	c.items = make(map[string]*list.Element, c.maxItems)
	c.lruList.Init()
}

// Size returns the number of entries currently held (including potentially
// expired ones that have not yet been evicted).
func (c *L1Cache) Size() int {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return len(c.items)
}

// Cleanup scans all entries and removes those that have expired.
// This is a background maintenance operation; normal eviction is lazy (on Get).
func (c *L1Cache) Cleanup() {
	c.mu.Lock()
	defer c.mu.Unlock()

	now := time.Now().UnixNano()
	var toRemove []*list.Element
	for elem := c.lruList.Back(); elem != nil; elem = elem.Prev() {
		entry := elem.Value.(*lruEntry)
		if entry.item.Expiration > 0 && now > entry.item.Expiration {
			toRemove = append(toRemove, elem)
		}
	}
	for _, elem := range toRemove {
		entry := elem.Value.(*lruEntry)
		delete(c.items, entry.key)
		c.lruList.Remove(elem)
	}
}

// Increment atomically adds delta to the int64 counter stored at key,
// creating it with value delta if it does not exist.
// Used for rate-limit counters, login-failure counters, etc.
// O(1).
func (c *L1Cache) Increment(key string, delta int64, ttl time.Duration) int64 {
	var expiration int64
	if ttl > 0 {
		expiration = time.Now().Add(ttl).UnixNano()
	}

	c.mu.Lock()
	defer c.mu.Unlock()

	if elem, ok := c.items[key]; ok {
		entry := elem.Value.(*lruEntry)
		if !entry.item.Expired() {
			current := int64(0)
			switch v := entry.item.Value.(type) {
			case int64:
				current = v
			case int:
				current = int64(v)
			case float64:
				current = int64(v)
			}
			newVal := current + delta
			entry.item = &CacheItem{Value: newVal, Expiration: expiration}
			c.lruList.MoveToFront(elem)
			return newVal
		}
		// Expired: remove and recreate below.
		c.lruList.Remove(elem)
		delete(c.items, key)
	}

	// Key absent or expired: insert fresh counter.
	if c.lruList.Len() >= c.maxItems {
		c.evictLRU()
	}
	entry := &lruEntry{
		key:  key,
		item: &CacheItem{Value: delta, Expiration: expiration},
	}
	elem := c.lruList.PushFront(entry)
	c.items[key] = elem
	return delta
}
