package event_svc

import (
	"context"
	"errors"
	"slices"
	"strconv"
	"time"

	core_event "github.com/tidbcloud/core-svc/event"
	"github.com/tidbcloud/pkgv2/bizerr"
	"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/domain/service"
	"github.com/tidbcloud/serverless-service/internal/metric"
	"github.com/tidbcloud/serverless-service/internal/utils"
	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"go.uber.org/zap"
)

const (
	EventUnknown = "unknown"
)

var focusedEvents = []core_event.ClusterEventType{
	core_event.ClusterEventTypeCreated,
	core_event.ClusterEventTypeDeleted,
	ClusterEventTypeUpdateCapacity,
}

type BackupEventService struct {
	backupSchedule repo.BackupScheduleRepo
	userBackupRepo repo.UserBackupRepo
	mgmtSvc        facade.MgmtSvcFacade
	billingFacade  facade.BillingFacade
}

func NewBackupEventService(
	backupSchedule repo.BackupScheduleRepo,
	userBackupRepo repo.UserBackupRepo,
	mgmtSvc facade.MgmtSvcFacade,
	billingFacade facade.BillingFacade,
) *BackupEventService {
	return &BackupEventService{
		backupSchedule: backupSchedule,
		userBackupRepo: userBackupRepo,
		mgmtSvc:        mgmtSvc,
		billingFacade:  billingFacade,
	}
}

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

	if !slices.Contains(focusedEvents, event.EventType) {
		logger.Info("skip the cluster event")
		return nil
	}

	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")
			return
		}

		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		eventID := ctx.Value(config.CtxEventIDKey)
		eventIDString := EventUnknown
		if eventID != nil {
			if id, ok := eventID.(string); ok {
				eventIDString = id
			}
		}
		metric.BackupEventHandlerCounter.WithLabelValues(eventIDString, strconv.FormatUint(event.ClusterID, 10), string(event.EventType), status).
			Inc()
	}()

	if event.EventType == core_event.ClusterEventTypeCreated {
		return b.handleCreated(ctx, logger, event)
	}
	if event.EventType == core_event.ClusterEventTypeDeleted {
		return b.handleDeleted(ctx, logger, event)
	}
	if event.EventType == ClusterEventTypeUpdateCapacity {
		return b.handleClusterCapacityEvent(ctx, logger, event)
	}
	return nil
}

func (b *BackupEventService) handleCreated(
	ctx context.Context,
	logger *zap.Logger,
	event *core_event.ClusterEvent,
) error {
	logger.Info("start to handle cluster created event")
	cluster, err := b.mgmtSvc.GetCluster(ctx, event.ClusterID, tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC)
	if err != nil {
		if err.ErrCode() == bizerr.ErrCodeRecordNotFound {
			logger.Warn("cluster not found, skip creating backup schedule")
			return nil
		}
		logger.Error("failed to get cluster", zap.Error(err))
		return err
	}
	clusterPlan, err := service.GetClusterPlan(ctx, cluster, b.billingFacade)
	if err != nil {
		logger.Error("failed to get cluster plan", zap.Error(err))
		return err
	}
	retention := service.GetDefaultBackupRetentionByClusterPlan(clusterPlan)

	scheduleCron := service.GetDefaultBackupScheduleCron(cluster.Region)
	nextScheduleTime, err1 := calculateNextScheduleTime(scheduleCron)
	if err1 != nil {
		logger.Error("failed to calculate next schedule time", zap.Error(err1))
		return err1
	}
	kubernetes := cluster.GetServerlessCluster().GetCluster().GetKubernetes()
	if kubernetes == "" {
		logger.Error("kubernetes is empty")
		return errors.New("kubernetes is empty")
	}
	sourceToken := cluster.GetServerlessCluster().GetCluster().GetUserPrefix()
	enhancedEncryptionEnabled := cluster.GetServerlessCluster().GetCluster().GetEnhancedEncryptionEnabled()
	zoneRedundantEnabled := cluster.GetServerlessCluster().GetCluster().GetZoneRedundantEnabled()
	logger.Info("start to create backup schedule", zap.Uint64("clusterId", event.ClusterID),
		zap.String("cron", scheduleCron), zap.Time("next_schedule_time", nextScheduleTime),
		zap.Int("retention", int(retention.Hours())), zap.String("region", cluster.Region),
		zap.String("kubernetes", kubernetes))
	err = b.backupSchedule.Create(ctx, &entity.BackupSchedule{
		TenantID:                  strconv.FormatUint(event.TenantID, 10),
		ClusterID:                 strconv.FormatUint(event.ClusterID, 10),
		Region:                    cluster.Region,
		Retention:                 int(retention.Hours()),
		ScheduleCron:              scheduleCron,
		NextScheduleTime:          &nextScheduleTime,
		Kubernetes:                kubernetes,
		CloudProvider:             cluster.CloudProvider,
		SourceToken:               sourceToken,
		ServicePlan:               clusterPlan,
		EnhancedEncryptionEnabled: &enhancedEncryptionEnabled,
		ZoneRedundantEnabled:      &zoneRedundantEnabled,
	})
	if err == nil || err.ErrCode() != bizerr.ErrCodeRecordExist {
		return err
	}
	// this only happens when create cluster with backup schedule
	// in that case, the kubernetes field will be unknown, we need to update it
	logger.Warn("backup schedule already exists, skip creating backup schedule and set the kubernetes")
	err = b.backupSchedule.UpdateKubernetesByClusterID(ctx, strconv.FormatUint(event.ClusterID, 10), kubernetes)
	if err != nil {
		logger.Error("failed to update kubernetes", zap.Uint64("cluster_id", event.ClusterID), zap.Error(err))
		return err
	}
	return nil
}

func (b *BackupEventService) handleDeleted(
	ctx context.Context,
	logger *zap.Logger,
	event *core_event.ClusterEvent,
) error {
	logger.Info("start to handle cluster deleted event")
	return b.backupSchedule.DeleteByClusterID(ctx, strconv.FormatUint(event.ClusterID, 10))
}

func (b *BackupEventService) handleClusterCapacityEvent(
	ctx context.Context,
	logger *zap.Logger,
	event *core_event.ClusterEvent,
) error {
	logger.Info("start to handle cluster capacity event")
	clusterID := strconv.FormatUint(event.ClusterID, 10)

	cluster, err := b.mgmtSvc.GetCluster(ctx, event.ClusterID, tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC)
	if err != nil {
		if err.ErrCode() == bizerr.ErrCodeRecordNotFound {
			logger.Info("cluster not found, skip")
			return nil
		}
		logger.Error("failed to get cluster", zap.Error(err))
		return err
	}

	clusterPlan, err := service.GetClusterPlan(ctx, cluster, b.billingFacade)
	if err != nil {
		logger.Error("failed to get cluster plan", zap.Error(err))
		return err
	}

	schedules, err := b.backupSchedule.List(ctx, entity.ListBackupScheduleOptions{ClusterId: clusterID})
	if err != nil {
		logger.Error("failed to list backup schedule", zap.Error(err))
		return err
	}
	if len(schedules) == 0 {
		logger.Info("backup schedule not found, skip")
		return nil
	}
	schedule := schedules[0]
	if schedule.ServicePlan == clusterPlan {
		return nil
	}
	schedule.ServicePlan = clusterPlan
	if _, err := b.backupSchedule.Save(ctx, schedule); err != nil {
		logger.Error("failed to update cluster plan in backup schedule", zap.Error(err))
		return err
	}
	return nil
}

func calculateNextScheduleTime(scheduleCron string) (time.Time, error) {
	expr, err := constant.BackupCronParse.Parse(scheduleCron)
	if err != nil {
		return time.Time{}, err
	}
	nextTime := expr.Next(time.Now().UTC())
	return nextTime.UTC(), nil
}
