package dbutil

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

	"github.com/pingcap/errors"
	"github.com/tidbcloud/central-sdk/config"
	"github.com/tidbcloud/central-sdk/db/entity"
	"gorm.io/gorm"
	"k8s.io/utils/pointer"
)

// GetClusterProfile gets a cluster profile
func GetClusterProfile(db *gorm.DB, profileName string, regionID entity.ID) (entity.ClusterProfile, error) {
	profile := entity.ClusterProfile{}
	err := entity.NewClusterProfileQuerySet(db).NameEq(profileName).RegionIDEq(regionID).One(&profile)
	return profile, TagNotFound(err)
}

func GetComponentProfilesByComponentNameAndProfileId(db *gorm.DB, profileID entity.ID, componentName entity.ComponentName) (*entity.ComponentProfile, error) {
	componentProfile := entity.ComponentProfile{}
	err := entity.NewComponentProfileQuerySet(db).ClusterProfileIDEq(profileID).ComponentNameEq(componentName).PreloadRegionDisk().PreloadMachineProfile().One(&componentProfile)
	return &componentProfile, err
}

func GetScaleComponent(tx *gorm.DB, scaleID uint64) (*entity.ScaleOperationTracking, error) {
	scale := entity.ScaleOperationTracking{}
	if err := entity.NewScaleOperationTrackingQuerySet(tx).IDEq(entity.ID(scaleID)).One(&scale); err != nil {
		if nil != errors.Find(err, func(e error) bool { return e == gorm.ErrRecordNotFound }) {
			return nil, nil
		}
		return nil, errors.Annotate(err, "Get ScaleComponent")
	}
	return &scale, nil
}

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

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

// ShootFromUniqueIndex looks up a shoot
func ShootFromUniqueIndex(db *gorm.DB, vpcId string, name string) (entity.Shoot, error) {
	shoot := &entity.Shoot{}
	err := entity.NewShootQuerySet(db).VpcIDEq(vpcId).NameEq(name).One(shoot)
	ann := fmt.Sprintf("ShootFromUniqueIndex, vpcId: %s, name: %s", vpcId, name)
	return *shoot, errors.Annotate(TagNotFound(err), ann)
}

// GetShoot looks up a shoot
func GetShoot(tx *gorm.DB, projectID entity.ID, name string) (entity.Shoot, error) {
	shoot := entity.Shoot{}
	if err := entity.NewShootQuerySet(tx).NameEq(name).ProjectIDEq(projectID).One(&shoot); err != nil {
		if errors.Find(err, func(e error) bool { return e == gorm.ErrRecordNotFound }) != nil {
			return shoot, nil
		}
		return shoot, errors.Annotate(err, "Get Shoot")
	}
	return shoot, nil
}

func GetProvider(tx *gorm.DB, name config.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)
}

func GetRegionFromUniqueIndex(db *gorm.DB, providerID entity.ID, regionName string) (*entity.Region, error) {
	region := entity.Region{}
	err := entity.NewRegionQuerySet(db.Preload("Zones")).NameEq(regionName).ProviderIDEq(providerID).One(&region)
	if err != nil {
		return &region, errors.Annotatef(TagNotFound(err), "failed to get region %s", regionName)
	}
	return &region, nil
}

func GetRegion(db *gorm.DB, providerName config.Cloud, regionName string) (*entity.Region, error) {
	region := &entity.Region{}
	provider, err := GetProvider(db, providerName)
	if err != nil {
		return region, errors.Annotatef(err, "failed to get provider %s", providerName)
	}

	region, err = GetRegionFromUniqueIndex(db, provider.ID, regionName)
	if err != nil {
		return region, err
	}

	var zones []entity.Zone
	errAZ := entity.NewZoneQuerySet(db).RegionIDEq(region.ID).All(&zones)
	if errAZ != nil {
		return nil, errAZ
	}
	for i := range zones {
		zone := zones[i]
		region.Zones = append(region.Zones, &zone)
	}
	return region, nil
}

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 GetClusterRegions(tx *gorm.DB, clusterID entity.ID) ([]entity.Region, error) {
	var components []entity.Component
	err := entity.NewComponentQuerySet(tx).ClusterIDEq(clusterID).All(&components)
	if err != nil {
		return nil, err
	}
	var componentsIDs []entity.ID
	for _, item := range components {
		componentsIDs = append(componentsIDs, item.ID)
	}
	if len(componentsIDs) == 0 {
		return nil, nil
	}

	var componentMachine []entity.ComponentMachine
	err = entity.NewComponentMachineQuerySet(tx).PreloadMachine().ComponentIDIn(componentsIDs...).All(&componentMachine)
	if err != nil {
		return nil, err
	}
	var regionsIds []entity.ID
	for _, item := range componentMachine {
		var zone entity.Zone
		err = entity.NewZoneQuerySet(tx).IDEq(item.Machine.ZoneID).One(&zone)
		if err != nil {
			return nil, errors.Errorf("clusterID %d, get zone by id %d, error %v", clusterID.Uint64(), item.Machine.ZoneID.Uint64(), err)
		}
		regionsIds = append(regionsIds, zone.RegionID)
	}
	if len(regionsIds) == 0 {
		return nil, nil
	}

	tx = tx.Preload("Zones")

	var regions []entity.Region
	err = entity.NewRegionQuerySet(tx).IDIn(regionsIds...).All(&regions)
	return regions, err
}

func GetMachineProfileByID(tx *gorm.DB, machineProfileID entity.ID) (*entity.MachineProfile, error) {
	var machineProfile entity.MachineProfile
	err := entity.NewMachineProfileQuerySet(tx).IDEq(machineProfileID).One(&machineProfile)
	return &machineProfile, errors.Annotate(err, "GetClusterComponentMachineByComponentID")
}

func GetRegionDisk(db *gorm.DB, diskProfileID entity.ID, regionID entity.ID, capacity uint32) (*entity.RegionDisk, error) {
	regionDisk := entity.RegionDisk{}
	err := entity.
		NewRegionDiskQuerySet(db).
		DiskProfileIDEq(diskProfileID).
		RegionIDEq(regionID).
		CapacityEq(capacity).One(&regionDisk)
	return &regionDisk, err
}

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 GetClustersReadyForBackup(tx *gorm.DB, tenantID entity.ID) ([]entity.Cluster, error) {
	clusters, err := FindClusters(tx, ClusterFilter{
		TenantID: &tenantID,
		Statuss:  entity.ReadyForBackupClusterStatus,
	})
	if err != nil {
		return clusters, err
	}

	var pocDeletedClusters []entity.Cluster
	tx = PreloadCluster(tx)
	err = entity.NewClusterQuerySet(tx).StatusEq(entity.ClusterStatusDeleted).PocDeletedEq(true).TenantIDEq(tenantID).All(&pocDeletedClusters)
	if err != nil {
		return clusters, err
	}
	clusters = append(clusters, pocDeletedClusters...)
	return clusters, nil
}

func GetAliveCluster(tx *gorm.DB, tenantID entity.ID) ([]entity.Cluster, error) {
	return FindClusters(tx, ClusterFilter{
		TenantID: &tenantID,
		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 GetDeletedClusters(tx *gorm.DB, tenantID entity.ID) ([]entity.Cluster, error) {
	return FindClusters(tx, ClusterFilter{
		TenantID: &tenantID,
		Deleted:  pointer.BoolPtr(true),
	})
}

func GetClusterCurrentScaleOperationTracking(tx *gorm.DB, clusterID entity.ID) (*entity.ClusterOperation, []entity.ScaleOperationTracking, error) {
	var clusterOperation entity.ClusterOperation
	if err := entity.NewClusterOperationQuerySet(tx).ClusterIDEq(clusterID).StatusEq(entity.ClusterOperationStatusCreated).TypeEq(entity.ClusterOperationTypeScale).One(&clusterOperation); err != nil {
		return nil, nil, errors.Annotate(err, "query cluster operation failed")
	}
	scaleOperationTracking := make([]entity.ScaleOperationTracking, 0)
	err := entity.NewScaleOperationTrackingQuerySet(tx).ClusterOperationIDEq(clusterOperation.ID).All(&scaleOperationTracking)
	return &clusterOperation, scaleOperationTracking, errors.Annotate(err, "scaleOperationTracking")
}

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(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 := GetClusterRegions(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).
		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
}

// GetDeletedCluster fetches the cluster which has been deleted but before cleaned and does not verify orgID and projectID
func GetDeletedCluster(tx *gorm.DB, orgID, 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")
	}
	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
}

// ListClusters lists cluster by tenantID.
// By default only alive clusters are listed. This behavior can be changed by supplying additional modifiers.
func ListClusters(tx *gorm.DB, tenantID entity.ID, modifiers ...ListClusterModifier) ([]entity.Cluster, error) {
	f := buildListClusterFilter(modifiers...)
	f.TenantID = &tenantID
	return FindClusters(tx, f)
}

// ListClustersByProject lists cluster by tenantID and projectID.
// By default only alive clusters are listed. This behavior can be changed by supplying additional modifiers.
func ListClustersByProject(tx *gorm.DB, tenantID entity.ID, projectID entity.ID, modifiers ...ListClusterModifier) ([]entity.Cluster, error) {
	f := buildListClusterFilter(modifiers...)
	f.TenantID = &tenantID
	f.ProjectID = &projectID
	return FindClusters(tx, f)
}

// ListClustersByProjectAndProvider lists cluster by tenantID, projectID and providerID.
// By default only alive clusters are listed. This behavior can be changed by supplying additional modifiers.
func ListClustersByProjectAndProvider(tx *gorm.DB, tenantID entity.ID, projectID entity.ID, providerID entity.ID, modifiers ...ListClusterModifier) ([]entity.Cluster, error) {
	f := buildListClusterFilter(modifiers...)
	f.TenantID = &tenantID
	f.ProjectID = &projectID
	f.ProviderID = &providerID
	return FindClusters(tx, 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
}
