package nodegroup

import (
	"context"
	"fmt"
	"strings"

	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	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/log"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"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/util"
)

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/

	BottleRocketX86AMIType = "BOTTLEROCKET_x86_64"
	BottleRocketARMAMIType = ARMAMIType

	// DefaultRootVolumeSizeForBottleRocketAMIGiB define the default volume size of root volume which store system files
	// in ec2 instance created by `bottlerocket` AMI.
	// Since the root volume is only used for storing system files, 20 GB is enough for that.
	DefaultRootVolumeSizeForBottleRocketAMIGiB = 20 // 20GiB
)

// NormalizeVersion converts version string to normalized version which removes all dot, dash and underscore.
// Since the nodegroup name can not contain dots.
func NormalizeVersion(version string) string {
	var normalized string
	for _, c := range version {
		if c != '.' && c != '-' && c != '_' {
			normalized += string(c)
		}
	}
	return normalized
}

// GetAMITypeByInstanceType resolves the EKS Bottlerocket AMI type for the given
// instance type by looking up its architecture from the provided list.
// Falls back to x86_64 if the instance type is not found.
func GetAMITypeByInstanceType(instanceTypes []cloudv1.InstanceType, instanceType string) (_ string, found bool) {
	for _, it := range instanceTypes {
		if it.Name == instanceType {
			if it.Arch == consts.ARM64ArchName {
				return BottleRocketARMAMIType, true
			}
			return BottleRocketX86AMIType, true
		}
	}
	return BottleRocketX86AMIType, false
}

// TiDBClusterNodeGroupNamePrefix returns the nodegroup name for a TiDB Cluster,
// based on the given prefix, k8s version and the network CR of the cluster.
// The prefix is usually the `cloudv1.NodegroupSpec` name, and the k8sVersion is usually the cluster nodeVersion status.
//
// This name generator algorithm keeps compatibility with existing cluster CRs:
// For existing cluster CRs, the cluster nodeVersion status is empty;
// for upgraded cluster CRs, the cluster nodeVersion status is non-empty.
func TiDBClusterNodeGroupNamePrefix(prefix, k8sVersion string) string {
	if k8sVersion != "" {
		normVer := NormalizeVersion(k8sVersion)
		prefix = prefix + normVer
	}

	return prefix
}

// GetTiDBClusterNodeGroupNameIn3Layer returns the nodegroup name for a TiDB Cluster.
// For 3-layer arch, it returns a name like: `tidb-t4g-medium-{yyy}-75a4588d-us-west-2a-1-27`.
func GetTiDBClusterNodeGroupNameIn3Layer(cluster *cloudv1.Cluster, machineType, componentType, k8sVersion string, zones []string, extraGroupName string) string {
	region := cluster.Labels[consts.RegionLabel]

	var suffix strings.Builder
	suffix.WriteString(region)
	for _, zone := range zones {
		suffix.WriteString(strings.TrimPrefix(zone, region))
	}
	suffix.WriteString("-" + strings.ReplaceAll(k8sVersion, ".", "-"))
	prefix := componentType + "-" + strings.ReplaceAll(machineType, ".", "-")

	identity := cluster.ClusterScopeName()
	if extraGroupName != "" {
		identity += "-" + extraGroupName
		prefix = "ex-" + prefix
	}

	// {component}-{machineType}-{ns-cluster}-{hash}-{zones}-{version}
	// ex-{component}-{machineType}-{ns-cluster-extraGroupName}-{hash}-{zones}-{version}
	return namer.NewName(identity, prefix, suffix.String(), -1)
}

// GetExtraGroupNameFromClusterSelectorKey returns the extra group name from the cluster selector key.
// The cluster selector key is like `tidbcloud-cluster: 10394864812054402243-1978320159374311424`.
// The extra group name is the second part of the cluster selector key.
func GetExtraGroupNameFromClusterSelectorKey(key string) string {
	sl := strings.Split(key, "-")
	if len(sl) == 2 {
		return sl[1]
	}
	return ""
}

// GetBaseLaunchTemplate returns a base LaunchTemplate object,
// which can be used for both managed nodegroups and self-managed nodegroups.
//
// Parameters:
// - name: Launch Template name
// - region: AWS Region
// - providerConfig: Crossplane ProviderConfig CR name
// - labels: Kubernetes labels to add to the Launch Template object
// - tags: AWS tags to apply to the associated resources
// - blockDeviceMappings: Block device mappings for the Launch Template
func GetBaseLaunchTemplate(
	name, region, providerConfig string,
	labels, tags map[string]string,
	blockDeviceMappings []*ec2v1alpha1.LaunchTemplateBlockDeviceMappingRequest,
) *ec2v1alpha1.LaunchTemplate {
	return &ec2v1alpha1.LaunchTemplate{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: labels,
		},
		Spec: ec2v1alpha1.LaunchTemplateSpec{
			ForProvider: ec2v1alpha1.LaunchTemplateParameters{
				Region:             region,
				LaunchTemplateName: pointer.String(name),
				LaunchTemplateData: &ec2v1alpha1.RequestLaunchTemplateData{
					TagSpecifications:   GetLaunchTemplateTagSpecifications(tags),
					BlockDeviceMappings: blockDeviceMappings,
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: providerConfig},
			},
		},
	}
}

// GetBlockDeviceMappingsForBottlerocket returns the block device mappings for `bottlerocket` AMI.
// In `bottlerocket` AMI, there are two devices need to be mounted(`/dev/xvda` and `/dev/xvdb`).
// The `/dev/xvda` is root device and is only used for holding some system files, like bootloader.
// and the `/dex/xvdb` is data device and is used as persistent storage for container images, container orchestration etc,
// See https://github.com/bottlerocket-os/bottlerocket#default-volumes to get more information.
//
// In summary, we totally mount ng.MachineSpec.SystemVolumeSize + 20GiB for ec2 node to storage system files.
func GetBlockDeviceMappingsForBottlerocket(systemVolumeSize int64) []*ec2v1alpha1.LaunchTemplateBlockDeviceMappingRequest {
	return []*ec2v1alpha1.LaunchTemplateBlockDeviceMappingRequest{
		{
			DeviceName: pointer.String("/dev/xvda"),
			EBS: &ec2v1alpha1.LaunchTemplateEBSBlockDeviceRequest{
				VolumeSize: pointer.Int64(DefaultRootVolumeSizeForBottleRocketAMIGiB),
				VolumeType: pointer.String("gp3"),
				IOPS:       pointer.Int64(3000),
				Throughput: pointer.Int64(125),
			},
		},
		{
			DeviceName: pointer.String("/dev/xvdb"),
			EBS: &ec2v1alpha1.LaunchTemplateEBSBlockDeviceRequest{
				VolumeSize: pointer.Int64(systemVolumeSize),
				VolumeType: pointer.String("gp3"),
				IOPS:       pointer.Int64(3000),
				Throughput: pointer.Int64(125),
			},
		},
	}
}

func GetLaunchTemplateTagSpecifications(tags map[string]string) []*ec2v1alpha1.LaunchTemplateTagSpecificationRequest {
	return []*ec2v1alpha1.LaunchTemplateTagSpecificationRequest{
		{
			ResourceType: pointer.String(string(ec2types.ResourceTypeVolume)),
			Tags:         util.GetEC2V1AlphaTags(tags),
		},
		{
			ResourceType: pointer.String(string(ec2types.ResourceTypeInstance)),
			Tags:         util.GetEC2V1AlphaTags(tags),
		},
		{
			ResourceType: pointer.String(string(ec2types.ResourceTypeNetworkInterface)),
			Tags:         util.GetEC2V1AlphaTags(tags),
		},
	}
}

func IsComponentScalingDown(provider gitops.Provider, spec cloudv1.ClusterComponent, status cloudv1.ClusterComponentStatus) (bool, error) {
	if spec.GetInstanceType() == "" {
		return false, nil
	}
	desired, err := provider.FindInstanceType(spec.GetInstanceType())
	if err != nil {
		return false, fmt.Errorf("failed to find the desired instance type %s: %v", spec.GetInstanceType(), err)
	}

	for _, it := range status.GetInstanceTypes() {
		if it == "" {
			continue
		}
		current, err := provider.FindInstanceType(it)
		if err != nil {
			return false, fmt.Errorf("failed to find the current instance type %s: %v", it, err)
		}
		if cloudv1.IsScalingDown(current, desired) {
			return true, nil
		}
	}

	return false, nil
}

func IsComponentScalingOut(spec cloudv1.ClusterComponent, status cloudv1.ClusterComponentStatus) bool {
	return spec.GetReplicas() > int(status.GetUpdatedReplicas())
}

func GetTiDBNodeVersion(ctx context.Context, cluster *cloudv1.Cluster, providerType string) (currentVersion string, err error) {
	logger := log.FromContext(ctx)

	switch providerType {
	case consts.ProviderTypeEKS:
		status, e := cloudv1.EKSClusterStatusFromCluster(cluster)
		if e != nil {
			err = e
			break
		}
		currentVersion = status.NodeVersion
	case consts.ProviderTypeGKE:
		status, e := cloudv1.GKEClusterStatusFromCluster(cluster)
		if e != nil {
			err = e
			break
		}
		currentVersion = status.NodeVersion
	case consts.ProviderTypeAKS:
		status, e := cloudv1.AKSClusterStatusFromCluster(cluster)
		if e != nil {
			err = e
			break
		}
		currentVersion = status.NodeVersion
	}

	// If we encountered an error while retrieving provider-specific status, return it.
	if err != nil {
		return
	}

	logger.Info("nodeVersion from status", "version", currentVersion)
	// If the node version from status is empty, fall back to the spec's Kubernetes version.
	if currentVersion == "" {
		if cluster.Spec.Kubernetes.Version != "" {
			logger.Info("nodeVersion from spec", "version", cluster.Spec.Kubernetes.Version)
			currentVersion = cluster.Spec.Kubernetes.Version
		} else {
			err = fmt.Errorf("failed to determine TiDB node version: status nodeVersion and spec kubernetes.version are both empty")
		}
	}
	return
}
