package dbutil

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

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

type CreateBackupRequired struct {
	Name                  string
	Description           string
	ClusterID             entity.ID
	MaxBackups            uint64
	Schedule              string
	Version               string
	ClusterConfigurations entity.ClusterConfigurations
	Method                entity.BackupMethod
	Type                  entity.BackupType
	Targets               string
	PDList                entity.IDList
	TiDBList              entity.IDList
	TiKVList              entity.IDList
	TiFlashList           entity.IDList
}

// fetches the entities of a QuerySet
func BackupScheduleFetch(qs entity.BackupScheduleQuerySet) ([]entity.BackupSchedule, error) {
	var backupSchedules []entity.BackupSchedule
	err := qs.All(&backupSchedules)
	return backupSchedules, errors.Annotate(err, "Fetch BackupSchedules")
}

// fetches the entities of a QuerySet
func BackupFetch(qs entity.BackupQuerySet) ([]entity.Backup, error) {
	var backups []entity.Backup
	err := qs.All(&backups)
	return backups, errors.Annotate(err, "Fetch Backups")
}

func ListBackupSchedule(tx *gorm.DB, clusterID entity.ID) ([]entity.BackupSchedule, error) {
	return BackupScheduleFetch(ClusterBackupSchedules(tx, clusterID).OrderDescByID())
}

func ClusterBackups(tx *gorm.DB, clusterID entity.ID) ([]entity.Backup, error) {
	var backups []entity.Backup
	backupSchedules, err := BackupScheduleFetch(ClusterBackupSchedules(tx, clusterID))
	if err != nil {
		return nil, err
	}
	for _, schedule := range backupSchedules {
		s := schedule
		backupList, err := BackupFetch(ScheduleBackups(tx, schedule.ID))
		if err != nil {
			return nil, errors.Annotate(err, "Backups cluster query")
		}
		for _, backup := range backupList {
			backup.BackupSchedule = &s
			backup.BackupScheduleID = s.ID
			backups = append(backups, backup)
		}
	}
	return backups, nil
}

func BackupsInProgress(tx *gorm.DB, clusterID entity.ID) ([]entity.Backup, error) {
	var inProgress []entity.Backup
	backupSchedules, err := BackupScheduleFetch(ClusterBackupSchedules(tx, clusterID))
	if err != nil {
		return nil, err
	}
	for _, schedule := range backupSchedules {
		s := schedule
		backups, err := BackupFetch(ScheduleBackups(tx, schedule.ID))
		if err != nil {
			return nil, errors.Annotate(err, "Backups in progress query")
		}
		for _, backup := range backups {
			if backup.Status == "" || backup.Status == entity.BackupStatus_RUNNING || backup.Status == entity.BackupStatus_PENDING {
				backup.BackupSchedule = &s
				backup.BackupScheduleID = s.ID
				inProgress = append(inProgress, backup)
			}
		}
	}
	return inProgress, nil
}

func BackupsFetchNotCleanedUp(tx *gorm.DB, clusterID entity.ID) ([]entity.Backup, error) {
	var (
		err             error
		backups         []entity.Backup
		notCleanup      []entity.Backup
		backupSchedules []entity.BackupSchedule
	)
	err = entity.NewBackupScheduleQuerySet(tx).ClusterIDEq(clusterID).PreloadCluster().All(&backupSchedules)
	if err != nil {
		return nil, err
	}
	for _, schedule := range backupSchedules {
		s := schedule
		err := entity.NewBackupQuerySet(tx).BackupScheduleIDEq(schedule.ID).All(&backups)
		if err != nil {
			return nil, errors.Annotate(err, "Backups in progress query")
		}
		for _, backup := range backups {
			if backup.CleanedUp == nil || !*backup.CleanedUp {
				backup.BackupSchedule = &s
				backup.BackupScheduleID = s.ID
				notCleanup = append(notCleanup, backup)
			}
		}
	}
	return notCleanup, nil
}

func NextCanBackup(tx *gorm.DB, clusterID entity.ID) (*entity.Backup, error) {
	var nextCanBackup *entity.Backup
	var tmpBackup entity.Backup
	backupSchedules, err := BackupScheduleFetch(ClusterBackupSchedules(tx, clusterID))
	if err != nil {
		return nil, err
	}
	nextCanBackup = nil
	for _, schedule := range backupSchedules {
		s := schedule
		backups, err := BackupFetch(ScheduleBackups(tx, schedule.ID))
		if err != nil {
			return nil, err
		}
		for _, backup := range backups {
			if backup.Status == "" || backup.Status == entity.BackupStatus_PENDING {
				if nextCanBackup == nil || nextCanBackup.CreatedAt.After(backup.CreatedAt) {
					backup.BackupSchedule = &s
					backup.BackupScheduleID = s.ID
					tmpBackup = backup
					nextCanBackup = &tmpBackup
				}
			} else if backup.Status == entity.BackupStatus_RUNNING {
				return nil, nil
			}
		}
	}
	return nextCanBackup, nil
}

func AssembleComponentList(cluster entity.Cluster) (entity.IDList, entity.IDList, entity.IDList, entity.IDList) {
	var pdList, tidbList, tikvList, tiflashList entity.IDList
	for _, component := range cluster.Components {
		for _, machine := range component.ComponentMachines {
			if component.ComponentName == entity.PD && machine.Status != entity.DeletedComponent {
				pdList.IDs = append(pdList.IDs, machine.ID)
			} else if component.ComponentName == entity.TiDB && machine.Status != entity.DeletedComponent {
				tidbList.IDs = append(tidbList.IDs, machine.ID)
			} else if component.ComponentName == entity.TiKV && machine.Status != entity.DeletedComponent {
				tikvList.IDs = append(tikvList.IDs, machine.ID)
			} else if component.ComponentName == entity.TiFlash && machine.Status != entity.DeletedComponent {
				tiflashList.IDs = append(tiflashList.IDs, machine.ID)
			}
		}
	}
	return pdList, tidbList, tikvList, tiflashList
}

func ConvertBackup(schedule entity.BackupSchedule, cluster entity.Cluster) *entity.Backup {
	pdList, tidbList, tikvList, tiflashList := AssembleComponentList(cluster)
	return &entity.Backup{
		BackupScheduleID:         schedule.ID,
		Targets:                  schedule.Targets,
		Status:                   entity.BackupStatus_PENDING,
		CleanedUp:                pointer.BoolPtr(false),
		PdComponentMachines:      pdList,
		TidbComponentMachines:    tidbList,
		TikvComponentMachines:    tikvList,
		TiflashComponentMachines: tiflashList,
	}
}

func ConvertBackupTracking(backup *entity.Backup) *entity.BackupTracking {
	return &entity.BackupTracking{
		BackupID:    backup.ID,
		Status:      backup.Status,
		Description: "",
	}
}

func CreateBackup(tx *gorm.DB, schedule entity.BackupSchedule, cluster entity.Cluster) error {
	backup := ConvertBackup(schedule, cluster)
	err := schedule.Update(tx, entity.BackupScheduleDBSchema.NextBackupTime)
	if err != nil {
		return err
	}
	err = backup.Create(tx)
	if err != nil {
		return err
	}
	backupTracking := ConvertBackupTracking(backup)
	return backupTracking.Create(tx)
}

func UpdateBackup(tx *gorm.DB, backup *entity.Backup) error {
	backupTracking := ConvertBackupTracking(backup)
	err := backup.Update(tx, entity.BackupDBSchema.Status, entity.BackupDBSchema.FinishedAt)
	if err != nil {
		return err
	}
	return backupTracking.Create(tx)
}

// ScheduleBackup schedules a backup via the backup schedule
// if all condition is prepared, insert a job to backup table
func ScheduleBackup(tx *gorm.DB, cluster entity.Cluster) error {
	now := time.Now()
	backupSchedules, err := BackupScheduleFetch(ClusterAutoBackupSchedules(tx, cluster.ID))
	if err != nil {
		return err
	}
	for _, schedule := range backupSchedules {
		if schedule.NextBackupTime == nil || now.After(*schedule.NextBackupTime) {
			expr := cronexpr.MustParse(schedule.Schedule)
			nextTime := expr.Next(now)
			if schedule.NextBackupTime != nil {
				schedule.NextBackupTime = &nextTime
				err = CreateBackup(tx, schedule, cluster)
				if err != nil {
					return err
				}
			} else {
				schedule.NextBackupTime = &nextTime
				err := schedule.Update(tx, entity.BackupScheduleDBSchema.NextBackupTime)
				if err != nil {
					return err
				}
			}
		}
	}
	return nil
}

func CreateBackupSchedule(tx *gorm.DB, userID, clusterID entity.ID) (entity.BackupSchedule, error) {
	scheduleBackup := entity.BackupSchedule{
		Name:        fmt.Sprintf("ab-%d", clusterID),
		Type:        entity.BackupType_Auto,
		ClusterID:   clusterID,
		MaxBackups:  7,
		Schedule:    "0 00 02 1/1 * ? *",
		Targets:     "",
		UserID:      userID,
		Method:      entity.BackupMethod_Full,
		Description: "",
	}
	if err := scheduleBackup.Create(tx); err != nil {
		return scheduleBackup, errors.Annotate(err, "failed to create cluster backup schedule")
	}
	return scheduleBackup, nil
}

func UpsertBackupSchedule(tx *gorm.DB, cluster entity.Cluster, newSchedule *entity.BackupSchedule, change func(oldBS, newBS *entity.BackupSchedule) bool) (*entity.BackupSchedule, error) {
	schedule, err := GetBackupAutoSchedule(tx, cluster.ID)
	if err != nil {
		var err2 error
		if nil != errors.Find(err, func(e error) bool { return e == gorm.ErrRecordNotFound }) {
			schedule, err2 = CreateBackupSchedule(tx, cluster.CreatedByID, cluster.ID)
			if err2 != nil {
				return nil, err
			}
		} else {
			return nil, err
		}
	}
	if !change(&schedule, newSchedule) {
		// If the backup settings are not modified, do nothing
		return &schedule, nil
	}
	schedule.Method = newSchedule.Method
	schedule.Schedule = newSchedule.Schedule
	schedule.MaxBackups = newSchedule.MaxBackups
	schedule.NextBackupTime = nil
	return &schedule, schedule.Update(
		tx,
		entity.BackupScheduleDBSchema.Method,
		entity.BackupScheduleDBSchema.Schedule,
		entity.BackupScheduleDBSchema.MaxBackups,
		entity.BackupScheduleDBSchema.NextBackupTime)
}

func UpdateBackupCleanedUp(tx *gorm.DB, backup entity.Backup, cleanedUp bool) error {
	backup.CleanedUp = &cleanedUp
	return backup.Update(tx, entity.BackupDBSchema.CleanedUp)
}

func DeleteBackup(tx *gorm.DB, orgID, projectID, clusterID, backupID entity.ID) error {
	backup, err := GetBackupByID(tx, orgID, projectID, clusterID, backupID)
	if err != nil {
		return err
	}
	now := time.Now()
	return tx.Model(backup).Update("deleted_at", now).Error
}

func DeleteDeletedClusterBackups(tx *gorm.DB, orgID entity.ID, projectID entity.ID, clusterID entity.ID, backupType entity.BackupType) error {
	var backupSchedules []entity.BackupSchedule
	var err error
	if backupType == entity.BackupType_All {
		backupSchedules, err = BackupScheduleFetch(ClusterBackupSchedules(tx, clusterID))
		if err != nil {
			return err
		}
	} else {
		backupSchedules, err = BackupScheduleFetch(ClusterBackupSchedules(tx, clusterID).TypeEq(backupType))
		if err != nil {
			return err
		}
	}
	for _, schedule := range backupSchedules {
		backups, err := BackupFetch(ScheduleBackups(tx, schedule.ID))
		if err != nil {
			return errors.Annotate(err, "backups in progress query")
		}
		for _, backup := range backups {
			if backup.DeletedAt == nil {
				if err = DeleteBackup(tx, orgID, projectID, clusterID, backup.ID); err != nil {
					return errors.Annotate(err, fmt.Sprintf("failed to delete backup %d", backup.ID))
				}
			}
		}
	}

	return nil
}

// OldScheduledBackup returns an old scheduled backup that should be deleted if it exists.
func OldScheduledBackup(tx *gorm.DB, clusterID entity.ID) ([]entity.Backup, error) {
	backupSchedules, err := BackupScheduleFetch(ClusterAutoBackupSchedules(tx, clusterID))
	if err != nil {
		return nil, err
	}
	if len(backupSchedules) == 0 {
		return nil, nil
	}
	schedule := backupSchedules[0]
	var backups []entity.Backup
	getBackups := entity.NewBackupQuerySet(tx).DeletedAtIsNull().BackupScheduleIDEq(schedule.ID)
	if err := getBackups.OrderDescByCreatedAt().All(&backups); err != nil {
		return nil, err
	}
	if len(backups) > int(schedule.MaxBackups) {
		return backups[schedule.MaxBackups:], nil
	}
	return nil, nil
}

func GetBackupByIDForWorker(tx *gorm.DB, id entity.ID) (*entity.Backup, error) {
	var backup entity.Backup
	err := entity.NewBackupQuerySet(tx).PreloadBackupSchedule().IDEq(id).One(&backup)
	return &backup, errors.Annotate(TagNotFound(err), "GetBackupByIDForWorker")
}

// GetBackupByID：used by service API
func GetBackupByID(tx *gorm.DB, orgID, projectID, clusterID, id entity.ID) (*entity.Backup, error) {
	var backup entity.Backup
	err := entity.NewBackupQuerySet(tx.Preload("BackupSchedule.Cluster")).PreloadBackupSchedule().IDEq(id).One(&backup)
	if err != nil {
		return &backup, errors.Annotate(TagNotFound(err), "GetBackupByID")
	}
	err = CheckResourceOrgIDAndProjectID(orgID, backup.BackupSchedule.Cluster.TenantID, projectID, backup.BackupSchedule.Cluster.ProjectID)
	if err != nil {
		return &backup, err
	}
	if backup.BackupSchedule.ClusterID != clusterID {
		return &backup, errors.Errorf("cluster %d does not have backup %d", clusterID, id)
	}
	return &backup, nil
}
