package util

import (
	"context"
	"errors"
	"fmt"

	"github.com/aws/aws-sdk-go-v2/service/iam"
	iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/smithy-go"
	perrors "github.com/pingcap/errors"

	awsapi "github.com/tidbcloud/infra-api/pkg/providers/aws"
)

// CreateOrUpdateRole create the role or update the assume role policy if the role already exist.
func CreateOrUpdateRole(ctx context.Context, iamClient awsapi.IAMClient, params *iam.CreateRoleInput) error {
	_, err := iamClient.CreateRole(ctx, params)
	if err != nil {
		if IsIAMEntityAlreadyExists(err) {
			updateAssumeRolePolicyInput := &iam.UpdateAssumeRolePolicyInput{
				PolicyDocument: params.AssumeRolePolicyDocument,
				RoleName:       params.RoleName,
			}
			_, err := iamClient.UpdateAssumeRolePolicy(ctx, updateAssumeRolePolicyInput)
			return err
		}

		return err
	}

	return nil
}

// EnsureRole makes sure the role is created and put the inline polices.
func EnsureRole(
	ctx context.Context,
	iamClient awsapi.IAMClient,
	createInput *iam.CreateRoleInput,
	policys ...*iam.PutRolePolicyInput,
) error {
	err := CreateOrUpdateRole(ctx, iamClient, createInput)
	if err != nil {
		return err
	}

	for _, p := range policys {
		_, err = iamClient.PutRolePolicy(ctx, p)
		if err != nil {
			return perrors.Annotatef(err, "failed to PutRolePolicy: %+v", *p)
		}
	}

	return nil
}

// EnsureUser make sure the user created.
func EnsureUser(ctx context.Context, iamClient awsapi.IAMClient, params *iam.CreateUserInput) error {
	_, err := iamClient.CreateUser(ctx, params)
	if err != nil {
		if IsIAMEntityAlreadyExists(err) {
			return nil
		}
		return err
	}

	return nil
}

// IsIAMEntityAlreadyExists return true if the created entity already exist.
//
// A test on aws DeleteRole return error:
//
// operation error IAM: CreateRole, https response error StatusCode: 409, RequestID: 3b07f36e-b1e0-48e2-bea7-7edbd0894dc2, api error EntityAlreadyExists: Role with name dbaas-infra-dev-1234560-cf124554-import already exists
//
// Not retruning *iamtypes.EntityAlreadyExistsException
func IsIAMEntityAlreadyExists(err error) bool {
	var notExist *iamtypes.EntityAlreadyExistsException
	if errors.As(err, &notExist) {
		return true
	}

	var apiErr smithy.APIError
	if errors.As(err, &apiErr) {
		code := apiErr.ErrorCode()
		if code == "EntityAlreadyExists" {
			return true
		}
	}

	return false
}

// IsIAMEntityAlreadyExists return true if the created entity already exist.
// ref: https://aws.github.io/aws-sdk-go-v2/docs/migrating/#errors-types
// A test on aws DeleteRole return error:
//
// operation error IAM: DeleteRole, https response error StatusCode: 404, RequestID: 12b14a5d-3729-4a82-896e-7648f405533f, api error NoSuchEntity: The role with name dbaas-infra-dev-1234560-default-network-sample-import cannot be found.""
//
// Not retruning *iamtypes.NoSuchEntityException
// iamtypes.NoSuchEntityException.ErrorCode() is "NoSuchEntityException"
func IsIAMNoSuchEntityException(err error) bool {
	var e *iamtypes.NoSuchEntityException
	if errors.As(err, &e) {
		return true
	}

	var apiErr smithy.APIError
	if errors.As(err, &apiErr) {
		code := apiErr.ErrorCode()
		if code == "NoSuchEntity" {
			return true
		}
	}

	return false
}

func DeleteRoleIgnoreNotFound(
	ctx context.Context,
	iamClient awsapi.IAMClient,
	roleName string,
) error {
	err := DeleteAndDetachRolePolicies(ctx, iamClient, roleName)
	// Ignore NoSuchEntity for if the role is already deleted
	// but we still try to delete again.
	if err != nil && !IsIAMNoSuchEntityException(err) {
		return perrors.AddStack(err)
	}

	input := &iam.DeleteRoleInput{
		RoleName: aws.String(roleName),
	}

	_, err = iamClient.DeleteRole(ctx, input)

	if err != nil {
		if IsIAMNoSuchEntityException(err) {
			return nil
		}
		return perrors.AddStack(err)
	}

	return nil
}

// DeleteUserAccessKeys delete all the access keys of the user.
func DeleteUserAccessKeys(
	ctx context.Context,
	iamClient awsapi.IAMClient,
	name string,
) error {
	listInput := &iam.ListAccessKeysInput{
		UserName: aws.String(name),
	}

	listOutput, err := iamClient.ListAccessKeys(ctx, listInput)
	if err != nil {
		return err
	}

	for _, item := range listOutput.AccessKeyMetadata {
		input := &iam.DeleteAccessKeyInput{
			UserName:    aws.String(name),
			AccessKeyId: item.AccessKeyId,
		}

		_, err := iamClient.DeleteAccessKey(ctx, input)
		if err != nil {
			return perrors.Annotatef(err, "failed to delete key %q for user %q", *item.AccessKeyId, name)
		}
	}

	return nil
}

func DeleteUserIgnoreNotFound(
	ctx context.Context,
	iamClient awsapi.IAMClient,
	roleName string,
) error {
	input := &iam.DeleteUserInput{
		UserName: aws.String(roleName),
	}

	_, err := iamClient.DeleteUser(ctx, input)

	if err != nil {
		if IsIAMNoSuchEntityException(err) {
			return nil
		}
		return perrors.AddStack(err)
	}

	return nil
}

// DeleteAndDetachRolePolicies delete and detach all polices about the role.
func DeleteAndDetachRolePolicies(
	ctx context.Context,
	client awsapi.IAMClient,
	roleName string,
) error {
	// delete all inline policy
	output, err := client.ListRolePolicies(ctx, &iam.ListRolePoliciesInput{
		RoleName: aws.String(roleName),
	})
	if err != nil {
		return err
	}

	for _, policy := range output.PolicyNames {
		_, err := client.DeleteRolePolicy(ctx, &iam.DeleteRolePolicyInput{
			RoleName:   aws.String(roleName),
			PolicyName: aws.String(policy),
		})
		if err != nil {
			return perrors.Annotatef(err, "failed to delete policy %q for role %q", policy, roleName)
		}
	}

	// detach all policy
	attachedPolicies, err := client.ListAttachedRolePolicies(ctx, &iam.ListAttachedRolePoliciesInput{
		RoleName: aws.String(roleName),
	})
	if err != nil {
		return err
	}

	for _, policy := range attachedPolicies.AttachedPolicies {
		_, err := client.DetachRolePolicy(ctx, &iam.DetachRolePolicyInput{
			RoleName:  aws.String(roleName),
			PolicyArn: policy.PolicyArn,
		})
		if err != nil {
			return perrors.Annotatef(err, "failed to detach policy %q for role %q", *policy.PolicyArn, roleName)
		}
	}

	return nil
}

// GetRootUserArn return the arn of the root user, like `arn:aws:iam::123456789012:root`.
func GetRootUserArn(accountID string) string {
	return fmt.Sprintf("arn:aws:iam::%s:root", accountID)
}
