package dbutil

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

	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/tidbcloud/metrics-server/central/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(ctx context.Context, tx *gorm.DB, cluster entity.Cluster) (*entity.Region, error) {
	regions, err := GetClusterRegionsV2(ctx, 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 entity.Cloud, tenantID, projectID entity.ID) ([]entity.Cluster, error) {
	return FindClusters(tx, ClusterFilter{
		Deleted:      pointer.BoolPtr(true),
		TenantID:     &tenantID,
		ProjectID:    &projectID,
		ProviderName: &provider,
	})
}

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