package br

import (
	"context"
	"time"

	"github.com/aws/smithy-go/ptr"
	"github.com/samber/lo"
	"github.com/tidbcloud/pkgv2/db"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.opentelemetry.io/otel"
	"go.uber.org/zap"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/br"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/infra/repoimpl/common"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/infra/repoimpl/schema"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/pagination"
)

var backupSettingTracer = otel.Tracer("backup_setting_repo_impl")

type backupSettingRepoImpl struct {
	provider   db.Provider
	backupRepo br.BackupRepo
}

func NewBackupSettingRepo(provider db.Provider, backupRepo br.BackupRepo) br.BackupSettingRepo {
	return &backupSettingRepoImpl{
		provider:   provider,
		backupRepo: backupRepo,
	}
}

func (r *backupSettingRepoImpl) Create(ctx context.Context, backupSetting *entity.BackupSetting) bizerr.BizError {
	ctx, span := backupSettingTracer.Start(ctx, "backupSettingRepo.Create")
	defer span.End()

	if backupSetting == nil {
		return bizerr.New(bizerr.ErrCodeUnknown, "backup setting entity is nil")
	}

	wdb := r.provider.WriteDB(ctx)
	schemaBackupSetting := backupSetting.BackupSetting
	now := time.Now().UTC()
	schemaBackupSetting.CreatedAt = now
	schemaBackupSetting.UpdatedAt = now
	schemaBackupSetting.LastScheduleUpdatedAt = now

	if err := wdb.Create(schemaBackupSetting).Error; err != nil {
		return bizerr.WrapGormError(err, "failed to create backup setting")
	}
	return nil
}

func (r *backupSettingRepoImpl) Save(ctx context.Context, backupSetting *entity.BackupSetting) bizerr.BizError {
	ctx, span := backupSettingTracer.Start(ctx, "backupSettingRepo.Save")
	defer span.End()

	if backupSetting == nil {
		return bizerr.New(bizerr.ErrCodeUnknown, "backup setting entity is nil")
	}

	if backupSetting.ClusterId == 0 || backupSetting.ID == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "backup setting ID and cluster ID cannot be zero for update operation")
	}

	wdb := r.provider.WriteDB(ctx)

	schemaBackupSetting := backupSetting.BackupSetting
	schemaBackupSetting.UpdatedAt = time.Now().UTC()

	if berr := common.SaveWithVersioning(wdb, schemaBackupSetting, nil); berr != nil {
		return berr.Chain("failed to save backup setting")
	}

	return nil
}

func (r *backupSettingRepoImpl) OpenBackupReconciliationExclusion(
	ctx context.Context,
	sharedPoolId uint64,
	operationId string,
	startAt time.Time,
) bizerr.BizError {
	if sharedPoolId == 0 || operationId == "" || startAt.IsZero() {
		return bizerr.New(bizerr.ErrCodeParams, "shared pool id, operation id and exclusion start are required")
	}
	now := time.Now().UTC()
	exclusion := &schema.BackupReconciliationExclusion{
		CreatedAt:    now,
		UpdatedAt:    now,
		SharedPoolId: sharedPoolId,
		OperationId:  operationId,
		StartAt:      startAt.UTC(),
	}
	if err := r.provider.WriteDB(ctx).
		Where("shared_pool_id = ? AND operation_id = ?", sharedPoolId, operationId).
		FirstOrCreate(exclusion).Error; err != nil {
		return bizerr.WrapGormError(err, "open backup reconciliation exclusion")
	}
	return nil
}

func (r *backupSettingRepoImpl) CloseBackupReconciliationExclusion(
	ctx context.Context,
	sharedPoolId uint64,
	endAt time.Time,
) bizerr.BizError {
	if sharedPoolId == 0 || endAt.IsZero() {
		return bizerr.New(bizerr.ErrCodeParams, "shared pool id and exclusion end are required")
	}
	result := r.provider.WriteDB(ctx).
		Model(&schema.BackupReconciliationExclusion{}).
		Where("shared_pool_id = ? AND end_at IS NULL", sharedPoolId).
		Updates(map[string]any{"end_at": endAt.UTC(), "updated_at": time.Now().UTC()})
	if result.Error != nil {
		return bizerr.WrapGormError(result.Error, "close backup reconciliation exclusion")
	}
	if result.RowsAffected == 0 {
		return bizerr.New(bizerr.ErrCodeRecordNotFound, "open backup reconciliation exclusion for shared pool %d not found", sharedPoolId)
	}
	return nil
}

func (r *backupSettingRepoImpl) ListBackupReconciliationExclusions(
	ctx context.Context,
	sharedPoolId uint64,
	startAt, endAt time.Time,
) ([]br.BackupReconciliationExclusion, bizerr.BizError) {
	if sharedPoolId == 0 || startAt.IsZero() || endAt.IsZero() || endAt.Before(startAt) {
		return nil, bizerr.New(bizerr.ErrCodeParams, "valid shared pool id and exclusion query range are required")
	}
	var rows []schema.BackupReconciliationExclusion
	// Read from the writer so an ACTIVE lifecycle observed immediately after the
	// atomic Resume commit cannot race a lagging replica that has not received
	// the closed exclusion yet.
	if err := r.provider.WriteDB(ctx).
		Where("shared_pool_id = ?", sharedPoolId).
		Where("start_at <= ?", endAt.UTC()).
		Where("end_at IS NULL OR end_at >= ?", startAt.UTC()).
		Order("start_at").
		Find(&rows).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "list backup reconciliation exclusions")
	}
	result := make([]br.BackupReconciliationExclusion, 0, len(rows))
	for _, row := range rows {
		result = append(result, br.BackupReconciliationExclusion{StartAt: row.StartAt, EndAt: row.EndAt})
	}
	return result, nil
}

func (r *backupSettingRepoImpl) BatchUpdateName(
	ctx context.Context,
	tenantId uint64,
	clusterNames map[uint64]string,
) bizerr.BizError {
	ctx, span := backupSettingTracer.Start(ctx, "backupSettingRepo.BatchUpdateName")
	defer span.End()

	if tenantId == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "tenant id cannot be zero")
	}
	if len(clusterNames) == 0 {
		return nil
	}

	wdb := r.provider.WriteDB(ctx)
	if err := wdb.Transaction(func(tx *gorm.DB) error {
		for clusterId, name := range clusterNames {
			if clusterId == 0 || name == "" {
				continue
			}
			if err := tx.Unscoped().Model(&schema.BackupSetting{}).
				Where("tenant_id = ? AND cluster_id = ?", tenantId, clusterId).
				Update("name", name).Error; err != nil {
				return err
			}
		}
		return nil
	}); err != nil {
		return bizerr.WrapGormError(err, "failed to batch update backup setting names")
	}

	return nil
}

func (r *backupSettingRepoImpl) UpdateLastAutoBackupAt(
	ctx context.Context,
	clusterId uint64,
	lastAutoBackupAt *time.Time,
) (time.Time, bizerr.BizError) {
	ctx, span := backupSettingTracer.Start(ctx, "backupSettingRepo.UpdateLastAutoBackupAt")
	defer span.End()

	wdb := r.provider.WriteDB(ctx)

	utcLastAutoBackupAt := lastAutoBackupAt.UTC()
	result := wdb.Model(&schema.BackupSetting{}).
		Where("cluster_id = ?", clusterId).
		Updates(map[string]any{
			"last_auto_backup_at": gorm.Expr(
				"CASE WHEN last_auto_backup_at IS NULL OR last_auto_backup_at < ? THEN ? ELSE last_auto_backup_at END",
				utcLastAutoBackupAt,
				utcLastAutoBackupAt,
			),
			"updated_at": time.Now().UTC(),
		})
	if result.Error != nil {
		return time.Time{}, bizerr.WrapGormError(result.Error, "failed to update last auto backup at")
	}
	if result.RowsAffected == 0 {
		return time.Time{}, bizerr.New(bizerr.ErrCodeRecordNotFound, "backup setting for cluster %d not found", clusterId)
	}

	var setting schema.BackupSetting
	if err := wdb.Select("last_auto_backup_at").Where("cluster_id = ?", clusterId).Take(&setting).Error; err != nil {
		return time.Time{}, bizerr.WrapGormError(err, "failed to get updated last auto backup at")
	}
	if setting.LastAutoBackupAt == nil {
		return time.Time{}, bizerr.New(bizerr.ErrCodeUnknown, "updated last auto backup at for cluster %d is nil", clusterId)
	}
	return setting.LastAutoBackupAt.UTC(), nil
}

func (r *backupSettingRepoImpl) Delete(ctx context.Context, clusterId uint64) bizerr.BizError {
	ctx, span := backupSettingTracer.Start(ctx, "backupSettingRepo.Delete")
	defer span.End()

	if clusterId == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "cluster ID cannot be zero for delete operation")
	}

	wdb := r.provider.WriteDB(ctx)

	// First, get the existing backup setting to ensure it exists and is not already deleted
	var schemaBackupSetting schema.BackupSetting
	if err := wdb.Where("cluster_id = ? AND deleted_at IS NULL", clusterId).First(&schemaBackupSetting).Error; err != nil {
		return bizerr.WrapGormError(err, "failed to get backup setting for deletion")
	}

	// Perform soft delete by updating DeletedAt field and setting state to deleted
	now := wdb.NowFunc().UTC()
	schemaBackupSetting.DeletedAt = gorm.DeletedAt{Time: now, Valid: true}
	schemaBackupSetting.State = schema.BackupSettingStateDeleted
	schemaBackupSetting.UpdatedAt = now

	if berr := common.SaveWithVersioning(wdb, &schemaBackupSetting, nil); berr != nil {
		return berr.Chain("failed to soft delete backup setting")
	}

	return nil
}

func (r *backupSettingRepoImpl) Get(ctx context.Context, clusterId uint64) (*entity.BackupSetting, bizerr.BizError) {
	ctx, span := backupSettingTracer.Start(ctx, "backupSettingRepo.Get")
	defer span.End()

	// Validate cluster ID is not zero
	if clusterId == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster ID cannot be zero")
	}

	rdb := r.provider.ReadDB(ctx)
	var schemaBackupSetting schema.BackupSetting
	if err := rdb.Unscoped().Where("cluster_id = ?", clusterId).First(&schemaBackupSetting).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "failed to get backup setting")
	}

	return &entity.BackupSetting{
		BackupSetting: &schemaBackupSetting,
	}, nil
}

func (r *backupSettingRepoImpl) GetActiveForUpdate(
	ctx context.Context,
	clusterId uint64,
) (*entity.BackupSetting, bizerr.BizError) {
	ctx, span := backupSettingTracer.Start(ctx, "backupSettingRepo.GetActiveForUpdate")
	defer span.End()

	if clusterId == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster ID cannot be zero")
	}

	wdb := r.provider.WriteDB(ctx)
	var schemaBackupSetting schema.BackupSetting
	if err := wdb.Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("cluster_id = ? AND state = ?", clusterId, schema.BackupSettingStateCreated).
		First(&schemaBackupSetting).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "failed to get active backup setting for update")
	}

	return &entity.BackupSetting{BackupSetting: &schemaBackupSetting}, nil
}

func (r *backupSettingRepoImpl) UnscopedQuery(
	ctx context.Context,
	filter *br.QueryBackupSettingFilter,
	page *pagination.Page,
) ([]*entity.BackupSetting, bizerr.BizError) {
	ctx, span := backupSettingTracer.Start(ctx, "backupSettingRepo.Query")
	defer span.End()

	rdb := r.provider.ReadDB(ctx)
	query := buildBackupSettingUnscopedQuery(rdb, filter)

	if page != nil {
		page = pagination.NormalizePage(page)
		query = query.Limit(int(page.PageSize))
		query = query.Offset(int(page.Offset))
	}

	var schemaBackupSettings []*schema.BackupSetting
	// we need to use Unscoped() to get the deleted backup settings
	if err := query.Unscoped().Find(&schemaBackupSettings).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "failed to query backup settings")
	}

	backupSettings := make([]*entity.BackupSetting, len(schemaBackupSettings))
	for i, schemaBackupSetting := range schemaBackupSettings {
		backupSettings[i] = &entity.BackupSetting{
			BackupSetting: schemaBackupSetting,
		}
	}

	return backupSettings, nil
}

func (r *backupSettingRepoImpl) UnscopedCount(
	ctx context.Context,
	filter *br.QueryBackupSettingFilter,
) (int64, bizerr.BizError) {
	ctx, span := backupSettingTracer.Start(ctx, "backupSettingRepo.UnscopedCount")
	defer span.End()

	rdb := r.provider.ReadDB(ctx)
	query := buildBackupSettingUnscopedQuery(rdb, filter)
	var count int64
	if err := query.Unscoped().Count(&count).Error; err != nil {
		return 0, bizerr.WrapGormError(err, "failed to count backup settings")
	}
	return count, nil
}

func buildBackupSettingUnscopedQuery(rdb *gorm.DB, filter *br.QueryBackupSettingFilter) *gorm.DB {
	query := rdb.Model(&schema.BackupSetting{})
	if filter != nil {
		if len(filter.RegionIds) > 0 {
			query = query.Where("region_id IN ?", filter.RegionIds)
		}
		if len(filter.ServicePlans) > 0 {
			query = query.Where("service_plan IN ?", filter.ServicePlans)
		}
		if len(filter.ClusterIds) > 0 {
			query = query.Where("cluster_id IN ?", filter.ClusterIds)
		}
		if filter.Name != "" {
			query = query.Where("name LIKE ?", "%"+filter.Name+"%")
		}
		if len(filter.SharedPoolIds) > 0 {
			query = query.Where("shared_pool_id IN ?", filter.SharedPoolIds)
		}
		if len(filter.TenantIds) > 0 {
			query = query.Where("tenant_id IN ?", filter.TenantIds)
		}
		if len(filter.Modes) > 0 {
			query = query.Where("mode IN ?", filter.Modes)
		}
		if len(filter.States) > 0 {
			query = query.Where("state IN ?", filter.States)
		}
		if len(filter.ExcludeStates) > 0 {
			query = query.Where("state NOT IN ?", filter.ExcludeStates)
		}
		if filter.LastScheduleUpdatedAtGte != nil {
			query = query.Where("last_schedule_updated_at >= ?", filter.LastScheduleUpdatedAtGte.UTC())
		}
		if filter.DeletedAtGte != nil {
			query = query.Where("deleted_at >= ?", filter.DeletedAtGte.UTC())
		}
		if filter.DeletedAtLte != nil {
			query = query.Where("deleted_at <= ?", filter.DeletedAtLte.UTC())
		}
		if filter.Deleted != nil {
			if *filter.Deleted {
				query = query.Where("deleted_at IS NOT NULL")
			} else {
				query = query.Where("deleted_at IS NULL")
			}
		}
		if filter.OrderBy != nil {
			query = query.Order(*filter.OrderBy)
		} else {
			query = query.Order("deleted_at DESC")
		}
	}
	return query
}

func (r *backupSettingRepoImpl) TransitStateAfterBackupDeletedIfNeeded(
	ctx context.Context,
	clusterId uint64,
) bizerr.BizError {
	ctx, span := backupSettingTracer.Start(ctx, "backupSettingRepo.TransitStateAfterBackupDeletedIfNeeded")
	defer span.End()

	wdb := r.provider.WriteDB(ctx)

	var bs schema.BackupSetting
	if err := wdb.Unscoped().Where("cluster_id = ?", clusterId).First(&bs).Error; err != nil {
		return bizerr.WrapGormError(err, "failed to get backup setting")
	}

	isStateChanged, berr := r.changeBackupSettingStateIfNeeed(ctx, &bs)
	if berr != nil {
		return berr.Chain("failed to change backup setting state")
	}

	if !isStateChanged {
		tilog.Info(
			ctx,
			"no need to transit backup setting state",
			zap.Any("clusterId", clusterId),
			zap.Any("backupSettingState", bs.State),
		)
		return nil
	}

	bs.UpdatedAt = time.Now().UTC()
	if err := common.SaveWithVersioning(wdb, &bs, nil); err != nil {
		return bizerr.WrapGormError(err, "failed to save backup setting")
	}
	tilog.Info(
		ctx,
		"backup setting state changed",
		zap.Any("clusterId", clusterId),
		zap.Any("backupSettingState", bs.State),
	)

	return nil
}

func (r *backupSettingRepoImpl) changeBackupSettingStateIfNeeed(
	ctx context.Context,
	bs *schema.BackupSetting,
) (changed bool, err bizerr.BizError) {
	// checke backup setting status
	if !lo.Contains(schema.BackupSettingStatesClusterDeletedStillHaveBackups, bs.State) {
		tilog.Info(
			ctx,
			"backup setting is not in the state of deleted still have backups, no need to transit state",
			zap.Any("backupSetting", bs),
		)
		return false, nil
	}

	if bs.State == schema.BackupSettingStateDeleted {
		// check auto backup data
		// check auto backups count
		numOfAutoBackups, err := r.backupRepo.UnscopedCount(ctx, &br.QueryBackupsFilter{
			ClusterIds:   []uint64{bs.ClusterId},
			TriggerTypes: []schema.BackupTriggerType{schema.BackupTriggerTypeAuto},
			Deleted:      ptr.Bool(false),
		})
		if err != nil {
			return false, bizerr.WrapError(err, "failed to count auto backups")
		}

		if numOfAutoBackups > 0 {
			tilog.Info(
				ctx,
				"there are auto backups, no need to transit state",
				zap.Int64("numOfAutoBackups", numOfAutoBackups),
				zap.Any("clusterId", bs.ClusterId),
				zap.Any("backupSettingState", bs.State),
			)
			return false, nil
		}

		tilog.Info(
			ctx,
			"no auto backup left, check manual backups",
			zap.Any("clusterId", bs.ClusterId),
			zap.Any("backupSettingState", bs.State),
		)

		// check manual backups count
		numOfManualBackups, err := r.backupRepo.UnscopedCount(ctx, &br.QueryBackupsFilter{
			ClusterIds:   []uint64{bs.ClusterId},
			TriggerTypes: []schema.BackupTriggerType{schema.BackupTriggerTypeManual},
			Deleted:      ptr.Bool(false),
		})
		if err != nil {
			return false, bizerr.WrapError(err, "failed to count manual backups")
		}

		if numOfManualBackups > 0 {
			tilog.Info(
				ctx,
				"there are manual backups, need to transit backup setting to expired state",
				zap.Int64("numOfManualBackups", numOfManualBackups),
				zap.Any("clusterId", bs.ClusterId),
				zap.Any("backupSettingState", bs.State),
			)
			bs.State = schema.BackupSettingStateExpired
			return true, nil
		}

		tilog.Info(
			ctx,
			"no manual backup, need to transit backup setting to cleanup state",
			zap.Any("clusterId", bs.ClusterId),
			zap.Any("backupSettingState", bs.State),
		)
		bs.State = schema.BackupSettingStateCleanup
		return true, nil
	}

	if bs.State == schema.BackupSettingStateExpired {
		// check manual backups count
		numOfManualBackups, err := r.backupRepo.UnscopedCount(ctx, &br.QueryBackupsFilter{
			ClusterIds:   []uint64{bs.ClusterId},
			TriggerTypes: []schema.BackupTriggerType{schema.BackupTriggerTypeManual},
			Deleted:      ptr.Bool(false),
		})
		if err != nil {
			return false, bizerr.WrapError(err, "failed to count manual backups")
		}

		if numOfManualBackups > 0 {
			tilog.Info(
				ctx,
				"there are manual backups, no need to transit state",
				zap.Int64("numOfManualBackups", numOfManualBackups),
				zap.Any("clusterId", bs.ClusterId),
				zap.Any("backupSettingState", bs.State),
			)
			return false, nil
		}

		tilog.Info(
			ctx,
			"no manual backup, need to transit backup setting to cleanup state",
			zap.Any("clusterId", bs.ClusterId),
			zap.Any("backupSettingState", bs.State),
		)
		bs.State = schema.BackupSettingStateCleanup
		return true, nil
	}

	return false, bizerr.New(bizerr.ErrCodeUnknown, "backup setting state is not supported")
}
