package common

import (
	"context"
	"strings"
	"time"

	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	manualv1alpha1 "github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	gkev1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	"github.com/pingcap/errors"
	azurev1beta1 "github.com/upbound/provider-azure/apis/containerservice/v1beta1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	sharednodepoolgc "github.com/tidbcloud/infra-provider/pkg/nodepoolgc"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/appbox/task"
)

const nodePoolGCRequeueInterval = 10 * time.Second

// NodePoolGC deletes workload nodepools that belong to an APPBox before the
// APPBox's Kubernetes cluster is deleted.
type NodePoolGC struct {
	*task.BaseTask
}

func NewNodePoolGC(c *task.Context) *NodePoolGC {
	return &NodePoolGC{BaseTask: &task.BaseTask{Context: c}}
}

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

func (t *NodePoolGC) Delete(ctx context.Context) deploy.Result {
	pools, err := t.listNodePools(ctx)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(nodePoolGCRequeueInterval)
	}

	logger := log.FromContext(ctx)
	for _, pool := range pools {
		for _, companion := range pool.companions {
			logger.Info("deleting nodepool companion", "name", companion.GetName(), "kind", companion.GetObjectKind().GroupVersionKind().Kind)
			if err := client.IgnoreNotFound(t.Local.Delete(ctx, companion)); err != nil {
				return deploy.Panic(err).WithRequeuAfter(nodePoolGCRequeueInterval)
			}
		}
		logger.Info("deleting APPBox nodepool", "name", pool.primary.GetName(), "kind", pool.primary.GetObjectKind().GroupVersionKind().Kind)
		if err := client.IgnoreNotFound(t.Local.Delete(ctx, pool.primary)); err != nil {
			return deploy.Panic(err).WithRequeuAfter(nodePoolGCRequeueInterval)
		}
	}
	return deploy.Done()
}

func (t *NodePoolGC) PostDelete(ctx context.Context) deploy.Result {
	pools, err := t.listNodePools(ctx)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(nodePoolGCRequeueInterval)
	}
	if len(pools) > 0 {
		return deploy.Panic(errors.Errorf("waiting for %d APPBox nodepool(s) to be deleted", len(pools))).WithRequeuAfter(nodePoolGCRequeueInterval)
	}
	return deploy.Done()
}

type appBoxNodePool struct {
	primary    client.Object
	companions []client.Object
}

func (t *NodePoolGC) listNodePools(ctx context.Context) ([]appBoxNodePool, error) {
	clusterName := t.APPBox.Status.KubernetesClusterName
	if clusterName == "" {
		return nil, nil
	}

	switch t.Config.Provider {
	case consts.ProviderTypeEKS:
		var list manualv1alpha1.NodeGroupList
		if err := sharednodepoolgc.ListBRNodePools(ctx, t.Local, &list); err != nil {
			return nil, err
		}
		pools := make([]appBoxNodePool, 0, len(list.Items))
		for i := range list.Items {
			nodeGroup := &list.Items[i]
			if nodeGroup.Spec.ForProvider.ClusterName != clusterName {
				continue
			}
			pool := appBoxNodePool{primary: nodeGroup}
			launchTemplate := &ec2v1alpha1.LaunchTemplate{}
			err := t.Local.Get(ctx, client.ObjectKeyFromObject(nodeGroup), launchTemplate)
			if err == nil {
				pool.companions = append(pool.companions, launchTemplate)
			} else if !apierrors.IsNotFound(err) {
				return nil, err
			}
			pools = append(pools, pool)
		}
		return pools, nil

	case consts.ProviderTypeGKE:
		var list gkev1beta1.NodePoolList
		if err := sharednodepoolgc.ListBRNodePools(ctx, t.Local, &list); err != nil {
			return nil, err
		}
		pools := make([]appBoxNodePool, 0, len(list.Items))
		for i := range list.Items {
			pool := &list.Items[i]
			if resourceName(pool.Spec.ForProvider.Cluster) == clusterName {
				pools = append(pools, appBoxNodePool{primary: pool})
			}
		}
		return pools, nil

	case consts.ProviderTypeAKS:
		var list azurev1beta1.KubernetesClusterNodePoolList
		if err := sharednodepoolgc.ListBRNodePools(ctx, t.Local, &list); err != nil {
			return nil, err
		}
		pools := make([]appBoxNodePool, 0, len(list.Items))
		for i := range list.Items {
			pool := &list.Items[i]
			if pool.Spec.ForProvider.KubernetesClusterID != nil && resourceName(*pool.Spec.ForProvider.KubernetesClusterID) == clusterName {
				pools = append(pools, appBoxNodePool{primary: pool})
			}
		}
		return pools, nil

	default:
		return nil, errors.Errorf("unsupported nodepool GC provider %q", t.Config.Provider)
	}
}

func resourceName(resourceID string) string {
	parts := strings.Split(strings.Trim(resourceID, "/"), "/")
	if len(parts) == 0 {
		return ""
	}
	return parts[len(parts)-1]
}
