package admin

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

// cacheEntry pairs the admin aggregate with its bcrypt hash. The
// hash is private to the admin package: handlers receive only the
// Admin shape, and Verify consumes the hash directly off the cache.
type cacheEntry struct {
	admin Admin
	passwordHash []byte
}

// Cache is the in-memory write-through projection of the rows in
// `backend.admin_accounts`. Reads (Get) are RLocked; writes (Put,
// Remove) are Locked.
//
// The cache mirrors the `auth.Cache` and `user.Cache` idioms: callers
// commit to Postgres first, then update the cache. A commit failure
// leaves the cache untouched, matching the previous DB state.
type Cache struct {
	mu sync.RWMutex
	byName map[string]cacheEntry
	ready atomic.Bool
}

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

// 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
	}
	admins, hashes, err := store.ListAll(ctx)
	if err != nil {
		return fmt.Errorf("admin cache warm: %w", err)
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	c.byName = make(map[string]cacheEntry, len(admins))
	for i, a := range admins {
		c.byName[a.Username] = cacheEntry{
			admin: a,
			passwordHash: hashes[i],
		}
	}
	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
// and user caches 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 admin accounts. 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.byName)
}

// Get returns the cached entry for username and a presence flag.
// Misses always return the zero entry and false.
func (c *Cache) Get(username string) (Admin, []byte, bool) {
	if c == nil {
		return Admin{}, nil, false
	}
	c.mu.RLock()
	defer c.mu.RUnlock()
	entry, ok := c.byName[username]
	if !ok {
		return Admin{}, nil, false
	}
	return entry.admin, entry.passwordHash, true
}

// Put stores admin and its bcrypt hash in the cache. It is safe to
// call on an existing entry — the value is overwritten with the
// latest snapshot. The slice is stored by reference; callers must not
// mutate it after handing it to Put.
func (c *Cache) Put(admin Admin, passwordHash []byte) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	c.byName[admin.Username] = cacheEntry{
		admin: admin,
		passwordHash: passwordHash,
	}
}

// Remove evicts the entry for username. Calling Remove on a missing
// entry is a no-op. The current implementation ships no Delete operation; the helper
// exists for symmetry with `auth.Cache` / `user.Cache` and for any
// future hard-delete flow.
func (c *Cache) Remove(username string) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	delete(c.byName, username)
}
