package cluster

import (
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"net"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.uber.org/zap"
	"gorm.io/gorm"

	globalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	regionalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/regional/serverless/v1"
	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	openapiserverlessv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/serverless/v1beta1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/adapter/eventbus"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	clustersvc "github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/service/serverless/cluster"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/apimiddleware/bizerror"
	"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/grpc/metadata"
)

func (c serverImpl) UpdateCluster(
	ctx context.Context,
	req *openapiserverlessv1beta1.PartialUpdateClusterRequest,
) (*openapiserverlessv1beta1.Cluster, error) {
	var (
		err         error
		reqMeta     *metadata.RequestMeta
		res         *openapiserverlessv1beta1.Cluster
		updateMask  string
		updateValue any
	)
	tenantID := unknownTenantID
	startTime := time.Now()
	defer func() {
		recordWriteClusterAPIMetrics(apiUpdateCluster, tenantID, time.Since(startTime).Seconds(), err)
	}()

	detailsFunc := func() auditlog.Details {
		details := eventbus.NewAuditLogDetails(eventbus.TierTypeServerless)
		details[auditlog.KeyUpdateMask] = updateMask
		details[auditlog.KeyUpdateValue] = updateValue
		eventbus.AppendServerlessClusterDetails(details, res)
		return details
	}
	defer func() {
		c.auditLogSender.SubmitAuditLog(ctx, auditlog.EventUpdateCluster, reqMeta, detailsFunc, err)
	}()

	if req.Cluster == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster is nil")
	}

	// Extract request meta.
	reqMeta, err = metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}
	tenantID = strconv.FormatUint(reqMeta.OrgID, 10)

	cluster := &entity.Cluster{}
	cluster.ClusterId, err = strconv.ParseUint(req.Cluster.ClusterId, 10, 64)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cluster id")
	}

	if req.UpdateMask == nil || len(req.UpdateMask.Paths) != 1 {
		err = errors.New("update mask is nil or empty")
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "update mask is nil or empty")
	}
	updateMask = req.UpdateMask.Paths[0]

	// Cluster get full view from domain service
	cluster, err = c.clusterSvc.GetCluster(ctx, cluster.ClusterId, entity.ClusterViewClusterViewFull)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerror.NewNotFound(err, "cluster not found")
		}
		return nil, bizerror.NewInternal(err, "failed to get cluster")
	}

	serverlessCluster, err := cluster.GetServerlessCluster()
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to get serverless cluster")
	}

	if serverlessCluster.GetUserStatus() == globalserverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_CREATING {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster is still creating")
	}

	// keep old entity.Cluster
	var newCluster *entity.Cluster
	newCluster, updateValue, err = buildNewCluster(serverlessCluster, req, updateMask, cluster)
	if err != nil {
		return nil, err
	}

	// Update cluster in domain service
	if newCluster != nil {
		newCluster.UserId = reqMeta.UserID
		cluster, err = c.clusterSvc.UpdateCluster(ctx, newCluster)
		if err != nil {
			return nil, err
		}
	}

	// check custom status
	var (
		customStatusMap map[uint64][]*entity.ClusterCustomStatus
		customStatuses  []*entity.ClusterCustomStatus
	)
	customStatusMap, err = c.customStatusSvc.ListClusterCustomStatus(ctx, []uint64{cluster.ClusterId}, nil, false)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		tilog.Warn(ctx, "failed to get custom status", zap.String("cluster_id", req.Cluster.ClusterId), zap.Error(err))
	}
	customStatuses = customStatusMap[cluster.ClusterId]

	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, bizerror.NewInternal(err, "failed to get cluster custom status")
	}
	res, err = buildClusterFromEntity(cluster, customStatuses, openapiserverlessv1beta1.ClusterView_FULL)
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to get cluster")
	}

	return res, nil
}

func buildNewCluster(
	serverlessCluster *globalserverlessv1.ServerlessServiceCluster,
	req *openapiserverlessv1beta1.PartialUpdateClusterRequest,
	updateMask string,
	cluster *entity.Cluster,
) (*entity.Cluster, any, error) {
	var (
		newCluster  *entity.Cluster
		err         error
		updateValue any
	)
	if isPGStarterNetworkUpdate(updateMask) && isPGStarterCluster(cluster, serverlessCluster) {
		return nil, nil, bizerr.New(
			bizerr.ErrCodeParams,
			"network endpoint updates are not supported for pg starter clusters",
		)
	}
	switch updateMask {
	case "labels":
		updateValue = req.Cluster.Labels
		if err := checkClusterLabels(req.Cluster.Labels); err != nil {
			return nil, nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid labels")
		}
		newCluster = buildNewEntityCluster(
			cluster,
			func(c *entity.Cluster, serverless *globalserverlessv1.ServerlessServiceCluster) {
				c.Labels = req.Cluster.Labels
			},
		)
	case "display_name":
		// Update display name in db
		updateValue = req.Cluster.DisplayName
		if req.Cluster.DisplayName != "" {
			if !common.CheckDisplayNameFmt(req.Cluster.DisplayName) {
				err = fmt.Errorf("cluster name not allowed: %s", req.Cluster.DisplayName)
				return nil, nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid display name")
			}
			newCluster = buildNewEntityCluster(
				cluster,
				func(c *entity.Cluster, serverless *globalserverlessv1.ServerlessServiceCluster) {
					c.DisplayName = req.Cluster.DisplayName
				},
			)
		}
	// only need update spending_limit.monthly
	case "spending_limit", "spending_limit.monthly":
		if serverlessCluster.GetCluster().GetAutoScaling().GetMinRcu() > 0 {
			// essential plan does not support edit spending limit
			msg := "spending limit is not supported in essential plan"
			return nil, nil, bizerr.WrapErrorWithCode(errors.New(msg), bizerr.ErrCodeParams, "%s", msg)
		}

		updateValue = req.Cluster.SpendingLimit
		if req.Cluster.SpendingLimit == nil {
			err = fmt.Errorf("spend limit is nil")
			return nil, nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "spend limit is nil")
		}
		newCluster = buildNewEntityCluster(
			cluster,
			func(c *entity.Cluster, serverless *globalserverlessv1.ServerlessServiceCluster) {
				c.ServicePlan = openapicommonv1beta1.ServicePlan_Starter
				serverless.SpendLimit = &globalserverlessv1.SpendLimit{
					Monthly: uint32(req.Cluster.SpendingLimit.Monthly),
				}
				serverless.Cluster = &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{}
			},
		)
	case "auto_scaling", "auto_scaling.min_rcu", "auto_scaling.max_rcu":
		// starter plan does not support upgrade to essential temporarily, start.
		if serverlessCluster.GetCluster().GetAutoScaling().GetMinRcu() == 0 {
			msg := "auto scaling is not supported on the starter plan. please contact support for assistance with upgrading your service plan"
			return nil, nil, bizerr.WrapErrorWithCode(errors.New(msg), bizerr.ErrCodeParams, "%s", msg)
		}
		// starter plan does not support upgrade to essential temporarily, end.
		updateValue = req.GetCluster().GetAutoScaling()
		if req.GetCluster().GetAutoScaling() == nil {
			err = fmt.Errorf("auto scaling is nil")
			return nil, nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "auto scaling is nil")
		}
		if req.GetCluster().GetAutoScaling().GetMinRcu() > req.GetCluster().GetAutoScaling().GetMaxRcu() ||
			req.GetCluster().GetAutoScaling().GetMinRcu() < common.ServerlessAutoScalingMinRcu ||
			req.GetCluster().GetAutoScaling().GetMaxRcu() > common.ServerlessAutoScalingMaxRcu {
			err = fmt.Errorf(
				"valid rcu range %v ~ %v",
				common.ServerlessAutoScalingMinRcu,
				common.ServerlessAutoScalingMaxRcu,
			)
			return nil, nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid rcu range")
		}
		newCluster = buildNewEntityCluster(
			cluster,
			func(c *entity.Cluster, serverless *globalserverlessv1.ServerlessServiceCluster) {
				c.ServicePlan = openapicommonv1beta1.ServicePlan_Essential
				serverless.Cluster = &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{
					AutoScaling: &regionalserverlessv1.ServerlessRegionalServiceClusterInfo_AutoScaling{
						MinRcu: req.GetCluster().GetAutoScaling().GetMinRcu(),
						MaxRcu: req.GetCluster().GetAutoScaling().GetMaxRcu(),
					},
				}
			},
		)
	case "automated_backup_policy":
		updateValue = req.Cluster.AutomatedBackupPolicy
		if req.Cluster.AutomatedBackupPolicy == nil {
			err = fmt.Errorf("automated backup policy is nil")
			return nil, nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "automated backup policy is nil")
		}
		policy := &globalserverlessv1.AutomatedBackupPolicy{
			BackupTime:    req.Cluster.AutomatedBackupPolicy.StartTime,
			RetentionDays: uint64(req.Cluster.AutomatedBackupPolicy.RetentionDays),
		}
		serverlessCluster.BackupPolicy = policy
		if err := clustersvc.CheckBackupPolicy(serverlessCluster); err != nil {
			return nil, nil, err
		}
		newCluster = buildNewEntityCluster(
			cluster,
			func(c *entity.Cluster, serverless *globalserverlessv1.ServerlessServiceCluster) {
				serverless.BackupPolicy = policy
			})
	case "endpoints.public_endpoint.disabled", "endpoints.public.disabled":
		updateValue = req.Cluster.Endpoints
		if req.Cluster.Endpoints == nil || req.Cluster.Endpoints.Public == nil {
			return nil, nil, bizerr.New(bizerr.ErrCodeParams, "public endpoint is nil")
		}
		var authorizedNetworks []*regionalserverlessv1.PublicEndpoint_AuthorizedNetwork
		if publicEndpoint := serverlessCluster.GetCluster().GetEndpoints().GetPublic(); publicEndpoint != nil {
			authorizedNetworks = publicEndpoint.GetAuthorizedNetwork()
		}
		newCluster = buildNewEntityCluster(
			cluster,
			func(c *entity.Cluster, serverless *globalserverlessv1.ServerlessServiceCluster) {
				serverless.Cluster = &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{
					Endpoints: &regionalserverlessv1.Endpoints{
						Public: &regionalserverlessv1.PublicEndpoint{
							Disabled: req.Cluster.Endpoints.Public.Disabled,
							/* keep authorized_networks field unchanged */
							AuthorizedNetwork: authorizedNetworks,
						},
					},
				}
			},
		)
	case "endpoints.public.authorized_networks":
		updateValue = req.Cluster.Endpoints
		if req.GetCluster().GetEndpoints().GetPublic() == nil {
			return nil, nil, bizerr.New(bizerr.ErrCodeParams, "public endpoint is nil")
		}
		if err := checkAuthorizedNetworks(req.Cluster.Endpoints.Public.AuthorizedNetworks); err != nil {
			return nil, nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid authorized networks")
		}

		networks := make(
			[]*regionalserverlessv1.PublicEndpoint_AuthorizedNetwork,
			len(req.Cluster.Endpoints.Public.AuthorizedNetworks),
		)
		for k, v := range req.Cluster.Endpoints.Public.AuthorizedNetworks {
			networks[k] = &regionalserverlessv1.PublicEndpoint_AuthorizedNetwork{
				StartIpAddress: v.StartIpAddress,
				EndIpAddress:   v.EndIpAddress,
				DisplayName:    v.DisplayName,
			}
		}

		newCluster = buildNewEntityCluster(
			cluster,
			func(c *entity.Cluster, serverless *globalserverlessv1.ServerlessServiceCluster) {
				disabled := false
				if publicEndpoint := serverlessCluster.GetCluster().GetEndpoints().GetPublic(); publicEndpoint != nil {
					disabled = publicEndpoint.GetDisabled()
				}
				serverless.Cluster = &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{
					Endpoints: &regionalserverlessv1.Endpoints{
						Public: &regionalserverlessv1.PublicEndpoint{
							/* keep disabled field unchanged */
							Disabled:          disabled,
							AuthorizedNetwork: networks,
						},
					},
				}
			},
		)
	case "endpoints.private.authorized_networks":
		updateValue = req.Cluster.Endpoints
		if req.GetCluster().GetEndpoints().GetPrivate() == nil {
			return nil, nil, bizerr.New(bizerr.ErrCodeParams, "private endpoint is nil")
		}
		if err := checkPrivateAuthorizedNetworks(req.Cluster.Endpoints.Private.AuthorizedNetworks); err != nil {
			return nil, nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid private authorized networks")
		}

		newCluster = buildNewEntityCluster(
			cluster,
			func(c *entity.Cluster, serverless *globalserverlessv1.ServerlessServiceCluster) {
				disabled := false
				if privateEndpoint := serverlessCluster.GetCluster().GetEndpoints().GetPrivate(); privateEndpoint != nil {
					disabled = privateEndpoint.GetDisabled()
				}
				serverless.Cluster = &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{
					Endpoints: &regionalserverlessv1.Endpoints{
						Private: &regionalserverlessv1.PrivateEndpoint{
							/* keep disabled field unchanged */
							Disabled: disabled,
							AuthorizedNetwork: cvtOpenAPIPrivateAuthorizedNetworksToRegional(
								req.Cluster.Endpoints.Private.AuthorizedNetworks,
							),
						},
					},
				}
			},
		)
	case "audit_log_config":
		updateValue = req.Cluster.AuditLogConfig
		if req.GetCluster().GetAuditLogConfig() == nil {
			return nil, nil, bizerr.New(bizerr.ErrCodeParams, "audit log config is nil")
		}
		newCluster = buildNewEntityCluster(
			cluster,
			func(c *entity.Cluster, serverless *globalserverlessv1.ServerlessServiceCluster) {
				serverless.Cluster = &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{
					AuditLogConfig: &regionalserverlessv1.ServerlessRegionalServiceClusterInfo_AuditLogConfig{
						Enabled:    req.Cluster.AuditLogConfig.Enabled,
						Unredacted: req.Cluster.AuditLogConfig.Unredacted,
					},
				}
			},
		)
	case "audit_log_config.enabled":
		updateValue = req.Cluster.AuditLogConfig
		if req.GetCluster().GetAuditLogConfig() == nil || req.GetCluster().GetAuditLogConfig().Enabled == nil {
			return nil, nil, bizerr.New(bizerr.ErrCodeParams, "audit log config enabled is nil")
		}
		newCluster = buildNewEntityCluster(
			cluster,
			func(c *entity.Cluster, serverless *globalserverlessv1.ServerlessServiceCluster) {
				serverless.Cluster = &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{
					AuditLogConfig: &regionalserverlessv1.ServerlessRegionalServiceClusterInfo_AuditLogConfig{
						Enabled: req.Cluster.AuditLogConfig.Enabled,
					},
				}
			},
		)
	case "audit_log_config.unredacted":
		updateValue = req.Cluster.AuditLogConfig
		if req.GetCluster().GetAuditLogConfig() == nil || req.GetCluster().GetAuditLogConfig().Unredacted == nil {
			return nil, nil, bizerr.New(bizerr.ErrCodeParams, "audit log config unredacted is nil")
		}
		newCluster = buildNewEntityCluster(
			cluster,
			func(c *entity.Cluster, serverless *globalserverlessv1.ServerlessServiceCluster) {
				serverless.Cluster = &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{
					AuditLogConfig: &regionalserverlessv1.ServerlessRegionalServiceClusterInfo_AuditLogConfig{
						Unredacted: req.Cluster.AuditLogConfig.Unredacted,
					},
				}
			},
		)
	default:
		err = fmt.Errorf("update mask %s is not supported", updateMask)
		return nil, nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "update mask is not supported")
	}
	return newCluster, updateValue, nil
}

func isPGStarterNetworkUpdate(updateMask string) bool {
	switch updateMask {
	case "endpoints.public_endpoint.disabled", "endpoints.public.disabled", "endpoints.public.authorized_networks":
		return true
	default:
		return false
	}
}

func isPGStarterCluster(cluster *entity.Cluster, serverlessCluster *globalserverlessv1.ServerlessServiceCluster) bool {
	if cluster != nil &&
		cluster.ServicePlan == openapicommonv1beta1.ServicePlan_Starter &&
		cluster.EngineType == openapicommonv1beta1.EngineType_POSTGRESQL_COMPATIBLE {
		return true
	}

	if serverlessCluster != nil &&
		strings.EqualFold(serverlessCluster.GetEngineType(), openapicommonv1beta1.EngineType_POSTGRESQL_COMPATIBLE.String()) {
		if serverlessCluster.GetCluster().GetAutoScaling().GetMaxRcu() == 0 {
			return true
		}
	}

	return false
}

// Only support update one field at a time.
func buildNewEntityCluster(
	cls *entity.Cluster,
	b func(c *entity.Cluster, serverless *globalserverlessv1.ServerlessServiceCluster),
) *entity.Cluster {
	cluster := &entity.Cluster{
		ClusterId:      cls.ClusterId,
		ProjectId:      cls.ProjectId,
		OrganizationId: cls.OrganizationId,
		DeployType:     entity.DeployTypeServerless,
	}
	serverless := &globalserverlessv1.ServerlessServiceCluster{
		ClusterId: strconv.FormatUint(cls.ClusterId, 10),
		ProjectId: cls.ProjectId,
		TenantId:  cls.OrganizationId,
	}

	b(cluster, serverless)

	cluster.GlobalCluster = serverless
	return cluster
}

func checkClusterLabels(labels map[string]string) error {
	if labels == nil {
		return fmt.Errorf("labels is nil")
	}

	immutableLabels := []string{common.ProjectIDLabelKey, common.OrgIDLabelKey}
	for _, k := range immutableLabels {
		if _, ok := labels[k]; ok {
			return fmt.Errorf("%s label is not allowed to be modified", k)
		}
	}
	return nil
}

func checkAuthorizedNetworks(networks []*openapiserverlessv1beta1.Cluster_Endpoints_Public_AuthorizedNetwork) error {
	if len(networks) > 100 {
		return fmt.Errorf("%d authorized network items, maximum length is 100", len(networks))
	}
	uniqueKey := make(map[string]bool)
	for _, v := range networks {
		if len(v.DisplayName) > 255 {
			return fmt.Errorf("maximum display_name is 255 bytes, %s is too long", v.DisplayName)
		}
		if !regexp.MustCompile(`^[a-zA-Z0-9_]+$`).MatchString(v.DisplayName) {
			return fmt.Errorf("invalid display_name %s", v.DisplayName)
		}
		if _, ok := uniqueKey[v.DisplayName]; ok {
			return fmt.Errorf("duplicate display_name %s", v.DisplayName)
		}
		uniqueKey[v.DisplayName] = true

		sip, err := ipToUint32(v.StartIpAddress)
		if err != nil {
			return err
		}
		eip, err := ipToUint32(v.EndIpAddress)
		if err != nil {
			return err
		}
		if sip > eip {
			return fmt.Errorf(
				"start_ip_address %s should not be greater than end_ip_address %s",
				v.StartIpAddress,
				v.EndIpAddress,
			)
		}
		if _, ok := uniqueKey[v.StartIpAddress+"-"+v.EndIpAddress]; ok {
			return fmt.Errorf(
				"duplicate ip range start_ip_address=%s, end_ip_address=%s",
				v.StartIpAddress,
				v.EndIpAddress,
			)
		}
		uniqueKey[v.StartIpAddress+"-"+v.EndIpAddress] = true
	}
	return nil
}

func checkPrivateAuthorizedNetworks(networks []*openapiserverlessv1beta1.Cluster_Endpoints_Private_AuthorizedNetwork) error {
	if len(networks) > 100 {
		return fmt.Errorf("%d private authorized network items, maximum length is 100", len(networks))
	}

	seen := make(map[string]struct{}, len(networks))
	for _, network := range networks {
		vpcEndpointID := strings.TrimSpace(network.GetVpcEndpointId())
		if vpcEndpointID == "" {
			return errors.New("vpc_endpoint_id is required")
		}
		if strings.TrimSpace(network.GetDisplayName()) == "" {
			return errors.New("display_name is required")
		}
		if !strings.HasPrefix(vpcEndpointID, "vpce-") && !strings.HasPrefix(vpcEndpointID, "ep-") {
			return fmt.Errorf("invalid vpc_endpoint_id %s", network.GetVpcEndpointId())
		}
		if _, ok := seen[vpcEndpointID]; ok {
			return fmt.Errorf("duplicate vpc_endpoint_id %s", network.GetVpcEndpointId())
		}
		seen[vpcEndpointID] = struct{}{}
	}

	return nil
}

func cvtOpenAPIPrivateAuthorizedNetworksToRegional(
	networks []*openapiserverlessv1beta1.Cluster_Endpoints_Private_AuthorizedNetwork,
) []*regionalserverlessv1.PrivateEndpoint_AuthorizedNetwork {
	if len(networks) == 0 {
		return nil
	}

	res := make([]*regionalserverlessv1.PrivateEndpoint_AuthorizedNetwork, 0, len(networks))
	for _, network := range networks {
		res = append(res, &regionalserverlessv1.PrivateEndpoint_AuthorizedNetwork{
			VpcEndpointId: strings.TrimSpace(network.GetVpcEndpointId()),
			DisplayName:   network.GetDisplayName(),
		})
	}
	return res
}

func cvtRegionalPrivateAuthorizedNetworksToOpenAPI(
	networks []*regionalserverlessv1.PrivateEndpoint_AuthorizedNetwork,
) []*openapiserverlessv1beta1.Cluster_Endpoints_Private_AuthorizedNetwork {
	if len(networks) == 0 {
		return nil
	}

	res := make([]*openapiserverlessv1beta1.Cluster_Endpoints_Private_AuthorizedNetwork, 0, len(networks))
	for _, network := range networks {
		res = append(res, &openapiserverlessv1beta1.Cluster_Endpoints_Private_AuthorizedNetwork{
			VpcEndpointId: network.GetVpcEndpointId(),
			DisplayName:   network.GetDisplayName(),
		})
	}
	return res
}

func ipToUint32(ipStr string) (uint32, error) {
	ip := net.ParseIP(ipStr)
	if ip == nil || ip.To4() == nil {
		return 0, fmt.Errorf("invalid IPv4 address %s", ipStr)
	}
	return binary.BigEndian.Uint32(ip.To4()), nil
}
