package resourcepool

import (
	"maps"
	"math"
	"slices"
	"strconv"
	"strings"

	"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"
	openapingv1beta2 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/nextgen/v1beta2"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/enum"
)

func createRequestToGlobal(
	req *openapingv1beta2.CreateResourcePoolRequest,
	organizationID uint64,
	createdBy string,
) (*globalngv1.CreateResourcePoolRequest, error) {
	if req == nil || req.GetResourcePool() == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "resource_pool is required")
	}

	pool := req.GetResourcePool()
	providerName, regionName, err := parseRegion(pool.GetRegionId())
	if err != nil {
		return nil, err
	}
	vcpuLimit, vcpuMode, err := vcpuLimitToGlobal(pool.GetVcpuLimit())
	if err != nil {
		return nil, err
	}
	capacity, err := capacityToGlobal(pool.GetCapacity())
	if err != nil {
		return nil, err
	}
	highAvailabilityType, err := highAvailabilityTypeToGlobal(pool.GetHighAvailabilityType())
	if err != nil {
		return nil, err
	}
	return &globalngv1.CreateResourcePoolRequest{
		ResourcePool: &globalngv1.ResourcePool{
			Name:                 pool.GetDisplayName(),
			ProviderId:           providerName,
			RegionId:             regionName,
			OwnerType:            globalngv1.ResourcePool_ORGANIZATION,
			OwnerId:              organizationID,
			Cidr:                 pool.GetVpcCidr(),
			ServicePlan:          globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:           vcpuLimit,
			HighAvailabilityType: highAvailabilityType,
			VcpuLimitMode:        vcpuMode,
			CloudResourceTags:    cloneStringMap(pool.GetCloudResourceTags()),
		},
		BootstrapCapacity: capacity,
		VcpuLimitMode:     vcpuMode,
		CreatedBy:         createdBy,
		ValidateOnly:      req.GetValidateOnly(),
	}, nil
}

func listRequestToGlobal(
	req *openapingv1beta2.ListResourcePoolsRequest,
	organizationID uint64,
) (*globalngv1.ListResourcePoolsRequest, error) {
	if req == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "request is required")
	}
	if req.GetPageSize() < 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "page_size must not be negative")
	}

	providers := make([]globalcommonv1.CloudProvider_Name, 0, len(req.GetRegionIds())+1)
	regions := make([]globalcommonv1.Region_Name, 0, len(req.GetRegionIds()))
	if req.GetCloudProvider() != 0 {
		provider := common.OpenApiProviderToGlobal(req.GetCloudProvider())
		if provider == globalcommonv1.CloudProvider_NAME_UNSPECIFIED {
			return nil, bizerr.New(bizerr.ErrCodeParams, "invalid cloud_provider")
		}
		providers = append(providers, provider)
	}
	for _, regionID := range req.GetRegionIds() {
		provider, region, err := parseRegion(regionID)
		if err != nil {
			return nil, err
		}
		if len(providers) > 0 && req.GetCloudProvider() != 0 && providers[0] != provider {
			return nil, bizerr.New(bizerr.ErrCodeParams, "region_id does not match cloud_provider")
		}
		providers = appendUniqueProvider(providers, provider)
		regions = append(regions, region)
	}

	states := make([]globalngv1.ResourcePool_LifecycleState, 0, len(req.GetStates()))
	for _, state := range req.GetStates() {
		converted, err := lifecycleStateToGlobal(state)
		if err != nil {
			return nil, err
		}
		states = append(states, converted)
		if state == openapingv1beta2.ResourcePool_ACTIVE {
			states = append(states, globalngv1.ResourcePool_PAUSE_PRECHECKING)
		}
	}

	return &globalngv1.ListResourcePoolsRequest{
		ResourcePoolName: req.GetDisplayName(),
		ProviderIds:      providers,
		RegionIds:        regions,
		OwnerType:        globalngv1.ResourcePool_ORGANIZATION,
		OwnerIds:         []uint64{organizationID},
		ServicePlan:      globalcommonv1.ServicePlan_BYOC,
		LifecycleStates:  states,
		PageSize:         uint32(req.GetPageSize()),
		PageToken:        req.GetPageToken(),
		View:             globalngv1.ListResourcePoolsRequest_BASIC,
	}, nil
}

func profileToOpenAPI(profile *globalngv1.ResourcePoolProfile) (*openapingv1beta2.ResourcePool, error) {
	if profile == nil || profile.GetResourcePool() == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "resource pool response is incomplete")
	}
	pool := profile.GetResourcePool()
	_, regionID, cloudProvider, err := common.BuildOpenApiRegionName(
		pool.GetProviderId().String(),
		pool.GetRegionId().String(),
	)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeUnknown, "invalid resource pool region")
	}
	state, err := lifecycleStateToOpenAPI(pool.GetLifecycleState())
	if err != nil {
		return nil, err
	}
	highAvailabilityType, err := highAvailabilityTypeToOpenAPI(pool.GetHighAvailabilityType())
	if err != nil {
		return nil, err
	}

	result := &openapingv1beta2.ResourcePool{
		DisplayName:          pool.GetName(),
		RegionId:             regionID,
		RegionDisplayName:    profile.GetRegion().GetDisplayName(),
		CloudProvider:        cloudProvider,
		VpcCidr:              pool.GetCidr(),
		HighAvailabilityType: highAvailabilityType,
		VcpuLimit:            vcpuLimitToOpenAPI(pool),
		Capacity:             capacityToOpenAPI(profile.GetCapacity()),
		CloudResourceTags:    cloneStringMap(pool.GetCloudResourceTags()),
	}
	if profile.O11YClusterCidr != nil {
		result.O11YClusterCidr = proto.String(profile.GetO11YClusterCidr())
	}
	if pool.GetDataplaneId() != "" {
		result.DataplaneId = proto.String(pool.GetDataplaneId())
	}
	if pool.GetZoneConfig() != nil {
		result.ZoneConfig = &openapingv1beta2.ResourcePool_ZoneConfig{
			NodeZones: append([]string(nil), pool.GetZoneConfig().GetNodeZones()...),
		}
	}
	if pool.GetId() != 0 {
		result.Name = common.ResourcePoolName(pool.GetId())
		result.ResourcePoolId = strconv.FormatUint(pool.GetId(), 10)
		result.TidbInstanceCount = proto.Int64(pool.GetInstanceCount())
		result.State = &state
		result.CreateTime = pool.GetCreateTime()
		result.UpdateTime = pool.GetUpdateTime()
		result.PausedAt = pool.GetPausedAt()
		result.AutomaticResumeAt = pool.GetAutomaticResumeAt()
		if message := pool.GetStateMessageCode(); message != "" {
			result.StateMessage = proto.String(message)
		}
		if pool.GetCreatedBy() != "" {
			result.CreatedBy = proto.String(common.ConvCreatedBy(pool.GetCreatedBy()))
		}
		result.VcpuCapacitySummary = vcpuCapacitySummaryToOpenAPI(pool, profile.ProvisionedDatabaseVcpus)
	}
	return result, nil
}

func profileToSummary(profile *globalngv1.ResourcePoolProfile) (*openapingv1beta2.ResourcePoolSummary, error) {
	if profile == nil || profile.GetResourcePool() == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "resource pool summary is incomplete")
	}
	pool := profile.GetResourcePool()
	_, regionID, cloudProvider, err := common.BuildOpenApiRegionName(
		pool.GetProviderId().String(),
		pool.GetRegionId().String(),
	)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeUnknown, "invalid resource pool region")
	}
	state, err := lifecycleStateToOpenAPI(pool.GetLifecycleState())
	if err != nil {
		return nil, err
	}
	highAvailabilityType, err := highAvailabilityTypeToOpenAPI(pool.GetHighAvailabilityType())
	if err != nil {
		return nil, err
	}
	return &openapingv1beta2.ResourcePoolSummary{
		Name:                 common.ResourcePoolName(pool.GetId()),
		ResourcePoolId:       strconv.FormatUint(pool.GetId(), 10),
		DisplayName:          pool.GetName(),
		RegionId:             regionID,
		RegionDisplayName:    profile.GetRegion().GetDisplayName(),
		CloudProvider:        cloudProvider,
		TidbInstanceCount:    pool.GetInstanceCount(),
		State:                state,
		HighAvailabilityType: highAvailabilityType,
		VcpuLimit:            vcpuLimitToOpenAPI(pool),
		CreateTime:           pool.GetCreateTime(),
		UpdateTime:           pool.GetUpdateTime(),
		PausedAt:             pool.GetPausedAt(),
		AutomaticResumeAt:    pool.GetAutomaticResumeAt(),
	}, nil
}

func capacityToGlobal(
	capacity *openapingv1beta2.ResourcePoolCapacity,
) (*globalngv1.ResourcePoolBootstrapCapacity, error) {
	if capacity == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "capacity is required")
	}
	if capacity.GetTikv() == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "capacity.tikv is required")
	}
	tikv, err := componentCapacityToGlobal(capacity.GetTikv(), "tikv", false)
	if err != nil {
		return nil, err
	}
	var tiflash *globalngv1.ComponentCapacity
	if capacity.GetTiflash() != nil {
		tiflash, err = componentCapacityToGlobal(capacity.GetTiflash(), "tiflash", false)
		if err != nil {
			return nil, err
		}
	}
	return &globalngv1.ResourcePoolBootstrapCapacity{Tikv: tikv, Tiflash: tiflash}, nil
}

func targetCapacityToGlobal(
	capacity *openapingv1beta2.ResourcePoolCapacity,
) (*globalngv1.ResourcePoolCapacity, error) {
	if capacity == nil || capacity.GetTikv() == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "target_capacity.tikv is required")
	}
	tikv, err := componentCapacityToGlobal(capacity.GetTikv(), "tikv", false)
	if err != nil {
		return nil, err
	}
	var tiflash *globalngv1.ComponentCapacity
	if capacity.GetTiflash() != nil {
		tiflash, err = componentCapacityToGlobal(capacity.GetTiflash(), "tiflash", true)
		if err != nil {
			return nil, err
		}
	}
	return &globalngv1.ResourcePoolCapacity{
		Tikv:    tikv,
		Tiflash: tiflash,
	}, nil
}

func componentCapacityToGlobal(
	component *openapingv1beta2.ComponentCapacity,
	field string,
	allowZero bool,
) (*globalngv1.ComponentCapacity, error) {
	if component == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "capacity.%s is required", field)
	}
	if component.NodeCount == nil || component.GetNodeCount() < 0 || (!allowZero && component.GetNodeCount() == 0) {
		return nil, bizerr.New(bizerr.ErrCodeParams, "capacity.%s.node_count is invalid", field)
	}
	if allowZero && component.GetNodeCount() == 0 {
		return &globalngv1.ComponentCapacity{NodeCount: proto.Int64(0)}, nil
	}
	if component.GetNodeSpec() == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "capacity.%s.node_spec is required", field)
	}
	if component.GetNodeSpec().GetCpu() <= 0 || component.GetNodeSpec().GetMemoryGi() <= 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "capacity.%s.node_spec must be positive", field)
	}
	storages := make([]*globalngv1.Storage, 0, len(component.GetStorages()))
	seenStorages := make(map[string]struct{}, len(component.GetStorages()))
	for _, storage := range component.GetStorages() {
		if storage == nil || storage.GetName() == "" || storage.GetSizeGi() <= 0 {
			return nil, bizerr.New(bizerr.ErrCodeParams, "capacity.%s contains invalid storage", field)
		}
		name := strings.ToLower(strings.TrimSpace(storage.GetName()))
		if name != "data" {
			return nil, bizerr.New(
				bizerr.ErrCodeParams,
				"capacity.%s contains unsupported storage %q",
				field,
				name,
			)
		}
		if _, ok := seenStorages[name]; ok {
			return nil, bizerr.New(
				bizerr.ErrCodeParams,
				"capacity.%s contains duplicate storage %q",
				field,
				name,
			)
		}
		seenStorages[name] = struct{}{}
		storages = append(storages, &globalngv1.Storage{
			Name:   name,
			SizeGi: storage.GetSizeGi(),
		})
	}
	return &globalngv1.ComponentCapacity{
		NodeSpec: &globalngv1.NodeSpecSelection{
			Cpu:      component.GetNodeSpec().GetCpu(),
			MemoryGi: component.GetNodeSpec().GetMemoryGi(),
		},
		NodeCount: proto.Int64(component.GetNodeCount()),
		Storages:  storages,
	}, nil
}

func capacityToOpenAPI(capacity *globalngv1.ResourcePoolCapacity) *openapingv1beta2.ResourcePoolCapacity {
	if capacity == nil {
		return nil
	}
	return &openapingv1beta2.ResourcePoolCapacity{
		Tikv:    componentCapacityToOpenAPI(capacity.GetTikv()),
		Tiflash: componentCapacityToOpenAPI(capacity.GetTiflash()),
	}
}

func componentCapacityToOpenAPI(component *globalngv1.ComponentCapacity) *openapingv1beta2.ComponentCapacity {
	if component == nil {
		return nil
	}
	storages := make([]*openapingv1beta2.Storage, 0, len(component.GetStorages()))
	for _, storage := range component.GetStorages() {
		if storage == nil || !strings.EqualFold(strings.TrimSpace(storage.GetName()), "data") {
			continue
		}
		storages = append(storages, &openapingv1beta2.Storage{
			Name:   "data",
			SizeGi: storage.GetSizeGi(),
		})
	}
	result := &openapingv1beta2.ComponentCapacity{Storages: storages}
	if component.GetNodeSpec() != nil {
		result.NodeSpec = &openapingv1beta2.NodeSpecSelection{
			Cpu:      component.GetNodeSpec().GetCpu(),
			MemoryGi: component.GetNodeSpec().GetMemoryGi(),
		}
	}
	if component.NodeCount != nil {
		result.NodeCount = proto.Int64(component.GetNodeCount())
	}
	return result
}

func constraintsToOpenAPI(
	in *globalngv1.ResourcePoolCapacityConstraints,
) *openapingv1beta2.ResourcePoolCapacityConstraints {
	if in == nil {
		return nil
	}
	return &openapingv1beta2.ResourcePoolCapacityConstraints{
		ResourcePoolId:  strconv.FormatUint(in.GetResourcePoolId(), 10),
		CurrentCapacity: capacityToOpenAPI(in.GetCurrentCapacity()),
		Tikv:            componentConstraintsToOpenAPI(in.GetTikv()),
		Tiflash:         componentConstraintsToOpenAPI(in.GetTiflash()),
	}
}

func componentConstraintsToOpenAPI(
	in *globalngv1.ComponentCapacityConstraints,
) *openapingv1beta2.ComponentCapacityConstraints {
	if in == nil {
		return nil
	}
	reasons := make([]*openapingv1beta2.CapacityConstraintReason, 0, len(in.GetBlockingReasons()))
	for _, reason := range in.GetBlockingReasons() {
		reasons = append(reasons, reasonToOpenAPI(reason))
	}
	out := &openapingv1beta2.ComponentCapacityConstraints{BlockingReasons: reasons}
	if in.GetNodeCount() != nil {
		out.NodeCount = &openapingv1beta2.NodeCountConstraints{
			Current:        in.GetNodeCount().GetCurrent(),
			Min:            in.GetNodeCount().GetMin(),
			Max:            in.GetNodeCount().GetMax(),
			Rule:           nodeCountRuleToOpenAPI(in.GetNodeCount().GetRule()),
			AllowCrossZero: in.GetNodeCount().GetAllowCrossZero(),
		}
	}
	if in.GetNodeSpec() != nil {
		candidates := make([]*openapingv1beta2.NodeSpecCandidate, 0, len(in.GetNodeSpec().GetCandidates()))
		for _, candidate := range in.GetNodeSpec().GetCandidates() {
			candidateReasons := make([]*openapingv1beta2.CapacityConstraintReason, 0, len(candidate.GetReasons()))
			for _, reason := range candidate.GetReasons() {
				candidateReasons = append(candidateReasons, reasonToOpenAPI(reason))
			}
			candidates = append(candidates, &openapingv1beta2.NodeSpecCandidate{
				NodeSpec:  nodeSpecToOpenAPI(candidate.GetNodeSpec()),
				Available: candidate.GetAvailable(),
				Reasons:   candidateReasons,
			})
		}
		out.NodeSpec = &openapingv1beta2.NodeSpecConstraints{
			Current:    nodeSpecToOpenAPI(in.GetNodeSpec().GetCurrent()),
			Candidates: candidates,
		}
	}
	return out
}

func nodeSpecToOpenAPI(in *globalngv1.ResourcePoolNodeSpec) *openapingv1beta2.NodeSpec {
	if in == nil {
		return nil
	}
	storages := make([]*openapingv1beta2.NodeSpecStorage, 0, len(in.GetStorages()))
	for _, storage := range in.GetStorages() {
		if storage == nil || !strings.EqualFold(strings.TrimSpace(storage.GetName()), "data") {
			continue
		}
		storages = append(storages, &openapingv1beta2.NodeSpecStorage{
			Name:          "data",
			DefaultSizeGi: storage.GetDefaultSizeGi(),
			MinSizeGi:     storage.GetMinSizeGi(),
			MaxSizeGi:     storage.GetMaxSizeGi(),
			Resizable:     storage.Resizable,
			CurrentSizeGi: storage.CurrentSizeGi,
		})
	}
	return &openapingv1beta2.NodeSpec{
		NodeSpecKey:   in.GetNodeSpecKey(),
		DisplayName:   in.GetDisplayName(),
		ComponentType: openapingv1beta2.NodeSpecComponentType(in.GetComponentType()),
		Cpu:           in.GetCpu(),
		MemoryGi:      in.GetMemoryGi(),
		IsDefault:     in.GetIsDefault(),
		Storages:      storages,
	}
}

func nodeCountRuleToOpenAPI(in *globalngv1.NodeCountRule) *openapingv1beta2.NodeCountRule {
	if in == nil {
		return nil
	}
	return &openapingv1beta2.NodeCountRule{
		Type:  openapingv1beta2.NodeCountRuleType(in.GetType()),
		Value: in.GetValue(),
	}
}

func reasonToOpenAPI(in *globalngv1.CapacityConstraintReason) *openapingv1beta2.CapacityConstraintReason {
	if in == nil {
		return nil
	}
	return &openapingv1beta2.CapacityConstraintReason{Code: in.GetCode(), Message: in.GetMessage()}
}

func vcpuLimitToGlobal(
	limit *openapingv1beta2.VcpuLimit,
) (uint64, globalngv1.ResourcePool_VcpuLimitMode, error) {
	if limit == nil {
		return 0, globalngv1.ResourcePool_VCPU_LIMIT_MODE_UNSPECIFIED,
			bizerr.New(bizerr.ErrCodeParams, "vcpu_limit is required")
	}
	switch policy := limit.GetPolicy().(type) {
	case *openapingv1beta2.VcpuLimit_LimitedVcpus:
		if policy.LimitedVcpus <= 0 {
			return 0, 0, bizerr.New(bizerr.ErrCodeParams, "limited_vcpus must be positive")
		}
		return uint64(policy.LimitedVcpus), globalngv1.ResourcePool_LIMITED, nil
	case *openapingv1beta2.VcpuLimit_Unlimited:
		if !policy.Unlimited {
			return 0, 0, bizerr.New(bizerr.ErrCodeParams, "unlimited must be true")
		}
		return 0, globalngv1.ResourcePool_UNLIMITED, nil
	default:
		return 0, 0, bizerr.New(bizerr.ErrCodeParams, "vcpu_limit policy is required")
	}
}

func vcpuLimitToOpenAPI(pool *globalngv1.ResourcePool) *openapingv1beta2.VcpuLimit {
	if pool == nil {
		return nil
	}
	if pool.GetVcpuLimitMode() == globalngv1.ResourcePool_UNLIMITED {
		return &openapingv1beta2.VcpuLimit{
			Policy: &openapingv1beta2.VcpuLimit_Unlimited{Unlimited: true},
		}
	}
	if pool.GetVcpusLimit() == 0 || pool.GetVcpusLimit() > math.MaxInt64 {
		return nil
	}
	return &openapingv1beta2.VcpuLimit{
		Policy: &openapingv1beta2.VcpuLimit_LimitedVcpus{
			LimitedVcpus: int64(pool.GetVcpusLimit()),
		},
	}
}

func vcpuCapacitySummaryToOpenAPI(
	pool *globalngv1.ResourcePool,
	provisioned *int64,
) *openapingv1beta2.VcpuCapacitySummary {
	if pool == nil || provisioned == nil {
		return nil
	}
	summary := &openapingv1beta2.VcpuCapacitySummary{
		ProvisionedDatabaseVcpus: *provisioned,
		VcpuLimit:                vcpuLimitToOpenAPI(pool),
	}
	if pool.GetVcpuLimitMode() == globalngv1.ResourcePool_UNLIMITED ||
		pool.GetVcpusLimit() == 0 ||
		pool.GetVcpusLimit() > math.MaxInt64 {
		return summary
	}
	limit := int64(pool.GetVcpusLimit())
	remaining := limit - *provisioned
	summary.RemainingVcpus = proto.Int64(remaining)
	summary.LimitUsageRatio = proto.Float64(float64(*provisioned) / float64(limit))
	return summary
}

func parseRegion(
	regionID string,
) (globalcommonv1.CloudProvider_Name, globalcommonv1.Region_Name, error) {
	provider, region, err := common.ParseOpenApiRegionNameOrId(regionID)
	if err != nil {
		return 0, 0, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid region_id")
	}
	providerValue, err := enum.FromString[globalcommonv1.CloudProvider_Name](provider)
	if err != nil {
		return 0, 0, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid region provider")
	}
	regionValue, err := enum.FromString[globalcommonv1.Region_Name](region)
	if err != nil {
		return 0, 0, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid region")
	}
	return globalcommonv1.CloudProvider_Name(providerValue), globalcommonv1.Region_Name(regionValue), nil
}

func highAvailabilityTypeToOpenAPI(
	in globalngv1.ResourcePool_HighAvailabilityType,
) (openapingv1beta2.ResourcePool_HighAvailabilityType, error) {
	switch in {
	case globalngv1.ResourcePool_REGIONAL:
		return openapingv1beta2.ResourcePool_REGIONAL, nil
	case globalngv1.ResourcePool_ZONAL:
		return openapingv1beta2.ResourcePool_ZONAL, nil
	case globalngv1.ResourcePool_HIGH_AVAILABILITY_TYPE_UNSPECIFIED:
		return openapingv1beta2.ResourcePool_HIGH_AVAILABILITY_TYPE_UNSPECIFIED, nil
	default:
		return 0, bizerr.New(bizerr.ErrCodeUnknown, "invalid resource pool high availability type")
	}
}

func highAvailabilityTypeToGlobal(
	in openapingv1beta2.ResourcePool_HighAvailabilityType,
) (globalngv1.ResourcePool_HighAvailabilityType, error) {
	switch in {
	case openapingv1beta2.ResourcePool_REGIONAL:
		return globalngv1.ResourcePool_REGIONAL, nil
	case openapingv1beta2.ResourcePool_ZONAL:
		return globalngv1.ResourcePool_ZONAL, nil
	case openapingv1beta2.ResourcePool_HIGH_AVAILABILITY_TYPE_UNSPECIFIED:
		return globalngv1.ResourcePool_HIGH_AVAILABILITY_TYPE_UNSPECIFIED, nil
	default:
		return 0, bizerr.New(bizerr.ErrCodeParams, "invalid resource pool high availability type")
	}
}

func lifecycleStateToGlobal(
	in openapingv1beta2.ResourcePool_State,
) (globalngv1.ResourcePool_LifecycleState, error) {
	switch in {
	case openapingv1beta2.ResourcePool_CREATING:
		return globalngv1.ResourcePool_CREATING, nil
	case openapingv1beta2.ResourcePool_ACTIVE:
		return globalngv1.ResourcePool_ACTIVE, nil
	case openapingv1beta2.ResourcePool_MODIFYING:
		return globalngv1.ResourcePool_MODIFYING, nil
	case openapingv1beta2.ResourcePool_FAILED:
		return globalngv1.ResourcePool_FAILED, nil
	case openapingv1beta2.ResourcePool_PAUSING:
		return globalngv1.ResourcePool_PAUSING, nil
	case openapingv1beta2.ResourcePool_PAUSED:
		return globalngv1.ResourcePool_PAUSED, nil
	case openapingv1beta2.ResourcePool_RESUMING:
		return globalngv1.ResourcePool_RESUMING, nil
	default:
		return 0, bizerr.New(bizerr.ErrCodeParams, "invalid resource pool state")
	}
}

func lifecycleStateToOpenAPI(
	in globalngv1.ResourcePool_LifecycleState,
) (openapingv1beta2.ResourcePool_State, error) {
	switch in {
	case globalngv1.ResourcePool_CREATING:
		return openapingv1beta2.ResourcePool_CREATING, nil
	case globalngv1.ResourcePool_ACTIVE,
		globalngv1.ResourcePool_PAUSE_PRECHECKING:
		return openapingv1beta2.ResourcePool_ACTIVE, nil
	case globalngv1.ResourcePool_MODIFYING:
		return openapingv1beta2.ResourcePool_MODIFYING, nil
	case globalngv1.ResourcePool_FAILED:
		return openapingv1beta2.ResourcePool_FAILED, nil
	case globalngv1.ResourcePool_PAUSING:
		return openapingv1beta2.ResourcePool_PAUSING, nil
	case globalngv1.ResourcePool_PAUSED:
		return openapingv1beta2.ResourcePool_PAUSED, nil
	case globalngv1.ResourcePool_RESUMING:
		return openapingv1beta2.ResourcePool_RESUMING, nil
	case globalngv1.ResourcePool_LIFECYCLE_STATE_UNSPECIFIED:
		return openapingv1beta2.ResourcePool_STATE_UNSPECIFIED, nil
	default:
		return 0, bizerr.New(bizerr.ErrCodeUnknown, "invalid resource pool lifecycle state")
	}
}

func appendUniqueProvider(
	providers []globalcommonv1.CloudProvider_Name,
	provider globalcommonv1.CloudProvider_Name,
) []globalcommonv1.CloudProvider_Name {
	if slices.Contains(providers, provider) {
		return providers
	}
	return append(providers, provider)
}

func cloneStringMap(in map[string]string) map[string]string {
	if in == nil {
		return nil
	}
	out := make(map[string]string, len(in))
	maps.Copy(out, in)
	return out
}

func cloneStringSlice(in []string) []string {
	return slices.Clone(in)
}

func validateOrganizationPool(
	profile *globalngv1.ResourcePoolProfile,
	organizationID uint64,
) error {
	if profile == nil || profile.GetResourcePool() == nil {
		return bizerr.New(bizerr.ErrCodeRecordNotFound, "resource pool not found")
	}
	pool := profile.GetResourcePool()
	if pool.GetOwnerType() != globalngv1.ResourcePool_ORGANIZATION ||
		pool.GetOwnerId() != organizationID ||
		pool.GetServicePlan() != globalcommonv1.ServicePlan_BYOC {
		return bizerr.New(bizerr.ErrCodeRecordNotFound, "resource pool not found")
	}
	return nil
}

func resolveUpdatePaths(
	req *openapingv1beta2.UpdateResourcePoolRequest,
) (updateName, updateVcpu bool, err error) {
	paths := req.GetUpdateMask().GetPaths()
	if len(paths) == 0 {
		updateName = req.GetResourcePool().GetDisplayName() != ""
		updateVcpu = req.GetResourcePool().GetVcpuLimit() != nil
		if updateName == updateVcpu {
			return false, false, bizerr.New(
				bizerr.ErrCodeParams,
				"exactly one of display_name or vcpu_limit must be populated",
			)
		}
		return updateName, updateVcpu, nil
	}
	if len(paths) != 1 {
		return false, false, bizerr.New(bizerr.ErrCodeParams, "update_mask must contain exactly one path")
	}
	for _, path := range paths {
		switch path {
		case "display_name":
			updateName = true
		case "vcpu_limit":
			updateVcpu = true
		default:
			return false, false, bizerr.New(bizerr.ErrCodeParams, "unsupported update path %q", path)
		}
	}
	return updateName, updateVcpu, nil
}

func NodeSpecsResponseToOpenAPI(
	in *globalngv1.ListNodeSpecsResponse,
) *openapingv1beta2.ListNodeSpecsResponse {
	if in == nil {
		return nil
	}
	specs := make([]*openapingv1beta2.NodeSpec, 0, len(in.GetNodeSpecs()))
	for _, spec := range in.GetNodeSpecs() {
		specs = append(specs, nodeSpecToOpenAPI(spec))
	}
	var countOptions *openapingv1beta2.NodeCountOptions
	if in.GetNodeCountOptions() != nil {
		countOptions = &openapingv1beta2.NodeCountOptions{
			DefaultNodeCount: in.GetNodeCountOptions().GetDefaultNodeCount(),
			MinNodeCount:     in.GetNodeCountOptions().GetMinNodeCount(),
			MaxNodeCount:     in.GetNodeCountOptions().GetMaxNodeCount(),
			Rule:             nodeCountRuleToOpenAPI(in.GetNodeCountOptions().GetRule()),
		}
	}
	return &openapingv1beta2.ListNodeSpecsResponse{
		NodeSpecs:        specs,
		NodeCountOptions: countOptions,
		TotalSize:        in.GetTotalSize(),
		NextPageToken:    in.GetNextPageToken(),
	}
}
