package br

import (
	"context"
	"errors"
	"slices"
	"time"

	"github.com/tidbcloud/pkgv2/db"
	"go.opentelemetry.io/otel"
	"gorm.io/gorm"

	"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 tracer = otel.Tracer("backup_repo_impl")

type backupRepoImpl struct {
	provider db.Provider
}

func NewBackupRepo(provider db.Provider) br.BackupRepo {
	return &backupRepoImpl{
		provider: provider,
	}
}

func (r *backupRepoImpl) Create(ctx context.Context, backup *entity.Backup) bizerr.BizError {
	ctx, span := tracer.Start(ctx, "backupRepo.Create")
	defer span.End()

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

	wdb := r.provider.WriteDB(ctx)
	schemaBackup := backup.Backup
	normalizeAutoBackupType(schemaBackup)
	schemaBackup.CreatedAt = time.Now()
	schemaBackup.UpdatedAt = time.Now()

	if schemaBackup.KernelBackupId > 0 {
		var existing schema.Backup
		err := wdb.Where("cluster_id = ? AND kernel_backup_id = ? AND deleted_at IS NULL", schemaBackup.ClusterId, schemaBackup.KernelBackupId).
			First(&existing).Error
		if err == nil {
			// preserve optimistic-lock versioning when performing an idempotent create/update
			schemaBackup.ID = existing.ID
			schemaBackup.Versioning = existing.Versioning
			schemaBackup.CreatedAt = existing.CreatedAt
			schemaBackup.UpdatedAt = time.Now()
			if berr := common.SaveWithVersioning(wdb, schemaBackup, nil); berr != nil {
				return berr.Chain("failed to update backup")
			}
			return nil
		}
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return bizerr.WrapGormError(err, "failed to query existing backup")
		}
	}

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

	return nil
}

func (r *backupRepoImpl) Save(ctx context.Context, backup *entity.Backup) bizerr.BizError {
	ctx, span := tracer.Start(ctx, "backupRepo.Save")
	defer span.End()

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

	wdb := r.provider.WriteDB(ctx)
	schemaBackup := backup.Backup
	normalizeAutoBackupType(schemaBackup)
	schemaBackup.UpdatedAt = time.Now()

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

	return nil
}

func normalizeAutoBackupType(backup *schema.Backup) {
	if backup.AutoBackupType == "" {
		backup.AutoBackupType = schema.AutoBackupTypeUnknown
	}
}

func (r *backupRepoImpl) Delete(ctx context.Context, filter *br.DeleteBackupsFilter) bizerr.BizError {
	ctx, span := tracer.Start(ctx, "backupRepo.DeleteBackups")
	defer span.End()

	if filter == nil {
		return bizerr.New(bizerr.ErrCodeParams, "delete filter cannot be nil")
	}

	// Validate that at least one filter criteria is provided
	if len(filter.Ids) == 0 && len(filter.ClusterIds) == 0 && len(filter.OrganizationIds) == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "at least one filter criteria must be provided")
	}

	// Validate that no backup ID is zero
	if slices.Contains(filter.Ids, 0) {
		return bizerr.New(bizerr.ErrCodeParams, "backup ID cannot be zero for delete operation")
	}

	// Validate that no cluster ID is zero
	if slices.Contains(filter.ClusterIds, 0) {
		return bizerr.New(bizerr.ErrCodeParams, "cluster ID cannot be zero for delete operation")
	}

	// Validate that no organization ID is zero
	if slices.Contains(filter.OrganizationIds, 0) {
		return bizerr.New(bizerr.ErrCodeParams, "organization ID cannot be zero for delete operation")
	}

	wdb := r.provider.WriteDB(ctx)
	query := wdb.Model(&schema.Backup{})

	// Apply filters
	if len(filter.Ids) > 0 {
		query = query.Where("id IN ?", filter.Ids)
	}
	if len(filter.ClusterIds) > 0 {
		query = query.Where("cluster_id IN ?", filter.ClusterIds)
	}
	if len(filter.OrganizationIds) > 0 {
		query = query.Where("tenant_id IN ?", filter.OrganizationIds)
	}
	if len(filter.TriggerTypes) > 0 {
		query = query.Where("trigger_type IN ?", filter.TriggerTypes)
	}
	if filter.ExpirationTimeLte != nil {
		query = query.Where("expiration_time <= ?", filter.ExpirationTimeLte)
	}

	// Perform soft delete by updating DeletedAt field to current time
	// Only update records that are not already soft deleted
	query = query.Where("deleted_at IS NULL")

	if err := query.Update("deleted_at", wdb.NowFunc()).Error; err != nil {
		return bizerr.WrapGormError(err, "failed to soft delete backups with filter")
	}

	return nil
}

func (r *backupRepoImpl) Get(ctx context.Context, backupId uint64) (*entity.Backup, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "backupRepo.Get")
	defer span.End()

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

	rdb := r.provider.ReadDB(ctx)
	var schemaBackup schema.Backup
	if err := rdb.Where("id = ?", backupId).First(&schemaBackup).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "failed to get backup")
	}

	return &entity.Backup{Backup: &schemaBackup}, nil
}

func (r *backupRepoImpl) UnscopedQuery(
	ctx context.Context,
	filter *br.QueryBackupsFilter,
	page *pagination.Page,
) ([]*entity.Backup, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "backupRepo.Query")
	defer span.End()

	rdb := r.provider.ReadDB(ctx)
	query := rdb.Model(&schema.Backup{})

	query = buildCommonQuery(query, filter)

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

	// Add ordering (default by creation time descending for consistent pagination)
	if filter.OrderBy != nil {
		query = query.Order(*filter.OrderBy)
	} else {
		query = query.Order("created_at DESC")
	}

	var schemaBackups []*schema.Backup
	if err := query.Unscoped().Find(&schemaBackups).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "failed to query backups")
	}

	backups := make([]*entity.Backup, len(schemaBackups))
	for i, schemaBackup := range schemaBackups {
		backups[i] = &entity.Backup{Backup: schemaBackup}
	}

	return backups, nil
}

func (r *backupRepoImpl) UnscopedCount(ctx context.Context, filter *br.QueryBackupsFilter) (int64, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "backupRepo.Count")
	defer span.End()

	rdb := r.provider.ReadDB(ctx)
	query := rdb.Model(&schema.Backup{})

	query = buildCommonQuery(query, filter)

	var count int64
	if err := query.Unscoped().Count(&count).Error; err != nil {
		return 0, bizerr.WrapGormError(err, "failed to count backups")
	}

	return count, nil
}

func buildCommonQuery(query *gorm.DB, filter *br.QueryBackupsFilter) *gorm.DB {
	// Apply filters (same as Query method)
	if len(filter.Ids) > 0 {
		query = query.Where("id IN ?", filter.Ids)
	}
	if len(filter.States) > 0 {
		query = query.Where("state IN ?", filter.States)
	}
	if len(filter.ClusterIds) > 0 {
		query = query.Where("cluster_id IN ?", filter.ClusterIds)
	}
	if len(filter.SharedPoolIds) > 0 {
		query = query.Where("shared_pool_id IN ?", filter.SharedPoolIds)
	}
	if len(filter.OrganizationIds) > 0 {
		query = query.Where("tenant_id IN ?", filter.OrganizationIds)
	}
	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.TriggerTypes) > 0 {
		query = query.Where("trigger_type IN ?", filter.TriggerTypes)
	}
	if len(filter.PauseOperationIds) > 0 {
		query = query.Where("pause_operation_id IN ?", filter.PauseOperationIds)
	}
	if len(filter.Purposes) > 0 {
		query = query.Where("purpose IN ?", filter.Purposes)
	}
	if len(filter.AutoBackupTypes) > 0 {
		query = query.Where("auto_backup_type IN ?", filter.AutoBackupTypes)
	}

	if filter.BackupTimeLte != nil {
		query = query.Where("backup_time <= ?", filter.BackupTimeLte)
	}
	if filter.BackupTimeGte != nil {
		query = query.Where("backup_time >= ?", filter.BackupTimeGte)
	}
	if filter.ExpirationTimeGt != nil {
		query = query.Where("expiration_time > ?", filter.ExpirationTimeGt)
	}
	if filter.ExcludeExpiredAutoBackupsAt != nil {
		query = query.Where(
			"(trigger_type != ? OR expiration_time > ?)",
			schema.BackupTriggerTypeAuto,
			filter.ExcludeExpiredAutoBackupsAt,
		)
	}

	if filter.Deleted != nil {
		if *filter.Deleted {
			query = query.Unscoped().Where("deleted_at IS NOT NULL")
		} else {
			query = query.Where("deleted_at IS NULL")
		}
	}

	return query
}
