package dbutil

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

	"github.com/pingcap/dbaas/pkg/constants"
	"github.com/pingcap/dbaas/pkg/redact"
	"github.com/pingcap/dbaas/pkg/tidb/component"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/tidbcloud/central-sdk/config"
	"github.com/tidbcloud/central-sdk/db/entity"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

type ClusterCreate struct {
	ProjectID             entity.ID
	Name                  string
	Description           string
	EncryptedRootPassword string
	Provider              config.Cloud
	Region                string
	ProfileName           string
	TiKVCount             uint32
	TiDBCount             uint32
	TiFlashCount          uint32
	PDCount               uint32
	Registry              string
	Recovering            bool
	ClusterConfigurations *entity.ClusterConfigurations
}

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

func ValidateClusterName(tx *gorm.DB, name string, projectID entity.ID) error {
	count, err := entity.NewClusterQuerySet(tx).NameEq(name).ProjectIDEq(projectID).DeletedTimeIsNull().Count()
	if err != nil {
		return errcode.NewInternalErr(errors.New("failed to query cluster count by name"))
	}
	if count > 0 {
		return errcode.NewInvalidInputErr(errors.New(ErrUniqueClusterName))
	}
	return nil
}

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
}

func AddClusterKMS(tx *gorm.DB, clusterID, regionID entity.ID, kmsArn, kmsID string) error {
	kms := entity.KMS{
		Updatable: entity.Updatable{},
		ClusterID: clusterID,
		RegionID:  regionID,
		KmsID:     kmsID,
		KmsArn:    kmsArn,
	}
	return errors.Annotate(kms.Create(tx), "AddClusterKMS")
}

func assembleDisk(regionDisk *entity.RegionDisk) *entity.Disk {
	return &entity.Disk{
		RegionDisk: regionDisk,
	}
}

func assembleMachine(tx *gorm.DB, regionID, zoneID, machineProfileID entity.ID, name string, regionDisk *entity.RegionDisk) (*entity.Machine, error) {
	machineProfile, err := GetMachineProfileByID(tx, machineProfileID)
	if err != nil {
		return nil, errors.Annotate(err, fmt.Sprintf("failed to get machine profile machine profile ID %d", machineProfileID))
	}
	diskList := []*entity.Disk{assembleDisk(regionDisk)}
	return &entity.Machine{
		Name:             name,
		MachineProfileID: machineProfile.ID,
		ZoneID:           zoneID,
		Disks:            diskList,
		Status:           entity.MachineStatus_CREATING,
	}, nil
}

func assembleComponentMachine(tx *gorm.DB, regionID, zoneID, machineProfileID entity.ID, regionDisk *entity.RegionDisk) (*entity.ComponentMachine, error) {
	log.L().Info("assembleComponentMachine", zap.Uint64("regionID", regionID.Uint64()), zap.Uint64("zoneID", zoneID.Uint64()))
	// TODO: generate machine name by a method
	machineName := "temple"
	machine, err := assembleMachine(tx, regionID, zoneID, machineProfileID, machineName, regionDisk)
	if err != nil {
		return nil, err
	}
	return &entity.ComponentMachine{
		Status:  entity.CreatingComponent,
		Machine: machine,
	}, nil
}

func assembleComponent(tx *gorm.DB, componentName component.Name, clusterConf ClusterCreate) (*entity.Component, error) {
	logConf, _ := redact.AsMapRedactKeys(redact.PasswordFields, clusterConf)
	log.L().Info("assembleComponent", zap.Any("req", logConf), zap.String("component name", componentName.String()))
	var componentMachines []*entity.ComponentMachine
	provider, err := GetProvider(tx, clusterConf.Provider)
	if err != nil {
		return nil, err
	}
	region, err := GetRegionFromUniqueIndex(tx, provider.ID, clusterConf.Region)
	if err != nil {
		return nil, err
	}
	clusterProfile, err := GetClusterProfile(tx, clusterConf.ProfileName, region.ID)
	if err != nil {
		return nil, err
	}
	componentProfile, err := GetComponentProfilesByComponentNameAndProfileId(tx, clusterProfile.ID, entity.ComponentName(componentName))
	if err != nil {
		return nil, err
	}
	var idx, compNumber uint32
	if componentName == component.PD {
		compNumber = componentProfile.Replicas
	}
	if componentName == component.TiDB {
		compNumber = clusterConf.TiDBCount
	}
	if componentName == component.TiKV {
		compNumber = clusterConf.TiKVCount
	}
	if componentName == component.TiFlash {
		compNumber = clusterConf.TiFlashCount
	}
	for idx = 0; idx < compNumber; idx++ {
		compMachine, err := assembleComponentMachine(tx, region.ID, region.Zones[idx%uint32(len(region.Zones))].ID, componentProfile.MachineProfileID, componentProfile.RegionDisk)
		if err != nil {
			return nil, err
		}
		componentMachines = append(componentMachines, compMachine)
	}

	return &entity.Component{
		ComponentMachines: componentMachines,
		ComponentName:     entity.ComponentName(componentName),
	}, nil
}

func assembleCluster(tx *gorm.DB, clusterConf ClusterCreate, userID, tenantID entity.ID) (*entity.Cluster, error) {
	logConf, _ := redact.AsMapRedactKeys([]string{"password"}, clusterConf)
	log.L().Info("assembleCluster", zap.Uint64("userID", userID.Uint64()), zap.Any("req", logConf))
	provider, err := GetProvider(tx, clusterConf.Provider)
	if err != nil {
		return nil, err
	}
	region, err := GetRegionFromUniqueIndex(tx, provider.ID, clusterConf.Region)
	if err != nil {
		return nil, err
	}
	clusterProfile, err := GetClusterProfile(tx, clusterConf.ProfileName, region.ID)
	if err != nil {
		return nil, err
	}
	var components []entity.Component
	pdComponents, err := assembleComponent(tx, component.PD, clusterConf)
	if err != nil {
		return nil, err
	}
	components = append(components, *pdComponents)
	tidbComponents, err := assembleComponent(tx, component.TiDB, clusterConf)
	if err != nil {
		return nil, err
	}
	components = append(components, *tidbComponents)
	tikvComponents, err := assembleComponent(tx, component.TiKV, clusterConf)
	if err != nil {
		return nil, err
	}
	components = append(components, *tikvComponents)
	if constants.IsTiFlashProfile(clusterConf.ProfileName) && clusterConf.TiFlashCount > 0 {
		tiflashComponents, err := assembleComponent(tx, component.TiFlash, clusterConf)
		if err != nil {
			return nil, err
		}
		components = append(components, *tiflashComponents)
	}
	return &entity.Cluster{
		Name:                  clusterConf.Name,
		TenantID:              tenantID,
		ProjectID:             clusterConf.ProjectID,
		CreatedByID:           userID,
		Status:                entity.ClusterStatusInitializing,
		EncryptedRootPassword: clusterConf.EncryptedRootPassword,
		Components:            components,
		Replicas:              3,
		ProviderID:            provider.ID,
		ClusterProfileID:      clusterProfile.ID,
	}, nil
}

func CreateCluster(tx *gorm.DB, userID, tenantID entity.ID, clusterConf ClusterCreate, plan entity.TenantPlan) (*entity.Cluster, error) {
	cluster, err := assembleCluster(tx, clusterConf, userID, tenantID)
	if err != nil {
		return nil, err
	}

	err = tx.Create(cluster).Error
	if err != nil {
		return nil, err
	}

	if plan == entity.TenantPlanPOC || plan == entity.TenantPlanOnDemand {
		_, err = CreateBackupSchedule(tx, userID, cluster.ID)
		if err != nil {
			return nil, errors.Annotate(err, "failed to create backup schedule record")
		}
	}
	return cluster, err
}

func DeletePocCluster(tx *gorm.DB, clusterID entity.ID) error {
	log.L().Info("delete poc 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().SetPocDeleted(true).SetDeletedTime(&now).Update()
}

func DeleteCluster(tx *gorm.DB, clusterID entity.ID) error {
	log.L().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()
}

func DeleteConnection(tx *gorm.DB, connectionID entity.ID) error {
	connection := entity.Connection{}
	connection.ID = connectionID
	return errors.Annotate(connection.Delete(tx), "DeleteConnection")
}

func MarkConnectionSuccess(tx *gorm.DB, id entity.ID) error {
	success := true
	udp := entity.NewConnectionQuerySet(tx).IDEq(id).GetUpdater()
	return udp.SetCreateSuccess(&success).Update()
}

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

	// TODO: Simplify these status...
	Status   *entity.ClusterStatus
	Statuss  []entity.ClusterStatus
	Deleted  *bool
	Cleared  *bool
	Creating *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)
	}

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

	return clusters, nil
}

type ClusterOperationFilter struct {
	Type      *entity.ClusterOperationType
	Status    *entity.ClusterOperationStatus
	TenantID  *entity.ID
	ClusterID *entity.ID
}

func FindClusterOperations(tx *gorm.DB, filter ClusterOperationFilter) (ops []entity.ClusterOperation, err error) {
	if filter.TenantID != nil {
		tx = tx.Joins("JOIN clusters ON cluster_operations.cluster_id = clusters.id").
			Where("clusters.tenant_id = ?", (*filter.TenantID).Uint64())
	}

	if filter.ClusterID != nil {
		tx = tx.Where("cluster_operations.cluster_id = ?", filter.ClusterID.Uint64())
	}

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

	qs := entity.NewClusterOperationQuerySet(tx).PreloadCluster()

	if filter.Type != nil {
		qs = qs.TypeEq(*filter.Type)
	}

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

	return
}

type ComponentMachineFilter struct {
	Status *entity.ComponentMachineStatus
}

func UpdateComponentMachinesStatus(tx *gorm.DB, componentID entity.ID, filter ComponentMachineFilter) error {
	qs := entity.NewComponentMachineQuerySet(tx).ComponentIDEq(componentID).StatusNotIn(entity.DeletedComponent)
	updater := qs.GetUpdater()

	if filter.Status != nil {
		updater = updater.SetStatus(*filter.Status)
	}

	return updater.Update()
}
