package middleware

import (
	"sync"
	"sync/atomic"
	"time"

	"github.com/treytartt/honeydue-api/internal/models"
)

// userCacheEntry holds a cached user record with an expiration time.
type userCacheEntry struct {
	user      *models.User
	expiresAt time.Time
}

// UserCache is a concurrency-safe in-memory cache for User records, keyed by
// user ID. Entries expire after a configurable TTL. The cache uses a sync.Map
// for lock-free reads on the hot path, with periodic lazy eviction of stale
// entries during Set operations and a hard size cap to bound memory.
type UserCache struct {
	store    sync.Map
	ttl      time.Duration
	lastGC   time.Time
	gcMu     sync.Mutex
	gcEvery  time.Duration
	size     atomic.Int64 // approximate count; sync.Map has no Len()
	maxSize  int64
}

// NewUserCache creates a UserCache with the given TTL for entries.
// maxSize is the soft upper bound on the number of cached users; when
// exceeded, the next Set will trigger an eviction sweep before storing.
// Pass <=0 for no size cap.
func NewUserCache(ttl time.Duration, maxSize int) *UserCache {
	c := &UserCache{
		ttl:     ttl,
		lastGC:  time.Now(),
		gcEvery: 2 * time.Minute,
		maxSize: int64(maxSize),
	}
	return c
}

// Get returns a cached user by ID, or nil if not found or expired.
func (c *UserCache) Get(userID uint) *models.User {
	val, ok := c.store.Load(userID)
	if !ok {
		return nil
	}
	entry := val.(*userCacheEntry)
	if time.Now().After(entry.expiresAt) {
		if _, loaded := c.store.LoadAndDelete(userID); loaded {
			c.size.Add(-1)
		}
		return nil
	}
	// Return a shallow copy so callers cannot mutate the cached value.
	user := *entry.user
	return &user
}

// Set stores a user in the cache. It also triggers a background garbage-
// collection sweep if enough time has elapsed since the last one or if the
// cache has grown past maxSize.
func (c *UserCache) Set(user *models.User) {
	// Store a copy to prevent external mutation of the cached object.
	copied := *user
	if _, loaded := c.store.Swap(user.ID, &userCacheEntry{
		user:      &copied,
		expiresAt: time.Now().Add(c.ttl),
	}); !loaded {
		c.size.Add(1)
	}
	if c.maxSize > 0 && c.size.Load() > c.maxSize {
		c.evictUntilUnderCap()
	}
	c.maybeGC()
}

// evictUntilUnderCap walks the cache and drops the oldest expirable entries
// until size is under maxSize. Cheap O(n) walk; runs only when the cap is
// breached, which should be rare in practice (TTL handles most eviction).
func (c *UserCache) evictUntilUnderCap() {
	now := time.Now()
	// First pass: drop expired entries.
	c.store.Range(func(key, value any) bool {
		entry := value.(*userCacheEntry)
		if now.After(entry.expiresAt) {
			if _, loaded := c.store.LoadAndDelete(key); loaded {
				c.size.Add(-1)
			}
		}
		return c.size.Load() > c.maxSize
	})
	// Second pass: if still over cap, drop entries closest to expiry.
	if c.size.Load() <= c.maxSize {
		return
	}
	type scored struct {
		key       any
		expiresAt time.Time
	}
	candidates := make([]scored, 0, 64)
	c.store.Range(func(key, value any) bool {
		entry := value.(*userCacheEntry)
		candidates = append(candidates, scored{key, entry.expiresAt})
		return true
	})
	// Sort by expiry ascending — drop closest-to-expiry first.
	for i := 1; i < len(candidates); i++ {
		for j := i; j > 0 && candidates[j-1].expiresAt.After(candidates[j].expiresAt); j-- {
			candidates[j-1], candidates[j] = candidates[j], candidates[j-1]
		}
	}
	overshoot := int(c.size.Load() - c.maxSize)
	for i := 0; i < overshoot && i < len(candidates); i++ {
		if _, loaded := c.store.LoadAndDelete(candidates[i].key); loaded {
			c.size.Add(-1)
		}
	}
}

// Invalidate removes a user from the cache by ID.
func (c *UserCache) Invalidate(userID uint) {
	if _, loaded := c.store.LoadAndDelete(userID); loaded {
		c.size.Add(-1)
	}
}

// maybeGC lazily sweeps expired entries at most once per gcEvery interval.
func (c *UserCache) maybeGC() {
	c.gcMu.Lock()
	if time.Since(c.lastGC) < c.gcEvery {
		c.gcMu.Unlock()
		return
	}
	c.lastGC = time.Now()
	c.gcMu.Unlock()

	now := time.Now()
	c.store.Range(func(key, value any) bool {
		entry := value.(*userCacheEntry)
		if now.After(entry.expiresAt) {
			if _, loaded := c.store.LoadAndDelete(key); loaded {
				c.size.Add(-1)
			}
		}
		return true
	})
}

// TimezoneCache tracks the last-known timezone per user ID so the timezone
// middleware only writes to the database when the value actually changes.
type TimezoneCache struct {
	store sync.Map
}

// NewTimezoneCache creates a new TimezoneCache.
func NewTimezoneCache() *TimezoneCache {
	return &TimezoneCache{}
}

// GetAndCompare returns true if the cached timezone for the user matches tz.
// If the timezone is different (or not yet cached), it updates the cache and
// returns false, signaling that a DB write is needed.
func (tc *TimezoneCache) GetAndCompare(userID uint, tz string) (unchanged bool) {
	val, loaded := tc.store.Load(userID)
	if loaded {
		if cached, ok := val.(string); ok && cached == tz {
			return true
		}
	}
	tc.store.Store(userID, tz)
	return false
}
