package dbutil

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

	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/pkgv2/log"
	"go.opentelemetry.io/otel"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

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

type ClusterCreate struct {
	ProjectID                   entity.ID
	Name                        string
	Description                 string
	EncryptedRootPassword       string
	EncryptedCloudAdminPassword string
	Provider                    entity.Cloud
	Region                      string
	ProfileName                 string
	TiKVCount                   uint32
	TiDBCount                   uint32
	TiFlashCount                uint32
	PDCount                     uint32
	Registry                    string
	Free                        bool
	Recovering                  bool
	PrimaryClusterID            uint64
	ReplicationID               uint64
}

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

func UpsertCidrTemplate(tx *gorm.DB, cidr *entity.CidrTemplate) error {
	var oldCidr entity.CidrTemplate
	err := entity.NewCidrTemplateQuerySet(tx).ProjectIDEq(cidr.ProjectID).One(&oldCidr)
	if err == nil {
		cidr.ID = oldCidr.ID
		return errors.Annotate(cidr.Update(tx, entity.CidrTemplateDBSchema.Cidr), "UpsertCidrTemplate")
	} else if err == gorm.ErrRecordNotFound {
		return errors.Annotate(cidr.Create(tx), "UpsertCidrTemplate")
	}
	return err
}

func UpsertClusterCidr(tx *gorm.DB, cidr *entity.ClusterCidr) error {
	var existCidr entity.ClusterCidr
	err := entity.NewClusterCidrQuerySet(tx).ClusterIDEq(cidr.ClusterID).One(&existCidr)
	if err == nil {
		cidr.ID = existCidr.ID
		return errors.Annotate(cidr.Update(tx, entity.ClusterCidrDBSchema.Cidr), "UpsertClusterCidr")
	} else if err == gorm.ErrRecordNotFound {
		return errors.Annotate(cidr.Create(tx), "UpsertClusterCidr")
	}
	return err
}

// 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
}

type PeeringFilter struct {
	ProjectID *entity.ID
	Region    *string

	Status  *entity.ClusterStatus
	Statuss []entity.ClusterStatus
}

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
}

// GetProviderDevTierClustersCount returns provider active dev tier clusters count
func GetProviderDevTierClustersCount(tx *gorm.DB, providerName entity.Cloud) (int64, error) {
	var count int64
	provider, err := GetProvider(tx, providerName)
	if err != nil {
		return count, err
	}
	return entity.NewClusterQuerySet(tx).FreeEq(true).
		StatusNotIn(entity.DeletedOrClearedClusterStatus...).ProviderIDEq(provider.ID).Count()
}

func IsComponentsReplicaSame(left, right []*entity.ClusterComponent) bool {
	if len(left) != len(right) {
		return false
	}

	lComponentType2replica := map[entity.ComponentName]uint32{}
	rComponentType2replica := map[entity.ComponentName]uint32{}
	for _, component := range left {
		lComponentType2replica[component.ComponentType] = component.Replicas
	}
	for _, component := range right {
		rComponentType2replica[component.ComponentType] = component.Replicas
	}
	for tp, replica := range lComponentType2replica {
		if replica != rComponentType2replica[tp] {
			return false
		}
	}
	return true
}
