// Package endpointcreds provides support for retrieving credentials from an
// arbitrary HTTP endpoint.
//
// The credentials endpoint Provider can receive both static and refreshable
// credentials that will expire. Credentials are static when an "Expiration"
// value is not provided in the endpoint's response.
//
// Static credentials will never expire once they have been retrieved. The format
// of the static credentials response:
//    {
//        "AccessKeyId" : "MUA...",
//        "SecretAccessKey" : "/7PC5om....",
//    }
//
// Refreshable credentials will expire within the "ExpiryWindow" of the Expiration
// value in the response. The format of the refreshable credentials response:
//    {
//        "AccessKeyId" : "MUA...",
//        "SecretAccessKey" : "/7PC5om....",
//        "Token" : "AQoDY....=",
//        "Expiration" : "2016-02-25T06:03:31Z"
//    }
//
// Errors should be returned in the following format and only returned with 400
// or 500 HTTP status codes.
//    {
//        "code": "ErrorCode",
//        "message": "Helpful error message."
//    }
package endpointcreds

import (
	"context"
	"fmt"
	"net/http"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client"
	"github.com/aws/smithy-go/middleware"
)

// ProviderName is the name of the credentials provider.
const ProviderName = `CredentialsEndpointProvider`

type getCredentialsAPIClient interface {
	GetCredentials(context.Context, *client.GetCredentialsInput, ...func(*client.Options)) (*client.GetCredentialsOutput, error)
}

// Provider satisfies the aws.CredentialsProvider interface, and is a client to
// retrieve credentials from an arbitrary endpoint.
type Provider struct {
	// The AWS Client to make HTTP requests to the endpoint with. The endpoint
	// the request will be made to is provided by the aws.Config's
	// EndpointResolver.
	client getCredentialsAPIClient

	options Options
}

// HTTPClient is a client for sending HTTP requests
type HTTPClient interface {
	Do(*http.Request) (*http.Response, error)
}

// Options is structure of configurable options for Provider
type Options struct {
	// Endpoint to retrieve credentials from. Required
	Endpoint string

	// HTTPClient to handle sending HTTP requests to the target endpoint.
	HTTPClient HTTPClient

	// Set of options to modify how the credentials operation is invoked.
	APIOptions []func(*middleware.Stack) error

	// The Retryer to be used for determining whether a failed requested should be retried
	Retryer aws.Retryer

	// Optional authorization token value if set will be used as the value of
	// the Authorization header of the endpoint credential request.
	AuthorizationToken string
}

// New returns a credentials Provider for retrieving AWS credentials
// from arbitrary endpoint.
func New(endpoint string, optFns ...func(*Options)) *Provider {
	o := Options{
		Endpoint: endpoint,
	}

	for _, fn := range optFns {
		fn(&o)
	}

	p := &Provider{
		client: client.New(client.Options{
			HTTPClient: o.HTTPClient,
			Endpoint:   o.Endpoint,
			APIOptions: o.APIOptions,
			Retryer:    o.Retryer,
		}),
		options: o,
	}

	return p
}

// Retrieve will attempt to request the credentials from the endpoint the Provider
// was configured for. And error will be returned if the retrieval fails.
func (p *Provider) Retrieve(ctx context.Context) (aws.Credentials, error) {
	resp, err := p.getCredentials(ctx)
	if err != nil {
		return aws.Credentials{}, fmt.Errorf("failed to load credentials, %w", err)
	}

	creds := aws.Credentials{
		AccessKeyID:     resp.AccessKeyID,
		SecretAccessKey: resp.SecretAccessKey,
		SessionToken:    resp.Token,
		Source:          ProviderName,
	}

	if resp.Expiration != nil {
		creds.CanExpire = true
		creds.Expires = *resp.Expiration
	}

	return creds, nil
}

func (p *Provider) getCredentials(ctx context.Context) (*client.GetCredentialsOutput, error) {
	return p.client.GetCredentials(ctx, &client.GetCredentialsInput{AuthorizationToken: p.options.AuthorizationToken})
}
