package eks

import (
	"context"
	"fmt"

	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	"k8s.io/apimachinery/pkg/labels"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/eks-provider/controllers/tiflow/spec"
	"github.com/tidbcloud/eks-provider/controllers/tiflow/task"
	"github.com/tidbcloud/eks-provider/controllers/tiflow/task/cluster"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
)

// NodeGroup manage lifecycle of node group.
type NodeGroup struct {
	task.BaseTask
	*cluster.BaseContext

	tidbClusterName string
	*v1alpha1.Conditions
}

func NewNodeGroup(ctx *cluster.BaseContext, tidbClusterName string, objCond *v1alpha1.Conditions) *NodeGroup {
	return &NodeGroup{
		BaseContext:     ctx,
		tidbClusterName: tidbClusterName,
		Conditions:      objCond,
	}
}

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

func (t *NodeGroup) Create(ctx context.Context) deploy.Result {
	infraResource := &spec.TiflowClusterInfraResource{
		AWSSVC:   t.BaseContext.AWSSVC,
		Network:  t.BaseContext.Network,
		Provider: t.BaseContext.Provider,

		TiflowClusterName: t.BaseContext.Namer.GetTiflowClusterName(),
		TiflowSpec:        t.BaseContext.TiflowSpec,

		Labels: cluster.LabelsForNodeGroup(t.BaseContext, t.tidbClusterName),
		Taints: t.BaseContext.Namer.Labels(),
	}

	awsResourceMap, err := spec.GetTiflowSpecToAWSConverter().Execute(infraResource)
	if err != nil {
		return deploy.Error(err)
	}

	for _, awsResource := range awsResourceMap {
		if err := k8s.Apply(ctx, t.BaseContext.Local, awsResource.LaunchTemplate); err != nil {
			return deploy.Error(err)
		}

		if err := k8s.Apply(ctx, t.BaseContext.Local, awsResource.NodeGroup); err != nil {
			return deploy.Error(err)
		}
	}
	return deploy.Done()
}

func (t *NodeGroup) PostCreate(ctx context.Context) deploy.Result {
	isReady, err := t.checkNgListIsReady(ctx)
	if err != nil {
		return deploy.Error(err)
	}

	cluster.UpdateCondition(t.Conditions, v1alpha1.TiflowClusterNodeGroupReady, isReady)

	if isReady {
		return deploy.Done()
	} else {
		return deploy.Error(fmt.Errorf("wait nodegroup ready"))
	}
}

func (t *NodeGroup) checkNgListIsReady(ctx context.Context) (bool, error) {
	ngList := &manualv1alpha1.NodeGroupList{}
	err := t.BaseContext.Local.List(ctx, ngList, &client.MatchingLabelsSelector{
		Selector: labels.Set(cluster.LabelsForNodeGroup(t.BaseContext, t.tidbClusterName)).AsSelector(),
	})
	if err != nil {
		return false, err
	}

	for _, item := range ngList.Items {
		if item.Status.AtProvider.Status != manualv1alpha1.NodeGroupStatusActive {
			return false, nil
		}
	}
	return true, nil
}

func (t *NodeGroup) Delete(ctx context.Context) deploy.Result {
	selector := cluster.ComponentNodePoolSelector(t.BaseContext)
	if err := t.BaseContext.Local.DeleteAllOf(ctx, &manualv1alpha1.NodeGroup{}, selector); err != nil {
		return deploy.Error(err)
	}

	// following patch is to fix issue 'cannot determine creation result - remove the crossplane.io/external-create-pending
	// annotation if it is safe to proceed' which will stuck the deletion
	ngs := &manualv1alpha1.NodeGroupList{}
	ops := k8s.ListOpts[*manualv1alpha1.NodeGroup]{
		ObjectList: ngs,
		Unpack:     func() []*manualv1alpha1.NodeGroup { return k8s.MapPtr(ngs.Items) },
	}
	if err := ops.PatchFilterBy(
		ctx,
		t.BaseContext.Local,
		func(_ *manualv1alpha1.NodeGroup) bool { return true },
		[]k8s.ObjectPatch{k8s.RemoveCrossplaneExternalCreatePending, k8s.DeletionTrigger},
		selector,
	); err != nil {
		return deploy.Error(err)
	}

	if err := t.BaseContext.Local.DeleteAllOf(ctx, &ec2v1alpha1.LaunchTemplate{},
		selector); err != nil {
		return deploy.Error(err)
	}
	ngLaunchTemplates := &ec2v1alpha1.LaunchTemplateList{}
	ngLaunchTemplateOps := k8s.ListOpts[*ec2v1alpha1.LaunchTemplate]{
		ObjectList: ngLaunchTemplates,
		Unpack:     func() []*ec2v1alpha1.LaunchTemplate { return k8s.MapPtr(ngLaunchTemplates.Items) },
	}
	if err := ngLaunchTemplateOps.PatchFilterBy(
		ctx,
		t.BaseContext.Local,
		func(_ *ec2v1alpha1.LaunchTemplate) bool { return true },
		[]k8s.ObjectPatch{k8s.RemoveCrossplaneExternalCreatePending, k8s.DeletionTrigger},
		selector,
	); err != nil {
		return deploy.Error(err)
	}

	return deploy.Done()
}

func (t *NodeGroup) PostDelete(ctx context.Context) deploy.Result {
	selector := cluster.ComponentNodePoolSelector(t.BaseContext)
	ngs := &manualv1alpha1.NodeGroupList{}
	ops := k8s.ListOpts[*manualv1alpha1.NodeGroup]{
		ObjectList: ngs,
		Unpack:     func() []*manualv1alpha1.NodeGroup { return k8s.MapPtr(ngs.Items) },
	}
	list, err := ops.ListFilterBy(ctx, t.BaseContext.Local, func(_ *manualv1alpha1.NodeGroup) bool {
		return true
	}, selector)
	if client.IgnoreNotFound(err) != nil {
		return deploy.Error(err)
	}
	if len(list) != 0 {
		return deploy.Error(fmt.Errorf("nodegroup not deleted, remain %d", len(list)))
	}

	ngLaunchTemplates := &ec2v1alpha1.LaunchTemplateList{}
	ngLaunchTemplateOps := k8s.ListOpts[*ec2v1alpha1.LaunchTemplate]{
		ObjectList: ngLaunchTemplates,
		Unpack:     func() []*ec2v1alpha1.LaunchTemplate { return k8s.MapPtr(ngLaunchTemplates.Items) },
	}
	ngLaunchTemplateList, err := ngLaunchTemplateOps.ListFilterBy(ctx, t.BaseContext.Local, func(_ *ec2v1alpha1.LaunchTemplate) bool {
		return true
	}, selector)
	if client.IgnoreNotFound(err) != nil {
		return deploy.Error(err)
	}
	if len(ngLaunchTemplateList) != 0 {
		return deploy.Error(fmt.Errorf("nodegroup launchtemplate not deleted, remain %d", len(ngLaunchTemplateList)))
	}
	return deploy.Done()
}
