package auth

import (
	"context"

	"github.com/tech/sendico/pkg/db/repository"
	"github.com/tech/sendico/pkg/db/template"
	"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"
)

// ArchivableDB implements archive operations with permission checking
type ArchivableDBImp[T model.PermissionBoundStorable] struct {
	dbImp         *template.DBImp[T]
	logger        mlogger.Logger
	enforcer      Enforcer
	createEmpty   func() T
	getArchivable func(T) model.Archivable
}

// NewArchivableDB creates a new auth.ArchivableDB instance
func newArchivableDBImp[T model.PermissionBoundStorable](
	dbImp *template.DBImp[T],
	logger mlogger.Logger,
	enforcer Enforcer,
	createEmpty func() T,
	getArchivable func(T) model.Archivable,
) ArchivableDB[T] {
	return &ArchivableDBImp[T]{
		dbImp:         dbImp,
		logger:        logger.Named("archivable"),
		enforcer:      enforcer,
		createEmpty:   createEmpty,
		getArchivable: getArchivable,
	}
}

// SetArchived sets the archived status of an entity with permission checking
func (db *ArchivableDBImp[T]) SetArchived(ctx context.Context, accountRef, objectRef bson.ObjectID, archived bool) error {
	// Check permissions using enforceObject helper
	if err := enforceObjectByRef(ctx, db.dbImp, db.enforcer, model.ActionUpdate, accountRef, objectRef); err != nil {
		db.logger.Warn("Failed to enforce object permission", zap.Error(err),
			mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived))
		return err
	}

	// Get the object to check current archived status
	obj := db.createEmpty()
	if err := db.dbImp.Get(ctx, objectRef, obj); err != nil {
		db.logger.Warn("Failed to get object for setting archived status", zap.Error(err),
			mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived))
		return err
	}

	// Extract archivable from the object
	archivable := db.getArchivable(obj)
	currentArchived := archivable.IsArchived()
	if currentArchived == archived {
		db.logger.Debug("No change needed - same archived status", mzap.AccRef(accountRef),
			mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived))
		return nil // No change needed
	}

	// Set the archived status
	patch := repository.Patch().Set(repository.IsArchivedField(), archived)
	if err := db.dbImp.Patch(ctx, objectRef, patch); err != nil {
		db.logger.Warn("Failed to set archived status on object", zap.Error(err),
			mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived))
		return err
	}

	db.logger.Debug("Successfully set archived status on object", mzap.AccRef(accountRef),
		mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived))
	return nil
}

// IsArchived checks if an entity is archived with permission checking
func (db *ArchivableDBImp[T]) IsArchived(ctx context.Context, accountRef, objectRef bson.ObjectID) (bool, error) {
	// // Check permissions using single Enforce
	if err := enforceObjectByRef(ctx, db.dbImp, db.enforcer, model.ActionRead, accountRef, objectRef); err != nil {
		db.logger.Debug("Permission denied for checking archived status", mzap.AccRef(accountRef),
			mzap.ObjRef("object_ref", objectRef), zap.String("action", string(model.ActionRead)))
		return false, merrors.AccessDenied("read", "object", objectRef)
	}
	obj := db.createEmpty()
	if err := db.dbImp.Get(ctx, objectRef, obj); err != nil {
		db.logger.Warn("Failed to get object for checking archived status", zap.Error(err),
			mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
		return false, err
	}
	archivable := db.getArchivable(obj)
	return archivable.IsArchived(), nil
}

// Archive archives an entity with permission checking (sets archived to true)
func (db *ArchivableDBImp[T]) Archive(ctx context.Context, accountRef, objectRef bson.ObjectID) error {
	return db.SetArchived(ctx, accountRef, objectRef, true)
}

// Unarchive unarchives an entity with permission checking (sets archived to false)
func (db *ArchivableDBImp[T]) Unarchive(ctx context.Context, accountRef, objectRef bson.ObjectID) error {
	return db.SetArchived(ctx, accountRef, objectRef, false)
}
