package gke

import (
	"context"
	"fmt"
	"regexp"

	gkev1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	gkev1beta2 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta2"
	"github.com/hashicorp/go-multierror"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"

	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/namer"
	"github.com/tidbcloud/infra-provider/pkg/nodepool"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

const (
	maxNodeCountPerZone int64 = 500
)

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

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

func isControlPlaneUpgraded(gke *gkev1beta2.Cluster, desiredVer string) bool {
	return desiredVer != "" && formatGKEVersion(gke.Status.AtProvider.CurrentMasterVersion) == desiredVer
}

func getGKECR(ctx context.Context, cli client.Client, network *cloudv1.Network) (*gkev1beta2.Cluster, error) {
	var gke gkev1beta2.Cluster
	key := types.NamespacedName{Name: util.GetAnnotation(network, consts.AnnotationKeyDataplaneName)}
	if err := cli.Get(ctx, key, &gke); err != nil {
		return nil, err
	}
	return &gke, nil
}

// 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, network *cloudv1.Network) ([]*cloudv1.Cluster, error) {
	var list cloudv1.ClusterList
	if err := infraCli.List(ctx, &list, client.InNamespace(network.Namespace)); err != nil {
		return nil, fmt.Errorf("failed to list clusters under network %s/%s: %v", network.Namespace, network.Name, err)
	}
	var clusters []*cloudv1.Cluster
	for i := range list.Items {
		c := &list.Items[i]
		if c.Spec.NetworkName == network.Name {
			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, network *cloudv1.Network, 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.LegacyTiDBNodePoolName(network, desiredVersion, *oldNP.Spec.ForProvider.Config.MachineType),
						Labels: oldNP.GetLabels(),
					},
					Spec: oldNP.Spec,
				}

				// 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 getNodePoolVersion(np *gkev1beta1.NodePool) string {
	if np.Spec.ForProvider.Version != nil {
		return formatGKEVersion(*np.Spec.ForProvider.Version)
	}

	return np.Spec.ForProvider.Config.Labels[consts.K8sVersionLabelKey]
}

// 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, np *gkev1beta1.NodePool) (int64, error) {
	var total int64

	for _, url := range np.Status.AtProvider.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 node pool %s: %w", np.Name, err)
		}

		total += size
	}
	return total, nil
}

// getAllNodePoolsUnderNetwork lists all node pools under a given network.
func getAllNodePoolsUnderNetwork(ctx context.Context, infraCli client.Client, network *cloudv1.Network) (nps []*gkev1beta1.NodePool, err error) {
	var list gkev1beta1.NodePoolList
	if err = infraCli.List(ctx, &list, client.MatchingLabelsSelector{
		Selector: labels.Set{
			namer.ComponentSelectorKey: "shared", // Avoid listing the admin node pools.
			"tidbcloud-namespace":      network.Namespace,
			"tidbcloud-network":        network.Name,
		}.AsSelector(),
	}); client.IgnoreNotFound(err) != nil {
		return nil, fmt.Errorf("failed to list tidb node pools: %w", err)
	}

	for i := range list.Items {
		nps = append(nps, &list.Items[i])
	}
	return nps, nil
}

// 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(ctx context.Context, infraCli client.Client, cluster *cloudv1.Cluster, desiredVersion string) (oldNPs, newNPs []*gkev1beta1.NodePool, needCreate bool, err error) {
	var list gkev1beta1.NodePoolList
	if err = infraCli.List(ctx, &list, client.MatchingLabelsSelector{
		Selector: labels.Set{
			"tidbcloud-cluster":   infraapicommon.ComponentShared,
			"tidbcloud-namespace": cluster.Namespace,
			"tidbcloud-network":   cluster.Spec.NetworkName,
		}.AsSelector(),
	}); err != nil {
		return nil, nil, false, fmt.Errorf("failed to list tidb node pools: %w", err)
	}

	// 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{}{}
		}
	}

	for i := range list.Items {
		np := &list.Items[i]
		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 getNodePoolVersion(np) == desiredVersion {
					newNPs = append(newNPs, np)
				} else {
					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 toNodePoolList(nps []*gkev1beta1.NodePool) *gkev1beta1.NodePoolList {
	var list gkev1beta1.NodePoolList
	for _, np := range nps {
		list.Items = append(list.Items, *np)
	}
	return &list
}

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

func getResourceLabels(network *cloudv1.Network) map[string]string {
	resLabels := network.GetLabels()

	// Append billing resLabels
	resLabels[cloudv1.TagLabelPrefix+infraapicommon.UsedBy] = infraapicommon.CustomerTiDB
	resLabels[cloudv1.TagLabelPrefix+infraapicommon.Cluster] = infraapicommon.ClustersShared

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

func addResourceLabelsToNodePools(ctx context.Context, gcpClient gcp.Client, nps []*gkev1beta1.NodePool, resLabels map[string]string) 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 errs.ErrorOrNil()
}
