package eks

import (
	"context"
	"time"

	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/selection"
	"sigs.k8s.io/controller-runtime/pkg/client"

	eksv1alpha1 "github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/eks-provider/controllers/dm/task"
	"github.com/tidbcloud/eks-provider/controllers/dm/task/common"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
)

// NodePoolGC cleanup all node pool which have different spec-code as the dm-worker or dm-master.
// during scale-up-down, it's responsible to clean up previous node pool
// note: this task only works on CREATE phase, it does NOT clean up node pool we're using.
type NodePoolGC struct {
	*task.BaseTask
	taskName string
}

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

func NewNodePoolGC(ctx *task.Context) *NodePoolGC {
	return &NodePoolGC{
		BaseTask: &task.BaseTask{Context: ctx},
		taskName: "NodePoolGC",
	}
}

func (t *NodePoolGC) IsReadyForGC(ctx context.Context) bool {
	if !t.IsNodePoolReady(ctx, t.DMMasterSpec) || !t.IsNodePoolReady(ctx, t.DMWorkerSpec) {
		return false
	}

	dmCluster := &v1alpha1.DMCluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      common.GetDMClusterName(t.DMJob),
			Namespace: common.GetDMClusterNamespace(t.DMJob, t.GetRegion()),
		},
	}
	clusterCli := t.Context.EnvProvider.GetClient()
	if err := clusterCli.Get(ctx, client.ObjectKeyFromObject(dmCluster), dmCluster); err != nil {
		t.Logger.Error(err, "failed to get dmcluster", "name", dmCluster.Name)
		return false
	}
	if !t.IsDMClusterReady(dmCluster) {
		t.Logger.Info("current dmcluster is not ready", "name", dmCluster.Name)
		return false
	}

	t.Logger.Info("current dmcluster is ready", "name", dmCluster.Name)
	return true
}

func (t *NodePoolGC) IsNodePoolReady(ctx context.Context, dmSpec *eksv1alpha1.DMSpec) bool {
	nodePoolName := getNodePoolName(t.BaseTask, dmSpec)
	nodePool := manualv1alpha1.NodeGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name: nodePoolName,
		},
	}
	isReady := false
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(&nodePool), &nodePool); err == nil {
		isReady = nodePool.Status.AtProvider.Status == "ACTIVE"
	} else {
		t.Logger.Error(err, "fail to get node group "+nodePoolName)
	}

	if !isReady {
		t.Logger.Info("current nodepool not ready", "memberType", dmSpec.MemberType, "specCode", dmSpec.SpecCode)
		return false
	}

	t.Logger.Info("current nodepool is ready", "memberType", dmSpec.MemberType, "specCode", dmSpec.SpecCode)
	return true
}

func (t *NodePoolGC) Create(ctx context.Context) deploy.Result {
	selector, err := t.generateSelector()
	if err != nil {
		t.Logger.Info("generate selector error", "error", err)
		return t.Retry()
	}

	if !t.IsReadyForGC(ctx) {
		t.Logger.Info("not ready for gc, retry")
		return t.Retry()
	}

	return t.GC(ctx, selector)
}

func (t *NodePoolGC) generateSelector() (*client.MatchingLabelsSelector, error) {
	specRequirement, err := labels.NewRequirement(namer.DMMemberSpecLabelKey, selection.NotIn, []string{t.DMMasterSpec.SpecCode, t.DMWorkerSpec.SpecCode})
	if err != nil {
		return nil, err
	}
	jobNameRequirement, err := labels.NewRequirement(namer.DMJobNameLabelKey, selection.Equals, []string{t.DMJob.Name})
	if err != nil {
		return nil, err
	}

	var requirements []labels.Requirement
	for label, value := range t.GetLables() {
		req, err := labels.NewRequirement(label, selection.Equals, []string{value})
		if err != nil {
			return nil, err
		}
		requirements = append(requirements, *req)
	}

	selector := &client.MatchingLabelsSelector{
		Selector: labels.NewSelector().
			Add(*specRequirement).
			Add(*jobNameRequirement).
			Add(requirements...),
	}

	return selector, nil
}

func (t *NodePoolGC) GC(ctx context.Context, selector *client.MatchingLabelsSelector) deploy.Result {
	t.Logger.Info("gc nodepool", "selector", selector.String())

	if err := t.Local.DeleteAllOf(ctx, &manualv1alpha1.NodeGroup{}, selector); err != nil {
		t.Logger.Error(err, "label selector not ready")
		return t.Retry()
	}

	return deploy.Done()
}

func (t *NodePoolGC) PostCreate(ctx context.Context) deploy.Result {
	if !t.IsReadyForGC(ctx) {
		return t.Retry()
	}

	selector, err := t.generateSelector()
	if err != nil {
		return t.Retry()
	}

	return t.CheckGCResult(ctx, selector)
}

func (t *NodePoolGC) CheckGCResult(ctx context.Context, selector *client.MatchingLabelsSelector) deploy.Result {
	nodePools := &manualv1alpha1.NodeGroupList{}
	ops := k8s.ListOpts[*manualv1alpha1.NodeGroup]{
		ObjectList: nodePools,
		Unpack:     func() []*manualv1alpha1.NodeGroup { return k8s.MapPtr(nodePools.Items) },
	}

	list, err := ops.ListFilterBy(ctx, t.Local,
		func(_ *manualv1alpha1.NodeGroup) bool { return true }, selector)
	if client.IgnoreNotFound(err) != nil {
		t.Logger.Error(err, "query nodepool occur error!")
		return t.Retry()
	}

	if len(list) != 0 {
		t.Logger.Info("nodepool not deleted, remain!", "size", len(list))
		return t.Retry()
	}

	return deploy.Done()
}

func (t *NodePoolGC) Delete(ctx context.Context) deploy.Result {
	selector, err := t.generateSelector()
	if err != nil {
		t.Logger.Info("generate selector error", "error", err)
		return t.Retry()
	}

	return t.GC(ctx, selector)
}

func (t *NodePoolGC) PostDelete(ctx context.Context) deploy.Result {
	selector, err := t.generateSelector()
	if err != nil {
		return t.Retry()
	}

	return t.CheckGCResult(ctx, selector)
}

func (t *NodePoolGC) Retry() deploy.Result {
	return deploy.Error(k8s.RequeueOnlyError).WithRequeuAfter(time.Second * 10)
}
