package server

import (
	"context"
	"fmt"
	"sort"
	"strings"

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

type gkeHeterogeneousDeployIntentRef struct {
	gkeHeterogeneousIntentLocation
	nodePoolName     string
	instanceType     string
	computeClassName string
	role             string
}

func (s *ClusterServer) validateGKEHeterogeneousDeployIntent(ctx context.Context, original, updated *cloudv1.Cluster) error {
	return s.validateSharedGKEHeterogeneousNodePoolIntentConflicts(ctx, original, updated)
}

func (s *ClusterServer) validateSharedGKEHeterogeneousNodePoolIntentConflicts(ctx context.Context, original, updated *cloudv1.Cluster) error {
	if updated == nil || updated.GetProviderType() != cloudv1.ProviderTypeGKE {
		return nil
	}
	clusters, err := s.listGKEClustersInCandidateKubernetesClusterScopesForSharedNodePoolIntentValidation(ctx, original, updated)
	if err != nil {
		return err
	}

	refs, err := collectGKEHeterogeneousSharedNodePoolIntentRefs(original, updated, clusters)
	if err != nil {
		return err
	}
	if err := validateSharedGKEHeterogeneousNodePoolIntentRefConflicts(refs); err != nil {
		return err
	}
	return nil
}

func collectGKEHeterogeneousSharedNodePoolIntentRefs(original, updated *cloudv1.Cluster, clusters []*cloudv1.Cluster) ([]gkeHeterogeneousDeployIntentRef, error) {
	var out []gkeHeterogeneousDeployIntentRef
	for _, cluster := range clusters {
		refs, err := gkeHeterogeneousDeployIntentRefsInSharedScope(original, updated, cluster)
		if err != nil {
			return nil, err
		}
		if cluster == nil || refs == nil {
			continue
		}
		out = append(out, refs...)
	}
	return out, nil
}

func gkeHeterogeneousDeployIntentRefsInSharedScope(original, updated, cluster *cloudv1.Cluster) ([]gkeHeterogeneousDeployIntentRef, error) {
	if cluster == nil {
		return nil, nil
	}
	sharesUpdatedScope := nodepoolutil.GKEClustersShareHeterogeneousNodePoolScope(updated, cluster)
	sharesOriginalScope := original != nil && nodepoolutil.GKEClustersShareHeterogeneousNodePoolScope(original, cluster)
	if !sharesUpdatedScope && !sharesOriginalScope {
		return nil, nil
	}
	refs, err := gkeHeterogeneousDeployIntentRefsForCluster(cluster)
	if err != nil {
		return nil, status.Errorf(
			codes.FailedPrecondition,
			"cannot inspect gke heterogeneous deploy intent for cluster %s/%s: %s",
			cluster.Namespace,
			cluster.Name,
			status.Convert(err).Message(),
		)
	}
	return refs, nil
}

func validateSharedGKEHeterogeneousNodePoolIntentRefConflicts(refs []gkeHeterogeneousDeployIntentRef) error {
	byNodePool := map[string]gkeHeterogeneousDeployIntentRef{}
	for _, ref := range refs {
		current, ok := byNodePool[ref.nodePoolName]
		if !ok {
			byNodePool[ref.nodePoolName] = ref
			continue
		}
		if current.instanceType != ref.instanceType {
			return status.Errorf(
				codes.FailedPrecondition,
				"gke shared nodepool %s has conflicting instance type %s from %s and %s from %s",
				ref.nodePoolName,
				current.instanceType,
				current.locationForMessage(),
				ref.instanceType,
				ref.locationForMessage(),
			)
		}
		if current.computeClassName != ref.computeClassName {
			return status.Errorf(
				codes.FailedPrecondition,
				"gke shared nodepool %s has conflicting compute class %s from %s and %s from %s",
				ref.nodePoolName,
				current.computeClassName,
				current.locationForMessage(),
				ref.computeClassName,
				ref.locationForMessage(),
			)
		}
		if current.role != ref.role {
			return status.Errorf(
				codes.FailedPrecondition,
				"gke shared nodepool %s has conflicting role %s from %s and %s from %s",
				ref.nodePoolName,
				current.role,
				current.locationForMessage(),
				ref.role,
				ref.locationForMessage(),
			)
		}
	}
	return nil
}

func (s *ClusterServer) listGKEClustersInCandidateKubernetesClusterScopesForSharedNodePoolIntentValidation(ctx context.Context, original, updated *cloudv1.Cluster) ([]*cloudv1.Cluster, error) {
	peersByKey := map[string]*cloudv1.Cluster{}
	addPeers := func(peers []*cloudv1.Cluster) {
		for _, peer := range peers {
			key := gkeHeterogeneousClusterIdentityKey(peer)
			if key == "" {
				continue
			}
			peersByKey[key] = peer
		}
	}

	if original != nil {
		peers, err := s.listGKEClustersInSameKubernetesClusterScopeForSharedNodePool(ctx, original)
		if err != nil {
			return nil, err
		}
		addPeers(peers)
	}
	peers, err := s.listGKEClustersInSameKubernetesClusterScopeForSharedNodePool(ctx, updated)
	if err != nil {
		return nil, err
	}
	addPeers(peers)

	// The updated Cluster object may not have been written yet, and the
	// heterogeneous nodepool scope can differ between original and updated
	// during validation. Use the union of both bounded peer sets, then validate
	// the candidate object itself.
	peersByKey[gkeHeterogeneousClusterIdentityKey(updated)] = updated
	if original != nil && !sameGKEHeterogeneousClusterIdentity(original, updated) {
		delete(peersByKey, gkeHeterogeneousClusterIdentityKey(original))
	}

	keys := make([]string, 0, len(peersByKey))
	for key := range peersByKey {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	out := make([]*cloudv1.Cluster, 0, len(keys))
	for _, key := range keys {
		out = append(out, peersByKey[key])
	}
	return out, nil
}

func gkeHeterogeneousDeployIntentRefsForCluster(cluster *cloudv1.Cluster) ([]gkeHeterogeneousDeployIntentRef, error) {
	if cluster == nil || cluster.GetProviderType() != cloudv1.ProviderTypeGKE {
		return nil, nil
	}
	out := make([]gkeHeterogeneousDeployIntentRef, 0)
	if err := forEachConfiguredGKEHeterogeneousIntent(cluster, func(intentConfig gkeHeterogeneousIntentConfig, resolvedGroupID string) error {
		state, err := loadGKEHeterogeneousNodePoolBindingState(cluster, intentConfig, resolvedGroupID)
		if err != nil {
			return err
		}
		for _, binding := range state.ExplicitBindings() {
			out = append(out, gkeHeterogeneousDeployIntentRef{
				gkeHeterogeneousIntentLocation: gkeHeterogeneousIntentLocation{
					clusterName:      cluster.Name,
					clusterNamespace: cluster.Namespace,
					componentType:    intentConfig.componentType,
					tidbGroupID:      resolvedGroupID,
				},
				nodePoolName:     strings.TrimSpace(binding.NodePoolName),
				instanceType:     strings.TrimSpace(binding.InstanceType),
				computeClassName: strings.TrimSpace(state.computeClassName),
				role:             strings.TrimSpace(binding.Role),
			})
		}
		return nil
	}); err != nil {
		return nil, err
	}
	return out, nil
}

func sameGKEHeterogeneousClusterIdentity(a, b *cloudv1.Cluster) bool {
	if a == nil || b == nil {
		return false
	}
	return a.Namespace == b.Namespace && a.Name == b.Name
}

func gkeHeterogeneousClusterIdentityKey(cluster *cloudv1.Cluster) string {
	if cluster == nil {
		return ""
	}
	return cluster.Namespace + "/" + cluster.Name
}

func (r gkeHeterogeneousIntentLocation) clusterID() string {
	if r.clusterNamespace == "" {
		return r.clusterName
	}
	return r.clusterNamespace + "/" + r.clusterName
}

func (r gkeHeterogeneousIntentLocation) groupIDForMessage() string {
	if strings.TrimSpace(r.tidbGroupID) == "" {
		if r.componentType == apicommon.ComponentType_TIDB {
			return "<default>"
		}
		return "<none>"
	}
	return r.tidbGroupID
}

func (r gkeHeterogeneousDeployIntentRef) locationForMessage() string {
	return fmt.Sprintf(
		"cluster %s component %s group %s computeClass %s",
		r.clusterID(),
		r.componentType,
		r.groupIDForMessage(),
		r.computeClassName,
	)
}
