// Copyright 2024 Google LLC
//
// 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 externalaccount

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

	"cloud.google.com/go/auth"
	iexacc "cloud.google.com/go/auth/credentials/internal/externalaccount"
	"cloud.google.com/go/auth/internal"
	"cloud.google.com/go/auth/internal/credsfile"
	"github.com/googleapis/gax-go/v2/internallog"
)

// Options for creating a [cloud.google.com/go/auth.Credentials].
type Options struct {
	// Audience is the Secure Token Service (STS) audience which contains the
	// resource name for the workload identity pool or the workforce pool and
	// the provider identifier in that pool. Required.
	Audience string
	// SubjectTokenType is the STS token type based on the Oauth2.0 token
	// exchange spec. Expected values include:
	// - “urn:ietf:params:oauth:token-type:jwt”
	// - “urn:ietf:params:oauth:token-type:id-token”
	// - “urn:ietf:params:oauth:token-type:saml2”
	// - “urn:ietf:params:aws:token-type:aws4_request”
	// Required.
	SubjectTokenType string
	// TokenURL is the STS token exchange endpoint. If not provided, will
	// default to https://sts.UNIVERSE_DOMAIN/v1/token, with UNIVERSE_DOMAIN set
	// to the default service domain googleapis.com unless UniverseDomain is
	// set. Optional.
	TokenURL string
	// TokenInfoURL is the token_info endpoint used to retrieve the account
	// related information (user attributes like account identifier, eg. email,
	// username, uid, etc). This is needed for gCloud session account
	// identification. Optional.
	TokenInfoURL string
	// ServiceAccountImpersonationURL is the URL for the service account
	// impersonation request. This is only required for workload identity pools
	// when APIs to be accessed have not integrated with UberMint.
	ServiceAccountImpersonationURL string
	// ServiceAccountImpersonationLifetimeSeconds is the number of seconds the
	// service account impersonation token will be valid for.
	ServiceAccountImpersonationLifetimeSeconds int
	// ClientSecret is currently only required if token_info endpoint also
	// needs to be called with the generated GCP access token. When provided,
	// STS will be called with additional basic authentication using client_id
	// as username and client_secret as password. Optional.
	ClientSecret string
	// ClientID is only required in conjunction with ClientSecret, as described
	// above. Optional.
	ClientID string
	// CredentialSource contains the necessary information to retrieve the token
	// itself, as well as some environmental information. Optional.
	CredentialSource *CredentialSource
	// QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth
	// libraries will set the x-goog-user-project which overrides the project
	// associated with the credentials. Optional.
	QuotaProjectID string
	// Scopes contains the desired scopes for the returned access token.
	// Optional.
	Scopes []string
	// WorkforcePoolUserProject should be set when it is a workforce pool and
	// not a workload identity pool. The underlying principal must still have
	// serviceusage.services.use IAM permission to use the project for
	// billing/quota. Optional.
	WorkforcePoolUserProject string
	// UniverseDomain is the default service domain for a given Cloud universe.
	// This value will be used in the default STS token URL. The default value
	// is "googleapis.com". It will not be used if TokenURL is set. Optional.
	UniverseDomain string
	// SubjectTokenProvider is an optional token provider for OIDC/SAML
	// credentials. One of SubjectTokenProvider, AWSSecurityCredentialProvider
	// or CredentialSource must be provided. Optional.
	SubjectTokenProvider SubjectTokenProvider
	// AwsSecurityCredentialsProvider is an AWS Security Credential provider
	// for AWS credentials. One of SubjectTokenProvider,
	// AWSSecurityCredentialProvider or CredentialSource must be provided. Optional.
	AwsSecurityCredentialsProvider AwsSecurityCredentialsProvider

	// Client configures the underlying client used to make network requests
	// when fetching tokens. Optional.
	Client *http.Client
	// Logger is used for debug logging. If provided, logging will be enabled
	// at the loggers configured level. By default logging is disabled unless
	// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
	// logger will be used. Optional.
	Logger *slog.Logger
}

// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
type CredentialSource struct {
	// File is the location for file sourced credentials.
	// One field amongst File, URL, Executable, or EnvironmentID should be
	// provided, depending on the kind of credential in question.
	File string
	// Url is the URL to call for URL sourced credentials.
	// One field amongst File, URL, Executable, or EnvironmentID should be
	// provided, depending on the kind of credential in question.
	URL string
	// Executable is the configuration object for executable sourced credentials.
	// One field amongst File, URL, Executable, or EnvironmentID should be
	// provided, depending on the kind of credential in question.
	Executable *ExecutableConfig
	// EnvironmentID is the EnvironmentID used for AWS sourced credentials.
	// This should start with "AWS".
	// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
	EnvironmentID string

	// Headers are the headers to attach to the request for URL sourced
	// credentials.
	Headers map[string]string
	// RegionURL is the metadata URL to retrieve the region from for EC2 AWS
	// credentials.
	RegionURL string
	// RegionalCredVerificationURL is the AWS regional credential verification
	// URL, will default to `https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15`
	// if not provided.
	RegionalCredVerificationURL string
	// IMDSv2SessionTokenURL is the URL to retrieve the session token when using
	// IMDSv2 in AWS.
	IMDSv2SessionTokenURL string
	// Format is the format type for the subject token. Used for File and URL
	// sourced credentials.
	Format *Format
}

// Format contains information needed to retrieve a subject token for URL or
// File sourced credentials.
type Format struct {
	// Type should be either "text" or "json". This determines whether the file
	// or URL sourced credentials expect a simple text subject token or if the
	// subject token will be contained in a JSON object. When not provided
	// "text" type is assumed.
	Type string
	// SubjectTokenFieldName is only required for JSON format. This is the field
	// name that the credentials will check for the subject token in the file or
	// URL response. This would be "access_token" for azure.
	SubjectTokenFieldName string
}

// ExecutableConfig contains information needed for executable sourced credentials.
type ExecutableConfig struct {
	// Command is the the full command to run to retrieve the subject token.
	// This can include arguments. Must be an absolute path for the program. Required.
	Command string
	// TimeoutMillis is the timeout duration, in milliseconds. Defaults to 30000 milliseconds when not provided. Optional.
	TimeoutMillis int
	// OutputFile is the absolute path to the output file where the executable will cache the response.
	// If specified the auth libraries will first check this location before running the executable. Optional.
	OutputFile string
}

// SubjectTokenProvider can be used to supply a subject token to exchange for a
// GCP access token.
type SubjectTokenProvider interface {
	// SubjectToken should return a valid subject token or an error.
	// The external account token provider does not cache the returned subject
	// token, so caching logic should be implemented in the provider to prevent
	// multiple requests for the same subject token.
	SubjectToken(ctx context.Context, opts *RequestOptions) (string, error)
}

// RequestOptions contains information about the requested subject token or AWS
// security credentials from the Google external account credential.
type RequestOptions struct {
	// Audience is the requested audience for the external account credential.
	Audience string
	// Subject token type is the requested subject token type for the external
	// account credential. Expected values include:
	// “urn:ietf:params:oauth:token-type:jwt”
	// “urn:ietf:params:oauth:token-type:id-token”
	// “urn:ietf:params:oauth:token-type:saml2”
	// “urn:ietf:params:aws:token-type:aws4_request”
	SubjectTokenType string
}

// AwsSecurityCredentialsProvider can be used to supply AwsSecurityCredentials
// and an AWS Region to exchange for a GCP access token.
type AwsSecurityCredentialsProvider interface {
	// AwsRegion should return the AWS region or an error.
	AwsRegion(ctx context.Context, opts *RequestOptions) (string, error)
	// GetAwsSecurityCredentials should return a valid set of
	// AwsSecurityCredentials or an error. The external account token provider
	// does not cache the returned security credentials, so caching logic should
	// be implemented in the provider to prevent multiple requests for the
	// same security credentials.
	AwsSecurityCredentials(ctx context.Context, opts *RequestOptions) (*AwsSecurityCredentials, error)
}

// AwsSecurityCredentials models AWS security credentials.
type AwsSecurityCredentials struct {
	// AccessKeyId is the AWS Access Key ID - Required.
	AccessKeyID string `json:"AccessKeyID"`
	// SecretAccessKey is the AWS Secret Access Key - Required.
	SecretAccessKey string `json:"SecretAccessKey"`
	// SessionToken is the AWS Session token. This should be provided for
	// temporary AWS security credentials - Optional.
	SessionToken string `json:"Token"`
}

func (o *Options) validate() error {
	if o == nil {
		return fmt.Errorf("externalaccount: options must be provided")
	}
	return nil
}

func (o *Options) client() *http.Client {
	if o.Client != nil {
		return o.Client
	}
	return internal.DefaultClient()
}

func (o *Options) toInternalOpts() *iexacc.Options {
	if o == nil {
		return nil
	}
	iOpts := &iexacc.Options{
		Audience:                       o.Audience,
		SubjectTokenType:               o.SubjectTokenType,
		TokenURL:                       o.TokenURL,
		TokenInfoURL:                   o.TokenInfoURL,
		ServiceAccountImpersonationURL: o.ServiceAccountImpersonationURL,
		ServiceAccountImpersonationLifetimeSeconds: o.ServiceAccountImpersonationLifetimeSeconds,
		ClientSecret:                   o.ClientSecret,
		ClientID:                       o.ClientID,
		QuotaProjectID:                 o.QuotaProjectID,
		Scopes:                         o.Scopes,
		WorkforcePoolUserProject:       o.WorkforcePoolUserProject,
		UniverseDomain:                 o.UniverseDomain,
		SubjectTokenProvider:           toInternalSubjectTokenProvider(o.SubjectTokenProvider),
		AwsSecurityCredentialsProvider: toInternalAwsSecurityCredentialsProvider(o.AwsSecurityCredentialsProvider),
		Client:                         o.client(),
		IsDefaultClient:                o.Client == nil,
		Logger:                         internallog.New(o.Logger),
	}
	if o.CredentialSource != nil {
		cs := o.CredentialSource
		iOpts.CredentialSource = &credsfile.CredentialSource{
			File:                        cs.File,
			URL:                         cs.URL,
			Headers:                     cs.Headers,
			EnvironmentID:               cs.EnvironmentID,
			RegionURL:                   cs.RegionURL,
			RegionalCredVerificationURL: cs.RegionalCredVerificationURL,
			CredVerificationURL:         cs.URL,
			IMDSv2SessionTokenURL:       cs.IMDSv2SessionTokenURL,
		}
		if cs.Executable != nil {
			cse := cs.Executable
			iOpts.CredentialSource.Executable = &credsfile.ExecutableConfig{
				Command:       cse.Command,
				TimeoutMillis: cse.TimeoutMillis,
				OutputFile:    cse.OutputFile,
			}
		}
		if cs.Format != nil {
			csf := cs.Format
			iOpts.CredentialSource.Format = &credsfile.Format{
				Type:                  csf.Type,
				SubjectTokenFieldName: csf.SubjectTokenFieldName,
			}
		}
	}
	return iOpts
}

// NewCredentials returns a [cloud.google.com/go/auth.Credentials] configured
// with the provided options.
func NewCredentials(opts *Options) (*auth.Credentials, error) {
	if err := opts.validate(); err != nil {
		return nil, err
	}

	tp, err := iexacc.NewTokenProvider(opts.toInternalOpts())
	if err != nil {
		return nil, err
	}

	var udp, qpp auth.CredentialsPropertyProvider
	if opts.UniverseDomain != "" {
		udp = internal.StaticCredentialsProperty(opts.UniverseDomain)
	}
	if opts.QuotaProjectID != "" {
		qpp = internal.StaticCredentialsProperty(opts.QuotaProjectID)
	}
	return auth.NewCredentials(&auth.CredentialsOptions{
		TokenProvider:          auth.NewCachedTokenProvider(tp, nil),
		UniverseDomainProvider: udp,
		QuotaProjectIDProvider: qpp,
	}), nil
}

func toInternalSubjectTokenProvider(stp SubjectTokenProvider) iexacc.SubjectTokenProvider {
	if stp == nil {
		return nil
	}
	return &subjectTokenProviderAdapter{stp: stp}
}

func toInternalAwsSecurityCredentialsProvider(scp AwsSecurityCredentialsProvider) iexacc.AwsSecurityCredentialsProvider {
	if scp == nil {
		return nil
	}
	return &awsSecurityCredentialsAdapter{scp: scp}
}

func toInternalAwsSecurityCredentials(sc *AwsSecurityCredentials) *iexacc.AwsSecurityCredentials {
	if sc == nil {
		return nil
	}
	return &iexacc.AwsSecurityCredentials{
		AccessKeyID:     sc.AccessKeyID,
		SecretAccessKey: sc.SecretAccessKey,
		SessionToken:    sc.SessionToken,
	}
}

func toRequestOptions(opts *iexacc.RequestOptions) *RequestOptions {
	if opts == nil {
		return nil
	}
	return &RequestOptions{
		Audience:         opts.Audience,
		SubjectTokenType: opts.SubjectTokenType,
	}
}

// subjectTokenProviderAdapter is an adapter to convert the user supplied
// interface to its internal counterpart.
type subjectTokenProviderAdapter struct {
	stp SubjectTokenProvider
}

func (tp *subjectTokenProviderAdapter) SubjectToken(ctx context.Context, opts *iexacc.RequestOptions) (string, error) {
	return tp.stp.SubjectToken(ctx, toRequestOptions(opts))
}

// awsSecurityCredentialsAdapter is an adapter to convert the user supplied
// interface to its internal counterpart.
type awsSecurityCredentialsAdapter struct {
	scp AwsSecurityCredentialsProvider
}

func (sc *awsSecurityCredentialsAdapter) AwsRegion(ctx context.Context, opts *iexacc.RequestOptions) (string, error) {
	return sc.scp.AwsRegion(ctx, toRequestOptions(opts))
}

func (sc *awsSecurityCredentialsAdapter) AwsSecurityCredentials(ctx context.Context, opts *iexacc.RequestOptions) (*iexacc.AwsSecurityCredentials, error) {
	resp, err := sc.scp.AwsSecurityCredentials(ctx, toRequestOptions(opts))
	if err != nil {
		return nil, err
	}
	return toInternalAwsSecurityCredentials(resp), nil
}
