/*
 * MinIO Go Library for Amazon S3 Compatible Cloud Storage
 * Copyright 2017 MinIO, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package credentials

import (
	"net/http"
	"sync"
	"time"
)

const (
	// STSVersion sts version string
	STSVersion = "2011-06-15"

	// How much duration to slash from the given expiration duration
	defaultExpiryWindow = 0.8
)

// defaultCredContext is used when the credential context doesn't
// actually matter or the default context is suitable.
var defaultCredContext = &CredContext{Client: http.DefaultClient}

// A Value is the S3 credentials value for individual credential fields.
type Value struct {
	// S3 Access key ID
	AccessKeyID string

	// S3 Secret Access Key
	SecretAccessKey string

	// S3 Session Token
	SessionToken string

	// Expiration of this credentials - null means no expiration associated
	Expiration time.Time

	// Signature Type.
	SignerType SignatureType
}

// A Provider is the interface for any component which will provide credentials
// Value. A provider is required to manage its own Expired state, and what to
// be expired means.
type Provider interface {
	// RetrieveWithCredContext returns nil if it successfully retrieved the
	// value. Error is returned if the value were not obtainable, or empty.
	// optionally takes CredContext for additional context to retrieve credentials.
	RetrieveWithCredContext(cc *CredContext) (Value, error)

	// Retrieve returns nil if it successfully retrieved the value.
	// Error is returned if the value were not obtainable, or empty.
	//
	// Deprecated: Retrieve() exists for historical compatibility and should not
	// be used. To get new credentials use the RetrieveWithCredContext function
	// to ensure the proper context (i.e. HTTP client) will be used.
	Retrieve() (Value, error)

	// IsExpired returns if the credentials are no longer valid, and need
	// to be retrieved.
	IsExpired() bool
}

// CredContext is passed to the Retrieve function of a provider to provide
// some additional context to retrieve credentials.
type CredContext struct {
	// Client specifies the HTTP client that should be used if an HTTP
	// request is to be made to fetch the credentials.
	Client *http.Client

	// Endpoint specifies the MinIO endpoint that will be used if no
	// explicit endpoint is provided.
	Endpoint string
}

// A Expiry provides shared expiration logic to be used by credentials
// providers to implement expiry functionality.
//
// The best method to use this struct is as an anonymous field within the
// provider's struct.
//
// Example:
//
//	type IAMCredentialProvider struct {
//	    Expiry
//	    ...
//	}
type Expiry struct {
	// The date/time when to expire on
	expiration time.Time

	// If set will be used by IsExpired to determine the current time.
	// Defaults to time.Now if CurrentTime is not set.
	CurrentTime func() time.Time
}

// SetExpiration sets the expiration IsExpired will check when called.
//
// If window is greater than 0 the expiration time will be reduced by the
// window value.
//
// Using a window is helpful to trigger credentials to expire sooner than
// the expiration time given to ensure no requests are made with expired
// tokens.
func (e *Expiry) SetExpiration(expiration time.Time, window time.Duration) {
	if e.CurrentTime == nil {
		e.CurrentTime = time.Now
	}
	cut := window
	if cut < 0 {
		expireIn := expiration.Sub(e.CurrentTime())
		cut = time.Duration(float64(expireIn) * (1 - defaultExpiryWindow))
	}
	e.expiration = expiration.Add(-cut)
}

// IsExpired returns if the credentials are expired.
func (e *Expiry) IsExpired() bool {
	if e.CurrentTime == nil {
		e.CurrentTime = time.Now
	}
	return e.expiration.Before(e.CurrentTime())
}

// Credentials - A container for synchronous safe retrieval of credentials Value.
// Credentials will cache the credentials value until they expire. Once the value
// expires the next Get will attempt to retrieve valid credentials.
//
// Credentials is safe to use across multiple goroutines and will manage the
// synchronous state so the Providers do not need to implement their own
// synchronization.
//
// The first Credentials.Get() will always call Provider.Retrieve() to get the
// first instance of the credentials Value. All calls to Get() after that
// will return the cached credentials Value until IsExpired() returns true.
type Credentials struct {
	sync.Mutex

	creds        Value
	forceRefresh bool
	provider     Provider
}

// New returns a pointer to a new Credentials with the provider set.
func New(provider Provider) *Credentials {
	return &Credentials{
		provider:     provider,
		forceRefresh: true,
	}
}

// Get returns the credentials value, or error if the credentials Value failed
// to be retrieved.
//
// Will return the cached credentials Value if it has not expired. If the
// credentials Value has expired the Provider's Retrieve() will be called
// to refresh the credentials.
//
// If Credentials.Expire() was called the credentials Value will be force
// expired, and the next call to Get() will cause them to be refreshed.
//
// Deprecated: Get() exists for historical compatibility and should not be
// used. To get new credentials use the Credentials.GetWithContext function
// to ensure the proper context (i.e. HTTP client) will be used.
func (c *Credentials) Get() (Value, error) {
	return c.GetWithContext(nil)
}

// GetWithContext returns the credentials value, or error if the
// credentials Value failed to be retrieved.
//
// Will return the cached credentials Value if it has not expired. If the
// credentials Value has expired the Provider's Retrieve() will be called
// to refresh the credentials.
//
// If Credentials.Expire() was called the credentials Value will be force
// expired, and the next call to Get() will cause them to be refreshed.
func (c *Credentials) GetWithContext(cc *CredContext) (Value, error) {
	if c == nil {
		return Value{}, nil
	}
	if cc == nil {
		cc = defaultCredContext
	}

	c.Lock()
	defer c.Unlock()

	if c.isExpired() {
		creds, err := c.provider.RetrieveWithCredContext(cc)
		if err != nil {
			return Value{}, err
		}
		c.creds = creds
		c.forceRefresh = false
	}

	return c.creds, nil
}

// Expire expires the credentials and forces them to be retrieved on the
// next call to Get().
//
// This will override the Provider's expired state, and force Credentials
// to call the Provider's Retrieve().
func (c *Credentials) Expire() {
	c.Lock()
	defer c.Unlock()

	c.forceRefresh = true
}

// IsExpired returns if the credentials are no longer valid, and need
// to be refreshed.
//
// If the Credentials were forced to be expired with Expire() this will
// reflect that override.
func (c *Credentials) IsExpired() bool {
	c.Lock()
	defer c.Unlock()

	return c.isExpired()
}

// isExpired helper method wrapping the definition of expired credentials.
func (c *Credentials) isExpired() bool {
	return c.forceRefresh || c.provider.IsExpired()
}
