// Package gamelimits holds the per-tier, per-kind active-game caps (a guest funnel) with a hot
// in-memory cache over the single-row backend.config, so a login or a game-create never queries it.
package gamelimits

import (
	"context"
	"database/sql"
	"fmt"
	"sync"

	"github.com/go-jet/jet/v2/postgres"

	"scrabble/backend/internal/postgres/jet/backend/model"
	"scrabble/backend/internal/postgres/jet/backend/table"
)

// Kind is a game's kind, mirroring backend.games.game_kind. 0 (unknown) is an untagged game, never gated.
type Kind int16

const (
	KindUnknown Kind = 0
	KindVsAI    Kind = 1
	KindRandom  Kind = 2
	KindFriends Kind = 3
)

// String returns the kind's label for admin game lists and logs.
func (k Kind) String() string {
	switch k {
	case KindVsAI:
		return "vs_ai"
	case KindRandom:
		return "random"
	case KindFriends:
		return "friends"
	default:
		return "unknown"
	}
}

// Unlimited is the limit sentinel meaning no cap.
const Unlimited = -1

// Limits is a tier's active-game caps per kind (-1 = unlimited).
type Limits struct {
	VsAI    int
	Random  int
	Friends int
}

// Cap returns the limit for kind; the unknown kind is never capped.
func (l Limits) Cap(kind Kind) int {
	switch kind {
	case KindVsAI:
		return l.VsAI
	case KindRandom:
		return l.Random
	case KindFriends:
		return l.Friends
	default:
		return Unlimited
	}
}

// Config is the per-tier limit config (the single backend.config row).
type Config struct {
	Guest   Limits
	Durable Limits
}

// Store reads and writes the single-row backend.config.
type Store struct{ db *sql.DB }

// NewStore constructs a Store over db.
func NewStore(db *sql.DB) *Store { return &Store{db: db} }

func (s *Store) load(ctx context.Context) (Config, error) {
	var c model.Config
	if err := postgres.SELECT(table.Config.AllColumns).FROM(table.Config).LIMIT(1).
		QueryContext(ctx, s.db, &c); err != nil {
		return Config{}, fmt.Errorf("gamelimits: load config: %w", err)
	}
	return Config{
		Guest:   Limits{VsAI: int(c.GuestVsAiLimit), Random: int(c.GuestRandomLimit), Friends: int(c.GuestFriendsLimit)},
		Durable: Limits{VsAI: int(c.DurableVsAiLimit), Random: int(c.DurableRandomLimit), Friends: int(c.DurableFriendsLimit)},
	}, nil
}

func (s *Store) save(ctx context.Context, c Config) error {
	if _, err := s.db.ExecContext(ctx,
		`UPDATE backend.config SET guest_vs_ai_limit=$1, guest_random_limit=$2, guest_friends_limit=$3,
		   durable_vs_ai_limit=$4, durable_random_limit=$5, durable_friends_limit=$6 WHERE only_row`,
		c.Guest.VsAI, c.Guest.Random, c.Guest.Friends, c.Durable.VsAI, c.Durable.Random, c.Durable.Friends); err != nil {
		return fmt.Errorf("gamelimits: save config: %w", err)
	}
	return nil
}

// Service fronts the config with an in-memory cache (single-instance, matching the deploy). Load
// once at startup; Update after an admin edit refreshes it in place.
type Service struct {
	store *Store
	mu    sync.RWMutex
	cfg   Config
}

// NewService constructs a Service over store. Call Load before serving.
func NewService(store *Store) *Service { return &Service{store: store} }

// Load reads the config into the cache. Call once at startup.
func (s *Service) Load(ctx context.Context) error {
	c, err := s.store.load(ctx)
	if err != nil {
		return err
	}
	s.set(c)
	return nil
}

func (s *Service) set(c Config) {
	s.mu.Lock()
	s.cfg = c
	s.mu.Unlock()
}

// Get returns the cached config.
func (s *Service) Get() Config {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.cfg
}

// LimitsFor returns the cached limits for the account tier (guest or durable).
func (s *Service) LimitsFor(isGuest bool) Limits {
	c := s.Get()
	if isGuest {
		return c.Guest
	}
	return c.Durable
}

// Update saves the config and refreshes the cache in place (the admin edit).
func (s *Service) Update(ctx context.Context, c Config) error {
	if err := s.store.save(ctx, c); err != nil {
		return err
	}
	s.set(c)
	return nil
}
