package br

import (
	"context"
	"fmt"
	"time"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	"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/infra/repoimpl/schema"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func (w *worker) ExpireBackups(ctx context.Context) error {
	if !w.cfg.ExpireConfig.Enable {
		tilog.Info(ctx, "expire backups is disabled, skip")
		return nil
	}

	startTime := time.Now().UTC()
	ExpireBackupsWorkerRunsTotal.Inc()

	defer func() {
		duration := time.Since(startTime).Seconds()
		ExpireBackupsWorkerDuration.Observe(duration)
	}()

	tilog.Info(ctx, "expire backups")
	backupSettings, err := w.backupSettingRepo.UnscopedQuery(ctx, &br.QueryBackupSettingFilter{
		ExcludeStates: schema.BackupSettingStatesClusterDeletedNoAutoBackups,
	}, nil)
	if err != nil {
		ExpireBackupsErrorTotal.WithLabelValues(LabelValue_NA, LabelValue_NA, ExpireBackupsErrorType_QueryBackupSettings).Inc()
		tilog.Error(ctx, "failed to query non-cleanup backup settings", zap.Error(err))
		return fmt.Errorf("failed to query non-cleanup backup settings: %w", err)
	}

	tilog.Info(ctx, "found uncleanup backup settings", zap.Any("number", len(backupSettings)))

	for _, backupSetting := range backupSettings {
		now := time.Now().UTC()

		tilog.Info(ctx, "checking backup setting", zap.Any("backupSetting", backupSetting))
		if hasCleanRecently(backupSetting, &now) {
			tilog.Info(ctx, "backup setting has clean recently", zap.Any("backupSetting", backupSetting))
			continue
		}

		tilog.Info(ctx, "try to deleting expired backups", zap.Any("backupSetting", backupSetting))

		// Retention cleanup intentionally continues during Pause preparation.
		// The filter removes automatic backups only, so it cannot delete the
		// operation-owned manual backups required by the Pause workflow.
		// TODO: can check the affected backups, if 0, don't need to update backup setting's clean status
		err := w.backupRepo.Delete(ctx, &br.DeleteBackupsFilter{
			ClusterIds:        []uint64{backupSetting.ClusterId},
			TriggerTypes:      []schema.BackupTriggerType{schema.BackupTriggerTypeAuto},
			ExpirationTimeLte: &now,
		})
		if err != nil {
			tilog.Error(
				ctx,
				"failed to delete expired backups",
				zap.Error(err),
				zap.Uint64("cluster_id", backupSetting.ClusterId),
			)
			ExpireBackupsErrorTotal.WithLabelValues(
				fmt.Sprintf("%d", backupSetting.SharedPoolId),
				fmt.Sprintf("%d", backupSetting.ClusterId),
				ExpireBackupsErrorType_DeleteBackups,
			).Inc()
			continue
		}

		tilog.Info(ctx, "deleting expired backups success", zap.Any("backupSetting", backupSetting))

		backupSetting.LastCleanAt = &now
		err = w.backupSettingRepo.Save(ctx, backupSetting)
		if err != nil && err.ErrCode() != bizerr.ErrCodeOptimisticLockConflict {
			tilog.Error(ctx, "failed to set last clean at",
				zap.Error(err),
				zap.Uint64("cluster_id", backupSetting.ClusterId),
				zap.Time("last_clean_at", *backupSetting.LastCleanAt),
			)
			ExpireBackupsErrorTotal.WithLabelValues(
				fmt.Sprintf("%d", backupSetting.SharedPoolId),
				fmt.Sprintf("%d", backupSetting.ClusterId),
				ExpireBackupsErrorType_SetLastCleanAt,
			).Inc()
			continue
		}
		tilog.Info(ctx, "set last clean at success", zap.Any("backupSetting", backupSetting))

		err = w.backupSettingRepo.TransitStateAfterBackupDeletedIfNeeded(ctx, backupSetting.ClusterId)
		if err != nil {
			tilog.Error(
				ctx,
				"failed to transit state after backup deleted if needed",
				zap.Error(err),
				zap.Any("backupSetting", backupSetting),
			)
			ExpireBackupsErrorTotal.WithLabelValues(
				fmt.Sprintf("%d", backupSetting.SharedPoolId),
				fmt.Sprintf("%d", backupSetting.ClusterId),
				ExpireBackupsErrorType_TransitState,
			).Inc()
			continue
		}
		tilog.Info(ctx, "transit state after backup deleted if needed success", zap.Any("backupSetting", backupSetting))
	}
	return nil
}

func hasCleanRecently(backupSetting *entity.BackupSetting, now *time.Time) bool {
	if backupSetting.LastCleanAt == nil {
		return false
	}
	switch backupSetting.Schedule.Type {
	case "per-day":
		return backupSetting.LastCleanAt.After(now.Add(-1 * time.Hour * 24))
	case "per-hour":
		return backupSetting.LastCleanAt.After(now.Add(-1 * time.Hour))
	case "per-minute": // lower clean frequency for per-minute schedule
		return backupSetting.LastCleanAt.After(now.Add(-1 * time.Hour))
	default:
		panic(fmt.Sprintf("invalid schedule type: %s", backupSetting.Schedule.Type))
	}
}
