package br

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

const DeleteManualBackupDataActionName = "delete-manual-backup-data"

type deleteManualBackupDataAction struct {
	manualBackupRepo repo.ManualBackupRepo
	sharedPoolRepo   repo.SharedPoolRepo
	osRepo           repo.SharedPoolObjectStoreRepo
	bucketSvc        dplanemgrv1.BucketServiceClient
}

func NewDeleteManualBackupDataAction(
	manualBackupRepo repo.ManualBackupRepo,
	sharedPoolRepo repo.SharedPoolRepo,
	osRepo repo.SharedPoolObjectStoreRepo,
	bucketSvc dplanemgrv1.BucketServiceClient,
) actions.Action {
	return actions.CreateActionFromAsync(&deleteManualBackupDataAction{
		manualBackupRepo: manualBackupRepo,
		sharedPoolRepo:   sharedPoolRepo,
		osRepo:           osRepo,
		bucketSvc:        bucketSvc,
	}, time.Minute, time.Minute*10)
}

func (a *deleteManualBackupDataAction) Name() string {
	return DeleteManualBackupDataActionName
}

func (a *deleteManualBackupDataAction) Start(args actions.ActionArgs) error {
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, fmt.Sprintf("start %s action...", a.Name()))

	backupID, ok := GetManualBackupID(args)
	if !ok {
		return fmt.Errorf("manual backup id is not found")
	}

	clusterID, ok := GetTiDBClusterID(args)
	if !ok {
		return fmt.Errorf("tidb cluster id is not found")
	}

	manualBackup, err := a.manualBackupRepo.Load(ctx, clusterID, backupID, true)
	if err != nil {
		tilog.Error(ctx, "failed to load manual backup", zap.Error(err), zap.String("backupId", backupID), zap.String("clusterId", clusterID))
		return fmt.Errorf("failed to load manual backup: %w", err)
	}

	if manualBackup.CopyPrefix == "" {
		tilog.Info(ctx, "copy prefix is empty, backup data was never copied, skip deletion", zap.String("backupId", backupID), zap.String("clusterId", clusterID))
		return nil
	}

	err = AddActionForManualBackup(ctx, a.manualBackupRepo, manualBackup, schema.ManualBackupActionDeleteData)
	if err != nil {
		tilog.Error(ctx, "failed to add action to manual backup", zap.Error(err), zap.String("backupId", backupID), zap.String("clusterId", clusterID))
		return fmt.Errorf("failed to add action to manual backup: %w", err)
	}

	sharedPool, err := a.sharedPoolRepo.Load(ctx, manualBackup.SharedPoolID, true)
	if err != nil {
		tilog.Error(ctx, "failed to load shared pool", zap.Error(err), zap.String("sharedPoolId", manualBackup.SharedPoolID))
		return fmt.Errorf("failed to load shared pool: %w", err)
	}

	sharedPoolObjectStore, err := a.osRepo.Load(ctx, manualBackup.SharedPoolID, schema.SharedPoolObjectStoreTypeData, true)
	if err != nil {
		tilog.Error(ctx, "failed to load sharedPoolObjectStore", zap.Error(err), zap.String("sharedPoolId", manualBackup.SharedPoolID))
		return fmt.Errorf("failed to load sharedPoolObjectStore: %w", err)
	}

	bucketId := sharedPoolObjectStore.StoreBucket
	if bucketId == "" {
		bucketId = entity.GetDataBucketId(manualBackup.SharedPoolID, sharedPoolObjectStore.StoreRegion)
		tilog.Warn(ctx, "bucket id is empty in sharedPoolObjectStore, use generated bucket id", zap.String("sharedPoolId", manualBackup.SharedPoolID), zap.String("bucketId", bucketId))
	}

	_, err = a.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, skip applying lifecycle rules for manual backup deletion", zap.String("bucketId", bucketId))
			return nil
		}
		tilog.Error(ctx, "failed to get bucket before applying lifecycle rules", zap.String("bucketId", bucketId), zap.Error(err))
		return fmt.Errorf("failed to get bucket before applying lifecycle rules: %w", err)
	}

	ruleId := fmt.Sprintf("manual-backup-del-%s-%s", clusterID, strings.ToLower(backupID))
	addReq := &dplanemgrv1.AddManualBackupLifecycleRulesRequest{
		DataplaneId: sharedPool.GetDataplaneName(),
		BucketId:    bucketId,
		Rules: []*dplanemgrv1.LifecycleRule{
			{
				Id:             ruleId,
				Enabled:        true,
				PrefixFilter:   manualBackup.CopyPrefix,
				ExpirationDays: 1,
			},
		},
	}
	_, err = a.bucketSvc.AddManualBackupLifecycleRules(ctx, addReq)
	if err != nil {
		tilog.Error(ctx, "failed to add manual backup lifecycle rules", zap.Any("request", addReq), zap.Error(err))
		return fmt.Errorf("failed to add manual backup lifecycle rules: %w", err)
	}
	tilog.Info(ctx, "successfully added manual backup lifecycle rules", zap.String("ruleId", ruleId), zap.String("prefix", manualBackup.CopyPrefix))

	now := time.Now()
	manualBackup.LifecycleRuleAdded = &now
	if err := a.manualBackupRepo.Save(ctx, manualBackup); err != nil {
		tilog.Error(ctx, "failed to save lifecycle rule added timestamp", zap.Error(err))
		return fmt.Errorf("failed to save lifecycle rule added timestamp: %w", err)
	}

	return nil
}

func (a *deleteManualBackupDataAction) CheckFinish(args actions.ActionArgs) (bool, error) {
	ctx := utils.GenerateContext(args)

	backupID, ok := GetManualBackupID(args)
	if !ok {
		return false, fmt.Errorf("manual backup id is not found")
	}

	clusterID, ok := GetTiDBClusterID(args)
	if !ok {
		return false, fmt.Errorf("tidb cluster id is not found")
	}

	manualBackup, err := a.manualBackupRepo.Load(ctx, clusterID, backupID, true)
	if err != nil {
		tilog.Error(ctx, "failed to load manual backup", zap.Error(err), zap.String("backupId", backupID), zap.String("clusterId", clusterID))
		return false, fmt.Errorf("failed to load manual backup: %w", err)
	}

	if manualBackup.CopyPrefix == "" {
		tilog.Info(ctx, "copy prefix is empty, skip checking lifecycle rules", zap.String("backupId", backupID), zap.String("clusterId", clusterID))
		return true, nil
	}

	sharedPool, err := a.sharedPoolRepo.Load(ctx, manualBackup.SharedPoolID, true)
	if err != nil {
		tilog.Error(ctx, "failed to load shared pool", zap.Error(err), zap.String("sharedPoolId", manualBackup.SharedPoolID))
		return false, fmt.Errorf("failed to load shared pool: %w", err)
	}

	sharedPoolObjectStore, err := a.osRepo.Load(ctx, manualBackup.SharedPoolID, schema.SharedPoolObjectStoreTypeData, true)
	if err != nil {
		tilog.Error(ctx, "failed to load sharedPoolObjectStore", zap.Error(err), zap.String("sharedPoolId", manualBackup.SharedPoolID))
		return false, fmt.Errorf("failed to load sharedPoolObjectStore: %w", err)
	}

	bucketId := sharedPoolObjectStore.StoreBucket
	if bucketId == "" {
		bucketId = entity.GetDataBucketId(manualBackup.SharedPoolID, sharedPoolObjectStore.StoreRegion)
		tilog.Warn(ctx, "bucket id is empty in sharedPoolObjectStore, use generated bucket id", zap.String("sharedPoolId", manualBackup.SharedPoolID), zap.String("bucketId", bucketId))
	}

	_, err = a.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, skip checking lifecycle rules", zap.String("bucketId", bucketId))
			return true, nil
		}
		tilog.Error(ctx, "failed to get bucket before checking lifecycle rules", zap.String("bucketId", bucketId), zap.Error(err))
		return false, fmt.Errorf("failed to get bucket before checking lifecycle rules: %w", err)
	}

	req := &dplanemgrv1.GetLifecycleRulesRequest{
		DataplaneId: sharedPool.GetDataplaneName(),
		BucketId:    bucketId,
	}
	resp, err := a.bucketSvc.GetLifecycleRules(ctx, req)
	if err != nil {
		tilog.Error(ctx, "failed to get lifecycle rules", zap.Any("request", req), zap.Error(err))
		return false, fmt.Errorf("failed to get lifecycle rules: %w", err)
	}
	if resp.Status != dplanemgrv1.GetLifecycleRulesResponse_Synced {
		tilog.Info(ctx, "lifecycle rules not synced yet for manual backup deletion", zap.Any("response", resp))
		return false, nil
	}
	tilog.Info(ctx, "lifecycle rules synced for manual backup deletion", zap.Any("response", resp))
	return true, nil
}
