// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package iamauth

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io/ioutil"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/endpoints"
	"github.com/aws/aws-sdk-go/aws/request"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/iam"
	"github.com/aws/aws-sdk-go/service/sts"
	"github.com/hashicorp/consul-awsauth/responses"
	"github.com/hashicorp/go-hclog"
)

type LoginInput struct {
	Creds            *credentials.Credentials
	IncludeIAMEntity bool
	STSEndpoint      string
	STSRegion        string

	Logger hclog.Logger

	ServerIDHeaderValue string
	// Customizable header names
	ServerIDHeaderName     string
	GetEntityMethodHeader  string
	GetEntityURLHeader     string
	GetEntityHeadersHeader string
	GetEntityBodyHeader    string
}

// GenerateLoginData populates the necessary data to send for the bearer token.
// https://github.com/hashicorp/go-secure-stdlib/blob/main/awsutil/generate_credentials.go#L232-L301
func GenerateLoginData(in *LoginInput) (map[string]interface{}, error) {
	cfg := aws.Config{
		Credentials: in.Creds,
		// These are empty strings by default (i.e. not enabled)
		Region:              aws.String(in.STSRegion),
		Endpoint:            aws.String(in.STSEndpoint),
		STSRegionalEndpoint: endpoints.RegionalSTSEndpoint,
	}

	stsSession, err := session.NewSessionWithOptions(session.Options{Config: cfg})
	if err != nil {
		return nil, err
	}

	svc := sts.New(stsSession)
	stsRequest, _ := svc.GetCallerIdentityRequest(nil)

	// Include the iam:GetRole or iam:GetUser request in headers.
	if in.IncludeIAMEntity {
		entityRequest, err := formatSignedEntityRequest(svc, in)
		if err != nil {
			return nil, err
		}

		headersJson, err := json.Marshal(entityRequest.HTTPRequest.Header)
		if err != nil {
			return nil, err
		}
		requestBody, err := ioutil.ReadAll(entityRequest.HTTPRequest.Body)
		if err != nil {
			return nil, err
		}

		stsRequest.HTTPRequest.Header.Add(in.GetEntityMethodHeader, entityRequest.HTTPRequest.Method)
		stsRequest.HTTPRequest.Header.Add(in.GetEntityURLHeader, entityRequest.HTTPRequest.URL.String())
		stsRequest.HTTPRequest.Header.Add(in.GetEntityHeadersHeader, string(headersJson))
		stsRequest.HTTPRequest.Header.Add(in.GetEntityBodyHeader, string(requestBody))
	}

	// Inject the required auth header value, if supplied, and then sign the request including that header
	if in.ServerIDHeaderValue != "" {
		stsRequest.HTTPRequest.Header.Add(in.ServerIDHeaderName, in.ServerIDHeaderValue)
	}

	stsRequest.Sign()

	// Now extract out the relevant parts of the request
	headersJson, err := json.Marshal(stsRequest.HTTPRequest.Header)
	if err != nil {
		return nil, err
	}
	requestBody, err := ioutil.ReadAll(stsRequest.HTTPRequest.Body)
	if err != nil {
		return nil, err
	}

	return map[string]interface{}{
		"iam_http_request_method": stsRequest.HTTPRequest.Method,
		"iam_request_url":         base64.StdEncoding.EncodeToString([]byte(stsRequest.HTTPRequest.URL.String())),
		"iam_request_headers":     base64.StdEncoding.EncodeToString(headersJson),
		"iam_request_body":        base64.StdEncoding.EncodeToString(requestBody),
	}, nil
}

func formatSignedEntityRequest(svc *sts.STS, in *LoginInput) (*request.Request, error) {
	// We need to retrieve the IAM user or role for the iam:GetRole or iam:GetUser request.
	// GetCallerIdentity returns this and requires no permissions.
	resp, err := svc.GetCallerIdentity(nil)
	if err != nil {
		return nil, err
	}

	arn, err := responses.ParseArn(*resp.Arn)
	if err != nil {
		return nil, err
	}

	iamSession, err := session.NewSessionWithOptions(session.Options{
		Config: aws.Config{
			Credentials: svc.Config.Credentials,
		},
	})
	if err != nil {
		return nil, err
	}
	iamSvc := iam.New(iamSession)

	var req *request.Request
	switch arn.Type {
	case "role", "assumed-role":
		req, _ = iamSvc.GetRoleRequest(&iam.GetRoleInput{RoleName: &arn.FriendlyName})
	case "user":
		req, _ = iamSvc.GetUserRequest(&iam.GetUserInput{UserName: &arn.FriendlyName})
	default:
		return nil, fmt.Errorf("entity %s is not an IAM role or IAM user", arn.Type)
	}

	// Inject the required auth header value, if supplied, and then sign the request including that header
	if in.ServerIDHeaderValue != "" {
		req.HTTPRequest.Header.Add(in.ServerIDHeaderName, in.ServerIDHeaderValue)
	}

	req.Sign()
	return req, nil
}
