package resourcepool

import (
	"context"
	"math"
	"strings"

	"google.golang.org/grpc/codes"
	"google.golang.org/protobuf/proto"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/resourcepoolcapacity"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func (s *serviceImpl) GetResourcePoolCapacityConstraints(
	ctx context.Context,
	request *globalngv1.GetResourcePoolCapacityConstraintsRequest,
) (*globalngv1.ResourcePoolCapacityConstraints, bizerr.BizError) {
	if request == nil || request.GetResourcePoolId() == 0 || request.GetOrganizationId() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "resource_pool_id and organization_id are required")
	}

	constraints, err := s.regionService.GetByocSharedComponentScaleConstraints(
		ctx,
		&globalngv1.GetByocSharedComponentScaleConstraintsRequest{
			ResourcePoolId: request.GetResourcePoolId(),
			OrganizationId: request.GetOrganizationId(),
		},
	)
	if err != nil {
		return nil, err
	}
	if constraints == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "resource pool capacity constraints are unavailable")
	}

	tikv, findErr := findUniqueScaleConstraint(
		constraints.GetComponents(),
		globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIKV,
	)
	if findErr != nil {
		return nil, findErr
	}
	compute, findErr := findUniqueScaleConstraint(
		constraints.GetComponents(),
		globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_COMPUTE,
	)
	if findErr != nil {
		return nil, findErr
	}
	currentCapacity, capacityErr := capacityFromConstraintComponents(tikv, compute)
	if capacityErr != nil {
		return nil, capacityErr
	}
	resourcePool, loadErr := s.resourcePoolRepo.Load(ctx, request.GetResourcePoolId(), false)
	if loadErr != nil {
		return nil, loadErr
	}
	if resourcePool == nil ||
		resourcePool.GetOwnerType() != globalngv1.ResourcePool_ORGANIZATION ||
		resourcePool.GetOwnerId() != request.GetOrganizationId() ||
		resourcePool.GetServicePlan() != globalcommonv1.ServicePlan_BYOC {
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "resource pool not found")
	}
	tikvSpecs, specErr := s.listAllConstraintNodeSpecs(
		ctx,
		request.GetOrganizationId(),
		resourcePool.GetProviderId(),
		resourcePool.GetRegionId(),
		globalngv1.NodeSpecComponentType_NODE_SPEC_COMPONENT_TYPE_TIKV,
	)
	if specErr != nil {
		return nil, specErr
	}
	computeSpecs, specErr := s.listAllConstraintNodeSpecs(
		ctx,
		request.GetOrganizationId(),
		resourcePool.GetProviderId(),
		resourcePool.GetRegionId(),
		globalngv1.NodeSpecComponentType_NODE_SPEC_COMPONENT_TYPE_TIFLASH,
	)
	if specErr != nil {
		return nil, specErr
	}
	tikvConstraints, constraintErr := publicComponentConstraints(
		tikv,
		globalngv1.NodeSpecComponentType_NODE_SPEC_COMPONENT_TYPE_TIKV,
		tikvSpecs,
	)
	if constraintErr != nil {
		return nil, constraintErr
	}
	computeConstraints, constraintErr := publicComponentConstraints(
		compute,
		globalngv1.NodeSpecComponentType_NODE_SPEC_COMPONENT_TYPE_TIFLASH,
		computeSpecs,
	)
	if constraintErr != nil {
		return nil, constraintErr
	}
	previewReasons := publicPreviewConstraintReasons(constraints.GetPreview(), constraints.GetComponents())
	if len(previewReasons) > 0 {
		tikvConstraints.BlockingReasons = append(tikvConstraints.GetBlockingReasons(), previewReasons...)
		computeConstraints.BlockingReasons = append(computeConstraints.GetBlockingReasons(), previewReasons...)
	}

	return &globalngv1.ResourcePoolCapacityConstraints{
		ResourcePoolId:  request.GetResourcePoolId(),
		CurrentCapacity: currentCapacity,
		Tikv:            tikvConstraints,
		Tiflash:         computeConstraints,
	}, nil
}

func publicPreviewConstraintReasons(
	preview *globalngv1.ByocSharedComponentScalePreviewSummary,
	components []*globalngv1.ByocSharedComponentScaleConstraint,
) []*globalngv1.CapacityConstraintReason {
	if preview == nil || (preview.GetAllowed() && len(preview.GetBlockingReasons()) == 0) {
		return nil
	}
	if len(preview.GetBlockingReasons()) == 0 {
		return []*globalngv1.CapacityConstraintReason{{
			Code:    "CAPACITY_CHANGE_NOT_ALLOWED",
			Message: "The requested capacity change is not currently allowed.",
		}}
	}
	componentMessages := make(map[string]struct{})
	for _, component := range components {
		for _, reason := range component.GetBlockingReasons() {
			if message := strings.TrimSpace(reason.GetMessage()); message != "" {
				componentMessages[message] = struct{}{}
			}
		}
	}
	result := make([]*globalngv1.CapacityConstraintReason, 0, len(preview.GetBlockingReasons()))
	seen := make(map[string]struct{}, len(preview.GetBlockingReasons()))
	for _, reason := range preview.GetBlockingReasons() {
		reason = strings.TrimSpace(reason)
		if _, isComponentReason := componentMessages[reason]; isComponentReason {
			continue
		}
		if _, exists := seen[reason]; exists {
			continue
		}
		seen[reason] = struct{}{}
		code, message := safeCapacityConstraintReason("", reason)
		result = append(result, &globalngv1.CapacityConstraintReason{Code: code, Message: message})
	}
	return result
}

func (s *serviceImpl) listAllConstraintNodeSpecs(
	ctx context.Context,
	organizationID uint64,
	providerID globalcommonv1.CloudProvider_Name,
	regionID globalcommonv1.Region_Name,
	componentType globalngv1.NodeSpecComponentType,
) ([]*globalngv1.ResourcePoolNodeSpec, bizerr.BizError) {
	var result []*globalngv1.ResourcePoolNodeSpec
	pageToken := ""
	for {
		response, err := s.regionService.ListNodeSpecs(ctx, &globalngv1.ListNodeSpecsRequest{
			OrganizationId:  organizationID,
			CloudProviderId: providerID,
			RegionId:        regionID,
			ComponentType:   componentType,
			PageSize:        100,
			PageToken:       pageToken,
		})
		if err != nil {
			return nil, err
		}
		if response == nil {
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "node spec discovery returned no response")
		}
		result = append(result, response.GetNodeSpecs()...)
		if response.GetNextPageToken() == "" {
			return result, nil
		}
		if response.GetNextPageToken() == pageToken {
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "node spec discovery returned a repeated page token")
		}
		pageToken = response.GetNextPageToken()
	}
}

func (s *serviceImpl) ResizeResourcePoolCapacity(
	ctx context.Context,
	request *globalngv1.ResizeResourcePoolCapacityRequest,
) (*globalngv1.ResizeResourcePoolCapacityResponse, bizerr.BizError) {
	if request == nil || request.GetResourcePoolId() == 0 || request.GetOrganizationId() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "resource_pool_id and organization_id are required")
	}
	if request.GetTargetCapacity() == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "target_capacity is required")
	}
	if request.GetTargetCapacity().GetTikv() == nil && request.GetTargetCapacity().GetTiflash() == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "at least one capacity target is required")
	}
	scaleRequest, err := buildPublicScaleRequest(request, true)
	if err != nil {
		return nil, err
	}
	preview, err := s.regionService.ScaleByocSharedComponents(ctx, scaleRequest)
	if err != nil {
		return nil, stableResizeServiceError(err)
	}
	if preview == nil || preview.GetPreview() == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "resource pool resize preview is unavailable")
	}
	if previewErr := blockedPublicResizeError(request, preview); previewErr != nil {
		return nil, previewErr
	}

	response, err := publicResizeResponse(request, preview)
	if err != nil {
		return nil, err
	}
	if request.GetValidateOnly() {
		return response, nil
	}
	submissionRequest, err := normalizedScaleSubmissionRequest(request, preview)
	if err != nil {
		return nil, err
	}
	if _, err := s.regionService.ScaleByocSharedComponents(ctx, submissionRequest); err != nil {
		return nil, stableResizeServiceError(err)
	}
	return response, nil
}

func normalizedScaleSubmissionRequest(
	request *globalngv1.ResizeResourcePoolCapacityRequest,
	preview *globalngv1.ScaleByocSharedComponentsResponse,
) (*globalngv1.ScaleByocSharedComponentsRequest, bizerr.BizError) {
	submission := &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: request.GetResourcePoolId(),
		OrganizationId: request.GetOrganizationId(),
	}
	if request.GetTargetCapacity().GetTikv() != nil {
		component, err := findUniqueScalePlan(
			preview.GetComponents(),
			globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIKV,
		)
		if err != nil {
			return nil, err
		}
		if component == nil || component.GetTarget() == nil {
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "normalized TiKV resize target is unavailable")
		}
		submission.Tikv = cloneScaleSubmissionTarget(component.GetTarget())
	}
	if request.GetTargetCapacity().GetTiflash() != nil {
		component, err := findUniqueScalePlan(
			preview.GetComponents(),
			globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_COMPUTE,
		)
		if err != nil {
			return nil, err
		}
		if component == nil || component.GetTarget() == nil {
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "normalized TiFlash resize target is unavailable")
		}
		submission.TiflashCompute = cloneScaleSubmissionTarget(component.GetTarget())
	}
	return submission, nil
}

func cloneScaleSubmissionTarget(
	target *globalngv1.ByocSharedComponentScaleTarget,
) *globalngv1.ByocSharedComponentScaleTarget {
	if target == nil {
		return nil
	}
	cloned, ok := proto.Clone(target).(*globalngv1.ByocSharedComponentScaleTarget)
	if !ok {
		return nil
	}
	return cloned
}

func stableResizeServiceError(err bizerr.BizError) bizerr.BizError {
	if err == nil {
		return nil
	}
	if err.ErrCode() != bizerr.ErrCodeParams {
		return err
	}
	if strings.Contains(strings.ToLower(err.Error()), "in progress") {
		return bizerr.New(
			bizerr.ErrCode(codes.FailedPrecondition),
			"the resource pool is not currently mutable",
		)
	}
	return bizerr.New(bizerr.ErrCodeParams, "the requested resource pool capacity change is invalid")
}

func buildPublicScaleRequest(
	request *globalngv1.ResizeResourcePoolCapacityRequest,
	validateOnly bool,
) (*globalngv1.ScaleByocSharedComponentsRequest, bizerr.BizError) {
	target := request.GetTargetCapacity()
	tikv, err := componentCapacityToScaleTarget(target.GetTikv(), map[string]struct{}{
		"DATA": {},
	})
	if err != nil {
		return nil, bizerr.WrapError(err, "invalid TiKV capacity target")
	}
	compute, err := componentCapacityToScaleTarget(target.GetTiflash(), map[string]struct{}{"DATA": {}})
	if err != nil {
		return nil, bizerr.WrapError(err, "invalid TiFlash capacity target")
	}
	return &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: request.GetResourcePoolId(),
		OrganizationId: request.GetOrganizationId(),
		Tikv:           tikv,
		TiflashCompute: compute,
		ValidateOnly:   validateOnly,
	}, nil
}

func componentCapacityToScaleTarget(
	component *globalngv1.ComponentCapacity,
	supportedStorages map[string]struct{},
) (*globalngv1.ByocSharedComponentScaleTarget, bizerr.BizError) {
	if component == nil {
		return nil, nil //nolint:nilnil // nil means the optional component was omitted.
	}
	target := &globalngv1.ByocSharedComponentScaleTarget{}
	if component.NodeCount != nil {
		value, err := checkedUint32(component.GetNodeCount(), "node_count")
		if err != nil {
			return nil, err
		}
		target.NodeCount = proto.Uint32(value)
	}
	if component.GetNodeSpec() != nil {
		cpu, err := checkedUint32(component.GetNodeSpec().GetCpu(), "cpu")
		if err != nil {
			return nil, err
		}
		memoryGi, err := checkedUint32(component.GetNodeSpec().GetMemoryGi(), "memory_gi")
		if err != nil {
			return nil, err
		}
		if cpu == 0 || memoryGi == 0 {
			return nil, bizerr.New(bizerr.ErrCodeParams, "node spec CPU and memory must be positive")
		}
		target.Cpu = proto.Uint32(cpu)
		target.MemoryGi = proto.Uint32(memoryGi)
	}
	seenStorages := make(map[string]struct{}, len(component.GetStorages()))
	for _, storage := range component.GetStorages() {
		if storage == nil {
			return nil, bizerr.New(bizerr.ErrCodeParams, "storage is required")
		}
		sizeGi, err := checkedUint32(storage.GetSizeGi(), "storage size_gi")
		if err != nil {
			return nil, err
		}
		if sizeGi == 0 {
			return nil, bizerr.New(bizerr.ErrCodeParams, "storage size_gi must be positive")
		}
		name := strings.ToUpper(strings.TrimSpace(storage.GetName()))
		if _, ok := supportedStorages[name]; !ok {
			return nil, bizerr.New(bizerr.ErrCodeParams, "unsupported storage name")
		}
		if _, ok := seenStorages[name]; ok {
			return nil, bizerr.New(bizerr.ErrCodeParams, "duplicate storage name")
		}
		seenStorages[name] = struct{}{}
		target.Storages = append(target.Storages, &globalngv1.ByocSharedComponentStorage{
			StorageName: name,
			SizeGi:      proto.Uint32(sizeGi),
		})
	}
	return target, nil
}

func checkedUint32(value int64, field string) (uint32, bizerr.BizError) {
	if value < 0 || value > math.MaxUint32 {
		return 0, bizerr.New(bizerr.ErrCodeParams, "%s is outside the supported range", field)
	}
	return uint32(value), nil
}

func blockedPublicResizeError(
	request *globalngv1.ResizeResourcePoolCapacityRequest,
	preview *globalngv1.ScaleByocSharedComponentsResponse,
) bizerr.BizError {
	if !preview.GetPreview().GetAllowed() {
		return stableResizeBlockedError(preview.GetPreview().GetBlockingReasons())
	}

	requested := map[globalngv1.ByocSharedComponentType]bool{
		globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIKV:            request.GetTargetCapacity().GetTikv() != nil,
		globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_COMPUTE: request.GetTargetCapacity().GetTiflash() != nil,
	}
	seen := make(map[globalngv1.ByocSharedComponentType]bool, len(requested))
	for _, component := range preview.GetComponents() {
		if !requested[component.GetComponentType()] {
			continue
		}
		seen[component.GetComponentType()] = true
		if component.GetAllowed() {
			continue
		}
		return stableResizeBlockedError(component.GetBlockingReasons())
	}
	for componentType, required := range requested {
		if required && !seen[componentType] {
			return bizerr.New(
				bizerr.ErrCode(codes.FailedPrecondition),
				"a requested resource pool component is not available for resizing",
			)
		}
	}
	return nil
}

func stableResizeBlockedError(reasons []string) bizerr.BizError {
	for _, reason := range reasons {
		normalized := strings.ToLower(strings.TrimSpace(reason))
		if strings.Contains(normalized, "vcpus") && strings.Contains(normalized, "limit") {
			return bizerr.New(
				bizerr.ErrCode(codes.ResourceExhausted),
				"the target capacity exceeds the resource pool vCPU limit",
			)
		}
	}
	for _, reason := range reasons {
		normalized := strings.ToLower(strings.TrimSpace(reason))
		if strings.Contains(normalized, "modifying") ||
			strings.Contains(normalized, "in progress") ||
			strings.Contains(normalized, "not provisioned") ||
			strings.Contains(normalized, "phase") ||
			strings.Contains(normalized, "cross-zero") {
			return bizerr.New(
				bizerr.ErrCode(codes.FailedPrecondition),
				"the resource pool is not currently mutable",
			)
		}
	}
	return bizerr.New(bizerr.ErrCodeParams, "the requested resource pool capacity change is invalid")
}

func publicResizeResponse(
	request *globalngv1.ResizeResourcePoolCapacityRequest,
	preview *globalngv1.ScaleByocSharedComponentsResponse,
) (*globalngv1.ResizeResourcePoolCapacityResponse, bizerr.BizError) {
	tikv, err := findUniqueScalePlan(
		preview.GetComponents(),
		globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIKV,
	)
	if err != nil {
		return nil, err
	}
	compute, err := findUniqueScalePlan(
		preview.GetComponents(),
		globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_COMPUTE,
	)
	if err != nil {
		return nil, err
	}
	current, err := capacityFromPlanComponents(tikv, compute, false, false)
	if err != nil {
		return nil, err
	}
	target, err := capacityFromPlanComponents(
		tikv,
		compute,
		true,
		request.GetTargetCapacity().GetTiflash() != nil &&
			request.GetTargetCapacity().GetTiflash().NodeCount != nil &&
			request.GetTargetCapacity().GetTiflash().GetNodeCount() == 0,
	)
	if err != nil {
		return nil, err
	}
	return &globalngv1.ResizeResourcePoolCapacityResponse{
		CurrentCapacity: current,
		TargetCapacity:  target,
	}, nil
}

func capacityFromConstraintComponents(
	tikv *globalngv1.ByocSharedComponentScaleConstraint,
	compute *globalngv1.ByocSharedComponentScaleConstraint,
) (*globalngv1.ResourcePoolCapacity, bizerr.BizError) {
	if tikv == nil || !tikv.GetExists() || tikv.GetCurrent() == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "TiKV capacity is missing from regional constraints")
	}
	if compute == nil || !compute.GetExists() || compute.GetCurrent() == nil {
		return nil, bizerr.New(
			bizerr.ErrCode(codes.FailedPrecondition),
			"TiFlash Compute capacity is not provisioned for this resource pool",
		)
	}
	return resourcepoolcapacity.ProjectScaleTargets(tikv.GetCurrent(), compute.GetCurrent(), false)
}

func capacityFromPlanComponents(
	tikv *globalngv1.ByocSharedComponentScalePlanComponent,
	compute *globalngv1.ByocSharedComponentScalePlanComponent,
	useTarget bool,
	includeZeroCompute bool,
) (*globalngv1.ResourcePoolCapacity, bizerr.BizError) {
	if tikv == nil || tikv.GetCurrent() == nil || tikv.GetTarget() == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "TiKV capacity is missing from resize preview")
	}
	tikvTarget := tikv.GetCurrent()
	var computeTarget *globalngv1.ByocSharedComponentScaleTarget
	if compute != nil {
		computeTarget = compute.GetCurrent()
	}
	if useTarget {
		tikvTarget = tikv.GetTarget()
		if compute != nil {
			computeTarget = compute.GetTarget()
		}
	}
	return resourcepoolcapacity.ProjectScaleTargets(tikvTarget, computeTarget, includeZeroCompute)
}

func publicComponentConstraints(
	component *globalngv1.ByocSharedComponentScaleConstraint,
	componentType globalngv1.NodeSpecComponentType,
	discovered []*globalngv1.ResourcePoolNodeSpec,
) (*globalngv1.ComponentCapacityConstraints, bizerr.BizError) {
	if component == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "regional capacity constraint component is missing")
	}
	out := &globalngv1.ComponentCapacityConstraints{
		BlockingReasons: publicConstraintReasons(component.GetBlockingReasons()),
	}
	if counts := component.GetNodeCountConstraints(); counts != nil {
		rule, err := publicNodeCountRule(counts.GetRule())
		if err != nil {
			return nil, err
		}
		out.NodeCount = &globalngv1.NodeCountConstraints{
			Current:        int64(counts.GetCurrent()),
			Min:            int64(counts.GetMin()),
			Max:            int64(counts.GetMax()),
			Rule:           rule,
			AllowCrossZero: counts.GetAllowCrossZero(),
		}
	}
	if specs := component.GetNodeSpecConstraints(); specs != nil {
		out.NodeSpec = &globalngv1.NodeSpecConstraints{}
		discoveredByTuple := make(map[[2]int64]*globalngv1.ResourcePoolNodeSpec, len(discovered))
		for _, spec := range discovered {
			if spec == nil {
				continue
			}
			discoveredByTuple[[2]int64{spec.GetCpu(), spec.GetMemoryGi()}] = spec
		}
		for _, candidate := range specs.GetCandidates() {
			discoveredSpec := discoveredByTuple[[2]int64{int64(candidate.GetCpu()), int64(candidate.GetMemoryGi())}]
			isCurrent := candidate.GetIsCurrent() || candidate.GetName() == specs.GetCurrent()
			if discoveredSpec == nil && !isCurrent {
				continue
			}
			nodeSpec := cloneResourcePoolNodeSpec(discoveredSpec)
			if nodeSpec == nil {
				nodeSpec = legacyCurrentNodeSpec(component, candidate, componentType)
			}
			nodeSpec.ComponentType = componentType
			overlayRuntimeStorageConstraints(nodeSpec, candidate.GetStorageConstraints())
			candidateOutput := &globalngv1.NodeSpecCandidate{
				NodeSpec:  nodeSpec,
				Available: candidate.GetAvailable(),
				Reasons:   publicConstraintReasons(candidate.GetReasons()),
			}
			out.NodeSpec.Candidates = append(out.NodeSpec.Candidates, candidateOutput)
			if isCurrent {
				out.NodeSpec.Current = nodeSpec
			}
		}
	}
	return out, nil
}

func publicConstraintReasons(
	reasons []*globalngv1.ByocSharedComponentScaleConstraintReason,
) []*globalngv1.CapacityConstraintReason {
	out := make([]*globalngv1.CapacityConstraintReason, 0, len(reasons))
	for _, reason := range reasons {
		code, message := safeCapacityConstraintReason(reason.GetCode(), reason.GetMessage())
		out = append(out, &globalngv1.CapacityConstraintReason{
			Code:    code,
			Message: message,
		})
	}
	return out
}

func safeCapacityConstraintReason(code, message string) (safeCode, safeMessage string) {
	switch strings.ToUpper(strings.TrimSpace(code)) {
	case "COMPONENT_PHASE_NOT_AVAILABLE":
		return "COMPONENT_PHASE_NOT_AVAILABLE", "The component is not currently available for resizing."
	case "BELOW_RU_REQUIREMENT":
		return "BELOW_RU_REQUIREMENT", "The target capacity does not satisfy the current resource requirements."
	case "INSUFFICIENT_LOCAL_DISK_SIZE":
		return "INSUFFICIENT_LOCAL_DISK_SIZE", "The target node spec does not provide enough local disk."
	case "SPEC_NOT_PRESENT_IN_SHARED_POOL":
		return "SPEC_NOT_PRESENT_IN_SHARED_POOL", "The target node spec is not available in this resource pool."
	}

	normalized := strings.ToLower(strings.TrimSpace(message))
	switch {
	case strings.Contains(normalized, "modifying"), strings.Contains(normalized, "in progress"):
		return "RESOURCE_POOL_MODIFYING", "The resource pool is currently being modified."
	case strings.Contains(normalized, "not provisioned"), strings.Contains(normalized, "phase"):
		return "COMPONENT_NOT_AVAILABLE", "The component is not available for resizing."
	case strings.Contains(normalized, "vcpus limit"):
		return "RESOURCE_POOL_VCPU_LIMIT_EXCEEDED", "The target capacity exceeds the resource pool vCPU limit."
	default:
		return "CAPACITY_CHANGE_NOT_ALLOWED", "The requested capacity change is not allowed."
	}
}

func cloneResourcePoolNodeSpec(in *globalngv1.ResourcePoolNodeSpec) *globalngv1.ResourcePoolNodeSpec {
	if in == nil {
		return nil
	}
	cloned, ok := proto.Clone(in).(*globalngv1.ResourcePoolNodeSpec)
	if !ok {
		return nil
	}
	return cloned
}

func legacyCurrentNodeSpec(
	component *globalngv1.ByocSharedComponentScaleConstraint,
	candidate *globalngv1.ByocSharedComponentScaleNodeSpecCandidate,
	componentType globalngv1.NodeSpecComponentType,
) *globalngv1.ResourcePoolNodeSpec {
	name := strings.TrimSpace(candidate.GetName())
	if name == "" {
		name = "current"
	}
	result := &globalngv1.ResourcePoolNodeSpec{
		NodeSpecKey:   name,
		DisplayName:   name,
		ComponentType: componentType,
		Cpu:           int64(candidate.GetCpu()),
		MemoryGi:      int64(candidate.GetMemoryGi()),
	}
	for _, storage := range component.GetCurrent().GetStorages() {
		if !strings.EqualFold(strings.TrimSpace(storage.GetStorageName()), "DATA") {
			continue
		}
		sizeGi := int64(storage.GetSizeGi())
		result.Storages = append(result.Storages, &globalngv1.NodeSpecStorage{
			Name:          "data",
			DefaultSizeGi: sizeGi,
			MinSizeGi:     sizeGi,
			MaxSizeGi:     sizeGi,
			Resizable:     proto.Bool(false),
			CurrentSizeGi: proto.Int64(sizeGi),
		})
	}
	return result
}

func overlayRuntimeStorageConstraints(
	nodeSpec *globalngv1.ResourcePoolNodeSpec,
	constraints []*globalngv1.ByocSharedComponentScaleStorageConstraint,
) {
	if nodeSpec == nil {
		return
	}
	byName := make(map[string]*globalngv1.NodeSpecStorage, len(nodeSpec.GetStorages()))
	for _, storage := range nodeSpec.GetStorages() {
		byName[strings.ToLower(strings.TrimSpace(storage.GetName()))] = storage
	}
	for _, constraint := range constraints {
		name := strings.ToLower(strings.TrimSpace(constraint.GetStorageName()))
		if name != "data" {
			continue
		}
		storage := byName[name]
		if storage == nil {
			currentSizeGi := int64(constraint.GetCurrentSizeGi())
			storage = &globalngv1.NodeSpecStorage{
				Name:          name,
				DefaultSizeGi: currentSizeGi,
				MinSizeGi:     currentSizeGi,
				MaxSizeGi:     currentSizeGi,
			}
			nodeSpec.Storages = append(nodeSpec.Storages, storage)
		}
		storage.Resizable = proto.Bool(constraint.GetResizable())
		storage.CurrentSizeGi = proto.Int64(int64(constraint.GetCurrentSizeGi()))
		if constraint.MaxSizeGi != nil {
			storage.MaxSizeGi = int64(constraint.GetMaxSizeGi())
		}
	}
}

func publicNodeCountRule(
	rule *globalngv1.ByocSharedComponentScaleNodeCountRule,
) (*globalngv1.NodeCountRule, bizerr.BizError) {
	if rule == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "regional node count rule is missing")
	}
	var ruleType globalngv1.NodeCountRuleType
	switch strings.ToUpper(rule.GetType()) {
	case "NONE":
		ruleType = globalngv1.NodeCountRuleType_NODE_COUNT_RULE_TYPE_NONE
	case "MULTIPLE_OF":
		ruleType = globalngv1.NodeCountRuleType_NODE_COUNT_RULE_TYPE_MULTIPLE_OF
	case "FIXED":
		ruleType = globalngv1.NodeCountRuleType_NODE_COUNT_RULE_TYPE_FIXED
	default:
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "unsupported regional node count rule")
	}
	return &globalngv1.NodeCountRule{Type: ruleType, Value: int64(rule.GetValue())}, nil
}

func findUniqueScaleConstraint(
	components []*globalngv1.ByocSharedComponentScaleConstraint,
	componentType globalngv1.ByocSharedComponentType,
) (*globalngv1.ByocSharedComponentScaleConstraint, bizerr.BizError) {
	var result *globalngv1.ByocSharedComponentScaleConstraint
	for _, component := range components {
		if component.GetComponentType() == componentType {
			if result != nil {
				return nil, bizerr.New(bizerr.ErrCodeUnknown, "duplicate regional capacity constraint component")
			}
			result = component
		}
	}
	return result, nil
}

func findUniqueScalePlan(
	components []*globalngv1.ByocSharedComponentScalePlanComponent,
	componentType globalngv1.ByocSharedComponentType,
) (*globalngv1.ByocSharedComponentScalePlanComponent, bizerr.BizError) {
	var result *globalngv1.ByocSharedComponentScalePlanComponent
	for _, component := range components {
		if component.GetComponentType() == componentType {
			if result != nil {
				return nil, bizerr.New(bizerr.ErrCodeUnknown, "duplicate resize preview component")
			}
			result = component
		}
	}
	return result, nil
}
