package user

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

	"github.com/google/uuid"
)

// Cache is the in-memory write-through projection of the active rows in
// `backend.entitlement_snapshots`. Reads (Get) are RLocked; writes (Add)
// are Locked.
//
// The cache keys snapshots by user_id. Soft-delete does not evict
// entries — read paths gate visibility through the
// `accounts.deleted_at IS NULL` predicate, so a cached snapshot for a
// soft-deleted user is harmless and is reaped on the next process
// reboot.
//
// The caller is expected to commit the corresponding database write
// *before* invoking Add so that the cache stays consistent under crash:
// a Postgres commit failure leaves the cache untouched, matching the
// previous DB state. This mirrors the post-commit write-through pattern
// established in `backend/internal/auth.Cache`.
type Cache struct {
	mu    sync.RWMutex
	byID  map[uuid.UUID]EntitlementSnapshot
	ready atomic.Bool
}

// NewCache constructs an empty Cache. The cache reports Ready() == false
// until Warm completes successfully.
func NewCache() *Cache {
	return &Cache{
		byID: make(map[uuid.UUID]EntitlementSnapshot),
	}
}

// Warm replaces the cache contents with every row loaded from store. It
// is intended to be called exactly once at process boot before the HTTP
// listener accepts traffic; successful completion flips Ready to true.
// Subsequent calls re-warm the cache (useful in tests).
func (c *Cache) Warm(ctx context.Context, store *Store) error {
	if c == nil {
		return nil
	}
	snapshots, err := store.ListEntitlementSnapshots(ctx)
	if err != nil {
		return err
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	c.byID = make(map[uuid.UUID]EntitlementSnapshot, len(snapshots))
	for _, snap := range snapshots {
		c.byID[snap.UserID] = snap
	}
	c.ready.Store(true)
	return nil
}

// Ready reports whether Warm has completed at least once. The HTTP
// readiness probe wires through this method together with the auth
// cache so `/readyz` only flips to 200 after every cache is hydrated.
func (c *Cache) Ready() bool {
	if c == nil {
		return false
	}
	return c.ready.Load()
}

// Size returns the number of cached entitlement snapshots. Useful for
// the startup log line and tests.
func (c *Cache) Size() int {
	if c == nil {
		return 0
	}
	c.mu.RLock()
	defer c.mu.RUnlock()
	return len(c.byID)
}

// Get returns the snapshot for userID and a presence flag. Misses
// always return the zero EntitlementSnapshot and false.
func (c *Cache) Get(userID uuid.UUID) (EntitlementSnapshot, bool) {
	if c == nil {
		return EntitlementSnapshot{}, false
	}
	c.mu.RLock()
	defer c.mu.RUnlock()
	s, ok := c.byID[userID]
	return s, ok
}

// Add stores snap in the cache. It is safe to call on an existing
// entry — the value is overwritten with the latest snapshot.
func (c *Cache) Add(snap EntitlementSnapshot) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	c.byID[snap.UserID] = snap
}
