package bearer

import (
	
	
	
	

	smithycontext 
	
)

// package variable that can be override in unit tests.
var timeNow = time.Now

// TokenCacheOptions provides a set of optional configuration options for the
// TokenCache TokenProvider.
type TokenCacheOptions struct {
	// The duration before the token will expire when the credentials will be
	// refreshed. If DisableAsyncRefresh is true, the RetrieveBearerToken calls
	// will be blocking.
	//
	// Asynchronous refreshes are deduplicated, and only one will be in-flight
	// at a time. If the token expires while an asynchronous refresh is in
	// flight, the next call to RetrieveBearerToken will block on that refresh
	// to return.
	RefreshBeforeExpires time.Duration

	// The timeout the underlying TokenProvider's RetrieveBearerToken call must
	// return within, or will be canceled. Defaults to 0, no timeout.
	//
	// If 0 timeout, its possible for the underlying tokenProvider's
	// RetrieveBearerToken call to block forever. Preventing subsequent
	// TokenCache attempts to refresh the token.
	//
	// If this timeout is reached all pending deduplicated calls to
	// TokenCache RetrieveBearerToken will fail with an error.
	RetrieveBearerTokenTimeout time.Duration

	// The minimum duration between asynchronous refresh attempts. If the next
	// asynchronous recent refresh attempt was within the minimum delay
	// duration, the call to retrieve will return the current cached token, if
	// not expired.
	//
	// The asynchronous retrieve is deduplicated across multiple calls when
	// RetrieveBearerToken is called. The asynchronous retrieve is not a
	// periodic task. It is only performed when the token has not yet expired,
	// and the current item is within the RefreshBeforeExpires window, and the
	// TokenCache's RetrieveBearerToken method is called.
	//
	// If 0, (default) there will be no minimum delay between asynchronous
	// refresh attempts.
	//
	// If DisableAsyncRefresh is true, this option is ignored.
	AsyncRefreshMinimumDelay time.Duration

	// Sets if the TokenCache will attempt to refresh the token in the
	// background asynchronously instead of blocking for credentials to be
	// refreshed. If disabled token refresh will be blocking.
	//
	// The first call to RetrieveBearerToken will always be blocking, because
	// there is no cached token.
	DisableAsyncRefresh bool
}

// TokenCache provides an utility to cache Bearer Authentication tokens from a
// wrapped TokenProvider. The TokenCache can be has options to configure the
// cache's early and asynchronous refresh of the token.
type TokenCache struct {
	options  TokenCacheOptions
	provider TokenProvider

	cachedToken            atomic.Value
	lastRefreshAttemptTime atomic.Value
	sfGroup                singleflight.Group
}

// NewTokenCache returns a initialized TokenCache that implements the
// TokenProvider interface. Wrapping the provider passed in. Also taking a set
// of optional functional option parameters to configure the token cache.
func ( TokenProvider,  ...func(*TokenCacheOptions)) *TokenCache {
	var  TokenCacheOptions
	for ,  := range  {
		(&)
	}

	return &TokenCache{
		options:  ,
		provider: ,
	}
}

// RetrieveBearerToken returns the token if it could be obtained, or error if a
// valid token could not be retrieved.
//
// The passed in Context's cancel/deadline/timeout will impacting only this
// individual retrieve call and not any other already queued up calls. This
// means underlying provider's RetrieveBearerToken calls could block for ever,
// and not be canceled with the Context. Set RetrieveBearerTokenTimeout to
// provide a timeout, preventing the underlying TokenProvider blocking forever.
//
// By default, if the passed in Context is canceled, all of its values will be
// considered expired. The wrapped TokenProvider will not be able to lookup the
// values from the Context once it is expired. This is done to protect against
// expired values no longer being valid. To disable this behavior, use
// smithy-go's context.WithPreserveExpiredValues to add a value to the Context
// before calling RetrieveBearerToken to enable support for expired values.
//
// Without RetrieveBearerTokenTimeout there is the potential for a underlying
// Provider's RetrieveBearerToken call to sit forever. Blocking in subsequent
// attempts at refreshing the token.
func ( *TokenCache) ( context.Context) (Token, error) {
	,  := .getCachedToken()
	if ! || .Expired(timeNow()) {
		return .refreshBearerToken()
	}

	// Check if the token should be refreshed before it expires.
	 := .Expired(timeNow().Add(.options.RefreshBeforeExpires))
	if ! {
		return , nil
	}

	if .options.DisableAsyncRefresh {
		return .refreshBearerToken()
	}

	.tryAsyncRefresh()

	return , nil
}

// tryAsyncRefresh attempts to asynchronously refresh the token returning the
// already cached token. If it AsyncRefreshMinimumDelay option is not zero, and
// the duration since the last refresh is less than that value, nothing will be
// done.
func ( *TokenCache) ( context.Context) {
	if .options.AsyncRefreshMinimumDelay != 0 {
		var  time.Time
		if  := .lastRefreshAttemptTime.Load();  != nil {
			 = .(time.Time)
		}

		if timeNow().Before(.Add(.options.AsyncRefreshMinimumDelay)) {
			return
		}
	}

	// Ignore the returned channel so this won't be blocking, and limit the
	// number of additional goroutines created.
	.sfGroup.DoChan("async-refresh", func() (interface{}, error) {
		,  := .refreshBearerToken()
		if .options.AsyncRefreshMinimumDelay != 0 {
			var  time.Time
			if  != nil {
				 = timeNow()
			}
			.lastRefreshAttemptTime.Store()
		}

		return , 
	})
}

func ( *TokenCache) ( context.Context) (Token, error) {
	 := .sfGroup.DoChan("refresh-token", func() (interface{}, error) {
		 := smithycontext.WithSuppressCancel()
		if  := .options.RetrieveBearerTokenTimeout;  != 0 {
			var  func()
			,  = context.WithTimeout(, )
			defer ()
		}
		return .singleRetrieve()
	})

	select {
	case  := <-:
		return .Val.(Token), .Err
	case <-.Done():
		return Token{}, fmt.Errorf("retrieve bearer token canceled, %w", .Err())
	}
}

func ( *TokenCache) ( context.Context) (interface{}, error) {
	,  := .provider.RetrieveBearerToken()
	if  != nil {
		return Token{}, fmt.Errorf("failed to retrieve bearer token, %w", )
	}

	.cachedToken.Store(&)
	return , nil
}

// getCachedToken returns the currently cached token and true if found. Returns
// false if no token is cached.
func ( *TokenCache) () (Token, bool) {
	 := .cachedToken.Load()
	if  == nil {
		return Token{}, false
	}

	 := .(*Token)
	if  == nil || .Value == "" {
		return Token{}, false
	}

	return *, true
}