package dbutil

// query.go contains common query fragments
import (
	"context"
	"time"

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

// lists all undeleted BackupSchedules for a cluster
func ClusterBackupSchedules(tx *gorm.DB, clusterID entity.ID) entity.BackupScheduleQuerySet {
	return entity.NewBackupScheduleQuerySet(tx).ClusterIDEq(clusterID).DeletedAtIsNull().PreloadCluster()
}

func GetBackupSchedulesByClusterIDs(tx *gorm.DB, clusterIDs []entity.ID) ([]entity.BackupSchedule, error) {
	if len(clusterIDs) == 0 {
		return nil, nil
	}

	var schedules []entity.BackupSchedule
	err := entity.NewBackupScheduleQuerySet(tx).ClusterIDIn(clusterIDs...).DeletedAtIsNull().All(&schedules)
	return schedules, err
}

// Get Backup via backupSchedule ID
func ScheduleBackups(tx *gorm.DB, backupScheduleID entity.ID) entity.BackupQuerySet {
	return entity.NewBackupQuerySet(tx).BackupScheduleIDEq(backupScheduleID).DeletedAtIsNull()
}

func GetProvider(tx *gorm.DB, name entity.Cloud) (entity.Provider, error) {
	provider := entity.Provider{}
	err := entity.NewProviderQuerySet(tx).NameEq(name).One(&provider)
	return provider, errors.Annotatef(TagNotFound(err), "failed to get provider %s", provider.Name)
}

type ListAccessLogQuery struct {
	StartTimeInSec int64
	EndTimeInSec   int64
	User           string
	Content        string
	From           int
	Size           int
}

func GetClusterRegionsV2(ctx context.Context, tx *gorm.DB, clusterID entity.ID) ([]entity.Region, error) {
	logger := log.WithContextL(ctx).Sugar().With(zap.Uint64("clusterID", clusterID.Uint64()))

	var clusterComponents []*entity.ClusterComponent
	if err := tx.Where("cluster_id=?", clusterID).
		Preload("Region").
		Find(&clusterComponents).Error; err != nil {
		return nil, err
	}

	if len(clusterComponents) == 0 {
		logger.Warnf("no component in cluster")
		return nil, nil
	}

	id2Region := make(map[uint64]*entity.Region)
	for _, each := range clusterComponents {
		id2Region[each.RegionID.Uint64()] = each.Region
	}
	var regions []entity.Region
	for _, each := range id2Region {
		regions = append(regions, *each)
	}
	return regions, nil
}

// GetClusterProviderAndRegion get region and provider of a cluster
func GetClusterProviderAndRegion(ctx context.Context, tx *gorm.DB, cluster entity.Cluster) (*entity.Provider, *entity.Region, error) {
	provider := entity.Provider{}
	err := entity.NewProviderQuerySet(tx).IDEq(cluster.ProviderID).One(&provider)
	if err != nil {
		return nil, nil, err
	}

	regions, err := GetClusterRegionsV2(ctx, tx, cluster.ID)
	if err != nil {
		return nil, nil, err
	}
	// TODO: From now on, there is only one region for each cluster
	// we should modify it when we support more than one region
	if len(regions) != 1 {
		return nil, nil, errors.New("only support one region per cluster")
	}
	return &provider, &regions[0], nil
}

// GetCluster gets cluster by clusterID
func GetClusterByID(tx *gorm.DB, orgID, projectID, clusterID entity.ID) (entity.Cluster, error) {
	tx = PreloadCluster(tx)
	cluster := entity.Cluster{}

	err := entity.NewClusterQuerySet(tx).
		StatusNotIn(entity.ClusterStatusCleared).
		IDEq(clusterID).One(&cluster)
	if err != nil {
		return cluster, TagNotFound(err)
	}
	err = CheckResourceOrgIDAndProjectID(orgID, cluster.TenantID, projectID, cluster.ProjectID)
	if err != nil {
		return cluster, err
	}
	return cluster, nil
}

// GetCluster gets cluster by clusterID and does not verify orgID and projectID
func GetCluster(tx *gorm.DB, clusterID entity.ID) (entity.Cluster, error) {
	tx = PreloadCluster(tx)

	cluster := entity.Cluster{}

	err := entity.NewClusterQuerySet(tx).
		PreloadProject().
		StatusNotIn(entity.ClusterStatusCleared).
		IDEq(clusterID).One(&cluster)
	if err != nil {
		return cluster, TagNotFound(err)
	}
	return cluster, nil
}

// GetDeletedCluster fetches the cluster which has been deleted but before cleaned
func GetDeletedClusterByID(tx *gorm.DB, orgID, projectID, clusterID entity.ID) (entity.Cluster, error) {
	tx = PreloadCluster(tx)
	cluster := entity.Cluster{}
	err := entity.NewClusterQuerySet(tx).IDEq(clusterID).StatusIn(entity.DeletedClusterStatus...).One(&cluster)
	if err != nil {
		return cluster, errors.Annotate(TagNotFound(err), "GetDeletedCluster")
	}
	err = CheckResourceOrgIDAndProjectID(orgID, cluster.TenantID, projectID, cluster.ProjectID)
	if err != nil {
		return cluster, err
	}
	return cluster, nil
}

type ListClusterModifier int

const (
	IncludeDeleting ListClusterModifier = 1 << 0
	IncludeCleared  ListClusterModifier = 1 << 1
)

// By default only alive clusters are listed.
func buildListClusterFilter(modifiers ...ListClusterModifier) ClusterFilter {
	allModifierFlags := 0
	for _, m := range modifiers {
		allModifierFlags = allModifierFlags | int(m)
	}

	f := ClusterFilter{}
	if (allModifierFlags & int(IncludeCleared)) == 0 {
		// If IncludeCleared flag is not set, exclude Cleared status by default.
		f.Cleared = pointer.BoolPtr(false)
	}
	if (allModifierFlags & int(IncludeDeleting)) == 0 {
		// If IncludeDeleting flag is not set, exclude Deleted status by default.
		f.Deleted = pointer.BoolPtr(false)
	}
	return f
}

func GetClusterMatchTimeRange(tx *gorm.DB, tenantID entity.ID, startTime int64, endTime int64) ([]entity.Cluster, error) {
	var (
		clusters []entity.Cluster
		err      error
	)
	if err = entity.NewClusterQuerySet(tx.Where("deleted_time is null or deleted_time>?", time.Unix(startTime, 0))).TenantIDEq(tenantID).CreatedAtLte(time.Unix(endTime, 0)).All(&clusters); err != nil {
		return nil, errors.AddStack(err)
	}
	return clusters, nil
}

func SumClusterTrafficUsageByTenant(tx *gorm.DB, tenantID entity.ID, period string) (ingressUsage, egressUsage int64, err error) {
	type Summary struct {
		SumIngressUsage int64 `gorm:"column:sum_ingress"`
		SumEgressUsage  int64 `gorm:"column:sum_egress"`
	}
	var summary Summary
	err = tx.Model(&entity.ClusterTrafficUsage{}).Where("tenant_id=? AND period=?", tenantID, period).
		Select("sum(ingress_usage) as sum_ingress, sum(egress_usage) as sum_egress").First(&summary).Error
	return summary.SumIngressUsage, summary.SumEgressUsage, errors.Annotate(err, "gorm.Select")
}
