package lobby

import (
	"context"
	"fmt"
	"sync"
	"sync/atomic"

	"github.com/google/uuid"
)

// Cache is the in-memory write-through projection of the active lobby
// state: games (any non-finished/non-cancelled status), per-game
// memberships, and the Race Name Directory canonical map.
//
// Reads (Get*) take RLocks; writes (Put*, Remove*) take Locks. The cache
// mirrors the `internal/auth.Cache`, `internal/user.Cache`, and
// `internal/admin.Cache` idioms — Postgres is the source of truth, the
// cache is updated only after a successful commit.
type Cache struct {
	mu          sync.RWMutex
	games       map[uuid.UUID]GameRecord
	memberships map[uuid.UUID]map[uuid.UUID]Membership // game_id -> membership_id -> Membership
	rnd         map[CanonicalKey]RaceNameEntry         // canonical -> latest entry (most recent write wins)
	ready       atomic.Bool
}

// NewCache constructs an empty Cache.
func NewCache() *Cache {
	return &Cache{
		games:       make(map[uuid.UUID]GameRecord),
		memberships: make(map[uuid.UUID]map[uuid.UUID]Membership),
		rnd:         make(map[CanonicalKey]RaceNameEntry),
	}
}

// Warm fills the cache from store. Must be called once at process boot
// before the HTTP listener accepts traffic. Subsequent calls re-warm.
func (c *Cache) Warm(ctx context.Context, store *Store) error {
	if c == nil {
		return nil
	}
	games, err := store.ListAllGames(ctx)
	if err != nil {
		return fmt.Errorf("lobby cache warm: games: %w", err)
	}
	memberships, err := store.ListAllMemberships(ctx)
	if err != nil {
		return fmt.Errorf("lobby cache warm: memberships: %w", err)
	}
	raceNames, err := store.ListAllRaceNames(ctx)
	if err != nil {
		return fmt.Errorf("lobby cache warm: race names: %w", err)
	}

	c.mu.Lock()
	defer c.mu.Unlock()
	c.games = make(map[uuid.UUID]GameRecord, len(games))
	for _, g := range games {
		if isCacheableStatus(g.Status) {
			c.games[g.GameID] = g
		}
	}
	c.memberships = make(map[uuid.UUID]map[uuid.UUID]Membership, len(c.games))
	for _, m := range memberships {
		if _, ok := c.games[m.GameID]; !ok {
			continue
		}
		bucket := c.memberships[m.GameID]
		if bucket == nil {
			bucket = make(map[uuid.UUID]Membership)
			c.memberships[m.GameID] = bucket
		}
		bucket[m.MembershipID] = m
	}
	c.rnd = make(map[CanonicalKey]RaceNameEntry, len(raceNames))
	for _, r := range raceNames {
		c.rnd[r.Canonical] = r
	}
	c.ready.Store(true)
	return nil
}

// Ready reports whether Warm completed at least once.
func (c *Cache) Ready() bool {
	if c == nil {
		return false
	}
	return c.ready.Load()
}

// Sizes returns the cardinalities of the three subordinate projections.
// Useful for the startup log line and tests.
func (c *Cache) Sizes() (games int, memberships int, raceNames int) {
	if c == nil {
		return 0, 0, 0
	}
	c.mu.RLock()
	defer c.mu.RUnlock()
	for _, b := range c.memberships {
		memberships += len(b)
	}
	return len(c.games), memberships, len(c.rnd)
}

// GetGame returns the cached game record together with a presence flag.
// Misses always return the zero record and false. Note that a finished
// or cancelled game is not in the cache; callers fall back to the store
// when isCacheableStatus(...)==false at write time.
func (c *Cache) GetGame(gameID uuid.UUID) (GameRecord, bool) {
	if c == nil {
		return GameRecord{}, false
	}
	c.mu.RLock()
	defer c.mu.RUnlock()
	g, ok := c.games[gameID]
	return g, ok
}

// ListGames returns a snapshot copy of every cached game. Terminal-
// state games (finished, cancelled) are evicted from the cache on
// `PutGame`, so the result reflects the live roster of running /
// paused / draft / starting / etc. games. The slice is freshly
// allocated and safe for the caller to mutate.
func (c *Cache) ListGames() []GameRecord {
	if c == nil {
		return nil
	}
	c.mu.RLock()
	defer c.mu.RUnlock()
	out := make([]GameRecord, 0, len(c.games))
	for _, g := range c.games {
		out = append(out, g)
	}
	return out
}

// PutGame stores game in the cache when its status is cacheable;
// terminal statuses (finished, cancelled) cause the entry to be evicted.
func (c *Cache) PutGame(game GameRecord) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if !isCacheableStatus(game.Status) {
		delete(c.games, game.GameID)
		delete(c.memberships, game.GameID)
		return
	}
	c.games[game.GameID] = game
}

// RemoveGame evicts the game and any cached memberships under it.
func (c *Cache) RemoveGame(gameID uuid.UUID) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	delete(c.games, gameID)
	delete(c.memberships, gameID)
}

// PutMembership stores or updates a membership row. Removes from cache
// when status is not active.
func (c *Cache) PutMembership(m Membership) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	bucket := c.memberships[m.GameID]
	if m.Status != MembershipStatusActive {
		if bucket != nil {
			delete(bucket, m.MembershipID)
			if len(bucket) == 0 {
				delete(c.memberships, m.GameID)
			}
		}
		return
	}
	if bucket == nil {
		bucket = make(map[uuid.UUID]Membership)
		c.memberships[m.GameID] = bucket
	}
	bucket[m.MembershipID] = m
}

// MembershipsForGame returns a copy of the active memberships for
// gameID. Empty when the game is not cached or has no active members.
func (c *Cache) MembershipsForGame(gameID uuid.UUID) []Membership {
	if c == nil {
		return nil
	}
	c.mu.RLock()
	defer c.mu.RUnlock()
	bucket := c.memberships[gameID]
	if len(bucket) == 0 {
		return nil
	}
	out := make([]Membership, 0, len(bucket))
	for _, m := range bucket {
		out = append(out, m)
	}
	return out
}

// PutRaceName stores or updates a race-name entry keyed by canonical.
// The cache is best-effort — it serves uniqueness fast-paths but Postgres
// is the authoritative reader on contention.
func (c *Cache) PutRaceName(entry RaceNameEntry) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	c.rnd[entry.Canonical] = entry
}

// RemoveRaceName evicts the entry at canonical.
func (c *Cache) RemoveRaceName(canonical CanonicalKey) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	delete(c.rnd, canonical)
}

// GetRaceName returns the cached entry plus a presence flag.
func (c *Cache) GetRaceName(canonical CanonicalKey) (RaceNameEntry, bool) {
	if c == nil {
		return RaceNameEntry{}, false
	}
	c.mu.RLock()
	defer c.mu.RUnlock()
	e, ok := c.rnd[canonical]
	return e, ok
}

// EvictUserMemberships removes every cached membership belonging to
// userID. Used by `OnUserBlocked` / `OnUserDeleted` after the cascade
// commits so the cache reflects the new persisted state.
func (c *Cache) EvictUserMemberships(userID uuid.UUID) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	for gameID, bucket := range c.memberships {
		for mid, m := range bucket {
			if m.UserID == userID {
				delete(bucket, mid)
			}
		}
		if len(bucket) == 0 {
			delete(c.memberships, gameID)
		}
	}
}

// EvictUserRaceNames removes every cached race-name owned by userID.
func (c *Cache) EvictUserRaceNames(userID uuid.UUID) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	for k, e := range c.rnd {
		if e.OwnerUserID == userID {
			delete(c.rnd, k)
		}
	}
}

// EvictOwnerGames evicts every cached game whose owner is userID. Used
// after the cascade cancels the user's owned games.
func (c *Cache) EvictOwnerGames(userID uuid.UUID) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	for gameID, g := range c.games {
		if g.OwnerUserID != nil && *g.OwnerUserID == userID {
			delete(c.games, gameID)
			delete(c.memberships, gameID)
		}
	}
}

// isCacheableStatus reports whether the cache should hold a game with
// the supplied status. Terminal statuses (finished, cancelled) are
// evicted; the in-memory cache only reflects active state.
func isCacheableStatus(status string) bool {
	switch status {
	case GameStatusFinished, GameStatusCancelled:
		return false
	default:
		return true
	}
}
