package gke

import (
	"context"
	"fmt"
	"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/cluster/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/gcp"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/scheme"
)

const (
	annoKeyPrefixUsedByCluster = "alpha.usedby.cluster.cloud.pingcap.com/"
	annoValInUse               = "in-use"
)

type ProviderConfig struct {
	task.BaseTask
	gcpCli gcp.Client
}

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

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

func (t *ProviderConfig) Create(ctx context.Context) deploy.Result {
	// TODO: also do it for migrated cluster
	if t.IsLegacy() {
		// legacy network is available, use the ProviderConfig created by Network Controller.
		return deploy.Done()
	}

	cl := cloudv1.ClusterList{}
	if err := t.Local.List(ctx, &cl, client.MatchingFields{
		"spec.kubernetes.name": t.Cluster.Spec.Kubernetes.Name,
	}); err != nil {
		return deploy.Error(err)
	}

	anno := map[string]string{}
	for _, c := range cl.Items {
		if !c.DeletionTimestamp.IsZero() {
			continue
		}

		if c.Spec.NetworkName != "" {
			continue
		}

		anno[annoKeyPrefixUsedByCluster+c.Name] = annoValInUse
	}

	config := &v1beta1.ProviderConfig{
		ObjectMeta: metav1.ObjectMeta{
			Name:        t.GetGCPProviderConfigName(), // per-GKE
			Annotations: anno,
			Labels: map[string]string{
				"tidbcloud-namespace": t.Cluster.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.Panic(fmt.Errorf("provider config `%s` for gke `%s` is deleting, wait for it being deleted", config.Name, t.Cluster.Spec.Kubernetes.Name))
	}

	return deploy.Done()
}

func (t *ProviderConfig) Delete(ctx context.Context) deploy.Result {
	// TODO: also do it for migrated cluster
	if t.IsLegacy() {
		return deploy.Done()
	}

	config := &v1beta1.ProviderConfig{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.GetGCPProviderConfigName(),
		},
	}

	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[annoKeyPrefixUsedByCluster+t.Cluster.Name]; ok {
		if err := k8s.PatchObject(ctx, t.Local, config, removeAnnoPatch(annoKeyPrefixUsedByCluster+t.Cluster.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, annoKeyPrefixUsedByCluster) {
			return deploy.Done()
		}
	}

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

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

func (t *ProviderConfig) revokeArtifactRegistryPolicy(ctx context.Context) error {

	if !t.IsRegionalManaged() {
		return nil
	}

	saEmail := t.GetGCPNodePoolServiceAccountEmail()
	if saEmail == "" {
		return nil
	}

	return t.gcpCli.ResourceManager().DelProjectPolicyBinding(
		ctx,
		t.Config.FullManage.GCPProjectID,
		roleArtifactRegistryReader,
		fmt.Sprintf("serviceAccount:%s", saEmail),
	)
}

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.GetGCPProviderConfigName(),
	}); 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
}
