package br

import (
	"fmt"

	dplanemgrv1 "github.com/tidbcloud/cluster-service-ng/api/dataplane-manager/v1"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/mysql/schema"
)

const (
	// rule expire Incomplete Multipart Upload data, should be alaways working
	RuleIdAbortIncompleteMultipartUpload = "Abort Incomplete Multipart Upload"
	// rule expire tiflash data, which should only work after shared pool is deleted
	RuleIdCleanupTiflashData = "Cleanup Tiflash Data"
	// rule expire tiflash data where cluster is living
	RuleIdExpireTiflashData = "Expire Tiflash Data"

	// rules cleanup tici data, which should only work after shared pool is deleted
	RuleIdCleanupTiciData = "Cleanup Tici Data"

	// These rules will handle most of data of auto backup which are time streaming data, which can be expired gradually.
	// But there is a exception, sst data under /cse without deleted tag can not be expired gradually, we can only delete them after all other stream data is deleted.
	// prefix: /cse/backup/
	RuleIdBackupMetaExpirationRule = "Backup Meta Expiration Rule"
	// prefix: /cse/store_backup/
	RuleIdLightweightWALAndRlogExpirationRule = "Lightweight WAL And Rlog Expiration Rule"
	// prefix: /cse/ with tag "deleted" is true
	RuleIdUnusedSSTExpirationRule = "Unused SST Expiration Rule"
	// prefix: /cse/archive/
	RuleIdCleanupArchiveRule = "Cleanup Archive Rule"
	// rule expire packed backup data, packed backup data is data produced during cross cluster restore, will should remove it after restore is completed
	RuleIdPackedBackupExpirationRule = "Packed Backup Expiration Rule"
	// prefix: /cse/
	// this rule is design to remove sst data under /cse without deleted tag, can be only applied when all other streaming auto backup data is deleted.
	RuleIdCleanupAllAutoBackupData = "Cleanup All Auto Backup Data"
)

func MakeLifecycleRules(
	backupGcSetting *entity.BackupGcSetting,
	sharedPool *entity.SharedPool,
) []*dplanemgrv1.LifecycleRule {
	return makeLifecycleRules(backupGcSetting, sharedPool, true)
}

// MakeLifecycleRulesWithoutPauseProtection returns the configured BR lifecycle
// rules even while the pool is RESUMING. It is only for the Resume workflow
// after archive progress has proved it safe to release physical GC protection.
func MakeLifecycleRulesWithoutPauseProtection(
	backupGcSetting *entity.BackupGcSetting,
	sharedPool *entity.SharedPool,
) []*dplanemgrv1.LifecycleRule {
	return makeLifecycleRules(backupGcSetting, sharedPool, false)
}

func makeLifecycleRules(
	backupGcSetting *entity.BackupGcSetting,
	sharedPool *entity.SharedPool,
	applyPauseProtection bool,
) []*dplanemgrv1.LifecycleRule {
	dfsPrefix := sharedPool.GetDfsPrefix()
	if dfsPrefix == "" {
		// avoid delete data because of bug
		panic("dfsPrefix is empty")
	}
	tiflashPrefix := sharedPool.GetTiFlashRootPrefix()
	if tiflashPrefix == "" {
		// avoid delete data because of bug
		panic("tiflashPrefix is empty")
	}

	ticiPrefix := sharedPool.GetTiCIPrefix()
	if ticiPrefix == "" {
		// avoid delete data because of bug
		panic("ticiPrefix is empty")
	}

	// make lifecycle rules for backup gc setting based on status
	newRules := []*dplanemgrv1.LifecycleRule{}
	switch backupGcSetting.Status {
	case schema.BackupGcStatusSPCreating, schema.BackupGcStatusSPRunning:
		// Logical retention may be shorter than the T3 archive delay for
		// internal configurations. Keep product-level retention unchanged, but
		// never let physical T1/T2 expiration overtake archive eligibility.
		backupGcSetting = ArchiveSafeBackupGcSetting(backupGcSetting)
		newRules = append(newRules, rulesBasic(tiflashPrefix)...)
		newRules = append(newRules, rulesExpireSteamingAutoBackup(backupGcSetting, dfsPrefix)...)
	case schema.BackupGcStatusSPDeleting, schema.BackupGcStatusSPDeleted:
		newRules = append(newRules, rulesBasic(tiflashPrefix)...)
		newRules = append(newRules, rulesExpireSteamingAutoBackup(backupGcSetting, dfsPrefix)...)
		newRules = append(newRules, rulesExpireTiflashData(tiflashPrefix)...)
		newRules = append(newRules, rulesCleanupTiciData(ticiPrefix)...)
	case schema.BackupGcStatusAutoBackupExpired:
		newRules = append(newRules, rulesBasic(tiflashPrefix)...)
		newRules = append(newRules, rulesExpireTiflashData(tiflashPrefix)...)
		newRules = append(newRules, rulesExpireAllAutoBackupData(dfsPrefix)...)
		newRules = append(newRules, rulesCleanupTiciData(ticiPrefix)...)
	default:
		panic(fmt.Sprintf("unexpected backup gc setting status: %s", backupGcSetting.Status))
	}
	if applyPauseProtection && sharedPool.PauseResumeState != entity.PauseResumeStateNone {
		disableBRObjectExpirationRules(newRules)
	}
	return newRules
}

// ArchiveSafeBackupGcSetting returns a shallow copy whose enabled archive-input
// expiration values are at least one day longer than the configured T3 archive
// delay. Packed backups are restore artifacts and do not participate in T3
// archive progression. This deliberately does not mutate or persist the
// logical GC setting received from Global.
func ArchiveSafeBackupGcSetting(setting *entity.BackupGcSetting) *entity.BackupGcSetting {
	if setting == nil {
		return nil
	}
	return ArchiveSafeBackupGcSettingForDelay(setting, setting.MinT3DataAgeDays)
}

// ArchiveSafeBackupGcSettingForDelay applies the physical TTL floor for an
// explicit archive delay. Update workflows use the larger of the live and
// desired delays before mutating TiBRGC, keeping the transition safe even if a
// later action is delayed or fails.
func ArchiveSafeBackupGcSettingForDelay(setting *entity.BackupGcSetting, archiveDelayDays uint32) *entity.BackupGcSetting {
	if setting == nil || archiveDelayDays == 0 {
		return setting
	}
	safe := *setting
	minimumTTL := archiveDelayDays + 1
	if minimumTTL == 0 { // uint32 overflow; retain the largest representable value.
		minimumTTL = archiveDelayDays
	}
	safe.MaxT1MetaLifetimeDays = applyExpirationFloor(setting.MaxT1MetaLifetimeDays, minimumTTL)
	safe.MaxT1WalLifetimeDays = applyExpirationFloor(setting.MaxT1WalLifetimeDays, minimumTTL)
	safe.MaxT2SstLifetimeDays = applyExpirationFloor(setting.MaxT2SstLifetimeDays, minimumTTL)
	return &safe
}

func applyExpirationFloor(value *uint32, minimum uint32) *uint32 {
	if value == nil || *value == 0 || *value >= minimum {
		return value
	}
	effective := minimum
	return &effective
}

// disableBRObjectExpirationRules keeps the complete lifecycle configuration in
// place while preventing physical deletion of BR data during a pool pause.
// Logical backup/PITR retention is owned by Global and deliberately does not
// depend on these object-store rules.
//
// The pause state is persisted on SharedPool, so a concurrent GC-setting apply
// cannot accidentally re-enable the rules while the pool is pausing, paused,
// or resuming.
func disableBRObjectExpirationRules(rules []*dplanemgrv1.LifecycleRule) {
	brRuleIDs := map[string]struct{}{
		RuleIdBackupMetaExpirationRule:            {},
		RuleIdLightweightWALAndRlogExpirationRule: {},
		RuleIdUnusedSSTExpirationRule:             {},
		RuleIdCleanupArchiveRule:                  {},
		RuleIdPackedBackupExpirationRule:          {},
		RuleIdCleanupAllAutoBackupData:            {},
	}
	for _, rule := range rules {
		if _, ok := brRuleIDs[rule.GetId()]; ok {
			rule.Enabled = false
		}
	}
}

func rulesBasic(tiflashPrefix string) []*dplanemgrv1.LifecycleRule {
	rules := []*dplanemgrv1.LifecycleRule{
		{
			Id:                             RuleIdAbortIncompleteMultipartUpload,
			Enabled:                        true,
			IncompleteUploadExpirationDays: 1,
		},
		{
			Id:           RuleIdExpireTiflashData,
			Enabled:      true,
			PrefixFilter: fmt.Sprintf("%s/", tiflashPrefix),
			TagsFilter: []*dplanemgrv1.LifecycleRule_Tag{
				{
					Key:   "tiflash_deleted",
					Value: "true",
				},
			},
			ExpirationDays: 1,
		},
	}
	return rules
}

func rulesExpireTiflashData(tiflashPrefix string) []*dplanemgrv1.LifecycleRule {
	return []*dplanemgrv1.LifecycleRule{
		{
			Id:             RuleIdCleanupTiflashData,
			Enabled:        true,
			PrefixFilter:   fmt.Sprintf("%s/", tiflashPrefix),
			ExpirationDays: 1,
		},
	}
}

func rulesCleanupTiciData(ticiPrefix string) []*dplanemgrv1.LifecycleRule {
	return []*dplanemgrv1.LifecycleRule{
		{
			Id:             RuleIdCleanupTiciData,
			Enabled:        true,
			PrefixFilter:   fmt.Sprintf("%s/", ticiPrefix),
			ExpirationDays: 1,
		},
	}
}

func rulesExpireSteamingAutoBackup(backupGcSetting *entity.BackupGcSetting, dfsPrefix string) []*dplanemgrv1.LifecycleRule {
	rules := []*dplanemgrv1.LifecycleRule{}
	if backupGcSetting.MaxT1MetaLifetimeDays != nil && *backupGcSetting.MaxT1MetaLifetimeDays != 0 {
		rules = append(rules, &dplanemgrv1.LifecycleRule{
			Id:             RuleIdBackupMetaExpirationRule,
			Enabled:        true,
			PrefixFilter:   fmt.Sprintf("%s/backup/", dfsPrefix),
			ExpirationDays: *backupGcSetting.MaxT1MetaLifetimeDays,
		})
	}

	if backupGcSetting.MaxT1WalLifetimeDays != nil && *backupGcSetting.MaxT1WalLifetimeDays != 0 {
		rules = append(rules, &dplanemgrv1.LifecycleRule{
			Id:             RuleIdLightweightWALAndRlogExpirationRule,
			Enabled:        true,
			PrefixFilter:   fmt.Sprintf("%s/store_backup/", dfsPrefix),
			ExpirationDays: *backupGcSetting.MaxT1WalLifetimeDays,
		})
	}

	if backupGcSetting.MaxT2SstLifetimeDays != nil && *backupGcSetting.MaxT2SstLifetimeDays != 0 {
		rules = append(rules, &dplanemgrv1.LifecycleRule{
			Id:           RuleIdUnusedSSTExpirationRule,
			Enabled:      true,
			PrefixFilter: fmt.Sprintf("%s/", dfsPrefix),
			TagsFilter: []*dplanemgrv1.LifecycleRule_Tag{
				{
					Key:   "deleted",
					Value: "true",
				},
			},
			ExpirationDays: *backupGcSetting.MaxT2SstLifetimeDays,
		})
	}

	if backupGcSetting.MaxT3DataLifetimeDays != nil && *backupGcSetting.MaxT3DataLifetimeDays != 0 {
		rules = append(rules, &dplanemgrv1.LifecycleRule{
			Id:             RuleIdCleanupArchiveRule,
			Enabled:        true,
			PrefixFilter:   fmt.Sprintf("%s/archive", dfsPrefix),
			ExpirationDays: *backupGcSetting.MaxT3DataLifetimeDays,
		})
	}

	if backupGcSetting.MaxT1PackedLifetimeDays != nil && *backupGcSetting.MaxT1PackedLifetimeDays != 0 {
		rules = append(rules, &dplanemgrv1.LifecycleRule{
			Id:             RuleIdPackedBackupExpirationRule,
			Enabled:        true,
			PrefixFilter:   fmt.Sprintf("%s/packed_backup/", dfsPrefix),
			ExpirationDays: *backupGcSetting.MaxT1PackedLifetimeDays,
		})
	}

	return rules
}

func rulesExpireAllAutoBackupData(dfsPrefix string) []*dplanemgrv1.LifecycleRule {
	return []*dplanemgrv1.LifecycleRule{
		{
			Id:             RuleIdCleanupAllAutoBackupData,
			Enabled:        true,
			PrefixFilter:   fmt.Sprintf("%s/", dfsPrefix),
			ExpirationDays: 1,
		},
	}
}
