// Package session is the gateway's in-memory session cache. It maps an opaque
// bearer token to the backend account id, falling back to the backend's resolve
// endpoint on a miss and caching the result for a bounded TTL. The backend
// remains the source of truth (sessions are revoke-only there); the cache only
// shortcuts the hot path.
package session

import (
	"context"
	"sync"
	"time"
)

// Resolver resolves a token to an account id, its guest flag and its trusted
// platform ("<kind>/<subtype>", empty when untrusted) at the backend (the cache
// miss path). backendclient.Client satisfies it.
type Resolver interface {
	ResolveSession(ctx context.Context, token string) (string, bool, string, error)
}

// Cache resolves session tokens to account ids, caching hits for ttl.
type Cache struct {
	backend Resolver
	ttl     time.Duration
	max     int
	now     func() time.Time

	mu      sync.Mutex
	entries map[string]entry
}

type entry struct {
	userID   string
	isGuest  bool
	platform string
	expires  time.Time
}

// NewCache constructs a Cache over backend with the given TTL and maximum size.
func NewCache(backend Resolver, ttl time.Duration, max int) *Cache {
	if max <= 0 {
		max = 1
	}
	return &Cache{
		backend: backend,
		ttl:     ttl,
		max:     max,
		now:     func() time.Time { return time.Now() },
		entries: make(map[string]entry),
	}
}

// Resolve returns the account id, guest flag and trusted platform for token,
// consulting the cache first and the backend on a miss (caching the result). An
// empty token is rejected by the backend like any unknown token.
func (c *Cache) Resolve(ctx context.Context, token string) (string, bool, string, error) {
	if uid, guest, platform, ok := c.lookup(token); ok {
		return uid, guest, platform, nil
	}
	uid, guest, platform, err := c.backend.ResolveSession(ctx, token)
	if err != nil {
		return "", false, "", err
	}
	c.store(token, uid, guest, platform)
	return uid, guest, platform, nil
}

// Invalidate drops a token from the cache (e.g. after a revoke).
func (c *Cache) Invalidate(token string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	delete(c.entries, token)
}

// lookup returns a live cached account id, guest flag and platform for token.
func (c *Cache) lookup(token string) (string, bool, string, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	e, ok := c.entries[token]
	if !ok || !c.now().Before(e.expires) {
		return "", false, "", false
	}
	return e.userID, e.isGuest, e.platform, true
}

// store caches token -> (userID, isGuest, platform), sweeping expired entries and
// bounding the size.
func (c *Cache) store(token, userID string, isGuest bool, platform string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if len(c.entries) >= c.max {
		c.evictLocked()
	}
	c.entries[token] = entry{userID: userID, isGuest: isGuest, platform: platform, expires: c.now().Add(c.ttl)}
}

// evictLocked removes expired entries and, if still at capacity, drops arbitrary
// entries until below the limit. The caller holds c.mu.
func (c *Cache) evictLocked() {
	now := c.now()
	for k, e := range c.entries {
		if !now.Before(e.expires) {
			delete(c.entries, k)
		}
	}
	for k := range c.entries {
		if len(c.entries) < c.max {
			break
		}
		delete(c.entries, k)
	}
}
