package dbutil

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

	"github.com/pingcap/dbaas/pkg/capacity"
	"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"
	"k8s.io/utils/pointer"
)

// ClusterUpdateStatus update a cluster's status
func ClusterUpdateStatus(tx *gorm.DB, clusterID entity.ID, newStatus entity.ClusterStatus) error {
	if newStatus == entity.ClusterStatusCleared || newStatus == entity.ClusterStatusDeleted || newStatus == entity.ClusterStatusDeletecomplete {
		log.Info("change cluster status", zap.Uint64("clusterID", clusterID.Uint64()), zap.String("status", string(newStatus)))
	}

	if newStatus == entity.ClusterStatusDeleted {
		return entity.NewClusterQuerySet(tx).IDEq(clusterID).GetUpdater().SetStatus(newStatus).Update()
	}

	// Ensure that the cluster status is changed from deleted to deletecomplete
	if newStatus == entity.ClusterStatusDeletecomplete {
		updated, err := entity.NewClusterQuerySet(tx).IDEq(clusterID).StatusEq(entity.ClusterStatusDeleted).GetUpdater().SetStatus(newStatus).UpdateNum()
		if err != nil {
			return errors.Annotate(err, fmt.Sprintf("update cluster status failed, id=%d", clusterID.Uint64()))
		}
		if updated < 1 {
			return fmt.Errorf("update cluster status failed, id=%d, affected=%d", clusterID.Uint64(), updated)
		}
		return nil
	}

	// Ensure that the cluster status is changed from deletecomplete to cleared
	if newStatus == entity.ClusterStatusCleared {
		updated, err := entity.NewClusterQuerySet(tx).IDEq(clusterID).StatusEq(entity.ClusterStatusDeletecomplete).GetUpdater().SetStatus(newStatus).UpdateNum()
		if err != nil {
			return errors.Annotate(err, fmt.Sprintf("update cluster status failed, id=%d", clusterID.Uint64()))
		}
		if updated < 1 {
			return fmt.Errorf("update cluster status failed, id=%d, affected=%d", clusterID.Uint64(), updated)
		}
		return nil
	}

	// update clusters set status=? where id=? and status not in ('deleted', 'deletecomplete', 'cleared')
	updated, err := entity.NewClusterQuerySet(tx).IDEq(clusterID).StatusNotIn(entity.DeletedOrClearedClusterStatus...).GetUpdater().SetStatus(newStatus).UpdateNum()
	if err != nil {
		return errors.Annotate(err, fmt.Sprintf("update cluster status failed, id=%d", clusterID.Uint64()))
	}
	if updated < 1 {
		return fmt.Errorf("update cluster status failed, id=%d, affected=%d", clusterID.Uint64(), updated)
	}
	return nil
}

// ClusterUpdateFinishedTime update a cluster's creation finished_Time
func ClusterUpdateCreationFinishedTime(tx *gorm.DB, clusterID entity.ID, finishedTime *time.Time) error {
	updated, err := entity.NewClusterQuerySet(tx).IDEq(clusterID).GetUpdater().SetCreationFinishedTime(finishedTime).UpdateNum()
	if err != nil {
		return errors.Annotate(err, fmt.Sprintf("update cluster finished_time failed, id=%d", clusterID.Uint64()))
	}
	if updated < 1 {
		return fmt.Errorf("update cluster finished_time failed, id=%d， affected=%d", clusterID.Uint64(), updated)
	}
	return nil
}

func GetClusterRegion(tx *gorm.DB, cluster *entity.Cluster) (*entity.Region, error) {
	regions, err := GetClusterRegions(tx, cluster.ID)
	if err != nil {
		return 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, errors.New("only support one region per cluster")
	}

	return &regions[0], nil
}

func ListDeletedClustersByProject(tx *gorm.DB, tenantID, projectID entity.ID) ([]entity.Cluster, error) {
	return FindClusters(tx, ClusterFilter{
		Deleted:   pointer.BoolPtr(true),
		TenantID:  &tenantID,
		ProjectID: &projectID,
	})
}

func ListDeletedClustersByProjectAndProvider(tx *gorm.DB, provider config.Cloud, tenantID, projectID entity.ID) ([]entity.Cluster, error) {
	return FindClusters(tx, ClusterFilter{
		Deleted:      pointer.BoolPtr(true),
		TenantID:     &tenantID,
		ProjectID:    &projectID,
		ProviderName: &provider,
	})
}

type ClusterCreateResources struct {
	TiKVGi         uint32
	StorageCompute *capacity.Compute
	QueryCompute   *capacity.Compute
	ControlCompute *capacity.Compute
}

type ComponentScale struct {
	ComponentName string
	Replicas      uint32
}

func (c *ComponentScale) GetReplicas() uint32 {
	return c.Replicas
}

func (c *ComponentScale) GetComponentName() string {
	return c.ComponentName
}

type ScaleClusterComponents struct {
	ClusterID    uint64
	Components   []*ComponentScale
	AllowScaleIn bool
}

func CreateScaleComponent(tx *gorm.DB, componentID, opID entity.ID, replicas uint32) (*entity.ScaleOperationTracking, error) {
	component := entity.Component{}
	if err := entity.NewComponentQuerySet(tx).PreloadCluster().IDEq(componentID).One(&component); err != nil {
		return nil, errors.Annotatef(TagNotFound(err), "component id %v", uint32(componentID))
	}

	sc := entity.ScaleOperationTracking{
		ComponentID:        componentID.Uint64(),
		Status:             entity.ScaleOperationTrackingScaleInit,
		ClusterOperationID: opID,
		Replicas:           replicas,
	}

	err := sc.Create(tx)
	if err != nil {
		return nil, err
	}

	return &sc, nil
}

func CreateClusterConnection(tx *gorm.DB, clusterID entity.ID, connectionType entity.ClusterConnectionType, location string) (*entity.Connection, error) {
	address := &entity.Connection{
		ClusterID: clusterID,
		Type:      connectionType,
		DSN:       location,
	}

	err := address.Create(tx)
	if err != nil {
		return nil, errors.AddStack(err)
	}

	return address, nil
}

func AddOrUpdateRole(tx *gorm.DB, role *entity.Role) error {
	if role.ID > 0 {
		if err := role.Update(tx, entity.RoleDBSchema.Privileges, entity.RoleDBSchema.Name); err != nil {
			return errors.Annotate(err, "update role")
		}
	} else {
		if err := tx.Create(role).Error; err != nil {
			return errors.Annotate(err, "create role")
		}
	}
	return nil
}

func UpsertShoot(db *gorm.DB, shoot *entity.Shoot) error {
	_, err := ShootFromUniqueIndex(db, shoot.VpcID, shoot.Name)
	if err == nil {
		return nil
	} else if nil != errors.Find(err, func(e error) bool { return e == gorm.ErrRecordNotFound }) {
		return shoot.Create(db)
	} else {
		return err
	}
}

func UpdateTenantStatus(tx *gorm.DB, id entity.ID, status entity.TenantStatus) (err error) {
	var tenant entity.Tenant
	err = entity.NewTenantQuerySet(tx.Preload("Quotas")).IDEq(id).One(&tenant)
	if err != nil {
		return TagNotFound(err)
	}
	tenant.Status = status
	return tenant.Update(tx, entity.TenantDBSchema.Status)
}

func AddAccessLogNow(tx *gorm.DB, tenantID entity.ID, user, content string) error {
	return AddAccessLog(tx, tenantID, user, content, time.Now().Unix())
}

func AddAccessLog(tx *gorm.DB, tenantID entity.ID, user string, content string, happen int64) error {
	accessLog := entity.AuditLog{
		TenantID: tenantID,
		User:     user,
		Content:  content,
		Base: entity.Base{
			CreatedAt: time.Unix(happen, 0),
		},
	}
	return errors.Annotate(tx.Create(&accessLog).Error, "add access log failed")
}
