package oidccache

import (
	"context"
	"sync"
	"time"

	"code.justin.tv/amzn/TwitchS2S2/internal/oidc"
	"code.justin.tv/amzn/TwitchS2S2/internal/oidc/oidciface"
)

// if validation keys are cached for longer than this, we will refresh them.
const staleDuration = 15 * time.Minute

// Cache caches OIDC responses
type Cache struct {
	OIDC oidciface.OIDCAPI

	lock                  sync.RWMutex
	validationKeys        map[string]oidc.ValidationKey
	validationKeysStale   time.Time
	validationKeysExpired time.Time
}

// RefreshIfStale refreshes the cache if stale.
func (oc *Cache) RefreshIfStale(ctx context.Context) error {
	return oc.refreshIfStale(ctx)
}

// Configuration returns the OIDC configuration
func (oc *Cache) Configuration() *oidc.Configuration {
	return oc.OIDC.Configuration()
}

// ValidationKeys returns all validation keys uncached
func (oc *Cache) ValidationKeys(ctx context.Context) (map[string]oidc.ValidationKey, time.Duration, error) {
	return oc.OIDC.ValidationKeys(ctx)
}

// ValidationKey returns the validation key by ID from cache
func (oc *Cache) ValidationKey(ctx context.Context, keyID string) (oidc.ValidationKey, error) {
	if err := oc.refreshIfStale(ctx); err != nil {
		return nil, err
	}

	oc.lock.RLock()
	defer oc.lock.RUnlock()
	key, ok := oc.validationKeys[keyID]
	if !ok {
		return nil, &oidc.ErrUnknownSigningKey{KID: keyID}
	}
	return key, nil
}

func (oc *Cache) refreshIfStale(ctx context.Context) error {
	var stale bool

	oc.withLock(func() {
		stale = time.Now().After(oc.validationKeysStale)
	})

	if stale {
		oc.lock.Lock()
		defer oc.lock.Unlock()
		keys, maxAge, err := oc.ValidationKeys(ctx)
		if err != nil {
			if time.Now().After(oc.validationKeysExpired) {
				return err
			}

			// if we get an error but the current token is not expired, we re-use the
			// current stale token.
			return nil
		}

		oc.validationKeys = keys
		oc.validationKeysStale = time.Now().Add(staleDuration)
		oc.validationKeysExpired = time.Now().Add(maxAge)
	}

	return nil
}

func (oc *Cache) withLock(f func()) {
	oc.lock.RLock()
	defer oc.lock.RUnlock()
	f()
}
