package indexable

import (
	"context"

	"github.com/tech/sendico/pkg/db/repository"
	"github.com/tech/sendico/pkg/db/repository/builder"
	"github.com/tech/sendico/pkg/db/storable"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/mutil/mzap"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

// IndexableDB implements db.IndexableDB interface with generic support
type IndexableDB[T storable.Storable] struct {
	repo         repository.Repository
	logger       mlogger.Logger
	createEmpty  func() T
	getIndexable func(T) *model.Indexable
}

// NewIndexableDB creates a new IndexableDB instance
func NewIndexableDB[T storable.Storable](
	repo repository.Repository,
	logger mlogger.Logger,
	createEmpty func() T,
	getIndexable func(T) *model.Indexable,
) *IndexableDB[T] {
	return &IndexableDB[T]{
		repo:         repo,
		logger:       logger,
		createEmpty:  createEmpty,
		getIndexable: getIndexable,
	}
}

// Reorder implements the db.IndexableDB interface with single filter parameter
func (db *IndexableDB[T]) Reorder(ctx context.Context, objectRef bson.ObjectID, newIndex int, filter builder.Query) error {
	// Get current object to find its index
	obj := db.createEmpty()
	err := db.repo.Get(ctx, objectRef, obj)
	if err != nil {
		db.logger.Error("Failed to get object for reordering",
			zap.Error(err),
			mzap.ObjRef("object_ref", objectRef),
			zap.Int("new_index", newIndex))
		return err
	}

	// Extract index from the object
	indexable := db.getIndexable(obj)
	currentIndex := indexable.Index
	if currentIndex == newIndex {
		db.logger.Debug("No reordering needed - same index",
			mzap.ObjRef("object_ref", objectRef),
			zap.Int("current_index", currentIndex),
			zap.Int("new_index", newIndex))
		return nil // No change needed
	}

	// Simple reordering logic
	if currentIndex < newIndex {
		// Moving down: shift items between currentIndex+1 and newIndex up by -1
		patch := repository.Patch().Inc(repository.IndexField(), -1)
		reorderFilter := filter.
			And(repository.IndexOpFilter(currentIndex+1, builder.Gte)).
			And(repository.IndexOpFilter(newIndex, builder.Lte))

		updatedCount, err := db.repo.PatchMany(ctx, reorderFilter, patch)
		if err != nil {
			db.logger.Error("Failed to shift objects during reordering (moving down)",
				zap.Error(err),
				mzap.ObjRef("object_ref", objectRef),
				zap.Int("current_index", currentIndex),
				zap.Int("new_index", newIndex),
				zap.Int("updated_count", updatedCount))
			return err
		}
		db.logger.Debug("Successfully shifted objects (moving down)",
			mzap.ObjRef("object_ref", objectRef),
			zap.Int("updated_count", updatedCount))
	} else {
		// Moving up: shift items between newIndex and currentIndex-1 down by +1
		patch := repository.Patch().Inc(repository.IndexField(), 1)
		reorderFilter := filter.
			And(repository.IndexOpFilter(newIndex, builder.Gte)).
			And(repository.IndexOpFilter(currentIndex-1, builder.Lte))

		updatedCount, err := db.repo.PatchMany(ctx, reorderFilter, patch)
		if err != nil {
			db.logger.Error("Failed to shift objects during reordering (moving up)",
				zap.Error(err),
				mzap.ObjRef("object_ref", objectRef),
				zap.Int("current_index", currentIndex),
				zap.Int("new_index", newIndex),
				zap.Int("updated_count", updatedCount))
			return err
		}
		db.logger.Debug("Successfully shifted objects (moving up)",
			mzap.ObjRef("object_ref", objectRef),
			zap.Int("updated_count", updatedCount))
	}

	// Update the target object to new index
	patch := repository.Patch().Set(repository.IndexField(), newIndex)
	err = db.repo.Patch(ctx, objectRef, patch)
	if err != nil {
		db.logger.Error("Failed to update target object index",
			zap.Error(err),
			mzap.ObjRef("object_ref", objectRef),
			zap.Int("current_index", currentIndex),
			zap.Int("new_index", newIndex))
		return err
	}

	db.logger.Info("Successfully reordered object",
		mzap.ObjRef("object_ref", objectRef),
		zap.Int("old_index", currentIndex),
		zap.Int("new_index", newIndex))
	return nil
}
