package br

import (
	"context"
	"fmt"
	"strconv"
	"time"

	"github.com/aws/smithy-go/ptr"
	clusterngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	eventdata "github.com/tidbcloud/eventbus-datastruct/event"

	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"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/domain/service/displayevent"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/infra/repoimpl/schema"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/pagination"
)

// DeleteBackups implements Service.
func (s *serviceImpl) DeleteBackup(ctx context.Context, backup *globalngv1.Backup) bizerr.BizError {
	ctx, span := tracer.Start(ctx, "Domain.DeleteBackups")
	defer span.End()

	backupId, err := strconv.ParseUint(backup.GetId(), 10, 64)
	if err != nil {
		return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid backup id")
	}

	clusterId := uint64(0)
	if backup.GetClusterId() != "" {
		var err error
		clusterId, err = strconv.ParseUint(backup.GetClusterId(), 10, 64)
		if err != nil {
			return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "ConvertClusterId")
		}
	}

	organizationId := uint64(0)
	if backup.GetOrganizationId() != "" {
		var err error
		organizationId, err = strconv.ParseUint(backup.GetOrganizationId(), 10, 64)
		if err != nil {
			return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "ConvertOrganizationId")
		}
	}

	if clusterId == 0 && organizationId == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "cluster ID and organization ID cannot be empty at the same time")
	}

	filter := &br.DeleteBackupsFilter{}
	if clusterId != 0 {
		filter.ClusterIds = append(filter.ClusterIds, clusterId)
	}
	if organizationId != 0 {
		filter.OrganizationIds = append(filter.OrganizationIds, organizationId)
	}
	filter.Ids = append(filter.Ids, backupId)

	err = s.transaction.Transaction(ctx, func(ctx context.Context) error {
		backups, berr := s.backupRepo.UnscopedQuery(ctx, &br.QueryBackupsFilter{
			Ids:     []uint64{backupId},
			Deleted: ptr.Bool(false),
		}, nil)
		if berr != nil {
			return berr.Chain("UnscopedQueryBackups").OverwriteCode(bizerr.ErrCodeDB)
		}

		if len(backups) == 0 {
			return bizerr.New(bizerr.ErrCodeParams, "backup not found")
		}

		berr = s.validateBackupDeleteProtection(ctx, backups[0], time.Now().UTC())
		if berr != nil {
			return berr
		}

		if backups[0].TriggerType == schema.BackupTriggerTypeManual {
			cluster, berr := s.clusterRepo.Get(ctx, backups[0].ClusterId, true)
			if berr != nil {
				return berr.Chain("failed to get cluster for manual backup deletion")
			}

			region, berr := s.regionRepo.Get(ctx, cluster.RegionId)
			if berr != nil {
				return berr.Chain("failed to get region for manual backup deletion")
			}

			berr = s.lockResourcePoolForManualBackupDeletion(ctx, backups[0].SharedPoolId)
			if berr != nil {
				return berr
			}

			deleteReq := &clusterngv1beta.DeleteManualBackupRequest{
				ClusterId: fmt.Sprintf("%d", backups[0].ClusterId),
				BackupId:  fmt.Sprintf("%d", backups[0].ID),
			}

			berr = s.regionalCli.DeleteManualBackup(ctx, region, deleteReq)
			if berr != nil {
				return berr.Chain("DeleteManualBackup").OverwriteCode(bizerr.ErrCodeDB)
			}
		}

		berr = s.backupRepo.Delete(ctx, filter)
		if berr != nil {
			return berr.Chain("DeleteBackup").OverwriteCode(bizerr.ErrCodeDB)
		}

		berr = s.backupSettingRepo.TransitStateAfterBackupDeletedIfNeeded(ctx, backups[0].ClusterId)
		if berr != nil {
			return berr.Chain("TransitStateAfterBackupDeletedIfNeeded").OverwriteCode(bizerr.ErrCodeDB)
		}

		displayEv := newBackupDisplayEvent(
			ctx,
			backups[0],
			displayevent.DeleteBackup,
			eventdata.EventStatusSucceeded,
			"Backup deleted successfully.",
		)
		displayevent.Write(ctx, s.eventCommitter, displayEv)

		return nil
	})

	if err != nil {
		return bizerr.WrapError(err, "failed to delete backup")
	}

	return nil
}

func (s *serviceImpl) validateBackupDeleteProtection(
	ctx context.Context,
	backup *entity.Backup,
	now time.Time,
) bizerr.BizError {
	if backup == nil || backup.Backup == nil {
		return bizerr.New(bizerr.ErrCodeUnknown, "backup is nil")
	}

	if backup.TriggerType != schema.BackupTriggerTypeAuto || backup.State != schema.BackupStateSuccess {
		return nil
	}

	if !backup.ExpirationTime.IsZero() && !now.Before(backup.ExpirationTime) {
		return nil
	}

	orderBy := "backup_time DESC, id DESC"
	latestAutoBackups, berr := s.backupRepo.UnscopedQuery(ctx, &br.QueryBackupsFilter{
		States:       []schema.BackupState{schema.BackupStateSuccess},
		ClusterIds:   []uint64{backup.ClusterId},
		TriggerTypes: []schema.BackupTriggerType{schema.BackupTriggerTypeAuto},
		Deleted:      ptr.Bool(false),
		OrderBy:      &orderBy,
	}, &pagination.Page{
		PageSize: 1,
	})
	if berr != nil {
		return berr.Chain("failed to query latest auto backup for delete protection").OverwriteCode(bizerr.ErrCodeDB)
	}

	if len(latestAutoBackups) == 0 || latestAutoBackups[0].ID != backup.ID {
		return nil
	}

	return bizerr.New(
		bizerr.ErrCodeParams,
		"This backup is protected under the Data Safety Policy and cannot be deleted until expiration. Contact support if you want to delete it before it expires.",
	)
}
