package network

import (
	"context"
	"fmt"
	"strings"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/iam"
	iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	awsapi "github.com/tidbcloud/infra-api/pkg/providers/aws"
	iamutil "github.com/tidbcloud/infra-api/pkg/util"
	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/tidbcloud/shared-provider/pkg/types"
	"github.com/tidbcloud/shared-provider/pkg/utils/labels"
	"github.com/tidbcloud/shared-provider/pkg/utils/namespace"
)

func (r *Reconciler) ensureBackupRole(ctx context.Context, net *cloudv1.Network) error {
	region := net.Spec.Region
	bucketName := getBackupBucketName(r.Env, region, labels.GetProjectFromLabels(net.Labels))
	tenantID := labels.GetTenantFromLabels(net.Labels)
	projectID := labels.GetProjectFromLabels(net.Labels)
	tags := buildTags(tenantID, projectID, r.Env)

	// TODO: reconsider whether backup role should be created in manager
	roleName := getAWSBackupRoleName(net.Spec.Provider, r.KubeClusterName, net.Namespace)

	if err := r.ensureAWSBackupRole(ctx, net, roleName, bucketName, tags); err != nil {
		return err
	}
	roleArn := getAWSIamIdentity(r.AWSAccountID, roleName)

	// set backup role name in the network status
	status, err := types.SharedNetworkStatusFromProvider(net)
	if err != nil {
		return err
	}
	if status.BackupRole != roleArn {
		status.BackupRole = roleArn
		if err = types.SetSharedNetworkStatus(net, status); err != nil {
			return err
		}
		if err = r.InfraClient.Status().Update(ctx, net); err != nil {
			return err
		}
	}
	return nil
}

func (r *Reconciler) deleteBackupRole(ctx context.Context, net *cloudv1.Network) error {
	roleName := getAWSBackupRoleName(net.Spec.Provider, r.KubeClusterName, net.Namespace)

	switch net.Spec.Provider {
	case types.ProviderTypeAWSSharedTier:
		return iamutil.DeleteRoleIgnoreNotFound(ctx, r.AwsClient.IAM(), roleName)
	case types.ProviderTypeGCPSharedTier:
		return iamutil.DeleteRoleIgnoreNotFound(ctx, r.AwsClient.IAM(), roleName)
	default:
		logger := log.FromContext(ctx)
		logger.Error(nil, "unknown provider type", "type", net.Spec.Provider)
	}

	return nil
}

func (r *Reconciler) ensureAWSBackupRole(ctx context.Context, net *cloudv1.Network,
	roleName, bucketName string, tags map[string]string) error {
	// nolint: gomnd // magic number
	audSub := fmt.Sprintf("%s:sub", strings.SplitN(r.OIDCProviderARN, "/", 2)[1])
	audSA := fmt.Sprintf("system:serviceaccount:%s:%s",
		namespace.GetOwnedNamespaceNameFromNetwork(net), types.S3BackupServiceAccount)

	assumePolicy := awsapi.NewAssumeRoleWithWebIdentityPolicy(r.OIDCProviderARN,
		awsapi.StatementCondition{StringEquals: map[string]string{audSub: audSA}})
	assumePolicyJSON, err := assumePolicy.ToString()
	if err != nil {
		return err
	}

	var awsTags []iamtypes.Tag
	for k, v := range tags {
		awsTags = append(awsTags, iamtypes.Tag{
			Key:   aws.String(k),
			Value: aws.String(v),
		})
	}
	roleInput := &iam.CreateRoleInput{
		AssumeRolePolicyDocument: aws.String(assumePolicyJSON),
		RoleName:                 &roleName,
		Description:              aws.String("role used to backup and restore clusters"),
		Path:                     aws.String("/"),
		Tags:                     awsTags,
	}

	statements := []awsapi.PolicyStatement{
		{
			Effect: "Allow",
			Action: []string{
				"kms:Encrypt",
				"kms:Decrypt",
				"kms:GenerateDataKey",
				"kms:GenerateDataKeyWithoutPlaintext",
			},
			Resource: []string{"*"},
			Condition: awsapi.StatementCondition{
				StringEquals: map[string]string{
					"aws:ResourceTag/sa": fmt.Sprintf("${%s}", audSub),
				},
			},
		},
	}

	if net.Spec.Provider == types.ProviderTypeAWSSharedTier {
		statements = append(statements, awsapi.PolicyStatement{
			Effect: "Allow",
			Action: []string{"s3:*"},
			Resource: []string{
				fmt.Sprintf("arn:aws:s3:::%s", bucketName),
				fmt.Sprintf("arn:aws:s3:::%s/*", bucketName)},
		})
	}

	policy := awsapi.NewRolePolicy(statements...)

	policyJSON, err := policy.ToString()
	if err != nil {
		return err
	}
	policyInput := &iam.PutRolePolicyInput{
		PolicyDocument: aws.String(policyJSON),
		PolicyName:     aws.String("backup-policy"),
		RoleName:       &roleName,
	}

	return iamutil.EnsureRole(ctx, r.AwsClient.IAM(), roleInput, policyInput)
}
