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"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"

	"github.com/tidbcloud/eks-provider/controllers/autoops"
	"github.com/tidbcloud/eks-provider/controllers/network/task"
	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 {
	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
	// add node group name to master node asg tags
	// cluster-autoscaler will use this tag to find out the node group
	tags["tidbcloud.com/nodegroup-name"] = t.nodegroupName()

	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 {
		return deploy.Error(fmt.Errorf("failed apply launch template: %v", err))
	}

	ng := manualv1alpha1.NodeGroup{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "eks.aws.crossplane.io/v1alpha1",
			Kind:       "NodeGroup",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:   t.nodegroupName(),
			Labels: t.Namer.Labels(),
		},
		Spec: manualv1alpha1.NodeGroupSpec{
			ForProvider: manualv1alpha1.NodeGroupParameters{
				ClusterName:   t.Cluster.GetName(),
				Region:        t.Network.Spec.Region,
				InstanceTypes: []string{"t3.medium"},
				ScalingConfig: &manualv1alpha1.NodeGroupScalingConfig{
					MaxSize: proto.Int(10),
					MinSize: proto.Int(3),
				},
				NodeRole: t.EKSNetworkSpec.NodeRoleArn,
				Subnets:  t.EKSNetworkSpec.SubnetIds,
				Tags:     tags,
				// It will be set to the k8s node label
				// https://doc.crds.dev/github.com/crossplane/provider-aws/eks.aws.crossplane.io/NodeGroup/v1alpha1@v0.18.1#spec-forProvider-tags
				Labels: map[string]string{
					"tidbcloud.com/component": "shared",
				},
				LaunchTemplate: &manualv1alpha1.LaunchTemplateSpecification{
					NameRef: &xpv1.Reference{
						Name: t.nodegroupName(),
					},
				},
				CapacityType: t.Config.CapacityType,
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.providerConfigName()},
			},
		},
	}

	ng.Spec.ForProvider.AMIType = aws.String(t.Config.Network.AdminNodesAMIType)
	if err := t.ignoreChanges(ctx, &ng); err != nil {
		return deploy.Error(err)
	}
	if err := k8s.Apply(ctx, t.Local, &ng); err != nil {
		return deploy.Error(fmt.Errorf("failed apply node group: %v", err))
	}

	return deploy.Done()
}

func (t *Nodegroup) ignoreChanges(ctx context.Context, newNG *manualv1alpha1.NodeGroup) error {
	key := types.NamespacedName{
		Name: t.nodegroupName(),
	}
	var ng manualv1alpha1.NodeGroup
	if err := t.Local.Get(ctx, key, &ng); err != nil {
		if apierrors.IsNotFound(err) {
			return nil
		}
		return err
	}

	// ignore AMIType change
	newNG.Spec.ForProvider.AMIType = ng.Spec.ForProvider.AMIType

	return nil
}

func (t *Nodegroup) nodegroupName() string {
	return namer.UniqueIdentifier(t.Network.ObjectMeta, namer.WithPrefix(consts.TiDBCloudPrefix))
}

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

func (t *Nodegroup) PostCreate(ctx context.Context) deploy.Result {
	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()}
	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, t.Logger, t.Local, cli, &g)
			if err != nil {
				return deploy.Fatal(err)
			}
		case manualv1alpha1.NodeGroupStatusCreateFailed:
			err := autoops.DeleteNodeGroupsIfIAMThrottled(ctx, t.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))
	}
	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(),
		},
	}
	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)
}
