package com.atlassian.stash.internal.rest.user;

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.permission.*;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.rest.BadRequestException;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.permission.RestPermitted;
import com.atlassian.bitbucket.rest.user.RestApplicationUser;
import com.atlassian.bitbucket.rest.user.RestDetailedGroup;
import com.atlassian.bitbucket.rest.user.RestPermittedGroup;
import com.atlassian.bitbucket.rest.user.RestPermittedUser;
import com.atlassian.bitbucket.rest.util.ResourcePatterns;
import com.atlassian.bitbucket.rest.util.ResponseFactory;
import com.atlassian.bitbucket.rest.util.RestPage;
import com.atlassian.bitbucket.rest.util.RestUtils;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.UserService;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.sun.jersey.spi.resource.Singleton;

import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.Set;

/**
 * @response.representation.4xx.example {@link RestErrors#EXAMPLE}
 */
@Path(ResourcePatterns.PROJECT_URI + "/permissions")
@Consumes({MediaType.APPLICATION_JSON})
@Produces({RestUtils.APPLICATION_JSON_UTF8})
@Singleton
@AnonymousAllowed
public class ProjectPermissionResource extends AbstractPermissionResource {

    public ProjectPermissionResource(PermissionAdminService permissionAdminService,
                                     UserService userService,
                                     I18nService i18nService) {
        super(i18nService, permissionAdminService, userService);
    }

    /**
     * Retrieve a page of groups that have been granted at least one permission for the specified project.
     * <p>
     * The authenticated user must have <strong>PROJECT_ADMIN</strong> permission for the specified project or a higher
     * global permission to call this resource.
     *
     * @param filter if specified only group names containing the supplied string will be returned
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestPermittedGroup#PAGE_EXAMPLE}
     * @response.representation.200.doc A page of groups and their highest permissions for the specified project.
     *
     * @response.representation.401.doc The currently authenticated user is not a project administrator for the
     *                                  specified project.
     *
     * @response.representation.404.doc The specified project does not exist.
     */
    @GET
    @Path("groups")
    public Response getGroupsWithAnyPermission(@Context Project project,
                                               @QueryParam("filter") String filter,
                                               @Context PageRequest pageRequest) {
        Page<PermittedGroup> page = permissionAdminService.findGroupsWithProjectPermission(project, filter,
                pageRequest);

        return ResponseFactory.ok(new RestPage<>(page, RestPermittedGroup.REST_TRANSFORM)).build();
    }

    /**
     * Retrieve a page of groups that have no granted permissions for the specified project.
     * <p>
     * The authenticated user must have <strong>PROJECT_ADMIN</strong> permission for the specified project or a higher
     * global permission to call this resource.
     *
     * @param filter if specified only group names containing the supplied string will be returned
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestDetailedGroup#PAGE_EXAMPLE}
     * @response.representation.200.doc A page of groups that have not been granted any permissions for the specified
     *                                  project.
     *
     * @response.representation.401.doc The currently authenticated user is not a project administrator for the
     *                                  specified project.
     *
     * @response.representation.404.doc The specified project does not exist.
     */
    @GET
    @Path("groups/none")
    public Response getGroupsWithoutAnyPermission(@Context Project project,
                                                  @QueryParam("filter") String filter,
                                                  @Context PageRequest pageRequest) {
        Page<String> page = permissionAdminService.findGroupsWithoutProjectPermission(project, filter, pageRequest);

        return ResponseFactory.ok(new RestPage<>(page, RestDetailedGroup.NAME_TRANSFORM)).build();
    }

    /**
     * Retrieve a page of users that have been granted at least one permission for the specified project.
     * <p>
     * The authenticated user must have <strong>PROJECT_ADMIN</strong> permission for the specified project or a higher
     * global permission to call this resource.
     *
     * @param filter if specified only group names containing the supplied string will be returned
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestPermittedUser#PAGE_EXAMPLE}
     * @response.representation.200.doc A page of users and their highest permissions for the specified project.
     *
     * @response.representation.401.doc The currently authenticated user is not a project administrator for the
     *                                  specified project.
     *
     * @response.representation.404.doc The specified project does not exist.
     */
    @GET
    @Path("users")
    public Response getUsersWithAnyPermission(@Context Project project,
                                              @QueryParam("filter") String filter,
                                              @Context PageRequest pageRequest) {
        Page<PermittedUser> page = permissionAdminService.findUsersWithProjectPermission(project, filter, pageRequest);

        return ResponseFactory.ok(new RestPage<>(page, RestPermittedUser.REST_TRANSFORM)).build();
    }

    /**
     * Retrieve a page of <i>licensed</i> users that have no granted permissions for the specified project.
     * <p>
     * The authenticated user must have <strong>PROJECT_ADMIN</strong> permission for the specified project or a higher
     * global permission to call this resource.
     *
     * @param filter if specified only group names containing the supplied string will be returned
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestDetailedGroup#PAGE_EXAMPLE}
     * @response.representation.200.doc A page of users that have not been granted any permissions for the specified
     *                                  project.
     *
     * @response.representation.401.doc The currently authenticated user is not a project administrator for the
     *                                  specified project.
     *
     * @response.representation.404.doc The specified project does not exist.
     */
    @GET
    @Path("users/none")
    public Response getUsersWithoutPermission(@Context Project project,
                                              @QueryParam("filter") String filter,
                                              @Context PageRequest pageRequest) {
        Page<ApplicationUser> page = permissionAdminService.findLicensedUsersWithoutProjectPermission(project, filter,
                pageRequest);

        return ResponseFactory.ok(new RestPage<>(page, RestApplicationUser.REST_TRANSFORM)).build();
    }

    /**
     * Promote or demote a group's permission level for the specified project.
     * <p>
     * The authenticated user must have <strong>PROJECT_ADMIN</strong> permission for the specified project or a higher
     * global permission to call this resource. In addition, a user may not demote a group's permission level if their
     * own permission level would be reduced as a result.
     *
     * @param permissionName The permission to grant.
     *                       See the [permissions documentation](https://confluence.atlassian.com/display/BitbucketServer/Using+project+permissions)
     *                       for a detailed explanation of what each permission entails.
     *                       Available project permissions are:
     *                       <ul>
     *                           <li>PROJECT_READ</li>
     *                           <li>PROJECT_WRITE</li>
     *                           <li>PROJECT_ADMIN</li>
     *                       </ul>
     *
     * @param groupNames the names of the groups
     *
     * @response.representation.204.doc The requested permission was granted.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#VALIDATION_EXAMPLE}
     * @response.representation.400.doc The request was malformed or the specified permission does not exist.
     *
     * @response.representation.401.doc The currently authenticated user is not an administrator for the specified
     *                                  project.
     *
     * @response.representation.403.doc The action was disallowed as it would reduce the currently authenticated user's
     *                                  permission level.
     *
     * @response.representation.404.doc The specified project or group does not exist.
     */
    @PUT
    @Path("groups")
    public Response setPermissionForGroups(@Context Project project,
                                           @QueryParam("permission") String permissionName,
                                           @QueryParam("name") Set<String> groupNames) {
        Permission permission = validatePermission(permissionName, Project.class);
        Set<String> groups = validateGroups(groupNames, false);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(permission, project)
                .groups(groups)
                .build());
        return ResponseFactory.noContent().build();
    }

    /**
     * Promote or demote a user's permission level for the specified project.
     * <p>
     * The authenticated user must have <strong>PROJECT_ADMIN</strong> permission for the specified project or a higher
     * global permission to call this resource. In addition, a user may not reduce their own permission level unless
     * they have a global permission that already implies that permission.
     *
     * @param permissionName the permission to grant. See the [permissions documentation](https://confluence.atlassian.com/display/BitbucketServer/Using+project+permissions)
     *                       for a detailed explanation of what each permission entails.
     *                       Available project permissions are:
     *                       <ul>
     *                           <li>PROJECT_READ</li>
     *                           <li>PROJECT_WRITE</li>
     *                           <li>PROJECT_ADMIN</li>
     *                       </ul>
     * @param usernames the names of the users
     *
     * @response.representation.204.doc The requested permission was granted.
     *
     * @response.representation.400.example {@link RestErrors#VALIDATION_EXAMPLE}
     * @response.representation.400.doc The request was malformed or the specified permission does not exist.
     *
     * @response.representation.401.doc The currently authenticated user is not an administrator for the specified
     *                                  project.
     *
     * @response.representation.403.doc The action was disallowed as it would reduce the currently authenticated user's
     *                                  permission level.
     *
     * @response.representation.404.doc The specified project or user does not exist.
     */
    @PUT
    @Path("users")
    public Response setPermissionForUsers(@Context Project project,
                                          @QueryParam("name") Set<String> usernames,
                                          @QueryParam("permission") String permissionName) {
        Permission permission = validatePermission(permissionName, Project.class);
        Set<ApplicationUser> users = validateUsers(usernames, false);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(permission, project)
                .users(users)
                .build());
        return ResponseFactory.noContent().build();
    }

    /**
     * Revoke all permissions for the specified project for a group.
     * <p>
     * The authenticated user must have <strong>PROJECT_ADMIN</strong> permission for the specified project or a higher
     * global permission to call this resource.
     * <p>
     * In addition, a user may not revoke a group's permissions if it will reduce their own permission level.
     *
     * @param groupName the name of the group
     *
     * @response.representation.204.doc All project permissions were revoked from the group for the specified project.
     *
     * @response.representation.401.doc The currently authenticated user is not an administrator of the specified
     *                                  project.
     *
     * @response.representation.404.doc The specified project or group does not exist.
     *
     * @response.representation.409.doc The action was disallowed as it would reduce the currently authenticated user's
     *                                  permission level.
     */
    @DELETE
    @Path("groups")
    public Response revokePermissionsForGroup(@Context Project project,
                                              @QueryParam("name") String groupName) {
        groupName = validateGroup(groupName, true);

        permissionAdminService.revokeAllProjectPermissions(project, groupName);
        return ResponseFactory.noContent().build();
    }

    /**
     * Revoke all permissions for the specified project for a user.
     * <p>
     * The authenticated user must have <strong>PROJECT_ADMIN</strong> permission for the specified project or a higher
     * global permission to call this resource.
     * <p>
     * In addition, a user may not revoke their own project permissions if they do not have a higher global permission.
     *
     * @param username the name of the user
     *
     * @response.representation.204.doc All project permissions were revoked from the user for the specified project.
     *
     * @response.representation.401.doc The currently authenticated user is not an administrator of the specified
     *                                  project.
     *
     * @response.representation.404.doc The specified project or group does not exist.
     *
     * @response.representation.409.doc The action was disallowed as it would reduce the currently authenticated user's
     *                                  permission level.
     */
    @DELETE
    @Path("users")
    public Response revokePermissionsForUser(@Context Project project,
                                             @QueryParam("name") String username) {
        ApplicationUser user = validateUser(username, true);

        permissionAdminService.revokeAllProjectPermissions(project, user);
        return ResponseFactory.noContent().build();
    }

    /**
     * Check whether the specified permission is the default permission (granted to all users) for a project.
     * <p>
     * The authenticated user must have <strong>PROJECT_ADMIN</strong> permission for the specified project or a higher
     * global permission to call this resource.
     *
     * @param permissionName the permission to grant
     *                       Available project permissions are:
     *                       <ul>
     *                           <li>PROJECT_READ</li>
     *                           <li>PROJECT_WRITE</li>
     *                           <li>PROJECT_ADMIN</li>
     *                       </ul>
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname permitted
     * @response.representation.200.example {@link RestPermitted#RESPONSE_EXAMPLE}
     * @response.representation.200.doc A simple entity indicating whether the specified permission is the default
     *                                  permission for this project.
     *
     * @response.representation.400.doc The request was malformed or the specified permission does not exist.
     *
     * @response.representation.401.doc The currently authenticated user is not an administrator of the specified
     *                                  project.
     *
     * @response.representation.404.doc The specified project does not exist.
     */
    @GET
    @Path("{permission}/all")
    public Response hasAllUserPermission(@Context Project project,
                                         @PathParam("permission") String permissionName) {
        Permission permission = validatePermission(permissionName, Project.class);

        return ResponseFactory
                .ok(new RestPermitted(permissionAdminService.hasAllProjectPermission(permission, project)))
                .build();
    }

    /**
     * Grant or revoke a project permission to all users, i.e. set the default permission.
     * <p>
     * The authenticated user must have <strong>PROJECT_ADMIN</strong> permission for the specified project or a higher
     * global permission to call this resource.
     *
     * @param allow <em>true</em> to grant the specified permission to all users, or <em>false</em> to revoke it
     * @param permissionName the permission to grant
     *                       Available project permissions are:
     *                       <ul>
     *                           <li>PROJECT_READ</li>
     *                           <li>PROJECT_WRITE</li>
     *                           <li>PROJECT_ADMIN</li>
     *                       </ul>
     *
     * @response.representation.204.doc The requested permission was successfully granted or revoked.
     *
     * @response.representation.400.example {@link RestErrors#VALIDATION_EXAMPLE}
     * @response.representation.400.doc The request was malformed or the specified permission does not exist.
     *
     * @response.representation.401.doc The currently authenticated user is not an administrator of the specified
     *                                  project.
     *
     * @response.representation.404.doc The specified project does not exist.
     */
    @POST
    @Path("{permission}/all")
    public Response modifyAllUserPermission(@Context Project project,
                                            @PathParam("permission") String permissionName,
                                            // this is not very RESTful. We should change it to PUT and DELETE
                                            // methods instead
                                            @QueryParam("allow") Boolean allow) {
        Permission permission = validatePermission(permissionName, Project.class);
        if (allow == null) {
            String message = i18nService.getMessage("bitbucket.rest.permissionadmin.invalidallow");
            throw new BadRequestException(message);
        }

        if (allow) {
            permissionAdminService.grantAllProjectPermission(permission, project);
        } else {
            permissionAdminService.revokeAllProjectPermission(permission, project);
        }

        return ResponseFactory.noContent().build();
    }
}
