package innerauth

import (
	"container/list"
	"crypto/sha256"
	"sync"
	"time"
)

const apiKeyCacheMaxEntries = 10000

type apiKeyCache struct {
	mu       sync.Mutex
	ttl      time.Duration
	authn    map[[sha256.Size]byte]*apiKeyAuthnCacheEntry
	authnLRU *list.List
	authz    map[[sha256.Size]byte]*apiKeyAuthzCacheEntry
	authzLRU *list.List
	now      func() time.Time
}

type apiKeyAuthnCacheEntry struct {
	identity  APIKeyAuthnResult
	expiresAt time.Time
	element   *list.Element
}

type apiKeyAuthzCacheEntry struct {
	expiresAt time.Time
	element   *list.Element
}

func newAPIKeyCache(ttl time.Duration) *apiKeyCache {
	return &apiKeyCache{
		ttl:      ttl,
		authn:    make(map[[sha256.Size]byte]*apiKeyAuthnCacheEntry),
		authnLRU: list.New(),
		authz:    make(map[[sha256.Size]byte]*apiKeyAuthzCacheEntry),
		authzLRU: list.New(),
		now:      time.Now,
	}
}

func (c *apiKeyCache) getAuthn(encodedAPIKey string) (APIKeyAuthnResult, bool) {
	key := sha256.Sum256([]byte(encodedAPIKey))
	now := c.now()
	c.mu.Lock()
	defer c.mu.Unlock()
	entry, ok := c.authn[key]
	if !ok {
		return APIKeyAuthnResult{}, false
	}
	if now.After(entry.expiresAt) {
		c.deleteAuthnLocked(key, entry)
		return APIKeyAuthnResult{}, false
	}
	c.authnLRU.MoveToFront(entry.element)
	return entry.identity, true
}

func (c *apiKeyCache) rememberAuthn(encodedAPIKey string, identity APIKeyAuthnResult) {
	now := c.now()
	c.mu.Lock()
	defer c.mu.Unlock()
	c.pruneLocked(now)
	key := sha256.Sum256([]byte(encodedAPIKey))
	if entry, ok := c.authn[key]; ok {
		entry.identity = identity
		entry.expiresAt = now.Add(c.ttl)
		c.authnLRU.MoveToFront(entry.element)
		return
	}
	entry := &apiKeyAuthnCacheEntry{
		identity:  identity,
		expiresAt: now.Add(c.ttl),
	}
	entry.element = c.authnLRU.PushFront(key)
	c.authn[key] = entry
	for len(c.authn) > apiKeyCacheMaxEntries {
		oldest := c.authnLRU.Back()
		oldestKey := oldest.Value.([sha256.Size]byte)
		c.deleteAuthnLocked(oldestKey, c.authn[oldestKey])
	}
}

func (c *apiKeyCache) hasAuthz(publicKey, requestURI, resourceURI string) bool {
	key := apiKeyAuthzCacheKey(publicKey, requestURI, resourceURI)
	now := c.now()
	c.mu.Lock()
	defer c.mu.Unlock()
	entry, ok := c.authz[key]
	if !ok {
		return false
	}
	if now.After(entry.expiresAt) {
		c.deleteAuthzLocked(key, entry)
		return false
	}
	c.authzLRU.MoveToFront(entry.element)
	return true
}

func (c *apiKeyCache) rememberAuthz(publicKey, requestURI, resourceURI string) {
	now := c.now()
	c.mu.Lock()
	defer c.mu.Unlock()
	c.pruneLocked(now)
	key := apiKeyAuthzCacheKey(publicKey, requestURI, resourceURI)
	if entry, ok := c.authz[key]; ok {
		entry.expiresAt = now.Add(c.ttl)
		c.authzLRU.MoveToFront(entry.element)
		return
	}
	entry := &apiKeyAuthzCacheEntry{expiresAt: now.Add(c.ttl)}
	entry.element = c.authzLRU.PushFront(key)
	c.authz[key] = entry
	for len(c.authz) > apiKeyCacheMaxEntries {
		oldest := c.authzLRU.Back()
		oldestKey := oldest.Value.([sha256.Size]byte)
		c.deleteAuthzLocked(oldestKey, c.authz[oldestKey])
	}
}

func (c *apiKeyCache) pruneLocked(now time.Time) {
	for key, entry := range c.authn {
		if now.After(entry.expiresAt) {
			c.deleteAuthnLocked(key, entry)
		}
	}
	for key, entry := range c.authz {
		if now.After(entry.expiresAt) {
			c.deleteAuthzLocked(key, entry)
		}
	}
}

func (c *apiKeyCache) deleteAuthnLocked(key [sha256.Size]byte, entry *apiKeyAuthnCacheEntry) {
	delete(c.authn, key)
	c.authnLRU.Remove(entry.element)
}

func (c *apiKeyCache) deleteAuthzLocked(key [sha256.Size]byte, entry *apiKeyAuthzCacheEntry) {
	delete(c.authz, key)
	c.authzLRU.Remove(entry.element)
}

func apiKeyAuthzCacheKey(publicKey, requestURI, resourceURI string) [sha256.Size]byte {
	return sha256.Sum256([]byte(publicKey + "\x00" + requestURI + "\x00" + resourceURI))
}
