package dbutil

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

	"github.com/gorhill/cronexpr"
	"github.com/pingcap/dbaas/central/config"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/dbaas/central/grpc/grpcli"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/pkgv2/tracing"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	"gorm.io/gorm"
	"k8s.io/utils/pointer"
)

type CreateBackupRequired struct {
	Name        string
	Description string
	ClusterID   entity.ID
	MaxBackups  uint64
	Schedule    string
	Version     string
	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 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 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
}

// TODO(chenfei): change request
func GetTikvReplicas(cluster *entity.Cluster) uint32 {
	for _, component := range cluster.ComponentsV2 {
		if component.ComponentType == entity.TiKV {
			return component.Replicas
		}
	}
	return 0
}

func ConvertBackup(schedule entity.BackupSchedule, cluster entity.Cluster, snapshotID uint64) *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,
		ClusterSnapshotID: entity.ID(snapshotID),
	}
}

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

func CreateBackup(ctx context.Context, tx *gorm.DB, schedule entity.BackupSchedule, cluster entity.Cluster) (err error) {
	_, span := Tracer.Start(ctx, "CreateBackup")
	defer func() {
		if err != nil {
			span.RecordError(err)
			span.SetStatus(codes.Error, err.Error())
		}
		span.SetAttributes(attribute.String("cluster_id", strconv.FormatUint(uint64(cluster.ID), 10)))
		span.SetAttributes(attribute.String("type", string(schedule.Type)))
		span.End()
	}()
	snapshotID, berr := grpcli.CreateClusterSnapshot(ctx, cluster.ID.Uint64())
	if berr != nil {
		return berr
	}

	backup := ConvertBackup(schedule, cluster, snapshotID)
	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)
}

// ScheduleBackup schedules a backup via the backup schedule
// if all condition is prepared, insert a job to backup table
func ScheduleBackup(ctx context.Context, 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(ctx, 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
}

type AutoBackupConfig struct {
	MaxBackups uint64
	Schedule   string
}

// Backup schedule format refer to https://github.com/gorhill/cronexpr
func GetClusterMaxAutoBackupsConfig(ctx context.Context, tx *gorm.DB, cluster entity.Cluster) AutoBackupConfig {
	hour := config.DefaultAutoBackupHour
	maxBackups := config.MaxAutoBackups

	if cluster.Free {
		maxBackups = config.DevTierMaxAutoBackups
		// Avoid all dev tier clusters from starting backups at the same time
		hour = time.Now().UTC().Hour()
	} else {
		region, err := GetClusterRegion(ctx, tx, cluster)
		if err == nil {
			defaultBackupTime := region.DefaultBackupTime
			t, parseErr := time.Parse(config.RegionDefaultBackupTimeFormat, defaultBackupTime)
			if parseErr == nil {
				hour = t.UTC().Hour()
			}
		}
	}

	autoBackupConfig := AutoBackupConfig{
		MaxBackups: maxBackups,
		Schedule:   fmt.Sprintf("0 00 %02d 1/1 * ? *", hour),
	}
	return autoBackupConfig
}

func CreateBackupSchedule(tx *gorm.DB, userID, clusterID entity.ID, autoBackupConfig AutoBackupConfig) (entity.BackupSchedule, error) {
	scheduleBackup := entity.BackupSchedule{
		Name:        fmt.Sprintf("ab-%d", clusterID),
		Type:        entity.BackupType_Auto,
		ClusterID:   clusterID,
		MaxBackups:  autoBackupConfig.MaxBackups,
		Schedule:    autoBackupConfig.Schedule,
		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(ctx context.Context, tx *gorm.DB, cluster entity.Cluster, newSchedule *entity.BackupSchedule, change func(oldBS, newBS *entity.BackupSchedule) bool) (_ *entity.BackupSchedule, err error) {
	_, span := Tracer.Start(ctx, "UpsertBackupSchedule")
	defer func() { tracing.EndSpan(span, err) }()
	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 }) {
			autoBackupConfig := GetClusterMaxAutoBackupsConfig(ctx, tx, cluster)
			schedule, err2 = CreateBackupSchedule(tx, cluster.CreatedByID, cluster.ID, autoBackupConfig)
			if err2 != nil {
				return nil, err
			}
			span.AddEvent("CreateBackupSchedule")
		} 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 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 DeleteBackupsByClusterIDs(tx *gorm.DB, clusterIDs []entity.ID) error {
	schedules, err := GetBackupSchedulesByClusterIDs(tx, clusterIDs)
	if err != nil {
		return errors.Annotate(err, "get backupSchedules by clusterIds")
	}
	if len(schedules) == 0 {
		return nil
	}

	scheduleIDs := make([]entity.ID, 0, len(schedules))
	for _, each := range schedules {
		scheduleIDs = append(scheduleIDs, each.ID)
	}
	now := time.Now()
	return tx.Model(&entity.Backup{}).Where("backup_schedule_id in (?) AND deleted_at is null", scheduleIDs).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]

	// keep the maximum number of SUCCESSFUL backups
	var backups []entity.Backup
	getBackups := entity.NewBackupQuerySet(tx).DeletedAtIsNull().StatusIn(entity.BackupStatus_SUCCESS).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
}
