package gke

import (
	"context"
	"strings"

	"github.com/crossplane-contrib/provider-gcp/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"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/eks-provider/controllers/imports/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/scheme"
)

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 {

	il := cloudv1.ImportList{}
	if err := t.Local.List(ctx, &il, client.MatchingFields{
		"spec.clusterName": t.Import.Spec.ClusterName,
	}); err != nil {
		return deploy.Error(err)
	}
	anno := map[string]string{}
	for _, i := range il.Items {
		if !i.DeletionTimestamp.IsZero() {
			continue
		}
		anno[task.AnnoKeyPrefixUsedByImport+i.Name] = task.AnnoValInUse
	}
	config := &v1beta1.ProviderConfig{
		ObjectMeta: metav1.ObjectMeta{
			Name:        t.GetGKEProviderConfigName(),
			Annotations: anno,
			Labels: map[string]string{
				"tidbcloud-namespace": t.Import.Namespace,
			},
		},
		Spec: v1beta1.ProviderConfigSpec{
			ProjectID: t.EnvProvider.GetAccount(),
			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 gke %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.GetGKEProviderConfigName(),
		},
	}

	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.AnnoKeyPrefixUsedByImport+t.Import.Name]; ok {
		if err := k8s.PatchObject(ctx, t.Local, config, removeAnnoPatch(task.AnnoKeyPrefixUsedByImport+t.Import.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.AnnoKeyPrefixUsedByImport) {
			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.GetGKEProviderConfigName(),
	}); 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
}
