package cron

import (
	"context"
	"fmt"
	"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/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"
)

type CleanDataBucketJob struct {
	expression string

	gcRepo                    repo.BackupGcSettingRepo
	manualBackupRepo          repo.ManualBackupRepo
	sharedPoolObjectStoreRepo repo.SharedPoolObjectStoreRepo
	sharedPoolRepo            repo.SharedPoolRepo
	bucketSvc                 dplanemgrv1.BucketServiceClient
}

func NewCleanDataBucketJob(
	expression string,
	gcRepo repo.BackupGcSettingRepo,
	manualBackupRepo repo.ManualBackupRepo,
	sharedPoolObjectStoreRepo repo.SharedPoolObjectStoreRepo,
	sharedPoolRepo repo.SharedPoolRepo,
	bucketSvc dplanemgrv1.BucketServiceClient,
) *CleanDataBucketJob {
	return &CleanDataBucketJob{
		expression:                expression,
		gcRepo:                    gcRepo,
		manualBackupRepo:          manualBackupRepo,
		sharedPoolObjectStoreRepo: sharedPoolObjectStoreRepo,
		sharedPoolRepo:            sharedPoolRepo,
		bucketSvc:                 bucketSvc,
	}
}

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

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

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

	// get all possible to clean backup gc settings
	gcSettings, err := j.gcRepo.List(ctx, &repo.BackupGcSettingFilter{
		IncludeStatuses: []string{schema.BackupGcStatusAutoBackupExpired},
	})
	if err != nil {
		tilog.Error(ctx, "failed to list backup gc settings", zap.Error(err))
		return fmt.Errorf("failed to list backup gc settings: %w", err)
	}
	if len(gcSettings) == 0 {
		tilog.Info(ctx, "no backup gc settings to clean")
		return nil
	}

	// try best to get shared pool ids of to delete buckets, log error if any
	sharedPoolIdsOfToDeleteBuckets := j.getToDeleteSharedPoolIds(ctx, gcSettings)
	tilog.Info(ctx, "shared pool ids of to delete buckets",
		zap.Int("count", len(sharedPoolIdsOfToDeleteBuckets)),
		zap.Strings("shared_pool_ids", sharedPoolIdsOfToDeleteBuckets))

	// try best to delete the buckets, log error if any
	j.doDelete(ctx, sharedPoolIdsOfToDeleteBuckets)
	tilog.Info(ctx, "clean data bucket job execution finished")

	return nil
}

func (j *CleanDataBucketJob) getToDeleteSharedPoolIds(ctx context.Context, gcSettings []*entity.BackupGcSetting) []string {
	var sharedPoolIdsOfToDeleteBuckets []string
	for _, gcSetting := range gcSettings {
		// check if auto backup data is cleaned up
		maxRetentionDays := gcSetting.GetMaxAutoBackupDataRetentionDays()
		dataExpiredTime := gcSetting.SharedPoolDeletedAt.Add(time.Duration(maxRetentionDays) * 24 * time.Hour)
		dataSafelyCleanupTime := dataExpiredTime.Add(4 * 24 * time.Hour)
		notSureIfCleanup := dataSafelyCleanupTime.After(time.Now())
		if notSureIfCleanup {
			tilog.Info(ctx, "not sure if auto backup data is cleaned up",
				zap.String("shared_pool_id", gcSetting.SharedPoolID),
				zap.Duration("max_retention_days", time.Duration(maxRetentionDays)*24*time.Hour),
				zap.Time("shared_pool_deleted_at", *gcSetting.SharedPoolDeletedAt),
				zap.Time("now", time.Now()),
			)
			continue
		}
		hasManualBackupInDB := gcSetting.HasManualBackup != nil && *gcSetting.HasManualBackup
		if j.manualBackupRepo != nil {
			exists, err := j.manualBackupRepo.ExistsUndeletedBySharedPool(ctx, gcSetting.SharedPoolID)
			if err != nil {
				tilog.Error(ctx, "failed to query manual backups before bucket deletion, skip for safety",
					zap.String("shared_pool_id", gcSetting.SharedPoolID),
					zap.Error(err))
				continue
			}
			hasManualBackupInDB = exists

			if j.gcRepo != nil && (gcSetting.HasManualBackup == nil || *gcSetting.HasManualBackup != exists) {
				gcSetting.HasManualBackup = &exists
				if err := j.gcRepo.Save(ctx, gcSetting); err != nil {
					tilog.Warn(ctx, "failed to self-heal has_manual_backup flag",
						zap.String("shared_pool_id", gcSetting.SharedPoolID),
						zap.Bool("expected", exists),
						zap.Error(err))
				}
			}
		}

		if hasManualBackupInDB {
			tilog.Info(ctx, "manual backup still exists, skip deleting bucket", zap.String("shared_pool_id", gcSetting.SharedPoolID))
			continue
		}
		sharedPoolIdsOfToDeleteBuckets = append(sharedPoolIdsOfToDeleteBuckets, gcSetting.SharedPoolID)
	}

	return sharedPoolIdsOfToDeleteBuckets
}

func (j *CleanDataBucketJob) doDelete(ctx context.Context, sharedPoolIds []string) {
	for _, sharedPoolId := range sharedPoolIds {
		bucketInfo, err := j.sharedPoolObjectStoreRepo.Load(ctx, sharedPoolId, schema.SharedPoolObjectStoreTypeData, true)
		if err != nil {
			tilog.Error(ctx, "failed to load data bucket info", zap.Error(err), zap.String("shared_pool_id", sharedPoolId))
			continue
		}

		sharedPool, err := j.sharedPoolRepo.Load(ctx, sharedPoolId, true)
		if err != nil {
			tilog.Error(ctx, "failed to load shared pool", zap.Error(err), zap.String("shared_pool_id", sharedPoolId))
			continue
		}

		_, err = j.bucketSvc.GetBucket(ctx, &dplanemgrv1.GetBucketRequest{
			DataplaneId: sharedPool.GetDataplaneName(),
			BucketId:    bucketInfo.StoreBucket,
		})
		if err != nil {
			if status.Code(err) == codes.NotFound {
				tilog.Info(ctx, "bucket not found, skip delete", zap.String("bucketId", bucketInfo.StoreBucket))
			} else {
				tilog.Error(ctx, "failed to get bucket info from dataplane manager", zap.Error(err), zap.String("shared_pool_id", sharedPoolId))
				continue
			}
		} else {
			// delete the bucket in dataplane manager side
			_, err = j.bucketSvc.DeleteBucket(ctx, &dplanemgrv1.DeleteBucketRequest{
				DataplaneId: sharedPool.GetDataplaneName(),
				BucketId:    bucketInfo.StoreBucket,
			})
			if err != nil {
				tilog.Error(ctx, "failed to delete bucket in dataplane manager", zap.Error(err), zap.String("shared_pool_id", sharedPoolId))
				continue
			}
		}

		if err := j.sharedPoolObjectStoreRepo.Delete(ctx, sharedPoolId, schema.SharedPoolObjectStoreTypeData); err != nil {
			tilog.Error(ctx, "failed to delete data bucket meta in database", zap.Error(err), zap.String("shared_pool_id", sharedPoolId))
			continue
		}

		// delete backup gc setting
		if err := j.gcRepo.Delete(ctx, sharedPoolId); err != nil {
			tilog.Error(ctx, "failed to delete backup gc setting meta in database", zap.Error(err), zap.String("shared_pool_id", sharedPoolId))
			continue
		}
	}
}
