package auth

import (
	"context"
	"errors"

	"github.com/tech/sendico/pkg/db/policy"
	"github.com/tech/sendico/pkg/db/repository"
	"github.com/tech/sendico/pkg/db/repository/builder"
	"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/mservice"
	"github.com/tech/sendico/pkg/mutil/mzap"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.uber.org/zap"
)

type AccountBoundDBImp[T model.AccountBoundStorable] struct {
	Logger        mlogger.Logger
	DBImp         *template.DBImp[T]
	Enforcer      Enforcer
	PermissionRef bson.ObjectID
	Collection    mservice.Type
}

func (db *AccountBoundDBImp[T]) enforce(ctx context.Context, action model.Action, object model.AccountBoundStorable, accountRef bson.ObjectID) error {
	// FIRST: Check if the object's AccountRef equals the calling accountRef - if so, ALLOW
	objectAccountRef := object.GetAccountRef()
	if objectAccountRef != nil && *objectAccountRef == accountRef {
		db.Logger.Debug("Access granted - object belongs to calling account",
			mzap.ObjRef("object_account_ref", *objectAccountRef),
			mzap.ObjRef("calling_account_ref", accountRef),
			zap.String("action", string(action)))
		return nil
	}

	// SECOND: If not owned by calling account, check organization-level permissions
	organizationRef := object.GetOrganizationRef()
	res, err := db.Enforcer.Enforce(ctx, db.PermissionRef, accountRef, organizationRef, organizationRef, action)
	if err != nil {
		db.Logger.Warn("Failed to enforce permission",
			zap.Error(err), mzap.ObjRef("permission_ref", db.PermissionRef),
			mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef),
			zap.String("action", string(action)))
		return err
	}
	if !res {
		db.Logger.Debug("Access denied", mzap.ObjRef("permission_ref", db.PermissionRef),
			mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef),
			zap.String("action", string(action)))
		return merrors.AccessDenied(db.Collection, string(action), bson.NilObjectID)
	}
	return nil
}

func (db *AccountBoundDBImp[T]) enforceInterface(ctx context.Context, action model.Action, object model.AccountBoundStorable, accountRef bson.ObjectID) error {
	// FIRST: Check if the object's AccountRef equals the calling accountRef - if so, ALLOW
	objectAccountRef := object.GetAccountRef()
	if objectAccountRef != nil && *objectAccountRef == accountRef {
		db.Logger.Debug("Access granted - object belongs to calling account",
			mzap.ObjRef("object_account_ref", *objectAccountRef),
			mzap.ObjRef("calling_account_ref", accountRef),
			zap.String("action", string(action)))
		return nil
	}

	// SECOND: If not owned by calling account, check organization-level permissions
	organizationRef := object.GetOrganizationRef()
	res, err := db.Enforcer.Enforce(ctx, db.PermissionRef, accountRef, organizationRef, organizationRef, action)
	if err != nil {
		db.Logger.Warn("Failed to enforce permission",
			zap.Error(err), mzap.ObjRef("permission_ref", db.PermissionRef),
			mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef),
			zap.String("action", string(action)))
		return err
	}
	if !res {
		db.Logger.Debug("Access denied", mzap.ObjRef("permission_ref", db.PermissionRef),
			mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef),
			zap.String("action", string(action)))
		return merrors.AccessDenied(db.Collection, string(action), bson.NilObjectID)
	}
	return nil
}

func (db *AccountBoundDBImp[T]) Create(ctx context.Context, accountRef bson.ObjectID, object T) error {
	orgRef := object.GetOrganizationRef()
	db.Logger.Debug("Attempting to create object", mzap.AccRef(accountRef),
		mzap.ObjRef("organization_ref", orgRef), zap.String("collection", db.Collection))

	// Check organization update permission for create operations
	if err := db.enforce(ctx, model.ActionUpdate, object, accountRef); err != nil {
		return err
	}

	if err := db.DBImp.Create(ctx, object); err != nil {
		db.Logger.Warn("Failed to create object", zap.Error(err), mzap.AccRef(accountRef),
			mzap.ObjRef("organization_ref", orgRef), zap.String("collection", db.Collection))
		return err
	}

	db.Logger.Debug("Successfully created object", mzap.AccRef(accountRef),
		mzap.ObjRef("organization_ref", orgRef), zap.String("collection", db.Collection))
	return nil
}

func (db *AccountBoundDBImp[T]) Get(ctx context.Context, accountRef, objectRef bson.ObjectID, result T) error {
	db.Logger.Debug("Attempting to get object", mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))

	// First get the object to check its organization
	if err := db.DBImp.Get(ctx, objectRef, result); err != nil {
		db.Logger.Warn("Failed to get object", zap.Error(err), mzap.AccRef(accountRef),
			mzap.ObjRef("object_ref", objectRef), zap.String("collection", db.Collection))
		return err
	}

	// Check organization read permission
	if err := db.enforce(ctx, model.ActionRead, result, accountRef); err != nil {
		return err
	}

	db.Logger.Debug("Successfully retrieved object", mzap.AccRef(accountRef),
		mzap.ObjRef("organization_ref", result.GetOrganizationRef()), zap.String("collection", db.Collection))
	return nil
}

func (db *AccountBoundDBImp[T]) Update(ctx context.Context, accountRef bson.ObjectID, object T) error {
	db.Logger.Debug("Attempting to update object", mzap.AccRef(accountRef), mzap.StorableRef(object))

	// Check organization update permission
	if err := db.enforce(ctx, model.ActionUpdate, object, accountRef); err != nil {
		return err
	}

	if err := db.DBImp.Update(ctx, object); err != nil {
		db.Logger.Warn("Failed to update object", zap.Error(err), mzap.AccRef(accountRef),
			mzap.ObjRef("organization_ref", object.GetOrganizationRef()), mzap.StorableRef(object))
		return err
	}

	db.Logger.Debug("Successfully updated object", mzap.AccRef(accountRef),
		mzap.ObjRef("organization_ref", object.GetOrganizationRef()), mzap.StorableRef(object))
	return nil
}

func (db *AccountBoundDBImp[T]) Patch(ctx context.Context, accountRef, objectRef bson.ObjectID, patch builder.Patch) error {
	db.Logger.Debug("Attempting to patch object", mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))

	// First get the object to check its organization
	objs, err := db.DBImp.Repository.ListAccountBound(ctx, repository.IDFilter(objectRef))
	if err != nil {
		db.Logger.Warn("Failed to get object for permission check when deleting", zap.Error(err), mzap.ObjRef("object_ref", objectRef))
		return err
	}
	if len(objs) == 0 {
		db.Logger.Debug("Permission denied for deletion", mzap.ObjRef("object_ref", objectRef), mzap.AccRef(accountRef))
		return merrors.AccessDenied(db.Collection, string(model.ActionDelete), objectRef)
	}

	// Check organization update permission
	if err := db.enforce(ctx, model.ActionUpdate, objs[0], accountRef); err != nil {
		return err
	}

	if err := db.DBImp.Patch(ctx, objectRef, patch); err != nil {
		db.Logger.Warn("Failed to patch object", zap.Error(err), mzap.AccRef(accountRef),
			mzap.ObjRef("object_ref", objectRef), zap.String("collection", db.Collection))
		return err
	}

	db.Logger.Debug("Successfully patched object", mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
	return nil
}

func (db *AccountBoundDBImp[T]) Delete(ctx context.Context, accountRef, objectRef bson.ObjectID) error {
	db.Logger.Debug("Attempting to delete object", mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))

	// First get the object to check its organization
	objs, err := db.DBImp.Repository.ListAccountBound(ctx, repository.IDFilter(objectRef))
	if err != nil {
		db.Logger.Warn("Failed to get object for permission check when deleting", zap.Error(err), mzap.ObjRef("object_ref", objectRef))
		return err
	}
	if len(objs) == 0 {
		db.Logger.Debug("Permission denied for deletion", mzap.ObjRef("object_ref", objectRef), mzap.AccRef(accountRef))
		return merrors.AccessDenied(db.Collection, string(model.ActionDelete), objectRef)
	}
	// Check organization update permission for delete operations
	if err := db.enforce(ctx, model.ActionUpdate, objs[0], accountRef); err != nil {
		return err
	}

	if err := db.DBImp.Delete(ctx, objectRef); err != nil {
		db.Logger.Warn("Failed to delete object", zap.Error(err), mzap.AccRef(accountRef),
			mzap.ObjRef("object_ref", objectRef), zap.String("collection", db.Collection))
		return err
	}

	db.Logger.Debug("Successfully deleted object", mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
	return nil
}

func (db *AccountBoundDBImp[T]) DeleteMany(ctx context.Context, accountRef bson.ObjectID, query builder.Query) error {
	db.Logger.Debug("Attempting to delete many objects", mzap.AccRef(accountRef), zap.String("collection", db.Collection))

	// Get all candidate objects for batch permission checking
	allObjects, err := db.DBImp.Repository.ListPermissionBound(ctx, query)
	if err != nil {
		db.Logger.Warn("Failed to list objects for delete many", zap.Error(err), mzap.AccRef(accountRef))
		return err
	}

	// Use batch enforcement for efficiency
	allowedResults, err := db.Enforcer.EnforceBatch(ctx, allObjects, accountRef, model.ActionUpdate)
	if err != nil {
		db.Logger.Warn("Failed to enforce batch permissions for delete many", zap.Error(err), mzap.AccRef(accountRef))
		return err
	}

	// Build query for objects that passed permission check
	var allowedIDs []bson.ObjectID
	for _, obj := range allObjects {
		if allowedResults[*obj.GetID()] {
			allowedIDs = append(allowedIDs, *obj.GetID())
		}
	}

	if len(allowedIDs) == 0 {
		db.Logger.Debug("No objects allowed for deletion", mzap.AccRef(accountRef))
		return nil
	}

	// Delete only the allowed objects
	allowedQuery := query.And(repository.Query().In(repository.IDField(), allowedIDs))
	if err := db.DBImp.DeleteMany(ctx, allowedQuery); err != nil {
		db.Logger.Warn("Failed to delete many objects", zap.Error(err), mzap.AccRef(accountRef))
		return err
	}

	db.Logger.Debug("Successfully deleted many objects", mzap.AccRef(accountRef), zap.Int("count", len(allowedIDs)))
	return nil
}

func (db *AccountBoundDBImp[T]) FindOne(ctx context.Context, accountRef bson.ObjectID, query builder.Query, result T) error {
	db.Logger.Debug("Attempting to find one object", mzap.AccRef(accountRef), zap.String("collection", db.Collection))

	// For FindOne, we need to check read permission after finding the object
	if err := db.DBImp.FindOne(ctx, query, result); err != nil {
		db.Logger.Warn("Failed to find one object", zap.Error(err), mzap.AccRef(accountRef))
		return err
	}

	// Check organization read permission for the found object
	if err := db.enforce(ctx, model.ActionRead, result, accountRef); err != nil {
		return err
	}

	db.Logger.Debug("Successfully found one object", mzap.AccRef(accountRef),
		mzap.ObjRef("organization_ref", result.GetOrganizationRef()))
	return nil
}

func (db *AccountBoundDBImp[T]) ListIDs(ctx context.Context, accountRef bson.ObjectID, query builder.Query) ([]bson.ObjectID, error) {
	db.Logger.Debug("Attempting to list object IDs", mzap.AccRef(accountRef), zap.String("collection", db.Collection))

	// Get all candidate objects for batch permission checking
	allObjects, err := db.DBImp.Repository.ListPermissionBound(ctx, query)
	if err != nil {
		db.Logger.Warn("Failed to list objects for ID filtering", zap.Error(err), mzap.AccRef(accountRef))
		return nil, err
	}

	// Use batch enforcement for efficiency
	allowedResults, err := db.Enforcer.EnforceBatch(ctx, allObjects, accountRef, model.ActionRead)
	if err != nil {
		db.Logger.Warn("Failed to enforce batch permissions for ID listing", zap.Error(err), mzap.AccRef(accountRef))
		return nil, err
	}

	// Filter to only allowed object IDs
	var allowedIDs []bson.ObjectID
	for _, obj := range allObjects {
		if allowedResults[*obj.GetID()] {
			allowedIDs = append(allowedIDs, *obj.GetID())
		}
	}

	db.Logger.Debug("Successfully filtered object IDs", zap.Int("total_count", len(allObjects)),
		zap.Int("allowed_count", len(allowedIDs)), mzap.AccRef(accountRef))
	return allowedIDs, nil
}

func (db *AccountBoundDBImp[T]) ListAccountBound(ctx context.Context, accountRef, organizationRef bson.ObjectID, query builder.Query) ([]model.AccountBoundStorable, error) {
	db.Logger.Debug("Attempting to list account bound objects", mzap.AccRef(accountRef), zap.String("collection", db.Collection))

	// Build query to find objects where accountRef matches OR is null/absent
	accountQuery := repository.WithOrg(accountRef, organizationRef)

	// Combine with the provided query
	finalQuery := query.And(accountQuery)

	// Get all candidate objects
	allObjects, err := db.DBImp.Repository.ListAccountBound(ctx, finalQuery)
	if err != nil {
		db.Logger.Warn("Failed to list account bound objects", zap.Error(err), mzap.AccRef(accountRef))
		return nil, err
	}

	// Filter objects based on read permissions (AccountBoundStorable doesn't have permission info, so we check organization level)
	var allowedObjects []model.AccountBoundStorable
	for _, obj := range allObjects {
		if err := db.enforceInterface(ctx, model.ActionRead, obj, accountRef); err == nil {
			allowedObjects = append(allowedObjects, obj)
		} else if !errors.Is(err, merrors.ErrAccessDenied) {
			// If the error is something other than AccessDenied, we want to fail
			db.Logger.Warn("Error while enforcing read permission", zap.Error(err), mzap.ObjRef("object_ref", *obj.GetID()))
			return nil, err
		}
		// If AccessDenied, we simply skip that object
	}

	db.Logger.Debug("Successfully filtered account bound objects", zap.Int("total_count", len(allObjects)),
		zap.Int("allowed_count", len(allowedObjects)), mzap.AccRef(accountRef))
	return allowedObjects, nil
}

func (db *AccountBoundDBImp[T]) GetByAccountRef(ctx context.Context, accountRef bson.ObjectID, result T) error {
	db.Logger.Debug("Attempting to get object by account ref", mzap.AccRef(accountRef))

	// Build query to find objects where accountRef matches OR is null/absent
	query := repository.WithoutOrg(accountRef)

	if err := db.DBImp.FindOne(ctx, query, result); err != nil {
		db.Logger.Warn("Failed to get object by account ref", zap.Error(err), mzap.AccRef(accountRef))
		return err
	}

	// Check organization read permission for the found object
	if err := db.enforce(ctx, model.ActionRead, result, accountRef); err != nil {
		return err
	}

	db.Logger.Debug("Successfully retrieved object by account ref", mzap.AccRef(accountRef),
		mzap.ObjRef("organization_ref", result.GetOrganizationRef()))
	return nil
}

func (db *AccountBoundDBImp[T]) DeleteByAccountRef(ctx context.Context, accountRef bson.ObjectID) error {
	db.Logger.Debug("Attempting to delete objects by account ref", mzap.AccRef(accountRef))

	// Build query to find objects where accountRef matches OR is null/absent
	query := repository.WithoutOrg(accountRef)

	// Get all candidate objects for individual permission checking
	allObjects, err := db.DBImp.Repository.ListAccountBound(ctx, query)
	if err != nil {
		db.Logger.Warn("Failed to list objects for delete by account ref", zap.Error(err), mzap.AccRef(accountRef))
		return err
	}

	// Check permissions for each object individually (AccountBoundStorable doesn't have permission info)
	var allowedIDs []bson.ObjectID
	for _, obj := range allObjects {
		if err := db.enforceInterface(ctx, model.ActionUpdate, obj, accountRef); err == nil {
			allowedIDs = append(allowedIDs, *obj.GetID())
		} else if !errors.Is(err, merrors.ErrAccessDenied) {
			// If the error is something other than AccessDenied, we want to fail
			db.Logger.Warn("Error while enforcing update permission", zap.Error(err), mzap.ObjRef("object_ref", *obj.GetID()))
			return err
		}
		// If AccessDenied, we simply skip that object
	}

	if len(allowedIDs) == 0 {
		db.Logger.Debug("No objects allowed for deletion by account ref", mzap.AccRef(accountRef))
		return nil
	}

	// Delete only the allowed objects
	allowedQuery := query.And(repository.Query().In(repository.IDField(), allowedIDs))
	if err := db.DBImp.DeleteMany(ctx, allowedQuery); err != nil {
		db.Logger.Warn("Failed to delete objects by account ref", zap.Error(err), mzap.AccRef(accountRef))
		return err
	}

	db.Logger.Debug("Successfully deleted objects by account ref", mzap.AccRef(accountRef), zap.Int("count", len(allowedIDs)))
	return nil
}

func (db *AccountBoundDBImp[T]) DeleteCascade(ctx context.Context, objectRef bson.ObjectID) error {
	return db.DBImp.DeleteCascade(ctx, objectRef)
}

// CreateAccountBoundImp creates a concrete AccountBoundDBImp instance for internal use
func CreateAccountBoundImp[T model.AccountBoundStorable](
	ctx context.Context,
	logger mlogger.Logger,
	pdb policy.DB,
	enforcer Enforcer,
	collection mservice.Type,
	db *mongo.Database,
) (*AccountBoundDBImp[T], error) {
	logger = logger.Named("account_bound")
	var policy model.PolicyDescription
	if err := pdb.GetBuiltInPolicy(ctx, mservice.Organizations, &policy); err != nil {
		logger.Warn("Failed to fetch organization policy description", zap.Error(err))
		return nil, err
	}
	res := &AccountBoundDBImp[T]{
		Logger:        logger,
		DBImp:         template.Create[T](logger, collection, db),
		Enforcer:      enforcer,
		PermissionRef: policy.ID,
		Collection:    collection,
	}
	return res, nil
}
