package com.atlassian.stash.internal.user;

import com.atlassian.bitbucket.*;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.event.permission.*;
import com.atlassian.bitbucket.event.user.GroupCleanupEvent;
import com.atlassian.bitbucket.event.user.UserCleanupEvent;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.bitbucket.license.LicenseService;
import com.atlassian.bitbucket.permission.*;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.project.ProjectType;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.UserType;
import com.atlassian.bitbucket.util.*;
import com.atlassian.crowd.embedded.impl.IdentifierUtils;
import com.atlassian.event.api.EventListener;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.plugin.spring.AvailableToPlugins;
import com.atlassian.stash.experimental.user.*;
import com.atlassian.stash.internal.InternalConverter;
import com.atlassian.stash.internal.annotation.Secured;
import com.atlassian.stash.internal.annotation.Unsecured;
import com.atlassian.stash.internal.page.PageConstants;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Iterables;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Nonnull;
import java.util.Collections;
import java.util.Comparator;
import java.util.Set;
import java.util.function.Predicate;

import static com.atlassian.bitbucket.permission.Permission.*;
import static com.atlassian.bitbucket.util.PageUtils.filterPages;
import static com.atlassian.stash.internal.CommonValidations.*;
import static com.atlassian.stash.internal.InternalConverter.convertToInternalUser;
import static com.google.common.base.Preconditions.checkNotNull;

@AvailableToPlugins(interfaces = {PermissionAdminService.class, ExperimentalPermissionAdminService.class})
@Service("permissionAdminService")
@SuppressWarnings("ConstantConditions")
@Transactional(readOnly = true)
public class DefaultPermissionAdminService implements ExperimentalPermissionAdminService {

    private static final Comparator<String> CASE_INSENSITIVE_COMPARATOR = IdentifierUtils::compareToInLowerCase;

    private static final Comparator<ApplicationUser> CASE_INSENSITIVE_NAME_COMPARATOR =
            (u1, u2) -> IdentifierUtils.compareToInLowerCase(u1.getName(), u2.getName());

    public static final int INTERNAL_PAGE_LIMIT = 500;

    private final EventPublisher eventPublisher;
    private final GlobalPermissionDao globalPermissionDao;
    private final ProjectPermissionDao projectPermissionDao;
    private final RepositoryPermissionDao repositoryPermissionDao;
    private final I18nService i18nService;
    private final LicenseService licenseService;
    private final PermissionService permissionService;
    private final PermissionValidationService permissionValidationService;
    private final AuthenticationContext authenticationContext;
    private final InternalUserService userService;

    @Value(PageConstants.MAX_GROUPS)
    private int maxGroups;
    @Value(PageConstants.MAX_USERS)
    private int maxUsers;

    @Autowired
    public DefaultPermissionAdminService(InternalUserService userService,
                                         GlobalPermissionDao globalPermissionDao,
                                         ProjectPermissionDao projectPermissionDao,
                                         RepositoryPermissionDao repositoryPermissionDao,
                                         AuthenticationContext authenticationContext,
                                         PermissionService permissionService,
                                         PermissionValidationService permissionValidationService,
                                         I18nService i18nService,
                                         LicenseService licenseService,
                                         EventPublisher eventPublisher) {
        this.userService = userService;
        this.globalPermissionDao = globalPermissionDao;
        this.projectPermissionDao = projectPermissionDao;
        this.repositoryPermissionDao = repositoryPermissionDao;
        this.authenticationContext = authenticationContext;
        this.permissionService = permissionService;
        this.permissionValidationService = permissionValidationService;
        this.i18nService = i18nService;
        this.licenseService = licenseService;
        this.eventPublisher = eventPublisher;
    }

    @Nonnull
    @PreAuthorize("hasGlobalPermission('ADMIN')")
    @Override
    public Page<ProjectPermission> searchProjects(@Nonnull ProjectPermissionSearchRequest request,
                                                  @Nonnull PageRequest pageRequest) {
        validateRequest(request);
        validatePageRequest(pageRequest);

        return projectPermissionDao.findHighestPermissionsForUser(request.getUser().getId(), pageRequest);
    }

    @Nonnull
    @PreAuthorize("hasGlobalPermission('ADMIN')")
    @Override
    public Page<RepositoryPermission> searchRepositories(@Nonnull RepositoryPermissionSearchRequest request,
                                                         @Nonnull PageRequest pageRequest) {
        validateRequest(request);
        validatePageRequest(pageRequest);

        return repositoryPermissionDao.findHighestPermissionsForUser(request.getUser().getId(), pageRequest);
    }

    @Nonnull
    @Override
    @Secured("Secured internally using the PermissionValidationService")
    public Page<PermittedUser> searchUsers(@Nonnull PermittedUserSearchRequest request,
                                           @Nonnull PageRequest pageRequest) {
        validateRequest(request);
        validatePageRequest(pageRequest);

        PageRequest restrictedPageRequest = pageRequest.buildRestrictedPageRequest(maxUsers);

        return request.accept(new PermittedUserSearchRequest.Visitor<Page<PermittedUser>>() {
            @Override
            public Page<PermittedUser> visit(@Nonnull PermittedUserGlobalSearchRequest request) {
                permissionValidationService.validateForGlobal(Permission.ADMIN);

                return globalPermissionDao.findHighestPermissionPerUser(request.getUserType(),
                        request.getNameFilter(), restrictedPageRequest);
            }

            @Override
            public Page<PermittedUser> visit(@Nonnull PermittedUserProjectSearchRequest request) {
                permissionValidationService.validateForProject(request.getProject(), Permission.PROJECT_ADMIN);

            return projectPermissionDao.findHighestPermissionPerUser(request.getProject().getId(),
                        request.getUserType(), request.getNameFilter(), restrictedPageRequest);
            }

            @Override
            public Page<PermittedUser> visit(@Nonnull PermittedUserRepositorySearchRequest request) {
                permissionValidationService.validateForRepository(request.getRepository(), Permission.REPO_ADMIN);

                return repositoryPermissionDao.findHighestPermissionPerUser(request.getRepository().getId(),
                        request.getUserType(), request.getNameFilter(), restrictedPageRequest);
            }
        });
    }

    @Nonnull
    @Override
    @Secured("Secured internally using the PermissionValidationService")
    public Page<PermittedGroup> searchGroups(@Nonnull PermittedGroupSearchRequest request,
                                             @Nonnull PageRequest pageRequest) {
        validateRequest(request);
        validatePageRequest(pageRequest);

        PageRequest restrictedPageRequest = pageRequest.buildRestrictedPageRequest(maxGroups);

        return request.accept(new PermittedGroupSearchRequest.Visitor<Page<PermittedGroup>>() {
            @Override
            public Page<PermittedGroup> visit(@Nonnull PermittedGroupGlobalSearchRequest request) {
                permissionValidationService.validateForGlobal(Permission.ADMIN);

                return globalPermissionDao.findHighestPermissionPerGroup(request.getNameFilter(), restrictedPageRequest);
            }

            @Override
            public Page<PermittedGroup> visit(@Nonnull PermittedGroupProjectSearchRequest request) {
                permissionValidationService.validateForProject(request.getProject(), Permission.PROJECT_ADMIN);

                return projectPermissionDao.findHighestPermissionPerGroup(request.getProject().getId(),
                        request.getNameFilter(), restrictedPageRequest);
            }

            @Override
            public Page<PermittedGroup> visit(@Nonnull PermittedGroupRepositorySearchRequest request) {
                permissionValidationService.validateForRepository(request.getRepository(), Permission.REPO_ADMIN);

                return repositoryPermissionDao.findHighestPermissionPerGroup(request.getRepository().getId(),
                        request.getNameFilter(), restrictedPageRequest);
            }
        });
    }

    @Nonnull
    @Override
    @Secured("Secured internally using the PermissionValidationService")
    public Page<ApplicationUser> searchUsersLacking(@Nonnull PermittedUserSearchRequest request,
                                                    @Nonnull PageRequest pageRequest) {
        checkNotNull(request, "request");
        validatePageRequest(pageRequest);

        PageProvider<ApplicationUser> provider = request.accept(new PermittedUserSearchRequest.Visitor<PageProvider<ApplicationUser>>() {
            @Override
            public PageProvider<ApplicationUser> visit(@Nonnull PermittedUserGlobalSearchRequest request) {
                permissionValidationService.validateForGlobal(Permission.ADMIN);

                return innerPageRequest -> globalPermissionDao.findUsersWithPermission(
                        request.getUserType(), innerPageRequest);
            }

            @Override
            public PageProvider<ApplicationUser> visit(@Nonnull PermittedUserProjectSearchRequest request) {
                permissionValidationService.validateForProject(request.getProject(), Permission.PROJECT_ADMIN);

                return innerPageRequest -> projectPermissionDao.findUsersWithPermission(request.getProject().getId(),
                        request.getUserType(), innerPageRequest);
            }

            @Override
            public PageProvider<ApplicationUser> visit(@Nonnull PermittedUserRepositorySearchRequest request) {
                permissionValidationService.validateForRepository(request.getRepository(), Permission.REPO_ADMIN);

                return innerPageRequest -> repositoryPermissionDao.findUsersWithPermission(request.getRepository().getId(),
                        request.getUserType(), innerPageRequest);
            }
        });

        return getUsersWithoutPermission(withoutUsers(provider), request.getUserType(), request.getNameFilter(), pageRequest);
    }

    @Nonnull
    @Override
    @Secured("Secured internally using the PermissionValidationService")
    public Page<String> searchGroupsLacking(@Nonnull PermittedGroupSearchRequest request,
                                            @Nonnull PageRequest pageRequest) {
        checkNotNull(request, "request");
        validatePageRequest(pageRequest);

        PageProvider<String> provider = request.accept(new PermittedGroupSearchRequest.Visitor<PageProvider<String>>() {
            @Override
            public PageProvider<String> visit(@Nonnull PermittedGroupGlobalSearchRequest request) {
                permissionValidationService.validateForGlobal(Permission.ADMIN);

                return globalPermissionDao::findGroupsWithPermission;
            }

            @Override
            public PageProvider<String> visit(@Nonnull PermittedGroupProjectSearchRequest request) {
                permissionValidationService.validateForProject(request.getProject(), Permission.PROJECT_ADMIN);

                return innerPageRequest -> projectPermissionDao.findGroupsWithPermission(
                        request.getProject().getId(), innerPageRequest);
            }

            @Override
            public PageProvider<String> visit(@Nonnull PermittedGroupRepositorySearchRequest request) {
                permissionValidationService.validateForRepository(request.getRepository(), Permission.REPO_ADMIN);

                return innerPageRequest -> repositoryPermissionDao.findGroupsWithPermission(
                        request.getRepository().getId(), innerPageRequest);
            }
        });

        return getGroupsWithoutPermission(request.getNameFilter(), withoutGroups(provider), pageRequest);
    }

    @Override
    @Nonnull
    @PreAuthorize("hasGlobalPermission('ADMIN')")
    public Page<PermittedUser> findUsersWithGlobalPermission(String filter, @Nonnull PageRequest pageRequest) {
        return searchUsers(new PermittedUserSearchRequest.Builder().nameFilter(filter).build(), pageRequest);
    }

    @Override
    @Nonnull
    @PreAuthorize("hasGlobalPermission('ADMIN')")
    public Page<ApplicationUser> findUsersWithoutGlobalPermission(String filter, @Nonnull PageRequest pageRequest) {
        return searchUsersLacking(new PermittedUserSearchRequest.Builder().nameFilter(filter).build(), pageRequest);
    }

    @Override
    @Nonnull
    @PreAuthorize("hasGlobalPermission('ADMIN')")
    public Page<PermittedGroup> findGroupsWithGlobalPermission(String filter, @Nonnull PageRequest pageRequest) {
        return searchGroups(new PermittedGroupSearchRequest.Builder().nameFilter(filter).build(), pageRequest);
    }

    @Override
    @Nonnull
    @PreAuthorize("hasGlobalPermission('ADMIN')")
    public Page<String> findGroupsWithoutGlobalPermission(String filter, @Nonnull PageRequest pageRequest) {
        return searchGroupsLacking(new PermittedGroupSearchRequest.Builder().nameFilter(filter).build(), pageRequest);
    }

    @Override
    @Nonnull
    @PreAuthorize("hasProjectPermission(#project, 'PROJECT_ADMIN')")
    public Page<PermittedUser> findUsersWithProjectPermission(@Nonnull Project project,
                                                              String filter,
                                                              @Nonnull PageRequest pageRequest) {
        return searchUsers(
                new PermittedUserSearchRequest.Builder().project(project).nameFilter(filter).build(),
                pageRequest
        );
    }

    @Override
    @Nonnull
    @Unsecured("Secured internally in searchUsersLacking()")
    public Page<ApplicationUser> findLicensedUsersWithoutProjectPermission(@Nonnull Project project,
                                                                           String filter,
                                                                           @Nonnull PageRequest pageRequest) {
        PageProvider<ApplicationUser> userProvider = request -> searchUsersLacking(
                new PermittedUserSearchRequest.Builder()
                        .project(project)
                        .nameFilter(filter)
                        .build(),
                request
        );

        return filterPages(userProvider, licensedOnly(), pageRequest);
    }

    @Override
    @Nonnull
    @PreAuthorize("hasProjectPermission(#project, 'PROJECT_ADMIN')")
    public Page<PermittedGroup> findGroupsWithProjectPermission(@Nonnull Project project,
                                                                String filter,
                                                                @Nonnull PageRequest pageRequest) {
        return searchGroups(
                new PermittedGroupSearchRequest.Builder().project(project).nameFilter(filter).build(),
                pageRequest
        );
    }

    @Override
    @Nonnull
    @PreAuthorize("hasProjectPermission(#project, 'PROJECT_ADMIN')")
    public Page<String> findGroupsWithoutProjectPermission(@Nonnull Project project,
                                                           String filter,
                                                           @Nonnull PageRequest pageRequest) {
        return searchGroupsLacking(
                new PermittedGroupSearchRequest.Builder().project(project).nameFilter(filter).build(),
                pageRequest
        );
    }

    @Override
    @Nonnull
    @PreAuthorize("hasRepositoryPermission(#repository, 'REPO_ADMIN')")
    public Page<PermittedUser> findUsersWithRepositoryPermission(@Nonnull Repository repository,
                                                                 String filter,
                                                                 @Nonnull PageRequest pageRequest) {
        return searchUsers(new PermittedUserSearchRequest.Builder().repository(repository).nameFilter(filter).build(), pageRequest);
    }

    @Override
    @Nonnull
    @Unsecured("Secured internally in searchUsersLacking()")
    public Page<ApplicationUser> findLicensedUsersWithoutRepositoryPermission(@Nonnull Repository repository,
                                                                              String filter,
                                                                              @Nonnull PageRequest pageRequest) {
        PageProvider<ApplicationUser> userProvider = request -> searchUsersLacking(
                new PermittedUserSearchRequest.Builder()
                        .repository(repository)
                        .nameFilter(filter)
                        .build(),
                request);

        return filterPages(userProvider, licensedOnly(), pageRequest);
    }

    @Override
    @Nonnull
    @PreAuthorize("hasRepositoryPermission(#repository, 'REPO_ADMIN')")
    public Page<PermittedGroup> findGroupsWithRepositoryPermission(@Nonnull Repository repository,
                                                                   String filter,
                                                                   @Nonnull PageRequest pageRequest) {
        return searchGroups(new PermittedGroupSearchRequest.Builder().repository(repository).nameFilter(filter).build(), pageRequest);
    }

    @Override
    @Nonnull
    @PreAuthorize("hasRepositoryPermission(#repository, 'REPO_ADMIN')")
    public Page<String> findGroupsWithoutRepositoryPermission(@Nonnull Repository repository,
                                                              String filter,
                                                              @Nonnull PageRequest pageRequest) {
        return searchGroupsLacking(new PermittedGroupSearchRequest.Builder().repository(repository).nameFilter(filter).build(), pageRequest);
    }

    @Override
    @Transactional
    @PreAuthorize("hasGlobalPermission('ADMIN')")
    public void revokeAllGlobalPermissions(@Nonnull String group) {
        validateGroup(group);

        Permission revoked = doRevokeAllGlobalPermissions(group);
        if (revoked != null) {
            fireGlobalPermissionRequestedEvent(revoked, null, group, null);
            fireGlobalPermissionEvent(revoked, null, group, null);
        }
    }

    private Permission doRevokeAllGlobalPermissions(String group) {
        if (permissionService.hasGlobalPermission(group, Permission.SYS_ADMIN) &&
                !permissionService.hasGlobalPermission(Permission.SYS_ADMIN)) {
            throw new IntegrityException(i18nService.createKeyedMessage("bitbucket.service.permissionadmin.revoke.insufficient.permission", group));
        }

        Permission revoked = null;
        for (Permission permission : Permission.getGlobalPermissions()) {
            InternalGlobalPermission perm = buildPermission(permission, group, null);
            if (globalPermissionDao.hasPermissionEntry(perm) && revokeGlobalPermission(permission, group)) {
                revoked = Permission.max(revoked, permission);
            }
        }
        return revoked;
    }

    @Override
    @Transactional
    @PreAuthorize("hasGlobalPermission('ADMIN')")
    public void revokeAllGlobalPermissions(@Nonnull ApplicationUser user) {
        validateUser(user);

        Permission revoked = doRevokeAllGlobalPermissions(user);
        if (revoked != null) {
            fireGlobalPermissionRequestedEvent(revoked, null, null, user);
            fireGlobalPermissionEvent(revoked, null, null, user);
        }
    }

    private Permission doRevokeAllGlobalPermissions(ApplicationUser user) {
        // As an optimisation check the current users permission before checking the target users
        // It is more likely that the current users permissions are cached or the current thread's
        // permissions have been elevated
        if (!permissionService.hasGlobalPermission(Permission.SYS_ADMIN) &&
                permissionService.hasGlobalPermission(user, Permission.SYS_ADMIN)) {
            throw new IntegrityException(i18nService.createKeyedMessage(
                    "bitbucket.service.permissionadmin.revoke.insufficient.permission", user.getDisplayName()));
        }

        Permission revoked = null;
        for (Permission permission : Permission.getGlobalPermissions()) {
            InternalGlobalPermission perm = buildPermission(permission, null, user);
            if (globalPermissionDao.hasPermissionEntry(perm) && revokeGlobalPermission(permission, user)) {
                revoked = Permission.max(revoked, permission);
            }
        }
        return revoked;
    }

    @Override
    @Transactional
    @PreAuthorize("hasProjectPermission(#project, 'PROJECT_ADMIN')")
    public void revokeAllProjectPermissions(@Nonnull Project project, @Nonnull String group) {
        validateProject(project);
        validateGroup(group);

        Permission revoked = doRevokeAllProjectPermissions(project, group);
        if (revoked != null) {
            fireProjectPermissionRequestedEvent(project, revoked, null, group, null);
            fireProjectPermissionEvent(project, revoked, null, group, null);
        }
    }

    private Permission doRevokeAllProjectPermissions(Project project, String group) {
        Permission revoked = null;
        for (Permission permission : Permission.getPermissionsOn(Project.class)) {
            if (revokeProjectPermission(permission, project, group)) {
                revoked = Permission.max(revoked, permission);
            }
        }
        return revoked;
    }

    @Override
    @Transactional
    @PreAuthorize("hasProjectPermission(#project, 'PROJECT_ADMIN')")
    public void revokeAllProjectPermissions(@Nonnull Project project, @Nonnull ApplicationUser user) {
        validateProject(project);
        validateUser(user);

        Permission revoked = doRevokeAllProjectPermissions(project, user);
        if (revoked != null) {
            fireProjectPermissionRequestedEvent(project, revoked, null, null, user);
            fireProjectPermissionEvent(project, revoked, null, null, user);
        }
    }

    private Permission doRevokeAllProjectPermissions(Project project, ApplicationUser user) {
        Permission revoked = null;
        for (Permission permission : Permission.getPermissionsOn(Project.class)) {
            if (revokeProjectPermission(permission, project, user)) {
                revoked = Permission.max(revoked, permission);
            }
        }
        return revoked;
    }

    @Override
    @Transactional
    @PreAuthorize("hasRepositoryPermission(#repository, 'REPO_ADMIN')")
    public void revokeAllRepositoryPermissions(@Nonnull Repository repository, @Nonnull String group) {
        validateRepository(repository);
        validateGroup(group);

        Permission revoked = doRevokeAllRepositoryPermissions(repository, group);
        if (revoked != null) {
            fireRepositoryPermissionRequestedEvent(repository, revoked, null, group, null);
            fireRepositoryPermissionEvent(repository, revoked, null, group, null);
        }
    }

    private Permission doRevokeAllRepositoryPermissions(Repository repository, String group) {
        Permission revoked = null;
        for (Permission permission : Permission.getPermissionsOn(Repository.class)) {
            if (revokeRepositoryPermission(permission, repository, group)) {
                revoked = Permission.max(revoked, permission);
            }
        }
        return revoked;
    }

    @Override
    @Transactional
    @PreAuthorize("hasRepositoryPermission(#repository, 'REPO_ADMIN')")
    public void revokeAllRepositoryPermissions(@Nonnull Repository repository, @Nonnull ApplicationUser user) {
        validateRepository(repository);
        validateUser(user);

        Permission revoked = doRevokeAllRepositoryPermissions(repository, user);
        if (revoked != null) {
            fireRepositoryPermissionRequestedEvent(repository, revoked, null, null, user);
            fireRepositoryPermissionEvent(repository, revoked, null, null, user);
        }
    }

    private Permission doRevokeAllRepositoryPermissions(Repository repository, ApplicationUser user) {
        Permission revoked = null;
        for (Permission permission : Permission.getPermissionsOn(Repository.class)) {
            if (revokeRepositoryPermission(permission, repository, user)) {
                revoked = Permission.max(revoked, permission);
            }
        }
        return revoked;
    }

    @Override
    @Transactional
    @Secured("Permissions checks done internally")
    public void setPermission(@Nonnull SetPermissionRequest request) {
        Permission permission = request.getPermission();
        Project project = request.getProject();
        Repository repository = request.getRepository();

        boolean hasInsufficientGlobalPermission = permission.isGlobal()
                && !permissionService.hasGlobalPermission(Permission.max(permission, ADMIN));
        boolean hasInsufficientProjectPermission = permission.isResource(Project.class)
                && !permissionService.hasProjectPermission(project, PROJECT_ADMIN);
        boolean hasInsufficientRepositoryPermission = permission.isResource(Repository.class)
                && !permissionService.hasRepositoryPermission(repository, REPO_ADMIN);

        boolean hasInsufficientPermission = hasInsufficientGlobalPermission
                || hasInsufficientProjectPermission
                || hasInsufficientRepositoryPermission;

        if (hasInsufficientPermission) {
            throw new AuthorisationException(
                    i18nService.createKeyedMessage("bitbucket.service.permissionadmin.usercannotgrantpermission", permission));
        } else {
            doSetPermission(request);
        }
    }

    private void doSetPermission(@Nonnull SetPermissionRequest request) {
        Permission permission = request.getPermission();
        Project project = request.getProject();
        Repository repository = request.getRepository();
        Set<ApplicationUser> users = request.getUsers();
        Set<String> groups = request.getGroups();

        validateGrantablePermission(permission);

        if (permission.isGlobal()) {
            for (ApplicationUser user : users) {
                Permission revoked = doRevokeAllGlobalPermissions(user);
                fireGlobalPermissionRequestedEvent(revoked, permission, null, user);
                grantGlobalPermission(permission, user);
                fireGlobalPermissionEvent(revoked, permission, null, user);
            }
            for (String group : groups) {
                Permission revoked = doRevokeAllGlobalPermissions(group);
                fireGlobalPermissionRequestedEvent(revoked, permission, group, null);
                grantGlobalPermission(permission, group);
                fireGlobalPermissionEvent(revoked, permission, group, null);
            }
        } else if (permission.isResource(Project.class)) {
            for (ApplicationUser user : users) {
                Permission revoked = doRevokeAllProjectPermissions(project, user);
                fireProjectPermissionRequestedEvent(project, revoked, permission, null, user);
                grantPermission(new InternalProjectPermission.Builder()
                        .permission(permission)
                        .project(InternalConverter.convertToInternalProject(project))
                        .user(InternalConverter.convertToInternalUser(user))
                        .build());
                fireProjectPermissionEvent(project, revoked, permission, null, user);
            }
            for (String group : groups) {
                Permission revoked = doRevokeAllProjectPermissions(project, group);
                fireProjectPermissionRequestedEvent(project, revoked, permission, group, null);
                grantPermission(new InternalProjectPermission.Builder()
                        .permission(permission)
                        .project(InternalConverter.convertToInternalProject(project))
                        .group(group)
                        .build());
                fireProjectPermissionEvent(project, revoked, permission, group, null);
            }
        } else if (permission.isResource(Repository.class)) {
            for (ApplicationUser user : users) {
                Permission revoked = doRevokeAllRepositoryPermissions(repository, user);
                fireRepositoryPermissionRequestedEvent(repository, revoked, permission, null, user);
                grantPermission(new InternalRepositoryPermission.Builder()
                        .permission(permission)
                        .repository(InternalConverter.convertToInternalRepository(repository))
                        .user(InternalConverter.convertToInternalUser(user))
                        .build());
                fireRepositoryPermissionEvent(repository, revoked, permission, null, user);
            }
            for (String group : groups) {
                Permission revoked = doRevokeAllRepositoryPermissions(repository, group);
                fireRepositoryPermissionRequestedEvent(repository, revoked, permission, group, null);
                grantPermission(new InternalRepositoryPermission.Builder()
                        .permission(permission)
                        .repository(InternalConverter.convertToInternalRepository(repository))
                        .group(group)
                        .build());
                fireRepositoryPermissionEvent(repository, revoked, permission, group, null);
            }
        } else {
            throw new ServerException(i18nService.createKeyedMessage(
                    "bitbucket.service.permissionadmin.cannotgrantunknownpermission", Product.NAME, permission));
        }
    }

    private PageProvider<ApplicationUser> createUserProvider(UserType userType, String usernamePattern) {
        return request -> {
            Page<? extends ApplicationUser> page = userType == UserType.NORMAL
                    ? userService.findUsersByName(usernamePattern, request)
                    : userService.findServiceUsersByName(usernamePattern, request);

            return PageUtils.asPageOf(ApplicationUser.class, page);
        };
    }

    @Override
    @PreAuthorize("hasProjectPermission(#project, 'PROJECT_ADMIN')")
    public boolean hasAllProjectPermission(@Nonnull Permission permission, @Nonnull Project project) {
        validateProjectPermission(permission);
        validateGrantablePermission(permission);
        validateProject(project);

        return projectPermissionDao.hasPermissionEntry(new InternalProjectPermission.Builder()
                .permission(permission)
                .project(InternalConverter.convertToInternalProject(project))
                .build());
    }

    @Override
    @PreAuthorize("hasProjectPermission(#request.project, 'PROJECT_ADMIN')")
    public Permission getProjectPermission(@Nonnull ProjectPermissionRequest request) {
        checkNotNull(request, "request");
        return projectPermissionDao.getHighestPermissionForUser(request.getUser().getId(), request.getProject().getId());
    }

    @Override
    @PreAuthorize("hasRepositoryPermission(#request.repository, 'REPO_ADMIN')")
    public Permission getRepositoryPermission(@Nonnull RepositoryPermissionRequest request) {
        checkNotNull(request, "request");
        return repositoryPermissionDao.getHighestPermissionForUser(request.getUser().getId(), request.getRepository().getId());
    }

    private void grantGlobalPermission(@Nonnull Permission permission, @Nonnull ApplicationUser user) {
        // ask the licensing service if this is ok
        licenseService.validateCanLicenseUser(user, permission);

        InternalGlobalPermission perm = buildPermission(permission, null, user);
        grantPermission(perm);
    }

    private void grantGlobalPermission(@Nonnull Permission permission, @Nonnull String group) {
        // ask the licensing service if this is ok
        licenseService.validateCanLicenseGroup(group, permission);

        InternalGlobalPermission perm = buildPermission(permission, group, null);
        grantPermission(perm);
    }

    private void grantPermission(InternalGlobalPermission perm) {
        if (!globalPermissionDao.hasPermissionEntry(perm)) {
            globalPermissionDao.create(perm);
        }
    }

    private boolean grantPermission(InternalProjectPermission perm) {
        if (perm.getProject().getType() == ProjectType.PERSONAL) {
            throw new IntegrityException(i18nService.createKeyedMessage("bitbucket.service.permissionadmin.personalproject.grant"));
        }

        if (!projectPermissionDao.hasPermissionEntry(perm)) {
            projectPermissionDao.create(perm);

            return true;
        }

        return false;
    }

    private void grantPermission(InternalRepositoryPermission perm) {
        if (!repositoryPermissionDao.hasPermissionEntry(perm)) {
            repositoryPermissionDao.create(perm);
        }
    }

    @Override
    @Transactional
    @PreAuthorize("hasProjectPermission(#project, 'PROJECT_ADMIN')")
    public void grantAllProjectPermission(@Nonnull Permission permission, @Nonnull Project project) {
        validateProjectPermission(permission);
        validateGrantablePermission(permission);
        validateProject(project);
        InternalProjectPermission perm = new InternalProjectPermission.Builder()
                .permission(permission)
                .project(InternalConverter.convertToInternalProject(project))
                .build();
        Permission previousPermission = projectPermissionDao.getHighestDefaultPermission(project.getId());
        boolean granted = grantPermission(perm);
        if (granted) {
            boolean effectivePermissionChange = previousPermission == null || previousPermission.getImplyingPermissions().contains(permission);
            if (effectivePermissionChange) {
                fireProjectPermissionRequestedEvent(project, previousPermission, permission, null, null);
                fireProjectPermissionEvent(project, previousPermission, permission, null, null);
            }
            // else there is no change in effective permission, we still create the entry in the database due to a failure
            // in API design for default project permissions.
        }
    }

    private boolean revokeRepositoryPermission(@Nonnull Permission permission, @Nonnull Repository repository, @Nonnull ApplicationUser user) {
        validateRepository(repository);
        validateRepositoryPermission(permission);
        validateGrantablePermission(permission);

        // revoke is always allowed when revoking someone else's permission
        boolean revokeAllowed = !user.equals(authenticationContext.getCurrentUser());

        if (!revokeAllowed) {
            // revoke not yet allowed since user is working on their own permissions
            // we gate now if they are revoking a permission and they hold a higher permission
            revokeAllowed = currentUserHasImpliedRepositoryPermission(permission, repository);
        }

        if (!revokeAllowed) {
            // The current user is removing their own permission and they do not have a higher permission
            // Gate through now if they have the permission through group membership
            revokeAllowed = permissionService.hasRepositoryPermissionThroughGroupMembership(repository, permission, Collections.<String>emptySet());
        }

        if (!revokeAllowed) {
            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.permissionadmin.cantrevokeownrepositorypermission", repository.getName());
            throw new IntegrityException(message);
        }
        return revokePermission(new InternalRepositoryPermission.Builder()
                .permission(permission)
                .repository(InternalConverter.convertToInternalRepository(repository))
                .user(InternalConverter.convertToInternalUser(user))
                .build());
    }

    private boolean revokeRepositoryPermission(@Nonnull Permission permission, @Nonnull Repository repository, @Nonnull String group) {
        validateRepository(repository);
        validateRepositoryPermission(permission);
        validateGrantablePermission(permission);
        ApplicationUser currentUser = authenticationContext.getCurrentUser();

        // Revoke can always go ahead if the user is removing permission from a group
        // of which they are not a member
        boolean revokeAllowed = currentUser == null || !userService.isUserInGroup(currentUser, group);

        if (!revokeAllowed) {
            // revoke not yet allowed since user is working on their own group
            // we gate now if they are revoking a permission and they hold a higher permission
            revokeAllowed = currentUserHasImpliedRepositoryPermission(permission, repository);
        }

        if (!revokeAllowed) {
            // The current user is removing the permission of a group of which they are a member and they do not have a higher permission
            // Gate through now if they have explicit user permission
            revokeAllowed = permissionService.hasDirectRepositoryUserPermission(repository, permission);
        }

        if (!revokeAllowed) {
            // The user is removing a group permission which gives them their own permission and they do not have
            // direct permission themselves. Do they have permission through another group?
            revokeAllowed = permissionService.hasRepositoryPermissionThroughGroupMembership(repository, permission, Collections.singleton(group));
        }

        if (!revokeAllowed) {
            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.permissionadmin.cantrevokegrouprepositorypermission",
                    repository.getName(), group);
            throw new IntegrityException(message);
        }

        return revokePermission(new InternalRepositoryPermission.Builder()
                .permission(permission)
                .repository(InternalConverter.convertToInternalRepository(repository))
                .group(group)
                .build());
    }

    /**
     * Check whether the context user has the specified permission for this repository implied by a higher permission.
     *
     * @param permission a repository permission
     * @param repository the repository to check permissions against
     * @return true if the current user has the specified permission 'implied' by a higher permission, granted either
     * globally (e.g. ADMIN or SYS_ADMIN) or for the current project
     * @see Permission#getImplyingPermissions()
     */
    private boolean currentUserHasImpliedRepositoryPermission(Permission permission, Repository repository) {
        return Iterables.any(permission.getImplyingPermissions(), higherPermission -> {
            // some global permissions imply repository permissions, so check if the implying permission is global
            if (higherPermission.isGlobal()) {
                return permissionService.hasGlobalPermission(higherPermission);
            } else if (higherPermission.isResource(Project.class)) {
                return permissionService.hasProjectPermission(repository.getProject(), higherPermission);
            } else if (higherPermission.isResource(Repository.class)) {
                return permissionService.hasRepositoryPermission(repository, higherPermission);
            }
            return false;
        });
    }

    private boolean revokeProjectPermission(@Nonnull Permission permission, @Nonnull Project project, @Nonnull ApplicationUser user) {
        // revoke is always allowed when revoking someone else's permission
        boolean revokeAllowed = !user.equals(authenticationContext.getCurrentUser());

        if (!revokeAllowed) {
            // revoke not yet allowed since user is working on their own permissions
            // we gate now if they are revoking a permission and they hold a higher permission
            revokeAllowed = currentUserHasImpliedProjectPermission(permission, project);
        }

        if (!revokeAllowed) {
            // The current user is removing their own permission and they do not have a higher permission
            // Gate through now if they have the permission through group membership
            revokeAllowed = permissionService.hasProjectPermissionThroughGroupMembership(project, permission, Collections.<String>emptySet());
        }

        if (revokeAllowed) {
            return revokePermission(new InternalProjectPermission.Builder()
                    .permission(permission)
                    .project(InternalConverter.convertToInternalProject(project))
                    .user(InternalConverter.convertToInternalUser(user))
                    .build());
        } else {
            KeyedMessage message = i18nService.createKeyedMessage(
                    "bitbucket.service.permissionadmin.cantrevokeownprojectpermission", project.getName());
            throw new IntegrityException(message);
        }
    }

    private boolean revokeProjectPermission(@Nonnull Permission permission, @Nonnull Project project, @Nonnull String group) {
        ApplicationUser currentUser = authenticationContext.getCurrentUser();

        // Revoke can always go ahead if the user is removing permission from a group
        // of which they are not a member
        boolean revokeAllowed = currentUser == null || !userService.isUserInGroup(currentUser, group);

        if (!revokeAllowed) {
            // revoke not yet allowed since user is working on their own group
            // we gate now if they are revoking a permission and they hold a higher permission
            revokeAllowed = currentUserHasImpliedProjectPermission(permission, project);
        }

        if (!revokeAllowed) {
            // The current user is removing the permission of a group of which they are a member and they do not have a higher permission
            // Gate through now if they have explicit user permission
            revokeAllowed = permissionService.hasDirectProjectUserPermission(project, permission);
        }

        if (!revokeAllowed) {
            // The user is removing a group permission which gives them their own permission and they do not have
            // direct permission themselves. Do they have permission through another group?
            revokeAllowed = permissionService.hasProjectPermissionThroughGroupMembership(project, permission, Collections.singleton(group));
        }

        if (revokeAllowed) {
            return revokePermission(new InternalProjectPermission.Builder()
                    .permission(permission)
                    .project(InternalConverter.convertToInternalProject(project))
                    .group(group)
                    .build());
        } else {
            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.permissionadmin.cantrevokegroupprojectpermission",
                    project.getName(), group);
            throw new IntegrityException(message);
        }
    }

    /**
     * Check whether the context user has the specified permission for this project implied by a higher permission.
     *
     * @param permission a project permission
     * @param project    the project to check permissions against
     * @return true if the current user has the specified permission 'implied' by a higher permission, granted either
     * globally (e.g. ADMIN or SYS_ADMIN) or for the current project
     * @see Permission#getImplyingPermissions()
     */
    private boolean currentUserHasImpliedProjectPermission(Permission permission, Project project) {
        return Iterables.any(permission.getImplyingPermissions(), higherPermission -> {
            // some global permissions imply project permissions, so check if the implying permission is global
            if (higherPermission.isGlobal()) {
                return permissionService.hasGlobalPermission(higherPermission);
            } else if (higherPermission.isResource(Project.class)) {
                return permissionService.hasProjectPermission(project, higherPermission);
            }
            return false;
        });
    }

    private boolean revokeGlobalPermission(@Nonnull Permission permission, @Nonnull ApplicationUser user) {
        // revoke is always allowee else's permission
        boolean revokeAllowed = !user.equals(authenticationContext.getCurrentUser());

        if (!revokeAllowed) {
            // revoke not yet allowed since user is working on their own permissions
            // we gate now if they are revoking a permission and they hold a higher permission
            Set<Permission> implyingPermissions = permission.getImplyingPermissions();

            for (Permission higherPermission : implyingPermissions) {
                revokeAllowed = (permissionService.hasGlobalPermission(higherPermission));
                if (revokeAllowed) {
                    break;
                }
            }
        }

        if (!revokeAllowed) {
            // The current user is removing their own permission and they do not have a higher permission
            // Gate through now if they have the permission through group membership
            revokeAllowed = permissionService.hasGlobalPermissionThroughGroupMembership(permission, Collections.<String>emptySet());
        }

        if (revokeAllowed) {
            return revokePermission(new InternalGlobalPermission.Builder()
                    .permission(permission)
                    .user(InternalConverter.convertToInternalUser(user))
                    .build());
        } else {
            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.permissionadmin.cantrevokeownpermission");
            throw new IntegrityException(message);
        }
    }

    private boolean revokePermission(InternalGlobalPermission globalPermission) {
        return globalPermissionDao.revoke(globalPermission) > 0;
    }

    private boolean revokePermission(InternalProjectPermission projectPermission) {
        if (projectPermission.getProject().getType() == ProjectType.PERSONAL) {
            throw new IntegrityException(i18nService.createKeyedMessage("bitbucket.service.permissionadmin.personalproject.revoke"));
        }

        return projectPermissionDao.revoke(projectPermission) > 0;
    }

    private boolean revokePermission(InternalRepositoryPermission repositoryPermission) {
        return repositoryPermissionDao.revoke(repositoryPermission) > 0;
    }

    private boolean revokeGlobalPermission(@Nonnull Permission permission, @Nonnull String group) {
        ApplicationUser currentUser = convertToInternalUser(authenticationContext.getCurrentUser());

        // Revoke can always go ahead if the user is removing permission from a group
        // of which they are not a member
        boolean revokeAllowed = currentUser == null || !userService.isUserInGroup(currentUser, group);

        if (!revokeAllowed) {
            // revoke not yet allowed since user is working on their own group
            // we gate now if they are revoking a permission and they hold a higher permission
            Set<Permission> implyingPermissions = permission.getImplyingPermissions();

            for (Permission higherPermission : implyingPermissions) {
                revokeAllowed = (permissionService.hasGlobalPermission(higherPermission));
                if (revokeAllowed) {
                    break;
                }
            }
        }

        if (!revokeAllowed) {
            // The current user is removing the permission of a group of which they are a member and they do not have a higher permission
            // Gate through now if they have explicit user permission
            revokeAllowed = permissionService.hasDirectGlobalUserPermission(permission);
        }

        if (!revokeAllowed) {
            // The user is removing a group permission which gives them their own permission and they does not have
            // direct permission themselves. Do they have permission through another group?
            revokeAllowed = permissionService.hasGlobalPermissionThroughGroupMembership(permission, Collections.singleton(group));
        }

        if (revokeAllowed) {
            return revokePermission(new InternalGlobalPermission.Builder()
                    .permission(permission)
                    .group(group)
                    .build());
        } else {
            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.permissionadmin.cantrevokegrouppermission", group);
            throw new IntegrityException(message);
        }
    }


    @Override
    @Transactional
    @PreAuthorize("hasProjectPermission(#project, 'PROJECT_ADMIN')")
    public void revokeAllProjectPermission(@Nonnull Permission permission, @Nonnull Project project) {
        validateProjectPermission(permission);
        validateGrantablePermission(permission);
        validateProject(project);

        boolean revoked = revokePermission(new InternalProjectPermission.Builder()
                .permission(permission)
                .project(InternalConverter.convertToInternalProject(project))
                .build());
        if (revoked) {
            Permission newPermission = projectPermissionDao.getHighestDefaultPermission(project.getId());
            boolean effectivePermissionChange = newPermission == null || newPermission.getImplyingPermissions().contains(permission);
            if (effectivePermissionChange) {
                fireProjectPermissionRequestedEvent(project, permission, newPermission, null, null);
                fireProjectPermissionEvent(project, permission, newPermission, null, null);
            }
        }
    }

    @Override
    @Transactional
    @PreAuthorize("not isCurrentUser(#username) and (hasGlobalPermission('SYS_ADMIN') or (hasGlobalPermission('ADMIN') and not hasGlobalPermission(#username, 'SYS_ADMIN')))")
    public void revokeAllUserPermissions(@Nonnull String username) {
        validateUser(username); // both isCurrentUser(username) and hasGlobalPermission(username, perm) accepts null usernames

        ApplicationUser user = userService.getUserByName(username, true);
        if (user != null) {
            revokeAllUserPermissions(user);
        }
    }

    @Override
    @Transactional
    @PreAuthorize("not isCurrentUser(#user) and (hasGlobalPermission('SYS_ADMIN') or (hasGlobalPermission('ADMIN') and not hasGlobalPermission(#user, 'SYS_ADMIN')))")
    public void revokeAllUserPermissions(@Nonnull ApplicationUser user) {
        checkNotNull(user, "user");

        globalPermissionDao.revokeAll(user.getId());
        projectPermissionDao.revokeAll(user.getId());
        repositoryPermissionDao.revokeAll(user.getId());
    }

    @Override
    @Transactional
    @PreAuthorize("hasGlobalPermission('ADMIN')")
    public void revokeAllGroupPermissions(@Nonnull String name) {
        validateGroup(name);
        canRemovePermissionsFromGroup(name);

        globalPermissionDao.revokeAll(name);
        projectPermissionDao.revokeAll(name);
        repositoryPermissionDao.revokeAll(name);
    }

    private void canRemovePermissionsFromGroup(String name) {
        try {
            canDeleteGroup(name);
        } catch (IntegrityException e) {
            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.permissionadmin.cannotremovepermissionsgroup");
            throw new IntegrityException(message);
        } catch (ForbiddenException e) {
            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.permissionadmin.cannotdeletegroup",
                    "You cannot remove the permissions of the group {0} as it has System Administrator privileges, and you are" +
                            " not a System Administrator.", name);
            throw new ForbiddenException(message);
        }
    }

    @Override
    @PreAuthorize("hasGlobalPermission('ADMIN')")
    public void canRemoveUserFromGroup(@Nonnull String username, @Nonnull String group) {
        validateUser(username);
        validateGroup(group);

        ApplicationUser currentUser = authenticationContext.getCurrentUser();

        if (currentUser != null && IdentifierUtils.equalsInLowerCase(currentUser.getName(), username)) {
            // removing self from group
            if (!userService.isUserInGroup(currentUser, group)) {
                // user is not a member of the group so this can proceed
                return;
            }
            if (!canRemoveCurrentUserFromGroup(group)) {
                KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.permissionadmin.cannotremoveself");
                throw new IntegrityException(message);
            }
        } else {
            // removing another user from a group. This is ok unless the group has sys-admin and user does not have
            // sys-admin
            if (permissionService.hasGlobalGroupPermission(SYS_ADMIN, group)
                    && !permissionService.hasGlobalPermission(SYS_ADMIN)) {
                KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.permissionadmin.cannotremoveuser", username, group);
                throw new ForbiddenException(message);
            }
        }

    }

    private boolean canRemoveCurrentUserFromGroup(String group) {
        if (permissionService.hasGlobalGroupPermission(SYS_ADMIN, group)) {
            // The group has sys-admin privilege. The user can only be removed from the group if they have sys-admin
            // through a different means
            if (!permissionService.hasDirectGlobalUserPermission(SYS_ADMIN)
                    && !permissionService.hasGlobalPermissionThroughGroupMembership(SYS_ADMIN,
                    Collections.singleton(group))) {
                return false;
            }
        } else if (permissionService.hasGlobalGroupPermission(Permission.ADMIN, group)) {
            // The group has admin privilege. This can proceed if the user has sys-admin or
            // they have admin privilege through another means
            if (!permissionService.hasGlobalPermission(SYS_ADMIN)
                    && !permissionService.hasDirectGlobalUserPermission(Permission.ADMIN)
                    && !permissionService.hasGlobalPermissionThroughGroupMembership(Permission.ADMIN,
                    Collections.singleton(group))) {
                return false;
            }
        }
        return true;
    }

    @Override
    @PreAuthorize("hasGlobalPermission('ADMIN')")
    public void canAddUserToGroup(@Nonnull String group) {
        validateGroup(group);

        // We can only add a user to a group if we are a sys-admin or the group to which we are adding the user
        // does not have sys-admin privilege
        if (!permissionService.hasGlobalPermission(SYS_ADMIN)
                && permissionService.hasGlobalGroupPermission(SYS_ADMIN, group)) {
            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.permissionadmin.cannotaddusertogroup", group);
            throw new ForbiddenException(message);
        }
    }

    @Override
    @PreAuthorize("hasGlobalPermission('ADMIN')")
    public void canDeleteGroup(@Nonnull String group) {
        validateGroup(group);

        ApplicationUser currentUser = authenticationContext.getCurrentUser();
        if (currentUser != null && userService.isUserInGroup(currentUser, group)) {
            // we can delete the group if we can safely remove this user from the group
            if (!canRemoveCurrentUserFromGroup(group)) {
                KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.permissionadmin.cannotremovegroup");
                throw new IntegrityException(message);
            }
        } else {
            // we are deleting a group, which we are not a member of
            // we can not delete if we do not have SYS-ADMIN but the group does
            if (!permissionService.hasGlobalPermission(SYS_ADMIN)
                    && permissionService.hasGlobalGroupPermission(SYS_ADMIN, group)) {
                KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.permissionadmin.cannotdeletegroup", group);
                throw new ForbiddenException(message);
            }
        }
    }

    @Override
    @PreAuthorize("hasGlobalPermission('ADMIN')")
    public void canDeleteUser(@Nonnull String username) {
        validateUser(username);

        ApplicationUser currentUser = authenticationContext.getCurrentUser();
        // A user cannot delete themselves
        if (currentUser != null && IdentifierUtils.equalsInLowerCase(currentUser.getName(), username)) {
            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.permissionadmin.selfdelete");
            throw new IntegrityException(message);
        }

        ApplicationUser user = userService.getUserByName(username);
        if (user == null) {
            // there is no such user, so they sure can be deleted in crowd
            return;
        }

        // We do not allow a user to delete a sys-admin if the user themselves is not a sys-admin
        if (permissionService.hasGlobalPermission(user, SYS_ADMIN)
                && !permissionService.hasGlobalPermission(SYS_ADMIN)) {
            KeyedMessage message = i18nService.createKeyedMessage("bitbucket.service.permissionadmin.cannotdeleteuser", user.getName());
            throw new ForbiddenException(message);
        }
    }

    @EventListener
    public void onGroupDeleted(GroupCleanupEvent event) {
        //DO NOT CALL revokeAllGroupPermissions here! The validation performed by canRemovePermissionsFromGroup, called
        //by that method, can only trigger exceptions that will be swallowed by the event framework. It is not possible,
        //in this method, to prevent group deletion. It's only possible to clean up after the deleted group.
        globalPermissionDao.revokeAll(event.getGroup());
        projectPermissionDao.revokeAll(event.getGroup());
        repositoryPermissionDao.revokeAll(event.getGroup());
    }

    @EventListener
    public void onUserDeleted(UserCleanupEvent event) {
        //DO NOT CALL revokeAllUserPermissions here! The event provides the deleted ApplicationUser already; there is no need
        //to perform another database query for it. As with onGroupDeleted, it is not possible to prevent user deletion
        //in this method. It's only possible to clean up after the deleted user.
        //
        //Note: This will also revoke a user's access to its personal project. That access is restored in another event
        //listener in ProjectServiceImpl
        globalPermissionDao.revokeAll(event.getDeletedUser().getId());
        projectPermissionDao.revokeAll(event.getDeletedUser().getId());
        repositoryPermissionDao.revokeAll(event.getDeletedUser().getId());
    }

    private InternalGlobalPermission buildPermission(Permission permission, String group, ApplicationUser user) {
        return new InternalGlobalPermission.Builder()
                .permission(permission)
                .user(InternalConverter.convertToInternalUser(user))
                .group(group)
                .build();
    }

    private void fireGlobalPermissionEvent(Permission oldValue, Permission newValue,
                                           String group, ApplicationUser user) {
        eventPublisher.publish(visitPermissionAction(oldValue, newValue, new PermissionActionVisitor<PermissionEvent>() {
            @Override
            public GlobalPermissionGrantedEvent visitPermissionGranted() {
                return new GlobalPermissionGrantedEvent(DefaultPermissionAdminService.this, newValue, group, user);
            }

            @Override
            public GlobalPermissionModifiedEvent visitPermissionModified() {
                return new GlobalPermissionModifiedEvent(DefaultPermissionAdminService.this, oldValue, newValue, group, user);
            }

            @Override
            public GlobalPermissionRevokedEvent visitPermissionRevoked() {
                return new GlobalPermissionRevokedEvent(DefaultPermissionAdminService.this, oldValue, group, user);
            }
        }));
    }

    private void fireGlobalPermissionRequestedEvent(Permission oldValue, Permission newValue,
                                                    String group, ApplicationUser user) {
        SimpleCancelState cancelState = new SimpleCancelState();
        eventPublisher.publish(visitPermissionAction(oldValue, newValue, new PermissionActionVisitor<PermissionEvent>() {
            @Override
            public GlobalPermissionGrantRequestedEvent visitPermissionGranted() {
                return new GlobalPermissionGrantRequestedEvent(DefaultPermissionAdminService.this, newValue, group, user, cancelState);
            }

            @Override
            public GlobalPermissionModificationRequestedEvent visitPermissionModified() {
                return new GlobalPermissionModificationRequestedEvent(DefaultPermissionAdminService.this, oldValue, newValue, group, user, cancelState);
            }

            @Override
            public GlobalPermissionRevocationRequestedEvent visitPermissionRevoked() {
                return new GlobalPermissionRevocationRequestedEvent(DefaultPermissionAdminService.this, oldValue, group, user, cancelState);
            }
        }));

        maybeCancelRequest(oldValue, newValue, cancelState);
    }

    private void fireProjectPermissionEvent(Project project,
                                            Permission oldValue, Permission newValue,
                                            String group, ApplicationUser user) {
        eventPublisher.publish(visitPermissionAction(oldValue, newValue, new PermissionActionVisitor<ProjectPermissionEvent>() {
            @Override
            public ProjectPermissionGrantedEvent visitPermissionGranted() {
                return new ProjectPermissionGrantedEvent(DefaultPermissionAdminService.this, newValue, project, group, user);
            }

            @Override
            public ProjectPermissionModifiedEvent visitPermissionModified() {
                return new ProjectPermissionModifiedEvent(DefaultPermissionAdminService.this, oldValue, newValue, project, group, user);
            }

            @Override
            public ProjectPermissionRevokedEvent visitPermissionRevoked() {
                return new ProjectPermissionRevokedEvent(DefaultPermissionAdminService.this, oldValue, project, group, user);
            }
        }));
    }

    private void fireProjectPermissionRequestedEvent(Project project,
                                                     Permission oldValue, Permission newValue,
                                                     String group, ApplicationUser user) {
        SimpleCancelState cancelState = new SimpleCancelState();
        eventPublisher.publish(visitPermissionAction(oldValue, newValue, new PermissionActionVisitor<PermissionEvent>() {
            @Override
            public ProjectPermissionGrantRequestedEvent visitPermissionGranted() {
                return new ProjectPermissionGrantRequestedEvent(DefaultPermissionAdminService.this, newValue, project, group, user, cancelState);
            }

            @Override
            public ProjectPermissionModificationRequestedEvent visitPermissionModified() {
                return new ProjectPermissionModificationRequestedEvent(DefaultPermissionAdminService.this, oldValue, newValue, project, group, user, cancelState);
            }

            @Override
            public ProjectPermissionRevocationRequestedEvent visitPermissionRevoked() {
                return new ProjectPermissionRevocationRequestedEvent(DefaultPermissionAdminService.this, oldValue, project, group, user, cancelState);
            }
        }));

        maybeCancelRequest(oldValue, newValue, cancelState);
    }

    private void fireRepositoryPermissionEvent(Repository repository,
                                               Permission oldValue, Permission newValue,
                                               String group, ApplicationUser user) {
        eventPublisher.publish(visitPermissionAction(oldValue, newValue, new PermissionActionVisitor<RepositoryPermissionEvent>() {
            @Override
            public RepositoryPermissionGrantedEvent visitPermissionGranted() {
                return new RepositoryPermissionGrantedEvent(DefaultPermissionAdminService.this, newValue, repository, group, user);
            }

            @Override
            public RepositoryPermissionModifiedEvent visitPermissionModified() {
                return new RepositoryPermissionModifiedEvent(DefaultPermissionAdminService.this, oldValue, newValue, repository, group, user);
            }

            @Override
            public RepositoryPermissionRevokedEvent visitPermissionRevoked() {
                return new RepositoryPermissionRevokedEvent(DefaultPermissionAdminService.this, oldValue, repository, group, user);
            }
        }));

    }

    private void fireRepositoryPermissionRequestedEvent(Repository repository,
                                                        Permission oldValue, Permission newValue,
                                                        String group, ApplicationUser user) {
        SimpleCancelState cancelState = new SimpleCancelState();
        eventPublisher.publish(visitPermissionAction(oldValue, newValue, new PermissionActionVisitor<RepositoryPermissionEvent>() {
            @Override
            public RepositoryPermissionGrantRequestedEvent visitPermissionGranted() {
                return new RepositoryPermissionGrantRequestedEvent(DefaultPermissionAdminService.this, newValue, repository, group, user, cancelState);
            }

            @Override
            public RepositoryPermissionModificationRequestedEvent visitPermissionModified() {
                return new RepositoryPermissionModificationRequestedEvent(DefaultPermissionAdminService.this, oldValue, newValue, repository, group, user, cancelState);
            }

            @Override
            public RepositoryPermissionRevocationRequestedEvent visitPermissionRevoked() {
                return new RepositoryPermissionRevocationRequestedEvent(DefaultPermissionAdminService.this, oldValue, repository, group, user, cancelState);
            }
        }));

        maybeCancelRequest(oldValue, newValue, cancelState);
    }

    private Page<ApplicationUser> getUsersWithoutPermission(Predicate<ApplicationUser> pageFilter,
                                                      UserType userType,
                                                      String filter,
                                                      PageRequest request) {
        PageProvider<ApplicationUser> userProvider = createUserProvider(userType, filter);
        Predicate<ApplicationUser> userMatchFilter = new UserPageFilter(filter);
        PageRequest pageRequest = request.buildRestrictedPageRequest(maxUsers);

        return filterPages(userProvider, userMatchFilter.and(pageFilter), pageRequest);
    }

    private Page<String> getGroupsWithoutPermission(String groupnamePattern,
                                                    Predicate<String> groupsWithoutPermissionFilter,
                                                    PageRequest request) {

        PageProvider<String> groupsProvider = request1 -> userService.findGroupsByName(groupnamePattern, request1);

        Predicate<String> groupPageFilter = new GroupPageFilter(groupnamePattern);

        PageRequest pageRequest = request.buildRestrictedPageRequest(maxGroups);
        //Transform all group names to lowercase. This ensures getGroupsWithoutProjectPermission and getGroupsWithoutGlobalPermission
        //use the same case (lowercase) as getGroupsWithPermission and getGroupsWithGlobalPermission
        return filterPages(groupsProvider, groupPageFilter.and(groupsWithoutPermissionFilter), pageRequest)
                .transform(IdentifierUtils.TO_LOWER_CASE::apply);
    }

    /**
     * @throws RequestCanceledException if the permission action was cancelled by an event listener. The exception is
     *                                  unchecked, and as a result any work done previous to this exception will be
     *                                  rolled back at the transaction boundary
     */
    private void maybeCancelRequest(Permission oldValue, Permission newValue, SimpleCancelState cancelState) {
        if (cancelState.isCanceled()) {
            throw visitPermissionAction(oldValue, newValue, new PermissionActionVisitor<RequestCanceledException>() {
                @Override
                public PermissionGrantCanceledException visitPermissionGranted() {
                    return new PermissionGrantCanceledException(
                            i18nService.createKeyedMessage("bitbucket.service.permissionadmin.grantcanceled"),
                            cancelState.getCancelMessages());
                }

                @Override
                public PermissionModificationCanceledException visitPermissionModified() {
                    return new PermissionModificationCanceledException(
                            i18nService.createKeyedMessage("bitbucket.service.permissionadmin.modificationcanceled"),
                            cancelState.getCancelMessages());
                }

                @Override
                public PermissionRevocationCanceledException visitPermissionRevoked() {
                    return new PermissionRevocationCanceledException(
                            i18nService.createKeyedMessage("bitbucket.service.permissionadmin.revocationcanceled"),
                            cancelState.getCancelMessages());
                }
            });
        }
    }

    /**
     * @param provider a group page provider for the groups to exclude
     * @return a stateful Predicate which filters a target iterator to not contain groups in the specified provider
     */
    @VisibleForTesting
    static Predicate<String> withoutGroups(PageProvider<String> provider) {
        return new InIteratorPredicate<>(
                CASE_INSENSITIVE_COMPARATOR,
                new PagedIterable<>(provider, INTERNAL_PAGE_LIMIT).iterator()
        ).negate();
    }

    /**
     * @param provider a user page provider for the users to exclude
     * @return a stateful Predicate which filters a target iterator to not contain users in the specified provider
     */
    @VisibleForTesting
    static Predicate<ApplicationUser> withoutUsers(PageProvider<ApplicationUser> provider) {
        return new InIteratorPredicate<>(
                CASE_INSENSITIVE_NAME_COMPARATOR,
                new PagedIterable<>(provider, INTERNAL_PAGE_LIMIT).iterator()
        ).negate();
    }

    private static <T> T visitPermissionAction(Permission existingPermission, Permission newPermission, PermissionActionVisitor<T> visitor) {
        if (existingPermission == null) {
            return visitor.visitPermissionGranted();
        } else if (newPermission == null) {
            return visitor.visitPermissionRevoked();
        } else {
            return visitor.visitPermissionModified();
        }
    }

    private Predicate<ApplicationUser> licensedOnly() {
        return user -> permissionService.hasGlobalPermission(user, Permission.LICENSED_USER);
    }

    private <R> R validateRequest(R request) {
        return checkNotNull(request, "request");
    }

    private interface PermissionActionVisitor<T> {
        T visitPermissionGranted();

        T visitPermissionModified();

        T visitPermissionRevoked();
    }

}
