package event_svc

import (
	"context"
	"fmt"
	"strings"

	core_event "github.com/tidbcloud/core-svc/event"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/db"
	"github.com/tidbcloud/pkgv2/tracing"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"github.com/tidbcloud/serverless-service/internal/domain/entity"
	"github.com/tidbcloud/serverless-service/internal/domain/facade"
	"github.com/tidbcloud/serverless-service/internal/domain/repo"
	"github.com/tidbcloud/serverless-service/internal/utils"
	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"go.uber.org/zap"
)

type ClusterEventService struct {
	billingFacade   facade.BillingFacade
	clusterRepo     repo.ClusterRepo
	clusterTidbPool repo.ClusterTidbPoolRepo
	canaryRepo      repo.CanaryRepo
	dbProvider      db.TransProvider
	mgmtFacade      facade.MgmtSvcFacade
	accountFacade   facade.AccountFacade
}

func NewClusterEventService(
	billingFacade facade.BillingFacade,
	clusterRepo repo.ClusterRepo,
	clusterTidbPool repo.ClusterTidbPoolRepo,
	canaryRepo repo.CanaryRepo,
	dbProvider db.TransProvider,
	mgmtFacade facade.MgmtSvcFacade,
	accountFacade facade.AccountFacade,
) *ClusterEventService {
	return &ClusterEventService{
		billingFacade:   billingFacade,
		clusterRepo:     clusterRepo,
		clusterTidbPool: clusterTidbPool,
		canaryRepo:      canaryRepo,
		dbProvider:      dbProvider,
		mgmtFacade:      mgmtFacade,
		accountFacade:   accountFacade,
	}
}

func (s *ClusterEventService) HandleEvent(ctx context.Context, event *core_event.ClusterEvent) (err error) {
	ctx, span := Tracer.Start(ctx, "clustersvc.HandleClusterEvent")
	logger := utils.GetEventBusLoggerFromContext(ctx).
		With(zap.Uint64("cluster_id", event.ClusterID)).
		With(zap.String("event_type", string(event.EventType)))

	logger.Info("start to handle cluster event")
	defer func() {
		tracing.EndSpan(span, err)
		if err != nil {
			logger.Error("failed to handle cluster event", zap.Error(err))
		} else {
			logger.Info("succeed to handle cluster event")
		}
	}()

	switch event.EventType {
	case core_event.ClusterEventTypeCreated:
		return s.handleCreated(ctx, event.ClusterID, event.ProjectID)
	case core_event.ClusterEventTypeDeleted:
		return s.handleDeleted(ctx, event.TenantID, event.ClusterID)
	}

	return nil
}

func (s *ClusterEventService) handleCreatedImpl(ctx context.Context, clusterID, projectID uint64) error {
	err := s.clusterTidbPool.Save(ctx, &entity.ClusterTidbPool{
		Type:             constant.TypeSpendLimit,
		ClusterID:        clusterID,
		SpendLimitStatus: constant.SpendLimitEventStatusCreateCluster,
	})
	if err != nil {
		return err
	}

	mgmtCluster, err := s.mgmtFacade.GetCluster(ctx, clusterID, tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC)
	if mgmtCluster == nil || err != nil {
		return err
	}
	region := mgmtCluster.GetRegion()
	provider := mgmtCluster.GetCloudProvider()
	kubernetes := mgmtCluster.GetServerlessCluster().Cluster.GetKubernetes()
	isEssential := mgmtCluster.GetServerlessCluster().GetCluster().GetAutoScaling().GetMaxRcu() > 0
	forceNewClustersToCanaryPool := config.GetKubernetesConfig(
		provider,
		region,
		kubernetes,
	).ForceNewClustersToCanaryPool
	forceNewEssentialToCanaryPool := config.GetKubernetesConfig(
		provider,
		region,
		kubernetes,
	).ForceNewEssentialToCanaryPool
	if (!isEssential && forceNewClustersToCanaryPool) ||
		(isEssential && forceNewEssentialToCanaryPool) {
		return s.canaryRepo.AddClusters(ctx, []*repo.CanaryCluster{
			{ClusterID: clusterID, ProjectID: 0},
		})
	}

	// If the cluster is not belong to a canary project, ignore it.
	_, err = s.canaryRepo.GetProject(ctx, projectID)
	if err != nil {
		if err.ErrCode() == bizerr.ErrCodeRecordNotFound {
			return nil
		}
	}

	return s.canaryRepo.AddClusters(ctx, []*repo.CanaryCluster{
		{ClusterID: clusterID, ProjectID: projectID},
	})
}

func (s *ClusterEventService) sendEssentialNotification(ctx context.Context, clusterID uint64) {
	if _, err := s.clusterRepo.GetClusterByID(ctx, clusterID); err == nil ||
		!strings.Contains(err.ErrMsg(), "not found") {
		// ignore: cluster already exist
		return
	}
	mgmtCluster, _ := s.mgmtFacade.GetCluster(ctx, clusterID, tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC)
	if mgmtCluster.GetServerlessCluster().GetCluster().GetAutoScaling().GetMaxRcu() == 0 ||
		utils.IsNotifyBlockTenantsAndEnv(mgmtCluster.GetOrganizationId()) {
		// not essential plan or block tenant, ignore
		return
	}
	tenant, _ := s.accountFacade.GetTenant(ctx, mgmtCluster.OrganizationId)
	if tenant == nil {
		tenant = &facade.Tenant{}
	}
	utils.SendLarkMessage(
		fmt.Sprintf("[%s] New Essential Cluster", strings.ToUpper(config.Global().ServerConfig.Stage)),
		fmt.Sprintf(
			"Tenant: %d / %s / %s / %v\n Cluster: %d/%s / %s/%s\n Min RCU: %d\n Max RCU: %d\n Zone Redundant: %v\n Dual-layer Encryption: %v",
			mgmtCluster.OrganizationId,
			tenant.Name,
			tenant.Email,
			tenant.Kind,
			mgmtCluster.ClusterId,
			mgmtCluster.ClusterName,
			mgmtCluster.CloudProvider,
			mgmtCluster.Region,
			mgmtCluster.GetServerlessCluster().GetCluster().GetAutoScaling().GetMinRcu(),
			mgmtCluster.GetServerlessCluster().GetCluster().GetAutoScaling().GetMaxRcu(),
			mgmtCluster.GetServerlessCluster().GetCluster().GetZoneRedundantEnabled(),
			mgmtCluster.GetServerlessCluster().GetCluster().GetEnhancedEncryptionEnabled(),
		),
		utils.LarkMsgLevelInfo)
}

func (s *ClusterEventService) handleCreated(ctx context.Context, clusterID, projectID uint64) error {
	go s.sendEssentialNotification(ctx, clusterID)

	return s.dbProvider.Transaction(ctx, func(ctx context.Context) error {
		return s.handleCreatedImpl(ctx, clusterID, projectID)
	})
}

func (s *ClusterEventService) handleDeleted(ctx context.Context, tenantID, clusterID uint64) error {
	if config.IsLocalStage() {
		return nil
	}

	if setting, _ := s.billingFacade.GetSpendLimit(ctx, tenantID, clusterID); setting != nil {
		if err := s.billingFacade.DeleteSpendLimit(ctx, tenantID, setting.SpendLimitSettingID); err != nil {
			utils.GetEventBusLoggerFromContext(ctx).With(zap.Uint64("cluster_id", clusterID)).
				Error("call billing DeleteSpendLimit failed", zap.Error(err))
		}
	}

	return s.clusterRepo.Remove(ctx, clusterID)
}
