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

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.pull.*;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.rest.BadRequestException;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.project.RestProject;
import com.atlassian.bitbucket.rest.pull.*;
import com.atlassian.bitbucket.rest.repository.RestRepository;
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.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.atlassian.stash.internal.rest.annotations.OptionalBody;
import com.google.common.collect.ImmutableList;
import com.sun.jersey.core.provider.EntityHolder;
import com.sun.jersey.spi.resource.Singleton;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.Nonnull;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.util.*;

import static com.atlassian.bitbucket.pull.PullRequestOrder.fromString;
import static com.atlassian.bitbucket.pull.PullRequestOrder.getDefaultOrder;
import static com.atlassian.bitbucket.rest.util.ResourcePatterns.PULL_REQUEST_ID;
import static com.atlassian.bitbucket.rest.util.ResourcePatterns.PULL_REQUEST_ID_PATH;
import static java.util.stream.Collectors.toSet;

/**
 * @see PullRequestCommentResource
 */
@AnonymousAllowed
@Consumes(MediaType.APPLICATION_JSON)
@Path(ResourcePatterns.REPOSITORY_URI + "/pull-requests")
@Produces(RestUtils.APPLICATION_JSON_UTF8)
@Singleton
@SuppressWarnings("PathAnnotation")
public class PullRequestResource extends AbstractPullRequestResource {

    private static final int MAX_PARTICIPANTS = 10;
    private static final String USERNAME = "username.";
    private static final String ROLE = "role.";
    private static final String APPROVED = "approved.";

    private final RepositoryService repositoryService;

    public PullRequestResource(I18nService i18nService, PullRequestService pullRequestService,
                               RepositoryService repositoryService) {
        super(i18nService, pullRequestService);

        this.repositoryService = repositoryService;
    }

    /**
     * Decline a 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 version the current version of the pull request. If the server's version isn't the same as the specified
     *                version the operation will fail. To determine the current version of the pull request it should be
     *                fetched from the server prior to this operation. Look for the 'version' attribute in the returned
     *                JSON structure.
     *
     * @request.representation.mediaType application/json
     * @request.representation.example {@link RestPullRequestDeclineRequest#DECLINE_REQUEST_EXAMPLE}
     *
     * @response.representation.200.doc The pull request was declined.
     *
     * @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.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 or has been updated since the version specified by
     *                                  the request.
     */
    @POST
    @Path(PULL_REQUEST_ID_PATH + "/decline")
    public Response decline(@Context Repository repository,
                            @PathParam(PULL_REQUEST_ID) long pullRequestId,
                            @QueryParam("version") @DefaultValue(RestUtils.DEFAULT_ENTITY_VERSION) int version,
                            @OptionalBody EntityHolder<RestPullRequestDeclineRequest> bodyHolder) {
        String comment = null;

        if (bodyHolder.hasEntity()) {
            RestPullRequestDeclineRequest declineRequest = bodyHolder.getEntity();

            comment = declineRequest.getComment();
            if (declineRequest.hasVersion()) {
                //If a version is supplied in the body, prefer that version to the query parameter
                version = declineRequest.getVersion();
            }
        }

        PullRequest declined = pullRequestService.decline(
                new PullRequestDeclineRequest.Builder(repository.getId(), pullRequestId, version)
                        .comment(comment)
                        .build());

        return ResponseFactory.ok(new RestPullRequest(declined)).build();
    }

    /**
     * Create a new pull request between two branches. The branches may be in the same repository, or different ones.
     * When using different repositories, they must still be in the same {@link Repository#getHierarchyId() hierarchy}.
     * <p>
     * The authenticated user must have <strong>REPO_READ</strong> permission for the "from" and "to"repositories to
     * call this resource.
     *
     * @request.representation.mediaType application/json
     * @request.representation.example {@link RestPullRequest#REQUEST_EXAMPLE}
     *
     * @response.representation.201.mediaType application/json
     * @response.representation.201.qname pullRequest
     * @response.representation.201.example {@link RestPullRequest#RESPONSE_EXAMPLE_BRAND_NEW}
     * @response.representation.201.doc The newly created pull request.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#VALIDATION_EXAMPLE}
     * @response.representation.400.doc The pull request entity supplied in the request was malformed.
     *
     * @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 create a pull
     *                                  request between the two specified repositories.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc One of the specified repositories or branches does not exist.
     *
     * @response.representation.409.mediaType application/json
     * @response.representation.409.qname errors
     * @response.representation.409.example {@link RestErrors#EXAMPLE}
     * @response.representation.409.doc One of the following error cases occurred (check the error message for more details):
     *                                  <ul>
     *                                      <li>There was a problem resolving one or more reviewers.</li>
     *                                      <li>The specified branches were the same.</li>
     *                                      <li>The <em>to</em> branch is already up-to-date with all the commits on the
     *                                      <em>from</em> branch.</li>
     *                                      <li>A pull request between the two branches already exists.</li>
     *                                  </ul>
     */
    @POST
    public Response create(@Context Repository to, @Context UriInfo uriInfo, RestPullRequest pullRequest) {
        String title = ensureHasProperty("title", pullRequest.getTitle());
        RestPullRequestRef fromRef = ensureHasProperty("fromRef", pullRequest.getFromRef());
        String fromBranchId = ensureHasProperty("fromRef.id", fromRef.getId());
        RestPullRequestRef toRef = ensureHasProperty("toRef", pullRequest.getToRef());
        String toBranchId = ensureHasProperty("toRef.id", toRef.getId());

        ensurePropertyNotSupplied("author", pullRequest.hasAuthor());
        ensurePropertyNotSupplied("participants", pullRequest.hasParticipants());

        Collection<RestPullRequestParticipant> reviewers = validateReviewers(pullRequest);

        RestRepository toRefRepository = toRef.getRepository();
        if (toRefRepository != null) {
            //If they specified the toRef's repository, verify that they did it correctly
            RestProject toProject = ensureHasProperty("toRef.repository.project", toRefRepository.getProject());
            String projectKey = ensureHasProperty("toRef.repository.project.key", toProject.getKey());
            String repositorySlug = ensureHasProperty("toRef.repository.slug", toRefRepository.getSlug());

            //If the request is well-formed, ensure the to repository specified matches the URL
            if (!(projectKey.equalsIgnoreCase(to.getProject().getKey()) &&
                    repositorySlug.equalsIgnoreCase(to.getSlug()))) {
                throw new BadRequestException(i18nService.getMessage("bitbucket.rest.pullrequests.wrongurl",
                        projectKey, repositorySlug, to.getProject().getKey(), to.getSlug()));
            }
        }

        RestRepository fromRefRepository = fromRef.getRepository();
        Repository from;
        if (fromRefRepository == null || fromRefRepository.isEmpty()) {
            //If the from repository was not specified, assume it is the same as the to repository
            from = to;
        } else {
            //Otherwise, if it was, verify that it was done correctly
            RestProject fromProject = ensureHasProperty("fromRef.repository.project", fromRefRepository.getProject());
            String projectKey = ensureHasProperty("fromRef.repository.project.key", fromProject.getKey());
            String repositorySlug = ensureHasProperty("fromRef.repository.slug", fromRefRepository.getSlug());

            //If the request is well-formed, load the from repository and ensure it exists (and is accessible)
            from = repositoryService.getBySlug(projectKey, repositorySlug);
            if (from == null) {
                throw noSuchRepoException(projectKey, repositorySlug);
            }
        }

        Set<String> reviewerNames = extractParticipantUsernames(reviewers);

        PullRequest created = pullRequestService.create(title, pullRequest.getDescription(), reviewerNames,
                from, fromBranchId, to, toBranchId);
        return ResponseFactory.created(uriInfo.getRequestUriBuilder().path(String.valueOf(created.getId())).build())
                .entity(new RestPullRequest(created))
                .build();
    }

    /**
     * Retrieve a page of activity associated with a pull request.
     * <p>
     * Activity items include comments, approvals, rescopes (i.e. adding and removing of commits), merges and more.
     * <p>
     * Different types of activity items may be introduced in newer versions of Stash or by user installed plugins, so
     * clients should be flexible enough to handle unexpected entity shapes in the returned page.
     * <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 fromId (optional) the id of the activity item to use as the first item in the returned page
     * @param fromType (required if <strong>fromId</strong> is present) the type of the activity item specified by
     *                 <strong>fromId</strong> (either <strong>COMMENT</strong> or <strong>ACTIVITY</strong>)
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestPullRequestActivity#PAGE_EXAMPLE}
     * @response.representation.200.doc A page of activity relating to the specified pull request.
     *
     * @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.
     *
     * @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
     *                                  specified 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
    @Path(PULL_REQUEST_ID_PATH + "/activities")
    public Response getActivities(@Context Repository repository,
                                  @PathParam(PULL_REQUEST_ID) long pullRequestId,
                                  @QueryParam("fromId") Long fromId,
                                  @QueryParam("fromType") String fromType,
                                  @Context PageRequest pageRequest) {
        if (fromId == null) {
            Page<? extends PullRequestActivity> page = pullRequestService.getActivities(repository.getId(), pullRequestId, pageRequest);
            return ResponseFactory.ok(new RestPage<>(page, RestPullRequestActivity.REST_TRANSFORM))
                    .build();
        } else {
            PullRequestActivityPage<? extends PullRequestActivity> page =
                    pullRequestService.getActivitiesStartingAt(repository.getId(), pullRequestId, toEntityType(fromType), fromId, pageRequest);
            return ResponseFactory.ok(new RestPullRequestActivityPage(page, RestPullRequestActivity.REST_TRANSFORM))
                    .build();
        }
    }

    /**
     * Retrieve a 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.
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname pullRequest
     * @response.representation.200.example {@link RestPullRequest#RESPONSE_EXAMPLE}
     * @response.representation.200.doc The specified 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
     *                                  specified 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
    @Path(PULL_REQUEST_ID_PATH)
    public Response get(@Context PullRequest pullRequest) {
        return ResponseFactory.ok(new RestPullRequest(pullRequest)).build();
    }

    /**
     * Retrieve a page of pull requests to or from the specified repository.
     * <p>
     * The authenticated user must have <strong>REPO_READ</strong> permission for the specified repository to call
     * this resource.
     *
     * Optionally clients can specify PR participant filters. Each filter has a mandatory {@code username.N}
     * parameter, and the optional {@code role.N} and {@code approved.N} parameters.
     * <ul>
     *     <li>
     *         {@code username.N} - the "root" of a single participant filter, where "N" is a natural number
     *         starting from 1. This allows clients to specify multiple participant filters, by providing consecutive
     *         filters as {@code username.1}, {@code username.2} etc. Note that the filters numbering has to start
     *         with 1 and be continuous for all filters to be processed. The total allowed number of participant
     *         filters is 10 and all filters exceeding that limit will be dropped.
     *     </li>
     *     <li>
     *         {@code role.N}(optional) the role associated with {@code username.N}.
     *         This must be one of {@code AUTHOR}, {@code REVIEWER}, or{@code PARTICIPANT}
     *     </li>
     *     <li>
     *         {@code approved.N}(optional) the approved status associated with {@code username.N}.
     *         That is whether {@code username.N} has approved the PR. Either {@code true}, or {@code false}
     *     </li>
     * </ul>
     * 
     *
     * @param direction (optional, defaults to <strong>INCOMING</strong>) the direction relative to the specified
     *                  repository. Either <strong>INCOMING</strong> or <strong>OUTGOING</strong>.
     * @param at (optional) a <i>fully-qualified</i> branch ID to find pull requests to or from,
     *           such as {@code refs/heads/master}
     * @param state  (optional, defaults to <strong>OPEN</strong>). Supply <strong>ALL</strong> to return pull request
     *               in any state. If a state is supplied only pull requests in the specified state will be returned.
     *               Either <strong>OPEN</strong>, <strong>DECLINED</strong> or <strong>MERGED</strong>.
     * @param order (optional, defaults to <strong>NEWEST</strong>) the order to return pull requests in, either
     *              <strong>OLDEST</strong> (as in: "oldest first") or <strong>NEWEST</strong>.
     * @param withAttributes (optional) defaults to true, whether to return additional pull request attributes
     * @param withProperties (optional) defaults to true, whether to return additional pull request properties
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestPullRequest#PAGE_EXAMPLE}
     * @response.representation.200.doc A page of pull requests that match the search criteria.
     *
     * @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.
     *
     * @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
     *                                  specified 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 getPage(@Context UriInfo uriInfo,
                            @Context Repository repository,
                            @QueryParam("direction") @DefaultValue("incoming") String direction,
                            @QueryParam("at") String at,
                            @QueryParam("state") String state,
                            @QueryParam("order") String order,
                            @QueryParam("withAttributes") @DefaultValue("true") Boolean withAttributes,
                            @QueryParam("withProperties") @DefaultValue("true") Boolean withProperties,
                            @Context PageRequest pageRequest) {
        PullRequestDirection prDirection = toDirection(direction);
        Iterable<PullRequestParticipantRequest> prParticipants = toParticipantSearchRequests(uriInfo.getQueryParameters());
        PullRequestState prState = toPullRequestState(state);
        PullRequestOrder resultOrder = fromString(order, getDefaultOrder());

        Page<PullRequest> pullRequestPage = pullRequestService.search(new PullRequestSearchRequest.Builder()
                .repositoryAndBranch(prDirection, repository.getId(), at)
                .state(prState)
                .order(resultOrder)
                .participants(prParticipants)
                .withProperties(withProperties)
                .build(), pageRequest);

        return ResponseFactory.ok(new RestPage<>(pullRequestPage, RestPullRequest.REST_TRANSFORM)).build();
    }

    /**
     * Test whether a pull request can be merged.
     * <p>
     * A pull request may not be merged if:
     * <ul>
     *     <li>there are conflicts that need to be manually resolved before merging; and/or</li>
     *     <li>one or more merge checks have vetoed the merge.</li>
     * </ul>
     * <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
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname pullRequest
     * @response.representation.200.example {@link RestPullRequestMergeability#RESPONSE_EXAMPLE}
     * @response.representation.200.doc The merge status of a the 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
     *                                  specified 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.409.mediaType application/json
     * @response.representation.409.qname errors
     * @response.representation.409.example {@link RestErrors#EXAMPLE}
     * @response.representation.409.doc The specified pull request is not open.
     */
    @GET
    @Path(PULL_REQUEST_ID_PATH + "/merge")
    public Response canMerge(@Context Repository repository,
                             @PathParam(PULL_REQUEST_ID) long pullRequestId) {
        PullRequestMergeability mergeability = pullRequestService.canMerge(repository.getId(), pullRequestId);

        return ResponseFactory.ok(new RestPullRequestMergeability(mergeability)).build();
    }

    /**
     * Merge the specified pull request.
     * <p>
     * The authenticated user must have <strong>REPO_WRITE</strong> permission for the repository that this pull request
     * targets to call this resource.
     *
     * @param version the current version of the pull request. If the server's version isn't the same as the specified
     *                version the operation will fail. To determine the current version of the pull request it should be
     *                fetched from the server prior to this operation. Look for the 'version' attribute in the returned
     *                JSON structure.
     * @param pullRequestId the id of the pull request within the repository

     * @request.representation.mediaType application/json
     * @request.representation.example {@link RestPullRequestMergeRequest#MERGE_REQUEST_EXAMPLE}
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname pullRequest
     * @response.representation.200.example {@link RestPullRequest#RESPONSE_EXAMPLE_MERGED}
     * @response.representation.200.doc The merged 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 merge the
     *                                  specified 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.409.mediaType application/json
     * @response.representation.409.qname errors
     * @response.representation.409.example {@link RestErrors#EXAMPLE}
     * @response.representation.409.doc One of the following error cases occurred (check the error message for more details):
     *                                  <ul>
     *                                      <li>The pull request has conflicts.</li>
     *                                      <li>A merge check vetoed the merge.</li>
     *                                      <li>The specified version is out of date.</li>
     *                                      <li>The specified pull request is not open.</li>
     *                                  </ul>
     */
    @POST
    @Path(PULL_REQUEST_ID_PATH + "/merge")
    public Response merge(@Context Repository repository,
                          @PathParam(PULL_REQUEST_ID) long pullRequestId,
                          @QueryParam("version") @DefaultValue(RestUtils.DEFAULT_ENTITY_VERSION) int version,
                          @OptionalBody EntityHolder<RestPullRequestMergeRequest> bodyHolder) {
        RestPullRequestMergeRequest mergeRequest = bodyHolder.getEntity();
        if (mergeRequest != null && mergeRequest.hasVersion()) {
            //If a version is supplied in the body, prefer that version to the query parameter
            version = mergeRequest.getVersion();
        }

        PullRequestMergeRequest.Builder builder = new PullRequestMergeRequest.Builder(repository.getId(), pullRequestId, version);
        if (mergeRequest != null) {
            builder.context(mergeRequest.getContext())
                    .message(mergeRequest.getMessage())
                    .strategyId(mergeRequest.getStrategyId());
        }

        return ResponseFactory.ok(new RestPullRequest(pullRequestService.merge(builder.build()))).build();
    }

    /**
     * Re-open a declined 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 version the current version of the pull request. If the server's version isn't the same as the specified
     *                version the operation will fail. To determine the current version of the pull request it should be
     *                fetched from the server prior to this operation. Look for the 'version' attribute in the returned
     *                JSON structure.
     * @param pullRequestId the id of the pull request within the repository
     *
     * @request.representation.mediaType application/json
     * @request.representation.example {@link RestPullRequestReopenRequest#REOPEN_REQUEST_EXAMPLE}

     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname pullRequest
     * @response.representation.200.example {@link RestPullRequest#RESPONSE_EXAMPLE}
     * @response.representation.200.doc The merged 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 reopen the
     *                                  specified 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.409.mediaType application/json
     * @response.representation.409.qname errors
     * @response.representation.409.example {@link RestErrors#EXAMPLE}
     * @response.representation.409.doc One of the following error cases occurred (check the error message for more
     *                                  details):
     *                                  <ul>
     *                                      <li>The pull request is not in a declined state.</li>
     *                                      <li>The specified version is out of date.</li>
     *                                  </ul>
     */
    @POST
    @Path(PULL_REQUEST_ID_PATH + "/reopen")
    public Response reopen(@Context Repository repository,
                           @PathParam(PULL_REQUEST_ID) long pullRequestId,
                           @QueryParam("version") @DefaultValue(RestUtils.DEFAULT_ENTITY_VERSION) int version,
                           @OptionalBody EntityHolder<RestPullRequestReopenRequest> bodyHolder) {
        if (bodyHolder.hasEntity()) {
            RestPullRequestReopenRequest reopenRequest = bodyHolder.getEntity();
            if (reopenRequest.hasVersion()) {
                //If a version is supplied in the body, prefer that version to the query parameter
                version = reopenRequest.getVersion();
            }
        }
        PullRequest pullRequest = pullRequestService.reopen(repository.getId(), pullRequestId, version);

        return ResponseFactory.ok(new RestPullRequest(pullRequest)).build();
    }

    /**
     * Update the title, description, reviewers or destination branch of an existing pull request.
     * <p>
     * <strong>Note:</strong> the <em>reviewers</em> list may be updated using this resource. However the
     * <em>author</em> and <em>participants</em> list may not.
     * <p>
     * The authenticated user must either:
     * <ul>
     *     <li>be the author of the pull request and have the <strong>REPO_READ</strong> permission for the repository
     *     that this pull request targets; or</li>
     *     <li>have the <strong>REPO_WRITE</strong> permission for the repository that this pull request targets</li>
     * </ul>
     * to call this resource.
     *
     * @request.representation.mediaType application/json
     * @request.representation.example {@link RestPullRequest#REQUEST_EXAMPLE_UPDATE}
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname pullRequest
     * @response.representation.200.example {@link RestPullRequest#RESPONSE_EXAMPLE}
     * @response.representation.200.doc The updated pull request.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#VALIDATION_EXAMPLE}
     * @response.representation.400.doc One of the following error cases occurred (check the error message for more details):
     *                                  <ul>
     *                                      <li>The request tried to modify the <em>author</em> or <em>participants</em>.</li>
     *                                      <li>The pull request's version attribute was not specified.</li>
     *                                      <li>A reviewer's username was not specified.</li>
     *                                      <li>The toRef id value was incorrectly left blank</li>
     *                                  </ul>
     *
     * @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
     *                                  specified pull request.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc One of the specified repositories or branches does not exist.
     *
     * @response.representation.409.mediaType application/json
     * @response.representation.409.qname errors
     * @response.representation.409.example {@link RestErrors#EXAMPLE}
     * @response.representation.409.doc One of the following error cases occurred (check the error message for more details):
     *                                  <ul>
     *                                      <li>The specified version is out of date.</li>
     *                                      <li>One of the reviewers could not be added to the pull request.</li>
     *                                      <li>If updating the destination branch:
     *                                          <ul>
     *                                              <li>There is already an open pull request with an identical to branch</li>
     *                                              <li>The from and new to branch <i>are</i> the same</li>
     *                                              <li>The new destination branch up-to-date is up-to-date with all of
     *                                                  changes from the from branch, resulting in a pull request with
     *                                                  nothing to merge
     *                                              </li>
     *                                          </ul>
     *                                      </li>
     *                                  </ul>
     */
    @PUT
    @Path(PULL_REQUEST_ID_PATH)
    public Response update(@Context PullRequest pullRequest, RestPullRequest restPullRequest) {
        ensurePropertySupplied("version", restPullRequest.hasVersion());
        ensurePropertyNotSupplied("author", restPullRequest.hasAuthor());
        ensurePropertyNotSupplied("participants", restPullRequest.hasParticipants());

        Collection<RestPullRequestParticipant> reviewers = validateReviewers(restPullRequest);

        if (restPullRequest.getToRef() != null && StringUtils.isBlank(restPullRequest.getToRef().getId())) {
            throw new BadRequestException(i18nService.getMessage("bitbucket.rest.pullrequests.update.toref.blank"));
        }

        if (restPullRequest.hasTitle() || restPullRequest.hasDescription() || restPullRequest.hasReviewers()
                || restPullRequest.getToRef() != null) {
            String newDescription = restPullRequest.hasDescription() ?
                    StringUtils.trimToNull(restPullRequest.getDescription()) : // if a blank description is provided, clear it
                    pullRequest.getDescription(); // no description provided - retain the current

            PullRequestUpdateRequest.Builder builder = new PullRequestUpdateRequest.Builder(pullRequest, restPullRequest.getVersion())
                    .title(StringUtils.defaultIfBlank(restPullRequest.getTitle(), pullRequest.getTitle()))
                    .description(newDescription)
                    .reviewers(extractParticipantUsernames(reviewers))
                    .toBranchId(restPullRequest.getToRef() != null ? restPullRequest.getToRef().getId() : null);
            return ResponseFactory.ok()
                    .entity(new RestPullRequest(pullRequestService.update(builder.build())))
                    .build();
        } else {
            throw new BadRequestException(i18nService.getMessage("bitbucket.rest.pullrequests.invalidupdateproperties"));
        }
    }

    private Set<String> extractParticipantUsernames(Collection<RestPullRequestParticipant> participants) {
        return participants.stream().map(participant -> participant.getUser().getName()).collect(toSet());
    }

    private PullRequestEntityType toEntityType(String entityType) {
        try {
            return StringUtils.isEmpty(entityType) ? PullRequestEntityType.ACTIVITY : PullRequestEntityType.valueOf(entityType.toUpperCase(Locale.ROOT));
        } catch (IllegalArgumentException e) {
            throw new BadRequestException(
                    i18nService.getMessage("bitbucket.rest.pullrequests.activity.entity.invalid",
                            toLowercaseNames(PullRequestEntityType.values())));
        }
    }

    private PullRequestState toPullRequestState(String state) {
        try {
            if (StringUtils.isEmpty(state)) {
                return PullRequestState.OPEN;
            } else if (state.equalsIgnoreCase("all")) {
                return null;
            } else {
                return PullRequestState.valueOf(state.toUpperCase(Locale.ROOT));
            }
        } catch (IllegalArgumentException e) {
            throw new BadRequestException(
                    i18nService.getMessage("bitbucket.rest.pullrequests.state.invalid",
                            toLowercaseNames(PullRequestState.values())));
        }
    }

    private Iterable<PullRequestParticipantRequest> toParticipantSearchRequests(@Nonnull MultivaluedMap<String, String> parameters) {
        ImmutableList.Builder<PullRequestParticipantRequest> participants = ImmutableList.builder();

        for (int i = 1; i <= MAX_PARTICIPANTS && parameters.containsKey(USERNAME + i); i++) {
            PullRequestRole role;
            try {
                role = parameters.containsKey(ROLE + i) ? PullRequestRole.valueOf(parameters.getFirst(ROLE + i)) : null;
            } catch (IllegalArgumentException e) {
                throw new BadRequestException(i18nService.getMessage("bitbucket.rest.pullrequests.invalidrole",
                        parameters.getFirst(ROLE + i), parameters.getFirst(USERNAME + i)));
            }

            participants.add(new PullRequestParticipantRequest.Builder(parameters.getFirst(USERNAME + i))
                    .approved(parameters.containsKey(APPROVED + i) ? Boolean.valueOf(parameters.getFirst(APPROVED + i)) : null)
                    .role(role)
                    .build());
        }

        return participants.build();
    }

    private Collection<RestPullRequestParticipant> validateReviewers(RestPullRequest restPullRequest) {
        Collection<RestPullRequestParticipant> reviewers = restPullRequest.getReviewers();
        if (!reviewers.isEmpty()) {
            int i = 0;
            for (RestPullRequestParticipant participant : reviewers) {
                ensureHasProperty("reviewers[" + i + "].user", participant.getUser());
                ensureHasProperty("reviewers[" + i + "].user.name", participant.getUser().getName());
                i++;
            }
        }
        return reviewers;
    }
}
