package middleware

import (
	"sync"
	"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.
type UserCache struct {
	store   sync.Map
	ttl     time.Duration
	lastGC  time.Time
	gcMu    sync.Mutex
	gcEvery time.Duration
}

// NewUserCache creates a UserCache with the given TTL for entries.
func NewUserCache(ttl time.Duration) *UserCache {
	return &UserCache{
		ttl:     ttl,
		lastGC:  time.Now(),
		gcEvery: 2 * time.Minute,
	}
}

// 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) {
		c.store.Delete(userID)
		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.
func (c *UserCache) Set(user *models.User) {
	// Store a copy to prevent external mutation of the cached object.
	copied := *user
	c.store.Store(user.ID, &userCacheEntry{
		user:      &copied,
		expiresAt: time.Now().Add(c.ttl),
	})
	c.maybeGC()
}

// Invalidate removes a user from the cache by ID.
func (c *UserCache) Invalidate(userID uint) {
	c.store.Delete(userID)
}

// 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) {
			c.store.Delete(key)
		}
		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
}
