package runtime

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

	"github.com/google/uuid"
)

// Cache is the in-memory write-through projection of the runtime
// records and engine version registry. Mirrors the lobby/auth/admin
// cache idiom: Postgres is the source of truth, the cache is updated
// only after a successful commit.
//
// Reads (Get*) take RLocks; writes (Put*, Remove*) take Locks. The
// cache only retains non-terminal runtime records so the active set
// stays small and warm.
type Cache struct {
	mu             sync.RWMutex
	runtimes       map[uuid.UUID]RuntimeRecord
	engineVersions map[string]EngineVersion
	ready          atomic.Bool
}

// NewCache returns an empty Cache.
func NewCache() *Cache {
	return &Cache{
		runtimes:       make(map[uuid.UUID]RuntimeRecord),
		engineVersions: make(map[string]EngineVersion),
	}
}

// Warm populates the cache from store. Must be called once at process
// boot before the HTTP listener accepts traffic.
func (c *Cache) Warm(ctx context.Context, store *Store) error {
	if c == nil {
		return nil
	}
	versions, err := store.ListEngineVersions(ctx)
	if err != nil {
		return fmt.Errorf("runtime cache warm: engine versions: %w", err)
	}
	records, err := store.ListAllRuntimeRecords(ctx)
	if err != nil {
		return fmt.Errorf("runtime cache warm: runtime records: %w", err)
	}

	c.mu.Lock()
	defer c.mu.Unlock()
	c.engineVersions = make(map[string]EngineVersion, len(versions))
	for _, v := range versions {
		c.engineVersions[v.Version] = v
	}
	c.runtimes = make(map[uuid.UUID]RuntimeRecord)
	for _, r := range records {
		if r.IsTerminal() {
			continue
		}
		c.runtimes[r.GameID] = 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 two projections; used by the
// startup log line and tests.
func (c *Cache) Sizes() (runtimes int, engineVersions int) {
	if c == nil {
		return 0, 0
	}
	c.mu.RLock()
	defer c.mu.RUnlock()
	return len(c.runtimes), len(c.engineVersions)
}

// GetRuntime returns the cached runtime record for gameID together
// with a presence flag.
func (c *Cache) GetRuntime(gameID uuid.UUID) (RuntimeRecord, bool) {
	if c == nil {
		return RuntimeRecord{}, false
	}
	c.mu.RLock()
	defer c.mu.RUnlock()
	r, ok := c.runtimes[gameID]
	return r, ok
}

// PutRuntime stores or updates the runtime record. Terminal statuses
// cause the entry to be evicted.
func (c *Cache) PutRuntime(rec RuntimeRecord) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if rec.IsTerminal() {
		delete(c.runtimes, rec.GameID)
		return
	}
	c.runtimes[rec.GameID] = rec
}

// RemoveRuntime evicts the entry for gameID.
func (c *Cache) RemoveRuntime(gameID uuid.UUID) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	delete(c.runtimes, gameID)
}

// ActiveRuntimes returns a snapshot copy of every cached runtime
// record. The reconciler and the scheduler both iterate this list.
func (c *Cache) ActiveRuntimes() []RuntimeRecord {
	if c == nil {
		return nil
	}
	c.mu.RLock()
	defer c.mu.RUnlock()
	out := make([]RuntimeRecord, 0, len(c.runtimes))
	for _, r := range c.runtimes {
		out = append(out, r)
	}
	return out
}

// GetEngineVersion returns the cached engine_versions row keyed by
// version label, together with a presence flag.
func (c *Cache) GetEngineVersion(version string) (EngineVersion, bool) {
	if c == nil {
		return EngineVersion{}, false
	}
	c.mu.RLock()
	defer c.mu.RUnlock()
	v, ok := c.engineVersions[version]
	return v, ok
}

// PutEngineVersion stores or updates the engine_versions cache entry.
func (c *Cache) PutEngineVersion(v EngineVersion) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	c.engineVersions[v.Version] = v
}

// ListEngineVersions returns a snapshot of the cached engine_versions
// rows ordered by created_at DESC. Falls back to a deterministic order
// by version label when timestamps tie.
func (c *Cache) ListEngineVersions() []EngineVersion {
	if c == nil {
		return nil
	}
	c.mu.RLock()
	defer c.mu.RUnlock()
	out := make([]EngineVersion, 0, len(c.engineVersions))
	for _, v := range c.engineVersions {
		out = append(out, v)
	}
	return out
}
