package dbutil

import (
	"strconv"
	"time"

	"github.com/pingcap/errors"
	"github.com/tidbcloud/dataflow-service/app/repo/entity"
	"gorm.io/gorm"
	"k8s.io/utils/pointer"
)

// GetLogBackupsByCluster get all not deleted log backups of the cluster
func GetLogBackupsByCluster(tx *gorm.DB, clusterID entity.ID) ([]entity.LogBackup, error) {
	var logBackups []entity.LogBackup
	err := entity.NewLogBackupQuerySet(tx).
		ClusterIDEq(clusterID).
		DeletedAtIsNull().
		PreloadBackupSchedule().
		OrderDescByCreatedAt().
		All(&logBackups)
	return logBackups, err
}

// GetActiveLogBackupByCluster get the not deleted and not stopped log backup of the cluster
func GetActiveLogBackupByCluster(tx *gorm.DB, clusterID entity.ID) (*entity.LogBackup, error) {
	logBackup := new(entity.LogBackup)
	err := entity.NewLogBackupQuerySet(tx).
		ClusterIDEq(clusterID).
		DeletedAtIsNull().
		StatusNotIn(entity.LogBackupStatusStopped).
		PreloadBackupSchedule().
		OrderDescByStartTS().
		One(logBackup)
	return logBackup, err
}

// GetLogBackupByID get the log backup
func GetLogBackupByID(tx *gorm.DB, logBackupID entity.ID) (*entity.LogBackup, error) {
	logBackup := new(entity.LogBackup)
	err := entity.NewLogBackupQuerySet(tx).IDEq(logBackupID).PreloadBackupSchedule().One(logBackup)
	return logBackup, err
}

// GetLatestLogBackupByScheduleID get the log backup of the backup schedule
func GetLatestLogBackupByScheduleID(tx *gorm.DB, backupScheduleID entity.ID) (*entity.LogBackup, error) {
	logBackup := new(entity.LogBackup)
	err := entity.NewLogBackupQuerySet(tx).
		BackupScheduleIDEq(backupScheduleID).
		StatusEq(entity.LogBackupStatusRunning).
		OrderDescByCreatedAt().
		One(logBackup)
	return logBackup, err
}

// GetReservedLogBackupsByScheduleID get the log backup of the backup schedule and order by startTS asc.
func GetReservedLogBackupsByScheduleID(tx *gorm.DB, backupScheduleID entity.ID) ([]entity.LogBackup, error) {
	var logBackups []entity.LogBackup
	err := entity.NewLogBackupQuerySet(tx).
		DeletedAtIsNull().
		BackupScheduleIDEq(backupScheduleID).
		OrderAscByStartTS().
		All(&logBackups)
	return logBackups, err
}

// ListActiveLogBackups list all not deleted and not stopped log backups of all clusters
func ListActiveLogBackups(tx *gorm.DB) ([]entity.LogBackup, error) {
	var logBackups []entity.LogBackup
	err := entity.NewLogBackupQuerySet(tx).
		DeletedAtIsNull().
		StatusNotIn(entity.LogBackupStatusStopped).
		PreloadBackupSchedule().
		All(&logBackups)
	return logBackups, err
}

// ListNotDeletedLogBackups list all not deleted log backups of all clusters
func ListNotDeletedLogBackups(tx *gorm.DB) ([]entity.LogBackup, error) {
	var logBackups []entity.LogBackup
	err := entity.NewLogBackupQuerySet(tx).
		DeletedAtIsNull().
		PreloadBackupSchedule().
		All(&logBackups)
	return logBackups, err
}

func ListAllLogBackups(tx *gorm.DB) ([]entity.LogBackup, error) {
	var logBackups []entity.LogBackup
	err := entity.NewLogBackupQuerySet(tx).All(&logBackups)
	return logBackups, err
}

// CreateLogBackup create a log backup record
func CreateLogBackup(tx *gorm.DB, logBackup *entity.LogBackup) error {
	return logBackup.Create(tx)
}

// UpdateLogBackup update all log backup fields by ID
func UpdateLogBackup(tx *gorm.DB, logBackup *entity.LogBackup) error {
	return tx.Save(logBackup).Error
}

// StopLogBackup mark the log backup to stopping status
// it will check whether it can be stopped
func StopLogBackup(tx *gorm.DB, logBackupID entity.ID) error {
	var logBackup entity.LogBackup
	if err := entity.NewLogBackupQuerySet(tx).IDEq(logBackupID).One(&logBackup); err != nil {
		return errors.Annotate(err, "get log backup")
	}
	if logBackup.Status != entity.LogBackupStatusScheduled && logBackup.Status != entity.LogBackupStatusPending && logBackup.Status != entity.LogBackupStatusRunning && logBackup.Status != entity.LogBackupStatusPaused && logBackup.Status != entity.LogBackupStatusFailed {
		return errors.New("do not allow to stop log backup by checking status")
	}
	if logBackup.Status == entity.LogBackupStatusScheduled || logBackup.Status == entity.LogBackupStatusFailed {
		logBackup.Status = entity.LogBackupStatusStopped
	} else {
		logBackup.Status = entity.LogBackupStatusStopping
		now := time.Now()
		logBackup.StoppingAt = &now
	}
	if err := logBackup.Update(tx, entity.LogBackupDBSchema.Status, entity.LogBackupDBSchema.StoppingAt); err != nil {
		return errors.Annotate(err, "update log backup status to stopping")
	}
	return nil
}

func MarkLogBackupAsStopped(tx *gorm.DB, logBackup *entity.LogBackup) error {
	if logBackup.Status == entity.LogBackupStatusStopped {
		return nil
	}
	logBackup.Status = entity.LogBackupStatusStopped
	return logBackup.Update(tx, entity.LogBackupDBSchema.Status)
}

// DeleteLogBackup mark the log backup to be deleted
// it will check whether it can be deleted
func DeleteLogBackup(tx *gorm.DB, logBackupID entity.ID) error {
	var logBackup entity.LogBackup
	if err := entity.NewLogBackupQuerySet(tx).IDEq(logBackupID).One(&logBackup); err != nil {
		return errors.Annotate(err, "get log backup")
	}
	if logBackup.Status != entity.LogBackupStatusStopped {
		return errors.New("only allow to delete stopped log backup")
	}
	if logBackup.DeletedAt != nil {
		return nil
	}
	now := time.Now()
	logBackup.DeletedAt = &now
	if err := logBackup.Update(tx, entity.LogBackupDBSchema.DeletedAt); err != nil {
		return errors.Annotate(err, "delete log backup")
	}
	return nil
}

// CreateLogBackupTruncate create a LogBackupTruncate to truncate the logBackup to truncateUntil
func CreateLogBackupTruncate(tx *gorm.DB, logBackup *entity.LogBackup, truncateUntil uint64) (*entity.LogBackupTruncate, error) {
	logBackupTruncate := &entity.LogBackupTruncate{
		LogBackupID: logBackup.ID,
		Until:       strconv.FormatUint(truncateUntil, 10),
		Status:      entity.LogBackupTruncateStatusPending,
	}
	err := logBackupTruncate.Create(tx)
	return logBackupTruncate, err
}

func MarkLogBackupTruncateRunning(tx *gorm.DB, logBackupTruncate *entity.LogBackupTruncate) error {
	if logBackupTruncate.Status != entity.LogBackupTruncateStatusPending {
		return errors.New("truncate status isn't pending")
	}
	logBackupTruncate.Status = entity.LogBackupTruncateStatusRunning
	now := time.Now()
	logBackupTruncate.RunningAt = &now
	return logBackupTruncate.Update(tx, entity.LogBackupTruncateDBSchema.Status, entity.LogBackupTruncateDBSchema.RunningAt)
}

// ListActiveLogBackupTruncateByLogBackup list all not finished truncate records of the log backup
func ListActiveLogBackupTruncateByLogBackup(tx *gorm.DB, logBackupID entity.ID) ([]entity.LogBackupTruncate, error) {
	var logBackupTruncateList []entity.LogBackupTruncate
	err := entity.NewLogBackupTruncateQuerySet(tx).
		DeletedAtIsNull().
		StatusNotIn(entity.LogBackupTruncateStatusSuccess, entity.LogBackupTruncateStatusFailed).
		LogBackupIDEq(logBackupID).
		OrderAscByCreatedAt().
		All(&logBackupTruncateList)
	return logBackupTruncateList, err
}

func ListLogBackupTruncateByStatus(tx *gorm.DB, status ...entity.LogBackupTruncateStatus) ([]entity.LogBackupTruncate, error) {
	var logBackupTruncateList []entity.LogBackupTruncate
	qs := entity.NewLogBackupTruncateQuerySet(tx).
		DeletedAtIsNull().
		OrderAscByCreatedAt()
	if len(status) > 0 {
		qs.StatusIn(status...)
	}
	err := qs.All(&logBackupTruncateList)
	return logBackupTruncateList, err
}

// FinishLogBackupTruncate mark the truncate record to success status
// it will check whether it can be set to success
func FinishLogBackupTruncate(tx *gorm.DB, logBackupTruncateID entity.ID) error {
	var logBackupTruncate entity.LogBackupTruncate
	if err := entity.NewLogBackupTruncateQuerySet(tx).IDEq(logBackupTruncateID).One(&logBackupTruncate); err != nil {
		return errors.Annotate(err, "get log backup truncate")
	}
	if logBackupTruncate.DeletedAt != nil {
		return nil
	}
	if logBackupTruncate.Status == entity.LogBackupTruncateStatusSuccess || logBackupTruncate.Status == entity.LogBackupTruncateStatusFailed {
		return nil
	}

	logBackupTruncate.Status = entity.LogBackupTruncateStatusSuccess
	if err := logBackupTruncate.Update(tx, entity.LogBackupTruncateDBSchema.Status); err != nil {
		return errors.Annotate(err, "update log backup truncate status")
	}
	return nil
}

func GetLatestLogBackupTruncate(tx *gorm.DB, logBackupID entity.ID) (*entity.LogBackupTruncate, error) {
	logBackupTruncate := new(entity.LogBackupTruncate)
	if err := entity.NewLogBackupTruncateQuerySet(tx).
		LogBackupIDEq(logBackupID).
		StatusEq(entity.LogBackupTruncateStatusSuccess).
		OrderDescByCreatedAt().
		Limit(1).
		One(logBackupTruncate); err != nil {
		return nil, err
	}
	return logBackupTruncate, nil
}

func GetLatestLogBackupSize(tx *gorm.DB, logBackupID entity.ID) (*entity.LogBackupSize, error) {
	logBackupSize := new(entity.LogBackupSize)
	if err := entity.NewLogBackupSizeQuerySet(tx).
		LogBackupIDEq(logBackupID).
		OrderDescByEndTS().
		Limit(1).
		One(logBackupSize); err != nil {
		return nil, err
	}
	return logBackupSize, nil
}

func ListLogBackupSizesBetween(tx *gorm.DB, startTime, endTime time.Time) ([]entity.LogBackupSize, error) {
	var logBackupSizes []entity.LogBackupSize
	err := entity.NewLogBackupSizeQuerySet(tx).EndTSLte(endTime).
		EndTSGt(startTime).PreloadLogBackup().All(&logBackupSizes)
	if err != nil {
		return nil, err
	}
	return logBackupSizes, nil
}

func ListNeedCleanLogBackups(tx *gorm.DB) ([]entity.LogBackup, error) {
	var logBackups []entity.LogBackup
	err := entity.NewLogBackupQuerySet(tx).
		DeletedAtIsNotNull().
		CleanedUpEq(false).
		PreloadBackupSchedule().
		OrderAscByDeletedAt().
		All(&logBackups)
	if err != nil {
		return nil, err
	}
	return logBackups, nil
}

func MarkLogBackupAsCleanedUp(tx *gorm.DB, logBackupID entity.ID) error {
	now := time.Now()
	return tx.Model(&entity.LogBackup{}).Where("id = ?", logBackupID).Updates(entity.LogBackup{
		CleanedUp: pointer.BoolPtr(true),
		Updatable: entity.Updatable{
			UpdatedAt: &now,
		},
	}).Error
}
