package backup

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/pingcap/dbaas/central/db"
	"github.com/pingcap/dbaas/central/db/dbutil"
	"github.com/pingcap/dbaas/central/db/entity"
	infraapi "github.com/pingcap/dbaas/central/infra-api"
	"github.com/pingcap/dbaas/central/model/util"
	"github.com/pingcap/errors"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/tracing"
	"go.opentelemetry.io/otel/attribute"
	"go.uber.org/multierr"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

func (c BgContext) restoreClusterBackups(ctx context.Context, cluster entity.Cluster) error {
	var (
		restores []entity.Restore
		err      error
	)
	err = db.Transaction(func(tx *gorm.DB) error {
		return entity.NewRestoreQuerySet(tx).ClusterIDEq(cluster.ID).StatusEq(entity.RestoreStatus_PENDING).All(&restores)
	})
	if err != nil {
		return err
	}
	if len(restores) == 0 {
		return nil
	}
	if cluster.Status != entity.ClusterStatusRecovering {
		c.Log().Info("cluster not ready for restore yet", zap.Uint64("clusterID", uint64(cluster.ID)))
		return nil
	}
	var errs error
	for _, restore := range restores {
		errs = multierr.Append(errs, c.restoreBackup(ctx, cluster, &restore))
	}
	return errs
}

// UpdateRestoreProgressForInfra updates the operation when the restore is completed
func (c BgContext) UpdateRestoreProgressForInfra(ctx context.Context, restoreCR *cloudv1.Restore, restore *entity.Restore) error {
	var err error
	restore.Status = util.GetRestoreStatusForInfraAPI(restoreCR)
	c.Log().Info("get restore status", zap.String("restoreName", restoreCR.GetName()), zap.String("status", string(restore.Status)))

	updateList := []entity.RestoreDBSchemaField{entity.RestoreDBSchema.Status}
	if restore.Status == entity.RestoreStatus_SUCCESS || restore.Status == entity.RestoreStatus_FAILED {
		now := time.Now()
		restore.FinishedAt = &now
		updateList = append(updateList, entity.RestoreDBSchema.FinishedAt)
	}
	restoreTracking := dbutil.ConvertRestoreTracking(restore)
	return db.Transaction(func(tx *gorm.DB) error {
		err = restore.Update(tx, updateList...)
		if err != nil {
			return err
		}
		return restoreTracking.Create(tx)
	})
}

func (c BgContext) restoreBackup(ctx context.Context, cluster entity.Cluster, restore *entity.Restore) error {

	var (
		err           error
		backup        *entity.Backup
		backupCluster entity.Cluster
	)
	err = db.Transaction(func(tx *gorm.DB) error {
		backup, err = dbutil.GetBackupByIDForWorker(tx, restore.BackupID)
		if err != nil {
			return err
		}
		if backup.Status != entity.BackupStatus_SUCCESS {
			return errors.Errorf("backup %d is not success, status %s", backup.ID, backup.Status)
		}
		err = entity.NewClusterQuerySet(dbutil.PreloadCluster(tx)).
			IDEq(backup.BackupSchedule.ClusterID).
			One(&backupCluster)
		if err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return err
	}

	c.Log().Debug("restoring from backup to new cluster", zap.String("backupName", backup.BackupSchedule.Name), zap.Uint64("backupID", uint64(backup.ID)), zap.String("restoreName", restore.Name), zap.Uint64("restoreID", uint64(restore.ID)), zap.Uint64("clusterID", uint64(restore.ClusterID)))

	restoreName := restoreCRDName(restore)

	err = c.infraAPI.CreateRestoreIgnoreExists(context.TODO(),
		cluster.TenantID.Uint64(), cluster.ProjectID.Uint64(), cluster.ID.Uint64(), infraapi.ClusterName(cluster.ID.Uint64()), backupCRDName(backup), restoreName)
	if err != nil {
		return errors.Annotate(err, "create restore CR failed")
	}
	// hack for UpdateRestoreProgress, it should always be Running when after created
	restoreCR := &cloudv1.Restore{
		Status: cloudv1.RestoreStatus{Phase: cloudv1.RestoreRunning},
	}

	c.Log().Debug("restored k8s backup", zap.String("backupName", backup.BackupSchedule.Name), zap.Uint64("backupID", uint64(backup.ID)))
	return errors.Annotate(c.UpdateRestoreProgressForInfra(ctx, restoreCR, restore), "save restore operation")
}

func restoreCRDName(restore *entity.Restore) string {
	return fmt.Sprintf("%s-%d", strings.ToLower(restore.Name), restore.ID)
}

// RestoreBackups creates a k8s job for all restore records
func (c BgContext) RestoreBackups(ctx context.Context) (err error) {
	ctx, span := Tracer.Start(ctx, "BgContext.RestoreBackups")
	defer func() { tracing.EndSpan(span, err) }()
	var (
		clusters      []entity.Cluster
		restoreStatus entity.ClusterStatus = entity.ClusterStatusRecovering
		logger                             = c.Log().With(log.FieldsFromContext(ctx)...)
	)
	tenantID := c.tenantAndUser.TenantID
	err = db.Transaction(func(tx *gorm.DB) error {
		clusters, err = dbutil.FindClusters(tx, dbutil.ClusterFilter{
			TenantID: &tenantID,
			Status:   &restoreStatus,
		})
		return err
	})
	if err != nil {
		logger.Error("failed to get recovering clusters", zap.Error(err))
		return errors.Annotate(err, "get recovering clusters")
	}
	span.AddEvent("FindClusters")

	var errs error
	for _, cluster := range clusters {
		errs = multierr.Append(errs, c.restoreClusterBackups(ctx, cluster))
	}
	span.AddEvent("restoreClusterBackups")
	for _, cluster := range clusters {
		var (
			restores []entity.Restore
		)
		err = db.Transaction(func(tx *gorm.DB) error {
			// Fetch the running restores from database.
			return entity.NewRestoreQuerySet(tx).
				ClusterIDEq(cluster.ID).
				PreloadCluster().
				All(&restores)
		})
		if err != nil {
			logger.Error("operate db failed", zap.Uint64("clusterID", uint64(cluster.ID)), zap.Error(err))
			errs = multierr.Append(errs, err)
			continue
		}
		_, childSpan := Tracer.Start(ctx, "UpdateClusterRestore")
		childSpan.SetAttributes(attribute.String("clusterID", strconv.FormatUint(uint64(cluster.ID), 10)))
		for _, restore := range restores {
			if restore.Status == entity.RestoreStatus_RUNNING || restore.Status == entity.RestoreStatus_PENDING {
				restoreCR, err := c.infraAPI.GetRestore(context.TODO(), cluster.ProjectID.Uint64(), restoreCRDName(&restore))
				if err != nil {
					errs = multierr.Append(errs, err)
				} else {
					errs = multierr.Append(errs, c.UpdateRestoreProgressForInfra(ctx, restoreCR, &restore))
				}
			}
			if restore.Status == entity.RestoreStatus_SUCCESS {
				errs = multierr.Append(errs, db.Transaction(func(tx *gorm.DB) error {
					return dbutil.ClusterUpdateStatus(tx, cluster.ID, entity.ClusterStatusNormal)
				}))

				if cluster.Free {
					err = db.Transaction(func(tx *gorm.DB) error {
						return dbutil.DeleteOldDevTierClustersBackups(ctx, tx, tenantID, cluster.ID)
					})
					if err != nil {
						errs = multierr.Append(errs, errors.AddStack(err))
					}
				}

				errs = multierr.Append(errs, c.infraAPI.DeleteRestoreIgnoreNotFound(context.TODO(), cluster.ProjectID.Uint64(), restoreCRDName(&restore)))
			} else {
				errs = multierr.Append(errs, db.Transaction(func(tx *gorm.DB) error {
					return dbutil.ClusterUpdateStatus(tx, cluster.ID, entity.ClusterStatusRecovering)
				}))
			}
		}
		childSpan.End()
	}
	if errs != nil {
		logger.Error("restore failed", zap.Error(errs))
	}
	return errs
}
