package server

import (
	"context"
	"strings"

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

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

func (s *ClusterServer) GetGKEHeterogeneousDeploy(ctx context.Context, req *apiv1.GetGKEHeterogeneousDeployRequest) (*apiv1.GKEHeterogeneousDeploy, error) {
	cluster, err := s.gkeHeterogeneousCluster(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	return s.gkeHeterogeneousDeploy(ctx, cluster, req.GetComponentType(), req.GetTidbGroupId())
}

func (s *ClusterServer) CreateGKEHeterogeneousDeploy(ctx context.Context, req *apiv1.CreateGKEHeterogeneousDeployRequest) (*apiv1.GKEHeterogeneousDeploy, error) {
	cluster, err := s.gkeHeterogeneousCluster(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	intentConfig, tidbGroupID, err := resolveGKEHeterogeneousIntent(cluster, req.GetComponentType(), req.GetTidbGroupId())
	if err != nil {
		return nil, err
	}
	if gkeHeterogeneousIntentConfigured(cluster, intentConfig, tidbGroupID) {
		return nil, status.Errorf(codes.AlreadyExists, "gke heterogeneous deploy for %s is already enabled", req.GetComponentType())
	}
	return s.setGKEHeterogeneousIntent(ctx, cluster, intentConfig, tidbGroupID, req.GetComputeClassName(), req.GetNodePoolNames(), req.GetInstanceTypes())
}

func (s *ClusterServer) UpdateGKEHeterogeneousDeploy(ctx context.Context, req *apiv1.UpdateGKEHeterogeneousDeployRequest) (*apiv1.GKEHeterogeneousDeploy, error) {
	cluster, err := s.gkeHeterogeneousCluster(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	intentConfig, tidbGroupID, err := resolveGKEHeterogeneousIntent(cluster, req.GetComponentType(), req.GetTidbGroupId())
	if err != nil {
		return nil, err
	}
	if !gkeHeterogeneousIntentConfigured(cluster, intentConfig, tidbGroupID) {
		return nil, status.Errorf(codes.FailedPrecondition, "gke heterogeneous deploy for %s is not enabled", req.GetComponentType())
	}
	return s.setGKEHeterogeneousIntent(ctx, cluster, intentConfig, tidbGroupID, req.GetComputeClassName(), req.GetNodePoolNames(), req.GetInstanceTypes())
}

func (s *ClusterServer) DeleteGKEHeterogeneousDeploy(ctx context.Context, req *apiv1.DeleteGKEHeterogeneousDeployRequest) (*emptypb.Empty, error) {
	cluster, err := s.gkeHeterogeneousCluster(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	intentConfig, tidbGroupID, err := resolveGKEHeterogeneousIntentForDelete(cluster, req.GetComponentType(), req.GetTidbGroupId())
	if err != nil {
		return nil, err
	}
	if err := s.clearGKEHeterogeneousComputeClassIntent(ctx, cluster, intentConfig, tidbGroupID); err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

func (s *ClusterServer) ListGKEHeterogeneousDeploys(ctx context.Context, req *apiv1.ListGKEHeterogeneousDeploysRequest) (*apiv1.ListGKEHeterogeneousDeploysResponse, error) {
	cluster, err := s.gkeHeterogeneousCluster(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	deploys := make([]*apiv1.GKEHeterogeneousDeploy, 0, len(cluster.GetExtraGroups())+2)
	tikvDeploy, err := s.gkeHeterogeneousDeploy(ctx, cluster, apicommon.ComponentType_TIKV, "")
	if err != nil {
		return nil, err
	}
	deploys = append(deploys, tikvDeploy)
	tidbGroupIDs, err := listGKEHeterogeneousTiDBGroupIDs(cluster)
	if err != nil {
		return nil, err
	}
	for _, groupID := range tidbGroupIDs {
		deploy, deployErr := s.gkeHeterogeneousDeploy(ctx, cluster, apicommon.ComponentType_TIDB, groupID)
		if deployErr != nil {
			return nil, deployErr
		}
		deploys = append(deploys, deploy)
	}
	return &apiv1.ListGKEHeterogeneousDeploysResponse{Deploys: deploys}, nil
}

func listGKEHeterogeneousTiDBGroupIDs(cluster *cloudv1.Cluster) ([]string, error) {
	if cluster == nil {
		return nil, status.Error(codes.Internal, "cluster is nil")
	}

	defaultGroupID := strings.TrimSpace(cluster.GetDefaultGroupID())
	hasDefaultTiDBIntent := strings.TrimSpace(cluster.GetAnnotations()[cloudv1.AnnoKeyGKEComputeClassForTiDBBindings]) != ""
	missingDefaultGroupIDErr := func() error {
		return status.Errorf(
			codes.FailedPrecondition,
			"cluster %s/%s has default TiDB heterogeneous intent but is missing %s",
			cluster.Namespace,
			cluster.Name,
			cloudv1.DefaultLoadBalancerIDAnnotationKey,
		)
	}
	if hasDefaultTiDBIntent && defaultGroupID == "" {
		return nil, missingDefaultGroupIDErr()
	}

	groupIDs := gkeheterogeneous.ActiveTiDBGroupIDs(cluster)
	validGroupIDs := make([]string, 0, len(groupIDs))
	for _, groupID := range groupIDs {
		groupID = strings.TrimSpace(groupID)
		if groupID == "" {
			if hasDefaultTiDBIntent {
				return nil, missingDefaultGroupIDErr()
			}
			continue
		}
		validGroupIDs = append(validGroupIDs, groupID)
	}
	if len(validGroupIDs) > 0 {
		return validGroupIDs, nil
	}

	if defaultGroupID == "" {
		return nil, nil
	}
	return []string{defaultGroupID}, nil
}

func (s *ClusterServer) setGKEHeterogeneousIntent(ctx context.Context, cluster *cloudv1.Cluster, intentConfig gkeHeterogeneousIntentConfig, tidbGroupID, computeClassName string, nodePoolNames, instanceTypes []string) (*apiv1.GKEHeterogeneousDeploy, error) {
	baseInstanceType, bindingAnnotation, err := buildGKEHeterogeneousIntentBindingAnnotation(cluster, intentConfig, tidbGroupID, computeClassName, nodePoolNames, instanceTypes)
	if err != nil {
		return nil, err
	}

	updated := cluster.DeepCopy()
	bindingKey := gkeHeterogeneousIntentAnnotationKey(updated, intentConfig, tidbGroupID)
	value, err := cloudv1.EncodeGKEComputeClassBindingAnnotation(
		bindingAnnotation,
		bindingKey,
		baseInstanceType,
		gkeheterogeneous.NodePoolName(updated, baseInstanceType),
	)
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}
	updated.UpdateAnnotation(bindingKey, value)
	if err := s.validateGKEHeterogeneousDeployIntent(ctx, cluster, updated); err != nil {
		return nil, err
	}
	if err := s.validateGKEHeterogeneousDeployBindingNodePools(ctx, cluster, bindingAnnotation); err != nil {
		return nil, err
	}
	if err := s.KubeClient.Update(ctx, updated); err != nil {
		return nil, status.Errorf(codes.Internal, "update cluster compute class intent: %s", err)
	}
	return gkeHeterogeneousDeployIntentAcceptedResponse(intentConfig, tidbGroupID, bindingKey, value, bindingAnnotation), nil
}

func gkeHeterogeneousDeployIntentAcceptedResponse(intentConfig gkeHeterogeneousIntentConfig, tidbGroupID, bindingKey, bindingValue string, bindingAnnotation cloudv1.GKEComputeClassBindingAnnotation) *apiv1.GKEHeterogeneousDeploy {
	nodePoolNames := make([]string, 0, len(bindingAnnotation.NodePools))
	instanceTypes := make([]string, 0, len(bindingAnnotation.NodePools))
	for _, binding := range bindingAnnotation.NodePools {
		if nodePoolName := strings.TrimSpace(binding.NodePoolName); nodePoolName != "" {
			nodePoolNames = append(nodePoolNames, nodePoolName)
		}
		if instanceType := strings.TrimSpace(binding.InstanceType); instanceType != "" {
			instanceTypes = append(instanceTypes, instanceType)
		}
	}

	clusterAnnotations := map[string]string{}
	if value := strings.TrimSpace(bindingValue); value != "" {
		clusterAnnotations[bindingKey] = value
	}
	return &apiv1.GKEHeterogeneousDeploy{
		Enabled:            true,
		ComponentType:      intentConfig.componentType,
		TidbGroupId:        tidbGroupID,
		ComputeClassName:   strings.TrimSpace(bindingAnnotation.ComputeClassName),
		NodePoolNames:      nodePoolNames,
		InstanceTypes:      instanceTypes,
		ClusterAnnotations: clusterAnnotations,
		Status:             apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY,
	}
}

func (s *ClusterServer) validateGKEHeterogeneousDeployBindingNodePools(ctx context.Context, cluster *cloudv1.Cluster, bindingAnnotation cloudv1.GKEComputeClassBindingAnnotation) error {
	state := &gkeHeterogeneousNodePoolBindingState{
		computeClassName:    bindingAnnotation.ComputeClassName,
		bindings:            bindingAnnotation.NodePools,
		hasExplicitBindings: true,
	}
	for _, binding := range state.ExplicitBindings() {
		nodePool, err := s.getGKEHeterogeneousNodePoolResource(ctx, binding.NodePoolName)
		if err != nil {
			if status.Code(err) == codes.NotFound {
				return status.Errorf(codes.FailedPrecondition, "heterogeneous nodepool %s must be prepared before enabling deploy", binding.NodePoolName)
			}
			return err
		}
		if err := validateGKEHeterogeneousNodePoolBindingResource(cluster, state, binding, nodePool); err != nil {
			return err
		}
	}
	return nil
}

func (s *ClusterServer) clearGKEHeterogeneousComputeClassIntent(ctx context.Context, cluster *cloudv1.Cluster, intentConfig gkeHeterogeneousIntentConfig, tidbGroupID string) error {
	updated := cluster.DeepCopy()
	delete(updated.Annotations, gkeHeterogeneousIntentAnnotationKey(updated, intentConfig, tidbGroupID))
	if err := s.KubeClient.Update(ctx, updated); err != nil {
		return status.Errorf(codes.Internal, "update cluster compute class intent: %s", err)
	}
	return nil
}

func (s *ClusterServer) gkeHeterogeneousDeploy(ctx context.Context, cluster *cloudv1.Cluster, componentType apicommon.ComponentType, tidbGroupID string) (*apiv1.GKEHeterogeneousDeploy, error) {
	intentConfig, tidbGroupID, err := resolveGKEHeterogeneousIntent(cluster, componentType, tidbGroupID)
	if err != nil {
		return nil, err
	}
	enabled := gkeHeterogeneousIntentEnabled(cluster, intentConfig, tidbGroupID)
	state, err := loadGKEHeterogeneousNodePoolBindingState(cluster, intentConfig, tidbGroupID)
	if err != nil {
		return nil, err
	}
	computeClassName := ""
	if enabled {
		computeClassName = state.computeClassName
	}
	deployStatus, err := s.gkeHeterogeneousDeployStatus(ctx, cluster, enabled, state)
	if err != nil {
		return nil, err
	}
	bindingKey := gkeHeterogeneousIntentAnnotationKey(cluster, intentConfig, tidbGroupID)
	clusterAnnotations := map[string]string{}
	if value := strings.TrimSpace(cluster.GetAnnotations()[bindingKey]); value != "" {
		clusterAnnotations[bindingKey] = value
	}
	return &apiv1.GKEHeterogeneousDeploy{
		Enabled:            enabled,
		ComponentType:      componentType,
		TidbGroupId:        tidbGroupID,
		ComputeClassName:   computeClassName,
		NodePoolNames:      state.NodePoolNames(),
		InstanceTypes:      state.InstanceTypes(),
		ClusterAnnotations: clusterAnnotations,
		Status:             deployStatus,
	}, nil
}

func (s *ClusterServer) gkeHeterogeneousDeployStatus(ctx context.Context, cluster *cloudv1.Cluster, enabled bool, state *gkeHeterogeneousNodePoolBindingState) (apiv1.GKEHeterogeneousDeployStatus, error) {
	if !enabled {
		return apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_PREPARED, nil
	}
	for _, binding := range state.ExplicitBindings() {
		nodePool, err := s.getGKEHeterogeneousNodePoolResource(ctx, binding.NodePoolName)
		if err != nil {
			if status.Code(err) == codes.NotFound {
				return apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY, nil
			}
			return apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_UNSPECIFIED, err
		}
		ready, err := validateGKEHeterogeneousDeployNodePoolReady(cluster, state, binding, nodePool)
		if err != nil {
			return apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_UNSPECIFIED, err
		}
		if !ready {
			return apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY, nil
		}
	}
	return apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_READY_FOR_GATEKEEPER, nil
}

func validateGKEHeterogeneousDeployNodePoolReady(cluster *cloudv1.Cluster, state *gkeHeterogeneousNodePoolBindingState, binding cloudv1.GKEComputeClassNodePoolBinding, nodePool *gkev1beta1.NodePool) (bool, error) {
	if err := validateGKEHeterogeneousNodePoolBindingIdentity(cluster, binding, nodePool); err != nil {
		return false, err
	}
	expectedComputeClass := strings.TrimSpace(state.computeClassName)
	if expectedComputeClass == "" {
		return true, nil
	}
	return gkeheterogeneous.NodePoolReadyForGatekeeper(nodePool, expectedComputeClass)
}
