// Package service add by blacknc <blacknc@163.com> at 2023/03/17 18:31:46
package service

import (
	"context"
	"dataservices/commons/config"
	"dataservices/commons/constants"
	comerrs "dataservices/commons/errs"
	"dataservices/commons/grpc"
	"dataservices/commons/http"
	"dataservices/commons/logger"
	"dataservices/ddb"
	dto "dataservices/internal/dto/dataapi"
	"dataservices/internal/entity"
	"dataservices/internal/repository"
	"dataservices/internal/repository_ddb"
	commonSrv "dataservices/internal/service/common"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	"github.com/tidbcloud/dataservices/idl/pbgen/proto/dataapi"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"k8s.io/apimachinery/pkg/api/errors"
)

var validClusterStatus = map[core.UserClusterStatus]bool{
	core.UserClusterStatus_USER_CLUSTER_STATUS_NORMAL:    true,
	core.UserClusterStatus_USER_CLUSTER_STATUS_IMPORTING: true,
}

func CreateAppCluster(ctx context.Context, req dto.CreateAppClusterReq) error {

	if err := CheckDataAppCanDeploy(ctx, req.DataappID); err != nil {
		return err
	}

	// check cluster status
	if err := checkClusterStatus(ctx, req.OrgID, req.ProjectID, req.ClusterIDs, req.CreatedBy); err != nil {
		return err
	}

	return repository.Transaction(ctx, func(ctx context.Context) error {

		if err := createAppClusterWithTx(ctx, req, true); err != nil {
			return err
		}

		if _, err := TriggerClusterCreateFromUI(ctx, req.DataappID, req.ClusterIDs, req.CreatedBy); err != nil {
			return err
		}

		return nil
	})
}

func createAppClusterWithTx(ctx context.Context, req dto.CreateAppClusterReq, needPush bool) error {

	// check cluster is already exists
	clusterList, err := repository.ListAppCluster(ctx, dto.ListAppClusterReq{
		DataappID:  req.DataappID,
		ClusterIDs: req.ClusterIDs,
	})
	if err != nil {
		return err
	}
	clusterMap := make(map[uint64]*entity.AppCluster)
	for _, cluster := range clusterList {
		clusterMap[cluster.ClusterID] = cluster
	}
	var existsCluster []string
	appClusters := make([]entity.AppCluster, 0, len(req.ClusterIDs))
	for _, clusterID := range req.ClusterIDs {
		if _, ok := clusterMap[clusterID]; ok {
			existsCluster = append(existsCluster, strconv.FormatUint(clusterID, 10))
			continue
		}
		appClusters = append(appClusters, entity.AppCluster{
			DataappID: req.DataappID,
			ClusterID: clusterID,
			CreatedBy: req.CreatedBy,
			UpdatedBy: req.CreatedBy,
		})
	}
	if !req.SkipExists && len(existsCluster) > 0 {
		return errs.NewBadRequest("Cluster already in link list: "+strings.Join(existsCluster, ","), nil)
	}
	if len(appClusters) == 0 {
		return nil
	}

	// add app cluster
	err = repository.MultiCreateAppCluster(ctx, appClusters)
	if err != nil {
		return err
	}

	if !needPush {
		return nil
	}

	// notify eventbus
	return publishAppClusterEvent(ctx, req.DataappID, req.ClusterIDs, req.CreatedBy, dto.DataAppBindClusterActionAddCluster)
}

func DeleteAppCluster(ctx context.Context, req dto.DeleteAppClusterReq) error {

	if err := CheckDataAppCanDeploy(ctx, req.DataappID); err != nil {
		return err
	}

	// check cluster is already exists
	clusterCount, err := repository.CountAppCluster(ctx, dto.ListAppClusterReq{
		DataappID: req.DataappID,
		ClusterID: req.ClusterID,
	})
	if err != nil {
		return err
	} else if clusterCount == 0 {
		return errs.NewBadRequest(fmt.Sprintf("Cluster %d is not included in the list of data sources.", req.ClusterID), nil)
	}

	// check dataapp has at least one cluster
	clusterCount, err = repository.CountAppCluster(ctx, dto.ListAppClusterReq{
		DataappID: req.DataappID,
	})
	if err != nil {
		return err
	} else if clusterCount <= 1 {
		return errs.NewBadRequest("Sorry, a dataapp MUST include at least one datasource.", nil)
	}

	return repository.Transaction(ctx, func(ctx context.Context) error {
		// delete app cluster
		if err = repository.DeleteAppCluster(ctx, req.DataappID, req.ClusterID, req.CreatedBy); err != nil {
			return err
		}

		if _, err := TriggerClusterDeleteFromUI(ctx, req.DataappID, req.ClusterID, req.CreatedBy); err != nil {
			return err
		}

		// notify eventbus
		if err = publishAppClusterEvent(ctx, req.DataappID, []uint64{req.ClusterID}, req.CreatedBy, dto.DataAppBindClusterActionDelCluster); err != nil {
			return err
		}
		return nil
	})
}

func GetAppCluster(ctx context.Context, appID, clusterID uint64) (*dto.AppClusterInfo, error) {
	appInfos, err := ListAppCluster(ctx, dto.ListAppClusterReq{
		DataappID: appID,
		ClusterID: clusterID,
	})
	if err != nil {
		return nil, err
	}
	if len(appInfos) == 0 {
		return nil, errs.NewBadRequest("No APP Cluster Info found", nil)
	}
	return &appInfos[0], nil
}

func ListAppCluster(ctx context.Context, req dto.ListAppClusterReq) ([]dto.AppClusterInfo, error) {

	appClusterList, err := repository.ListAppCluster(ctx, req)
	if err != nil {
		return nil, err
	} else if len(appClusterList) == 0 {
		return nil, nil
	}

	clusterIDs := make([]uint64, 0, len(appClusterList))
	for _, appCluster := range appClusterList {
		clusterIDs = append(clusterIDs, appCluster.ClusterID)
	}

	clusterList, err := grpc.ListClusters(ctx, &core.ListClustersReq{
		Filter: &core.ClusterFilter{
			ClusterIds: clusterIDs,
		},
	})
	if err != nil {
		return nil, err
	}
	result := make([]dto.AppClusterInfo, 0, len(clusterList.Clusters))
	for _, appCluster := range appClusterList {
		clusterInfo, ok := clusterList.Clusters[appCluster.ClusterID]
		if !ok || clusterInfo == nil {
			tilog.Error(ctx, "ListAppCluster not found cluster info",
				zap.Uint64("dataapp_id", req.DataappID),
				zap.Uint64("cluster_id", req.ClusterID))
			continue
		}
		result = append(result, dto.AppClusterInfo{
			ID:          appCluster.ID,
			DataappID:   appCluster.DataappID,
			ClusterID:   appCluster.ClusterID,
			CreatedAt:   appCluster.CreatedAt.Time,
			ClusterName: clusterInfo.Name,
			TierType:    clusterInfo.DeployType.String(),
			Cloud:       clusterInfo.CloudProviderName,
			Region:      clusterInfo.RegionDisplayName,
			RegionCode:  clusterInfo.RegionName,
		})
	}
	return result, nil
}

func ListClusterLinkedAPP(ctx context.Context, projectID, clusterID uint64) ([]*entity.DataApp, error) {

	// query app cluster list
	appClusterList, err := repository.ListAppCluster(ctx, dto.ListAppClusterReq{ClusterID: clusterID})
	if err != nil {
		return nil, err
	} else if len(appClusterList) == 0 {
		return nil, nil
	}

	// query app list
	appIDs := make([]uint64, 0, len(appClusterList))
	for _, appCluster := range appClusterList {
		appIDs = append(appIDs, appCluster.DataappID)
	}
	appList, err := repository.ListAllDataApps(ctx, appIDs)
	if err != nil {
		return nil, err
	}
	if len(appList) == 0 {
		return nil, nil
	}
	// filter app by projectID
	filteredApps := make([]*entity.DataApp, 0, len(appList))
	for _, app := range appList {
		if app.ProjectId == projectID {
			filteredApps = append(filteredApps, app)
		}
	}
	return filteredApps, nil
}

func GetClusterStatus(ctx context.Context, clusterIDs []uint64) ([]*dataapi.ClusterStatusInfo, error) {

	clusterMap, err := grpc.ListClusterByIdsWPC(ctx, clusterIDs)
	if err != nil {
		return nil, err
	}
	clusterKeys := make([]repository_ddb.UserClusterKey, 0, len(clusterIDs))
	for _, clusterID := range clusterIDs {
		clusterInfo := clusterMap[clusterID]
		if clusterInfo == nil {
			continue
		}
		if usesProviderScopedRegionalHost(clusterInfo) {
			continue
		}
		clusterKeys = append(clusterKeys, repository_ddb.UserClusterKey{
			ProjectID: clusterInfo.ProjectId,
			ClusterID: clusterID,
		})
	}

	userClusterInfos, err := repository_ddb.ListUserClusterByIDs(ctx, ddb.Client(), clusterKeys)
	if err != nil {
		return nil, err
	}
	userClusterMap := make(map[uint64]*entity.UserClusterInfo, len(clusterIDs))
	for _, userClusterInfo := range userClusterInfos {
		userClusterMap[userClusterInfo.ClusterId] = userClusterInfo
	}

	dedicatedIDs := make([]uint64, 0, len(clusterIDs))
	result := make([]*dataapi.ClusterStatusInfo, 0, len(clusterIDs))
	for _, clusterID := range clusterIDs {
		clusterInfo := clusterMap[clusterID]
		if clusterInfo == nil {
			result = append(result, &dataapi.ClusterStatusInfo{
				ClusterId: clusterID,
				Status:    dataapi.ClusterStatus_Unsupported,
			})
			continue
		}
		if usesProviderScopedRegionalHost(clusterInfo) {
			result = append(result, &dataapi.ClusterStatusInfo{
				ClusterId: clusterID,
				Status:    getProviderScopedClusterStatus(clusterInfo),
			})
			continue
		}
		if isDedicatedCluster(clusterInfo) {
			dedicatedIDs = append(dedicatedIDs, clusterID)
		} else {
			var status dataapi.ClusterStatus
			if userClusterMap[clusterID] == nil {
				status = dataapi.ClusterStatus_Creating
			} else {
				status = dataapi.ClusterStatus_Normal
			}
			result = append(result, &dataapi.ClusterStatusInfo{
				ClusterId: clusterID,
				Status:    status,
			})
		}
	}

	if len(dedicatedIDs) == 0 {
		return result, nil
	}

	for _, dedicatedID := range dedicatedIDs {
		clusterInfo := clusterMap[dedicatedID]
		userClusterInfo := userClusterMap[dedicatedID]
		status, err := checkClusterPrivateLinkWithClusterInfo(ctx, clusterInfo, false, userClusterInfo)
		if err != nil {
			logger.LogWithError(ctx, err, "checkClusterPrivateLinkWithClusterInfo failed", zap.Uint64("cluster_id", dedicatedID))
			status = dataapi.ClusterStatus_Unsupported
		}
		result = append(result, &dataapi.ClusterStatusInfo{
			ClusterId:   dedicatedID,
			Status:      status,
			IsDedicated: true,
		})
	}
	return result, nil
}

func getProviderScopedClusterStatus(clusterInfo *core.Cluster) dataapi.ClusterStatus {
	if clusterInfo == nil {
		return dataapi.ClusterStatus_Unsupported
	}
	if validClusterStatus[clusterInfo.UserStatus] {
		return dataapi.ClusterStatus_Normal
	}
	switch clusterInfo.UserStatus {
	case core.UserClusterStatus_USER_CLUSTER_STATUS_CREATING,
		core.UserClusterStatus_USER_CLUSTER_STATUS_SCALING,
		core.UserClusterStatus_USER_CLUSTER_STATUS_UPGRADING,
		core.UserClusterStatus_USER_CLUSTER_STATUS_RECOVERING,
		core.UserClusterStatus_USER_CLUSTER_STATUS_RESUMING:
		return dataapi.ClusterStatus_Creating
	default:
		return dataapi.ClusterStatus_Unsupported
	}
}

func checkClusterPrivateLinkWithClusterInfo(ctx context.Context, clusterInfo *core.Cluster, needLoadDDB bool, userClusterInfo *entity.UserClusterInfo) (dataapi.ClusterStatus, error) {

	var (
		clusterID = clusterInfo.Id
		supported bool
	)
	for _, feature := range clusterInfo.Features {
		if feature == core.AvailableFeature_DELEGATE_USER {
			supported = true
		}
	}
	if !supported || !isDedicatedCluster(clusterInfo) || clusterInfo.CloudProviderName != "aws" {
		return dataapi.ClusterStatus_Unsupported, nil
	}

	linkServiceResp, berr := grpc.GetPrivateLinkService(ctx, &core.GetPrivateLinkServiceReq{
		OrgId:     clusterInfo.TenantId,
		ProjectId: clusterInfo.ProjectId,
		ClusterId: clusterInfo.Id,
	})
	if berr != nil {
		logger.LogWithError(ctx, berr, "GetPrivateLinkService failed", zap.Uint64("cluster_id", clusterID))
		return dataapi.ClusterStatus_NeedCreate, nil
	}
	if linkServiceResp.PrivatelinkService == nil {
		logger.Error(ctx, "GetPrivateLinkService got empty service endpoint", zap.Uint64("cluster_id", clusterID))
		return dataapi.ClusterStatus_NeedCreate, nil
	}
	switch linkServiceResp.PrivatelinkService.Status {
	case core.PrivateLinkService_CREATING:
		return dataapi.ClusterStatus_Creating, nil
	case core.PrivateLinkService_DELETING:
		return dataapi.ClusterStatus_Unsupported, nil
	case core.PrivateLinkService_NOT_CONFIGURE:
		return dataapi.ClusterStatus_NeedCreate, nil
	}

	if needLoadDDB {
		var err error
		userClusterInfo, err = repository_ddb.GetUserClusterInfo(ctx, ddb.Client(), clusterInfo.ProjectId, clusterID)
		if err != nil {
			if err == comerrs.ErrUserClusterNotFound {
				return dataapi.ClusterStatus_NeedCreate, nil
			}
			logger.LogWithError(ctx, err, "GetUserClusterInfo failed", zap.Uint64("cluster_id", clusterID))
			return dataapi.ClusterStatus_Unsupported, err
		}
	}

	if userClusterInfo == nil {
		return dataapi.ClusterStatus_NeedCreate, nil
	} else if userClusterInfo.PrivateDNS == "" {
		return dataapi.ClusterStatus_Creating, nil
	} else {
		resp, err := http.ResolveHostWithProvider(ctx, clusterInfo.RegionName, http.ProviderForRegionalHostPrefix(clusterInfo.CloudProviderName, clusterInfo.GetDeployType()), userClusterInfo.PrivateDNS)
		if err != nil {
			return 0, err
		} else if resp.IP == "" {
			return dataapi.ClusterStatus_Creating, nil
		}
	}
	return dataapi.ClusterStatus_Normal, nil
}

func checkClusterStatus(ctx context.Context, orgID, projectID uint64, clusterIDs []uint64, operator string) error {

	if len(clusterIDs) == 0 {
		return nil
	}

	dedicatedAllowed, err := grpc.IsOrgHitDedicatedWhitelist(ctx, orgID, operator)
	if err != nil {
		return err
	}

	// check cluster status
	clusterMap, err := grpc.ListClusterByIds(ctx, orgID, projectID, clusterIDs)
	if err != nil {
		return err
	}
	var illegalCluster []string
	for _, clusterID := range clusterIDs {
		clusterInfo := clusterMap[clusterID]
		if clusterInfo == nil {
			illegalCluster = append(illegalCluster, strconv.FormatUint(clusterID, 10))
			tilog.Warn(ctx, "cluster not found", zap.Uint64("cluster_id", clusterID))
			continue
		}
		if !validClusterStatus[clusterInfo.UserStatus] {
			illegalCluster = append(illegalCluster, strconv.FormatUint(clusterID, 10))
			tilog.Warn(ctx, "invalid cluster status", zap.Uint64("cluster_id", clusterID), zap.Int32("user_status", int32(clusterInfo.UserStatus)))
			continue
		}
		if isDedicatedCluster(clusterInfo) {
			if !dedicatedAllowed {
				illegalCluster = append(illegalCluster, strconv.FormatUint(clusterID, 10))
				tilog.Warn(ctx, "dedicated cluster not allowed, operator not in whitelist", zap.Uint64("cluster_id", clusterID), zap.String("operator", operator))
				continue
			}
			if !IsAWSDedicatedCluster(clusterInfo) {
				illegalCluster = append(illegalCluster, strconv.FormatUint(clusterID, 10))
				tilog.Warn(ctx, "only aws dedicated cluster allowed to bind with dataapp", zap.Uint64("cluster_id", clusterID), zap.String("cloud_provider_name", clusterInfo.GetCloudProviderName()))
				continue
			}
			if status, err := checkClusterPrivateLinkWithClusterInfo(ctx, clusterInfo, true, nil); err != nil {
				illegalCluster = append(illegalCluster, strconv.FormatUint(clusterID, 10))
				tilog.Warn(ctx, "check cluster private link status failed", zap.Uint64("cluster_id", clusterID), zap.Error(err))
			} else if status != dataapi.ClusterStatus_Normal {
				illegalCluster = append(illegalCluster, strconv.FormatUint(clusterID, 10))
				tilog.Warn(ctx, "cluster private link not ready", zap.Uint64("cluster_id", clusterID), zap.Int32("status", int32(status)), zap.Error(err))
			}
		}
	}
	if len(illegalCluster) > 0 {
		return errs.NewBadRequest("Cluster status is invalid. ClusterIDs: "+strings.Join(illegalCluster, ","), nil)
	}
	return nil
}

func getProjectAllClusterIDs(ctx context.Context, orgID, projectID uint64) ([]uint64, error) {
	clusterList, err := grpc.ListClusters(ctx, &core.ListClustersReq{
		Filter: &core.ClusterFilter{
			TenantIds:  []uint64{orgID},
			ProjectIds: []uint64{projectID},
		},
	})
	if err != nil {
		return nil, err
	}
	clusterIDs := make([]uint64, 0, len(clusterList.Clusters))
	for _, clusterInfo := range clusterList.Clusters {
		clusterIDs = append(clusterIDs, clusterInfo.Id)
	}
	return clusterIDs, nil
}

func isDedicatedCluster(clusterInfo *core.Cluster) bool {
	return clusterInfo.GetDeployType() == core.DeployType_DEPLOY_TYPE_DEDICATED
}

func IsAWSDedicatedCluster(clusterInfo *core.Cluster) bool {
	return isDedicatedCluster(clusterInfo) && clusterInfo.GetCloudProviderName() == constants.ClusterCloudProviderAWS
}

func IsSupportShadowAccount(clusterInfo *core.Cluster) bool {
	var supported bool
	for _, feature := range clusterInfo.Features {
		if feature == core.AvailableFeature_DELEGATE_USER {
			supported = true
		}
	}
	return supported
}

func SyncClusterWithPrivateLink(ctx context.Context, clusterIDs []uint64) map[uint64]string {
	result := make(map[uint64]string)
	logger := tilog.WithContextL(ctx)
	for _, clusterID := range clusterIDs {
		if err := doSyncClusterWithPrivateLink(ctx, logger.With(zap.Uint64("cluster_id", clusterID)), clusterID, nil); err != nil {
			result[clusterID] = err.Error()
		}
	}
	return result
}

func doSyncClusterWithPrivateLink(ctx context.Context, logger *zap.Logger, clusterID uint64, extraInfo map[string]interface{}) error {

	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	ddbClient := ddb.Client()

	clusterInfo, berr := grpc.GetClusterWithoutPermCheck(ctx, clusterID)
	if berr != nil {
		logger.Error("GetClusterWithoutPermCheck failed", zap.Error(berr))
		return berr
	}
	isDedicated := isDedicatedCluster(clusterInfo)
	if isDedicated && clusterInfo.CloudProviderName != "aws" {
		logger.Info("only aws cloud provider dedicated cluster supported", zap.String("cloud_provider", clusterInfo.CloudProviderName))
		return nil
	}

	userClusterInfo, err := repository_ddb.GetUserClusterInfo(ctx, ddbClient, clusterInfo.ProjectId, clusterID)
	if err != nil {
		if !errs.IsError(err, comerrs.ErrUserClusterNotFound) {
			logger.Error("GetUserClusterInfo failed", zap.Error(err))
			return err
		}
		userClusterInfo = &entity.UserClusterInfo{}
	}

	userClusterInfo.OrgId = clusterInfo.TenantId
	userClusterInfo.ProjectId = clusterInfo.ProjectId
	userClusterInfo.ClusterId = clusterInfo.Id
	userClusterInfo.Cloud = clusterInfo.CloudProviderName
	userClusterInfo.Region = clusterInfo.RegionName
	userClusterInfo.IsDedicate = isDedicated
	if isDedicated {
		if !config.SupportsDataserviceControl(clusterInfo.CloudProviderName, clusterInfo.RegionName) {
			logger.Info("skip GetPrivateLinkService for unsupported dataservice-control region", zap.String("cloud_provider", clusterInfo.CloudProviderName), zap.String("region", clusterInfo.RegionName))
			return nil
		}
		linkServiceResp, err := grpc.GetPrivateLinkService(ctx, &core.GetPrivateLinkServiceReq{
			OrgId:     clusterInfo.TenantId,
			ProjectId: clusterInfo.ProjectId,
			ClusterId: clusterInfo.Id,
		})
		if err != nil {
			logger.Error("GetPrivateLinkService failed", zap.Uint64("cluster_id", clusterID))
			return err
		}
		if linkServiceResp.PrivatelinkService.Status == core.PrivateLinkService_ACTIVE {
			userClusterInfo.EndpointService = linkServiceResp.PrivatelinkService.Name
			userClusterInfo.AZ = strings.Join(linkServiceResp.PrivatelinkService.AzIds, ",")
			userClusterInfo.Port = uint64(linkServiceResp.PrivatelinkService.Port)
		}
	} else {
		// privatelink has been used on prod env
		if !config.IsProd() {
			for _, connection := range clusterInfo.GetConnections() {
				if connection.Type == core.ClusterConnectionType_CLUSTER_CONNECTION_TYPE_PUBLIC {
					userClusterInfo.Host = connection.Host
					userClusterInfo.Port = uint64(connection.Port)
				}
			}
			if userClusterInfo.Host == "" || userClusterInfo.Port == 0 {
				err = errors.NewBadRequest("not found user public endpoint connection for dev/staging env")
				logger.Error("not found user public endpoint connection for dev/staging env")
				return err
			}
		} else {
			userClusterInfo.Port = uint64(4000)
		}
	}

	if err := repository_ddb.PutUserClusterInfo(ctx, ddbClient, userClusterInfo); err != nil {
		logger.Error("PutUserClusterInfo failed", zap.Error(err))
		return err
	}
	logger.Info("PutUserClusterInfo success", zap.Uint64("project_id", userClusterInfo.ProjectId), zap.Uint64("cluster_id", userClusterInfo.ClusterId))

	// delete old cluster info if project changed, this is for cluster migrate case, in other cases project should not be changed
	if oldProjectID, ok := extraInfo["old_project_id"].(string); ok {
		oldProjectIDUint, _ := strconv.ParseUint(oldProjectID, 10, 64)
		if oldProjectIDUint != clusterInfo.ProjectId {
			if err := repository_ddb.DeleteUserClusterInfo(ctx, ddbClient, oldProjectIDUint, clusterID); err != nil {
				logger.Error("DeleteUserClusterInfo failed when project changed", zap.Uint64("old_project_id", oldProjectIDUint),
					zap.Uint64("cluster_id", clusterID), zap.Error(err))
			} else {
				logger.Info("DeleteUserClusterInfo success when project changed", zap.Uint64("old_project_id", oldProjectIDUint), zap.Uint64("cluster_id", clusterID))
			}
		}
	}

	logger.Info("sync cluster with private link success", zap.Bool("is_dedicated", isDedicated),
		zap.String("endpoint_service", userClusterInfo.EndpointService),
		zap.String("host", userClusterInfo.Host), zap.Uint64("port", userClusterInfo.Port))

	// create private link interface endpoint if is dedicated cluster
	if isDedicated && userClusterInfo.PrivateDNS == "" {
		if ok, err := commonSrv.InDedicatedOrgWhitelist(ctx, clusterInfo.TenantId); err != nil {
			logger.Error("failed to check org in whitelist", zap.Uint64("org_id", clusterInfo.TenantId), zap.Uint64("cluster_id", clusterID), zap.Error(err))
			return err
		} else if ok {
			if err := commonSrv.PutPrivatelinkTask(ctx, clusterInfo.ProjectId, clusterID, clusterInfo.CloudProviderName, clusterInfo.RegionName, false, "sync_cluster"); err != nil {
				logger.Error("failed to put private link task for create cluster", zap.Uint64("cluster_id", clusterID), zap.Error(err))
				return err
			}
		}
	}
	return nil
}
