package eks

import (
	"context"
	"strings"
	"time"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/appbox/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/namer"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
	eksv1beta1 "github.com/upbound/provider-aws/apis/eks/v1beta1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	// adminRolePolicy is the aws pre-defined administrative access policy.
	adminRolePolicy = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
)

type AccessEntry struct {
	*task.BaseTask
}

func NewAccessEntryTask(ctx *task.Context) deploy.Task {
	return &AccessEntry{
		BaseTask: &task.BaseTask{Context: ctx},
	}
}

func (t *AccessEntry) Name() string {
	return "EKSAccessEntry"
}

func (t *AccessEntry) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	for _, obj := range t.getAllAccessEntryObjs() {
		if err := k8s.Apply(ctx, t.Local, obj); err != nil {
			return deploy.Error(err)
		}
	}
	return deploy.Done()
}

func (t *AccessEntry) Delete(ctx context.Context) deploy.Result {
	if err := t.updateManagementPolicies(ctx); err != nil {
		return deploy.Error(err)
	}
	for _, obj := range t.getAllAccessEntryObjs() {
		if err := k8s.DeleteObject(ctx, t.Local, obj); err != nil {
			return deploy.Error(err)
		}
	}
	return deploy.Done()
}

func (t *AccessEntry) updateManagementPolicies(ctx context.Context) error {
	entryName := controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(t.Config.ResourceNamePrefix))
	associationName := controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(t.Config.ResourceNamePrefix))

	entry := &eksv1beta1.AccessEntry{
		ObjectMeta: metav1.ObjectMeta{
			Name: entryName,
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(entry), entry); client.IgnoreNotFound(err) != nil {
		return err
	}
	entry.Spec.ResourceSpec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll}
	if err := k8s.PatchObject(ctx, t.Local, entry, controllers.ManagementPoliciesPatch); client.IgnoreNotFound(err) != nil {
		return err
	}

	association := &eksv1beta1.AccessPolicyAssociation{
		ObjectMeta: metav1.ObjectMeta{
			Name: associationName,
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(association), association); client.IgnoreNotFound(err) != nil {
		return err
	}
	association.Spec.ResourceSpec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll}
	if err := k8s.PatchObject(ctx, t.Local, association, controllers.ManagementPoliciesPatch); client.IgnoreNotFound(err) != nil {
		return err
	}

	for _, authorizedSubjectRole := range t.Config.ControllerConfig.APPBoxConfig.AuthorizedSubjects {
		authorizedSubjectsAccessEntryName := t.authorizedSubjectsAccessEntryName(authorizedSubjectRole)
		authorizedSubjectsEntry := &eksv1beta1.AccessEntry{
			ObjectMeta: metav1.ObjectMeta{
				Name: authorizedSubjectsAccessEntryName,
			},
		}
		authorizedSubjectsAssociation := &eksv1beta1.AccessPolicyAssociation{
			ObjectMeta: metav1.ObjectMeta{
				Name: authorizedSubjectsAccessEntryName,
			},
		}

		if err := t.Local.Get(ctx, client.ObjectKeyFromObject(authorizedSubjectsEntry), authorizedSubjectsEntry); client.IgnoreNotFound(err) != nil {
			return err
		}
		authorizedSubjectsEntry.Spec.ResourceSpec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll}
		if err := k8s.PatchObject(ctx, t.Local, authorizedSubjectsEntry, controllers.ManagementPoliciesPatch); client.IgnoreNotFound(err) != nil {
			return err
		}

		if err := t.Local.Get(ctx, client.ObjectKeyFromObject(authorizedSubjectsAssociation), authorizedSubjectsAssociation); client.IgnoreNotFound(err) != nil {
			return err
		}
		authorizedSubjectsAssociation.Spec.ResourceSpec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll}
		if err := k8s.PatchObject(ctx, t.Local, authorizedSubjectsAssociation, controllers.ManagementPoliciesPatch); client.IgnoreNotFound(err) != nil {
			return err
		}
	}
	return nil
}

func (t *AccessEntry) getAllAccessEntryObjs() []client.Object {
	objects := make([]client.Object, 0, 2+2*len(t.Config.ControllerConfig.APPBoxConfig.AuthorizedSubjects))

	clusterName := controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(t.Config.ResourceNamePrefix))
	entryName := controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(t.Config.ResourceNamePrefix))
	associationName := controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(t.Config.ResourceNamePrefix))
	roleArn := util.GetRoleARN(t.VPC.Spec.AccountRef.AccountID, t.Config.DataPlaneRoleName)
	entry, association := t.buildAccessEntry(entryName, associationName, clusterName, roleArn, adminRolePolicy)
	objects = append(objects, entry)
	objects = append(objects, association)

	// add additional access entries for other services
	for _, authorizedSubjectRole := range t.Config.ControllerConfig.APPBoxConfig.AuthorizedSubjects {
		authorizedSubjectsAccessEntryName := t.authorizedSubjectsAccessEntryName(authorizedSubjectRole)
		entry, association = t.buildAccessEntry(authorizedSubjectsAccessEntryName, authorizedSubjectsAccessEntryName, clusterName, authorizedSubjectRole, adminRolePolicy)
		objects = append(objects, entry)
		objects = append(objects, association)
	}
	return objects
}

func (t *AccessEntry) buildAccessEntry(entryName, associationName,
	clusterName, roleArn, policyArn string) (*eksv1beta1.AccessEntry, *eksv1beta1.AccessPolicyAssociation) {
	entry := &eksv1beta1.AccessEntry{
		ObjectMeta: metav1.ObjectMeta{
			Name: entryName,
		},
		Spec: eksv1beta1.AccessEntrySpec{
			ForProvider: eksv1beta1.AccessEntryParameters{
				ClusterName:  ptr.To(clusterName),
				PrincipalArn: ptr.To(roleArn),
				Region:       ptr.To(t.APPBox.Spec.Region),
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
		},
	}
	association := &eksv1beta1.AccessPolicyAssociation{
		ObjectMeta: metav1.ObjectMeta{
			Name: associationName,
		},
		Spec: eksv1beta1.AccessPolicyAssociationSpec{
			ForProvider: eksv1beta1.AccessPolicyAssociationParameters{
				AccessScope: &eksv1beta1.AccessScopeParameters{
					Type: ptr.To("cluster"),
				},
				ClusterName: ptr.To(clusterName),
				Region:      ptr.To(t.APPBox.Spec.Region),
				PrincipalArnRef: &xpv1.Reference{
					Name: entryName,
				},
				PolicyArn: ptr.To(policyArn),
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
		},
	}
	return entry, association
}

func (t *AccessEntry) authorizedSubjectsAccessEntryName(subject string) string {
	prefix := t.Config.ResourceNamePrefix + "-" + t.APPBox.Namespace + t.APPBox.Name
	name := strings.TrimPrefix(subject, "arn:aws:iam::")
	name = strings.ReplaceAll(name, ":", "-")
	name = strings.ReplaceAll(name, "/", "-")
	return namer.NewName(name, prefix, "", -1)
}
