package eks

import (
	"context"
	"fmt"
	"time"

	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	"github.com/aws/aws-sdk-go/aws"
	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/gogo/protobuf/proto"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	ctrl "sigs.k8s.io/controller-runtime"

	"github.com/tidbcloud/eks-provider/controllers/autoops"
	"github.com/tidbcloud/eks-provider/controllers/dumpling/task"
	dutils "github.com/tidbcloud/eks-provider/controllers/dumpling/task/utils"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

type Nodegroup struct {
	*task.BaseTask
}

func NewNodegroup(ctx *task.Context) *Nodegroup {
	return &Nodegroup{BaseTask: &task.BaseTask{Context: ctx}}
}

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

func (t *Nodegroup) Create(ctx context.Context) deploy.Result {
	switch phase := t.Dumpling.Status.Phase; phase {
	case "":
		t.BeginAt = util.Pointer(time.Now())
		// If node role is ready, create the nodegroup CR first,
		// though it can not be reconciled when eks is not ready.
		if t.EKSNetworkSpec.NodeRoleArn == "" {
			return deploy.Fatal(fmt.Errorf("node role is not ready"))
		}

		tags := cloudv1.TagsFromLabels(t.Network.Labels)
		tags = util.AddResourceName(tags, t.nodegroupName())
		tags[consts.TagKeyCrossplaneVendor] = consts.TagValCrossplaneVendor
		ngLaunchTemplate := ec2v1alpha1.LaunchTemplate{
			TypeMeta: metav1.TypeMeta{
				APIVersion: "ec2.aws.crossplane.io/v1alpha1",
				Kind:       "LaunchTemplate",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name:   t.nodegroupName(),
				Labels: t.Namer.Labels(),
			},
			Spec: ec2v1alpha1.LaunchTemplateSpec{
				ForProvider: ec2v1alpha1.LaunchTemplateParameters{
					Region:             t.Network.Spec.Region,
					LaunchTemplateName: aws.String(t.nodegroupName()),
					LaunchTemplateData: &ec2v1alpha1.RequestLaunchTemplateData{
						TagSpecifications: []*ec2v1alpha1.LaunchTemplateTagSpecificationRequest{
							{
								ResourceType: aws.String(string(ec2types.ResourceTypeVolume)),
								Tags:         util.GetEC2V1AlphaTags(tags),
							},
							{
								ResourceType: aws.String(string(ec2types.ResourceTypeInstance)),
								Tags:         util.GetEC2V1AlphaTags(tags),
							},
							{
								ResourceType: aws.String(string(ec2types.ResourceTypeNetworkInterface)),
								Tags:         util.GetEC2V1AlphaTags(tags),
							},
						},
					},
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: t.providerConfigName()},
				},
			},
		}
		if err := k8s.Apply(ctx, t.Local, &ngLaunchTemplate); err != nil {
			t.Dumpling.Status.Phase = cloudv1.DumplingPhaseFailed
			t.Dumpling.Status.Message = fmt.Sprintf("failed apply launch template: %v", err)
			return deploy.Error(fmt.Errorf("failed apply launch template: %v", err))
		}

		capacityType := "ON_DEMAND"
		KeyDedicated := "dedicated"
		TaintEffectNoSchedule := "NO_SCHEDULE"
		taintValue := fmt.Sprintf("%s-%s", string(cloudv1.ComponentBackup), t.Dumpling.Name) //todo

		ng := manualv1alpha1.NodeGroup{
			TypeMeta: metav1.TypeMeta{
				APIVersion: "eks.aws.crossplane.io/v1alpha1",
				Kind:       "NodeGroup",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name:   t.nodegroupName() + "-" + t.Dumpling.Name,
				Labels: t.Namer.Labels(),
			},
			Spec: manualv1alpha1.NodeGroupSpec{
				ForProvider: manualv1alpha1.NodeGroupParameters{
					ClusterName:   util.GetAnnotation(t.Network, consts.AnnotationKeyDataplaneName),
					Region:        t.Network.Spec.Region,
					InstanceTypes: []string{"c5.2xlarge"},
					ScalingConfig: &manualv1alpha1.NodeGroupScalingConfig{
						MaxSize: proto.Int(1),
						MinSize: proto.Int(1),
					},
					NodeRole: t.EKSNetworkSpec.NodeRoleArn,
					Subnets:  t.EKSNetworkSpec.SubnetIds,
					Tags:     tags,
					Labels: map[string]string{
						dutils.DumplingNodeAffinityKey(t.Dumpling): dutils.DumplingNodeAffinityValue(t.Dumpling),
					},
					LaunchTemplate: &manualv1alpha1.LaunchTemplateSpecification{
						NameRef: &xpv1.Reference{
							Name: t.nodegroupName(),
						},
					},
					CapacityType: &capacityType,
					Taints: []manualv1alpha1.Taint{{
						Key:    &KeyDedicated,
						Value:  &taintValue,
						Effect: TaintEffectNoSchedule,
					},
					},
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: t.providerConfigName()},
				},
			},
		}

		if err := k8s.Apply(ctx, t.Local, &ng); err != nil {
			t.Dumpling.Status.Phase = cloudv1.DumplingPhaseFailed
			t.Dumpling.Status.Message = fmt.Sprintf("failed apply node group: %v", err)
			return deploy.Error(fmt.Errorf("failed apply node group: %v", err))
		}

		t.Dumpling.Status.Phase = cloudv1.DumplingPhaseCreatingNodeGroup
	}
	return deploy.Done()
}

func (t *Nodegroup) nodegroupName() string {
	return "tidbcloud-node-" + t.Dumpling.Namespace + "-" + t.Dumpling.Name
}

func (t *Nodegroup) providerConfigName() string {
	return namer.UniqueIdentifier(t.Network.ObjectMeta)
}

func (t *Nodegroup) PostCreate(ctx context.Context) deploy.Result {
	switch phase := t.Dumpling.Status.Phase; phase {
	case cloudv1.DumplingPhaseCreatingNodeGroup:
		logger := ctrl.Log.WithName("controllers").WithName("Dumpling")
		cli, err := t.Remote.GetClient(ctx)
		if err != nil {
			return deploy.Panic(fmt.Errorf("failed get remote client: %v", err))
		}

		var g manualv1alpha1.NodeGroup
		key := types.NamespacedName{Name: t.nodegroupName() + "-" + t.Dumpling.Name}
		if err := t.Local.Get(ctx, key, &g); err != nil {
			return deploy.Fatal(err)
		}
		status := g.Status.AtProvider.Status
		if status != manualv1alpha1.NodeGroupStatusActive {
			switch status {
			case manualv1alpha1.NodeGroupStatusCreating:
				err := autoops.RemoveNodeGroupPendingAnnotationIfReady(ctx, logger, t.Local, cli, &g)
				if err != nil {
					return deploy.Fatal(err)
				}
			case manualv1alpha1.NodeGroupStatusCreateFailed:
				err := autoops.DeleteNodeGroupsIfIAMThrottled(ctx, logger, t.Local, &g)
				if err != nil {
					return deploy.Fatal(err)
				}
			}
			return deploy.Error(fmt.Errorf("non-active nodegroup %s has the state %q", g.Name, status))
		} else {
			t.Dumpling.Status.Phase = cloudv1.DumplingPhaseCreatedNodeGroup
		}
	}
	return deploy.Done()
}

func (t *Nodegroup) Delete(ctx context.Context) deploy.Result {
	// Delete control plane
	if err := t.deleteControlPlane(ctx); err != nil {
		return deploy.Panic(err)
	}

	return deploy.Done()
}

func (t *Nodegroup) PostDelete(ctx context.Context) deploy.Result {
	var g manualv1alpha1.NodeGroup
	key := types.NamespacedName{Name: t.nodegroupName()}
	return deploy.ErrorOrDone(k8s.GetDeletedObject(ctx, t.Local, key, &g))
}

func (t *Nodegroup) deleteControlPlane(ctx context.Context) error {
	g := manualv1alpha1.NodeGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.nodegroupName() + "-" + t.Dumpling.Name,
		},
	}
	if err := k8s.DeleteObject(ctx, t.Local, &g); err != nil {
		return err
	}
	if err := k8s.PatchObject(ctx, t.Local, &g, k8s.RemoveCrossplaneExternalCreatePending, k8s.DeletionTrigger); err != nil {
		return err
	}

	ngLaunchTemplate := ec2v1alpha1.LaunchTemplate{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "ec2.aws.crossplane.io/v1alpha1",
			Kind:       "LaunchTemplate",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: t.nodegroupName(),
		},
	}
	if err := k8s.DeleteObject(ctx, t.Local, &ngLaunchTemplate); err != nil {
		return err
	}

	return k8s.PatchObject(ctx, t.Local, &ngLaunchTemplate, k8s.RemoveCrossplaneExternalCreatePending, k8s.DeletionTrigger)
}
