package game

import (
	"sync"
	"time"

	"github.com/google/uuid"

	"scrabble/backend/internal/engine"
)

// keyedMutex hands out one mutex per game id, serialising every operation on a
// single game (engine.Game is not safe for concurrent use) while letting
// different games proceed in parallel. Locks are reference-counted and removed
// once no caller holds or awaits them.
type keyedMutex struct {
	mu    sync.Mutex
	locks map[uuid.UUID]*lockRef
}

type lockRef struct {
	mu   sync.Mutex
	refs int
}

func newKeyedMutex() *keyedMutex {
	return &keyedMutex{locks: make(map[uuid.UUID]*lockRef)}
}

// lock acquires the mutex for id and returns its release function.
func (k *keyedMutex) lock(id uuid.UUID) func() {
	k.mu.Lock()
	ref := k.locks[id]
	if ref == nil {
		ref = &lockRef{}
		k.locks[id] = ref
	}
	ref.refs++
	k.mu.Unlock()

	ref.mu.Lock()
	return func() {
		ref.mu.Unlock()
		k.mu.Lock()
		ref.refs--
		if ref.refs == 0 {
			delete(k.locks, id)
		}
		k.mu.Unlock()
	}
}

// gameCache holds live engine.Game values keyed by game id and evicts an entry
// once it has been idle for ttl. An evicted game is transparently rebuilt from
// the journal on next access, so eviction never affects correctness. It is safe
// for concurrent use.
type gameCache struct {
	mu      sync.Mutex
	entries map[uuid.UUID]*cachedGame
	ttl     time.Duration
	now     func() time.Time
}

type cachedGame struct {
	game       *engine.Game
	seats      []Seat
	variant    string
	lastAccess time.Time
}

func newGameCache(ttl time.Duration, now func() time.Time) *gameCache {
	return &gameCache{entries: make(map[uuid.UUID]*cachedGame), ttl: ttl, now: now}
}

// get returns the live game and its immutable seat list for id and refreshes its idle
// timer, or (nil, nil, false). The seats let a read check membership (and label seats)
// without re-loading the game from the store, since seats never change after a game starts.
func (c *gameCache) get(id uuid.UUID) (*engine.Game, []Seat, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	e, ok := c.entries[id]
	if !ok {
		return nil, nil, false
	}
	e.lastAccess = c.now()
	return e.game, e.seats, true
}

// put stores g as the live game for id together with its seat list. variant labels the
// entry so the active-games gauge can report counts by variant without inspecting engine
// internals; seats are the game's immutable seat standings for the membership fast path.
func (c *gameCache) put(id uuid.UUID, g *engine.Game, variant string, seats []Seat) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.entries[id] = &cachedGame{game: g, seats: seats, variant: variant, lastAccess: c.now()}
}

// remove drops id from the cache (used on a finished game and after a failed
// persist, so the next access rebuilds from the journal).
func (c *gameCache) remove(id uuid.UUID) {
	c.mu.Lock()
	defer c.mu.Unlock()
	delete(c.entries, id)
}

// sweep evicts every entry idle longer than ttl and returns how many were
// dropped.
func (c *gameCache) sweep() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	cutoff := c.now().Add(-c.ttl)
	var n int
	for id, e := range c.entries {
		if e.lastAccess.Before(cutoff) {
			delete(c.entries, id)
			n++
		}
	}
	return n
}

// size reports the number of resident games (for diagnostics and tests).
func (c *gameCache) size() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return len(c.entries)
}

// countByVariant tallies the resident games by their variant label. It backs the
// game_cache_active observable gauge; the resident set is the bounded number of
// live (active) games, so the scan under the lock is cheap.
func (c *gameCache) countByVariant() map[string]int {
	c.mu.Lock()
	defer c.mu.Unlock()
	out := make(map[string]int, len(c.entries))
	for _, e := range c.entries {
		out[e.variant]++
	}
	return out
}
