package dbutil

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

	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/errors"
	"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
}

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

// lists all BackupSchedules for a cluster
func GetBackupAutoSchedule(tx *gorm.DB, clusterID entity.ID) (entity.BackupSchedule, error) {
	backupSchedule := entity.BackupSchedule{}
	err := entity.NewBackupScheduleQuerySet(tx).ClusterIDEq(clusterID).TypeEq(entity.BackupType_Auto).PreloadCluster().One(&backupSchedule)
	return backupSchedule, TagNotFound(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 ListAccessLog(tx *gorm.DB, tenantID entity.ID, query *ListAccessLogQuery) ([]entity.AuditLog, int64, error) {
	qs := entity.NewAuditLogQuerySet(tx).TenantIDEq(tenantID)
	if query.StartTimeInSec > 0 {
		qs = qs.CreatedAtGte(time.Unix(query.StartTimeInSec, 0))
	}
	if query.EndTimeInSec > 0 {
		qs = qs.CreatedAtLte(time.Unix(query.EndTimeInSec, 0))
	}
	if query.User != "" {
		qs = qs.UserEq(query.User)
	}
	if query.Content != "" {
		qs = entity.NewAuditLogQuerySet(qs.GetDB().Where("content like ?", "%"+query.Content+"%"))
	}
	count, err := qs.Count()
	if err != nil {
		return nil, 0, errors.Annotate(err, "failed to query access log count")
	}
	var items []entity.AuditLog
	if err := qs.Offset(query.From).Limit(query.Size).OrderDescByCreatedAt().All(&items); err != nil {
		return nil, 0, errors.Annotate(err, "failed to query access log")
	}
	return items, count, nil
}

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
}

func GetDeployedCluster(tx *gorm.DB, tenantID entity.ID) ([]entity.Cluster, error) {
	return FindClusters(tx, ClusterFilter{
		TenantID: &tenantID,
		Creating: pointer.BoolPtr(false),
		Deleted:  pointer.BoolPtr(false),
		Cleared:  pointer.BoolPtr(false),
	})
}

func GetClustersForCleanupBackupRestore(tx *gorm.DB, tenantID entity.ID) ([]entity.Cluster, error) {
	return FindClusters(tx, ClusterFilter{
		TenantID: &tenantID,
		Creating: pointer.BoolPtr(false),
		Cleared:  pointer.BoolPtr(false),
	})
}

func GetProjectActiveClusters(tx *gorm.DB, projectID entity.ID) ([]entity.Cluster, error) {
	return FindClusters(tx, ClusterFilter{
		ProjectID: &projectID,
		Deleted:   pointer.BoolPtr(false),
		Cleared:   pointer.BoolPtr(false),
	})
}

func GetProjectNotClearedClusters(tx *gorm.DB, projectID entity.ID) ([]entity.Cluster, error) {
	return FindClusters(tx, ClusterFilter{
		ProjectID: &projectID,
		Cleared:   pointer.BoolPtr(false),
	})
}

func GetTotalFreeTrialPoint(tx *gorm.DB, tenantID entity.ID) (int64, error) {
	var freeTrialPoints []entity.FreeTrialPoint
	err := entity.NewFreeTrialPointQuerySet(tx).TenantIDEq(tenantID).All(&freeTrialPoints)
	if err != nil {
		return 0, err
	}
	var totalAmount int64 = 0
	for _, item := range freeTrialPoints {
		totalAmount += item.Amount
	}
	return totalAmount, 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
}

type ProjectFilter struct {
	TenantIDs  []entity.ID
	ProjectIDs []entity.ID
}

func FindProjects(tx *gorm.DB, filter *ProjectFilter) (map[entity.ID]*entity.Project, error) {
	if filter.TenantIDs != nil {
		tx = tx.Where("tenant_id in (?)", filter.TenantIDs)
	}

	if filter.ProjectIDs != nil {
		tx = tx.Where("id in (?)", filter.ProjectIDs)
	}

	var projs []*entity.Project
	if err := tx.Find(&projs).Error; err != nil {
		return nil, errors.AddStack(err)
	}
	outs := make(map[entity.ID]*entity.Project, len(projs))
	for _, each := range projs {
		outs[each.ID] = each
	}
	return outs, nil
}

func GetProject(ctx context.Context, tx *gorm.DB, projectID entity.ID) (*entity.Project, error) {
	proj := &entity.Project{}
	if err := tx.Model(&entity.Project{}).Where("id=?", projectID).First(proj).Error; err != nil {
		return nil, err
	}
	return proj, 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 LoadTrialByTenantID(ctx context.Context, tx *gorm.DB, tenantID entity.ID) (*entity.Trial, error) {
	var trial entity.Trial
	if err := entity.NewTrialQuerySet(tx).TenantIDEq(tenantID).TrialTypeEq(entity.TrialTypeDevTier).One(&trial); err != nil {
		return nil, err
	}
	return &trial, 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")
}
