package nodepool

import (
	"context"
	"crypto/md5"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"regexp"
	"strings"

	gkev1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	opv1alpha1 "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/gcp"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/nodegroup"
)

var (
	instanceGroupURLPattern = regexp.MustCompile(`^https://www.googleapis.com/compute/(\S+)/projects/(\S+)/zones/(\S+)/instanceGroupManagers/(\S+)$`)
)

const (
	// GKE node pool name has limit of 40, see https://github.com/tidbcloud/infra-provider/issues/793
	nodePoolNameLengthLimit                 = namer.GKENameLimit
	gkeHeterogeneousNodePoolScopeHashLength = 20
)

func TiDBNodePoolNameOptions(version, machineType string) []namer.IdentifierOptions {
	return []namer.IdentifierOptions{
		// Can not start with numbers, and can not contain dots.
		namer.WithPrefix(nodegroup.NormalizeVersion(version)),
		namer.WithPrefix(machineType),
		namer.WithLimit(nodePoolNameLengthLimit),
	}
}

func LegacyTiDBNodePoolName(network *cloudv1.Network, version, machineType string) string {
	return namer.UniqueIdentifier(
		network.ObjectMeta,
		TiDBNodePoolNameOptions(version, machineType)...,
	)
}

// OverlayNodeSelectorWithK8sVersionForComponents sets the K8sVersionLabelKey node selector to
// k8sVersion on the specified component types within the TidbCluster CR. Components whose spec
// is nil are skipped silently. Unknown component types are ignored.
// This is a no-op when k8sVersion is empty.
func OverlayNodeSelectorWithK8sVersionForComponents(tc *opv1alpha1.TidbCluster, k8sVersion string, components ...cloudv1.ComponentType) {
	if k8sVersion == "" {
		return
	}
	for _, comp := range components {
		var spec *opv1alpha1.ComponentSpec
		switch comp {
		case cloudv1.ComponentPD:
			if tc.Spec.PD != nil {
				spec = &tc.Spec.PD.ComponentSpec
			}
		case cloudv1.ComponentTiDB:
			if tc.Spec.TiDB != nil {
				spec = &tc.Spec.TiDB.ComponentSpec
			}
		case cloudv1.ComponentTiKV:
			if tc.Spec.TiKV != nil {
				spec = &tc.Spec.TiKV.ComponentSpec
			}
		case cloudv1.ComponentTiProxy:
			if tc.Spec.TiProxy != nil {
				spec = &tc.Spec.TiProxy.ComponentSpec
			}
		case cloudv1.ComponentTiFlash:
			if tc.Spec.TiFlash != nil {
				spec = &tc.Spec.TiFlash.ComponentSpec
			}
		}
		if spec != nil {
			if spec.NodeSelector == nil {
				spec.NodeSelector = make(map[string]string)
			}
			spec.NodeSelector[consts.K8sVersionLabelKey] = k8sVersion
		}
	}
}

// OverlayNodeSelectorWithK8sVersion sets a node selector that matches the provided Kubernetes version
// for each non-nil component (PD, TiDB, TiKV, TiProxy, TiFlash) in the TidbCluster CR.
// If no Kubernetes version is provided, this function does not change the TidbCluster CR.
func OverlayNodeSelectorWithK8sVersion(tc *opv1alpha1.TidbCluster, k8sVersion string) {
	OverlayNodeSelectorWithK8sVersionForComponents(tc, k8sVersion,
		cloudv1.ComponentPD, cloudv1.ComponentTiDB, cloudv1.ComponentTiKV,
		cloudv1.ComponentTiProxy, cloudv1.ComponentTiFlash,
	)
}

// OverlayNodeSelectorWithK8sVersionForTiProxy sets a node selector that matches the provided Kubernetes version
// for the TiProxy component in the TidbCluster CR.
// If no Kubernetes version is provided, this function does not change the TidbCluster CR.
func OverlayNodeSelectorWithK8sVersionForTiProxy(tc *opv1alpha1.TidbCluster, k8sVersion string) {
	OverlayNodeSelectorWithK8sVersionForComponents(tc, k8sVersion, cloudv1.ComponentTiProxy)
}

func IsComponentScalingUp(provider config.CloudConfigProvider, 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.IsScalingUp(current, desired) {
			return true, nil
		}
	}

	return false, nil
}

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

// GetNodeCount returns the number of nodes in a node pool.
// Since we can not get the node count directly from the node pool CR,
// we need to get the instance group size for each node pool via GCP API.
func GetNodeCount(ctx context.Context, gcpClient gcp.Client, instanceGroupUrls []string) (int64, error) {
	var total int64

	for _, url := range instanceGroupUrls {
		parts := instanceGroupURLPattern.FindStringSubmatch(url)
		if len(parts) != 5 {
			return 0, fmt.Errorf("parse instance group url error, find %d submatch strings", len(parts))
		}
		project := parts[2]
		zone := parts[3]
		instanceGroupName := parts[4]
		size, err := gcpClient.GetInstanceGroupSize(ctx, project, zone, instanceGroupName)
		if err != nil {
			return total, fmt.Errorf("failed to get instance group size for %s: %w", instanceGroupName, err)
		}

		total += size
	}
	return total, nil
}

// GetManagedInstanceCount returns the number of managed instances in GKE node pool
// instance group managers. Use this for heterogeneous idle GC, where the CR
// reports instanceGroupManagers URLs and deletion should check the manager view.
func GetManagedInstanceCount(ctx context.Context, gcpClient gcp.Client, instanceGroupManagerUrls []string) (int64, error) {
	var total int64

	for _, url := range instanceGroupManagerUrls {
		parts := instanceGroupURLPattern.FindStringSubmatch(url)
		if len(parts) != 5 {
			return 0, fmt.Errorf("parse instance group manager url error, find %d submatch strings", len(parts))
		}
		project := parts[2]
		zone := parts[3]
		instanceGroupManagerName := parts[4]
		size, err := gcpClient.GetInstanceGroupManagerManagedInstanceCount(ctx, project, zone, instanceGroupManagerName)
		if err != nil {
			return total, fmt.Errorf("failed to get managed instance count for instance group manager %s: %w", instanceGroupManagerName, err)
		}

		total += size
	}
	return total, nil
}

// Now NodePool is shared for multiple clusters
// NOTE: a buggy implementation uses the tidb cluster's name to generate nodepool name
// NOTE: some nodepools maybe untracked in prod, we can recycle these nodepools when upgrade k8s clusters
// NOTE: untracked nodepool may need to be recycled manually if all clusters have been deleted.
func GetGKENodePoolName(cluster *cloudv1.Cluster, machineType, k8sVersion string) string {
	return namer.GKENodePoolName(cluster, machineType, k8sVersion)
}

var gkeKubernetesClusterScopeLabelKeys = []string{
	cloudv1.TenantTagKey,
	cloudv1.ProjectTagKey,
}

func GKEKubernetesClusterScopeLabels(cluster *cloudv1.Cluster) (map[string]string, []string) {
	if cluster == nil || cluster.GetProviderType() != cloudv1.ProviderTypeGKE {
		return nil, nil
	}

	labels := cluster.GetLabels()
	scopeLabels := make(map[string]string, len(gkeKubernetesClusterScopeLabelKeys))
	missing := make([]string, 0, len(gkeKubernetesClusterScopeLabelKeys))
	for _, key := range gkeKubernetesClusterScopeLabelKeys {
		value := strings.TrimSpace(labels[key])
		if value == "" {
			missing = append(missing, key)
			continue
		}
		scopeLabels[key] = value
	}
	if len(missing) > 0 {
		return nil, missing
	}
	return scopeLabels, nil
}

func GKEClustersShareKubernetesClusterScope(target, cluster *cloudv1.Cluster) bool {
	if target == nil || cluster == nil || !cluster.DeletionTimestamp.IsZero() {
		return false
	}
	targetLabels, missing := GKEKubernetesClusterScopeLabels(target)
	if len(missing) > 0 || len(targetLabels) == 0 {
		return false
	}
	clusterLabels, missing := GKEKubernetesClusterScopeLabels(cluster)
	if len(missing) > 0 || len(clusterLabels) == 0 {
		return false
	}
	for key, expected := range targetLabels {
		if clusterLabels[key] != expected {
			return false
		}
	}
	return true
}

func GKENodePoolNamingScopeKey(cluster *cloudv1.Cluster) (string, bool) {
	if cluster == nil || cluster.GetProviderType() != cloudv1.ProviderTypeGKE {
		return "", false
	}
	if cluster.Spec.NetworkName != "" {
		return "legacy:" + cluster.Namespace + "/" + cluster.Spec.NetworkName, true
	}
	if cluster.FeatureEnabled(consts.ClusterFeatureGCP3LayerNodePool) {
		gkeName := cluster.Spec.Kubernetes.Name
		if strings.TrimSpace(gkeName) == "" {
			return "", false
		}
		return "gke:" + gkeName, true
	}
	return "cluster:" + cluster.ClusterScopeName(), true
}

func gkeHeterogeneousNodePoolScopeHash(cluster *cloudv1.Cluster) (string, bool) {
	scopeKey, ok := GKENodePoolNamingScopeKey(cluster)
	if !ok {
		return "", false
	}
	hash := sha256.Sum256([]byte(scopeKey))
	return hex.EncodeToString(hash[:])[:gkeHeterogeneousNodePoolScopeHashLength], true
}

func GKEHeterogeneousNodePoolScopeLabels(cluster *cloudv1.Cluster) (map[string]string, []string) {
	scopeLabels, missing := GKEKubernetesClusterScopeLabels(cluster)
	if len(missing) > 0 || len(scopeLabels) == 0 {
		return nil, missing
	}
	scopeHash, ok := gkeHeterogeneousNodePoolScopeHash(cluster)
	if !ok {
		return nil, []string{consts.GKEHeterogeneousNodePoolScopeLabelKey}
	}
	labels := make(map[string]string, len(scopeLabels)+1)
	for key, value := range scopeLabels {
		labels[key] = value
	}
	labels[consts.GKEHeterogeneousNodePoolScopeLabelKey] = scopeHash
	return labels, nil
}

func GKEClustersShareHeterogeneousNodePoolScope(target, cluster *cloudv1.Cluster) bool {
	if target == nil || cluster == nil || !cluster.DeletionTimestamp.IsZero() {
		return false
	}
	targetLabels, missing := GKEHeterogeneousNodePoolScopeLabels(target)
	if len(missing) > 0 || len(targetLabels) == 0 {
		return false
	}
	clusterLabels, missing := GKEHeterogeneousNodePoolScopeLabels(cluster)
	if len(missing) > 0 || len(clusterLabels) == 0 {
		return false
	}
	for key, expected := range targetLabels {
		if clusterLabels[key] != expected {
			return false
		}
	}
	return true
}

func GKEComputeClassNameForNodePool(np *gkev1beta1.NodePool) string {
	if computeClass := GKEDesiredComputeClassNameForNodePool(np); computeClass != "" {
		return computeClass
	}
	if computeClass := GKEComputeClassSpecLabelForNodePool(np); computeClass != "" {
		return computeClass
	}
	return GKEComputeClassSpecTaintForNodePool(np)
}

func GKEDesiredComputeClassNameForNodePool(np *gkev1beta1.NodePool) string {
	if np == nil {
		return ""
	}
	if np.Annotations != nil {
		if computeClass := strings.TrimSpace(np.Annotations[consts.AnnoKeyGKENodePoolComputeClass]); computeClass != "" {
			return computeClass
		}
	}
	return ""
}

func GKEComputeClassSpecLabelForNodePool(np *gkev1beta1.NodePool) string {
	if np == nil || np.Spec.ForProvider.Config == nil || np.Spec.ForProvider.Config.Labels == nil {
		return ""
	}
	return strings.TrimSpace(np.Spec.ForProvider.Config.Labels[consts.GKEComputeClassLabelKey])
}

func GKEComputeClassSpecTaintForNodePool(np *gkev1beta1.NodePool) string {
	taint := findGKENodePoolTaintByKey(np, consts.GKEComputeClassLabelKey)
	if taint == nil || strings.TrimSpace(taint.Effect) != "NO_SCHEDULE" {
		return ""
	}
	return strings.TrimSpace(taint.Value)
}

func GKEAppliedComputeClassNameForNodePool(np *gkev1beta1.NodePool) string {
	if np == nil || np.Annotations == nil {
		return ""
	}
	return strings.TrimSpace(np.Annotations[consts.AnnoKeyGKENodePoolComputeClassApplied])
}

func GKEComputeClassMetadataMatchesNodePool(np *gkev1beta1.NodePool, desiredComputeClass, appliedComputeClass string) bool {
	desiredComputeClass = strings.TrimSpace(desiredComputeClass)
	appliedComputeClass = strings.TrimSpace(appliedComputeClass)
	currentDesired := GKEDesiredComputeClassNameForNodePool(np)
	currentApplied := GKEAppliedComputeClassNameForNodePool(np)
	currentSpecLabel := GKEComputeClassSpecLabelForNodePool(np)
	currentSpecTaint := GKEComputeClassSpecTaintForNodePool(np)

	switch {
	case desiredComputeClass != "" && appliedComputeClass != "":
		return currentDesired == desiredComputeClass &&
			currentApplied == appliedComputeClass &&
			currentSpecLabel == appliedComputeClass &&
			currentSpecTaint == appliedComputeClass
	case desiredComputeClass != "":
		return currentDesired == desiredComputeClass &&
			(currentApplied == "" || currentApplied == desiredComputeClass)
	case appliedComputeClass != "":
		return currentDesired == "" &&
			currentApplied == appliedComputeClass &&
			currentSpecLabel == appliedComputeClass &&
			currentSpecTaint == appliedComputeClass
	default:
		return currentDesired == "" && currentApplied == "" && currentSpecLabel == "" && currentSpecTaint == ""
	}
}

func HasGKEComputeClassMetadataForNodePool(np *gkev1beta1.NodePool) bool {
	return GKEDesiredComputeClassNameForNodePool(np) != "" ||
		GKEAppliedComputeClassNameForNodePool(np) != "" ||
		GKEComputeClassSpecLabelForNodePool(np) != "" ||
		GKEComputeClassSpecTaintForNodePool(np) != ""
}

func ApplyGKEComputeClassMetadataForNodePool(np *gkev1beta1.NodePool, desiredComputeClass, appliedComputeClass string) {
	desiredComputeClass = strings.TrimSpace(desiredComputeClass)
	appliedComputeClass = strings.TrimSpace(appliedComputeClass)

	if desiredComputeClass != "" {
		SetGKEDesiredComputeClassForNodePool(np, desiredComputeClass)
		if appliedComputeClass != "" {
			SetGKEComputeClassSpecLabelForNodePool(np, appliedComputeClass)
			SetGKEComputeClassSpecTaintForNodePool(np, appliedComputeClass)
		}
	} else {
		// Empty desired means the nodepool is either fully cleared or in cleanup.
		// Keep applied independent: cleanup uses it as a delete guard until GKE
		// confirms the real node label has been removed.
		ClearGKEDesiredComputeClassForNodePool(np)
		if appliedComputeClass != "" {
			SetGKEComputeClassSpecLabelForNodePool(np, appliedComputeClass)
			SetGKEComputeClassSpecTaintForNodePool(np, appliedComputeClass)
		} else {
			ClearGKEComputeClassSpecLabelForNodePool(np)
			ClearGKEComputeClassSpecTaintForNodePool(np)
		}
	}

	if appliedComputeClass != "" {
		SetGKEAppliedComputeClassForNodePool(np, appliedComputeClass)
	} else {
		ClearGKEAppliedComputeClassForNodePool(np)
	}
}

func SetGKEDesiredComputeClassForNodePool(np *gkev1beta1.NodePool, computeClass string) {
	computeClass = strings.TrimSpace(computeClass)
	if np == nil || computeClass == "" {
		return
	}
	if np.Annotations == nil {
		np.Annotations = map[string]string{}
	}
	np.Annotations[consts.AnnoKeyGKENodePoolComputeClass] = computeClass
}

func SetGKEComputeClassSpecLabelForNodePool(np *gkev1beta1.NodePool, computeClass string) {
	computeClass = strings.TrimSpace(computeClass)
	if np == nil || computeClass == "" {
		return
	}
	if np.Spec.ForProvider.Config == nil {
		np.Spec.ForProvider.Config = &gkev1beta1.NodeConfig{}
	}
	if np.Spec.ForProvider.Config.Labels == nil {
		np.Spec.ForProvider.Config.Labels = map[string]string{}
	}
	np.Spec.ForProvider.Config.Labels[consts.GKEComputeClassLabelKey] = computeClass
}

func SetGKEComputeClassSpecTaintForNodePool(np *gkev1beta1.NodePool, computeClass string) {
	setGKENodePoolTaint(np, consts.GKEComputeClassLabelKey, computeClass, "NO_SCHEDULE")
}

func SetGKEComputeClassForNodePool(np *gkev1beta1.NodePool, computeClass string) {
	computeClass = strings.TrimSpace(computeClass)
	if np == nil || computeClass == "" {
		return
	}
	SetGKEDesiredComputeClassForNodePool(np, computeClass)
	if strings.TrimSpace(np.Annotations[consts.AnnoKeyGKENodePoolComputeClassApplied]) != computeClass {
		delete(np.Annotations, consts.AnnoKeyGKENodePoolComputeClassApplied)
	}
	SetGKEComputeClassSpecLabelForNodePool(np, computeClass)
	SetGKEComputeClassSpecTaintForNodePool(np, computeClass)
}

func SetGKEAppliedComputeClassForNodePool(np *gkev1beta1.NodePool, computeClass string) {
	computeClass = strings.TrimSpace(computeClass)
	if np == nil || computeClass == "" {
		return
	}
	if np.Annotations == nil {
		np.Annotations = map[string]string{}
	}
	np.Annotations[consts.AnnoKeyGKENodePoolComputeClassApplied] = computeClass
}

func ClearGKEAppliedComputeClassForNodePool(np *gkev1beta1.NodePool) {
	if np == nil || np.Annotations == nil {
		return
	}
	delete(np.Annotations, consts.AnnoKeyGKENodePoolComputeClassApplied)
	if len(np.Annotations) == 0 {
		np.Annotations = nil
	}
}

func ClearGKEDesiredComputeClassForNodePool(np *gkev1beta1.NodePool) {
	if np == nil {
		return
	}
	if np.Annotations != nil {
		delete(np.Annotations, consts.AnnoKeyGKENodePoolComputeClass)
		if len(np.Annotations) == 0 {
			np.Annotations = nil
		}
	}
}

func ClearGKEComputeClassSpecLabelForNodePool(np *gkev1beta1.NodePool) {
	if np == nil {
		return
	}
	if np.Spec.ForProvider.Config == nil {
		return
	}
	if np.Spec.ForProvider.Config.Labels != nil {
		delete(np.Spec.ForProvider.Config.Labels, consts.GKEComputeClassLabelKey)
	}
}

func ClearGKEComputeClassSpecTaintForNodePool(np *gkev1beta1.NodePool) {
	clearGKENodePoolTaint(np, consts.GKEComputeClassLabelKey)
}

func HasGKEHeterogeneousNodePoolIsolationTaint(np *gkev1beta1.NodePool) bool {
	taint := findGKENodePoolTaintByKey(np, consts.GKEHeterogeneousNodePoolIsolationTaintKey)
	return taint != nil &&
		strings.TrimSpace(taint.Value) == consts.GKEHeterogeneousNodePoolIsolationTaintValue &&
		strings.TrimSpace(taint.Effect) == "NO_SCHEDULE"
}

func SetGKEHeterogeneousNodePoolIsolationTaint(np *gkev1beta1.NodePool) {
	setGKENodePoolTaint(
		np,
		consts.GKEHeterogeneousNodePoolIsolationTaintKey,
		consts.GKEHeterogeneousNodePoolIsolationTaintValue,
		"NO_SCHEDULE",
	)
}

func ClearGKEHeterogeneousNodePoolIsolationTaint(np *gkev1beta1.NodePool) {
	clearGKENodePoolTaint(np, consts.GKEHeterogeneousNodePoolIsolationTaintKey)
}

func ClearGKEComputeClassForNodePool(np *gkev1beta1.NodePool) {
	ClearGKEDesiredComputeClassForNodePool(np)
	ClearGKEAppliedComputeClassForNodePool(np)
	ClearGKEComputeClassSpecLabelForNodePool(np)
	ClearGKEComputeClassSpecTaintForNodePool(np)
}

func findGKENodePoolTaintByKey(np *gkev1beta1.NodePool, key string) *gkev1beta1.NodeTaint {
	key = strings.TrimSpace(key)
	if np == nil || key == "" || np.Spec.ForProvider.Config == nil {
		return nil
	}
	for _, taint := range np.Spec.ForProvider.Config.Taints {
		if taint == nil {
			continue
		}
		if strings.TrimSpace(taint.Key) == key {
			return taint
		}
	}
	return nil
}

func setGKENodePoolTaint(np *gkev1beta1.NodePool, key, value, effect string) {
	key = strings.TrimSpace(key)
	value = strings.TrimSpace(value)
	effect = strings.TrimSpace(effect)
	if np == nil || key == "" || value == "" || effect == "" {
		return
	}
	if np.Spec.ForProvider.Config == nil {
		np.Spec.ForProvider.Config = &gkev1beta1.NodeConfig{}
	}
	clearGKENodePoolTaint(np, key)
	np.Spec.ForProvider.Config.Taints = append(np.Spec.ForProvider.Config.Taints, &gkev1beta1.NodeTaint{
		Key:    key,
		Value:  value,
		Effect: effect,
	})
}

func clearGKENodePoolTaint(np *gkev1beta1.NodePool, key string) {
	key = strings.TrimSpace(key)
	if np == nil || key == "" || np.Spec.ForProvider.Config == nil || len(np.Spec.ForProvider.Config.Taints) == 0 {
		return
	}
	taints := np.Spec.ForProvider.Config.Taints[:0]
	for _, taint := range np.Spec.ForProvider.Config.Taints {
		if taint == nil || strings.TrimSpace(taint.Key) == key {
			continue
		}
		taints = append(taints, taint)
	}
	np.Spec.ForProvider.Config.Taints = taints
}

func GetAKSNodePoolName(machineType, clusterName, zone, k8sVersion string) string {
	// https://learn.microsoft.com/en-us/troubleshoot/azure/azure-kubernetes/create-upgrade-delete/aks-common-issues-faq#what-naming-restrictions-are-enforced-for-aks-resources-and-parameters-
	// AKS node pool names must be all lowercase. The names must be 1-12 characters in length for Linux node pools and 1-6 characters for Windows node pools. A name must start with a letter, and the only allowed characters are letters and numbers.

	// example: "dedicated-dev-eastus-af0534b1cc-bb7a0a31", "Standard_DS2_v2", "1.28"
	originalString := fmt.Sprintf("%s/%s/%s/%s", clusterName, machineType, k8sVersion, zone)
	hash := md5.Sum([]byte(originalString))
	hashString := hex.EncodeToString(hash[:])
	result := fmt.Sprintf("n%s", hashString[:11])
	return result
}
