package dbutil

// Database access should be done through this file.
import (
	"context"
	"time"

	"github.com/pingcap/errors"
	entity "github.com/tidbcloud/core-svc/model"
	"github.com/tidbcloud/pkgv2/log"
	"go.opentelemetry.io/otel"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

var Tracer = otel.Tracer("central/db/dbutil")

const (
	ErrUniqueClusterName = "The cluster name must be unique in a project."
)

// DeleteClusterAndMakeFinalBackup mark the cluster is deleted and required to make final-backup
func DeleteClusterAndMakeFinalBackup(ctx context.Context, tx *gorm.DB, clusterID entity.ID) error {
	log.WithContextL(ctx).Info("delete cluster", zap.Uint64("clusterID", clusterID.Uint64()))
	now := time.Now()
	if err := ClusterUpdateStatus(tx, clusterID, entity.ClusterStatusDeleted); err != nil {
		return err
	}
	return entity.NewClusterQuerySet(tx).IDEq(clusterID).
		GetUpdater().SetFinalBackupBeforeDelete(true).SetDeletedTime(&now).Update()
}

func DeleteCluster(ctx context.Context, tx *gorm.DB, clusterID entity.ID) error {
	log.WithContextL(ctx).Info("delete cluster", zap.Uint64("clusterID", clusterID.Uint64()))
	now := time.Now()
	if err := ClusterUpdateStatus(tx, clusterID, entity.ClusterStatusDeleted); err != nil {
		return err
	}
	return entity.NewClusterQuerySet(tx).IDEq(clusterID).
		GetUpdater().SetDeletedTime(&now).Update()
}

type ClusterFilter struct {
	TenantID     *entity.ID
	ProjectID    *entity.ID
	ProviderID   *entity.ID
	ProviderName *entity.Cloud
	Name         *string

	// TODO: Simplify these status...
	Status   *entity.ClusterStatus
	Statuss  []entity.ClusterStatus
	Deleted  *bool
	Cleared  *bool
	Creating *bool
	Free     *bool
}

func FindClusters(tx *gorm.DB, filter ClusterFilter) (clusters []entity.Cluster, err error) {
	// see limitation, we can not use go-queryset
	// https://github.com/jirfag/go-queryset/issues/33
	tx = PreloadCluster(tx)

	if filter.Name != nil {
		tx = tx.Where("clusters.Name = ?", *filter.Name)
	}

	if filter.ProviderName != nil {
		tx = tx.Joins("JOIN providers ON providers.id = clusters.provider_id").
			Where("providers.name = ?", *filter.ProviderName)
	}

	if filter.Status != nil {
		tx = tx.Where("clusters.status = ?", *filter.Status)
	}

	if len(filter.Statuss) > 0 {
		tx = tx.Where("clusters.status IN (?)", filter.Statuss)
	}

	if filter.Deleted != nil {
		if *filter.Deleted {
			tx = tx.Where("clusters.status IN (?)", entity.DeletedClusterStatus)
		} else {
			tx = tx.Where("clusters.status NOT IN (?)", entity.DeletedClusterStatus)
		}
	}
	if filter.Cleared != nil {
		if *filter.Cleared {
			tx = tx.Where("clusters.status IN (?)", []entity.ClusterStatus{entity.ClusterStatusCleared})
		} else {
			tx = tx.Where("clusters.status NOT IN (?)", []entity.ClusterStatus{entity.ClusterStatusCleared})
		}
	}
	if filter.Creating != nil {
		if *filter.Creating {
			tx = tx.Where("clusters.status IN (?)", entity.CreatingClusterStatus)
		} else {
			tx = tx.Where("clusters.status NOT IN (?)", entity.CreatingClusterStatus)
		}
	}

	qs := entity.NewClusterQuerySet(tx)
	if filter.TenantID != nil {
		qs = qs.TenantIDEq(*filter.TenantID)
	}

	if filter.ProjectID != nil {
		qs = qs.ProjectIDEq(*filter.ProjectID)
	}

	if filter.ProviderID != nil {
		qs = qs.ProviderIDEq(*filter.ProviderID)
	}

	if filter.Free != nil {
		qs = qs.FreeEq(*filter.Free)
	}

	err = qs.OrderDescByCreatedAt().All(&clusters)
	if err != nil {
		return nil, errors.AddStack(err)
	}

	return clusters, nil
}

// Used to ensure that each tenant has only one dev tier cluster and its backups
// DeleteOldDevTierClustersBackups will delete other running dev tier clusters backups
func DeleteOldDevTierClustersBackups(ctx context.Context, tx *gorm.DB, tenantID entity.ID, clusterID entity.ID) error {
	var devTierClusters []entity.Cluster
	err := entity.NewClusterQuerySet(tx).TenantIDEq(tenantID).FreeEq(true).IDNe(clusterID).StatusNe(entity.ClusterStatusCleared).All(&devTierClusters)
	if err != nil {
		return errors.Annotate(err, "failed to query old dev tier clusters")
	}
	for _, devTierCluster := range devTierClusters {
		if !devTierCluster.Status.Deleted() {
			log.Warn(ctx, "dev tier cluster is not deleted", zap.Uint64("clusterID", clusterID.Uint64()), zap.String("status", string(devTierCluster.Status)))
			continue
		}
		err = DeleteDeletedClusterBackups(tx, devTierCluster.TenantID, devTierCluster.ProjectID, devTierCluster.ID, entity.BackupType_All)
		if err != nil {
			return errors.Annotatef(err, "failed to delete backups of dev tier cluster %d", devTierCluster.ID)
		}
	}

	return nil
}
