package gkeheterogeneous

import (
	"fmt"
	"sort"
	"strings"

	gkev1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

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

type ComputeClassIntentRef struct {
	ClusterName      string
	ClusterNamespace string
	ComponentType    apicommon.ComponentType
	TiDBGroupID      string
	ComputeClassName string
}

type ComputeClassNodePoolRef struct {
	NodePoolName              string
	DesiredComputeClassName   string
	AppliedComputeClassName   string
	SpecLabelComputeClassName string
	SpecTaintComputeClassName string
}

func ComputeClassIntentRefs(peers []*cloudv1.Cluster, computeClassName string) ([]ComputeClassIntentRef, error) {
	computeClassName = strings.TrimSpace(computeClassName)
	if computeClassName == "" {
		return nil, status.Error(codes.InvalidArgument, "compute_class_name is required")
	}

	out := make([]ComputeClassIntentRef, 0)
	seen := map[string]struct{}{}
	for _, peer := range peers {
		refs, err := computeClassIntentRefsForCluster(peer, computeClassName)
		if err != nil {
			return nil, status.Errorf(
				codes.FailedPrecondition,
				"cannot inspect gke heterogeneous deploy intent for cluster %s/%s: %s",
				peer.Namespace,
				peer.Name,
				status.Convert(err).Message(),
			)
		}
		for _, ref := range refs {
			key := strings.Join([]string{ref.ClusterID(), ref.ComponentType.String(), strings.TrimSpace(ref.TiDBGroupID)}, "\x00")
			if _, ok := seen[key]; ok {
				continue
			}
			seen[key] = struct{}{}
			out = append(out, ref)
		}
	}
	return out, nil
}

func computeClassIntentRefsForCluster(cluster *cloudv1.Cluster, computeClassName string) ([]ComputeClassIntentRef, error) {
	if cluster == nil || cluster.GetProviderType() != cloudv1.ProviderTypeGKE {
		return nil, nil
	}
	out := make([]ComputeClassIntentRef, 0)
	appendRefs := func(componentType apicommon.ComponentType, tidbGroupID, annotationKey, baseInstanceType string) error {
		raw := strings.TrimSpace(cluster.GetAnnotations()[annotationKey])
		if raw == "" {
			return nil
		}

		if computeClassName != "" {
			// Delete guards fail closed on malformed in-scope intent because we cannot
			// prove it is unrelated. Well-formed intent for other ComputeClasses can be
			// skipped without full binding validation.
			light, err := cloudv1.DecodeGKEComputeClassBindingAnnotation(raw)
			if err != nil {
				return status.Errorf(codes.FailedPrecondition, "decode gke compute class binding annotation %s: %s", annotationKey, err)
			}
			if strings.TrimSpace(light.ComputeClassName) != computeClassName {
				return nil
			}
		}

		baseInstanceType = strings.TrimSpace(baseInstanceType)
		if baseInstanceType == "" {
			return status.Error(codes.FailedPrecondition, "base instance type is empty")
		}
		annotation, explicit, err := cloudv1.ClusterGKEComputeClassBindingAnnotation(
			cluster,
			annotationKey,
			baseInstanceType,
			NodePoolName(cluster, baseInstanceType),
		)
		if err != nil {
			return status.Error(codes.FailedPrecondition, err.Error())
		}
		if !explicit {
			return nil
		}
		out = append(out, ComputeClassIntentRef{
			ClusterName:      cluster.Name,
			ClusterNamespace: cluster.Namespace,
			ComponentType:    componentType,
			TiDBGroupID:      strings.TrimSpace(tidbGroupID),
			ComputeClassName: strings.TrimSpace(annotation.ComputeClassName),
		})
		return nil
	}
	if cluster.TiKVGKEComputeClassIntentConfigured() {
		if err := appendRefs(apicommon.ComponentType_TIKV, "", cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, cluster.Spec.Cluster.TiKV.InstanceType); err != nil {
			return nil, err
		}
	}
	for _, groupID := range ConfiguredTiDBGroupIDs(cluster) {
		if !TiDBGroupExists(cluster, groupID) || !cluster.TiDBGKEComputeClassIntentConfigured(groupID) {
			continue
		}
		if err := appendRefs(apicommon.ComponentType_TIDB, groupID, cluster.TiDBGKEComputeClassBindingsAnnoKey(groupID), cluster.TiDBInstanceTypeForGroup(groupID)); err != nil {
			return nil, err
		}
	}
	return out, nil
}

func ConfiguredTiDBGroupIDs(cluster *cloudv1.Cluster) []string {
	if cluster == nil {
		return nil
	}
	refs := cluster.ConfiguredTiDBComputeClassAnnotationRefs()
	out := make([]string, 0, len(refs))
	seen := map[string]struct{}{}
	for _, ref := range refs {
		groupID := strings.TrimSpace(ref.TiDBGroupID)
		if _, ok := seen[groupID]; ok {
			continue
		}
		seen[groupID] = struct{}{}
		out = append(out, groupID)
	}
	return out
}

func ActiveTiDBGroupIDs(cluster *cloudv1.Cluster) []string {
	configured := ConfiguredTiDBGroupIDs(cluster)
	out := make([]string, 0, len(configured))
	for _, groupID := range configured {
		if TiDBGroupExists(cluster, groupID) {
			out = append(out, groupID)
		}
	}
	return out
}

func TiDBGroupExists(cluster *cloudv1.Cluster, groupID string) bool {
	if cluster == nil {
		return false
	}
	component, err := cluster.GetComponentSpecInGroup(strings.TrimSpace(groupID), cloudv1.ComponentTiDB)
	return err == nil && component != nil
}

func NodePoolName(cluster *cloudv1.Cluster, instanceType string) string {
	return namer.GKEHeterogeneousNodePoolNameForCluster(cluster, instanceType)
}

func ResourceName(clusterID string, componentType cloudv1.ComponentType, tidbGroupID, suffix string) string {
	identity := strings.TrimSpace(clusterID)
	if componentType == cloudv1.ComponentTiDB && strings.TrimSpace(tidbGroupID) != "" {
		identity += "-" + strings.TrimSpace(tidbGroupID)
	}
	componentName := string(componentType)
	return namer.NewName(cloudv1.NormalizeGKEResourceNameIdentity(identity), strings.TrimSpace(componentName), strings.TrimSpace(suffix), 63)
}

func NodePoolComputeClassRefs(nodePools []gkev1beta1.NodePool, computeClassName string) []ComputeClassNodePoolRef {
	computeClassName = strings.TrimSpace(computeClassName)
	if computeClassName == "" {
		return nil
	}

	out := make([]ComputeClassNodePoolRef, 0)
	for i := range nodePools {
		nodePool := &nodePools[i]
		desired := strings.TrimSpace(nodepoolutil.GKEDesiredComputeClassNameForNodePool(nodePool))
		applied := strings.TrimSpace(nodepoolutil.GKEAppliedComputeClassNameForNodePool(nodePool))
		specLabel := strings.TrimSpace(nodepoolutil.GKEComputeClassSpecLabelForNodePool(nodePool))
		specTaint := strings.TrimSpace(nodepoolutil.GKEComputeClassSpecTaintForNodePool(nodePool))
		if desired != computeClassName && applied != computeClassName && specLabel != computeClassName && specTaint != computeClassName {
			continue
		}
		out = append(out, ComputeClassNodePoolRef{
			NodePoolName:              nodePool.GetName(),
			DesiredComputeClassName:   desired,
			AppliedComputeClassName:   applied,
			SpecLabelComputeClassName: specLabel,
			SpecTaintComputeClassName: specTaint,
		})
	}
	return out
}

func NodePoolReadyForGatekeeper(nodePool *gkev1beta1.NodePool, expectedComputeClass string) (bool, error) {
	if nodePool == nil {
		return false, status.Error(codes.Internal, "nodepool is nil")
	}
	expectedComputeClass = strings.TrimSpace(expectedComputeClass)
	if expectedComputeClass == "" {
		return true, nil
	}

	desired := strings.TrimSpace(nodepoolutil.GKEDesiredComputeClassNameForNodePool(nodePool))
	if desired == "" {
		return false, nil
	}
	if desired != expectedComputeClass {
		return false, status.Errorf(codes.FailedPrecondition, "nodepool %s is bound to desired compute class %s, want %s", nodePool.GetName(), desired, expectedComputeClass)
	}

	applied := strings.TrimSpace(nodepoolutil.GKEAppliedComputeClassNameForNodePool(nodePool))
	if applied == "" {
		return false, nil
	}
	if applied != expectedComputeClass {
		return false, status.Errorf(codes.FailedPrecondition, "nodepool %s is bound to applied compute class %s, want %s", nodePool.GetName(), applied, expectedComputeClass)
	}

	specLabel := strings.TrimSpace(nodepoolutil.GKEComputeClassSpecLabelForNodePool(nodePool))
	if specLabel == "" {
		return false, nil
	}
	if specLabel != expectedComputeClass {
		return false, status.Errorf(codes.FailedPrecondition, "nodepool %s has compute class spec label %s, want %s", nodePool.GetName(), specLabel, expectedComputeClass)
	}

	specTaint := strings.TrimSpace(nodepoolutil.GKEComputeClassSpecTaintForNodePool(nodePool))
	if specTaint == "" {
		return false, nil
	}
	if specTaint != expectedComputeClass {
		return false, status.Errorf(codes.FailedPrecondition, "nodepool %s has compute class spec taint %s, want %s", nodePool.GetName(), specTaint, expectedComputeClass)
	}
	if nodepoolutil.HasGKEHeterogeneousNodePoolIsolationTaint(nodePool) {
		return false, nil
	}

	return crossplane.IsReadyAndSynced(nodePool.Status.Conditions), nil
}

func FormatComputeClassIntentRefs(refs []ComputeClassIntentRef) string {
	parts := make([]string, 0, len(refs))
	for _, ref := range refs {
		parts = append(parts, fmt.Sprintf(
			"cluster=%s, component=%s, tidb_group_id=%s",
			ref.ClusterID(),
			ref.ComponentType,
			ref.GroupIDForMessage(),
		))
	}
	sort.Strings(parts)
	return strings.Join(parts, "; ")
}

func FormatComputeClassNodePoolRefs(refs []ComputeClassNodePoolRef) string {
	parts := make([]string, 0, len(refs))
	for _, ref := range refs {
		desired := strings.TrimSpace(ref.DesiredComputeClassName)
		if desired == "" {
			desired = "<empty>"
		}
		applied := strings.TrimSpace(ref.AppliedComputeClassName)
		if applied == "" {
			applied = "<empty>"
		}
		specLabel := strings.TrimSpace(ref.SpecLabelComputeClassName)
		if specLabel == "" {
			specLabel = "<empty>"
		}
		specTaint := strings.TrimSpace(ref.SpecTaintComputeClassName)
		if specTaint == "" {
			specTaint = "<empty>"
		}
		parts = append(parts, fmt.Sprintf(
			"nodepool=%s, desired=%s, applied=%s, spec_label=%s, spec_taint=%s",
			ref.NodePoolName,
			desired,
			applied,
			specLabel,
			specTaint,
		))
	}
	sort.Strings(parts)
	return strings.Join(parts, "; ")
}

func (r ComputeClassIntentRef) ClusterID() string {
	if strings.TrimSpace(r.ClusterNamespace) == "" {
		return r.ClusterName
	}
	return r.ClusterNamespace + "/" + r.ClusterName
}

func (r ComputeClassIntentRef) GroupIDForMessage() string {
	if strings.TrimSpace(r.TiDBGroupID) == "" {
		if r.ComponentType == apicommon.ComponentType_TIDB {
			return "<default>"
		}
		return "<none>"
	}
	return r.TiDBGroupID
}
