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

import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.pull.*;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.rest.BadRequestException;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.pull.RestPullRequestParticipant;
import com.atlassian.bitbucket.rest.user.RestApplicationUser;
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.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.atlassian.plugins.rest.common.security.AuthorisationException;
import com.google.common.base.Joiner;
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.Locale;

import static com.atlassian.bitbucket.rest.util.ResourcePatterns.PULL_REQUEST_ID;
import static com.atlassian.bitbucket.rest.util.ResourcePatterns.USER_SLUG;

@AnonymousAllowed
@Consumes(MediaType.APPLICATION_JSON)
@Path(ResourcePatterns.PULL_REQUEST_URI + "/participants")
@Produces(RestUtils.APPLICATION_JSON_UTF8)
@Singleton
public class PullRequestParticipantResource extends AbstractPullRequestResource {

    private final AuthenticationContext authenticationContext;

    public PullRequestParticipantResource(I18nService i18nService, PullRequestService pullRequestService,
                                          AuthenticationContext authenticationContext) {
        super(i18nService, pullRequestService);
        this.authenticationContext = authenticationContext;
    }

    /**
     * Retrieves a page of the participants for a given pull request.
     * <p>
     * The authenticated user must have <strong>REPO_READ</strong> permission for the repository that this pull request
     * targets to call this resource.
     *
     * @param repository    the repository to which the pull request will be merged
     * @param pullRequestId the id of the pull request within the repository
     * @param pageRequest   the request specifying the start and limit of the page
     * @return the page of participants
     *
     * @response.representation.201.mediaType application/json
     * @response.representation.201.qname page
     * @response.representation.201.example {@link RestPullRequestParticipant#PAGE}
     * @response.representation.201.doc Details of the participants in this pull request
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions to view the pull request.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The specified repository or pull request does not exist.
     */
    @GET
    public Response listParticipants(@Context Repository repository,
                                     @PathParam(PULL_REQUEST_ID) long pullRequestId,
                                     @Context final PageRequest pageRequest) {
        Page<? extends PullRequestParticipant> page = pullRequestService.getParticipants(
                repository.getId(), pullRequestId, pageRequest);
        return ResponseFactory.ok(new RestPage<>(page, RestPullRequestParticipant::new)).build();
    }

    /**
     * Assigns a participant to an explicit role in pull request. Currently only the REVIEWER role may be assigned.
     * <p>
     * If the user is not yet a participant in the pull request, they are made one and assigned the supplied role.
     * <p>
     * If the user is already a participant in the pull request, their previous role is replaced with the supplied role
     * unless they are already assigned the AUTHOR role which cannot be changed and will result in a Bad Request (400)
     * response code.
     * <p>
     * The authenticated user must have <strong>REPO_WRITE</strong> permission for the repository that this pull request
     * targets to call this resource.
     *
     * @param repository    the repository to which the pull request will be merged
     * @param pullRequestId the id of the pull request within the repository
     * @param participant   the participant's user name and desired role
     *
     * @request.representation.mediaType application/json
     * @request.representation.example {@link RestPullRequestParticipant#REQUEST_EXAMPLE_ADD_REVIEWER}
     *
     * @response.representation.201.mediaType application/json
     * @response.representation.201.qname participant
     * @response.representation.201.example {@link RestPullRequestParticipant#EXAMPLE_RESPONSE_REVIEWER_UNAPPROVED}
     * @response.representation.201.doc The created or updated participant.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The request does not have the username and role, or is attempting an invalid assignment
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions to update the pull request.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The specified repository or pull request does not exist.
     */
    @POST
    public Response assignParticipantRole(@Context Repository repository,
                                          @PathParam(PULL_REQUEST_ID) long pullRequestId,
                                          RestPullRequestParticipant participant) {
        RestApplicationUser user = ensureHasProperty("user", participant.getUser());
        String userName = ensureHasProperty("user.name", user.getName());
        if (ensureHasProperty("role", participant.getRole()) != PullRequestRole.REVIEWER) {
            throw new InvalidPullRequestRoleException(
                    i18nService.createKeyedMessage("bitbucket.rest.pullrequests.role.assign.restriction",
                            PullRequestRole.REVIEWER.name().toLowerCase(Locale.ROOT)));
        }

        PullRequestParticipant created = pullRequestService.addReviewer(repository.getId(), pullRequestId, userName);
        return ResponseFactory.ok(new RestPullRequestParticipant(created)).build();
    }

    /**
     * Unassigns a participant from the REVIEWER role they may have been given in a pull request.
     * <p>
     * If the participant has no explicit role this method has no effect.
     * <p>
     * Afterwards, the user will still remain a participant in the pull request but their role will be reduced to
     * PARTICIPANT. This is because once made a participant of a pull request,
     * a user will forever remain a participant. Only their role may be altered.
     * <p>
     * The authenticated user must have <strong>REPO_WRITE</strong> permission for the repository that this pull request
     * targets to call this resource.
     * <p>
     * <strong>Deprecated since 4.2</strong>. Use
     * /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/participants/{userSlug}
     * instead.
     *
     * @param repository    the repository to which the pull request will be merged
     * @param pullRequestId the id of the pull request within the repository
     * @param username      the participant's user name
     * @deprecated in 4.2. Use {@link #unassignParticipantRole(Repository, ApplicationUser, long)} instead
     *
     * @request.representation.mediaType application/json
     * @request.representation.example {@link RestPullRequestParticipant#REQUEST_EXAMPLE_REVIEWER}
     *
     * @response.representation.204.doc The update completed.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The request does not have the username
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions to update the pull request.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The specified repository or pull request does not exist.
     */
    @DELETE
    @Deprecated
    public Response unassignParticipantRole(@Context Repository repository,
                                            @PathParam(PULL_REQUEST_ID) long pullRequestId,
                                            @QueryParam("username") String username) {
        pullRequestService.unassignRole(repository.getId(), pullRequestId, ensureHasProperty("username", username));

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

    /**
     * Unassigns a participant from the REVIEWER role they may have been given in a pull request.
     * <p>
     * If the participant has no explicit role this method has no effect.
     * <p>
     * Afterwards, the user will still remain a participant in the pull request but their role will be reduced to
     * PARTICIPANT. This is because once made a participant of a pull request, a user will forever remain a participant.
     * Only their role may be altered.
     * <p>
     * The authenticated user must have <strong>REPO_WRITE</strong> permission for the repository that this pull request
     * targets to call this resource.
     *
     * @param repository    the repository to which the pull request will be merged
     * @param user          the participant
     * @param pullRequestId the id of the pull request within the repository
     *
     * @request.representation.mediaType application/json
     * @request.representation.example {@link RestPullRequestParticipant#REQUEST_EXAMPLE_REVIEWER}
     *
     * @response.representation.204.doc The update completed.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The request does not have the username
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions to update the pull request.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The specified repository or pull request does not exist.
     */
    @DELETE
    @Path(ResourcePatterns.USER_SLUG_PATH)
    public Response unassignParticipantRole(@Context Repository repository,
                                            @Context ApplicationUser user,
                                            @PathParam(PULL_REQUEST_ID) long pullRequestId) {
        pullRequestService.removeReviewer(repository.getId(), pullRequestId, user.getName());

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

    /**
     * Change the current user's status for a pull request. Implicitly adds the user as a participant if they are not
     * already. If the current user is the author, this method will fail.
     * <p>
     * The possible values for {@code status} are <strong>UNAPPROVED</strong>, <strong>NEEDS_WORK</strong>, or
     * <strong>APPROVED</strong>.
     * <p>
     * If the new {@code status} is <strong>NEEDS_WORK</strong> or <strong>APPROVED</strong> then the
     * {@code lastReviewedCommit} for the participant will be updated to the latest commit of the source branch of the
     * pull request.
     * <p>
     * The authenticated user must have <strong>REPO_READ</strong> permission for the repository that this pull request
     * targets to call this resource.
     *
     * @param pullRequestId the id of the pull request within the repository
     * @param userSlug the slug for the user changing their status
     * @param participant the participant representing the status to set, all fields except status are ignored
     *
     * @request.representation.mediaType application/json
     * @request.representation.example {@link RestPullRequestParticipant#REQUEST_EXAMPLE_APPROVED}
     *
     * @response.representation.201.mediaType application/json
     * @response.representation.201.qname participant
     * @response.representation.201.example {@link RestPullRequestParticipant#EXAMPLE_RESPONSE_REVIEWER_APPROVED}
     * @response.representation.201.doc Details of the new participant
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The currently authenticated user is the author of the PR and cannot have its status updated.
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions to view the pull request.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc  The specified repository or pull request does not exist.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#VALIDATION_EXAMPLE}
     * @response.representation.400.doc The specified status was invalid.
     *
     * @response.representation.409.mediaType application/json
     * @response.representation.409.qname errors
     * @response.representation.409.example {@link RestErrors#EXAMPLE}
     * @response.representation.409.doc  The pull request is not open.
     */
    @PUT
    @Path(ResourcePatterns.USER_SLUG_PATH)
    public Response updateStatus(@Context Repository repository, @PathParam(PULL_REQUEST_ID) long pullRequestId,
                                 @PathParam(USER_SLUG) String userSlug, RestPullRequestParticipant participant) {
        ensureIsValidUser(userSlug);
        return ResponseFactory.ok(new RestPullRequestParticipant(
                pullRequestService.setReviewerStatus(repository.getId(), pullRequestId,
                        getStatus(participant)))).build();
    }

    private void ensureIsValidUser(String userSlug) {
        ApplicationUser currentUser = authenticationContext.getCurrentUser();
        if (currentUser == null || currentUser.getSlug().equals(userSlug)) {
            //Allow null; it will trigger the right security exception when calling the PullRequestService
            return;
        }
        throw new AuthorisationException(i18nService.getMessage("bitbucket.service.pullrequest.update.permissions.wronguser"));
    }

    private PullRequestParticipantStatus getStatus(RestPullRequestParticipant participant) {
        try {
            return participant.getStatus();
        } catch (IllegalArgumentException ignored) {
            throw new BadRequestException(i18nService.getMessage(
                    "bitbucket.service.pullrequest.update.permissions.invalidstatus",
                    Joiner.on(',').join(PullRequestParticipantStatus.values())));
        }
    }
}
