package cron

import (
	"context"
	"fmt"
	"strings"
	"time"

	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/domain/server"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/mysql/schema"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

const ManualBackupLifecycleRuleRecycleAge = 4 * 24 * time.Hour

type CleanManualBackupDeletionRulesJob struct {
	expression       string
	manualBackupRepo repo.ManualBackupRepo
	sharedPoolRepo   repo.SharedPoolRepo
	osRepo           repo.SharedPoolObjectStoreRepo
	bucketSvc        dplanemgrv1.BucketServiceClient
}

func NewCleanManualBackupDeletionRulesJob(
	expression string,
	manualBackupRepo repo.ManualBackupRepo,
	sharedPoolRepo repo.SharedPoolRepo,
	osRepo repo.SharedPoolObjectStoreRepo,
	bucketSvc dplanemgrv1.BucketServiceClient,
) *CleanManualBackupDeletionRulesJob {
	return &CleanManualBackupDeletionRulesJob{
		expression:       expression,
		manualBackupRepo: manualBackupRepo,
		sharedPoolRepo:   sharedPoolRepo,
		osRepo:           osRepo,
		bucketSvc:        bucketSvc,
	}
}

func (j *CleanManualBackupDeletionRulesJob) Name() string {
	return "CleanManualBackupDeletionRulesJob"
}

func (j *CleanManualBackupDeletionRulesJob) CronExpression() string {
	return j.expression
}

func (j *CleanManualBackupDeletionRulesJob) Execute(ctx context.Context) error {
	tilog.Info(ctx, "start CleanManualBackupDeletionRulesJob execution ...")

	pendingRecycles, err := j.manualBackupRepo.ListPendingLifecycleRuleRecycles(ctx, ManualBackupLifecycleRuleRecycleAge)
	if err != nil {
		return fmt.Errorf("failed to list pending lifecycle rule recycles: %w", err)
	}
	if len(pendingRecycles) == 0 {
		tilog.Info(ctx, "no manual backup lifecycle rules to recycle")
		return nil
	}

	grouped := groupBySharedPool(pendingRecycles)

	for sharedPoolID, backups := range grouped {
		j.processSharedPool(ctx, sharedPoolID, backups)
	}
	return nil
}

func (j *CleanManualBackupDeletionRulesJob) processSharedPool(ctx context.Context, sharedPoolID string, backups []*entity.ManualBackup) {
	sharedPool, err := j.sharedPoolRepo.Load(ctx, sharedPoolID, true)
	if err != nil {
		tilog.Error(ctx, "failed to load shared pool", zap.Error(err), zap.String("sharedPoolId", sharedPoolID))
		return
	}
	// Active paused/transition pools defer cleanup; LifecycleRuleDeleted stays nil so a later cron retries it.
	// Deleted pools cannot resume, so their already-admitted backup teardown must remain drainable.
	if sharedPool.DeletedAt == nil && !server.SharedPoolAllowsOrdinaryMutation(sharedPool) {
		tilog.Info(ctx, "skip manual backup lifecycle rule cleanup while shared pool mutation is blocked",
			zap.String("sharedPoolId", sharedPoolID),
			zap.String("pauseResumeState", string(sharedPool.PauseResumeState)))
		return
	}

	osInfo, err := j.osRepo.Load(ctx, sharedPoolID, schema.SharedPoolObjectStoreTypeData, true)
	if err != nil {
		tilog.Error(ctx, "failed to load object store info", zap.Error(err), zap.String("sharedPoolId", sharedPoolID))
		return
	}

	bucketId := osInfo.StoreBucket
	if bucketId == "" {
		bucketId = entity.GetDataBucketId(sharedPoolID, osInfo.StoreRegion)
	}

	_, err = j.bucketSvc.GetBucket(ctx, &dplanemgrv1.GetBucketRequest{
		DataplaneId: sharedPool.GetDataplaneName(),
		BucketId:    bucketId,
	})
	if err != nil {
		if status.Code(err) == codes.NotFound {
			tilog.Info(ctx, "bucket not found, marking lifecycle rules as deleted", zap.String("bucketId", bucketId))
		} else {
			tilog.Error(ctx, "failed to get bucket", zap.Error(err), zap.String("sharedPoolId", sharedPoolID))
			return
		}
	} else {
		var ruleIDs []string
		for _, backup := range backups {
			ruleIDs = append(ruleIDs, fmt.Sprintf("manual-backup-del-%s-%s",
				backup.ClusterID, strings.ToLower(backup.BackupID)))
		}

		_, err = j.bucketSvc.DeleteManualBackupLifecycleRules(ctx, &dplanemgrv1.DeleteManualBackupLifecycleRulesRequest{
			DataplaneId: sharedPool.GetDataplaneName(),
			BucketId:    bucketId,
			RuleIds:     ruleIDs,
		})
		if err != nil {
			tilog.Error(ctx, "failed to delete manual backup lifecycle rules", zap.Error(err), zap.String("sharedPoolId", sharedPoolID))
			return
		}
		tilog.Info(ctx, "deleted manual backup lifecycle rules", zap.String("sharedPoolId", sharedPoolID), zap.Strings("ruleIds", ruleIDs))
	}

	now := time.Now()
	for _, backup := range backups {
		backup.LifecycleRuleDeleted = &now
		if err := j.manualBackupRepo.Save(ctx, backup); err != nil {
			tilog.Error(ctx, "failed to mark lifecycle rule deleted", zap.Error(err),
				zap.String("clusterId", backup.ClusterID), zap.String("backupId", backup.BackupID))
			continue
		}
	}

	tilog.Info(ctx, "recycled manual backup deletion rules",
		zap.String("sharedPoolId", sharedPoolID), zap.Int("count", len(backups)))
}

func groupBySharedPool(backups []*entity.ManualBackup) map[string][]*entity.ManualBackup {
	grouped := make(map[string][]*entity.ManualBackup)
	for _, backup := range backups {
		grouped[backup.SharedPoolID] = append(grouped[backup.SharedPoolID], backup)
	}
	return grouped
}
