package gke

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

	gkev1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	gkev1beta2 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta2"
	upboundcontainerv1beta1 "github.com/upbound/provider-gcp/apis/container/v1beta1"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/infra-provider/pkg/cloud/gcp"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/upgradeoperation/common"
)

var 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 != "" && common.FormatMajorMinorVersion(gke.Status.AtProvider.CurrentMasterVersion) == desiredVer
}

func getGKECR(ctx context.Context, cli client.Client, appBox *v1alpha1.APPBox) (*gkev1beta2.Cluster, error) {
	var gkeList gkev1beta2.ClusterList
	if err := cli.List(ctx, &gkeList, client.MatchingLabelsSelector{
		Selector: labels.SelectorFromSet(map[string]string{
			namer.LegacyNetworkNamespaceSelectorKey: appBox.Namespace,
			namer.AppBoxSelectorKey:                 appBox.Name,
		}),
	}); err != nil {
		return nil, fmt.Errorf("failed to list gke clusters for appbox %s/%s: %w", appBox.Namespace, appBox.Name, err)
	}
	if len(gkeList.Items) != 1 {
		return nil, fmt.Errorf("expected to find 1 gke cluster for appbox %s/%s, but found %d", appBox.Namespace, appBox.Name, len(gkeList.Items))
	}
	return &gkeList.Items[0], nil
}

func getGCPProjectID(s string) string {
	parts := strings.Split(s, "@")
	if len(parts) != 2 {
		return ""
	}
	projectParts := strings.Split(parts[1], ".")
	if len(projectParts) < 2 {
		return ""
	}
	return projectParts[0]
}

func getAdminNodePoolName(gke *gkev1beta2.Cluster) string {
	for _, np := range gke.Status.AtProvider.NodePools {
		if np != nil && np.Config != nil && (np.Config.Labels["cloud.pingcap.com/node-group-type"] == "control-plane" || np.Config.Labels["usedby"] == "dbaas-control-plane-shoot") {
			return np.Name
		}
	}
	return ""
}

// nodePool abstracts provider-specific GKE NodePool CRs for upgrade operations.
// All provider-specific operations (delete, apply, get node count, etc.) are encapsulated
// so callers don't need to know the underlying CR type.
type nodePool interface {
	GetName() string
	GetVersion() string
	SetVersion(version string)
	Delete(ctx context.Context, cli client.Client) error
	Apply(ctx context.Context, cli client.Client) error
	GetInstanceGroupURLs() []string
}

// contribNodePool implements nodePool for crossplane-contrib provider-gcp.
type contribNodePool struct {
	np *gkev1beta1.NodePool
}

func (n *contribNodePool) GetName() string {
	return n.np.Name
}

func (n *contribNodePool) GetVersion() string {
	if n.np.Spec.ForProvider.Version != nil {
		return *n.np.Spec.ForProvider.Version
	}
	return ""
}

func (n *contribNodePool) SetVersion(version string) {
	n.np.Spec.ForProvider.Version = pointer.String(version)
}

func (n *contribNodePool) Delete(ctx context.Context, cli client.Client) error {
	return cli.Delete(ctx, n.np)
}

func (n *contribNodePool) Apply(ctx context.Context, cli client.Client) error {
	return k8s.Apply(ctx, cli, n.np)
}

func (n *contribNodePool) GetInstanceGroupURLs() []string {
	return n.np.Status.AtProvider.InstanceGroupUrls
}

// upboundNodePool implements nodePool for upbound provider-gcp.
type upboundNodePool struct {
	np *upboundcontainerv1beta1.NodePool
}

func (n *upboundNodePool) GetName() string {
	return n.np.Name
}

func (n *upboundNodePool) GetVersion() string {
	// Prefer status version if available, otherwise fall back to spec version
	if n.np.Status.AtProvider.Version != nil {
		return *n.np.Status.AtProvider.Version
	}
	if n.np.Spec.ForProvider.Version != nil {
		return *n.np.Spec.ForProvider.Version
	}
	return ""
}

func (n *upboundNodePool) SetVersion(version string) {
	n.np.Spec.ForProvider.Version = pointer.String(version)
}

func (n *upboundNodePool) Delete(ctx context.Context, cli client.Client) error {
	return cli.Delete(ctx, n.np)
}

func (n *upboundNodePool) Apply(ctx context.Context, cli client.Client) error {
	return k8s.Apply(ctx, cli, n.np)
}

func (n *upboundNodePool) GetInstanceGroupURLs() []string {
	urls := make([]string, 0, len(n.np.Status.AtProvider.InstanceGroupUrls))
	for _, urlPtr := range n.np.Status.AtProvider.InstanceGroupUrls {
		if urlPtr != nil && *urlPtr != "" {
			urls = append(urls, *urlPtr)
		}
	}
	return urls
}

// getNodeCount calculates the total number of nodes in a NodePool by querying GCP.
func getNodeCount(ctx context.Context, gcpClient gcp.Client, np nodePool) (int64, error) {
	var total int64
	for _, url := range np.GetInstanceGroupURLs() {
		parts := instanceGroupURLPattern.FindStringSubmatch(url)
		if len(parts) != 5 {
			return 0, fmt.Errorf("failed to parse instance group url %q: got %d submatches", url, 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.GetName(), err)
		}
		total += size
	}
	return total, nil
}

// fetchNodePool fetches a GKE NodePool CR by name, trying crossplane-contrib first, then upbound.
func fetchNodePool(ctx context.Context, cli client.Client, name string) (nodePool, error) {
	var np gkev1beta1.NodePool
	err := cli.Get(ctx, client.ObjectKey{Name: name}, &np)
	if err == nil {
		return &contribNodePool{&np}, nil
	}
	if !errors.IsNotFound(err) {
		return nil, fmt.Errorf("failed to get node pool %s from crossplane-contrib: %w", name, err)
	}

	var upNP upboundcontainerv1beta1.NodePool
	if err := cli.Get(ctx, client.ObjectKey{Name: name}, &upNP); err != nil {
		if errors.IsNotFound(err) {
			return nil, fmt.Errorf("node pool %s not found in either crossplane-contrib or upbound provider", name)
		}
		return nil, fmt.Errorf("failed to get node pool %s from upbound provider: %w", name, err)
	}
	return &upboundNodePool{&upNP}, nil
}
