package controllers

import (
	"context"
	"crypto/md5"
	"fmt"
	"strings"

	iamv1beta1 "github.com/crossplane-contrib/provider-aws/apis/iam/v1beta1"
	v1alpha12 "github.com/crossplane-contrib/provider-gcp/apis/storage/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/pingcap/errors"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"github.com/tidbcloud/eks-provider/api/v1alpha1"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	provideraws "github.com/tidbcloud/infra-provider/pkg/providers/aws"
)

// PolicyType indicated the policy type for different provider
// currently we support two policies
// 1. s3/gcp bucket restore for cross region restoration.
// 2. aws ebs backup and restore.
type PolicyType string

const (
	BucketType PolicyType = "bucket"
	EBSType    PolicyType = "ebs"
)

type PolicyContext struct {
	// common fields for different provider
	// The Policy Name
	Name       string
	Typ        PolicyType
	Cluster    *cloudv1.Cluster
	Network    *cloudv1.Network
	BucketName string

	// For eks
	S3Prefixes []string
}

// BRAuthorizationMgr is aimed to grant/revoke authorization to node with specified policy. now it's used for
// 1. ebs based backup and restore with eks-provider.
// 2. cross region restoration with eks/gke providers.
// The common usage of BRAuthorizationMgr is for operations that need temporary permissions in a reconcile.
// just create a policyMgr and a PolicyContext.
type BRAuthorizationMgr interface {
	// GrantAuthorization create policy and authorize to node asynchronously, because we need some time to wait policy created.
	// it returns error when the policy not ready. it's by design.
	GrantAuthorization(ctx context.Context, policyContext *PolicyContext) error

	//RevokeAuthorization the policy and cleanup the related policy from existing node role arn.
	RevokeAuthorization(ctx context.Context, policyName string) error
}

type EKSBRAuthorizationMgr struct {
	cli client.Client

	RefName     string
	NodeRoleArn string
}

func NewEKSBRAuthorizationMgr(cli client.Client) *EKSBRAuthorizationMgr {
	return &EKSBRAuthorizationMgr{cli: cli}
}

func (e *EKSBRAuthorizationMgr) bucketPolicyTemplate(policyCtx *PolicyContext) provideraws.PolicyDocument {
	bucketName := policyCtx.BucketName
	s3Resources := []string{fmt.Sprintf("arn:aws:s3:::%s", bucketName)}
	for _, prefix := range policyCtx.S3Prefixes {
		s3Resources = append(s3Resources, fmt.Sprintf("arn:aws:s3:::%s/%s/*", bucketName, prefix))
	}
	return provideraws.PolicyDocument{
		Version: provideraws.PolicyVersion,
		Statement: []provideraws.PolicyStatement{
			{
				Effect:   provideraws.StatementEffectAllow,
				Action:   []string{"s3:*"},
				Resource: s3Resources,
			},
		},
	}
}

func (e *EKSBRAuthorizationMgr) EBSPolicyTemplate() provideraws.PolicyDocument {
	return provideraws.PolicyDocument{
		Version: provideraws.PolicyVersion,
		Statement: []provideraws.PolicyStatement{
			{
				Effect: "Allow",
				Action: []string{
					"ec2:AttachVolume",
					"ec2:CreateSnapshot",
					"ec2:CreateTags",
					"ec2:CreateVolume",
					"ec2:DeleteSnapshot",
					"ec2:DeleteTags",
					"ec2:DeleteVolume",
					"ec2:DescribeInstances",
					"ec2:DescribeSnapshots",
					"ec2:DescribeTags",
					"ec2:DescribeVolumes",
					"ec2:DetachVolume",
				},
				Resource: []string{"*"},
			},
		},
	}
}

func (e *EKSBRAuthorizationMgr) GrantAuthorization(ctx context.Context, policyCtx *PolicyContext) error {
	networkSpec, err := v1alpha1.EKSNetworkSpecForNetwork(policyCtx.Network)
	if err != nil {
		return err
	}
	if networkSpec.NodeRoleArn == "" {
		return errors.New(fmt.Sprintf("not find node role arn from network, cluster: %s", policyCtx.Cluster.Name))
	}
	e.NodeRoleArn = networkSpec.NodeRoleArn
	e.RefName = namer.UniqueIdentifier(policyCtx.Network.ObjectMeta)

	var pd provideraws.PolicyDocument
	switch policyCtx.Typ {
	case BucketType:
		pd = e.bucketPolicyTemplate(policyCtx)
	case EBSType:
		pd = e.EBSPolicyTemplate()
	}
	pdStr, err := pd.ToString()
	if err != nil {
		return err
	}
	policy := &iamv1beta1.Policy{
		ObjectMeta: metav1.ObjectMeta{
			Name: policyCtx.Name,
		},
		Spec: iamv1beta1.PolicySpec{
			ForProvider: iamv1beta1.PolicyParameters{
				Name:     policyCtx.Name,
				Document: pdStr,
			},
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: e.RefName},
			},
		},
	}

	_, err = controllerutil.CreateOrUpdate(ctx, e.cli, policy, func() error {
		return nil
	})
	if err != nil {
		return err
	}
	if len(policy.Status.AtProvider.ARN) == 0 {
		return errors.New(fmt.Sprintf("new created policy for not ready, cluster: %s, bucket: %s", policyCtx.Cluster.Name, policyCtx.BucketName))
	}

	return e.attachToNodeRole(ctx, policyCtx)
}

func (e *EKSBRAuthorizationMgr) RevokeAuthorization(ctx context.Context, policyName string) error {
	if err := e.detachFromNodeRole(ctx, policyName); err != nil {
		return errors.Annotate(err, "detach backup bucket policy")
	}

	policy := new(iamv1beta1.Policy)
	key := types.NamespacedName{
		Name: policyName,
	}
	if err := e.cli.Get(ctx, key, policy); err != nil {
		if errors.IsNotFound(err) {
			return nil
		}
		return err
	}
	return e.cli.Delete(ctx, policy)
}

func (e *EKSBRAuthorizationMgr) attachToNodeRole(ctx context.Context, policyCtx *PolicyContext) error {
	roleName := roleNameFromARN(e.NodeRoleArn)
	attachment := &iamv1beta1.RolePolicyAttachment{
		ObjectMeta: metav1.ObjectMeta{
			Name: nodeRoleAttachmentName(policyCtx.Name),
		},
		Spec: iamv1beta1.RolePolicyAttachmentSpec{
			ForProvider: iamv1beta1.RolePolicyAttachmentParameters{
				PolicyARNRef: &xpv1.Reference{
					Name: policyCtx.Name,
				},
				RoleNameRef: &xpv1.Reference{
					Name: roleName,
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: e.RefName},
			},
		},
	}

	_, err := controllerutil.CreateOrUpdate(ctx, e.cli, attachment, func() error {
		return nil
	})
	if err != nil {
		return err
	}
	if len(attachment.Status.AtProvider.AttachedPolicyARN) == 0 {
		return errors.New(fmt.Sprintf("role policy attachment is not ready, cluster: %s, bucket: %s", policyCtx.Cluster.Name, policyCtx.BucketName))
	}
	return nil
}

func (e *EKSBRAuthorizationMgr) detachFromNodeRole(ctx context.Context, policyName string) error {
	attachmentName := nodeRoleAttachmentName(policyName)
	attachment := new(iamv1beta1.RolePolicyAttachment)
	key := types.NamespacedName{
		Name: attachmentName,
	}
	if err := e.cli.Get(ctx, key, attachment); err != nil {
		if errors.IsNotFound(err) {
			return nil
		}
		return err
	}
	return e.cli.Delete(ctx, attachment)
}

type GKEBRAuthorizationMgr struct {
	cli client.Client
}

func NewGKEBRAuthorizationMgr(cli client.Client) *GKEBRAuthorizationMgr {
	return &GKEBRAuthorizationMgr{cli}
}

func (g *GKEBRAuthorizationMgr) GrantAuthorization(ctx context.Context, policyCtx *PolicyContext) error {
	refName := namer.UniqueIdentifier(policyCtx.Network.ObjectMeta)
	roleName := "roles/storage.admin"
	member := fmt.Sprintf("serviceAccount:%s", policyCtx.Network.Status.IAM.ServiceAccountEmail)
	policyMember := &v1alpha12.BucketPolicyMember{
		ObjectMeta: metav1.ObjectMeta{
			Name: policyCtx.Name,
		},
		Spec: v1alpha12.BucketPolicyMemberSpec{
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: refName},
			},
			ForProvider: v1alpha12.BucketPolicyMemberParameters{
				Bucket: &policyCtx.BucketName,
				Role:   roleName,
				Member: &member,
			},
		},
	}
	_, err := controllerutil.CreateOrUpdate(ctx, g.cli, policyMember, func() error {
		return nil
	})
	if err != nil {
		return err
	}
	return g.waitBucketPolicyReadyGCP(ctx, policyCtx.Name)
}

func (g *GKEBRAuthorizationMgr) waitBucketPolicyReadyGCP(ctx context.Context, policyName string) error {
	bucketPolicy := &v1alpha12.BucketPolicyMember{
		ObjectMeta: metav1.ObjectMeta{
			Name: policyName,
		},
	}
	err := g.cli.Get(ctx, client.ObjectKeyFromObject(bucketPolicy), bucketPolicy)
	if err != nil {
		return err
	}
	ready := bucketPolicy.GetCondition(xpv1.TypeReady)
	synced := bucketPolicy.GetCondition(xpv1.TypeSynced)
	if ready.Status != corev1.ConditionTrue && synced.Status != corev1.ConditionTrue {
		return fmt.Errorf("bucket policy not ready")
	}
	return nil
}

func (g *GKEBRAuthorizationMgr) RevokeAuthorization(ctx context.Context, policyName string) error {
	bucketPolicy := &v1alpha12.BucketPolicyMember{
		ObjectMeta: metav1.ObjectMeta{
			Name: policyName,
		},
	}
	err := g.cli.Delete(ctx, bucketPolicy)
	if err != nil && !errors.IsNotFound(err) {
		return err
	}
	return nil
}

func BucketPolicyName(restoreMeta metav1.ObjectMeta, restoreName string) string {
	return namer.UniqueIdentifier(restoreMeta, func(s string) string {
		return fmt.Sprintf("restore-%s-%s", restoreName, s)
	})
}

func NetworkPolicyName(networkMeta metav1.ObjectMeta) string {
	return namer.UniqueIdentifier(networkMeta, func(s string) string {
		return fmt.Sprintf("policy-%s", s)
	})
}

func nodeRoleAttachmentName(policyName string) string {
	policyHash := fmt.Sprintf("%x", md5.Sum([]byte(policyName)))
	return fmt.Sprintf("restore-%s", policyHash)
}

func roleNameFromARN(arn string) string {
	idx := strings.LastIndex(arn, "/")
	return arn[idx+1:]
}
