package app

import (
	"context"
	"errors"
	"regexp"

	"github.com/samber/lo"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/server"
	"github.com/tidbcloud/cluster-service-ng/domain/server/br"
	mysqlschema "github.com/tidbcloud/cluster-service-ng/pkg/storage/mysql/schema"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/emptypb"
)

const (
	autoscaleTriggerDiskExpand       = "disk_expand"
	autoscaleTriggerIOPSExpand       = "iops_expand"
	autoscaleTriggerThroughputExpand = "throughput_expand"
	autoscaleTriggerTiKVCPUScaleOut  = "tikv_cpu_scale_out"
)

var (
	backupTimePattern               = regexp.MustCompile(`^(?:[01]\d|2[0-3]):[0-5]\d$`)
	placementPolicyNumericIDPattern = regexp.MustCompile(`^\d+$`)
)

func isValidBackupTime(value string) bool {
	return backupTimePattern.MatchString(value)
}

// preserveStatusOrInternal keeps business gRPC codes, including lifecycle
// FailedPrecondition, visible at the API boundary.
func preserveStatusOrInternal(operation string, err error) error {
	if _, ok := status.FromError(err); ok {
		return err
	}
	return status.Errorf(codes.Internal, "%s failed: %s", operation, err)
}

func validatePlacementPolicyNumericID(fieldName, value string) error {
	if value == "" {
		return status.Errorf(codes.InvalidArgument, "%s is required", fieldName)
	}
	if !placementPolicyNumericIDPattern.MatchString(value) {
		return status.Errorf(codes.InvalidArgument, "%s must match pattern ^[0-9]+$", fieldName)
	}
	return nil
}

func validateTiKVGroupID(tikvGroupID string, required bool) error {
	if tikvGroupID == "" {
		if required {
			return status.Errorf(codes.InvalidArgument, "tikv_group_id is required")
		}
		return nil
	}
	if err := entity.ValidateLowercaseAlphanumericId("tikv_group_id", tikvGroupID); err != nil {
		return status.Errorf(codes.InvalidArgument, "%s", err)
	}
	return nil
}

type Server interface {
	apiv1.SharedPoolServiceServer
	apiv1.NodeInstanceGroupServiceServer
	apiv1.TiDBClusterServiceServer
	apiv1.RuConfigServiceServer
	apiv1.RestoreServiceServer
	apiv1.ExportServiceServer
	apiv1.TemplateServiceServer
	apiv1.AutoscalePolicyServiceServer
}

// Compatible cluster-service api
// will deprecate
type appImpl struct {
	apiv1.UnimplementedSharedPoolServiceServer
	apiv1.UnimplementedNodeInstanceGroupServiceServer

	region   string
	provider string

	// dependencies
	ngService server.Service
}

func newApp(ngService server.Service, provider, region string) Server {
	return &appImpl{
		region:    region,
		provider:  provider,
		ngService: ngService,
	}
}

// resource pool
func (s *appImpl) CreateSharedPool(ctx context.Context, req *apiv1.CreateSharedPoolRequest) (*apiv1.SharedPool, error) {
	if req.GetSharedPool() == nil {
		return nil, status.Errorf(codes.InvalidArgument, "shared pool is nil")
	}
	if req.GetEnableCapacityPlanner() && entity.GetEffectiveCapacityMode(req.GetCapacityMode()) != apiv1.CapacityMode_RCU_DRIVEN {
		return nil, status.Error(codes.InvalidArgument, "enable_capacity_planner is supported only for RCU-driven shared pools")
	}

	if entity.GetEffectivePDDeploymentMode(req.GetPdDeploymentMode()) == apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MONOLITHIC {
		for _, comp := range req.GetSharedPool().GetComponentGroups() {
			if comp == nil {
				continue
			}
			switch comp.GetComponentType() {
			case apiv1.ComponentType_PD_TSO, apiv1.ComponentType_PD_SCHEDULING, apiv1.ComponentType_RESOURCE_MANAGER:
				return nil, status.Errorf(codes.InvalidArgument, "pd_deployment_mode MONOLITHIC does not support component %s", comp.GetComponentType().String())
			}
		}
	}

	var deployType string
	tenantId := req.GetTenantId()
	if tenantId == "" {
		tenantId = "shared-multi-tenant"
		deployType = mysqlschema.SharedPoolDeployTypeSharedTiProxy
	}

	sharedPool := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			Id:                       req.GetSharedPool().GetId(),
			DataplaneName:            req.GetSharedPool().GetDataplaneName(),
			Pca:                      req.GetSharedPool().GetPca(),
			VcpusLimit:               req.GetVcpusLimit(),
			CapacityMode:             req.GetCapacityMode(),
			PdDeploymentMode:         req.GetPdDeploymentMode(),
			EnabledTikvRemoteWorkers: req.GetEnabledTikvRemoteWorkers(),
			TopologySpec:             req.GetTopologySpec(),
			PlacementGroupEnabled:    req.GetSharedPool().GetPlacementGroupEnabled(),
			CloudResourceTags:        req.GetSharedPool().GetCloudResourceTags(),
		},
		TenantId:              tenantId,
		DeployType:            deployType,
		RequestedTiflashMode:  req.GetTiflashMode(),
		EnableCapacityPlanner: req.GetEnableCapacityPlanner(),
	}

	if req.GetInitSharedPoolConfig() == nil {
		return nil, status.Error(codes.InvalidArgument, "initSharedPoolConfig is nil")
	}

	var err error
	if req.GetBootstrapCapacity() == nil && !req.GetEnforceO11YNetworkNonOverlap() {
		sharedPool, err = s.ngService.CreateSharedPool(
			ctx,
			sharedPool,
			req.GetRequestUnit(),
			req.GetInitSharedPoolConfig(),
		)
	} else {
		options := &server.CreateSharedPoolOptions{
			BootstrapCapacity:            toSharedPoolBootstrapCapacity(req.GetBootstrapCapacity()),
			EnforceO11yNetworkNonOverlap: req.GetEnforceO11YNetworkNonOverlap(),
		}
		sharedPool, err = s.ngService.CreateSharedPool(
			ctx,
			sharedPool,
			req.GetRequestUnit(),
			req.GetInitSharedPoolConfig(),
			options,
		)
	}
	if err != nil {
		return nil, err
	}

	// when creating a shared pool, we don't need to set the bucket for object storage and internal storage
	sharedPool.ObjectStorage.Bucket = ""
	sharedPool.InternalStorage.Bucket = ""
	return &sharedPool.SharedPool, nil
}

func toSharedPoolBootstrapCapacity(capacity *apiv1.SharedPoolBootstrapCapacity) *server.SharedPoolBootstrapCapacity {
	if capacity == nil {
		return nil
	}
	toComponent := func(component *apiv1.BootstrapComponentCapacity) *server.BootstrapComponentCapacity {
		if component == nil {
			return nil
		}
		return &server.BootstrapComponentCapacity{
			NodeSpec:  component.GetNodeSpec(),
			NodeCount: component.GetNodeCount(),
		}
	}
	return &server.SharedPoolBootstrapCapacity{
		TiKV:           toComponent(capacity.GetTikv()),
		TiFlashCompute: toComponent(capacity.GetTiflashCompute()),
	}
}

func (s *appImpl) ListNodeSpecs(ctx context.Context, req *apiv1.ListNodeSpecsRequest) (*apiv1.ListNodeSpecsResponse, error) {
	return s.ngService.ListNodeSpecs(ctx, req.GetTenantId(), req.GetComponentType())
}

func (s *appImpl) GetSharedPoolProvisionConfiguration(
	ctx context.Context,
	_ *apiv1.GetSharedPoolProvisionConfigurationRequest,
) (*apiv1.SharedPoolProvisionConfiguration, error) {
	return s.ngService.GetSharedPoolProvisionConfiguration(ctx)
}

func (s *appImpl) EvaluateSharedPoolNetworkCapacity(
	ctx context.Context,
	req *apiv1.EvaluateSharedPoolNetworkCapacityRequest,
) (*apiv1.EvaluateSharedPoolNetworkCapacityResponse, error) {
	return s.ngService.EvaluateSharedPoolNetworkCapacity(ctx, req)
}

func (s *appImpl) UpdateSharedPoolCloudResourceTags(ctx context.Context, req *apiv1.UpdateSharedPoolCloudResourceTagsRequest) (*apiv1.SharedPool, error) {
	sharedPool, err := s.ngService.UpdateSharedPoolCloudResourceTags(
		ctx,
		req.GetSharedPoolId(),
		req.GetCloudResourceTags(),
		req.GetRemoveCloudResourceTagKeys(),
	)
	if err != nil {
		return nil, err
	}
	return &sharedPool.SharedPool, nil
}

func (s *appImpl) ListSharedPools(ctx context.Context, req *apiv1.ListSharedPoolsRequest) (*apiv1.ListSharedPoolsResponse, error) {
	sharedPools, err := s.ngService.ListSharedPools(ctx, req.GetIds(), req.GetDataplaneNames())
	if err != nil {
		return nil, err
	}

	resp := &apiv1.ListSharedPoolsResponse{
		SharedPools: make([]*apiv1.SharedPool, 0),
		TotalCount:  int32(len(sharedPools)),
	}

	for _, sharedPool := range sharedPools {
		resp.SharedPools = append(resp.SharedPools, &sharedPool.SharedPool)
	}

	return resp, nil
}

func (s *appImpl) GetSharedPool(ctx context.Context, req *apiv1.GetSharedPoolRequest) (*apiv1.SharedPool, error) {
	sharedPool, err := s.ngService.GetSharedPool(ctx, req.GetSharedPoolId())
	if err != nil {
		return nil, err
	}

	return &sharedPool.SharedPool, nil
}

func (s *appImpl) ValidateSharedPoolPause(ctx context.Context, req *apiv1.ValidateSharedPoolPauseRequest) (*apiv1.ValidateSharedPoolPauseResponse, error) {
	response, err := s.ngService.ValidateSharedPoolPause(ctx, req.GetSharedPoolId())
	if err != nil {
		return nil, err
	}
	return response, nil
}

func (s *appImpl) PauseSharedPool(ctx context.Context, req *apiv1.PauseSharedPoolRequest) (*apiv1.SharedPool, error) {
	sharedPool, err := s.ngService.PauseSharedPool(ctx, req.GetSharedPoolId(), req.GetOperationId())
	if err != nil {
		return nil, err
	}
	return &sharedPool.SharedPool, nil
}

func (s *appImpl) ResumeSharedPool(ctx context.Context, req *apiv1.ResumeSharedPoolRequest) (*apiv1.SharedPool, error) {
	sharedPool, err := s.ngService.ResumeSharedPool(ctx, req.GetSharedPoolId(), req.GetOperationId())
	if err != nil {
		return nil, err
	}
	return &sharedPool.SharedPool, nil
}

func (s *appImpl) GetPauseResumeOperation(ctx context.Context, req *apiv1.GetPauseResumeOperationRequest) (*apiv1.PauseResumeOperation, error) {
	return s.ngService.GetPauseResumeOperation(ctx, req.GetSharedPoolId(), req.GetOperationId())
}

func (s *appImpl) GetComponentTemplate(
	ctx context.Context,
	req *apiv1.GetComponentTemplateRequest,
) (*apiv1.GetComponentTemplateResponse, error) {
	return s.ngService.GetComponentTemplate(ctx, req.GetComponentType(), req.GetVersion())
}

func (s *appImpl) DeleteSharedPool(ctx context.Context, req *apiv1.DeleteSharedPoolRequest) (*apiv1.SharedPool, error) {
	sharedPool, err := s.ngService.DeleteSharedPool(ctx, req.GetSharedPoolId(), req.GetPruneDataplane())
	if err != nil {
		return nil, err
	}

	return &sharedPool.SharedPool, nil
}

func (s *appImpl) ScaleSharedPool(ctx context.Context, req *apiv1.ScaleSharedPoolRequest) (*apiv1.SharedPool, error) {
	// scale_operation_id is an optional, additive field. The guarded scale-out
	// path (create + cancel) is not wired in this foundation PR, so the id is
	// ignored here and the mutation runs its normal legacy behavior. Ignoring
	// (rather than rejecting) keeps a non-empty id backward compatible.
	if md, ok := metadata.FromIncomingContext(ctx); ok {
		if triggers := md.Get("x-autoscale-trigger"); len(triggers) > 0 && triggers[0] != "" {
			trigger := triggers[0]
			ruConfig, err := s.ngService.GetRuConfig(ctx, req.GetSharedPoolId(), apiv1.RuConfig_SHARED_POOL)
			if err != nil {
				tilog.Error(ctx, "ScaleSharedPool: failed to get ru config for autoscale whitelist check",
					zap.String("sharedPoolId", req.GetSharedPoolId()), zap.Error(err))
				return nil, status.Errorf(codes.Internal, "failed to get ru config: %s", err)
			}
			flags := ruConfig.GetFeatureFlags()
			switch trigger {
			case autoscaleTriggerDiskExpand:
				if !flags.GetEnableAutoscaleDiskExpand() {
					return nil, status.Errorf(codes.PermissionDenied,
						"auto-scaler disk expand not allowed: enable_autoscale_disk_expand is disabled for shared pool %s",
						req.GetSharedPoolId())
				}
			case autoscaleTriggerIOPSExpand, autoscaleTriggerThroughputExpand:
				if !flags.GetEnableAutoscaleIopsThroughputExpand() {
					return nil, status.Errorf(codes.PermissionDenied,
						"auto-scaler iops/throughput expand not allowed: enable_autoscale_iops_throughput_expand is disabled for shared pool %s",
						req.GetSharedPoolId())
				}
			default:
				return nil, status.Errorf(codes.PermissionDenied,
					"auto-scaler scale not allowed: unknown autoscale trigger %q for shared pool %s",
					trigger, req.GetSharedPoolId())
			}
		}
	}
	sharedPool, err := s.ngService.ScaleSharedPool(ctx,
		req.GetSharedPoolId(),
		&entity.Component{
			Id:            req.GetComponent().GetId(),
			ComponentType: req.GetComponent().GetComponentType(),
			NodeQuota:     req.GetComponent().GetNodeQuota(),
			Storages:      req.GetComponent().GetStorages(),
		},
		req.GetForce())
	if err != nil {
		return nil, err
	}

	return &sharedPool.SharedPool, nil
}

func (s *appImpl) GetSharedPoolScaleConstraints(ctx context.Context, req *apiv1.GetSharedPoolScaleConstraintsRequest) (*apiv1.GetSharedPoolScaleConstraintsResponse, error) {
	return s.ngService.GetSharedPoolScaleConstraints(ctx, req.GetSharedPoolId())
}

func (s *appImpl) SetSharedPoolPDDeploymentMode(ctx context.Context, req *apiv1.SetSharedPoolPDDeploymentModeRequest) (*apiv1.SharedPool, error) {
	sharedPool, err := s.ngService.SetSharedPoolPDDeploymentMode(ctx, req.GetSharedPoolId(), req.GetPdDeploymentMode())
	if err != nil {
		return nil, err
	}
	return &sharedPool.SharedPool, nil
}

func (s *appImpl) SetSharedPoolTiKVRemoteWorkers(ctx context.Context, req *apiv1.SetSharedPoolTiKVRemoteWorkersRequest) (*apiv1.SharedPool, error) {
	sharedPool, err := s.ngService.SetSharedPoolTiKVRemoteWorkers(ctx, req.GetSharedPoolId(), req.GetEnabledTikvRemoteWorkers())
	if err != nil {
		return nil, err
	}
	return &sharedPool.SharedPool, nil
}

func (s *appImpl) UpdateSharedPoolVcpusLimit(ctx context.Context, req *apiv1.UpdateSharedPoolVcpusLimitRequest) (*apiv1.SharedPool, error) {
	sharedPool, err := s.ngService.UpdateSharedPoolVcpusLimit(ctx, req.GetSharedPoolId(), req.GetVcpusLimit())
	if err != nil {
		return nil, err
	}

	return &sharedPool.SharedPool, nil
}

func (s *appImpl) GetSharedPoolVcpusUsage(ctx context.Context, req *apiv1.GetSharedPoolVcpusUsageRequest) (*apiv1.GetSharedPoolVcpusUsageResponse, error) {
	return s.ngService.GetSharedPoolVcpusUsage(ctx, req.GetSharedPoolId())
}

func (s *appImpl) GetScaleOperation(ctx context.Context, req *apiv1.GetScaleOperationRequest) (*apiv1.ScaleOperation, error) {
	operation, err := s.ngService.GetScaleOperation(ctx, req.GetSharedPoolId(), req.GetScaleOperationId())
	if err != nil {
		return nil, err
	}
	return &operation.ScaleOperation, nil
}

func (s *appImpl) CancelScaleOperation(ctx context.Context, req *apiv1.CancelScaleOperationRequest) (*apiv1.ScaleOperation, error) {
	operation, err := s.ngService.CancelScaleOperation(
		ctx,
		req.GetSharedPoolId(),
		req.GetScaleOperationId(),
		req.GetReason(),
	)
	if err != nil {
		return nil, err
	}
	return &operation.ScaleOperation, nil
}

// CreateTiKVGroup creates a new TiKV group in the specified shared pool
func (s *appImpl) CreateTiKVGroup(ctx context.Context, req *apiv1.CreateTiKVGroupRequest) (*apiv1.Component, error) {
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared_pool_id is required")
	}

	if req.GetTikvGroupId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "tikv_group_id is required")
	}

	if req.GetNodeQuota() == nil {
		return nil, status.Errorf(codes.InvalidArgument, "node_quota is required")
	}

	if len(req.GetStorages()) == 0 {
		return nil, status.Errorf(codes.InvalidArgument, "storages are required")
	}

	// Convert proto storages to entity storages
	storages := make([]*entity.Storage, 0, len(req.GetStorages()))
	storages = append(storages, req.GetStorages()...)

	comp, err := s.ngService.CreateTiKVGroup(
		ctx,
		req.GetSharedPoolId(),
		req.GetTikvGroupId(),
		req.GetNodeQuota(),
		storages,
		req.GetLabels(),
		req.GetExclusive(),
		req.GetForce(),
	)
	if err != nil {
		return nil, err
	}

	return comp, nil
}

func (s *appImpl) DeleteTiKVGroup(ctx context.Context, req *apiv1.DeleteTiKVGroupRequest) (*emptypb.Empty, error) {
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared_pool_id is required")
	}
	if err := validateTiKVGroupID(req.GetTikvGroupId(), true); err != nil {
		return nil, err
	}
	if err := s.ngService.DeleteTiKVGroup(ctx, req.GetSharedPoolId(), req.GetTikvGroupId()); err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

func (s *appImpl) ListPlacementPolicies(ctx context.Context, req *apiv1.ListPlacementPoliciesRequest) (*apiv1.ListPlacementPoliciesResponse, error) {
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared_pool_id is required")
	}
	if err := validateTiKVGroupID(req.GetTikvGroupId(), false); err != nil {
		return nil, err
	}
	policies, err := s.ngService.ListPlacementPolicies(
		ctx,
		req.GetSharedPoolId(),
		req.GetTikvGroupId(),
	)
	if err != nil {
		return nil, err
	}
	return &apiv1.ListPlacementPoliciesResponse{
		PlacementPolicies: policies,
	}, nil
}

func (s *appImpl) CreatePlacementPolicy(ctx context.Context, req *apiv1.CreatePlacementPolicyRequest) (*apiv1.PlacementPolicy, error) {
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared_pool_id is required")
	}
	if err := validatePlacementPolicyNumericID("cluster_id", req.GetClusterId()); err != nil {
		return nil, err
	}
	if err := validateTiKVGroupID(req.GetTikvGroupId(), true); err != nil {
		return nil, err
	}
	return s.ngService.CreatePlacementPolicy(
		ctx,
		req.GetSharedPoolId(),
		req.GetClusterId(),
		req.GetTikvGroupId(),
	)
}

func (s *appImpl) GetPlacementPolicy(ctx context.Context, req *apiv1.GetPlacementPolicyRequest) (*apiv1.PlacementPolicy, error) {
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared_pool_id is required")
	}
	if err := validatePlacementPolicyNumericID("placement_policy_id", req.GetPlacementPolicyId()); err != nil {
		return nil, err
	}
	return s.ngService.GetPlacementPolicy(ctx, req.GetSharedPoolId(), req.GetPlacementPolicyId())
}

func (s *appImpl) UpdatePlacementPolicy(ctx context.Context, req *apiv1.UpdatePlacementPolicyRequest) (*apiv1.PlacementPolicy, error) {
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared_pool_id is required")
	}
	if err := validatePlacementPolicyNumericID("placement_policy_id", req.GetPlacementPolicyId()); err != nil {
		return nil, err
	}
	if err := validateTiKVGroupID(req.GetTikvGroupId(), true); err != nil {
		return nil, err
	}
	return s.ngService.UpdatePlacementPolicy(
		ctx,
		req.GetSharedPoolId(),
		req.GetPlacementPolicyId(),
		req.GetTikvGroupId(),
	)
}

func (s *appImpl) DeletePlacementPolicy(ctx context.Context, req *apiv1.DeletePlacementPolicyRequest) (*emptypb.Empty, error) {
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared_pool_id is required")
	}
	if err := validatePlacementPolicyNumericID("placement_policy_id", req.GetPlacementPolicyId()); err != nil {
		return nil, err
	}
	if err := s.ngService.DeletePlacementPolicy(ctx, req.GetSharedPoolId(), req.GetPlacementPolicyId()); err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

func (s *appImpl) ApplySharedPool(ctx context.Context, req *apiv1.ApplySharedPoolRequest) (*emptypb.Empty, error) {
	err := s.ngService.ApplySharedPool(ctx,
		req.GetSharedPoolId(),
		&apiv1.TargetComponent{
			Id:              req.GetId(),
			ComponentType:   req.GetComponentType(),
			TemplateVersion: req.GetTemplateVersion(),
		},
		req.GetForce(),
	)
	if err != nil {
		return nil, err
	}

	return &emptypb.Empty{}, nil
}

func (s *appImpl) ListComponentTemplateVersions(
	ctx context.Context,
	req *apiv1.ListComponentTemplateVersionsRequest,
) (*apiv1.ListComponentTemplateVersionsResponse, error) {
	return s.ngService.ListComponentTemplateVersions(ctx, req.GetComponentType())
}

func (s *appImpl) SetComponentTemplateVersionStatus(
	ctx context.Context,
	req *apiv1.SetComponentTemplateVersionStatusRequest,
) (*apiv1.TemplateVersionInfo, error) {
	return s.ngService.SetComponentTemplateVersionStatus(ctx, req.GetComponentType(), req.GetVersion(), req.GetStatus())
}

func (s *appImpl) GetTemplate(
	ctx context.Context,
	req *apiv1.GetTemplateRequest,
) (*apiv1.GetTemplateResponse, error) {
	return s.ngService.GetTemplate(ctx, req.GetTemplateName(), req.GetVersion())
}

func (s *appImpl) ListTemplateVersions(
	ctx context.Context,
	req *apiv1.ListTemplateVersionsRequest,
) (*apiv1.ListTemplateVersionsResponse, error) {
	return s.ngService.ListTemplateVersions(ctx, req.GetTemplateName())
}

func (s *appImpl) SetTemplateVersionStatus(
	ctx context.Context,
	req *apiv1.SetTemplateVersionStatusRequest,
) (*apiv1.TemplateVersionInfo, error) {
	return s.ngService.SetTemplateVersionStatus(ctx, req.GetTemplateName(), req.GetVersion(), req.GetStatus())
}

func (s *appImpl) ListTemplates(
	ctx context.Context,
	_ *apiv1.ListTemplatesRequest,
) (*apiv1.ListTemplatesResponse, error) {
	return s.ngService.ListTemplates(ctx)
}

func (s *appImpl) ListComponentTemplates(
	ctx context.Context,
	_ *apiv1.ListComponentTemplatesRequest,
) (*apiv1.ListComponentTemplatesResponse, error) {
	return s.ngService.ListComponentTemplates(ctx)
}

func (s *appImpl) SetSharedPoolComponentQuota(ctx context.Context, req *apiv1.SetSharedPoolComponentQuotaRequest) (*apiv1.Component, error) {
	// scale_operation_id is ignored until the guarded quota scale-out path is
	// wired; a non-empty id stays backward compatible and runs legacy behavior.
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared pool id is empty")
	}

	targetComponent := &entity.Component{
		Id:            req.GetId(),
		ComponentType: req.GetComponentType(),
		NodeQuota: &entity.NodeQuota{
			MinNodeCount: req.GetMinNodeCount(),
			MaxNodeCount: req.GetMaxNodeCount(),
		},
	}

	comp, err := s.ngService.SetSharedPoolComponentQuota(ctx, req.GetSharedPoolId(), targetComponent)
	if err != nil {
		return nil, err
	}
	return comp, nil
}

func (s *appImpl) SetSharedPoolComponentLabels(ctx context.Context, req *apiv1.SetSharedPoolComponentLabelsRequest) (*apiv1.Component, error) {
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared pool id is empty")
	}

	if req.GetComponentType() == apiv1.ComponentType_COMPONENT_TYPE_UNSPECIFIED {
		return nil, status.Errorf(codes.InvalidArgument, "component type is required")
	}

	if len(req.GetLabels()) == 0 {
		return nil, status.Errorf(codes.InvalidArgument, "labels cannot be empty")
	}

	targetComponent := &entity.Component{
		Id:            req.GetId(),
		ComponentType: req.GetComponentType(),
		Labels:        req.GetLabels(),
	}

	comp, err := s.ngService.SetSharedPoolComponentLabels(ctx, req.GetSharedPoolId(), targetComponent)
	if err != nil {
		return nil, err
	}
	return comp, nil
}

func (s *appImpl) SetSharedPoolComponentNodeVersion(ctx context.Context, req *apiv1.SetSharedPoolComponentNodeVersionRequest) (*apiv1.Component, error) {
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared pool id is empty")
	}

	if req.GetComponentType() == apiv1.ComponentType_COMPONENT_TYPE_UNSPECIFIED {
		return nil, status.Errorf(codes.InvalidArgument, "component type is required")
	}

	if req.GetNodeVersion() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "node_version is required")
	}

	comp, err := s.ngService.SetSharedPoolComponentNodeVersion(ctx, req.GetSharedPoolId(), req.GetId(), req.GetComponentType(), req.GetNodeVersion())
	if err != nil {
		return nil, err
	}
	return comp, nil
}

func (s *appImpl) SetSharedPoolComponentConfigVersion(ctx context.Context, req *apiv1.SetSharedPoolComponentConfigVersionRequest) (*emptypb.Empty, error) {
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared pool id is empty")
	}

	if len(req.GetComponents()) == 0 {
		return nil, status.Errorf(codes.InvalidArgument, "components is empty")
	}

	coreComponents, brComponents, dmComponents := partitionComponents(req.GetComponents())
	tilog.Info(ctx, "partition shared pool component config/version request",
		zap.String("sharedPoolId", req.GetSharedPoolId()),
		zap.String("scope", req.GetScope().String()),
		zap.String("mode", req.GetMode().String()),
		zap.Bool("force", req.GetForce()),
		zap.Int("coreCount", len(coreComponents)),
		zap.Int("brCount", len(brComponents)),
		zap.Int("dmCount", len(dmComponents)))

	if len(coreComponents) > 0 {
		err := s.ngService.SetSharedPoolComponentConfigOrVersion(ctx, req.GetSharedPoolId(), coreComponents, req.GetScope(), req.GetMode(), req.GetForce())
		if err != nil {
			return nil, err
		}
	}

	if len(brComponents) > 0 {
		err := s.ngService.SetBrComponentConfigOrVersion(ctx, req.GetSharedPoolId(), brComponents, req.GetScope())
		if err != nil {
			return nil, err
		}
	}

	if len(dmComponents) > 0 {
		err := s.ngService.SetDMComponentConfigOrVersion(ctx, req.GetSharedPoolId(), dmComponents, req.GetScope(), req.GetForce())
		if err != nil {
			return nil, err
		}
	}

	return &emptypb.Empty{}, nil
}

// DATA_MIGRATION must be handled by DMCluster flow, not core SharedPool ComponentGroups flow.
func partitionComponents(components []*apiv1.TargetComponent) ([]*apiv1.TargetComponent, []*apiv1.TargetComponent, []*apiv1.TargetComponent) {
	brComponentsTypes := []apiv1.ComponentType{
		apiv1.ComponentType_TIBR_AUTOBACKUP,
		apiv1.ComponentType_TIBRGC_T2_DFSGC,
		apiv1.ComponentType_TIBRGC_T3_ARCHIVE,
		apiv1.ComponentType_TIBR_RESTORE,
		apiv1.ComponentType_CLASSIC_RESTORE,
	}
	brComponents := make([]*apiv1.TargetComponent, 0)
	dmComponents := make([]*apiv1.TargetComponent, 0)
	coreComponents := make([]*apiv1.TargetComponent, 0)
	for _, component := range components {
		switch {
		case lo.Contains(brComponentsTypes, component.GetComponentType()):
			brComponents = append(brComponents, component)
		case component.GetComponentType() == apiv1.ComponentType_DATA_MIGRATION:
			dmComponents = append(dmComponents, component)
		default:
			coreComponents = append(coreComponents, component)
		}
	}
	return coreComponents, brComponents, dmComponents
}

func (s *appImpl) ListLightweightBackups(ctx context.Context, upReq *apiv1.ListLightweightBackupsRequest) (*apiv1.ListLightweightBackupsResponse, error) {
	downReq := &server.ListLightweightBackupsInput{ListLightweightBackupsRequest: upReq}

	downResp, err := s.ngService.ListLightweightBackups(ctx, downReq)
	if err != nil {
		tilog.Error(ctx, "ListLightweightBackups failed",
			zap.Error(err), zap.String("request", utils.JSON(downReq)))
		return nil, status.Errorf(codes.Internal, "ListLightweightBackups failed: %s", err.Error())
	}
	tilog.Info(ctx, "ListLightweightBackups succeeded", zap.String("request", utils.JSON(downReq)), zap.String("response", utils.JSON(downResp)))

	upResp := down2UpListLightweightBackupsResponse(downResp)
	return upResp, nil
}

func (s *appImpl) CreateLightweightRestore(ctx context.Context, upReq *apiv1.CreateLightweightRestoreRequest) (*emptypb.Empty, error) {
	err := validateCreateLightweightRestoreRequest(upReq)
	if err != nil {
		tilog.Error(ctx, "invalid CreateLightweightRestoreRequest", zap.Error(err), zap.String("request", upReq.String()))
		return nil, status.Errorf(codes.InvalidArgument, "validate request failed: %s", err.Error())
	}

	downReq := &br.CreateLightweightRestoreInput{CreateLightweightRestoreRequest: upReq}

	err = s.ngService.CreateLightweightRestore(ctx, downReq)
	if err != nil {
		tilog.Error(ctx, "CreateLightweightRestore failed",
			zap.Error(err), zap.String("request", utils.JSON(downReq)))
		return nil, preserveStatusOrInternal("CreateLightweightRestore", err)
	}
	tilog.Info(ctx, "CreateLightweightRestore succeeded", zap.String("request", utils.JSON(downReq)))

	return &emptypb.Empty{}, nil
}

func (s *appImpl) GetLightweightRestore(ctx context.Context, upReq *apiv1.GetLightweightRestoreRequest) (*apiv1.LightweightRestore, error) {
	downReq := &br.GetLightweightRestoreInput{GetLightweightRestoreRequest: upReq}

	downResp, err := s.ngService.GetLightweightRestore(ctx, downReq)
	if err != nil {
		if errors.Is(err, server.ErrLightweightRestoreNotFound) {
			return nil, status.Errorf(codes.NotFound, "LightweightRestore not found")
		}
		tilog.Error(ctx, "GetLightweightRestore failed",
			zap.Error(err), zap.String("request", utils.JSON(downReq)))
		return nil, status.Errorf(codes.Internal, "GetLightweightRestore failed: %s", err.Error())
	}
	tilog.Info(ctx, "GetLightweightRestore succeeded", zap.String("request", utils.JSON(downReq)), zap.String("response", utils.JSON(downResp)))

	return down2UpGetLightweightRestoreResponse(downResp), nil
}

func (s *appImpl) GetLightweightRestorePostActions(ctx context.Context, req *apiv1.GetLightweightRestorePostActionsRequest) (*apiv1.GetLightweightRestorePostActionsResponse, error) {
	downReq := &br.GetLightweightRestorePostActionsInput{GetLightweightRestorePostActionsRequest: req}
	resp, err := s.ngService.GetLightweightRestorePostActions(ctx, downReq)
	if err != nil {
		return nil, err
	}
	return resp.GetLightweightRestorePostActionsResponse, nil
}

func (s *appImpl) DeleteLightweightRestore(ctx context.Context, upReq *apiv1.DeleteLightweightRestoreRequest) (*emptypb.Empty, error) {
	downReq := &br.DeleteLightweightRestoreInput{DeleteLightweightRestoreRequest: upReq}

	err := s.ngService.DeleteLightweightRestore(ctx, downReq)
	if err != nil {
		tilog.Error(ctx, "DeleteLightweightRestore failed",
			zap.Error(err), zap.String("request", utils.JSON(downReq)))
		return nil, preserveStatusOrInternal("DeleteLightweightRestore", err)
	}
	tilog.Info(ctx, "DeleteLightweightRestore succeeded", zap.String("request", utils.JSON(downReq)))

	return &emptypb.Empty{}, nil
}

func (s *appImpl) CreateTiKVCapacityReservation(ctx context.Context, req *apiv1.CreateTiKVCapacityReservationRequest) (*apiv1.TiKVCapacityReservation, error) {
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared pool id is empty")
	}
	if req.GetBizType() == apiv1.TiKVCapacityReservationBizType_TIKV_CAPACITY_RESERVATION_BIZ_TYPE_UNSPECIFIED {
		return nil, status.Errorf(codes.InvalidArgument, "biz type is required")
	}
	if req.GetReservationId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "reservation id is empty")
	}
	if req.GetRequiredDataBytes() == 0 {
		return nil, status.Errorf(codes.InvalidArgument, "required_data_bytes must be greater than 0")
	}

	// Feature flag whitelist check for AUTO_SCALER biz type
	if req.GetBizType() == apiv1.TiKVCapacityReservationBizType_TIKV_CAPACITY_RESERVATION_BIZ_TYPE_AUTO_SCALER {
		trigger := inferAutoscaleTrigger(ctx)
		ruConfig, err := s.ngService.GetRuConfig(ctx, req.GetSharedPoolId(), apiv1.RuConfig_SHARED_POOL)
		if err != nil {
			tilog.Error(ctx, "CreateTiKVCapacityReservation: failed to get ru config for whitelist check",
				zap.String("sharedPoolId", req.GetSharedPoolId()), zap.Error(err))
			return nil, status.Errorf(codes.Internal, "failed to get ru config: %s", err)
		}
		flags := ruConfig.GetFeatureFlags()
		switch trigger {
		case autoscaleTriggerTiKVCPUScaleOut:
			if !flags.GetEnableAutoscaleTikvCpuScaleOut() {
				return nil, status.Errorf(codes.PermissionDenied,
					"auto-scaler capacity reservation not allowed: enable_autoscale_tikv_cpu_scale_out is disabled for shared pool %s",
					req.GetSharedPoolId())
			}
		case autoscaleTriggerDiskExpand:
			if !flags.GetEnableAutoscaleDiskExpand() {
				return nil, status.Errorf(codes.PermissionDenied,
					"auto-scaler capacity reservation not allowed: enable_autoscale_disk_expand is disabled for shared pool %s",
					req.GetSharedPoolId())
			}
		default:
			return nil, status.Errorf(codes.PermissionDenied,
				"auto-scaler capacity reservation not allowed: unknown autoscale trigger for shared pool %s",
				req.GetSharedPoolId())
		}
	}

	downReq := &server.CreateTiKVCapacityReservationInput{CreateTiKVCapacityReservationRequest: req}
	return s.ngService.CreateTiKVCapacityReservation(ctx, downReq)
}

func inferAutoscaleTrigger(ctx context.Context) string {
	if md, ok := metadata.FromIncomingContext(ctx); ok {
		if triggers := md.Get("x-autoscale-trigger"); len(triggers) > 0 && triggers[0] != "" {
			return triggers[0]
		}
	}
	return ""
}

func (s *appImpl) GetTiKVCapacityReservation(ctx context.Context, req *apiv1.GetTiKVCapacityReservationRequest) (*apiv1.TiKVCapacityReservation, error) {
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared pool id is empty")
	}
	if req.GetBizType() == apiv1.TiKVCapacityReservationBizType_TIKV_CAPACITY_RESERVATION_BIZ_TYPE_UNSPECIFIED {
		return nil, status.Errorf(codes.InvalidArgument, "biz type is required")
	}
	if req.GetReservationId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "reservation id is empty")
	}

	downReq := &server.GetTiKVCapacityReservationInput{GetTiKVCapacityReservationRequest: req}
	return s.ngService.GetTiKVCapacityReservation(ctx, downReq)
}

func (s *appImpl) DeleteTiKVCapacityReservation(ctx context.Context, req *apiv1.DeleteTiKVCapacityReservationRequest) (*emptypb.Empty, error) {
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared pool id is empty")
	}
	if req.GetBizType() == apiv1.TiKVCapacityReservationBizType_TIKV_CAPACITY_RESERVATION_BIZ_TYPE_UNSPECIFIED {
		return nil, status.Errorf(codes.InvalidArgument, "biz type is required")
	}
	if req.GetReservationId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "reservation id is empty")
	}

	downReq := &server.DeleteTiKVCapacityReservationInput{DeleteTiKVCapacityReservationRequest: req}
	if err := s.ngService.DeleteTiKVCapacityReservation(ctx, downReq); err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

func (s *appImpl) CreateClassicRestore(ctx context.Context, upReq *apiv1.CreateClassicRestoreRequest) (*emptypb.Empty, error) {
	err := validateCreateClassicRestoreRequest(upReq)
	if err != nil {
		tilog.Error(ctx, "invalid CreateClassicRestoreRequest", zap.Error(err), zap.String("request", utils.RedactedJSON(upReq)))
		return nil, status.Errorf(codes.InvalidArgument, "validate request failed: %s", err.Error())
	}

	downReq := &server.CreateClassicRestoreInput{CreateClassicRestoreRequest: upReq}

	err = s.ngService.CreateClassicRestore(ctx, downReq)
	if err != nil {
		tilog.Error(ctx, "CreateClassicRestore failed",
			zap.Error(err), zap.String("request", utils.RedactedJSON(downReq)))
		return nil, preserveStatusOrInternal("CreateClassicRestore", err)
	}
	tilog.Info(ctx, "CreateClassicRestore succeeded", zap.String("request", utils.RedactedJSON(downReq)))

	return &emptypb.Empty{}, nil
}

func (s *appImpl) GetClassicRestore(ctx context.Context, upReq *apiv1.GetClassicRestoreRequest) (*apiv1.ClassicRestore, error) {
	downReq := &server.GetClassicRestoreInput{GetClassicRestoreRequest: upReq}
	downResp, err := s.ngService.GetClassicRestore(ctx, downReq)
	if err != nil {
		return nil, err
	}
	return downResp.ClassicRestore, nil
}

func (s *appImpl) DeleteClassicRestore(ctx context.Context, upReq *apiv1.DeleteClassicRestoreRequest) (*emptypb.Empty, error) {
	downReq := &server.DeleteClassicRestoreInput{DeleteClassicRestoreRequest: upReq}
	err := s.ngService.DeleteClassicRestore(ctx, downReq)
	if err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

func (s *appImpl) CreateManualBackup(ctx context.Context, upReq *apiv1.CreateManualBackupRequest) (*emptypb.Empty, error) {
	if err := validateCreateManualBackupRequest(upReq); err != nil {
		tilog.Error(ctx, "invalid CreateManualBackupRequest", zap.Error(err), zap.String("request", upReq.String()))
		return nil, status.Errorf(codes.InvalidArgument, "validate request failed: %s", err.Error())
	}

	downReq := &server.CreateManualBackupInput{CreateManualBackupRequest: upReq}
	if err := s.ngService.CreateManualBackup(ctx, downReq); err != nil {
		tilog.Error(ctx, "CreateManualBackup failed", zap.Error(err), zap.String("request", utils.JSON(downReq)))
		return nil, preserveStatusOrInternal("CreateManualBackup", err)
	}
	tilog.Info(ctx, "CreateManualBackup succeeded", zap.String("request", utils.JSON(downReq)))
	return &emptypb.Empty{}, nil
}

func (s *appImpl) GetManualBackup(ctx context.Context, upReq *apiv1.GetManualBackupRequest) (*apiv1.ManualBackup, error) {
	downReq := &server.GetManualBackupInput{GetManualBackupRequest: upReq}
	downResp, err := s.ngService.GetManualBackup(ctx, downReq)
	if err != nil {
		return nil, err
	}
	return downResp.ManualBackup, nil
}

func (s *appImpl) DeleteManualBackup(ctx context.Context, upReq *apiv1.DeleteManualBackupRequest) (*emptypb.Empty, error) {
	downReq := &server.DeleteManualBackupInput{DeleteManualBackupRequest: upReq}
	if err := s.ngService.DeleteManualBackup(ctx, downReq); err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

func (s *appImpl) CreateManualBackupRestore(ctx context.Context, upReq *apiv1.CreateManualBackupRestoreRequest) (*emptypb.Empty, error) {
	downReq := &server.CreateManualBackupRestoreInput{CreateManualBackupRestoreRequest: upReq}
	if err := s.ngService.CreateManualBackupRestore(ctx, downReq); err != nil {
		tilog.Error(ctx, "CreateManualBackupRestore failed", zap.Error(err), zap.String("request", utils.JSON(downReq)))
		return nil, preserveStatusOrInternal("CreateManualBackupRestore", err)
	}
	tilog.Info(ctx, "CreateManualBackupRestore succeeded", zap.String("request", utils.JSON(downReq)))
	return &emptypb.Empty{}, nil
}

func (s *appImpl) DeleteManualBackupRestore(ctx context.Context, upReq *apiv1.DeleteManualBackupRestoreRequest) (*emptypb.Empty, error) {
	downReq := &server.DeleteManualBackupRestoreInput{DeleteManualBackupRestoreRequest: upReq}
	if err := s.ngService.DeleteManualBackupRestore(ctx, downReq); err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

func (s *appImpl) GetManualBackupRestore(ctx context.Context, upReq *apiv1.GetManualBackupRestoreRequest) (*apiv1.ManualBackupRestore, error) {
	downReq := &server.GetManualBackupRestoreInput{GetManualBackupRestoreRequest: upReq}
	downResp, err := s.ngService.GetManualBackupRestore(ctx, downReq)
	if err != nil {
		return nil, err
	}
	return downResp.ManualBackupRestore, nil
}

func (s *appImpl) GetManualBackupRestorePostActions(ctx context.Context, req *apiv1.GetManualBackupRestorePostActionsRequest) (*apiv1.GetManualBackupRestorePostActionsResponse, error) {
	downReq := &server.GetManualBackupRestorePostActionsInput{GetManualBackupRestorePostActionsRequest: req}
	resp, err := s.ngService.GetManualBackupRestorePostActions(ctx, downReq)
	if err != nil {
		return nil, err
	}
	return resp.GetManualBackupRestorePostActionsResponse, nil
}

func (s *appImpl) TestClassicBackupAccess(ctx context.Context, upReq *apiv1.TestClassicBackupAccessRequest) (*apiv1.TestClassicBackupAccessResponse, error) {
	err := validateTestClassicBackupAccessRequest(upReq)
	if err != nil {
		tilog.Error(ctx, "invalid TestClassicBackupAccessRequest", zap.Error(err), zap.String("request", utils.RedactedJSON(upReq)))
		return nil, status.Errorf(codes.InvalidArgument, "validate request failed: %s", err.Error())
	}

	downReq := &server.TestClassicBackupAccessInput{TestClassicBackupAccessRequest: upReq}
	downResp, err := s.ngService.TestClassicBackupAccess(ctx, downReq)
	if err != nil {
		return nil, err
	}
	return downResp.TestClassicBackupAccessResponse, nil
}

func (s *appImpl) UpdateAutoBackupMetering(ctx context.Context, req *apiv1.UpdateAutoBackupMeteringRequest) (*emptypb.Empty, error) {
	tilog.Info(ctx, "received update auto backup metering request",
		zap.String("clusterID", req.GetClusterId()),
		zap.String("mode", req.GetMode().String()),
		zap.String("skuName", req.GetSkuName()),
		zap.String("skuValue", req.GetSkuValue()),
		zap.Bool("hasRetentionDays", req.RetentionDays != nil),
		zap.Uint32("retentionDays", req.GetRetentionDays()),
		zap.String("dailyBackupTimeUTC", req.GetDailyBackupTimeUtc()),
	)
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}

	if req.GetSkuName() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "sku name is empty")
	}

	if req.GetSkuValue() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "sku value is empty")
	}
	if req.GetMode() == apiv1.AutoBackupMode_FLEXIBLE_RETENTION {
		if req.RetentionDays == nil || req.GetRetentionDays() > 33 {
			return nil, status.Errorf(codes.InvalidArgument, "flexible retention days must be set and between 0 and 33")
		}
		if !isValidBackupTime(req.GetDailyBackupTimeUtc()) {
			return nil, status.Errorf(codes.InvalidArgument, "daily backup time must be in HH:mm format")
		}
	}

	err := s.ngService.UpdateAutoBackupMetering(ctx, req)
	if err != nil {
		tilog.Error(ctx, "update auto backup metering request failed",
			zap.String("clusterID", req.GetClusterId()),
			zap.String("mode", req.GetMode().String()),
			zap.Error(err),
		)
		return nil, preserveStatusOrInternal("UpdateAutoBackupMetering", err)
	}
	tilog.Info(ctx, "update auto backup metering request completed",
		zap.String("clusterID", req.GetClusterId()),
		zap.String("mode", req.GetMode().String()),
	)
	return &emptypb.Empty{}, nil
}

func (s *appImpl) ReportAutoBackupMetering(ctx context.Context, req *apiv1.ReportAutoBackupMeteringRequest) (*emptypb.Empty, error) {
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared pool id is empty")
	}
	for _, backup := range req.GetBackups() {
		if backup.GetBackupId() == "" {
			return nil, status.Errorf(codes.InvalidArgument, "backup id is empty")
		}
	}

	if err := s.ngService.ReportAutoBackupMetering(ctx, req); err != nil {
		return nil, status.Errorf(codes.Internal, "ReportAutoBackupMetering failed: %s", err.Error())
	}
	return &emptypb.Empty{}, nil
}

func (s *appImpl) CheckPods(ctx context.Context, req *apiv1.CheckPodsRequest) (*apiv1.CheckPodsResponse, error) {
	out, err := s.ngService.CheckPods(ctx, req.GetSharedPoolId(), req.GetClusterId(), req.ComponentType, req.From)
	if err != nil {
		tilog.Error(ctx, "CheckPods failed",
			zap.Error(err), zap.String("request", utils.JSON(req)))
		return nil, status.Errorf(codes.Internal, "CheckPodRecreate failed: %s", err.Error())
	}
	return out, nil
}

// DM cluster
func (s *appImpl) CreateDMCluster(ctx context.Context, req *apiv1.CreateDMClusterRequest) (*apiv1.DMCluster, error) {
	if req.GetDmCluster().GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared pool id is empty")
	}

	if req.GetDmCluster().GetId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "dm cluster id is empty")
	}

	return s.ngService.CreateDMCluster(ctx, req.GetDmCluster())
}

func (s *appImpl) ListDMClusters(ctx context.Context, req *apiv1.ListDMClustersRequest) (*apiv1.ListDMClustersResponse, error) {
	dmClusters, err := s.ngService.ListDMClusters(ctx, req.GetSharedPoolId())
	if err != nil {
		return nil, err
	}
	resp := &apiv1.ListDMClustersResponse{
		DmClusters: dmClusters,
		TotalCount: int32(len(dmClusters)),
	}
	return resp, nil
}

func (s *appImpl) GetDMCluster(ctx context.Context, req *apiv1.GetDMClusterRequest) (*apiv1.DMCluster, error) {
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared pool id is empty")
	}

	if req.GetDmClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "dm cluster id is empty")
	}
	return s.ngService.GetDMCluster(ctx, req.GetSharedPoolId(), req.GetDmClusterId())
}

func (s *appImpl) ScaleDMCluster(ctx context.Context, req *apiv1.ScaleDMClusterRequest) (*apiv1.DMCluster, error) {
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared pool id is empty")
	}

	if req.GetDmClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "dm cluster id is empty")
	}

	return s.ngService.ScaleDMCluster(ctx, req.GetSharedPoolId(), req.GetDmClusterId(), req.GetMasterNodeQuota(), req.GetWorkerNodeQuota(), req.GetForce())
}

func (s *appImpl) DeleteDMCluster(ctx context.Context, req *apiv1.DeleteDMClusterRequest) (*emptypb.Empty, error) {
	if req.GetSharedPoolId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared pool id is empty")
	}

	if req.GetDmClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "dm cluster id is empty")
	}

	err := s.ngService.DeleteDMCluster(ctx, req.GetSharedPoolId(), req.GetDmClusterId())
	if err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

func (s *appImpl) ListExports(ctx context.Context, req *apiv1.ListExportsRequest) (*apiv1.ListExportsResponse, error) {
	exports, err := s.ngService.ListExports(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	return &apiv1.ListExportsResponse{
		Exports:   exports,
		TotalSize: int32(len(exports)),
	}, nil
}

func (s *appImpl) GetExport(ctx context.Context, req *apiv1.GetExportRequest) (*apiv1.Export, error) {
	return s.ngService.GetExport(ctx, req.GetClusterId(), req.GetExportId())
}

func (s *appImpl) CreateExport(ctx context.Context, req *apiv1.CreateExportRequest) (*apiv1.Export, error) {
	if req.GetExport() == nil {
		return nil, status.Errorf(codes.InvalidArgument, "export is nil")
	}
	req.Export.ClusterId = req.GetClusterId()
	return s.ngService.CreateExport(ctx, req.GetExport())
}

func (s *appImpl) CancelExport(ctx context.Context, req *apiv1.CancelExportRequest) (*apiv1.Export, error) {
	return s.ngService.CancelExport(ctx, req.GetClusterId(), req.GetExportId())
}

// TiDB Cluster
func (s *appImpl) CreateTiDBCluster(ctx context.Context, req *apiv1.CreateTiDBClusterRequest) (*apiv1.Cluster, error) {
	if req.GetId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}

	if req.GetTenantId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "tenant id is empty")
	}

	if req.GetRequestUnit() == nil && req.GetCapacityMode() != apiv1.CapacityMode_TOPOLOGY_DRIVEN {
		return nil, status.Errorf(codes.InvalidArgument, "ru setting unit is nil")
	}

	tidbCluster := &entity.TiDBCluster{
		Id:                    req.GetId(),
		TenantId:              req.GetTenantId(),
		SharedPoolId:          req.GetSharedPoolId(),
		RequestUnit:           req.GetRequestUnit(),
		GatewayIdentity:       req.GetGatewayIdentity(),
		SecurityConfiguration: req.GetSecurityConfiguration(),
		RuVersion:             req.GetRuVersion(),
		CapacityMode:          req.GetCapacityMode(),
		TopologySpec:          req.GetTopologySpec(),
	}

	tidbCluster, err := s.ngService.CreateTiDBCluster(ctx, tidbCluster)
	if err != nil {
		return nil, err
	}
	return tidbCluster, nil
}

func (s *appImpl) ListTiDBClusters(ctx context.Context, req *apiv1.ListTiDBClustersRequest) (*apiv1.ListTiDBClustersResponse, error) {
	tidbClusters, err := s.ngService.ListTiDBClusters(ctx, req.GetSharedPoolId(), req.GetTenantId())
	if err != nil {
		return nil, err
	}

	resp := &apiv1.ListTiDBClustersResponse{
		Clusters:   tidbClusters,
		TotalCount: int32(len(tidbClusters)),
	}

	return resp, nil
}

func (s *appImpl) GetTiDBCluster(ctx context.Context, req *apiv1.GetTiDBClusterRequest) (*apiv1.Cluster, error) {
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}

	tidbCluster, err := s.ngService.GetTiDBCluster(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	return tidbCluster, nil
}

func (s *appImpl) UpdateTiDBCluster(ctx context.Context, req *apiv1.UpdateTiDBClusterRequest) (*apiv1.Cluster, error) {
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}

	if req.GetRequestUnit() == nil {
		return nil, status.Errorf(codes.InvalidArgument, "ru setting unit is nil")
	}

	tidbCluster := &entity.TiDBCluster{
		Id:          req.GetClusterId(),
		RequestUnit: req.GetRequestUnit(),
		RuVersion:   req.GetRuVersion(),
	}

	tidbCluster, err := s.ngService.UpdateTiDBCluster(ctx, tidbCluster, req.GetForce()) // force is false by default
	if err != nil {
		return nil, err
	}
	return tidbCluster, nil
}

func (s *appImpl) ForceUpdateGatewayIdentity(ctx context.Context, req *apiv1.ForceUpdateGatewayIdentityRequest) (*emptypb.Empty, error) {
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}

	if req.GetGatewayIdentity() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "gateway identity is empty")
	}

	if err := s.ngService.ForceUpdateGatewayIdentity(ctx, req.GetClusterId(), req.GetGatewayIdentity()); err != nil {
		return nil, err
	}

	return &emptypb.Empty{}, nil
}

func (s *appImpl) DeleteTiDBCluster(ctx context.Context, req *apiv1.DeleteTiDBClusterRequest) (*apiv1.Cluster, error) {
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}

	tidbCluster, err := s.ngService.DeleteTiDBCluster(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	return tidbCluster, nil
}

func (s *appImpl) GetTiDBClusterTiProxyNodeQuota(ctx context.Context, req *apiv1.GetTiDBClusterTiProxyNodeQuotaRequest) (*apiv1.NodeQuota, error) {
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}

	nodeQuota, err := s.ngService.GetTiDBClusterTiProxyNodeQuota(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	return nodeQuota, nil
}

func (s *appImpl) SetTiProxyExternalDNS(ctx context.Context, req *apiv1.SetTiProxyExternalDNSRequest) (*emptypb.Empty, error) {
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}
	if req.GetTidbGroupId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "tidb group id is empty")
	}

	err := s.ngService.SetTiProxyExternalDNS(ctx, req)
	if err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

func (s *appImpl) GetProviderRole(ctx context.Context, req *apiv1.GetProviderRoleRequest) (*apiv1.ProviderRole, error) {
	providerRole, err := s.ngService.GetProviderRole(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	return providerRole, nil
}

func (s *appImpl) ListPlanReplayerFiles(
	ctx context.Context,
	req *apiv1.ListPlanReplayerFilesRequest,
) (*apiv1.ListPlanReplayerFilesResponse, error) {
	return s.ngService.ListPlanReplayerFiles(ctx, req)
}

func (s *appImpl) GeneratePlanReplayerFileDownloadURL(
	ctx context.Context,
	req *apiv1.GeneratePlanReplayerFileDownloadURLRequest,
) (*apiv1.GeneratePlanReplayerFileDownloadURLResponse, error) {
	return s.ngService.GeneratePlanReplayerFileDownloadURL(ctx, req)
}

func (s *appImpl) GetImportRoleConfig(ctx context.Context, req *apiv1.GetImportRoleConfigRequest) (*apiv1.ImportRoleConfig, error) {
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}

	config, err := s.ngService.GetImportRoleConfig(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	return config, nil
}

func (s *appImpl) SetAuditLogSetting(ctx context.Context, req *apiv1.SetAuditLogSettingRequest) (*apiv1.Cluster_AuditLogSetting, error) {
	tidbClusterId := req.GetClusterId()
	if tidbClusterId == "" {
		return nil, status.Errorf(codes.InvalidArgument, "tidb cluster id is empty")
	}

	setting, err := s.ngService.SetAuditLogSetting(ctx, tidbClusterId, req.GetAuditLogSetting())
	if err != nil {
		return nil, err
	}
	return setting, nil
}

func (s *appImpl) UpdateBackupGcSetting(ctx context.Context, upReq *apiv1.UpdateBackupGcSettingRequest) (*apiv1.BackupGcSetting, error) {
	downReq := &server.UpdateBackupGcSettingInput{UpdateBackupGcSettingRequest: upReq}
	downResp, err := s.ngService.UpdateBackupGcSetting(ctx, downReq)
	if err != nil {
		return nil, err
	}
	return downResp.BackupGcSetting, nil
}

func (s *appImpl) GetBackupGcSetting(ctx context.Context, upReq *apiv1.GetBackupGcSettingRequest) (*apiv1.BackupGcSetting, error) {
	downReq := &server.GetBackupGcSettingInput{GetBackupGcSettingRequest: upReq}
	downResp, err := s.ngService.GetBackupGcSetting(ctx, downReq)
	if err != nil {
		return nil, err
	}
	return downResp.BackupGcSetting, nil
}

func (s *appImpl) ApplySemConfig(ctx context.Context, req *apiv1.ApplySemConfigRequest) (*emptypb.Empty, error) {
	err := s.ngService.ApplySemConfig(ctx, req.GetSharedPoolId())
	if err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

// TiDB Group
func (s *appImpl) CreateTiDBGroup(ctx context.Context, req *apiv1.CreateTiDBGroupRequest) (*apiv1.TiDBGroup, error) {
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}

	tidbGroup := &entity.TiDBGroup{
		TiDBGroup: apiv1.TiDBGroup{
			Id:                    req.GetId(),
			TenantId:              req.GetTenantId(),
			ClusterId:             req.GetClusterId(),
			SqlEndpoint:           req.GetSqlEndpoint(),
			SecurityConfiguration: req.GetSecurityConfiguration(),
			Type:                  req.GetType(),
		},
		TopologySpec: req.GetTopologySpec(),
	}

	tidbGroup, err := s.ngService.CreateTiDBGroup(ctx, tidbGroup)
	if err != nil {
		return nil, err
	}
	return &tidbGroup.TiDBGroup, nil
}

func (s *appImpl) ListTiDBGroups(ctx context.Context, req *apiv1.ListTiDBGroupsRequest) (*apiv1.ListTiDBGroupsResponse, error) {
	tidbGroups, err := s.ngService.ListTiDBGroups(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}

	resp := &apiv1.ListTiDBGroupsResponse{
		TidbGroups: make([]*apiv1.TiDBGroup, 0),
		TotalCount: int32(len(tidbGroups)),
	}

	for _, tidbGroup := range tidbGroups {
		resp.TidbGroups = append(resp.TidbGroups, &tidbGroup.TiDBGroup)
	}

	return resp, nil
}

func (s *appImpl) GetTiDBGroup(ctx context.Context, req *apiv1.GetTiDBGroupRequest) (*apiv1.TiDBGroup, error) {
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}

	tidbGroupId := req.GetTidbGroupId()
	if tidbGroupId == "" {
		return nil, status.Errorf(codes.InvalidArgument, "tidb group id is empty")
	}

	tidbGroup, err := s.ngService.GetTiDBGroup(ctx, req.GetClusterId(), tidbGroupId)
	if err != nil {
		return nil, err
	}
	return &tidbGroup.TiDBGroup, nil
}

func (s *appImpl) DeleteTiDBGroup(ctx context.Context, req *apiv1.DeleteTiDBGroupRequest) (*apiv1.TiDBGroup, error) {
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}

	tidbGroupId := req.GetTidbGroupId()
	if tidbGroupId == "" {
		return nil, status.Errorf(codes.InvalidArgument, "tidb group id is empty")
	}

	tidbGroup, err := s.ngService.DeleteTiDBGroup(ctx, req.GetClusterId(), tidbGroupId)
	if err != nil {
		return nil, err
	}
	return &tidbGroup.TiDBGroup, nil
}

func (s *appImpl) UpdateTiDBGroup(ctx context.Context, req *apiv1.UpdateTiDBGroupRequest) (*apiv1.TiDBGroup, error) {
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}

	if req.GetTidbGroupId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "tidb group id is empty")
	}

	tidbGroup := &entity.TiDBGroup{
		TiDBGroup: apiv1.TiDBGroup{
			Id:          req.GetTidbGroupId(),
			ClusterId:   req.GetClusterId(),
			SqlEndpoint: req.GetSqlEndpoint(),
		},
	}

	tidbGroup, err := s.ngService.UpdateTiDBGroup(ctx, tidbGroup, false) // force is false by default
	if err != nil {
		return nil, err
	}
	return &tidbGroup.TiDBGroup, nil
}

func (s *appImpl) ScaleTiDBGroup(ctx context.Context, req *apiv1.ScaleTiDBGroupRequest) (*apiv1.TiDBGroup, error) {
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}

	if req.GetTidbGroupId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "tidb group id is empty")
	}

	tidbGroup := &entity.TiDBGroup{
		TiDBGroup: apiv1.TiDBGroup{
			Id:        req.GetTidbGroupId(),
			ClusterId: req.GetClusterId(),
			NodeQuota: req.GetNodeQuota(),
		},
	}

	tidbGroup, err := s.ngService.UpdateTiDBGroup(ctx, tidbGroup, req.GetForce())
	if err != nil {
		return nil, err
	}
	return &tidbGroup.TiDBGroup, nil
}

func (s *appImpl) ApplyTiDBGroup(ctx context.Context, req *apiv1.ApplyTiDBGroupRequest) (*emptypb.Empty, error) {
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}

	if req.GetTidbGroupId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "tidb group id is empty")
	}

	err := s.ngService.ApplyTiDBGroup(ctx, req.GetClusterId(), req.GetTidbGroupId(), req.GetForce())
	if err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

func (s *appImpl) CreatePrivateLinkService(ctx context.Context, req *apiv1.CreatePrivateLinkServiceRequest) (*apiv1.PrivateLinkService, error) {
	if req.GetPrivateLinkService() == nil {
		return nil, status.Errorf(codes.InvalidArgument, "private link service is nil")
	}

	if req.GetPrivateLinkService().GetTidbGroupId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "tidb group id is empty")
	}

	privateLinkService, err := s.ngService.CreatePrivateLinkService(ctx, req.GetClusterId(), req.GetPrivateLinkService())
	if err != nil {
		return nil, err
	}
	return privateLinkService, nil
}

func (s *appImpl) GetPrivateLinkService(ctx context.Context, req *apiv1.GetPrivateLinkServiceRequest) (*apiv1.PrivateLinkService, error) {
	privateLinkService, err := s.ngService.GetPrivateLinkService(ctx, req.GetClusterId(), req.GetTidbGroupId(), req.GetPrivateLinkServiceId())
	if err != nil {
		return nil, err
	}
	return privateLinkService, nil
}

func (s *appImpl) UpdatePrivateLinkService(ctx context.Context, req *apiv1.UpdatePrivateLinkServiceRequest) (*apiv1.PrivateLinkService, error) {
	privateLinkService, err := s.ngService.UpdatePrivateLinkService(ctx, req.GetClusterId(), req.GetTidbGroupId(), req.GetPrivateLinkServiceId(), req.GetPrivateLinkConnections())
	if err != nil {
		return nil, err
	}
	return privateLinkService, nil
}

func (s *appImpl) DeletePrivateLinkService(ctx context.Context, req *apiv1.DeletePrivateLinkServiceRequest) (*emptypb.Empty, error) {
	err := s.ngService.DeletePrivateLinkService(ctx, req.GetClusterId(), req.GetTidbGroupId(), req.GetPrivateLinkServiceId())
	if err != nil {
		return nil, err
	}

	return &emptypb.Empty{}, nil
}

func (s *appImpl) GetRuConfig(ctx context.Context, req *apiv1.GetRuConfigRequest) (*apiv1.RuConfig, error) {
	id := req.GetId()
	if id == "" {
		return nil, status.Errorf(codes.InvalidArgument, "id is empty")
	}

	ruConfig, err := s.ngService.GetRuConfig(ctx, id, req.GetScope())
	if err != nil {
		return nil, err
	}
	return &ruConfig.RuConfig, nil
}

func (s *appImpl) SetRuConfig(ctx context.Context, req *apiv1.SetRuConfigRequest) (*apiv1.RuConfig, error) {
	id := req.GetId()
	if id == "" {
		return nil, status.Errorf(codes.InvalidArgument, "id is empty")
	}

	ruConfig, err := s.ngService.SetRuConfig(ctx, id, req.GetScope(), &entity.RuConfig{RuConfig: apiv1.RuConfig{
		FeatureFlags: req.GetRuConfig().GetFeatureFlags(),
		Components:   req.GetRuConfig().GetComponents(),
	}})
	if err != nil {
		return nil, err
	}
	return &ruConfig.RuConfig, nil
}

func (s *appImpl) SetHPASpec(ctx context.Context, req *apiv1.SetHPASpecRequest) (*emptypb.Empty, error) {
	if err := s.ngService.SetHPASpec(
		ctx,
		req.GetSharedPoolId(),
		req.GetId(),
		req.GetComponentType(),
		req.GetSpec(),
	); err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

func (s *appImpl) SetScaleOutConfig(ctx context.Context, req *apiv1.SetScaleOutConfigRequest) (*emptypb.Empty, error) {
	if err := s.ngService.SetScaleOutConfig(
		ctx,
		req.GetSharedPoolId(),
		req.GetId(),
		req.GetComponentType(),
		req.GetConfig(),
	); err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

func (s *appImpl) SetHPAScaleBehavior(ctx context.Context, req *apiv1.SetHPAScaleBehaviorRequest) (*emptypb.Empty, error) {
	if err := s.ngService.SetHPAScaleBehavior(
		ctx,
		req.GetSharedPoolId(),
		req.GetGroupId(),
		req.GetComponentType(),
		req.ScaleUpEnabled,
		req.ScaleDownEnabled,
	); err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

func (s *appImpl) GetHPASpec(ctx context.Context, req *apiv1.GetHPASpecRequest) (*apiv1.GetHPASpecResponse, error) {
	specs, err := s.ngService.GetHPASpec(
		ctx,
		req.GetSharedPoolId(),
	)
	if err != nil {
		return nil, err
	}
	out := new(apiv1.GetHPASpecResponse)
	out.SharedPoolId = req.GetSharedPoolId()
	out.ComponentHpaSpecs = make([]*apiv1.ComponentHPASpec, 0, len(specs))
	for i := range specs {
		spec := &specs[i]
		out.ComponentHpaSpecs = append(out.ComponentHpaSpecs, spec)
	}
	return out, nil
}

func (s *appImpl) CreateNodeInstanceGroup(ctx context.Context, req *apiv1.CreateNodeInstanceGroupRequest) (*apiv1.NodeInstanceGroup, error) {
	if req.GetNodeInstanceGroup() == nil {
		return nil, status.Errorf(codes.InvalidArgument, "node_instance_group is nil")
	}
	group := req.GetNodeInstanceGroup()
	if group.GetNodeSpec() == nil {
		return nil, status.Errorf(codes.InvalidArgument, "node_instance_group.node_spec is nil")
	}
	fallbacks, err := toEntityFallbackInstances(group.GetFallbackInstances())
	if err != nil {
		return nil, err
	}
	entityGroup := &entity.NodeInstanceGroup{
		NodeSpec:            group.GetNodeSpec(),
		PrimaryInstanceType: group.GetPrimaryInstanceType(),
		FallbackInstances:   fallbacks,
	}

	created, err := s.ngService.CreateNodeInstanceGroup(ctx, entityGroup)
	if err != nil {
		return nil, err
	}
	return toNodeInstanceGroupPB(created), nil
}

func (s *appImpl) ListNodeInstanceGroups(ctx context.Context, req *apiv1.ListNodeInstanceGroupsRequest) (*apiv1.ListNodeInstanceGroupsResponse, error) {
	groups, err := s.ngService.ListNodeInstanceGroups(ctx, req.GetNodeSpec())
	if err != nil {
		return nil, err
	}
	resp := &apiv1.ListNodeInstanceGroupsResponse{
		NodeInstanceGroups: make([]*apiv1.NodeInstanceGroup, 0, len(groups)),
	}
	for _, group := range groups {
		resp.NodeInstanceGroups = append(resp.NodeInstanceGroups, toNodeInstanceGroupPB(group))
	}
	return resp, nil
}

func (s *appImpl) DeprecateNodeInstanceGroup(ctx context.Context, req *apiv1.DeprecateNodeInstanceGroupRequest) (*apiv1.NodeInstanceGroup, error) {
	group, err := s.ngService.DeprecateNodeInstanceGroup(ctx, req.GetName(), req.GetPrimaryInstanceType())
	if err != nil {
		return nil, err
	}
	return toNodeInstanceGroupPB(group), nil
}

func (s *appImpl) SetNodeInstanceGroupStatus(ctx context.Context, req *apiv1.SetNodeInstanceGroupStatusRequest) (*apiv1.NodeInstanceGroup, error) {
	nodeGroupStatus, err := toNodeInstanceGroupStatus(req.GetStatus())
	if err != nil {
		return nil, err
	}
	group, err := s.ngService.SetNodeInstanceGroupStatus(ctx, req.GetName(), req.GetPrimaryInstanceType(), nodeGroupStatus)
	if err != nil {
		return nil, err
	}
	return toNodeInstanceGroupPB(group), nil
}

func (s *appImpl) ListDataplaneNodeInstanceGroups(ctx context.Context, req *apiv1.ListDataplaneNodeInstanceGroupsRequest) (*apiv1.ListDataplaneNodeInstanceGroupsResponse, error) {
	overrides, err := s.ngService.ListDataplaneNodeInstanceGroups(ctx, req.GetDataplaneName())
	if err != nil {
		return nil, err
	}
	resp := &apiv1.ListDataplaneNodeInstanceGroupsResponse{
		DataplaneNodeInstanceGroups: make([]*apiv1.DataplaneNodeInstanceGroup, 0, len(overrides)),
	}
	for _, override := range overrides {
		resp.DataplaneNodeInstanceGroups = append(resp.DataplaneNodeInstanceGroups, toDataplaneNodeInstanceGroupPB(override))
	}
	return resp, nil
}

func (s *appImpl) SetDataplaneNodeInstanceGroupStatus(ctx context.Context, req *apiv1.SetDataplaneNodeInstanceGroupStatusRequest) (*apiv1.DataplaneNodeInstanceGroup, error) {
	if req.GetNodeSpec() == nil {
		return nil, status.Errorf(codes.InvalidArgument, "node_spec is nil")
	}
	statusValue, err := toDataplaneNodeInstanceGroupStatus(req.GetStatus())
	if err != nil {
		return nil, err
	}
	override, err := s.ngService.SetDataplaneNodeInstanceGroupStatus(ctx, req.GetDataplaneName(), entity.GetComponentNodeSpec(req.GetNodeSpec()), req.GetPrimaryInstanceType(), statusValue)
	if err != nil {
		return nil, err
	}
	return toDataplaneNodeInstanceGroupPB(override), nil
}

func (s *appImpl) SetNodeInstanceGroupInstanceTypeStatus(ctx context.Context, req *apiv1.SetNodeInstanceGroupInstanceTypeStatusRequest) (*apiv1.NodeInstanceGroup, error) {
	instanceStatus, err := toInstanceTypeStatus(req.GetStatus())
	if err != nil {
		return nil, err
	}
	group, err := s.ngService.SetNodeInstanceGroupInstanceTypeStatus(ctx, req.GetName(), req.GetPrimaryInstanceType(), req.GetInstanceType(), instanceStatus)
	if err != nil {
		return nil, err
	}
	return toNodeInstanceGroupPB(group), nil
}

func (s *appImpl) DeleteNodeInstanceGroup(ctx context.Context, req *apiv1.DeleteNodeInstanceGroupRequest) (*apiv1.NodeInstanceGroup, error) {
	group, err := s.ngService.DeleteNodeInstanceGroup(ctx, req.GetName(), req.GetPrimaryInstanceType())
	if err != nil {
		return nil, err
	}
	return toNodeInstanceGroupPB(group), nil
}

func (s *appImpl) UpdateNodeInstanceGroup(ctx context.Context, req *apiv1.UpdateNodeInstanceGroupRequest) (*apiv1.NodeInstanceGroup, error) {
	fallbacks, err := toEntityFallbackInstances(req.GetFallbackInstances())
	if err != nil {
		return nil, err
	}
	group, err := s.ngService.UpdateNodeInstanceGroup(ctx, req.GetName(), req.GetPrimaryInstanceType(), fallbacks)
	if err != nil {
		return nil, err
	}
	return toNodeInstanceGroupPB(group), nil
}

func toNodeInstanceGroupPB(group *entity.NodeInstanceGroup) *apiv1.NodeInstanceGroup {
	if group == nil {
		return nil
	}
	if group.GetName() == "" && group.GetNodeSpec() != nil {
		group.Name = entity.GetComponentNodeSpec(group.GetNodeSpec())
	}
	return group
}

func toNodeInstanceGroupStatus(value apiv1.NodeInstanceGroup_Status) (apiv1.NodeInstanceGroup_Status, error) {
	switch value {
	case apiv1.NodeInstanceGroup_ACTIVE, apiv1.NodeInstanceGroup_DEPRECATED, apiv1.NodeInstanceGroup_RETIRING:
		return value, nil
	default:
		return apiv1.NodeInstanceGroup_STATUS_UNSPECIFIED, status.Errorf(codes.InvalidArgument, "invalid node instance group status: %v", value)
	}
}

func toDataplaneNodeInstanceGroupStatus(value apiv1.DataplaneNodeInstanceGroup_Status) (apiv1.DataplaneNodeInstanceGroup_Status, error) {
	switch value {
	case apiv1.DataplaneNodeInstanceGroup_ACTIVE, apiv1.DataplaneNodeInstanceGroup_DEPRECATED:
		return value, nil
	default:
		return apiv1.DataplaneNodeInstanceGroup_STATUS_UNSPECIFIED, status.Errorf(codes.InvalidArgument, "invalid dataplane node instance group status: %v", value)
	}
}

func toInstanceTypeStatus(value apiv1.InstanceTypeStatus) (apiv1.InstanceTypeStatus, error) {
	switch value {
	case apiv1.InstanceTypeStatus_NORMAL, apiv1.InstanceTypeStatus_DISABLED:
		return value, nil
	default:
		return apiv1.InstanceTypeStatus_INSTANCE_TYPE_STATUS_UNSPECIFIED, status.Errorf(codes.InvalidArgument, "invalid instance type status: %v", value)
	}
}

func toDataplaneNodeInstanceGroupPB(override *entity.DataplaneNodeInstanceGroup) *apiv1.DataplaneNodeInstanceGroup {
	return override
}

// SetCassandraProxy forwards to the ngService. Request-body validation beyond
// a non-empty cluster_id is deferred to the ngService layer.
func (s *appImpl) SetCassandraProxy(ctx context.Context, req *apiv1.SetCassandraProxyRequest) (*apiv1.SetCassandraProxyResponse, error) {
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}
	return s.ngService.SetCassandraProxy(ctx, req)
}

// GetCassandraProxy forwards to the ngService.
func (s *appImpl) GetCassandraProxy(ctx context.Context, req *apiv1.GetCassandraProxyRequest) (*apiv1.GetCassandraProxyResponse, error) {
	if req.GetClusterId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "cluster id is empty")
	}
	return s.ngService.GetCassandraProxy(ctx, req)
}

func toEntityFallbackInstances(values []*apiv1.NodeInstanceGroup_FallbackInstance) ([]*entity.NodeInstanceGroupFallbackInstance, error) {
	if len(values) == 0 {
		return nil, nil
	}
	results := make([]*entity.NodeInstanceGroupFallbackInstance, 0, len(values))
	for _, value := range values {
		if value == nil {
			continue
		}
		if value.GetNodeSpec() == nil {
			return nil, status.Errorf(codes.InvalidArgument, "fallback_instances.node_spec is nil")
		}
		if value.GetPriority() == 0 {
			return nil, status.Errorf(codes.InvalidArgument, "fallback_instances.priority is required")
		}
		results = append(results, &apiv1.NodeInstanceGroup_FallbackInstance{
			InstanceType: value.GetInstanceType(),
			NodeSpec:     value.GetNodeSpec(),
			Priority:     value.GetPriority(),
		})
	}
	return results, nil
}
