package auth

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/merrors"
	"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 reordering with permission checking
type indexableDBImp[T storable.Storable] struct {
	repo         repository.Repository
	logger       mlogger.Logger
	enforcer     Enforcer
	createEmpty  func() T
	getIndexable func(T) *model.Indexable
}

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

// Reorder implements reordering with permission checking using EnforceBatch
func (db *indexableDBImp[T]) Reorder(ctx context.Context, accountRef, objectRef bson.ObjectID, newIndex int, filter builder.Query) error {
	// Get current object to find its index
	obj := db.createEmpty()
	if err := db.repo.Get(ctx, objectRef, obj); err != nil {
		db.logger.Warn("Failed to get object for reordering", zap.Error(err), zap.Int("new_index", newIndex),
			mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
		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.AccRef(accountRef),
			mzap.ObjRef("object_ref", objectRef), zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex))
		return nil // No change needed
	}

	// Determine which objects will be affected by the reordering
	var affectedObjects []model.PermissionBoundStorable

	if currentIndex < newIndex {
		// Moving down: items between currentIndex+1 and newIndex will be shifted up by -1
		reorderFilter := filter.
			And(repository.IndexOpFilter(currentIndex+1, builder.Gte)).
			And(repository.IndexOpFilter(newIndex, builder.Lte))

		// Get all affected objects using ListPermissionBound
		objects, err := db.repo.ListPermissionBound(ctx, reorderFilter)
		if err != nil {
			db.logger.Warn("Failed to get affected objects for reordering (moving down)",
				zap.Error(err), mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef),
				zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex))
			return err
		}
		affectedObjects = append(affectedObjects, objects...)
		db.logger.Debug("Found affected objects for moving down",
			mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(objects)))
	} else {
		// Moving up: items between newIndex and currentIndex-1 will be shifted down by +1
		reorderFilter := filter.
			And(repository.IndexOpFilter(newIndex, builder.Gte)).
			And(repository.IndexOpFilter(currentIndex-1, builder.Lte))

		// Get all affected objects using ListPermissionBound
		objects, err := db.repo.ListPermissionBound(ctx, reorderFilter)
		if err != nil {
			db.logger.Warn("Failed to get affected objects for reordering (moving up)", zap.Error(err),
				mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef),
				zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex))
			return err
		}
		affectedObjects = append(affectedObjects, objects...)
		db.logger.Debug("Found affected objects for moving up", mzap.AccRef(accountRef),
			mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(objects)))
	}

	// Add the target object to the list of objects that need permission checking
	targetObjects, err := db.repo.ListPermissionBound(ctx, repository.IDFilter(objectRef))
	if err != nil {
		db.logger.Warn("Failed to get target object for permission checking", zap.Error(err),
			mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
		return err
	}
	if len(targetObjects) > 0 {
		affectedObjects = append(affectedObjects, targetObjects[0])
	}

	// Check permissions for all affected objects using EnforceBatch
	db.logger.Debug("Checking permissions for reordering", mzap.AccRef(accountRef),
		mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(affectedObjects)),
		zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex))

	permissions, err := db.enforcer.EnforceBatch(ctx, affectedObjects, accountRef, model.ActionUpdate)
	if err != nil {
		db.logger.Warn("Failed to check permissions for reordering", zap.Error(err),
			mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(affectedObjects)))
		return merrors.Internal("failed to check permissions for reordering")
	}

	// Verify all objects have update permission
	for resObjectRef, hasPermission := range permissions {
		if !hasPermission {
			db.logger.Info("Permission denied for object during reordering", mzap.AccRef(accountRef),
				mzap.ObjRef("object_ref", objectRef), zap.String("action", string(model.ActionUpdate)))
			return merrors.AccessDenied(db.repo.Collection(), string(model.ActionUpdate), resObjectRef)
		}
	}

	db.logger.Debug("All permissions granted, proceeding with reordering", mzap.AccRef(accountRef),
		mzap.ObjRef("object_ref", objectRef), zap.Int("permission_count", len(permissions)))

	// All permissions checked, proceed with reordering
	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.Warn("Failed to shift objects during reordering (moving down)", zap.Error(err),
				mzap.AccRef(accountRef), 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.AccRef(accountRef),
			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.Warn("Failed to shift objects during reordering (moving up)", zap.Error(err),
				mzap.AccRef(accountRef), 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.AccRef(accountRef),
			mzap.ObjRef("object_ref", objectRef), zap.Int("updated_count", updatedCount))
	}

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

	db.logger.Debug("Successfully reordered object with permission checking",
		mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("old_index", currentIndex),
		zap.Int("new_index", newIndex), zap.Int("affected_count", len(affectedObjects)))
	return nil
}
