package auth

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

	"github.com/google/uuid"
)

// Cache is the in-memory write-through projection of the active rows in
// `backend.device_sessions`. Reads (Get) are RLocked; writes (Add,
// Remove, RemoveByUser) are Locked. The cache holds two maps:
//
//   - byID    maps device_session_id → Session.
//   - byUser  maps user_id → set of device_session_ids belonging to that
//     user, used to satisfy bulk revoke without scanning byID.
//
// Both maps are updated atomically inside one Lock per mutation. The
// caller is expected to commit the corresponding database write *before*
// invoking Add or Remove so that the cache stays consistent under crash:
// a Postgres commit failure leaves the cache untouched, matching the
// previous DB state.
type Cache struct {
	mu     sync.RWMutex
	byID   map[uuid.UUID]Session
	byUser map[uuid.UUID]map[uuid.UUID]struct{}
	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]Session),
		byUser: make(map[uuid.UUID]map[uuid.UUID]struct{}),
	}
}

// Warm replaces the cache contents with every active session 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 {
	sessions, err := store.ListActiveSessions(ctx)
	if err != nil {
		return err
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	c.byID = make(map[uuid.UUID]Session, len(sessions))
	c.byUser = make(map[uuid.UUID]map[uuid.UUID]struct{})
	for _, s := range sessions {
		c.byID[s.DeviceSessionID] = s
		set, ok := c.byUser[s.UserID]
		if !ok {
			set = make(map[uuid.UUID]struct{})
			c.byUser[s.UserID] = set
		}
		set[s.DeviceSessionID] = struct{}{}
	}
	c.ready.Store(true)
	return nil
}

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

// Size returns the number of cached active sessions. Useful in startup
// logs ("auth cache warmed: N sessions") and in 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 session with deviceSessionID and a presence flag.
// Misses always return the zero Session and false; callers should not
// inspect the returned value when ok is false.
func (c *Cache) Get(deviceSessionID uuid.UUID) (Session, bool) {
	if c == nil {
		return Session{}, false
	}
	c.mu.RLock()
	defer c.mu.RUnlock()
	s, ok := c.byID[deviceSessionID]
	return s, ok
}

// Add stores s in the cache. It is safe to call on an existing entry
// — both the primary map and the user index are updated to the latest
// snapshot.
func (c *Cache) Add(s Session) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	c.byID[s.DeviceSessionID] = s
	set, ok := c.byUser[s.UserID]
	if !ok {
		set = make(map[uuid.UUID]struct{})
		c.byUser[s.UserID] = set
	}
	set[s.DeviceSessionID] = struct{}{}
}

// Remove evicts the entry for deviceSessionID from both maps. Calling
// Remove on a missing entry is a no-op.
func (c *Cache) Remove(deviceSessionID uuid.UUID) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	s, ok := c.byID[deviceSessionID]
	if !ok {
		return
	}
	delete(c.byID, deviceSessionID)
	if set := c.byUser[s.UserID]; set != nil {
		delete(set, deviceSessionID)
		if len(set) == 0 {
			delete(c.byUser, s.UserID)
		}
	}
}

// ListByUser returns a freshly-allocated snapshot of every cached
// session belonging to userID. The user-surface "list my sessions"
// handler consumes this. An empty slice is returned for an unknown
// userID.
func (c *Cache) ListByUser(userID uuid.UUID) []Session {
	if c == nil {
		return nil
	}
	c.mu.RLock()
	defer c.mu.RUnlock()
	set, ok := c.byUser[userID]
	if !ok {
		return nil
	}
	out := make([]Session, 0, len(set))
	for id := range set {
		if sess, ok := c.byID[id]; ok {
			out = append(out, sess)
		}
	}
	return out
}

// RemoveByUser evicts every cached entry belonging to userID and returns
// the device_session_ids it removed. The returned slice is safe for the
// caller to hold past the call — it is freshly allocated.
func (c *Cache) RemoveByUser(userID uuid.UUID) []uuid.UUID {
	if c == nil {
		return nil
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	set, ok := c.byUser[userID]
	if !ok {
		return nil
	}
	removed := make([]uuid.UUID, 0, len(set))
	for id := range set {
		removed = append(removed, id)
		delete(c.byID, id)
	}
	delete(c.byUser, userID)
	return removed
}
