package service

import (
	"context"
	"sort"
	"strings"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/emptypb"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	apiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-agent/v1"
	apicommon "github.com/tidbcloud/infra-provider/api/tidbcloud/common"
	"github.com/tidbcloud/infra-provider/pkg/consts"
)

func (s *AgentService) CreateGKEHeterogeneousComputeClass(ctx context.Context, req *apiv1.CreateGKEHeterogeneousComputeClassRequest) (*apiv1.GKEHeterogeneousComputeClass, error) {
	clusterID, err := s.validateGKEClusterAgentRequest(req.GetClusterId(), "gke heterogeneous APIs")
	if err != nil {
		return nil, err
	}
	component, err := resolveGKEHeterogeneousComputeClassComponent(req.GetComponentType())
	if err != nil {
		return nil, err
	}
	name, bindings, err := gkeComputeClassNameAndBindingsFromInput(req.GetNodePoolNames(), req.GetInstanceTypes())
	if err != nil {
		return nil, err
	}

	candidates, err := s.gkeComputeClassCandidatesForBindings(ctx, name, bindings)
	if err != nil {
		return nil, err
	}
	matched, err := validateGKEComputeClassCandidates(bindings, candidates)
	if err != nil {
		return nil, err
	}
	if matched != nil {
		return gkeHeterogeneousComputeClassResponse(clusterID, component, matched)
	}

	obj, err := buildGKEComputeClass(name, bindings)
	if err != nil {
		return nil, err
	}
	obj, err = createManagedUnstructured(ctx, s.dataplaneCli, obj, validateSharedManagedGKEHeterogeneousComputeClass)
	if err != nil {
		return nil, err
	}
	return gkeHeterogeneousComputeClassResponse(clusterID, component, obj)
}

func (s *AgentService) GetGKEHeterogeneousComputeClass(ctx context.Context, req *apiv1.GetGKEHeterogeneousComputeClassRequest) (*apiv1.GetGKEHeterogeneousComputeClassResponse, error) {
	clusterID, err := s.validateGKEClusterAgentRequest(req.GetClusterId(), "gke heterogeneous APIs")
	if err != nil {
		return nil, err
	}

	name := strings.TrimSpace(req.GetName())
	baseInstanceType := strings.TrimSpace(req.GetBaseInstanceType())
	extraInstanceType := strings.TrimSpace(req.GetHeterogeneousInstanceType())
	if name != "" {
		if baseInstanceType != "" || extraInstanceType != "" {
			return nil, status.Error(codes.InvalidArgument, "name cannot be used with instance type filters")
		}
		obj, err := getSharedManagedGKEHeterogeneousComputeClass(ctx, s.dataplaneCli, name)
		if err != nil {
			return nil, err
		}
		computeClass, err := gkeHeterogeneousComputeClassResponse(clusterID, nil, obj)
		if err != nil {
			return nil, err
		}
		return &apiv1.GetGKEHeterogeneousComputeClassResponse{ComputeClasses: []*apiv1.GKEHeterogeneousComputeClass{computeClass}}, nil
	}

	selector := gkeBindingScopedComputeClassLabels()
	if baseInstanceType != "" {
		selector[gkeHeterogeneousBaseInstanceTypeLabelKey] = baseInstanceType
	}
	if extraInstanceType != "" {
		selector[gkeHeterogeneousExtraInstanceTypeLabelKey] = extraInstanceType
	}
	candidates, err := s.listBindingScopedComputeClasses(ctx, selector)
	if err != nil {
		return nil, err
	}
	sort.Slice(candidates, func(i, j int) bool {
		return candidates[i].GetName() < candidates[j].GetName()
	})
	resp := &apiv1.GetGKEHeterogeneousComputeClassResponse{
		ComputeClasses: make([]*apiv1.GKEHeterogeneousComputeClass, 0, len(candidates)),
	}
	for _, candidate := range candidates {
		bindings, err := validateBindingScopedGKEComputeClass(candidate)
		if err != nil {
			return nil, err
		}
		computeClass, err := gkeHeterogeneousComputeClassResponseWithBindings(clusterID, nil, candidate, bindings)
		if err != nil {
			return nil, err
		}
		resp.ComputeClasses = append(resp.ComputeClasses, computeClass)
	}
	return resp, nil
}

func (s *AgentService) UpdateGKEHeterogeneousComputeClass(ctx context.Context, req *apiv1.UpdateGKEHeterogeneousComputeClassRequest) (*apiv1.GKEHeterogeneousComputeClass, error) {
	clusterID, err := s.validateGKEClusterAgentRequest(req.GetClusterId(), "gke heterogeneous APIs")
	if err != nil {
		return nil, err
	}
	obj, err := getSharedManagedGKEHeterogeneousComputeClass(ctx, s.dataplaneCli, req.GetName())
	if err != nil {
		return nil, err
	}

	if len(req.GetNodePoolNames()) == 0 {
		return gkeHeterogeneousComputeClassResponse(clusterID, nil, obj)
	}
	nodePoolNames, normalizeErr := cloudv1.NormalizeGKENodePoolNames(req.GetNodePoolNames())
	if normalizeErr != nil {
		return nil, status.Error(codes.InvalidArgument, normalizeErr.Error())
	}
	currentNodePoolNames, err := gkeComputeClassNodePoolNames(obj)
	if err != nil {
		return nil, err
	}
	if len(nodePoolNames) != len(currentNodePoolNames) {
		return nil, status.Errorf(codes.InvalidArgument, "node_pool_names has %d items, want %d to match existing compute class priorities", len(nodePoolNames), len(currentNodePoolNames))
	}
	for i := range nodePoolNames {
		if nodePoolNames[i] != currentNodePoolNames[i] {
			return nil, status.Errorf(codes.FailedPrecondition, "compute class %s bindings are immutable", obj.GetName())
		}
	}
	return gkeHeterogeneousComputeClassResponse(clusterID, nil, obj)
}

func (s *AgentService) DeleteGKEHeterogeneousComputeClass(ctx context.Context, req *apiv1.DeleteGKEHeterogeneousComputeClassRequest) (*emptypb.Empty, error) {
	if _, err := s.validateGKEClusterAgentRequest(req.GetClusterId(), "gke heterogeneous APIs"); err != nil {
		return nil, err
	}
	obj, err := getSharedManagedGKEHeterogeneousComputeClass(ctx, s.dataplaneCli, req.GetName())
	if err != nil {
		if status.Code(err) == codes.NotFound {
			return &emptypb.Empty{}, nil
		}
		return nil, err
	}
	if assignName, inUse, inUseErr := referencingGKEHeterogeneousGatekeeperAssign(ctx, s.dataplaneCli, req.GetName()); inUseErr != nil {
		return nil, inUseErr
	} else if inUse {
		return nil, status.Errorf(codes.FailedPrecondition, "compute class %s is still referenced by gatekeeper assign %s", req.GetName(), assignName)
	}
	if err := s.dataplaneCli.Delete(ctx, obj); err != nil && !apierrors.IsNotFound(err) {
		return nil, gkeHeterogeneousResourceClientError(gkeComputeClassLabel, err, "delete compute class")
	}
	return &emptypb.Empty{}, nil
}

func buildGKEComputeClass(name string, bindings []cloudv1.GKEComputeClassNodePoolBinding) (*unstructured.Unstructured, error) {
	labels, err := gkeSharedHeterogeneousComputeClassLabels(bindings)
	if err != nil {
		return nil, err
	}
	annotations, err := gkeSharedHeterogeneousComputeClassAnnotations(bindings)
	if err != nil {
		return nil, err
	}
	obj := &unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": gkeComputeClassGroup + "/" + gkeComputeClassVersion,
			"kind":       gkeComputeClassKind,
			"metadata": map[string]interface{}{
				"name": name,
			},
			"spec": map[string]interface{}{
				// GKE defaults this to false. Keep the desired spec explicit so
				// repeated create calls can compare cleanly against live objects.
				"nodePoolAutoCreation": map[string]interface{}{
					"enabled": false,
				},
				"priorities": gkeComputeClassPriorities(bindings),
				// Keep the API surface narrow for now: heterogeneous deploys only support
				// the conservative GKE behavior and do not accept per-request overrides.
				"whenUnsatisfiable": gkeWhenUnsatisfiableDoNotScaleUp,
			},
		},
	}
	obj.SetGroupVersionKind(gkeComputeClassGVK)
	obj.SetLabels(labels)
	obj.SetAnnotations(annotations)
	return obj, nil
}

func gkeComputeClassNameAndBindingsFromInput(nodePoolNames, instanceTypes []string) (string, []cloudv1.GKEComputeClassNodePoolBinding, error) {
	normalizedNodePoolNames, err := cloudv1.NormalizeGKENodePoolNames(nodePoolNames)
	if err != nil {
		return "", nil, status.Error(codes.InvalidArgument, err.Error())
	}
	if len(instanceTypes) != 2 {
		return "", nil, status.Errorf(codes.InvalidArgument, "instance_types must contain exactly 2 items")
	}
	if len(normalizedNodePoolNames) != len(instanceTypes) {
		return "", nil, status.Errorf(codes.InvalidArgument, "node_pool_names has %d items, want %d to match instance_types", len(normalizedNodePoolNames), len(instanceTypes))
	}
	bindings, err := cloudv1.NormalizeGKEComputeClassOrderedBindings([]cloudv1.GKEComputeClassNodePoolBinding{
		{
			Role:         cloudv1.GKEComputeClassNodePoolRoleBase,
			InstanceType: instanceTypes[0],
			NodePoolName: normalizedNodePoolNames[0],
		},
		{
			Role:         cloudv1.GKEComputeClassNodePoolRoleExtra,
			InstanceType: instanceTypes[1],
			NodePoolName: normalizedNodePoolNames[1],
		},
	})
	if err != nil {
		return "", nil, status.Error(codes.InvalidArgument, err.Error())
	}
	name, err := cloudv1.GKEComputeClassNameForBindings(bindings)
	if err != nil {
		return "", nil, status.Error(codes.InvalidArgument, err.Error())
	}
	return name, bindings, nil
}

func gkeSharedHeterogeneousComputeClassLabels(bindings []cloudv1.GKEComputeClassNodePoolBinding) (map[string]string, error) {
	labels := map[string]string{
		consts.ManagedByLabelKey:                  gkeHeterogeneousManagedByClusterAgent,
		gkeHeterogeneousDeployLabelKey:            gkeComputeClassDeployLabelValue,
		gkeHeterogeneousComputeClassScopeLabelKey: gkeComputeClassScopeBindingsLabelValue,
	}
	if len(bindings) != 2 {
		return nil, status.Errorf(codes.InvalidArgument, "compute class bindings must contain exactly 2 nodePools, got %d", len(bindings))
	}
	labels[gkeHeterogeneousBaseInstanceTypeLabelKey] = bindings[0].InstanceType
	labels[gkeHeterogeneousExtraInstanceTypeLabelKey] = bindings[1].InstanceType
	labels[gkeHeterogeneousBaseNodePoolLabelKey] = bindings[0].NodePoolName
	labels[gkeHeterogeneousExtraNodePoolLabelKey] = bindings[1].NodePoolName
	hash, err := cloudv1.GKEComputeClassBindingsHash(bindings)
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}
	labels[gkeHeterogeneousBindingsHashLabelKey] = hash
	return labels, nil
}

func gkeSharedHeterogeneousComputeClassAnnotations(bindings []cloudv1.GKEComputeClassNodePoolBinding) (map[string]string, error) {
	canonical, err := cloudv1.EncodeGKEComputeClassBindings(bindings)
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}
	return map[string]string{
		gkeHeterogeneousBindingsAnnotationKey: canonical,
	}, nil
}

func gkeComputeClassPriorities(bindings []cloudv1.GKEComputeClassNodePoolBinding) []interface{} {
	priorities := make([]interface{}, 0, len(bindings))
	for _, binding := range bindings {
		priorities = append(priorities, map[string]interface{}{
			"nodepools": []interface{}{binding.NodePoolName},
		})
	}
	return priorities
}

func isGKEHeterogeneousComputeClass(obj *unstructured.Unstructured) bool {
	if obj == nil {
		return false
	}
	labels := obj.GetLabels()
	return labels[consts.ManagedByLabelKey] == gkeHeterogeneousManagedByClusterAgent &&
		labels[gkeHeterogeneousDeployLabelKey] == gkeComputeClassDeployLabelValue &&
		labels[gkeHeterogeneousComputeClassScopeLabelKey] == gkeComputeClassScopeBindingsLabelValue &&
		strings.TrimSpace(labels[gkeHeterogeneousBindingsHashLabelKey]) != ""
}

func gkeComputeClassNodePoolNames(obj *unstructured.Unstructured) ([]string, error) {
	if obj == nil {
		return nil, status.Error(codes.FailedPrecondition, "compute class is nil")
	}
	priorities, found, err := unstructured.NestedSlice(obj.Object, "spec", "priorities")
	if err != nil {
		return nil, status.Errorf(codes.FailedPrecondition, "compute class %s has invalid priorities: %s", obj.GetName(), err)
	}
	if !found || len(priorities) == 0 {
		return nil, status.Errorf(codes.FailedPrecondition, "compute class %s is missing priorities", obj.GetName())
	}
	out := []string{}
	for i, priority := range priorities {
		priorityMap, ok := priority.(map[string]interface{})
		if !ok {
			return nil, status.Errorf(codes.FailedPrecondition, "compute class %s priority %d is invalid", obj.GetName(), i)
		}
		nodePools, found, err := unstructured.NestedStringSlice(priorityMap, "nodepools")
		if err != nil {
			return nil, status.Errorf(codes.FailedPrecondition, "compute class %s priority %d has invalid nodepools: %s", obj.GetName(), i, err)
		}
		if !found || len(nodePools) == 0 {
			return nil, status.Errorf(codes.FailedPrecondition, "compute class %s priority %d is missing nodepools", obj.GetName(), i)
		}
		out = append(out, nodePools...)
	}
	return out, nil
}

func getSharedManagedGKEHeterogeneousComputeClass(ctx context.Context, cli client.Client, name string) (*unstructured.Unstructured, error) {
	name, err := cloudv1.NormalizeGKEComputeClassName(name)
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}
	obj, err := getGKEHeterogeneousResource(ctx, cli, gkeComputeClassGVK, name, gkeComputeClassLabel)
	if err != nil {
		return nil, err
	}
	if _, err := validateBindingScopedGKEComputeClass(obj); err != nil {
		return nil, status.Errorf(codes.PermissionDenied, "compute class %s is not managed by binding-scoped gke heterogeneous deploy: %s", name, err)
	}
	return obj, nil
}

func (s *AgentService) gkeComputeClassCandidatesForBindings(ctx context.Context, name string, bindings []cloudv1.GKEComputeClassNodePoolBinding) ([]*unstructured.Unstructured, error) {
	candidates := map[string]*unstructured.Unstructured{}
	obj := &unstructured.Unstructured{}
	obj.SetGroupVersionKind(gkeComputeClassGVK)
	if err := s.dataplaneCli.Get(ctx, client.ObjectKey{Name: name}, obj); err != nil {
		if !apierrors.IsNotFound(err) {
			return nil, gkeHeterogeneousResourceClientError(gkeComputeClassLabel, err, "get compute class %s", name)
		}
	} else {
		candidates[obj.GetName()] = obj
	}
	items, err := s.listBindingScopedComputeClassesByAnyBindingIdentity(ctx, bindings)
	if err != nil {
		return nil, err
	}
	for _, obj := range items {
		candidates[obj.GetName()] = obj
	}
	out := make([]*unstructured.Unstructured, 0, len(candidates))
	for _, obj := range candidates {
		out = append(out, obj)
	}
	return out, nil
}

func (s *AgentService) listBindingScopedComputeClasses(ctx context.Context, selector map[string]string) ([]*unstructured.Unstructured, error) {
	items := &unstructured.UnstructuredList{}
	items.SetGroupVersionKind(gkeComputeClassListGVK)
	if err := s.dataplaneCli.List(ctx, items, client.MatchingLabels(selector)); err != nil {
		return nil, gkeHeterogeneousResourceClientError(gkeComputeClassLabel, err, "list compute class")
	}
	out := make([]*unstructured.Unstructured, 0, len(items.Items))
	for i := range items.Items {
		out = append(out, items.Items[i].DeepCopy())
	}
	return out, nil
}

func (s *AgentService) listBindingScopedComputeClassesByAnyBindingIdentity(ctx context.Context, bindings []cloudv1.GKEComputeClassNodePoolBinding) ([]*unstructured.Unstructured, error) {
	candidates := map[string]*unstructured.Unstructured{}
	identities, err := gkeComputeClassNodePoolBindingIdentities(bindings)
	if err != nil {
		return nil, err
	}
	// A machine type can be used by different nodepools, but a nodepool can
	// belong to only one compute class. Query/Get paths may still use instance
	// type labels as filters.
	for _, identity := range identities {
		selector := gkeBindingScopedComputeClassLabels(identity.labelKey, identity.labelValue)
		items := &unstructured.UnstructuredList{}
		items.SetGroupVersionKind(gkeComputeClassListGVK)
		if err := s.dataplaneCli.List(ctx, items, client.MatchingLabels(selector)); err != nil {
			return nil, gkeHeterogeneousResourceClientError(gkeComputeClassLabel, err, "list compute class")
		}
		for i := range items.Items {
			obj := items.Items[i].DeepCopy()
			candidates[obj.GetName()] = obj
		}
	}
	out := make([]*unstructured.Unstructured, 0, len(candidates))
	for _, obj := range candidates {
		out = append(out, obj)
	}
	return out, nil
}

type gkeComputeClassBindingIdentity struct {
	labelKey   string
	labelValue string
}

func gkeComputeClassNodePoolBindingIdentities(bindings []cloudv1.GKEComputeClassNodePoolBinding) ([]gkeComputeClassBindingIdentity, error) {
	if len(bindings) != 2 {
		return nil, status.Errorf(codes.InvalidArgument, "compute class bindings must contain exactly 2 nodePools, got %d", len(bindings))
	}
	return []gkeComputeClassBindingIdentity{
		{labelKey: gkeHeterogeneousBaseNodePoolLabelKey, labelValue: bindings[0].NodePoolName},
		{labelKey: gkeHeterogeneousExtraNodePoolLabelKey, labelValue: bindings[0].NodePoolName},
		{labelKey: gkeHeterogeneousBaseNodePoolLabelKey, labelValue: bindings[1].NodePoolName},
		{labelKey: gkeHeterogeneousExtraNodePoolLabelKey, labelValue: bindings[1].NodePoolName},
	}, nil
}

func gkeBindingScopedComputeClassLabels(keyValue ...string) map[string]string {
	labels := map[string]string{
		consts.ManagedByLabelKey:                  gkeHeterogeneousManagedByClusterAgent,
		gkeHeterogeneousDeployLabelKey:            gkeComputeClassDeployLabelValue,
		gkeHeterogeneousComputeClassScopeLabelKey: gkeComputeClassScopeBindingsLabelValue,
	}
	for i := 0; i+1 < len(keyValue); i += 2 {
		labels[keyValue[i]] = strings.TrimSpace(keyValue[i+1])
	}
	return labels
}

func validateGKEComputeClassCandidates(desired []cloudv1.GKEComputeClassNodePoolBinding, candidates []*unstructured.Unstructured) (*unstructured.Unstructured, error) {
	var matched *unstructured.Unstructured
	for _, candidate := range candidates {
		existing, err := validateBindingScopedGKEComputeClass(candidate)
		if err != nil {
			return nil, status.Errorf(codes.AlreadyExists, "compute class %s already exists and is not a valid binding-scoped gke heterogeneous compute class: %s", candidate.GetName(), err)
		}
		if !cloudv1.GKEComputeClassBindingsEqual(existing, desired) {
			return nil, status.Errorf(
				codes.AlreadyExists,
				"compute class %s already uses nodepools %s/%s with different bindings",
				candidate.GetName(),
				existing[0].NodePoolName,
				existing[1].NodePoolName,
			)
		}
		matched = candidate
	}
	return matched, nil
}

func validateBindingScopedGKEComputeClass(obj *unstructured.Unstructured) ([]cloudv1.GKEComputeClassNodePoolBinding, error) {
	if obj == nil {
		return nil, status.Error(codes.FailedPrecondition, "compute class is nil")
	}
	if !isGKEHeterogeneousComputeClass(obj) {
		return nil, status.Errorf(codes.FailedPrecondition, "compute class %s is not a binding-scoped gke heterogeneous compute class", obj.GetName())
	}
	raw := strings.TrimSpace(obj.GetAnnotations()[gkeHeterogeneousBindingsAnnotationKey])
	if raw == "" {
		return nil, status.Errorf(codes.FailedPrecondition, "compute class %s is missing bindings annotation", obj.GetName())
	}
	bindings, err := cloudv1.DecodeGKEComputeClassBindings(raw)
	if err != nil {
		return nil, status.Errorf(codes.FailedPrecondition, "compute class %s has invalid bindings annotation: %s", obj.GetName(), err)
	}
	if len(bindings) != 2 {
		return nil, status.Errorf(codes.FailedPrecondition, "compute class %s bindings must contain exactly 2 nodePools, got %d", obj.GetName(), len(bindings))
	}
	hash, err := cloudv1.GKEComputeClassBindingsHash(bindings)
	if err != nil {
		return nil, status.Errorf(codes.FailedPrecondition, "compute class %s has invalid bindings: %s", obj.GetName(), err)
	}
	labels := obj.GetLabels()
	for _, check := range []struct {
		key  string
		want string
		desc string
	}{
		{key: gkeHeterogeneousBindingsHashLabelKey, want: hash, desc: "bindings hash label"},
		{key: gkeHeterogeneousBaseInstanceTypeLabelKey, want: bindings[0].InstanceType, desc: "base instance type label"},
		{key: gkeHeterogeneousExtraInstanceTypeLabelKey, want: bindings[1].InstanceType, desc: "heterogeneous instance type label"},
		{key: gkeHeterogeneousBaseNodePoolLabelKey, want: bindings[0].NodePoolName, desc: "base nodepool label"},
		{key: gkeHeterogeneousExtraNodePoolLabelKey, want: bindings[1].NodePoolName, desc: "heterogeneous nodepool label"},
	} {
		if strings.TrimSpace(labels[check.key]) != check.want {
			return nil, status.Errorf(codes.FailedPrecondition, "compute class %s %s does not match bindings annotation", obj.GetName(), check.desc)
		}
	}
	expectedName, err := cloudv1.GKEComputeClassNameForBindings(bindings)
	if err != nil {
		return nil, status.Errorf(codes.FailedPrecondition, "compute class %s has invalid bindings: %s", obj.GetName(), err)
	}
	if obj.GetName() != expectedName {
		return nil, status.Errorf(codes.FailedPrecondition, "compute class %s name does not match bindings hash %s", obj.GetName(), hash)
	}
	nodePoolNames, err := gkeComputeClassNodePoolNames(obj)
	if err != nil {
		return nil, err
	}
	if len(nodePoolNames) != 2 || nodePoolNames[0] != bindings[0].NodePoolName || nodePoolNames[1] != bindings[1].NodePoolName {
		return nil, status.Errorf(codes.FailedPrecondition, "compute class %s priorities do not match bindings annotation", obj.GetName())
	}
	nodePoolAutoCreationEnabled, found, err := unstructured.NestedBool(obj.Object, "spec", "nodePoolAutoCreation", "enabled")
	if err != nil {
		return nil, status.Errorf(codes.FailedPrecondition, "compute class %s has invalid nodePoolAutoCreation.enabled: %s", obj.GetName(), err)
	}
	if found && nodePoolAutoCreationEnabled {
		return nil, status.Errorf(codes.FailedPrecondition, "compute class %s nodePoolAutoCreation must be disabled", obj.GetName())
	}
	whenUnsatisfiable, found, err := unstructured.NestedString(obj.Object, "spec", "whenUnsatisfiable")
	if err != nil {
		return nil, status.Errorf(codes.FailedPrecondition, "compute class %s has invalid whenUnsatisfiable: %s", obj.GetName(), err)
	}
	if found && whenUnsatisfiable != gkeWhenUnsatisfiableDoNotScaleUp {
		return nil, status.Errorf(codes.FailedPrecondition, "compute class %s whenUnsatisfiable must be %s", obj.GetName(), gkeWhenUnsatisfiableDoNotScaleUp)
	}
	return bindings, nil
}

func validateSharedManagedGKEHeterogeneousComputeClass(current, desired *unstructured.Unstructured) error {
	currentBindings, err := validateBindingScopedGKEComputeClass(current)
	if err != nil {
		return status.Errorf(codes.AlreadyExists, "%s %s already exists and is not a valid binding-scoped gke heterogeneous compute class: %s", desired.GetKind(), desired.GetName(), err)
	}
	desiredBindings, err := validateBindingScopedGKEComputeClass(desired)
	if err != nil {
		return status.Errorf(codes.Internal, "desired %s %s has invalid compute class bindings: %s", desired.GetKind(), desired.GetName(), err)
	}
	if !cloudv1.GKEComputeClassBindingsEqual(currentBindings, desiredBindings) {
		return status.Errorf(codes.AlreadyExists, "%s %s already exists with different compute class bindings", desired.GetKind(), desired.GetName())
	}
	return nil
}

// referencingGKEHeterogeneousGatekeeperAssign returns one Assign that still
// points at this shared ComputeClass. A single reference is enough to block
// deletion; callers do not need the full list here.
func referencingGKEHeterogeneousGatekeeperAssign(ctx context.Context, cli client.Client, computeClassName string) (string, bool, error) {
	computeClassName = strings.TrimSpace(computeClassName)
	assigns := &unstructured.UnstructuredList{}
	assigns.SetGroupVersionKind(gatekeeperAssignListGVK)
	if err := cli.List(ctx, assigns, client.MatchingLabels{gkeHeterogeneousComputeClassLabelKey: computeClassName}); err != nil {
		return "", false, gkeHeterogeneousResourceClientError(gatekeeperAssignLabel, err, "list gatekeeper assigns")
	}
	if len(assigns.Items) > 0 {
		return assigns.Items[0].GetName(), true, nil
	}
	return "", false, nil
}

func gkeHeterogeneousComputeClassResponse(clusterID string, component *gkeHeterogeneousDataplaneComponent, obj *unstructured.Unstructured) (*apiv1.GKEHeterogeneousComputeClass, error) {
	bindings, err := validateBindingScopedGKEComputeClass(obj)
	if err != nil {
		return nil, err
	}
	return gkeHeterogeneousComputeClassResponseWithBindings(clusterID, component, obj, bindings)
}

func gkeHeterogeneousComputeClassResponseWithBindings(clusterID string, component *gkeHeterogeneousDataplaneComponent, obj *unstructured.Unstructured, bindings []cloudv1.GKEComputeClassNodePoolBinding) (*apiv1.GKEHeterogeneousComputeClass, error) {
	componentType := apicommon.ComponentType_COMPONENT_TYPE_UNSPECIFIED
	if component != nil {
		componentType = gkeHeterogeneousComponentType(component.name())
	}
	response := &apiv1.GKEHeterogeneousComputeClass{
		ClusterId:     clusterID,
		Name:          obj.GetName(),
		ComponentType: componentType,
	}
	whenUnsatisfiable, found, err := unstructured.NestedString(obj.Object, "spec", "whenUnsatisfiable")
	if err != nil {
		return nil, status.Errorf(codes.FailedPrecondition, "compute class %s has invalid whenUnsatisfiable: %s", obj.GetName(), err)
	}
	if found {
		response.WhenUnsatisfiable = whenUnsatisfiable
	}
	response.NodePoolNames = gkeComputeClassBindingNodePoolNames(bindings)
	response.InstanceTypes = gkeComputeClassBindingInstanceTypes(bindings)
	return response, nil
}

func gkeComputeClassBindingNodePoolNames(bindings []cloudv1.GKEComputeClassNodePoolBinding) []string {
	out := make([]string, 0, len(bindings))
	for _, binding := range bindings {
		out = append(out, binding.NodePoolName)
	}
	return out
}

func gkeComputeClassBindingInstanceTypes(bindings []cloudv1.GKEComputeClassNodePoolBinding) []string {
	out := make([]string, 0, len(bindings))
	for _, binding := range bindings {
		out = append(out, binding.InstanceType)
	}
	return out
}
