package apikey

import (
	portainer "github.com/portainer/portainer/api"

	lru "github.com/hashicorp/golang-lru"
)

const DefaultAPIKeyCacheSize = 1024

// entry is a tuple containing the user and API key associated to an API key digest
type entry[T any] struct {
	user   T
	apiKey portainer.APIKey
}

type UserCompareFn[T any] func(T, portainer.UserID) bool

// ApiKeyCache is a concurrency-safe, in-memory cache which primarily exists for to reduce database roundtrips.
// We store the api-key digest (keys) and the associated user and key-data (values) in the cache.
// This is required because HTTP requests will contain only the api-key digest in the x-api-key request header;
// digest value must be mapped to a portainer user (and respective key data) for validation.
// This cache is used to avoid multiple database queries to retrieve these user/key associated to the digest.
type ApiKeyCache[T any] struct {
	// cache type [string]entry cache (key: string(digest), value: user/key entry)
	// note: []byte keys are not supported by golang-lru Cache
	cache     *lru.Cache
	userCmpFn UserCompareFn[T]
}

// NewAPIKeyCache creates a new cache for API keys
func NewAPIKeyCache[T any](cacheSize int, userCompareFn UserCompareFn[T]) *ApiKeyCache[T] {
	cache, _ := lru.New(cacheSize)

	return &ApiKeyCache[T]{cache: cache, userCmpFn: userCompareFn}
}

// Get returns the user/key associated to an api-key's digest
// This is required because HTTP requests will contain the digest of the API key in header,
// the digest value must be mapped to a portainer user.
func (c *ApiKeyCache[T]) Get(digest string) (T, portainer.APIKey, bool) {
	val, ok := c.cache.Get(digest)
	if !ok {
		var t T

		return t, portainer.APIKey{}, false
	}

	tuple := val.(entry[T])

	return tuple.user, tuple.apiKey, true
}

// Set persists a user/key entry to the cache
func (c *ApiKeyCache[T]) Set(digest string, user T, apiKey portainer.APIKey) {
	c.cache.Add(digest, entry[T]{
		user:   user,
		apiKey: apiKey,
	})
}

// Delete evicts a digest's user/key entry key from the cache
func (c *ApiKeyCache[T]) Delete(digest string) {
	c.cache.Remove(digest)
}

// InvalidateUserKeyCache loops through all the api-keys associated to a user and removes them from the cache
func (c *ApiKeyCache[T]) InvalidateUserKeyCache(userId portainer.UserID) bool {
	present := false

	for _, k := range c.cache.Keys() {
		user, _, _ := c.Get(k.(string))
		if c.userCmpFn(user, userId) {
			present = c.cache.Remove(k) || present
		}
	}

	return present
}
