package br

import (
	"context"
	"fmt"
	"strconv"
	"time"

	clusterngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/proto"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/regionalserver"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/br"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/cluster"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/resourcepool"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/resourcepoolpolicy"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/displayevent"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/infra/repoimpl/schema"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func (s *serviceImpl) UpdateBackupSetting(
	ctx context.Context,
	req *globalngv1.UpdateBackupSettingRequest,
) (*globalngv1.BackupSetting, bizerr.BizError) {
	return s.updateBackupSetting(ctx, req, backupSettingUpdateSourceStandard)
}

type backupSettingUpdateSource int

const (
	backupSettingUpdateSourceStandard backupSettingUpdateSource = iota
	backupSettingUpdateSourceIncidentOverride
	backupSettingUpdateSourceSystemBackfill
)

func (s *serviceImpl) updateBackupSetting(
	ctx context.Context,
	req *globalngv1.UpdateBackupSettingRequest,
	updateSource backupSettingUpdateSource,
) (*globalngv1.BackupSetting, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.UpdateBackupSetting")
	defer span.End()

	if req == nil || req.BackupSetting == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "backup_setting is required")
	}
	clusterIdUint64, err := strconv.ParseUint(req.BackupSetting.ClusterId, 10, 64)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster_id is invalid")
	}

	backupSettingInDB, err := s.backupSettingRepo.Get(ctx, clusterIdUint64)
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to get backup setting from db")
	}
	return s.updateBackupSettingWithEntity(ctx, req, backupSettingInDB, updateSource)
}

func (s *serviceImpl) updateBackupSettingWithEntity(
	ctx context.Context,
	req *globalngv1.UpdateBackupSettingRequest,
	backupSettingInDB *entity.BackupSetting,
	updateSource backupSettingUpdateSource,
) (*globalngv1.BackupSetting, bizerr.BizError) {
	req = normalizeUpdateBackupSettingRequest(req, backupSettingInDB)

	if err := s.validateUpdateBackupSettingRequest(req, backupSettingInDB, updateSource); err != nil {
		return nil, err
	}
	if berr := s.checkResourcePoolForBRMutation(
		ctx,
		backupSettingInDB.SharedPoolId,
		resourcepoolpolicy.OperationScheduledBackup,
	); berr != nil {
		return nil, berr
	}
	if req.ValidateOnly {
		backupSetting, err := s.assembleBackupSetting(ctx, fmt.Sprintf("%d", backupSettingInDB.ClusterId))
		if err != nil {
			return nil, err.Chain("failed to assemble backup setting")
		}
		return backupSetting, nil
	}

	previousSetting := backupSettingEventStateFromEntity(backupSettingInDB)
	err := s.transaction.Transaction(ctx, func(ctx context.Context) error {
		if berr := s.lockResourcePoolForScheduledBackupMutation(ctx, backupSettingInDB.SharedPoolId); berr != nil {
			return berr
		}
		mode := schema.BackupModeStandardBundle
		if req.BackupSetting.GetMode() == globalngv1.BackupSetting_FLEXIBLE_RETENTION {
			mode = schema.BackupModeFlexibleRetention
		}
		retentionDays := StandardRetentionDays
		dailyBackupTimeUTC := DefaultDailyBackupTimeUTC
		if mode == schema.BackupModeFlexibleRetention {
			retentionDays = int(req.BackupSetting.GetRetentionDays())
			dailyBackupTimeUTC = req.BackupSetting.GetDailyBackupTimeUtc()
		}
		dailyMinute, _ := ParseDailyBackupTimeUTC(dailyBackupTimeUTC)

		backupSettingInDB.BackupPolicy = schema.BackupPolicyNameTIER1
		backupSettingInDB.Mode = mode
		backupSettingInDB.RetentionDays = retentionDays
		backupSettingInDB.DailyBackupTimeUTC = dailyBackupTimeUTC
		if shouldUpdateLastScheduleUpdatedAt(updateSource) {
			backupSettingInDB.LastScheduleUpdatedAt = time.Now()
		}
		scheduleType := "per-hour"
		scheduleAt := dailyMinute % 60
		if mode == schema.BackupModeFlexibleRetention {
			// Flexible retention exposes one daily backup at the user-selected
			// UTC time. A per-day schedule uses the minute-of-day as its offset.
			scheduleType = "per-day"
			scheduleAt = dailyMinute
		}
		backupSettingInDB.Schedule = schema.AutoSchedule{
			Type:                   scheduleType,
			At:                     scheduleAt,
			DailyBackupMinuteOfDay: dailyMinute,
		}
		if mode == schema.BackupModeFlexibleRetention {
			backupSettingInDB.Schedule.NonZeroClockRetentionDays = retentionDays
			backupSettingInDB.Schedule.ZeroClockRetentionDays = retentionDays
		} else {
			backupSettingInDB.Schedule.NonZeroClockRetentionDays = StandardRetentionDays
			backupSettingInDB.Schedule.ZeroClockRetentionDays = StandardDailyRetentionDays
		}
		ierr := s.backupSettingRepo.Save(ctx, backupSettingInDB)
		if ierr != nil {
			return ierr.Chain("failed to save backup setting")
		}

		ierr = s.updateAutoBackupExpiredTime(ctx, backupSettingInDB)
		if ierr != nil {
			return ierr.Chain("failed to update auto backup expired time")
		}

		return nil
	})
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to update backup setting")
	}
	if berr := UpdateSharedPoolBackupGCSetting(
		ctx,
		backupSettingInDB.SharedPoolId,
		s.backupSettingRepo,
		s.backupPolicyRepo,
		s.resourcePoolRepo,
		s.regionRepo,
		s.regionalCli,
	); berr != nil {
		return nil, berr.Chain("failed to update regional backup gc setting")
	}

	if berr := UpdateAutoBackupMetering(
		ctx,
		backupSettingInDB,
		s.clusterRepo,
		s.resourcePoolRepo,
		s.regionRepo,
		s.regionalCli,
	); berr != nil {
		return nil, berr.Chain("failed to update auto backup metetering")
	}

	updatedBackupSetting, berr := s.assembleBackupSetting(ctx, fmt.Sprintf("%d", backupSettingInDB.ClusterId))
	if berr != nil {
		return nil, berr.Chain("failed to assemble backup setting")
	}
	currentSetting := backupSettingEventStateFromEntity(backupSettingInDB)
	if shouldWriteBackupSettingDisplayEvent(updateSource) {
		displayEv := newBackupSettingDisplayEvent(
			ctx,
			backupSettingInDB.ClusterId,
			backupSettingInDB.TenantId,
			&previousSetting,
			&currentSetting,
		)
		displayevent.Write(ctx, s.eventCommitter, displayEv)
	}

	// TODO: send notification if needed

	return updatedBackupSetting, nil
}

func shouldWriteBackupSettingDisplayEvent(updateSource backupSettingUpdateSource) bool {
	return updateSource != backupSettingUpdateSourceSystemBackfill
}

func shouldUpdateLastScheduleUpdatedAt(updateSource backupSettingUpdateSource) bool {
	return updateSource != backupSettingUpdateSourceSystemBackfill
}

func normalizeUpdateBackupSettingRequest(
	req *globalngv1.UpdateBackupSettingRequest,
	backupSettingInDB *entity.BackupSetting,
) *globalngv1.UpdateBackupSettingRequest {
	if req == nil || req.BackupSetting == nil || backupSettingInDB == nil ||
		backupSettingInDB.ServicePlan != globalcommonv1.ServicePlan_Essential_V2.String() ||
		req.BackupSetting.GetMode() != globalngv1.BackupSetting_BACKUP_MODE_UNSPECIFIED {
		return req
	}

	normalized := proto.CloneOf(req)
	normalized.BackupSetting.Mode = globalngv1.BackupSetting_FLEXIBLE_RETENTION
	if normalized.BackupSetting.RetentionDays == nil {
		retentionDays := StandardRetentionDays
		if backupSettingInDB.Mode == schema.BackupModeFlexibleRetention {
			retentionDays = backupSettingInDB.RetentionDays
		}
		normalized.BackupSetting.RetentionDays = proto.Int32(int32(retentionDays))
	}
	if normalized.BackupSetting.DailyBackupTimeUtc == nil {
		dailyBackupTimeUTC := DefaultDailyBackupTimeUTC
		if backupSettingInDB.Mode == schema.BackupModeFlexibleRetention {
			dailyBackupTimeUTC = backupSettingInDB.DailyBackupTimeUTC
		}
		normalized.BackupSetting.DailyBackupTimeUtc = proto.String(dailyBackupTimeUTC)
	}
	return normalized
}

func UpdateAutoBackupMetering(
	ctx context.Context,
	backupSettingInDB *entity.BackupSetting,
	clusterRepo cluster.Repo,
	resourcePoolRepo resourcepool.Repo,
	regionRepo region.Repo,
	regionalCli regionalserver.Client,
) bizerr.BizError {
	clusterEntity, berr := clusterRepo.Get(ctx, backupSettingInDB.ClusterId, false)
	if berr != nil {
		return berr.Chain("failed to get cluster: %d", backupSettingInDB.ClusterId)
	}

	if clusterEntity.ServicePlan != globalcommonv1.ServicePlan_Premium &&
		clusterEntity.ServicePlan != globalcommonv1.ServicePlan_Essential_V2 &&
		clusterEntity.ServicePlan != globalcommonv1.ServicePlan_Premium_Reserved {
		tilog.Info(ctx, "cluster does not support auto backup metering, skip update", zap.Any("cluster", clusterEntity.ClusterId))
		return nil
	}

	sharedPool, berr := resourcePoolRepo.Load(ctx, backupSettingInDB.SharedPoolId, false)
	if berr != nil {
		return berr.Chain("failed to load shared pool: %d", backupSettingInDB.SharedPoolId)
	}
	regionEntity, berr := regionRepo.Get(ctx, sharedPool.RegionId)
	if berr != nil {
		return berr.Chain("failed to load region: %d", sharedPool.RegionId)
	}

	skuValue := convertBackupPolicyToSkuValue(backupSettingInDB.BackupPolicy)
	if skuValue == "" {
		return bizerr.New(bizerr.GeneralInternalErrorCode, "failed to convert backup policy to sku value: %s", backupSettingInDB.BackupPolicy)
	}
	mode := "STANDARD_BUNDLE"
	if backupSettingInDB.Mode == schema.BackupModeFlexibleRetention {
		mode = "FLEXIBLE_RETENTION"
	}
	req := &regionalserver.AutoBackupMeteringRequest{
		ClusterID: fmt.Sprintf("%d", backupSettingInDB.ClusterId),
		SkuName:   "backup-storage-v2", SkuValue: skuValue, Mode: mode,
	}
	if mode == "FLEXIBLE_RETENTION" {
		req.RetentionDays = proto.Uint32(uint32(backupSettingInDB.RetentionDays))
		req.DailyBackupTimeUTC = proto.String(backupSettingInDB.DailyBackupTimeUTC)
	}
	berr = regionalCli.UpdateAutoBackupMetering(ctx, regionEntity, req)
	if berr != nil {
		return berr.Chain("failed to update auto backup metetering")
	}
	tilog.Info(ctx, "updated auto backup metetering", zap.Any("req", req))
	return nil
}

func convertBackupPolicyToSkuValue(backupPolicy schema.BackupPolicyName) string {
	switch backupPolicy {
	case schema.BackupPolicyNameTIER1:
		return "T1"
	case schema.BackupPolicyNameTIER2:
		return "T2"
	case schema.BackupPolicyNameTIER3:
		return "T3"
	}
	return ""
}

func (s *serviceImpl) validateUpdateBackupSettingRequest(
	req *globalngv1.UpdateBackupSettingRequest,
	backupSettingInDB *entity.BackupSetting,
	updateSource backupSettingUpdateSource,
) bizerr.BizError {
	if req == nil || req.BackupSetting == nil {
		return bizerr.New(bizerr.ErrCodeParams, "backup_setting is required")
	}

	req = normalizeUpdateBackupSettingRequest(req, backupSettingInDB)
	mode := req.BackupSetting.GetMode()
	isEssentialV2 := backupSettingInDB.ServicePlan == globalcommonv1.ServicePlan_Essential_V2.String()
	if isEssentialV2 && mode != globalngv1.BackupSetting_FLEXIBLE_RETENTION {
		return bizerr.New(bizerr.ErrCodeParams, "essential_v2 clusters only support flexible retention")
	}
	if !isEssentialV2 && mode == globalngv1.BackupSetting_BACKUP_MODE_UNSPECIFIED {
		mode = globalngv1.BackupSetting_STANDARD_BUNDLE
	}
	if req.BackupSetting.BackupPolicy != globalngv1.BackupSetting_BackupPolicy_TIER_UNSPECIFIED &&
		req.BackupSetting.BackupPolicy != globalngv1.BackupSetting_BackupPolicy_TIER1 {
		return bizerr.New(bizerr.ErrCodeParams, "only TIER1 is supported")
	}

	retentionDays := StandardRetentionDays
	dailyTime := DefaultDailyBackupTimeUTC
	minFlexibleRetentionDays := int32(MinFlexibleRetentionDays)
	maxFlexibleRetentionDays := int32(MaxFlexibleRetentionDays)
	if updateSource == backupSettingUpdateSourceIncidentOverride {
		minFlexibleRetentionDays = MinOverrideFlexibleRetentionDays
	} else if isEssentialV2 {
		minFlexibleRetentionDays = EssentialV2MinFlexibleRetentionDays
		maxFlexibleRetentionDays = EssentialV2MaxFlexibleRetentionDays
	}
	switch mode {
	case globalngv1.BackupSetting_STANDARD_BUNDLE:
		if req.BackupSetting.RetentionDays != nil && req.BackupSetting.GetRetentionDays() != StandardRetentionDays {
			return bizerr.New(bizerr.ErrCodeParams, "standard bundle retention_days must be 7")
		}
		if req.BackupSetting.DailyBackupTimeUtc != nil {
			return bizerr.New(bizerr.ErrCodeParams, "daily_backup_time_utc is only supported for flexible retention")
		}
	case globalngv1.BackupSetting_FLEXIBLE_RETENTION:
		if req.BackupSetting.RetentionDays == nil || req.BackupSetting.GetRetentionDays() < minFlexibleRetentionDays ||
			req.BackupSetting.GetRetentionDays() > maxFlexibleRetentionDays {
			return bizerr.New(
				bizerr.ErrCodeParams,
				"flexible retention_days must be between %d and %d",
				minFlexibleRetentionDays,
				maxFlexibleRetentionDays,
			)
		}
		if req.BackupSetting.DailyBackupTimeUtc == nil {
			return bizerr.New(bizerr.ErrCodeParams, "daily_backup_time_utc is required for flexible retention")
		}
		if _, err := ParseDailyBackupTimeUTC(req.BackupSetting.GetDailyBackupTimeUtc()); err != nil {
			return bizerr.New(bizerr.ErrCodeParams, "%s", err.Error())
		}
		retentionDays = int(req.BackupSetting.GetRetentionDays())
		dailyTime = req.BackupSetting.GetDailyBackupTimeUtc()
	default:
		return bizerr.New(bizerr.ErrCodeParams, "invalid backup mode")
	}

	dbMode := backupSettingInDB.Mode
	if dbMode == "" {
		dbMode = schema.BackupModeStandardBundle
	}
	requestedMode := schema.BackupModeStandardBundle
	if mode == globalngv1.BackupSetting_FLEXIBLE_RETENTION {
		requestedMode = schema.BackupModeFlexibleRetention
	}
	noUpdate := dbMode == requestedMode && backupSettingInDB.RetentionDays == retentionDays && backupSettingInDB.DailyBackupTimeUTC == dailyTime
	if noUpdate {
		return bizerr.New(bizerr.ErrCodeParams, "no update for backup setting")
	}

	now := time.Now()
	if updateSource != backupSettingUpdateSourceSystemBackfill &&
		backupSettingInDB.LastScheduleUpdatedAt.Add(6*time.Hour).After(now) {
		// FIXME: add validation backup when we support more backup policies
		// return bizerr.New(bizerr.ErrCodeParams, "only one modification is allowed within 6 hours")
		return nil
	}

	return nil
}

/*
This method will handle following situation
1. When a user creates a new logical cluster.
2. When a user changes backup settings of a logical cluster.
3. Daily check living sharedpool for the situation that deleted logical cluster still have backup data.
*/
func UpdateSharedPoolBackupGCSetting(
	ctx context.Context,
	sharedPoolId uint64,
	backupSettingRepo br.BackupSettingRepo,
	backupPolicyRepo br.BackupPolicyRepo,
	resourcePoolRepo resourcepool.Repo,
	regionRepo region.Repo,
	regionalCli regionalserver.Client,
) bizerr.BizError {
	// get all the living backup settings in the shared pool
	backupSettings, berr := backupSettingRepo.UnscopedQuery(ctx, &br.QueryBackupSettingFilter{
		SharedPoolIds: []uint64{sharedPoolId},
		States:        schema.BackupSettingStatesForClusterHasBackup2Expired,
	}, nil)
	if berr != nil {
		return berr.Chain("failed to query backup settings of the shared pool: %d", sharedPoolId)
	}

	if len(backupSettings) == 0 {
		tilog.Info(ctx, "no backup settings found for the shared pool", zap.Uint64("shared_pool_id", sharedPoolId))
		return nil
	}

	longestPitrRetentionDays, longestDailyBackupRetentionDays, berr := calculateLongestRetentionDaysForSharedPool(
		ctx,
		backupSettings,
		backupPolicyRepo,
	)
	if berr != nil {
		return berr.Chain("failed to calculate longest retention days for shared pool")
	}

	// get the region of the shared pool
	sharedPool, berr := resourcePoolRepo.Load(ctx, sharedPoolId, true)
	if berr != nil {
		return berr.Chain("failed to load shared pool: %d", sharedPoolId)
	}
	regionEntity, berr := regionRepo.Get(ctx, sharedPool.RegionId)
	if berr != nil {
		return berr.Chain("failed to load region: %d", sharedPool.RegionId)
	}

	// get latest gc setting of the shared pool
	gcSetting, berr := regionalCli.GetBackupGcSetting(ctx, regionEntity, &clusterngv1beta.GetBackupGcSettingRequest{
		SharedPoolId: fmt.Sprintf("%d", sharedPoolId),
	})
	if berr != nil {
		return berr.Chain("failed to get backup gc setting")
	}

	// assign new parameter based on the longest retention days of the shared pool
	newGcSetting, isChanged := makeChanges2GcSetting(gcSetting, longestPitrRetentionDays, longestDailyBackupRetentionDays, sharedPool)
	if !isChanged {
		tilog.Info(ctx, "no changes to the gc setting of the shared pool", zap.Uint64("shared_pool_id", sharedPoolId))
		return nil
	}

	tilog.Info(ctx, "update backup gc setting", zap.Any("new_gc_setting", newGcSetting), zap.Any("old_gc_setting", gcSetting))

	// update the gc setting of the shared pool
	_, berr = regionalCli.UpdateBackupGcSetting(ctx, regionEntity, &clusterngv1beta.UpdateBackupGcSettingRequest{
		BackupGcSetting: newGcSetting,
	})
	if berr != nil {
		return berr.Chain("failed to update backup gc setting")
	}
	return nil
}

func makeChanges2GcSetting(
	gcSetting *clusterngv1beta.BackupGcSetting,
	longestPitrRetentionDays,
	longestDailyBackupRetentionDays int,
	sharedPool *entity.ResourcePool,
) (newGcSetting *clusterngv1beta.BackupGcSetting, isChanged bool) {
	newGcSetting = proto.CloneOf(gcSetting)
	if sharedPool.DeleteTime == nil { // only allow update these two filed when sharedpool is living
		// Notice: make it archive faster for now since archive logic is not that stable
		newGcSetting.MinT3DataAgeDays = 3
		// mark sst deleted tag time
		newGcSetting.MinT2DataAgeDays = uint32(longestPitrRetentionDays) + 1
	}
	newGcSetting.MaxT1MetaLifetimeDays = uint32(longestPitrRetentionDays) + 1
	newGcSetting.MaxT1WalLifetimeDays = uint32(longestPitrRetentionDays) + 1
	newGcSetting.MaxT2SstLifetimeDays = 7
	newGcSetting.MaxT3DataLifetimeDays = uint32(longestDailyBackupRetentionDays) + 1

	isChanged = !proto.Equal(gcSetting, newGcSetting)
	if isChanged {
		return newGcSetting, true
	}

	return nil, false
}

func calculateLongestRetentionDaysForSharedPool(
	ctx context.Context,
	backupSettings []*entity.BackupSetting,
	backupPolicyRepo br.BackupPolicyRepo,
) (longestPitrRetentionDays, longestDailyBackupRetentionDays int, berr bizerr.BizError) {
	allHaveModes := true
	for _, backupSetting := range backupSettings {
		if backupSetting.Mode == "" {
			allHaveModes = false
			break
		}
		if backupSetting.Schedule.NonZeroClockRetentionDays > longestPitrRetentionDays {
			longestPitrRetentionDays = backupSetting.Schedule.NonZeroClockRetentionDays
		}
		if backupSetting.Schedule.ZeroClockRetentionDays > longestDailyBackupRetentionDays {
			longestDailyBackupRetentionDays = backupSetting.Schedule.ZeroClockRetentionDays
		}
	}
	if allHaveModes {
		return longestPitrRetentionDays, longestDailyBackupRetentionDays, nil
	}

	backupPoliciesMap, berr := getAllBackupPolicies(ctx, backupPolicyRepo)
	if berr != nil {
		return 0, 0, berr.Chain("failed to get all backup policies")
	}

	// aggregate the backup policies
	backupPolicies := make(map[schema.BackupPolicyName]bool)
	for _, backupSetting := range backupSettings {
		if backupSetting.BackupPolicy == "" {
			continue
		}
		backupPolicies[backupSetting.BackupPolicy] = true
	}
	longestPitrRetentionDays, longestDailyBackupRetentionDays = 0, 0
	// find the longest retention days of of the shared pool
	for backupPolicy := range backupPolicies {
		backupPolicyInDB := backupPoliciesMap[backupPolicy]
		if backupPolicyInDB.Pitr != nil && backupPolicyInDB.Pitr.RetentionDays > longestPitrRetentionDays {
			longestPitrRetentionDays = backupPolicyInDB.Pitr.RetentionDays
		}
		if backupPolicyInDB.DailyBackup != nil && backupPolicyInDB.DailyBackup.RetentionDays > longestDailyBackupRetentionDays {
			longestDailyBackupRetentionDays = backupPolicyInDB.DailyBackup.RetentionDays
		}
	}
	return longestPitrRetentionDays, longestDailyBackupRetentionDays, nil
}

func getAllBackupPolicies(
	ctx context.Context,
	backupPolicyRepo br.BackupPolicyRepo,
) (map[schema.BackupPolicyName]*entity.BackupPolicy, bizerr.BizError) {
	backupPolicies, err := backupPolicyRepo.ListAll(ctx)
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to list backup policies")
	}
	backupPoliciesMap := make(map[schema.BackupPolicyName]*entity.BackupPolicy)
	for _, backupPolicy := range backupPolicies {
		backupPoliciesMap[backupPolicy.Name] = backupPolicy
	}
	return backupPoliciesMap, nil
}

func (s *serviceImpl) updateAutoBackupExpiredTime(
	ctx context.Context,
	backupSettingInDB *entity.BackupSetting,
) bizerr.BizError {
	now := time.Now().UTC()
	backups, berr := s.backupRepo.UnscopedQuery(ctx, &br.QueryBackupsFilter{
		ClusterIds: []uint64{backupSettingInDB.ClusterId},
		TriggerTypes: []schema.BackupTriggerType{
			schema.BackupTriggerTypeAuto,
		},
		ExpirationTimeGt: &now,
		Deleted:          proto.Bool(false),
	}, nil)
	if berr != nil {
		return berr.Chain("failed to query auto backups")
	}
	if len(backups) == 0 {
		return nil
	}
	for _, backup := range backups {
		if !backup.ExpirationTime.After(now) {
			continue
		}
		switch backup.AutoBackupType {
		case schema.AutoBackupTypeDaily:
			backup.ExpirationTime = backup.BackupTime.Add(time.Duration(backupSettingInDB.Schedule.ZeroClockRetentionDays) * 24 * time.Hour)
		case schema.AutoBackupTypeHourly:
			backup.ExpirationTime = backup.BackupTime.Add(time.Duration(backupSettingInDB.Schedule.NonZeroClockRetentionDays) * 24 * time.Hour)
		default:
			if IsDailyBackupSlotForBackupTime(backup.BackupTime, backupSettingInDB.Schedule) {
				backup.ExpirationTime = backup.BackupTime.Add(time.Duration(backupSettingInDB.Schedule.ZeroClockRetentionDays) * 24 * time.Hour)
			} else {
				backup.ExpirationTime = backup.BackupTime.Add(time.Duration(backupSettingInDB.Schedule.NonZeroClockRetentionDays) * 24 * time.Hour)
			}
		}
		berr = s.backupRepo.Save(ctx, backup)
		if berr != nil {
			return berr.Chain("failed to save auto backup: %s", backup.ID)
		}
	}
	return nil
}
