package gke

import (
	"context"
	"fmt"
	"regexp"
	"strings"

	"cloud.google.com/go/container/apiv1/containerpb"
	gkev1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	"github.com/go-logr/logr"
	"github.com/hashicorp/go-multierror"
	opv1alpha1 "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/selection"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/tidbcloud/eks-provider/controllers/upgradeoperation/common"
	"github.com/tidbcloud/eks-provider/controllers/utils"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/applier"
	"github.com/tidbcloud/infra-provider/pkg/applier/mutatefuncs"
	"github.com/tidbcloud/infra-provider/pkg/cloud/gcp"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	infraapicommon "github.com/tidbcloud/infra-provider/pkg/controllers/common"
	"github.com/tidbcloud/infra-provider/pkg/dataplane"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/nodepool"
)

const (
	maxNodeCountPerZone int64 = 500

	annoKeyResourceLabels = "gcp.cloud.pingcap.com/resource-labels"
)

var (
	nodePoolPattern = regexp.MustCompile(`projects/.*/locations/.*/clusters/.*/nodePools/.*`)
)

// formatGKEVersion formats the GKE version to the format of "x.y".
func formatGKEVersion(version string) string {
	re := regexp.MustCompile(`^(\d+\.\d+)`)
	match := re.FindStringSubmatch(version)
	if len(match) > 1 {
		return match[1]
	}
	return ""
}

// getAllTiDBClustersUnderNetwork lists all TiDB clusters under a given network.
// It returns a slice of pointers to the matched cluster CRs, or an error if the cluster list couldn't be retrieved.
func getAllTiDBClustersUnderNetwork(ctx context.Context, infraCli client.Client, envProvider dataplane.EnvProvider, cluster *cloudv1.Cluster) ([]*cloudv1.Cluster, error) {
	var list cloudv1.ClusterList
	if err := infraCli.List(ctx, &list, client.InNamespace(cluster.Namespace)); err != nil {
		return nil, fmt.Errorf("failed to list clusters under namespace %s: %v", cluster.Namespace, err)
	}
	clusters := make([]*cloudv1.Cluster, 0, len(list.Items))
	for i := range list.Items {
		c := &list.Items[i]
		if c.Spec.Kubernetes.Network == envProvider.GetNetwork() {
			clusters = append(clusters, c)
		}
	}
	return clusters, nil
}

// createNewVersionTiDBNodePools creates new version TiDB node pools based on the old node pools.
func createNewVersionNodePoolsForCluster(ctx context.Context, infraCli client.Client, cluster *cloudv1.Cluster, oldNPs []*gkev1beta1.NodePool, desiredVersion, gkeVersion string) error {
	var errs *multierror.Error

	// Get all instance types of the cluster.
	instanceTypes := map[string]struct{}{}
	for _, comp := range cluster.GetAllComponents() {
		instanceTypes[comp.GetInstanceType()] = struct{}{}
	}

	npApplier := applier.NewApplier(infraCli, mutatefuncs.MutateGKENodePool)
	for _, oldNP := range oldNPs {
		if it := oldNP.Spec.ForProvider.Config.MachineType; it != nil {
			if _, ok := instanceTypes[*it]; ok {
				newNP := &gkev1beta1.NodePool{
					ObjectMeta: metav1.ObjectMeta{
						Name:   nodepool.GetGKENodePoolName(cluster, *oldNP.Spec.ForProvider.Config.MachineType, desiredVersion),
						Labels: oldNP.GetLabels(),
					},
					Spec: oldNP.Spec,
				}

				// add anno to avoid GCP api call
				if newNP.ObjectMeta.Annotations == nil {
					newNP.ObjectMeta.Annotations = make(map[string]string)
				}
				newNP.ObjectMeta.Annotations[annoKeyResourceLabels] =
					utils.BuildResourceLabelsAnnotationValue(cloudv1.TagsFromLabels(utils.GenerateCommonNodePoolLabels(cluster)))

				// Modify the new node pool's node labels and version.
				newNP.Spec.ForProvider.Config.Labels[consts.K8sVersionLabelKey] = desiredVersion
				newNP.Spec.ForProvider.Version = pointer.String(gkeVersion)
				newNP.Spec.ForProvider.Autoscaling.MaxNodeCount = pointer.Int64(maxNodeCountPerZone)

				if _, err := npApplier.Expected(newNP).Apply(ctx); err != nil {
					errs = multierror.Append(errs, fmt.Errorf("unable to create new version TiDB node pool %s: %w", newNP.Name, err))
				}
			}
		}
	}

	return errs.ErrorOrNil()
}

func isNodePoolVersionMatch(np *gkev1beta1.NodePool, version string) bool {
	if np.Spec.ForProvider.Version == nil {
		return false
	}
	return formatGKEVersion(*np.Spec.ForProvider.Version) == version &&
		np.Spec.ForProvider.Config.Labels[consts.K8sVersionLabelKey] == version
}

// getAllTiDBNodePoolsUnderNetwork lists all tidb cluster node pools under a given gke.
func getAllTiDBNodePoolsUnderNetwork(ctx context.Context, infraCli client.Client, logger logr.Logger, gke *containerpb.Cluster) *gkev1beta1.NodePoolList {
	// If pd, tidb, tikv, tiflash, and tiproxy use different instance types.
	nps := make([]gkev1beta1.NodePool, 0, 5)
	for _, npStatus := range gke.GetNodePools() {
		if npStatus != nil && npStatus.GetConfig() != nil && npStatus.GetConfig().GetLabels()["cluster"] == "shared" {
			var np gkev1beta1.NodePool
			if err := infraCli.Get(ctx, types.NamespacedName{Name: npStatus.GetName()}, &np); err != nil {
				logger.Error(err, "failed to get the node pool CR", "nodepool", npStatus.GetName())
			}
			nps = append(nps, np)
		}
	}
	return &gkev1beta1.NodePoolList{Items: nps}
}

func getTiDBNodePoolLabelSelector(cluster *cloudv1.Cluster) (client.MatchingLabelsSelector, error) {
	ls := map[string]string{
		"tidb-component":    infraapicommon.ComponentShared,
		"tidbcloud-cluster": infraapicommon.ComponentShared,
	}

	networkName := cluster.Spec.NetworkName
	use3Layer := cluster.FeatureEnabled(consts.ClusterFeatureGCP3LayerNodePool)

	switch {
	case networkName != "":
		ls[namer.LegacyNetworkNamespaceSelectorKey] = cluster.Namespace
		ls[namer.LegacyNetworkSelectorKey] = networkName
	case use3Layer:
		ls[cloudv1.ProjectTagKey] = cluster.GetProjectID()
		ls[cloudv1.TenantTagKey] = cluster.GetTenantID()
	}

	labelSelector := client.MatchingLabelsSelector{
		Selector: labels.SelectorFromSet(ls),
	}

	// NOTE: it's a buggy implementation, but has been used in prod.
	// More detail in github.com/tidbcloud/infra-provider/pkg/nodepool.GetGKENodePoolName
	// Legacy clusters (no network name, no 3-layer feature): exclude nodepools that carry
	// project/tenant/network tags to avoid matching other-style pools.
	// Expected legacy labels:
	//   tidb-component: shared
	//   tidb-namespace: shared
	//   tidbcloud-cluster: shared
	//   gardener.wg.prefix.name: shared-shared
	if networkName == "" && !use3Layer {
		antiKeys := []string{
			cloudv1.ProjectTagKey,
			cloudv1.TenantTagKey,
			namer.LegacyNetworkNamespaceSelectorKey,
			namer.LegacyNetworkSelectorKey,
		}
		requirements := make([]labels.Requirement, 0, len(antiKeys))
		for _, key := range antiKeys {
			req, err := labels.NewRequirement(key, selection.DoesNotExist, nil)
			if err != nil {
				return labelSelector, fmt.Errorf("failed to generate anti-label %s: %w", key, err)
			}
			requirements = append(requirements, *req)
		}
		labelSelector.Selector = labelSelector.Selector.Add(requirements...)
	}

	return labelSelector, nil
}

func filterLegacyNodePoolsByAntiLabels(cluster *cloudv1.Cluster, npList *gkev1beta1.NodePoolList, currentVersion, desiredVersion string) *gkev1beta1.NodePoolList {
	// NOTE: label selector only works on metadata.labels.
	// For legacy clusters (no network name, no 3-layer feature), we must exclude nodepools that
	// carry any of these keys to avoid matching other-style pools.
	antiKeys := []string{
		cloudv1.ProjectTagKey,
		cloudv1.TenantTagKey,
		namer.LegacyNetworkNamespaceSelectorKey,
		namer.LegacyNetworkSelectorKey,
	}

	out := make([]gkev1beta1.NodePool, 0, len(npList.Items))
	for i := range npList.Items {
		np := npList.Items[i]
		// Exclude nodepools that carry any of these keys to avoid matching other-style pools.
		if hasAnyKey(np.GetLabels(), antiKeys) {
			continue
		}
		if np.Spec.ForProvider.Config == nil || np.Spec.ForProvider.Config.MachineType == nil {
			continue
		}
		// We only select the node pool that corresponds to the current cluster and either current or desired version,
		// to avoid matching other cluster's pools.
		currentNodePoolName := nodepool.GetGKENodePoolName(cluster, *np.Spec.ForProvider.Config.MachineType, currentVersion)
		desiredNodePoolName := nodepool.GetGKENodePoolName(cluster, *np.Spec.ForProvider.Config.MachineType, desiredVersion)
		if np.Name != currentNodePoolName && np.Name != desiredNodePoolName {
			continue
		}
		out = append(out, np)
	}

	return &gkev1beta1.NodePoolList{Items: out}
}

func hasAnyKey(m map[string]string, keys []string) bool {
	if len(m) == 0 {
		return false
	}
	for _, k := range keys {
		if _, ok := m[k]; ok {
			return true
		}
	}
	return false
}

// getTiDBNodePoolsByCluster function fetches the list of node pool CRs for a specific cluster CR
// and categorizes them into two categories: the old version and the new version.
// It also decides whether new node pool CRs need to be created depending upon their count and cluster's instance types.
func getTiDBNodePoolsByCluster(npList *gkev1beta1.NodePoolList, cluster *cloudv1.Cluster, desiredVersion string) (oldNPs, newNPs []*gkev1beta1.NodePool, needCreate bool, err error) {
	// Get all instance types of the cluster, since we create tidb node pools by instance type.
	instanceTypes := map[string]struct{}{}
	for _, comp := range cluster.GetAllComponents() {
		if it := comp.GetInstanceType(); len(it) > 0 {
			instanceTypes[comp.GetInstanceType()] = struct{}{}
		}
	}

	enable3layerNP := cluster.FeatureEnabled(consts.ClusterFeatureGCP3LayerNodePool)

	for i := range npList.Items {
		np := &npList.Items[i]
		// In 3-layer arch, tidb node pools do not have labels about its region or gke cluster,
		// so we need to filter them by the cluster name.
		if strings.Contains(np.Spec.ForProvider.Cluster, cluster.Spec.Kubernetes.Name) {
			if it := np.Spec.ForProvider.Config.MachineType; it != nil {
				// Only consider the node pool that has the same instance type as the cluster.
				if _, ok := instanceTypes[*it]; ok {
					if isNodePoolVersionMatch(np, desiredVersion) {
						newNPs = append(newNPs, np)
					} else if !enable3layerNP || np.Spec.ForProvider.Config.Labels[consts.LabelKeyFor3LayerArch] == consts.LabelValTrue {
						// If the cluster enables 3-layer nodepools, ignore nodepools those do not have the 3-layer label.
						oldNPs = append(oldNPs, np)
					}
				}
			}
		}
	}

	// If the number of new node pools is less than the number of instance types, we need to create new node pools.
	return oldNPs, newNPs, len(newNPs) < len(instanceTypes), nil
}

func allTiDBClustersSuspended(ctx context.Context, infraCli client.Client, envProvider dataplane.EnvProvider, cluster *cloudv1.Cluster) bool {
	clusters, _ := getAllTiDBClustersUnderNetwork(ctx, infraCli, envProvider, cluster)
	suspendCount := 0
	for _, c := range clusters {
		if c.Status.Phase == cloudv1.ClusterPhaseSuspend {
			suspendCount++
		}
	}
	if len(clusters) == suspendCount {
		return true
	}
	return false
}

func getResourceLabels(cluster *cloudv1.Cluster) map[string]string {
	// Append billing resLabels
	resLabels := utils.GenerateCommonNodePoolLabels(cluster)

	// Strip from tag prefix
	return cloudv1.TagsFromLabels(resLabels)
}

func addResourceLabelsToNodePools(ctx context.Context, gcpClient gcp.Client, nps []*gkev1beta1.NodePool, resLabels map[string]string) (int, error) {
	var errs *multierror.Error

	for _, np := range nps {
		if np.Status.AtProvider.Status != gkev1beta1.NodePoolStateRunning {
			errs = multierror.Append(errs, fmt.Errorf("node pool %s is not ready: %s", np.Name, np.Status.AtProvider.Status))
			continue
		}

		// The name of nodePool must be in the format: `projects/*/locations/*/clusters/*/nodePools/*`
		nodePoolName := nodePoolPattern.FindString(np.Status.AtProvider.SelfLink)
		if nodePoolName == "" {
			errs = multierror.Append(errs, fmt.Errorf("failed to extract node pool name from self link %s", np.Status.AtProvider.SelfLink))
			continue
		}

		op, err := gcpClient.AppendNodePoolResourceLabels(ctx, nodePoolName, resLabels)
		if err != nil {
			errs = multierror.Append(errs, fmt.Errorf("failed to set resource labels for node pool %s: %w", np.Name, err))
			continue
		}

		if op != nil {
			errs = multierror.Append(errs, fmt.Errorf("an unexpected update operation has occurred for node pool %s", np.Name))
			break
		}
	}

	return len(errs.WrappedErrors()), errs.ErrorOrNil()
}

func countReadyNodePools(nps []*gkev1beta1.NodePool) (ready, total int) {
	total = len(nps)
	for _, np := range nps {
		if np.Status.AtProvider.Status == gkev1beta1.NodePoolStateRunning {
			ready++
		}
	}
	return ready, total
}

// countTCPodsOnNewNodePools returns how many active tidb-operator pods are on new node pools.
func countTCPodsOnNewNodePools(ctx context.Context, cli client.Client, namespace string, newNPs []*gkev1beta1.NodePool) (onNew, total int, err error) {
	logger := log.FromContext(ctx)

	targetNPs := map[string]struct{}{}
	for _, np := range newNPs {
		targetNPs[np.Name] = struct{}{}
	}
	if len(targetNPs) == 0 {
		return 0, 0, fmt.Errorf("no new node pools found")
	}

	var podList corev1.PodList
	if err = cli.List(ctx, &podList,
		client.InNamespace(namespace),
		client.MatchingLabels{
			consts.ManagedByLabelKey: "tidb-operator",
		},
	); err != nil {
		return 0, 0, fmt.Errorf("unable to list tidb-operator pods in namespace %s: %w", namespace, err)
	}

	for _, pod := range podList.Items {
		_, exists := common.ComponentSet[pod.Labels[consts.ComponentLabelKey]]
		if !exists || !pod.DeletionTimestamp.IsZero() {
			continue
		}
		total++

		if pod.Spec.NodeName == "" {
			logger.Info("pod has not been scheduled", "pod", pod.Name)
			continue
		}

		var node corev1.Node
		if err = cli.Get(ctx, types.NamespacedName{Name: pod.Spec.NodeName}, &node); err != nil {
			return onNew, total, fmt.Errorf("unable to get node %s for pod %s: %w", pod.Spec.NodeName, pod.Name, err)
		}

		npName := node.Labels["cloud.google.com/gke-nodepool"]
		if _, ok := targetNPs[npName]; !ok {
			logger.Info("pod is still on old nodepool", "pod", pod.Name, "component", pod.Labels[consts.ComponentLabelKey], "nodepool", npName)
			continue
		}
		onNew++
	}

	if total == 0 {
		logger.Info("no active tidb-operator pods found in namespace", "namespace", namespace)
	}
	return onNew, total, nil
}

// countComponentPodsOnNewNodePools returns how many active component pods are on new node pools.
func countComponentPodsOnNewNodePools(ctx context.Context, cli client.Client, namespace, component string, newNPs []*gkev1beta1.NodePool, tc *opv1alpha1.TidbCluster) (onNew, total int, err error) {
	logger := log.FromContext(ctx)

	targetNPs := map[string]struct{}{}
	for _, np := range newNPs {
		targetNPs[np.Name] = struct{}{}
	}
	if len(targetNPs) == 0 {
		return 0, 0, fmt.Errorf("no new node pools found")
	}

	var podList corev1.PodList
	if err = cli.List(ctx, &podList,
		client.InNamespace(namespace),
		client.MatchingLabelsSelector{
			Selector: labels.SelectorFromSet(map[string]string{
				consts.ManagedByLabelKey: "tidb-operator",
				consts.ComponentLabelKey: component,
				consts.InstanceLabelKey:  tc.Name,
			}),
		},
	); err != nil {
		return 0, 0, fmt.Errorf("unable to list %s pods: %w", component, err)
	}

	for _, pod := range podList.Items {
		if pod.DeletionTimestamp != nil {
			continue
		}
		total++

		if pod.Spec.NodeName == "" {
			logger.Info("component pod has not been scheduled", "component", component, "pod", pod.Name)
			continue
		}

		var node corev1.Node
		if err = cli.Get(ctx, types.NamespacedName{Name: pod.Spec.NodeName}, &node); err != nil {
			return onNew, total, fmt.Errorf("unable to get node %s for pod %s: %w", pod.Spec.NodeName, pod.Name, err)
		}

		npName := node.Labels["cloud.google.com/gke-nodepool"]
		if _, ok := targetNPs[npName]; !ok {
			logger.Info("component pod is still on old nodepool", "component", component, "pod", pod.Name, "nodepool", npName)
			continue
		}
		onNew++
	}

	if total == 0 {
		logger.Info("no active component pods found", "component", component)
	}
	return onNew, total, nil
}
