package cacheauthorization

import (
	"bytes"
	"container/list"
	"crypto/subtle"
	"errors"
	"fmt"
	"sync"

	"code.justin.tv/amzn/TwitchS2S2/internal/authorization"
)

func newLru(maxLen int64) *lru {
	if maxLen <= 0 {
		// should never get in this state. upstream should be erroring.
		panic("AccessTokenCacheSize cannot be negative")
	}
	return &lru{
		cache:     make(map[lruKey]*list.Element),
		evictList: list.New(),
		maxLen:    maxLen,
	}
}

type lruKey struct {
	AuthorizationType string
	ClaimsFragment    string
}

type lruValue struct {
	Authorization      *authorization.Authorization
	AuthorizationToken string
	Key                lruKey
}

type lru struct {
	cache     map[lruKey]*list.Element
	evictList *list.List
	lock      sync.RWMutex
	maxLen    int64
}

func (l *lru) Fetch(authorizationType, authorizationToken string) (*authorization.Authorization, bool, error) {
	claimsFragment, err := l.getClaimsFragment(authorizationToken)
	if err != nil {
		return nil, false, err
	}

	element, ok := func() (*list.Element, bool) {
		l.lock.RLock()
		defer l.lock.RUnlock()
		element, ok := l.cache[lruKey{AuthorizationType: authorizationType, ClaimsFragment: claimsFragment}]
		return element, ok
	}()

	if !ok {
		return nil, false, nil
	}

	func() {
		l.lock.Lock()
		defer l.lock.Unlock()

		l.evictList.MoveToFront(element)
	}()

	if val, ok := element.Value.(*lruValue); ok {
		// Convert the cached token and supplied authorizationToken values to bytes and
		// compare using ConstantTimeCompare function to mitigate risk of timing attack.
		if subtle.ConstantTimeCompare([]byte(val.AuthorizationToken), []byte(authorizationToken)) == 1 {
			return val.Authorization, true, nil
		}
	}

	return nil, false, nil
}

func (l *lru) Put(authorizationType, authorizationToken string, authz *authorization.Authorization) error {
	claimsFragment, err := l.getClaimsFragment(authorizationToken)
	if err != nil {
		return err
	}

	key := lruKey{AuthorizationType: authorizationType, ClaimsFragment: claimsFragment}

	l.lock.Lock()
	defer l.lock.Unlock()

	if int64(l.evictList.Len()) >= l.maxLen {
		back := l.evictList.Back()
		l.evictList.Remove(back)
		delete(l.cache, back.Value.(*lruValue).Key)
	}

	l.cache[key] = l.evictList.PushFront(&lruValue{
		Authorization:      authz,
		AuthorizationToken: authorizationToken,
		Key:                key})

	return nil
}

func (l *lru) getClaimsFragment(authorizationToken string) (string, error) {
	bs := []byte(authorizationToken)
	nClaimsStart := bytes.IndexRune(bs, '.')
	if nClaimsStart < 0 {
		return "", errors.New("jwt: invalid section count: 0")
	}
	bs = bs[nClaimsStart+1:]

	nSignatureStart := bytes.IndexRune(bs, '.')
	if nSignatureStart < 0 {
		return "", errors.New("jwt: invalid section count: 1")
	}
	claims := string(bs[:nSignatureStart])
	bs = bs[nSignatureStart+1:]

	nExtraStart := bytes.IndexRune(bs, '.')
	if nExtraStart >= 0 {
		return "", fmt.Errorf("jwt: invalid section count: more than 2")
	}

	return claims, nil
}
