package spec

import (
	"fmt"
	"sort"

	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	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/pingcap/errors"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/pointer"

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

const (
	x86AMIType = "AL2_x86_64"          // amazon linux2 x86_64 https://aws.amazon.com/amazon-linux-2
	armAMIType = "BOTTLEROCKET_ARM_64" // container optimized linux https://aws.amazon.com/bottlerocket/
)

type TiflowClusterAwsResource struct {
	LaunchTemplate *ec2v1alpha1.LaunchTemplate
	NodeGroup      *manualv1alpha1.NodeGroup
}

type TiflowClusterInfraResource struct {
	AWSSVC procedure.AWSService

	Network  *cloudv1.Network
	Provider gitops.Provider

	TiflowClusterName string
	TiflowSpec        *v1alpha1.TiflowSpec

	Labels map[string]string
	Taints map[string]string
}

type TiflowSpecToAWSConverter interface {
	Execute(infraResource *TiflowClusterInfraResource) (map[string]*TiflowClusterAwsResource, error)
}

func GetTiflowSpecToAWSConverter() TiflowSpecToAWSConverter {
	return defaultTiflowSpecToAWSConverterSingleton
}

var defaultTiflowSpecToAWSConverterSingleton = &defaultTiflowSpecToAWSConverter{}

type defaultTiflowSpecToAWSConverter struct{}

func (d *defaultTiflowSpecToAWSConverter) Execute(infraResource *TiflowClusterInfraResource) (map[string]*TiflowClusterAwsResource, error) {
	allocateStrategy := infraResource.TiflowSpec.AllocateStrategy
	tiflowResourceAllocator := getTiflowResourceAllocator(allocateStrategy)
	if tiflowResourceAllocator == nil {
		return nil, fmt.Errorf("allocateStrategy is %s, can not match any allocator", allocateStrategy)
	}

	tiflowNgMap, err := tiflowResourceAllocator.Execute(infraResource)
	if err != nil {
		return nil, err
	}

	awsResourceMap := map[string]*TiflowClusterAwsResource{}
	for name, tiflowNg := range tiflowNgMap {
		resource, err := d.tiflowNgToToAwsResource(tiflowNg, infraResource)
		if err != nil {
			return nil, err
		}

		awsResourceMap[name] = resource
	}

	return awsResourceMap, nil
}

func (d *defaultTiflowSpecToAWSConverter) tiflowNgToToAwsResource(
	tiflowNg *nodegroup, infraResource *TiflowClusterInfraResource) (*TiflowClusterAwsResource, error) {

	network := infraResource.Network
	ngName := namer.UniqueIdentifier(network.ObjectMeta, namer.WithPrefix(tiflowNg.name))

	ngLaunchTemplate, err := d.generateLaunchTemplate(infraResource, ngName)
	if err != nil {
		return nil, err
	}

	nodeGroup, err := d.generateNodeGroup(tiflowNg, infraResource, ngName)
	if err != nil {
		return nil, err
	}

	return &TiflowClusterAwsResource{
		LaunchTemplate: ngLaunchTemplate,
		NodeGroup:      nodeGroup,
	}, nil
}

func (d *defaultTiflowSpecToAWSConverter) generateNodeGroup(
	tiflowNg *nodegroup, infraResource *TiflowClusterInfraResource, ngName string) (*manualv1alpha1.NodeGroup, error) {
	nodeGroup := &manualv1alpha1.NodeGroup{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "eks.aws.crossplane.io/v1alpha1",
			Kind:       "NodeGroup",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: ngName,
		},
	}

	labels := infraResource.Labels
	nodeGroup.Labels = collections.MergeMaps(nodeGroup.Labels, labels)

	err := d.fillSpecToNodeGroup(nodeGroup, infraResource, tiflowNg, ngName)
	if err != nil {
		return nil, err
	}

	return nodeGroup, nil
}

func (d *defaultTiflowSpecToAWSConverter) fillSpecToNodeGroup(nodeGroup *manualv1alpha1.NodeGroup,
	infraResource *TiflowClusterInfraResource, tiflowNg *nodegroup, ngName string) error {
	network := infraResource.Network
	nodeGroup.Spec.ResourceSpec = xpv1.ResourceSpec{
		ProviderConfigReference: &xpv1.Reference{Name: namer.UniqueIdentifier(network.ObjectMeta)},
	}
	err := d.fillParametersToNodeGroup(nodeGroup, infraResource, tiflowNg, ngName)
	return err
}

func (d *defaultTiflowSpecToAWSConverter) fillParametersToNodeGroup(
	nodeGroup *manualv1alpha1.NodeGroup, infraResource *TiflowClusterInfraResource, tiflowNg *nodegroup, ngName string) error {
	network := infraResource.Network
	networkSpec, err := v1alpha1.EKSNetworkSpecForNetwork(network)
	if err != nil {
		return errors.Annotate(err, "unable to get network spec")
	}

	nodeGroup.Spec.ForProvider.ClusterName = compatnamer.LegacyBuildClusterNameByNetwork(network)
	nodeGroup.Spec.ForProvider.NodeRole = networkSpec.NodeRoleArn
	nodeGroup.Spec.ForProvider.Region = network.Spec.Region
	nodeGroup.Spec.ForProvider.Labels = infraResource.Labels
	taints := []manualv1alpha1.Taint{}
	for key, value := range infraResource.Taints {
		taints = append(taints, manualv1alpha1.Taint{
			Key:    pointer.String(key),
			Value:  pointer.String(value),
			Effect: util.ToCrossplaneTaintEffect(corev1.TaintEffectNoSchedule),
		})
	}
	sort.Slice(taints,
		func(i, j int) bool {
			return *(taints[i].Key) < *(taints[j].Key)
		})
	nodeGroup.Spec.ForProvider.Taints = taints

	nodeGroup.Spec.ForProvider.LaunchTemplate = &manualv1alpha1.LaunchTemplateSpecification{
		NameRef: &xpv1.Reference{
			Name: ngName,
		},
	}

	err = d.fillInstanceTypeToNodeGroup(nodeGroup, infraResource, tiflowNg)
	if err != nil {
		return err
	}

	if nodeGroup.Spec.ForProvider.Tags == nil {
		nodeGroup.Spec.ForProvider.Tags = make(map[string]string)
	}
	nodeGroup.Spec.ForProvider.Tags[consts.TagKeyCrossplaneVendor] = consts.TagValCrossplaneVendor
	return nil
}

func (d *defaultTiflowSpecToAWSConverter) fillInstanceTypeToNodeGroup(
	nodeGroup *manualv1alpha1.NodeGroup, infraResource *TiflowClusterInfraResource, tiflowNg *nodegroup) error {
	amiType := x86AMIType
	if infraResource.TiflowSpec.InstanceType.Arch == consts.ARM64ArchName {
		amiType = armAMIType
	}
	nodeGroup.Spec.ForProvider.AMIType = &amiType
	nodeGroup.Spec.ForProvider.InstanceTypes = []string{infraResource.TiflowSpec.InstanceType.Name}
	nodeGroup.Spec.ForProvider.ScalingConfig = &manualv1alpha1.NodeGroupScalingConfig{
		MinSize: pointer.Int32(int32(tiflowNg.instanceNumber)),
		MaxSize: pointer.Int32(int32(tiflowNg.instanceNumber)),
	}
	subnets, err := subnet.DescribeSubnets(infraResource.AWSSVC, infraResource.Network)
	if err != nil {
		return err
	}
	subnetIds := subnet.FindSubnetIdsByZones(subnets, []string{tiflowNg.zone})
	nodeGroup.Spec.ForProvider.Subnets = subnetIds
	return nil
}

func (d *defaultTiflowSpecToAWSConverter) generateLaunchTemplate(
	infraResource *TiflowClusterInfraResource, ngName string) (*ec2v1alpha1.LaunchTemplate, error) {
	ngLaunchTemplate := &ec2v1alpha1.LaunchTemplate{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "ec2.aws.crossplane.io/v1alpha1",
			Kind:       "LaunchTemplate",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:   ngName,
			Labels: infraResource.Labels,
		},
	}

	err := d.fillSpecToLaunchTemplate(ngLaunchTemplate, infraResource, ngName)
	if err != nil {
		return nil, err
	}

	return ngLaunchTemplate, nil
}

func (d *defaultTiflowSpecToAWSConverter) fillSpecToLaunchTemplate(ngLaunchTemplate *ec2v1alpha1.LaunchTemplate,
	infraResource *TiflowClusterInfraResource, ngName string) error {
	ngLaunchTemplate.Spec.ResourceSpec = xpv1.ResourceSpec{
		ProviderConfigReference: &xpv1.Reference{Name: namer.UniqueIdentifier(infraResource.Network.ObjectMeta)},
	}
	return d.fillTemplateParametersToLaunchTemplate(ngLaunchTemplate, infraResource, ngName)
}

func (d *defaultTiflowSpecToAWSConverter) fillTemplateParametersToLaunchTemplate(
	ngLaunchTemplate *ec2v1alpha1.LaunchTemplate, infraResource *TiflowClusterInfraResource, ngName string) error {

	ngLaunchTemplate.Spec.ForProvider.Region = infraResource.Network.Spec.Region
	ngLaunchTemplate.Spec.ForProvider.LaunchTemplateName = pointer.String(ngName)
	if ngLaunchTemplate.Spec.ForProvider.LaunchTemplateData == nil {
		ngLaunchTemplate.Spec.ForProvider.LaunchTemplateData = &ec2v1alpha1.RequestLaunchTemplateData{}
	}

	d.fillTagsToLaunchTemplate(ngLaunchTemplate, infraResource, ngName)
	return d.fillSystemSizeToLaunchTemplate(ngLaunchTemplate, infraResource)
}

func (d *defaultTiflowSpecToAWSConverter) fillSystemSizeToLaunchTemplate(
	ngLaunchTemplate *ec2v1alpha1.LaunchTemplate, infraResource *TiflowClusterInfraResource) error {
	size, err := util.ParseDiskSizeAsGiB(infraResource.TiflowSpec.SystemVolumeSize)
	if err != nil {
		return err
	}
	ngLaunchTemplate.Spec.ForProvider.LaunchTemplateData.BlockDeviceMappings =
		[]*ec2v1alpha1.LaunchTemplateBlockDeviceMappingRequest{
			{
				DeviceName: pointer.String("/dev/xvda"),
				EBS: &ec2v1alpha1.LaunchTemplateEBSBlockDeviceRequest{
					VolumeSize: pointer.Int64(size),
					VolumeType: pointer.String("gp3"),
					IOPS:       pointer.Int64(3000),
					Throughput: pointer.Int64(125),
				},
			},
		}
	return nil
}

func (d *defaultTiflowSpecToAWSConverter) fillTagsToLaunchTemplate(
	ngLaunchTemplate *ec2v1alpha1.LaunchTemplate, infraResource *TiflowClusterInfraResource, ngName string) {

	if ngLaunchTemplate.Spec.ForProvider.LaunchTemplateData.TagSpecifications == nil {
		ngLaunchTemplate.Spec.ForProvider.LaunchTemplateData.TagSpecifications =
			[]*ec2v1alpha1.LaunchTemplateTagSpecificationRequest{}
	}

	tags := util.AddResourceName(collections.MergeMaps(infraResource.Labels), ngName)
	d.fillOneTagToLaunchTemplate(ngLaunchTemplate, ec2types.ResourceTypeVolume, tags)

	tags = util.AddResourceName(collections.MergeMaps(infraResource.Labels), ngName)
	d.fillOneTagToLaunchTemplate(ngLaunchTemplate, ec2types.ResourceTypeInstance, tags)

	tags = util.AddResourceName(collections.MergeMaps(infraResource.Labels), ngName)
	d.fillOneTagToLaunchTemplate(ngLaunchTemplate, ec2types.ResourceTypeNetworkInterface, tags)
}

func (d *defaultTiflowSpecToAWSConverter) fillOneTagToLaunchTemplate(
	ngLaunchTemplate *ec2v1alpha1.LaunchTemplate, resourceType ec2types.ResourceType, tags map[string]string) {
	index := d.queryLaunchTemplateTagSpecificationIndex(ngLaunchTemplate, resourceType)
	if index < 0 {
		d.buildNewLaunchTemplateTagSpecification(ngLaunchTemplate, resourceType, tags)
		return
	}
	d.fillExistedLaunchTemplateTagSpecification(ngLaunchTemplate, index, tags)
}

func (d *defaultTiflowSpecToAWSConverter) queryLaunchTemplateTagSpecificationIndex(
	ngLaunchTemplate *ec2v1alpha1.LaunchTemplate, resourceType ec2types.ResourceType) int {
	for i, tag := range ngLaunchTemplate.Spec.ForProvider.LaunchTemplateData.TagSpecifications {
		if *tag.ResourceType == string(resourceType) {
			return i
		}
	}
	return -1
}

func (d *defaultTiflowSpecToAWSConverter) buildNewLaunchTemplateTagSpecification(
	ngLaunchTemplate *ec2v1alpha1.LaunchTemplate, resourceType ec2types.ResourceType, tags map[string]string) {
	ngLaunchTemplate.Spec.ForProvider.LaunchTemplateData.TagSpecifications =
		append(ngLaunchTemplate.Spec.ForProvider.LaunchTemplateData.TagSpecifications,
			&ec2v1alpha1.LaunchTemplateTagSpecificationRequest{
				ResourceType: pointer.String(string(resourceType)),
				Tags:         util.GetEC2V1AlphaTags(tags),
			})
}

func (d *defaultTiflowSpecToAWSConverter) fillExistedLaunchTemplateTagSpecification(
	ngLaunchTemplate *ec2v1alpha1.LaunchTemplate, index int, tags map[string]string) {
	specification := ngLaunchTemplate.Spec.ForProvider.LaunchTemplateData.TagSpecifications[index]

	for _, tag := range specification.Tags {
		if value, ok := tags[*tag.Key]; ok {
			tag.Value = &value
			delete(tags, *tag.Key)
		}
	}
	if len(tags) <= 0 {
		return
	}
	leftTags := util.GetEC2V1AlphaTags(tags)
	specification.Tags = append(specification.Tags, leftTags...)
}
