package eks

import (
	"context"
	"strings"

	"github.com/crossplane-contrib/provider-aws/apis/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/crossplane-runtime/pkg/resource"
	"gomodules.xyz/jsonpatch/v2"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/eks-provider/controllers/dm/task"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/scheme"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

// ProviderConfig manage lifecycle of provider config, which is used by nodepool.
type ProviderConfig struct {
	task.BaseTask
}

func NewProviderConfig(ctx *task.Context) *ProviderConfig {
	return &ProviderConfig{BaseTask: task.BaseTask{Context: ctx}}
}

func (t *ProviderConfig) Name() string {
	return "ProviderConfig"
}

func (t *ProviderConfig) Create(ctx context.Context) deploy.Result {
	dl := v1alpha1.DMJobList{}
	if err := t.Local.List(ctx, &dl, client.MatchingFields{
		"spec.clusterName": t.DMJob.Spec.ClusterName,
	}); err != nil {
		return deploy.Panic(err)
	}

	anno := map[string]string{}
	for _, i := range dl.Items {
		if !i.DeletionTimestamp.IsZero() {
			continue
		}
		anno[task.AnnoKeyPrefixUsedByDM+i.Name] = task.AnnoValInUse
	}

	assumeRoleArn := util.GetRoleARN(t.EnvProvider.GetAccount(), t.Config.FullManage.AssumeRoleName)
	config := &v1beta1.ProviderConfig{
		ObjectMeta: metav1.ObjectMeta{
			Name:        t.GetProviderConfigName(),
			Annotations: anno,
			Labels:      t.GetLables(),
		},
		Spec: v1beta1.ProviderConfigSpec{
			AssumeRoleARN: pointer.String(assumeRoleArn),
			ExternalID:    pointer.String(t.Config.FullManage.AssumeRoleExternalID),
			Credentials: v1beta1.ProviderCredentials{
				Source: xpv1.CredentialsSourceInjectedIdentity,
			},
		},
	}

	if err := k8s.Apply(ctx, t.Local, config); err != nil {
		return deploy.Panic(err)
	}

	if !config.DeletionTimestamp.IsZero() {
		return deploy.Wait(deploy.DefaultWaitRequeueAfter, "provider config for eks %s is deleting, wait for it being deleted", t.Kubernetes.Name)
	}

	return deploy.Done()
}

func (t *ProviderConfig) Delete(ctx context.Context) deploy.Result {
	config := &v1beta1.ProviderConfig{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.GetProviderConfigName(),
		},
	}

	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(config), config); err != nil {
		if !apierrors.IsNotFound(err) {
			return deploy.Error(err)
		}

		return deploy.Done()
	}

	if _, ok := config.Annotations[task.AnnoKeyPrefixUsedByDM+t.DMJob.Name]; ok {
		if err := k8s.PatchObject(ctx, t.Local, config, removeAnnoPatch(task.AnnoKeyPrefixUsedByDM+t.DMJob.Name)); err != nil {
			return deploy.Error(err)
		}
	}

	for k := range config.Annotations {
		// there is still at least one cluster, skip providerconfig deletion
		if strings.HasPrefix(k, task.AnnoKeyPrefixUsedByDM) {
			return deploy.Done()
		}
	}

	if err := t.deleteAll(ctx); err != nil {
		return deploy.Error(err)
	}

	return deploy.ErrorOrDone(k8s.DeleteObject(ctx, t.Local, config))
}

func removeAnnoPatch(k string) k8s.ObjectPatch {
	return func(obj client.Object) *jsonpatch.Operation {
		return &jsonpatch.Operation{
			Operation: "remove",
			Path:      `/metadata/annotations/` + strings.ReplaceAll(k, "/", "~1"),
		}
	}
}

func (t *ProviderConfig) deleteAll(ctx context.Context) error {
	errList := []error{}
	usl := resource.MustCreateObject(v1beta1.ProviderConfigUsageListGroupVersionKind, scheme.Scheme).(resource.ProviderConfigUsageList)
	if err := t.Local.List(ctx, usl, client.MatchingLabels{
		xpv1.LabelKeyProviderName: t.GetProviderConfigName(),
	}); err != nil {
		errList = append(errList, err)
	}

	for _, item := range usl.GetItems() {
		ref := item.GetResourceReference()
		obj := resource.MustCreateObject(ref.GroupVersionKind(), scheme.Scheme).(client.Object)
		obj.SetName(ref.Name)
		if err := t.Local.Delete(ctx, obj); err != nil {
			errList = append(errList, err)
		}
	}

	if len(errList) != 0 {
		return errors.NewAggregate(errList)
	}

	return nil
}
