package configbackup

import (
	"context"
	"errors"
	"time"

	"github.com/tidbcloud/pkgv2/db"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/entity"
	repo "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/configbackup"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/infra/repoimpl/schema"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

type PolicyRepoImpl struct {
	provider db.Provider
}

func NewPolicyRepo(provider db.Provider) repo.PolicyRepo {
	return &PolicyRepoImpl{provider: provider}
}

func (r *PolicyRepoImpl) GetByClusterID(ctx context.Context, clusterID uint64) (*entity.ClusterConfigBackupPolicy, bizerr.BizError) {
	var model schema.ClusterConfigBackupPolicy
	if err := r.provider.ReadDB(ctx).Where("cluster_id = ?", clusterID).First(&model).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "get cluster config backup policy")
	}
	return policyModelToEntity(&model), nil
}

func (r *PolicyRepoImpl) Query(
	ctx context.Context,
	option *repo.PolicyQueryOption,
) ([]*entity.ClusterConfigBackupPolicy, uint32, bizerr.BizError) {
	rdb := r.provider.ReadDB(ctx)
	tx := rdb.Model(&schema.ClusterConfigBackupPolicy{}).Where("delete_policy_pending = ?", false)

	if option != nil && option.Filter != nil {
		tx = applyPolicyQueryFilter(tx, option.Filter)
	}

	var total int64
	if err := tx.Count(&total).Error; err != nil {
		return nil, 0, bizerr.WrapGormError(err, "count cluster config backup policies")
	}

	if option != nil {
		for _, sorter := range option.Sorters {
			order := sorter.OrderBy
			if sorter.Desc {
				order += " DESC"
			}
			tx = tx.Order(order)
		}

		if option.Page > 0 && option.PageSize > 0 {
			offset := (option.Page - 1) * option.PageSize
			tx = tx.Offset(int(offset)).Limit(int(option.PageSize))
		}
	}

	var models []*schema.ClusterConfigBackupPolicy
	if err := tx.Find(&models).Error; err != nil {
		return nil, 0, bizerr.WrapGormError(err, "query cluster config backup policies")
	}

	items := make([]*entity.ClusterConfigBackupPolicy, 0, len(models))
	for _, model := range models {
		items = append(items, policyModelToEntity(model))
	}

	return items, uint32(total), nil
}

func (r *PolicyRepoImpl) Upsert(ctx context.Context, policy *entity.ClusterConfigBackupPolicy) bizerr.BizError {
	wdb := r.provider.WriteDB(ctx)

	return bizerr.WrapGormError(
		wdb.Transaction(func(tx *gorm.DB) error {
			var model schema.ClusterConfigBackupPolicy
			err := tx.Where("cluster_id = ?", policy.ClusterID).First(&model).Error
			if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
				return err
			}

			newModel := policyEntityToModel(policy)
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return tx.Create(newModel).Error
			}

			newModel.ID = model.ID
			return tx.Model(&model).Select("*").Omit("created_at").Updates(newModel).Error
		}),
		"upsert cluster config backup policy",
	)
}

func (r *PolicyRepoImpl) Enable(ctx context.Context, clusterID uint64, intervalSec int32, now time.Time) bizerr.BizError {
	wdb := r.provider.WriteDB(ctx)

	return bizerr.WrapGormError(
		wdb.Transaction(func(tx *gorm.DB) error {
			var model schema.ClusterConfigBackupPolicy
			err := tx.Where("cluster_id = ?", clusterID).First(&model).Error
			if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
				return err
			}

			if errors.Is(err, gorm.ErrRecordNotFound) {
				newModel := &schema.ClusterConfigBackupPolicy{
					ClusterID:           clusterID,
					Enabled:             true,
					IntervalSec:         intervalSec,
					NextRunAt:           &now,
					DeleteBackupPending: false,
					DeletePolicyPending: false,
				}
				return tx.Create(newModel).Error
			}

			nextRunAt := now
			if model.Enabled {
				nextRunAt = now.Add(time.Duration(intervalSec) * time.Second)
			}

			updates := map[string]any{
				"enabled":               true,
				"interval_sec":          intervalSec,
				"next_run_at":           nextRunAt,
				"delete_backup_pending": false,
				"delete_policy_pending": false,
			}

			return tx.Model(&schema.ClusterConfigBackupPolicy{}).
				Where("cluster_id = ?", clusterID).
				Updates(updates).Error
		}),
		"enable cluster config backup policy",
	)
}

func (r *PolicyRepoImpl) Disable(ctx context.Context, clusterID uint64, option *repo.DisableOption) bizerr.BizError {
	wdb := r.provider.WriteDB(ctx)
	updates := map[string]any{
		"enabled":     false,
		"next_run_at": nil,
	}
	if option != nil {
		updates["delete_backup_pending"] = option.DeleteBackupPending
		updates["delete_policy_pending"] = option.DeletePolicyPending
	}
	return bizerr.WrapGormError(
		wdb.Transaction(func(tx *gorm.DB) error {
			var model schema.ClusterConfigBackupPolicy
			err := tx.Where("cluster_id = ?", clusterID).First(&model).Error
			if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
				return err
			}

			if errors.Is(err, gorm.ErrRecordNotFound) {
				return nil
			}

			return tx.Model(&schema.ClusterConfigBackupPolicy{}).
				Where("cluster_id = ?", clusterID).
				Updates(updates).Error
		}),
		"disable cluster config backup policy",
	)
}

func (r *PolicyRepoImpl) ClearDeleteBackupPending(ctx context.Context, clusterID uint64) bizerr.BizError {
	wdb := r.provider.WriteDB(ctx)
	return bizerr.WrapGormError(
		wdb.Model(&schema.ClusterConfigBackupPolicy{}).
			Where("cluster_id = ?", clusterID).
			Update("delete_backup_pending", false).Error,
		"clear delete backup pending flag",
	)
}

func (r *PolicyRepoImpl) ListPendingCleanup(
	ctx context.Context,
	limit int,
) ([]*entity.ClusterConfigBackupPolicy, bizerr.BizError) {
	rdb := r.provider.ReadDB(ctx)
	tx := rdb.Model(&schema.ClusterConfigBackupPolicy{}).
		Where("delete_backup_pending = ? OR delete_policy_pending = ?", true, true).
		Order("updated_at ASC")
	if limit > 0 {
		tx = tx.Limit(limit)
	}

	var models []*schema.ClusterConfigBackupPolicy
	if err := tx.Find(&models).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "list cluster config backup policies pending cleanup")
	}

	items := make([]*entity.ClusterConfigBackupPolicy, 0, len(models))
	for _, model := range models {
		items = append(items, policyModelToEntity(model))
	}
	return items, nil
}

func (r *PolicyRepoImpl) DeleteByClusterID(ctx context.Context, clusterID uint64) bizerr.BizError {
	return bizerr.WrapGormError(
		r.provider.WriteDB(ctx).Unscoped().Where("cluster_id = ?", clusterID).Delete(&schema.ClusterConfigBackupPolicy{}).Error,
		"delete cluster config backup policy",
	)
}

func (r *PolicyRepoImpl) ClaimDue(ctx context.Context, now time.Time, limit int) ([]*entity.ClusterConfigBackupPolicy, bizerr.BizError) {
	wdb := r.provider.WriteDB(ctx)
	var out []*entity.ClusterConfigBackupPolicy

	err := wdb.Transaction(func(tx *gorm.DB) error {
		var models []*schema.ClusterConfigBackupPolicy
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}).
			Where(
				"enabled = ? AND next_run_at IS NOT NULL AND next_run_at <= ? AND delete_backup_pending = ? AND delete_policy_pending = ?",
				true,
				now,
				false,
				false,
			).
			Order("next_run_at ASC").
			Limit(limit).
			Find(&models).Error; err != nil {
			return err
		}

		for _, model := range models {
			nextRunAt := now.Add(time.Duration(model.IntervalSec) * time.Second)
			if err := tx.Model(&schema.ClusterConfigBackupPolicy{}).
				Where("id = ?", model.ID).
				Update("next_run_at", nextRunAt).Error; err != nil {
				return err
			}
			model.NextRunAt = &nextRunAt
			out = append(out, policyModelToEntity(model))
		}
		return nil
	})
	if err != nil {
		return nil, bizerr.WrapGormError(err, "claim due cluster config backup policies")
	}
	return out, nil
}

func (r *PolicyRepoImpl) RecordSuccess(ctx context.Context, clusterID uint64, at time.Time) bizerr.BizError {
	return bizerr.WrapGormError(
		r.provider.WriteDB(ctx).Model(&schema.ClusterConfigBackupPolicy{}).
			Where("cluster_id = ?", clusterID).
			Updates(map[string]any{
				"last_backup_at":  at,
				"last_success_at": at,
				"last_error":      "",
			}).Error,
		"record cluster config backup success",
	)
}

func (r *PolicyRepoImpl) RecordFailure(ctx context.Context, clusterID uint64, at time.Time, errMsg string) bizerr.BizError {
	return bizerr.WrapGormError(
		r.provider.WriteDB(ctx).Model(&schema.ClusterConfigBackupPolicy{}).
			Where("cluster_id = ?", clusterID).
			Updates(map[string]any{
				"last_backup_at": at,
				"last_error":     errMsg,
			}).Error,
		"record cluster config backup failure",
	)
}

func policyModelToEntity(model *schema.ClusterConfigBackupPolicy) *entity.ClusterConfigBackupPolicy {
	return &entity.ClusterConfigBackupPolicy{
		ID:                  uint64(model.ID),
		ClusterID:           model.ClusterID,
		Enabled:             model.Enabled,
		IntervalSec:         model.IntervalSec,
		NextRunAt:           model.NextRunAt,
		LastBackupAt:        model.LastBackupAt,
		LastSuccessAt:       model.LastSuccessAt,
		LastError:           model.LastError,
		DeleteBackupPending: model.DeleteBackupPending,
		DeletePolicyPending: model.DeletePolicyPending,
	}
}

func policyEntityToModel(policy *entity.ClusterConfigBackupPolicy) *schema.ClusterConfigBackupPolicy {
	return &schema.ClusterConfigBackupPolicy{
		Model: gorm.Model{
			ID: uint(policy.ID),
		},
		ClusterID:           policy.ClusterID,
		Enabled:             policy.Enabled,
		IntervalSec:         policy.IntervalSec,
		NextRunAt:           policy.NextRunAt,
		LastBackupAt:        policy.LastBackupAt,
		LastSuccessAt:       policy.LastSuccessAt,
		LastError:           policy.LastError,
		DeleteBackupPending: policy.DeleteBackupPending,
		DeletePolicyPending: policy.DeletePolicyPending,
	}
}

func applyPolicyQueryFilter(tx *gorm.DB, filter *repo.PolicyQueryFilter) *gorm.DB {
	if len(filter.ClusterIDs) > 0 {
		tx = tx.Where("cluster_id IN ?", filter.ClusterIDs)
	}
	if filter.Enabled != nil {
		tx = tx.Where("enabled = ?", *filter.Enabled)
	}
	if filter.HasError != nil {
		if *filter.HasError {
			tx = tx.Where("last_error <> ''")
		} else {
			tx = tx.Where("last_error = ''")
		}
	}
	if filter.LastBackupAtFrom != nil {
		tx = tx.Where("last_backup_at >= ?", *filter.LastBackupAtFrom)
	}
	if filter.LastBackupAtTo != nil {
		tx = tx.Where("last_backup_at <= ?", *filter.LastBackupAtTo)
	}
	if filter.LastSuccessAtFrom != nil {
		tx = tx.Where("last_success_at >= ?", *filter.LastSuccessAtFrom)
	}
	if filter.LastSuccessAtTo != nil {
		tx = tx.Where("last_success_at <= ?", *filter.LastSuccessAtTo)
	}
	return tx
}
