// Package backup encapsulates all backup functionality
package backup

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

	"github.com/pingcap/dbaas/central/config"
	"github.com/pingcap/dbaas/central/db"
	"github.com/pingcap/dbaas/central/db/dbutil"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/dbaas/central/model/util"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/pkgv2/log"
	"go.uber.org/multierr"
	"go.uber.org/zap"
	"gorm.io/gorm"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
)

func backupCRDName(backup *entity.Backup) string {
	return util.BackupCRDName(backup)
}

func (c BgContext) finalBackupSucceed(ctx context.Context, tx *gorm.DB, clusterID, tenantID entity.ID) (bool, error) {
	backupSchedule := entity.BackupSchedule{}

	systemUser, err := dbutil.SystemUser(tx, tenantID)
	if err != nil {
		return false, err
	}

	err = entity.NewBackupScheduleQuerySet(tx).NameEq(config.FinalBackupName).TypeEq(entity.BackupType_Manual).ClusterIDEq(clusterID).UserIDEq(systemUser.ID).One(&backupSchedule)
	if err != nil {
		return false, err
	}

	count, err := entity.NewBackupQuerySet(tx).BackupScheduleIDEq(backupSchedule.ID).StatusEq(entity.BackupStatus_SUCCESS).Count()
	if err != nil {
		return false, err
	}

	if count > 0 {
		return true, nil
	}

	return false, nil
}

func (c BgContext) CleanupBackupRestoreJobs(ctx context.Context) error {
	logger := c.Log().With(log.FieldsFromContext(ctx)...)
	notificationMgr := util.NewNotificationMgr()
	dbProvider := db.NewProvider()
	org, err := c.Model.tenantModel.User(ctx, c.tenantAndUser).GetOrganization()
	if err != nil {
		c.Log().Error("failed to GetOrganization", zap.Error(err))
		return err
	}
	t, err := c.Model.tenantModel.User(ctx, c.tenantAndUser).GetTenant()
	if err != nil {
		return err
	}
	plan := entity.TenantPlan(strings.ToLower(org.Plan))
	orgID := c.tenantAndUser.TenantID

	var clusters []entity.Cluster
	err = db.Transaction(func(tx *gorm.DB) error {
		clusters, err = dbutil.GetClustersForCleanupBackupRestore(tx, orgID)
		return err
	})
	if err != nil {
		return errors.Annotate(err, "fetch deleted no cleaned up")
	}
	var errs error
	for _, cluster := range clusters {
		var backups []entity.Backup
		// TODO: put more logic into one transaction.
		err = db.Transaction(func(tx *gorm.DB) error {
			backups, err = dbutil.BackupsFetchNotCleanedUp(tx, cluster.ID)
			return err
		})
		if err != nil {
			errs = multierr.Append(errs, errors.Annotatef(err, "get backup in progress for cluster %d", cluster.ID))
			continue
		}

		if cluster.Status == entity.ClusterStatusDeletecomplete {
			infraCluster, err := c.infraAPI.GetCluster(context.TODO(), cluster.ProjectID.Uint64(), cluster.ID.Uint64())
			if err != nil {
				if !apierrors.IsNotFound(err) {
					errs = multierr.Append(errs, errors.Annotatef(err, "error occurs when get infra cluster %d", cluster.ID))
					continue
				}
			} else {
				if infraCluster.DeletionTimestamp == nil {
					errs = multierr.Append(errs, errors.Errorf("infra cluster is not deleted cluster %d", cluster.ID))
					continue
				}
			}

			if len(backups) == 0 {
				errUpdate := db.Transaction(func(tx *gorm.DB) error {
					logger.Info("change cluster status to cleared", zap.Uint64("cluster", cluster.ID.Uint64()))
					return dbutil.ClusterUpdateStatus(tx, cluster.ID, entity.ClusterStatusCleared)
				})
				if errUpdate != nil {
					errs = multierr.Append(errs, errors.Annotatef(errUpdate, "failed to update status to cleared for cluster %d", cluster.ID))
					continue
				}
			}

			if plan == entity.TenantPlanPOC && !cluster.Free {
				// support config by SRE team
				// poc manual backup reserved time default to 10 days after points run out
				// poc auto backup reserved time default to 7 days
				pocManualBackupReservedDays := time.Duration(util.GetPocManualBackupReservedDays())
				pocAutoBackupReservedDays := time.Duration(util.GetPocAutoBackupReservedDays())

				var (
					pocTrialEndedAt *time.Time
					err             error
				)
				err = db.Transaction(func(tx *gorm.DB) error {
					pocTrialEndedAt, err = dbutil.GetPOCTrialEndedAt(ctx, tx, org.ID)
					return err
				})
				if err != nil {
					errs = multierr.Append(errs, err)
					continue
				}
				// poc has ended
				if pocTrialEndedAt != nil && org.RemainingCredit == 0 {
					now := time.Now()
					manualBackupsWillDeletedAt := pocTrialEndedAt.Add(pocManualBackupReservedDays * 24 * time.Hour)
					// the reminder of manual backups will be deleted
					// send an email 48 hours before manual backup been deleted
					if manualBackupsWillDeletedAt.Add(-48 * time.Hour).Before(now) {
						tx := dbProvider.WriteDB(ctx)
						var user entity.User
						user, err = dbutil.GetUserByEmail(tx, org.Email)
						if err != nil {
							c.Log().Error("failed to get admin user of tenant", zap.Error(err))
							return err
						}
						endTime := util.ConvertUtcTime(manualBackupsWillDeletedAt, org.Timezone)

						if err := notificationMgr.SendNotificationOnce(ctx, tx, org.ID, entity.NotificationScenePocEndBackupWillDelete, string(entity.NotificationScenePocEndBackupWillDelete), func() error {
							// notify user
							vars4User := map[string]string{
								"userName": user.FirstName,
								"endAt":    endTime,
							}
							if err := c.mailFacade.SendMailWithTemplate(ctx, []string{org.Email}, config.EmailSubjectPocRetrieveDataBeforeDeleted, config.TNPocRetrieveDataBeforeDeleted, vars4User, nil); err != nil {
								return err
							}

							// notify ops/account-manager that customer's poc manual backups will be deleted
							vars := map[string]string{
								"now":     time.Now().Format(config.TimeFmtDTM),
								"orgName": org.Name,
								"orgId":   fmt.Sprint(org.ID),
								"endTime": endTime,
							}
							return c.mailFacade.SendMailWithTemplate(ctx, util.GetPocStatusUpdateRelatedManagerEmails(ctx, t), config.CustomerStatusUpdateSubject, config.TNCustomerUpdatePocEndBackupWillDeleted, vars, nil)
						}); err != nil {
							return err
						}
					}

					// clean manual backups
					if manualBackupsWillDeletedAt.Before(now) {
						errDel := db.Transaction(func(tx *gorm.DB) error {
							// we'll clean up manual backups only if the final manual backup had already succeed
							if cluster.FinalBackupBeforeDelete {
								succeed, err := c.finalBackupSucceed(ctx, tx, cluster.ID, orgID)
								if err != nil {
									c.Log().Error("failed to find succeed final manual backup", zap.Error(err))
									return err
								}

								if !succeed {
									return errors.New("final manual backup not found, reject to delete all backups")
								}
							}

							return dbutil.DeleteDeletedClusterBackups(tx, orgID, cluster.ProjectID, cluster.ID, entity.BackupType_Manual)
						})
						if errDel != nil {
							errs = multierr.Append(errs, errors.Annotatef(errDel, "delete all expired manual backups of cluster %d", cluster.ID))
							continue
						}
					}
				}

				if cluster.DeletedTime.Add(pocAutoBackupReservedDays * 24 * time.Hour).Before(time.Now()) {
					errDel := db.Transaction(func(tx *gorm.DB) error {
						return dbutil.DeleteDeletedClusterBackups(tx, orgID, cluster.ProjectID, cluster.ID, entity.BackupType_Auto)
					})
					if errDel != nil {
						errs = multierr.Append(errs, errors.Annotatef(errDel, "delete all expired auto backups of cluster %d", cluster.ID))
						continue
					}
				}
			}

			if plan == entity.TenantPlanOnDemand {
				// support config by SRE team
				// onDemand auto backup reserved time defatult to 7 days
				onDemandAutoBackupReservedDays := time.Duration(util.GetOnDemandAutoBackupReservedDays())
				if cluster.DeletedTime.Add(onDemandAutoBackupReservedDays * 24 * time.Hour).Before(time.Now()) {
					errDel := db.Transaction(func(tx *gorm.DB) error {
						return dbutil.DeleteDeletedClusterBackups(tx, orgID, cluster.ProjectID, cluster.ID, entity.BackupType_Auto)
					})
					if errDel != nil {
						errs = multierr.Append(errs, errors.Annotatef(errDel, "delete all expired auto backups of cluster %d", cluster.ID))
						continue
					}
				}
			}

			var restores []entity.Restore
			errRestore := db.Transaction(func(tx *gorm.DB) error {
				restores, err = dbutil.RestoresFetchAll(tx, cluster.ID)
				return err
			})
			if errRestore != nil {
				errs = multierr.Append(errs, errors.Annotatef(errRestore, "get restore in progress for cluster %d", cluster.ID))
				continue
			}
			for _, restore := range restores {
				errDel := c.infraAPI.DeleteRestoreIgnoreNotFound(context.TODO(), cluster.ProjectID.Uint64(), restoreCRDName(&restore))
				if errDel != nil {
					errs = multierr.Append(errs, errors.Annotatef(errDel, "delete restore CRD %d for cluster %d", restore.ID, cluster.ID))
				}
			}
		}
	}
	return errs
}

// ScheduleBackups looks at cluster backup settings and creates backup records accordingly
func (c BgContext) ScheduleBackups(ctx context.Context) error {
	var (
		clusters []entity.Cluster
		err      error
	)
	err = db.Transaction(func(tx *gorm.DB) error {
		clusters, err = dbutil.GetDeployedCluster(tx, c.tenantAndUser.TenantID)
		return err
	})
	if err != nil {
		return errors.Annotate(err, "Deployed clusters")
	}

	var errs error
	for _, cluster := range clusters {
		err = db.Transaction(func(tx *gorm.DB) error {
			return dbutil.ScheduleBackup(ctx, tx, cluster)
		})
		if err != nil {
			errs = multierr.Append(errs, err)
		}
		errs = multierr.Append(errs, c.deleteOldScheduledBackups(ctx, &cluster))
	}
	return errs
}

func (c BgContext) deleteOldScheduledBackups(ctx context.Context, cluster *entity.Cluster) error {
	var (
		backups []entity.Backup
		err     error
		logger  = c.Log().With(log.FieldsFromContext(ctx)...)
	)
	err = db.Transaction(func(tx *gorm.DB) error {
		backups, err = dbutil.OldScheduledBackup(tx, cluster.ID)
		return err
	})
	if err != nil {
		return err
	}
	if len(backups) == 0 {
		return nil
	}

	var errs error
	for _, backup := range backups {
		logger.Info("deleting old scheduled backup for cluster", zap.Uint64("backupID", uint64(backup.ID)), zap.Uint64("clusterID", cluster.ID.Uint64()))
		if err = db.Transaction(func(tx *gorm.DB) error {
			return dbutil.DeleteBackup(tx, c.tenantAndUser.TenantID, cluster.ProjectID, cluster.ID, backup.ID)
		}); err != nil {
			errs = multierr.Append(errs, err)
		}
	}
	return errs
}
