package signer

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"io"
	"net/http"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
	awsconfig "github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/credentials"
	"github.com/projectdiscovery/gologger"
	"github.com/projectdiscovery/utils/errkit"
)

const defaultEmptyPayloadHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"

// AWSOptions
type AWSOptions struct {
	AwsID          string
	AwsSecretToken string
	Service        string
	Region         string
}

// Validate Signature Arguments
func (a *AWSOptions) Validate() error {
	if a.Service == "" {
		return errors.New("aws service cannot be empty")
	}
	if a.Region == "" {
		return errors.New("aws region cannot be empty")
	}

	return nil
}

// AWS v4 signer
type AWSSigner struct {
	creds   *aws.Credentials
	signer  *v4.Signer
	options *AWSOptions
}

// SignHTTP
func (a *AWSSigner) SignHTTP(ctx context.Context, request *http.Request) error {
	if region, ok := ctx.Value(SignerArg("region")).(string); ok && region != "" {
		a.options.Region = region
	}
	if service, ok := ctx.Value(SignerArg("service")).(string); ok && service != "" {
		a.options.Service = service
	}
	if err := a.options.Validate(); err != nil {
		return err
	}
	// contentHash is sha256 hash of response body
	contentHash := a.getPayloadHash(request)
	if err := a.signer.SignHTTP(ctx, *a.creds, request, contentHash, a.options.Service, a.options.Region, time.Now()); err != nil {
		return errkit.Wrap(err, "failed to sign http request using aws v4 signer")
	}
	// add x-amz-content-sha256 header to request
	request.Header.Set("x-amz-content-sha256", contentHash)
	return nil
}

// getPayloadHash returns hex encoded SHA-256 of request body
func (a *AWSSigner) getPayloadHash(request *http.Request) string {
	if request.Body == nil {
		// Default Hash of Empty Payload
		return defaultEmptyPayloadHash
	}

	// no need to close request body since it is a reusablereadercloser
	bin, err := io.ReadAll(request.Body)
	if err != nil {
		gologger.Error().Msgf("aws signer: failed to read request body: %s", err)
	}
	sha256Hash := sha256.Sum256(bin)
	return hex.EncodeToString(sha256Hash[:])
}

// NewAwsSigner
func NewAwsSigner(opts *AWSOptions) (*AWSSigner, error) {
	credcache := aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(opts.AwsID, opts.AwsSecretToken, ""))
	awscred, err := credcache.Retrieve(context.TODO())
	if err != nil {
		return nil, err
	}
	return &AWSSigner{
		creds:   &awscred,
		options: opts,
		signer:  v4.NewSigner(),
	}, nil
}

// NewAwsSignerFromConfig
func NewAwsSignerFromConfig(opts *AWSOptions) (*AWSSigner, error) {
	/*
		NewAwsSignerFromConfig fetches credentials from both
		1. Environment Variables (old & new)
		2. Shared Credentials ($HOME/.aws)
	*/
	cfg, err := awsconfig.LoadDefaultConfig(context.TODO())
	if err != nil {
		return nil, err
	}
	credcache := aws.NewCredentialsCache(cfg.Credentials)
	awscred, err := credcache.Retrieve(context.TODO())
	if err != nil {
		return nil, err
	}
	return &AWSSigner{
		creds:   &awscred,
		options: opts,
		signer: v4.NewSigner(func(signer *v4.SignerOptions) {
			// signer.DisableURIPathEscaping = true
		}),
	}, nil
}

var AwsSkipList = map[string]interface{}{
	"region": struct{}{},
}

var AwsDefaultVars = map[string]interface{}{
	"region":  "us-east-2",
	"service": "sts",
}

var AwsInternalOnlyVars = map[string]interface{}{
	"aws-id":     struct{}{},
	"aws-secret": struct{}{},
}
