package nodegroup

import (
	"context"
	"fmt"

	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"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/eks-provider/api/v1alpha1"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/util/collections"
)

func buildNGName(ngSpecName string, network *cloudv1.Network) string {
	return namer.UniqueIdentifier(network.ObjectMeta, namer.WithPrefix(ngSpecName))
}

func getAMIType(arch string, forceBottleRocket bool) string {
	if forceBottleRocket {
		amiType := BottleRocketX86AMIType
		if arch == consts.ARM64ArchName {
			amiType = BottleRocketARMAMIType
		}
		return amiType
	}

	// old rules
	amiType := X86AMIType
	if arch == consts.ARM64ArchName {
		amiType = ARMAMIType
	}
	return amiType
}

func FromNodegroupSpec(
	ng cloudv1.NodegroupSpec,
	network *cloudv1.Network,
	networkSpec *v1alpha1.EKSNetworkSpec,
	subnetIds []string,
	securityGroupIDs []*string,
	capacityType *string, // optional: "OnDemand" or "Spot"
	forceBottleRocket bool,
) (group *manualv1alpha1.NodeGroup, ngLaunchTemplate *ec2v1alpha1.LaunchTemplate, ngMut func() error, launchTemplateMut func() error) {
	ngName := buildNGName(ng.Name, network)
	tags := ng.Labels
	tags = util.AddResourceName(tags, ngName)

	amiType := getAMIType(ng.MachineSpec.MachineArch, forceBottleRocket)

	ngLaunchTemplate = &ec2v1alpha1.LaunchTemplate{
		ObjectMeta: metav1.ObjectMeta{
			Name:   ngName,
			Labels: ng.Labels,
		},
	}

	launchTemplateMut = func() error {
		if !ngLaunchTemplate.DeletionTimestamp.IsZero() {
			return fmt.Errorf("launch template %s is deleted", ngLaunchTemplate.Name)
		}
		ngLaunchTemplate.Spec = ec2v1alpha1.LaunchTemplateSpec{
			ForProvider: ec2v1alpha1.LaunchTemplateParameters{
				Region:             network.Spec.Region,
				LaunchTemplateName: aws.String(ngName),
				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),
						},
					},
					SecurityGroupIDs: securityGroupIDs,
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: namer.UniqueIdentifier(network.ObjectMeta)},
			},
		}
		size, err := util.ParseDiskSizeAsGiB(ng.MachineSpec.SystemVolumeSize)
		if err != nil {
			return err
		}
		if forceBottleRocket {
			ngLaunchTemplate.Spec.ForProvider.LaunchTemplateData.BlockDeviceMappings = GetBlockDeviceMappingsForBottlerocket(size)
		} else {
			ngLaunchTemplate.Spec.ForProvider.LaunchTemplateData.BlockDeviceMappings = append(ngLaunchTemplate.Spec.ForProvider.LaunchTemplateData.BlockDeviceMappings, &ec2v1alpha1.LaunchTemplateBlockDeviceMappingRequest{
				DeviceName: aws.String("/dev/xvda"),
				EBS: &ec2v1alpha1.LaunchTemplateEBSBlockDeviceRequest{
					VolumeSize: pointer.Int64(size),
					VolumeType: aws.String("gp3"),
					IOPS:       aws.Int64(3000),
					Throughput: aws.Int64(125),
				},
			})
		}
		return nil
	}

	group = &manualv1alpha1.NodeGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name: ngName,
		},
	}
	ngMut = func() error {
		if !group.DeletionTimestamp.IsZero() {
			return fmt.Errorf("node group %s is deleted", group.Name)
		}
		group.Labels = collections.MergeMaps(group.Labels, ng.Labels)
		group.Annotations = collections.MergeMaps(group.Annotations, ng.Annotations)
		group.Spec = manualv1alpha1.NodeGroupSpec{
			ForProvider: manualv1alpha1.NodeGroupParameters{
				AMIType:       &amiType,
				ClusterName:   util.GetAnnotation(network, consts.AnnotationKeyDataplaneName),
				Labels:        ng.Labels,
				Region:        network.Spec.Region,
				Taints:        []manualv1alpha1.Taint{},
				InstanceTypes: []string{ng.MachineSpec.MachineType},
				ScalingConfig: &manualv1alpha1.NodeGroupScalingConfig{
					MinSize: pointer.Int32Ptr(ng.Minimum),
					MaxSize: pointer.Int32Ptr(ng.Maximum),
				},
				Subnets: subnetIds,
				LaunchTemplate: &manualv1alpha1.LaunchTemplateSpecification{
					NameRef: &xpv1.Reference{
						Name: ngName,
					},
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: namer.UniqueIdentifier(network.ObjectMeta)},
			},
		}
		if ng.NodeRole != nil {
			group.Spec.ForProvider.NodeRole = *ng.NodeRole
		} else {
			group.Spec.ForProvider.NodeRole = networkSpec.NodeRoleArn
		}

		if ng.Maximum == 0 {
			// Minimum value of MaxSize 1
			// https://docs.aws.amazon.com/zh_cn/eks/latest/APIReference/API_NodegroupScalingConfig.html
			group.Spec.ForProvider.ScalingConfig.MaxSize = pointer.Int32Ptr(1)
			group.Spec.ForProvider.ScalingConfig.DesiredSize = pointer.Int32Ptr(0)
		}

		for _, taint := range ng.Taints {
			effect := util.ToCrossplaneTaintEffect(taint.Effect)
			if effect == "" {
				continue
			}
			group.Spec.ForProvider.Taints = append(group.Spec.ForProvider.Taints, manualv1alpha1.Taint{
				Effect: effect,
				Key:    pointer.String(taint.Key),
				Value:  pointer.String(taint.Value),
			})
		}
		if group.Spec.ForProvider.Tags == nil {
			group.Spec.ForProvider.Tags = make(map[string]string)
		}
		group.Spec.ForProvider.Tags[consts.TagKeyCrossplaneVendor] = consts.TagValCrossplaneVendor
		return nil
	}

	if group.CreationTimestamp.IsZero() {
		group.Spec.ForProvider.CapacityType = capacityType
	}
	return group, ngLaunchTemplate, ngMut, launchTemplateMut
}

// GetTiDBClusterNodegroup returns the nodegroup CR and launch template CR for a TiDB Cluster.
// It includes k8s node version info in the nodegroup name.
func GetTiDBClusterNodegroup(
	cluster *cloudv1.Cluster,
	ng cloudv1.NodegroupSpec,
	ngName string,
	eksName string,
	region string,
	subnetIds []string,
	securityGroupIDs []*string,
	providerConfigName string,
	nodeRoleArn string,
	capacityType *string, // optional: "OnDemand" or "Spot"
) (group *manualv1alpha1.NodeGroup, ngLaunchTemplate *ec2v1alpha1.LaunchTemplate, err error) {
	tags := ng.Labels
	tags = util.AddResourceName(tags, ngName)
	amiType := X86AMIType
	if ng.MachineSpec.MachineArch == consts.ARM64ArchName {
		amiType = ARMAMIType
	}

	size, err := util.ParseDiskSizeAsGiB(ng.MachineSpec.SystemVolumeSize)
	if err != nil {
		return nil, nil, err
	}

	ngLaunchTemplate = &ec2v1alpha1.LaunchTemplate{
		ObjectMeta: metav1.ObjectMeta{
			Name:   ngName,
			Labels: ng.Labels,
		},
		Spec: ec2v1alpha1.LaunchTemplateSpec{
			ForProvider: ec2v1alpha1.LaunchTemplateParameters{
				Region:             region,
				LaunchTemplateName: aws.String(ngName),
				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),
						},
					},
					SecurityGroupIDs: securityGroupIDs,
					BlockDeviceMappings: []*ec2v1alpha1.LaunchTemplateBlockDeviceMappingRequest{
						{
							DeviceName: aws.String("/dev/xvda"),
							EBS: &ec2v1alpha1.LaunchTemplateEBSBlockDeviceRequest{
								VolumeSize: pointer.Int64(size),
								VolumeType: aws.String("gp3"),
								IOPS:       aws.Int64(3000),
								Throughput: aws.Int64(125),
							},
						},
					},
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: providerConfigName},
			},
		},
	}
	if amiType == ARMAMIType {
		ngLaunchTemplate.Spec.ForProvider.LaunchTemplateData.BlockDeviceMappings = GetBlockDeviceMappingsForBottlerocket(size)
	}

	taints := make([]manualv1alpha1.Taint, 0, len(ng.Taints))
	for _, taint := range ng.Taints {
		effect := util.ToCrossplaneTaintEffect(taint.Effect)
		if effect == "" {
			continue
		}
		taints = append(taints, manualv1alpha1.Taint{
			Effect: effect,
			Key:    pointer.String(taint.Key),
			Value:  pointer.String(taint.Value),
		})
	}

	group = &manualv1alpha1.NodeGroup{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "eks.aws.crossplane.io/v1alpha1",
			Kind:       "NodeGroup",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:        ngName,
			Labels:      ng.Labels,
			Annotations: ng.Annotations,
		},
		Spec: manualv1alpha1.NodeGroupSpec{
			ForProvider: manualv1alpha1.NodeGroupParameters{
				AMIType: &amiType,
				// TODO: refactor it
				ClusterName:   eksName,
				Labels:        ng.Labels,
				Region:        region,
				Taints:        taints,
				InstanceTypes: []string{ng.MachineSpec.MachineType},
				NodeRole:      nodeRoleArn,
				ScalingConfig: &manualv1alpha1.NodeGroupScalingConfig{
					MinSize: pointer.Int32Ptr(ng.Minimum),
					MaxSize: pointer.Int32Ptr(ng.Maximum),
				},
				Subnets: subnetIds,
				LaunchTemplate: &manualv1alpha1.LaunchTemplateSpecification{
					NameRef: &xpv1.Reference{
						Name: ngName,
					},
				},
				Tags: map[string]string{
					consts.TagKeyCrossplaneVendor: consts.TagValCrossplaneVendor,
				},
				CapacityType: capacityType,
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: providerConfigName},
			},
		},
	}

	return group, ngLaunchTemplate, nil
}

func DeleteNodeGroupBySpecName(ctx context.Context,
	cli client.Client, ngSpecName string, network *cloudv1.Network) error {
	ngName := buildNGName(ngSpecName, network)
	ngLaunchTemplate := &ec2v1alpha1.LaunchTemplate{ObjectMeta: metav1.ObjectMeta{Name: ngName}}
	if err := cli.Delete(ctx, ngLaunchTemplate); err != nil {
		return err
	}
	group := &manualv1alpha1.NodeGroup{ObjectMeta: metav1.ObjectMeta{Name: ngName}}
	return cli.Delete(ctx, group)
}
