// Package service add by blacknc <blacknc@163.com> at 2023/04/06 15:38:19
package service

import (
	"context"
	comerrs "dataservices/commons/errs"
	"dataservices/commons/eventbus"
	"dataservices/commons/grpc"
	"dataservices/ddb"
	dto "dataservices/internal/dto/dataapi"
	"dataservices/internal/entity"
	"dataservices/internal/repository"
	"dataservices/internal/repository_ddb"
	commonSrv "dataservices/internal/service/common"
	"strings"
	"time"

	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	"go.uber.org/zap"
)

const (
	privateLinkMaxRetry = 60
)

func init() {
	registerHandler(
		eventbus.HandlerConfig{
			EventType:       dto.BusEventCluster,
			DataStruct:      dto.ClusterEvent{},
			HandleFunc:      handleClusterEvent,
			NumOfGoroutines: 6,
		},
	)
}

func handleClusterEvent(ctx context.Context, logger *zap.Logger, event *eventbus.Event) (err error) {
	logger.Info("handleClusterEvent begin")
	defer func() {
		logger.Info("handleClusterEvent end", zap.Error(err))
	}()

	clusterEvent, ok := event.Data.(*dto.ClusterEvent)
	if !ok || clusterEvent == nil {
		logger.Error("got invalid event data", zap.Any("event", event))
		return comerrs.ErrInvalidEvent
	}

	switch clusterEvent.EventType {
	case dto.ClusterActionNormal, dto.ClusterActionProjectChanged:
		err = handleClusterNormalEvent(ctx, logger, clusterEvent)
	case dto.ClusterActionDel:
		err = handleClusterDeleteEvent(ctx, logger, clusterEvent)
	default:
		logger.Info("event will be dicard", zap.String("event_type", clusterEvent.EventType))
	}
	return err
}

func handleClusterNormalEvent(ctx context.Context, logger *zap.Logger, clusterEvent *dto.ClusterEvent) error {
	clusterID := clusterEvent.ClusterID
	extraInfo := clusterEvent.ExtraInfo
	logger = logger.With(zap.Uint64("cluster_id", clusterID))
	logger.Info("begin deal create/update cluster event")

	clusterInfo, err := grpc.GetClusterWithoutPermCheck(ctx, clusterID)
	if err != nil {
		logger.Error("GetClusterWithoutPermCheck failed", zap.Error(err))
		return err
	}
	// AliCloud serverless does not deploy regional control and does not need the
	// DDB/private-link sync path triggered by cluster create events.
	if shouldSkipClusterCreateSync(clusterInfo) {
		logger.Info("skip doSyncClusterWithPrivateLink for alicloud serverless cluster")
		return nil
	}

	err = doSyncClusterWithPrivateLink(ctx, logger, clusterID, extraInfo)
	if err == nil || !strings.Contains(err.Error(), "private link service is not ready") {
		return err
	}
	logger.Warn("wait cluster private link ready", zap.Error(err))
	go func() {
		defer func() {
			if r := recover(); r != nil {
				logger.Error("doSyncClusterWithPrivateLink panic", zap.Any("panic", r))
			}
		}()

		for i := 0; i < privateLinkMaxRetry; i++ {
			<-time.After(time.Second * 10)
			if err := doSyncClusterWithPrivateLink(ctx, logger, clusterID, extraInfo); err == nil {
				logger.Info("doSyncClusterWithPrivateLink succeed")
				return
			}
			logger.Error("doSyncClusterWithPrivateLink failed", zap.Error(err))
		}
		logger.Warn("doSyncClusterWithPrivateLink failed with max retry", zap.Int("max_retry", privateLinkMaxRetry))
	}()
	return nil
}

func shouldSkipClusterCreateSync(clusterInfo *core.Cluster) bool {
	return usesProviderScopedRegionalHost(clusterInfo)
}

func handleClusterDeleteEvent(ctx context.Context, logger *zap.Logger, clusterEvent *dto.ClusterEvent) error {
	projectID := clusterEvent.ProjectID
	clusterID := clusterEvent.ClusterID
	isDedicated := !clusterEvent.IsDevTier
	logger = logger.With(zap.Uint64("cluster_id", clusterID))
	appClusters, err := repository.ListAppCluster(ctx, dto.ListAppClusterReq{ClusterID: clusterID})
	if err != nil {
		logger.Error("list app cluster failed", zap.Error(err))
		return err
	}
	logger.Info("begin deal app cluster", zap.Int("count", len(appClusters)))

	var (
		linkClusterCount  int
		publishEventCount int
	)
	for _, appCluster := range appClusters {
		appID := appCluster.DataappID
		createdBy := appCluster.CreatedBy

		// delete app cluster
		if err := repository.DeleteAppCluster(ctx, appID, clusterID, createdBy); err != nil {
			logger.Error("delete multi app cluster failed",
				zap.Uint64("app_id", appID),
				zap.Error(err))
			continue
		}
		linkClusterCount++

		if _, err := TriggerClusterDeleteFromUI(ctx, appID, clusterID, createdBy); err != nil {
			logger.Error("TriggerClusterDeleteFromUI failed",
				zap.Uint64("app_id", appID),
				zap.Error(err))
			continue
		}

		// // notify eventbus
		// if err := publishAppClusterEvent(ctx, appID, []uint64{clusterID}, createdBy, dto.DataAppBindClusterActionDelCluster); err != nil {
		// 	logger.Error("publish cluster delete event failed",
		// 		zap.Uint64("app_id", appID),
		// 		zap.Uint64("cluster_id", clusterID),
		// 		zap.Error(err))
		// 	continue
		// }
		publishEventCount++
	}

	ddbClient := ddb.Client()

	// delete private link
	if isDedicated {
		clusterInfo, err := grpc.GetClusterByID(ctx, clusterID)
		if err != nil {
			logger.Error("GetClusterByID failed", zap.Error(err))
			return err
		}
		if err := commonSrv.PutPrivatelinkTask(ctx, projectID, clusterID, clusterInfo.CloudProviderName, clusterInfo.RegionName, true, "delete_cluster"); err != nil {
			logger.Error("failed to put private link task for delete cluster", zap.Uint64("cluster_id", clusterID), zap.Error(err))
			return err
		}
	} else {
		if err := repository_ddb.DeleteUserClusterInfo(ctx, ddbClient, projectID, clusterID); err != nil {
			logger.Warn("DeleteUserClusterInfo failed", zap.Error(err))
		}
	}

	appList, err := repository.ListDataApps(ctx, projectID, string(entity.DataAppTypeChat2Query))
	if err != nil {
		logger.Error("list dataapp failed, can not push app cluster event", zap.Error(err))
	} else {
		for _, appInfo := range appList {
			publishAppClusterEvent(ctx, appInfo.ID, []uint64{clusterID}, appInfo.CreatedBy, dto.DataAppBindClusterActionDelCluster)
		}
	}

	logger.Info("end deal cluster",
		zap.Int("total_link_cluster_count", len(appClusters)),
		zap.Int("del_link_cluster_succ_count", linkClusterCount),
		zap.Int("publish_event_succ_count", publishEventCount))
	return nil
}
