package server

import (
	"context"
	"strings"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	apicommon "github.com/tidbcloud/infra-provider/api/tidbcloud/common"
	"github.com/tidbcloud/infra-provider/pkg/gkeheterogeneous"
	nodepoolutil "github.com/tidbcloud/infra-provider/pkg/nodepool"
)

type gkeHeterogeneousIntentConfig struct {
	componentType        apicommon.ComponentType
	bindingAnnotationKey string
}

type gkeHeterogeneousIntentLocation struct {
	clusterName      string
	clusterNamespace string
	componentType    apicommon.ComponentType
	tidbGroupID      string
}

func (s *ClusterServer) gkeHeterogeneousCluster(ctx context.Context, clusterID string) (*cloudv1.Cluster, error) {
	clusterID = strings.TrimSpace(clusterID)
	if clusterID == "" {
		return nil, status.Error(codes.InvalidArgument, "cluster_id is required")
	}
	cluster, err := s.getClusterCR(ctx, clusterID)
	if err != nil {
		return nil, err
	}
	if cluster.GetProviderType() != cloudv1.ProviderTypeGKE {
		return nil, status.Errorf(codes.FailedPrecondition, "cluster %s is not GKE", clusterID)
	}
	return cluster, nil
}

func resolveGKEHeterogeneousTiDBGroupID(cluster *cloudv1.Cluster, componentType apicommon.ComponentType, groupID string) (string, error) {
	groupID = strings.TrimSpace(groupID)
	if componentType != apicommon.ComponentType_TIDB {
		// tidb_group_id only selects TiDB groups. TiKV has one heterogeneous
		// intent per cluster/component, so request values are ignored by design.
		return "", nil
	}
	if groupID == "" {
		return "", status.Error(codes.InvalidArgument, "tidb_group_id is required for TiDB heterogeneous deploy")
	}
	component, err := cluster.GetComponentSpecInGroup(groupID, cloudv1.ComponentTiDB)
	if err != nil {
		return "", status.Error(codes.NotFound, err.Error())
	}
	if component == nil {
		return "", status.Errorf(codes.NotFound, "TiDB group %s not found", groupID)
	}
	return groupID, nil
}

func resolveGKEHeterogeneousIntent(cluster *cloudv1.Cluster, componentType apicommon.ComponentType, requestedGroupID string) (gkeHeterogeneousIntentConfig, string, error) {
	intentConfig, err := gkeHeterogeneousIntentConfigForComponent(componentType)
	if err != nil {
		return gkeHeterogeneousIntentConfig{}, "", err
	}
	tidbGroupID, err := resolveGKEHeterogeneousTiDBGroupID(cluster, componentType, requestedGroupID)
	if err != nil {
		return gkeHeterogeneousIntentConfig{}, "", err
	}
	return intentConfig, tidbGroupID, nil
}

func resolveGKEHeterogeneousIntentForDelete(cluster *cloudv1.Cluster, componentType apicommon.ComponentType, requestedGroupID string) (gkeHeterogeneousIntentConfig, string, error) {
	intentConfig, err := gkeHeterogeneousIntentConfigForComponent(componentType)
	if err != nil {
		return gkeHeterogeneousIntentConfig{}, "", err
	}
	if componentType != apicommon.ComponentType_TIDB {
		return resolveGKEHeterogeneousIntent(cluster, componentType, requestedGroupID)
	}
	requestedGroupID = strings.TrimSpace(requestedGroupID)
	if requestedGroupID == "" {
		return gkeHeterogeneousIntentConfig{}, "", status.Error(codes.InvalidArgument, "tidb_group_id is required for TiDB heterogeneous deploy")
	}
	if gkeheterogeneous.TiDBGroupExists(cluster, requestedGroupID) {
		return intentConfig, requestedGroupID, nil
	}
	if strings.TrimSpace(cluster.GetAnnotations()[gkeHeterogeneousIntentAnnotationKey(cluster, intentConfig, requestedGroupID)]) != "" {
		return intentConfig, requestedGroupID, nil
	}
	return gkeHeterogeneousIntentConfig{}, "", status.Errorf(codes.NotFound, "TiDB group %s not found", requestedGroupID)
}

func gkeHeterogeneousIntentConfigForComponent(componentType apicommon.ComponentType) (gkeHeterogeneousIntentConfig, error) {
	switch componentType {
	case apicommon.ComponentType_TIKV:
		return gkeHeterogeneousIntentConfig{
			componentType:        componentType,
			bindingAnnotationKey: cloudv1.AnnoKeyGKEComputeClassForTiKVBindings,
		}, nil
	case apicommon.ComponentType_TIDB:
		return gkeHeterogeneousIntentConfig{
			componentType:        componentType,
			bindingAnnotationKey: cloudv1.AnnoKeyGKEComputeClassForTiDBBindings,
		}, nil
	default:
		return gkeHeterogeneousIntentConfig{}, status.Errorf(codes.InvalidArgument, "unsupported heterogeneous component type %s", componentType)
	}
}

func baseGKEHeterogeneousInstanceType(cluster *cloudv1.Cluster, componentType apicommon.ComponentType, tidbGroupID string) string {
	if cluster == nil {
		return ""
	}
	if componentType == apicommon.ComponentType_TIDB {
		return cluster.TiDBInstanceTypeForGroup(tidbGroupID)
	}
	return strings.TrimSpace(cluster.Spec.Cluster.TiKV.InstanceType)
}

func gkeHeterogeneousIntentAnnotationKey(cluster *cloudv1.Cluster, intentConfig gkeHeterogeneousIntentConfig, tidbGroupID string) string {
	if intentConfig.componentType != apicommon.ComponentType_TIDB {
		return intentConfig.bindingAnnotationKey
	}
	return cluster.TiDBGKEComputeClassBindingsAnnoKey(tidbGroupID)
}

func gkeHeterogeneousIntentEnabled(cluster *cloudv1.Cluster, intentConfig gkeHeterogeneousIntentConfig, tidbGroupID string) bool {
	if cluster == nil {
		return false
	}
	if intentConfig.componentType != apicommon.ComponentType_TIDB {
		return cluster.GKEComputeClassForTiKVEnabled()
	}
	return cluster.GKEComputeClassForTiDBGroupEnabled(tidbGroupID)
}

func gkeHeterogeneousIntentConfigured(cluster *cloudv1.Cluster, intentConfig gkeHeterogeneousIntentConfig, tidbGroupID string) bool {
	if cluster == nil {
		return false
	}
	if intentConfig.componentType == apicommon.ComponentType_TIDB {
		return cluster.TiDBGKEComputeClassIntentConfigured(tidbGroupID)
	}
	return cluster.TiKVGKEComputeClassIntentConfigured()
}

func forEachConfiguredGKEHeterogeneousIntent(cluster *cloudv1.Cluster, visit func(gkeHeterogeneousIntentConfig, string) error) error {
	if cluster == nil || cluster.GetProviderType() != cloudv1.ProviderTypeGKE {
		return nil
	}
	visitIntent := func(componentType apicommon.ComponentType, tidbGroupID string) error {
		intentConfig, resolvedGroupID, err := resolveGKEHeterogeneousIntent(cluster, componentType, tidbGroupID)
		if err != nil {
			return err
		}
		if !gkeHeterogeneousIntentConfigured(cluster, intentConfig, resolvedGroupID) {
			return nil
		}
		return visit(intentConfig, resolvedGroupID)
	}
	if err := visitIntent(apicommon.ComponentType_TIKV, ""); err != nil {
		return err
	}
	for _, groupID := range gkeheterogeneous.ActiveTiDBGroupIDs(cluster) {
		if err := visitIntent(apicommon.ComponentType_TIDB, groupID); err != nil {
			return err
		}
	}
	return nil
}

func requireGKEKubernetesClusterScopeLabels(cluster *cloudv1.Cluster) (map[string]string, error) {
	if cluster == nil {
		return nil, status.Error(codes.Internal, "cluster is nil")
	}
	scopeLabels, missingScopeLabels := nodepoolutil.GKEKubernetesClusterScopeLabels(cluster)
	if len(missingScopeLabels) > 0 {
		return nil, status.Errorf(codes.FailedPrecondition, "cluster %s/%s is missing required GKE Kubernetes cluster scope labels: %s", cluster.Namespace, cluster.Name, strings.Join(missingScopeLabels, ","))
	}
	return scopeLabels, nil
}

func requireGKEHeterogeneousNodePoolScopeLabels(cluster *cloudv1.Cluster) (map[string]string, error) {
	if cluster == nil {
		return nil, status.Error(codes.Internal, "cluster is nil")
	}
	scopeLabels, missingScopeLabels := nodepoolutil.GKEHeterogeneousNodePoolScopeLabels(cluster)
	if len(missingScopeLabels) > 0 {
		return nil, status.Errorf(codes.FailedPrecondition, "cluster %s/%s is missing required GKE heterogeneous nodepool scope labels: %s", cluster.Namespace, cluster.Name, strings.Join(missingScopeLabels, ","))
	}
	return scopeLabels, nil
}

func (s *ClusterServer) listGKEClustersInSameKubernetesClusterScopeForSharedNodePool(ctx context.Context, target *cloudv1.Cluster) ([]*cloudv1.Cluster, error) {
	if target == nil || target.GetProviderType() != cloudv1.ProviderTypeGKE {
		return nil, nil
	}
	if _, err := requireGKEHeterogeneousNodePoolScopeLabels(target); err != nil {
		return nil, err
	}
	scopeLabels, err := requireGKEKubernetesClusterScopeLabels(target)
	if err != nil {
		return nil, err
	}

	clusterList := &cloudv1.ClusterList{}
	if err := s.KubeClient.List(ctx, clusterList, client.MatchingLabels(scopeLabels)); err != nil {
		return nil, status.Errorf(codes.Internal, "list clusters in same GKE heterogeneous nodepool scope for gke shared nodepool peers: %s", err)
	}

	out := make([]*cloudv1.Cluster, 0, len(clusterList.Items)+1)
	seen := make(map[string]struct{}, len(clusterList.Items)+1)
	for i := range clusterList.Items {
		peer := &clusterList.Items[i]
		if !nodepoolutil.GKEClustersShareHeterogeneousNodePoolScope(target, peer) {
			continue
		}
		seen[peer.Namespace+"/"+peer.Name] = struct{}{}
		out = append(out, peer)
	}
	targetKey := target.Namespace + "/" + target.Name
	if _, ok := seen[targetKey]; !ok {
		out = append(out, target)
	}
	return out, nil
}
