package gardener

// change from https://github.com/tidbcloud/dbaas/blob/bec620656849e8b1b98816fd63990712d6b932ab/central/runtime/deployer/gardener/node_policy.go#L1

import (
	"context"
	"fmt"
	"strings"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/iam"
	"github.com/pingcap/errors"
	"go.uber.org/multierr"

	"github.com/tidbcloud/infra-api/pkg/consts"
	provideraws "github.com/tidbcloud/infra-api/pkg/providers/aws"
	providergcp "github.com/tidbcloud/infra-api/pkg/providers/gcp"
)

type Policy interface {
	PolicyName() string
	PolicyDocument() (string, error)
}

type s3Policy struct {
	projectId     string
	gardenProject string
	bucketName    string
}

func NewS3Policy(gardenProject string, projectId string, bucketName string) Policy {
	return s3Policy{
		projectId:     projectId,
		gardenProject: gardenProject,
		bucketName:    bucketName,
	}
}

func (s s3Policy) PolicyName() string {
	return fmt.Sprintf("dbaas-%s-%s-s3-access", s.gardenProject, s.projectId)
}

func (s s3Policy) PolicyDocument() (string, error) {
	p := provideraws.PolicyDocument{
		Version: provideraws.PolicyVersion,
		Statement: []provideraws.PolicyStatement{
			{
				Effect:   "Allow",
				Action:   []string{"s3:*"},
				Resource: []string{fmt.Sprintf("arn:aws:s3:::%s", s.bucketName), fmt.Sprintf("arn:aws:s3:::%s/*", s.bucketName)},
			},
		},
	}

	return p.ToString()
}

type sqsPolicy struct {
	policyName string
	accountID  string
	queueName  string
}

// queueName: the queue name of config.Context.SqsEndpoint() in central
func NewSqsPolicy(policyName string, accountID string, queueName string) Policy {
	return sqsPolicy{
		policyName: policyName,
		accountID:  accountID,
		queueName:  queueName,
	}
}

func (s sqsPolicy) PolicyName() string {
	return s.policyName
}

func (s sqsPolicy) PolicyDocument() (string, error) {
	queueName := s.queueName

	p := provideraws.PolicyDocument{
		Version: provideraws.PolicyVersion,
		Statement: []provideraws.PolicyStatement{
			{
				Effect:   "Allow",
				Action:   []string{"sqs:SendMessage"},
				Resource: []string{fmt.Sprintf("arn:aws:sqs:*:%s:%s", s.accountID, queueName)},
			},
		},
	}

	return p.ToString()
}

type assumeRolePolicy struct {
	policyName string
}

func NewAssumeRolePolicy(policyName string) Policy {
	return assumeRolePolicy{
		policyName: policyName,
	}
}

func (s assumeRolePolicy) PolicyName() string {
	return s.policyName
}

func (s assumeRolePolicy) PolicyDocument() (string, error) {
	p := provideraws.PolicyDocument{
		Version: provideraws.PolicyVersion,
		Statement: []provideraws.PolicyStatement{
			{
				Effect:   "Allow",
				Action:   []string{"sts:AssumeRole"},
				Resource: []string{"*"},
			},
		},
	}

	return p.ToString()
}

type certManagerPolicy struct{}

func NewCertManagerPolicy() Policy {
	return certManagerPolicy{}
}

func (c certManagerPolicy) PolicyName() string {
	return "dbaas-certmanager-policy"
}

func (c certManagerPolicy) PolicyDocument() (string, error) {
	p := provideraws.PolicyDocument{
		Version: provideraws.PolicyVersion,
		Statement: []provideraws.PolicyStatement{
			{
				Action:   []string{"route53:GetChange"},
				Effect:   "Allow",
				Resource: []string{"arn:aws:route53:::change/*"},
			},
			{
				Action:   []string{"route53:ChangeResourceRecordSets", "route53:ListResourceRecordSets"},
				Effect:   "Allow",
				Resource: []string{"arn:aws:route53:::hostedzone/*"},
			},
		},
	}

	return p.ToString()
}

type pcaPolicy struct{}

func NewPCAPolicy() Policy {
	return pcaPolicy{}
}

func (c pcaPolicy) PolicyName() string {
	return "dbaas-pca-policy"
}

func (c pcaPolicy) PolicyDocument() (string, error) {
	p := provideraws.PolicyDocument{
		Version: provideraws.PolicyVersion,
		Statement: []provideraws.PolicyStatement{
			{
				Action:   []string{"acm-pca:IssueCertificate", "acm-pca:GetCertificate", "acm-pca:GetCertificateAuthorityCertificate"},
				Effect:   "Allow",
				Resource: []string{"*"},
			},
		},
	}

	return p.ToString()
}

// ec2VolumeModifyPolicy allows to modify the volume, it is used by tidb-operator
type ec2VolumeModifyPolicy struct{}

func NewEC2VolumeModifyPolicy() Policy {
	return ec2VolumeModifyPolicy{}
}

func (s ec2VolumeModifyPolicy) PolicyName() string {
	return "dbaas-ec2-volume-modify"
}

func (s ec2VolumeModifyPolicy) PolicyDocument() (string, error) {
	p := provideraws.PolicyDocument{
		Version: provideraws.PolicyVersion,
		Statement: []provideraws.PolicyStatement{
			{
				Effect: "Allow",
				Action: []string{
					"ec2:ModifyVolume",
					"ec2:DescribeVolumes",
					"ec2:DescribeVolumesModifications",
				},
				Resource: []string{"*"},
			},
		},
	}

	return p.ToString()
}

func putPoliciesToNodeRole(
	ctx context.Context,
	client provideraws.IAMClient,
	shootName string,
	project string,
	accountID string,
	policies ...Policy) error {
	var errs error
	for _, p := range policies {
		pName := p.PolicyName()
		if err := putPolicy2Node(ctx, client, shootName, project, accountID, pName, p.PolicyDocument); err != nil {
			errs = multierr.Append(errs, err)
		}
	}

	return errs
}

// putPolicy2Node support Repeat execution.
func putPolicy2Node(
	ctx context.Context,
	client provideraws.IAMClient,
	shootName string,
	projectName string,
	accountID string,
	policyName string,
	policyFun func() (string, error)) error {
	nodeARN, err := getShootWorkerIamIdentity(consts.ProviderTypeAWS, projectName, shootName, accountID)
	if err != nil {
		return errors.Annotate(err, "getShootWorkerIamIdentity")
	}

	policy, err := policyFun()
	if err != nil {
		return errors.Annotate(err, "policyFun")
	}

	role := nodeARN[strings.LastIndex(nodeARN, "/")+1:]
	policyInput := &iam.PutRolePolicyInput{
		PolicyDocument: aws.String(policy),
		PolicyName:     aws.String(policyName),
		RoleName:       aws.String(role),
	}

	_, err = client.PutRolePolicy(ctx, policyInput)
	return errors.Annotatef(err, "PutRolePolicy %s %s", role, policyName)
}

// detachPolicy delete all inline policy and detach all policy of the node role.
func detachPolicy(
	ctx context.Context,
	client provideraws.IAMClient,
	shootName string,
	projectName string,
	accountID string,
) error {
	nodeARN, err := getShootWorkerIamIdentity(consts.ProviderTypeAWS, projectName, shootName, accountID)
	if err != nil {
		return err
	}

	// delete all inline policy
	roleName := aws.String(nodeARN[strings.LastIndex(nodeARN, "/")+1:])
	output, err := client.ListRolePolicies(ctx, &iam.ListRolePoliciesInput{
		RoleName: roleName,
	})
	if err != nil {
		return err
	}

	var errs error
	for _, policy := range output.PolicyNames {
		_, err := client.DeleteRolePolicy(ctx, &iam.DeleteRolePolicyInput{
			RoleName:   roleName,
			PolicyName: aws.String(policy),
		})

		errs = multierr.Append(errs, err)
	}

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

	for _, policy := range attachedPolicies.AttachedPolicies {
		_, err := client.DetachRolePolicy(ctx, &iam.DetachRolePolicyInput{
			RoleName:  roleName,
			PolicyArn: policy.PolicyArn,
		})

		errs = multierr.Append(errs, err)
	}

	return errs
}

// attachPolicyToNodeSa add policy binding to GCE node service account
// Note: GCP has some limitations for IAM  https://cloud.google.com/iam/quotas#limits
// Each policy can have at most 1500 member bindings
// Each GCP project can have at most 300 custom roles
// There are 3 resources (project, service account and bucket) that can have a policy
// For CloudDNS, we create only one role globally. cert-manager should be moved into seed cluster
// certmanagerRole: config.Global().InfraConfig.GcpCertManagerRole in central
func attachPolicyToNodeSa(
	gcpapi providergcp.GCPAPI,
	gardenProject string,
	shootName string,
	region string,
	project string,
	projectID string,
	certmanagerRole string,
) error {
	identity, err := getShootWorkerIamIdentity(consts.ProviderTypeGCP, gardenProject, shootName, projectID)
	if err != nil {
		return err
	}
	member := fmt.Sprintf("serviceAccount:%s", identity)

	return gcpapi.AddPolicyBinding(projectID, member, certmanagerRole)
}
