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

import com.atlassian.bitbucket.NoSuchEntityException;
import com.atlassian.bitbucket.RequestCanceledException;
import com.atlassian.bitbucket.ServiceException;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.commit.Commit;
import com.atlassian.bitbucket.commit.CommitRequest;
import com.atlassian.bitbucket.commit.CommitService;
import com.atlassian.bitbucket.commit.NoSuchCommitException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.pull.*;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.ui.PluginFormFragments;
import com.atlassian.bitbucket.ui.PluginFormFragmentsFactory;
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.bitbucket.util.PageUtils;
import com.atlassian.sal.api.features.DarkFeatureManager;
import com.atlassian.soy.springmvc.errors.DetailedError;
import com.atlassian.stash.internal.ApplicationConstants;
import com.atlassian.stash.internal.discovery.DiscoverableFeature;
import com.atlassian.stash.internal.discovery.FeatureDiscoveryService;
import com.atlassian.stash.internal.page.PageConstants;
import com.atlassian.stash.internal.plugin.PluginValidationErrors;
import com.atlassian.stash.internal.project.InternalProjectService;
import com.atlassian.stash.internal.pull.InternalPullRequestService;
import com.atlassian.stash.internal.scm.InternalScmService;
import com.atlassian.stash.internal.web.soy.StashSoyResponseBuilder;
import com.atlassian.stash.internal.web.util.CompareControllerSupport;
import com.atlassian.stash.internal.web.util.StashWebErrorUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.view.RedirectView;

import javax.validation.ConstraintViolationException;
import javax.validation.Valid;
import java.util.*;
import java.util.stream.Stream;

import static com.atlassian.bitbucket.pull.PullRequestOrder.fromString;
import static com.atlassian.bitbucket.pull.PullRequestOrder.getDefaultOrder;
import static com.atlassian.bitbucket.util.MoreCollectors.toImmutableSet;
import static com.atlassian.fugue.Iterables.first;
import static java.util.Optional.ofNullable;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;

@Controller
@RequestMapping("/projects/{projectKey}/repos/{repoSlug}/pull-requests")
public class PullRequestController extends CompareControllerSupport {

    private static final String COMMIT_LIST_FRAGMENT = "bitbucket.internal.feature.commits.commitsTable";
    private static final String COMMITS_KEY = "bitbucket.pull-request.nav.commits";
    private static final String CREATE_FORM_PLUGIN_HANDLER_KEY = "bitbucket.page.createPullRequest";
    private static final String DIFF_KEY = "bitbucket.pull-request.nav.diff";
    private static final String OVERVIEW_KEY = "bitbucket.pull-request.nav.overview";
    private static final Set<DiscoverableFeature> PULL_REQUEST_DISCOVERABLE_FEATURES =
            ImmutableSet.of(DiscoverableFeature.NEEDS_WORK, DiscoverableFeature.COMMIT_REVIEW);

    private static final String ITERATIVE_REVIEW_DARK_FEATURE_KEY = "dark.feature.shared.iterative-review";

    @VisibleForTesting
    static final String CREATE_VIEW = "bitbucket.internal.page.createPullRequest";
    @VisibleForTesting
    static final String LIST_VIEW = "bitbucket.internal.page.pullRequest.list";
    @VisibleForTesting
    static final String VIEW = "bitbucket.internal.page.pullRequest.v2.view";

    private final AuthenticationContext authenticationContext;
    private final DarkFeatureManager darkFeatureManager;
    private final FeatureDiscoveryService featureDiscoveryService;
    private final InternalPullRequestService pullRequestService;
    private final NavBuilder navBuilder;
    private final UserService userService;

    @Value(ApplicationConstants.PROP_PULL_REQUEST_MERGE_TIMEOUT)
    private int mergeTimeout;
    @Value(PageConstants.PULLREQUEST_DIFF_CONTEXT)
    private int relevantContextLines;

    @Autowired
    public PullRequestController(I18nService i18nService, RefService refService,
                                 PermissionService permissionService, InternalProjectService projectService,
                                 RepositoryService repositoryService, InternalScmService scmService,
                                 CommitService commitService, PluginFormFragmentsFactory fragmentsFactory,
                                 AuthenticationContext authenticationContext, DarkFeatureManager darkFeatureManager,
                                 FeatureDiscoveryService featureDiscoveryService, NavBuilder navBuilder,
                                 InternalPullRequestService pullRequestService, UserService userService) {
        super(i18nService, refService, permissionService, projectService, repositoryService, scmService,
                commitService, fragmentsFactory);

        this.authenticationContext = authenticationContext;
        this.darkFeatureManager = darkFeatureManager;
        this.featureDiscoveryService = featureDiscoveryService;
        this.navBuilder = navBuilder;
        this.pullRequestService = pullRequestService;
        this.userService = userService;
    }

    @RequestMapping(method = POST)
    public ModelAndView createPullRequest(
            WebRequest request,
            @PathVariable("projectKey") String toProjectKey,
            @PathVariable("repoSlug") String toRepoSlug,
            @Valid PullRequestForm form, Errors errors) {
        Repository fromRepository = getRepository(form.getFromRepoId());
        Repository toRepository = getRepository(toProjectKey, toRepoSlug);
        if (isEmptyRepository(fromRepository)) {
            return handleEmptyRepo(fromRepository);
        } else if (isEmptyRepository(toRepository)) {
            return handleEmptyRepo(toRepository);
        }

        Set<String> reviewers = parseReviewers(form.getReviewers());
        Set<ApplicationUser> resolvedReviewers = null;

        // It's a bit ugly the way we handle errors in a two-step process here:
        // you'll never get reviewer exceptions in pullRequestService.create if
        // you've already gotten title/description exceptions through @Valid PullRequestForm.
        // The reason we do this is that we don't want to have to map field validations on PullRequest (title, description)
        // to their equivalent properties on PullRequestForm (pullRequestTitle, pullRequestDescription) if we can help it.
        PullRequest existingPullRequest = null;
        String fromBranch = form.getFromBranch();
        String toBranch = form.getToBranch();

        Map<String, Object> formFragmentContext = Maps.newHashMap();
        formFragmentContext.put("fromRepository", fromRepository);
        formFragmentContext.put("toRepository", toRepository);

        PluginFormFragments pluginFormFragments = fragmentsFactory.forKey(CREATE_FORM_PLUGIN_HANDLER_KEY, formFragmentContext);
        PluginValidationErrors pluginValidationErrors = new PluginValidationErrors(errors);

        pluginFormFragments.validate(request.getParameterMap(), pluginValidationErrors);

        List<DetailedError> detailedErrors = null;
        if (!pluginValidationErrors.hasFieldErrors() && !errors.hasErrors()) {
            try {
                PullRequest pullRequest = pullRequestService.create(
                        form.getTitle(),
                        form.getDescription(),
                        reviewers,
                        fromRepository,
                        fromBranch,
                        toRepository,
                        toBranch);

                // update any form handlers registered by plugins
                formFragmentContext.put("pullRequest", pullRequest);
                pluginFormFragments = fragmentsFactory.forKey(CREATE_FORM_PLUGIN_HANDLER_KEY, formFragmentContext);
                pluginFormFragments.execute(request.getParameterMap());

                return new ModelAndView(new RedirectView(navBuilder.repo(toRepository).pullRequest(pullRequest.getId()).buildRelative()));
            } catch (DuplicatePullRequestException e) {
                existingPullRequest = e.getExistingPullRequest();
            } catch (InvalidPullRequestReviewersException e) {
                resolvedReviewers = e.getValidReviewers();
                for (KeyedMessage keyedMessage : e.getReviewerErrors().values()) {
                    errors.rejectValue("reviewers", keyedMessage.getKey(), keyedMessage.getLocalisedMessage());
                }
            } catch (RequestCanceledException e) {
                detailedErrors = ImmutableList.of(StashWebErrorUtils.toDetailedError(e));
            } catch (ServiceException e) {
                rejectException(errors, e);
            } catch (ConstraintViolationException e) {
                rejectException(errors, e);
            }
        }

        if (resolvedReviewers == null) {
            resolvedReviewers = resolveReviewers(reviewers);
        }

        String pluginFieldsContent = pluginFormFragments.getErrorHtml(request.getParameterMap(), pluginValidationErrors.getFieldErrors());

        return getCreateViewBuilder(fromRepository, fromBranch, toRepository, toBranch, form.getTitle(),
                form.getDescription(), resolvedReviewers, pluginFieldsContent)
                .putDetailedErrors(detailedErrors)
                .putValidationErrors(errors)
                .putIfNotNull("existingPullRequest", existingPullRequest)
                .build();
    }

    @RequestMapping(method = GET, params = "create")
    public ModelAndView createPullRequestView(
            @PathVariable("projectKey") String projectKey,
            @PathVariable("repoSlug") String repoSlug,
            @RequestParam(value = "sourceBranch", required = false) String fromBranch,
            @RequestParam(value = "targetBranch", required = false) String toBranch,
            @RequestParam(value = "targetRepoId", required = false, defaultValue = NO_TARGET_REPO_ID_AS_STRING) int targetRepoId,
            @RequestParam(value = "title", required = false) String title,
            @RequestParam(value = "description", required = false) String description,
            @RequestParam(value = "reviewers", required = false) String reviewers) {
        Repository fromRepository = getRepository(projectKey, repoSlug);
        if (isEmptyRepository(fromRepository)) {
            return handleEmptyRepo(fromRepository);
        }

        Repository toRepository = getTargetRepository(fromRepository, targetRepoId);
        Repository fork = repositoryService.findPersonalFork(fromRepository);
        Set<Repository> preloadRepositories = fork == null ? null : ImmutableSet.of(fork);

        return getCreateViewBuilder(fromRepository, fromBranch, toRepository, toBranch,
                title, description, resolveReviewers(parseReviewers(reviewers)))
                .putIfNotNull("additionalPreloadRepositories", preloadRepositories)
                .build();
    }

    private Set<String> parseReviewers(String reviewersField) {
        if (StringUtils.isBlank(reviewersField)) {
            return Collections.emptySet();
        }

        Set<String> reviewers = Sets.newHashSet();
        for (String reviewer : reviewersField.split("\\|!\\|")) {
            reviewers.add(reviewer.trim());
        }

        return reviewers;
    }

    private Set<ApplicationUser> resolveReviewers(Set<String> reviewers) {
        return reviewers.stream()
                .map(userService::getUserByName)
                .filter(Objects::nonNull)
                .collect(toImmutableSet());
    }

    private StashSoyResponseBuilder getCreateViewBuilder(Repository fromRepository, String fromBranch,
                                                         Repository toRepository, String toBranch,
                                                         String title, String description,
                                                         Set<ApplicationUser> reviewers, String pluginFieldsContent) {
        return addPullRequestParams(getCompareViewBuilder(CREATE_VIEW, fromRepository, fromBranch, toRepository,
                toBranch, pluginFieldsContent), title, description, reviewers);

    }

    private StashSoyResponseBuilder getCreateViewBuilder(Repository fromRepository, String fromBranch,
                                                         Repository toRepository, String toBranch,
                                                         String title, String description,
                                                         Set<ApplicationUser> reviewers) {
        return addPullRequestParams(getCompareViewBuilder(CREATE_VIEW, CREATE_FORM_PLUGIN_HANDLER_KEY, fromRepository,
                fromBranch, toRepository, toBranch), title, description, reviewers);
    }

    private StashSoyResponseBuilder addPullRequestParams(StashSoyResponseBuilder initialBuilder, String title,
                                                         String description, Set<ApplicationUser> reviewers) {
        return initialBuilder
                .putIfNotBlank("title", title)
                .putIfNotBlank("description", description)
                .putIfNotNull("reviewers", reviewers);
    }

    @RequestMapping(method = GET)
    public ModelAndView listPullRequestsView(
            @PathVariable("projectKey") String projectKey,
            @PathVariable("repoSlug") String repoSlug,
            @RequestParam(value = "state", required = false, defaultValue = "OPEN") String state,
            @RequestParam(value = "direction", required = false, defaultValue = "INCOMING") String direction,
            @RequestParam(value = "at", required = false) String branch,
            @RequestParam(value = "start", required = false, defaultValue = "0") int start,
            @RequestParam(value = "limit", required = false, defaultValue = "25") int limit,
            @RequestParam(value = "order", required = false) String order,
            @RequestParam(value = "author", required = false) String author,
            @RequestParam(value = "reviewing", required = false, defaultValue = "false") boolean reviewing) {
        if (!permissionService.hasGlobalPermission(Permission.LICENSED_USER)) {
            throw new AccessDeniedException(i18nService.getMessage("bitbucket.web.pullrequest.view.accessdenied"));
        }

        Repository repository = getRepository(projectKey, repoSlug);
        PullRequestState prState = getPullRequestState(state);
        PullRequestDirection prDirection = getPullRequestDirection(direction);
        Iterable<PullRequestParticipantRequest> prParticipants = getPullRequestParticipants(author, reviewing);
        PullRequestOrder resultsOrder = fromString(order, getDefaultOrder());
        PageRequest pageRequest = PageUtils.newRequest(start, limit);

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

        boolean repositoryHasPullRequests = true;

        if (pullRequestPage.getSize() == 0) {
            long repositoryPullRequestCount = pullRequestService.count(new PullRequestSearchRequest.Builder()
                    .toRepositoryId(repository.getId())
                    .build());

            repositoryHasPullRequests = repositoryPullRequestCount > 0;
        }

        return new StashSoyResponseBuilder(LIST_VIEW)
                .putRepository(repository)
                .put("pullRequestPage", pullRequestPage)
                .putIfNotNull("pullRequestOrder", resultsOrder == null ? null : resultsOrder.name().toLowerCase(Locale.ROOT))
                .put("repositoryHasPullRequests", repositoryHasPullRequests)
                .putIfNotNull("selectedAuthor", StringUtils.isBlank(author) ? null : first(pullRequestPage.getValues())
                        .map((pullRequest) -> pullRequest.getAuthor().getUser())
                        .<Supplier>getOrElse(() -> userService.getUserByName(author)))
                .putIfNotNull("selectedTargetBranch", StringUtils.isBlank(branch) ? null : first(pullRequestPage.getValues())
                        .<Ref>map(PullRequest::getToRef)
                        .<Supplier>getOrElse(() -> refService.resolveRef(new ResolveRefRequest.Builder(repository)
                                .refId(branch)
                                .build())))
                .build();
    }

    @RequestMapping(value = "/{pullRequestId}", method = GET)
    public ModelAndView viewPullRequest(
            @PathVariable("projectKey") String projectKey,
            @PathVariable("repoSlug") String repoSlug,
            @PathVariable("pullRequestId") long pullRequestId) {
        return new ModelAndView(new RedirectView(navBuilder.project(projectKey).repo(repoSlug).pullRequest(pullRequestId).overview().buildRelative()));
    }

    @RequestMapping(value = "/{pullRequestId}/activity", method = GET)
    public ModelAndView viewPullRequestActivity(
            @PathVariable("projectKey") String projectKey,
            @PathVariable("repoSlug") String repoSlug,
            @PathVariable("pullRequestId") long pullRequestId) {
        return viewPullRequest(projectKey, repoSlug, pullRequestId);
    }

    @RequestMapping(value = "/{pullRequestId}/overview", method = GET)
    public ModelAndView viewPullRequestOverview(
            @PathVariable("projectKey") String projectKey,
            @PathVariable("repoSlug") String repoSlug,
            @PathVariable("pullRequestId") long pullRequestId) {
        Repository repository = getRepository(projectKey, repoSlug);
        PullRequest pullRequest = getPullRequest(repository, pullRequestId);

        return getPullRequestViewBuilder(pullRequest, repository)
                .put("startingTabKey", OVERVIEW_KEY)
                .build();
    }

    @RequestMapping(value = "/{pullRequestId}/commits", method = GET)
    public ModelAndView viewPullRequestCommits(
            @PathVariable("projectKey") String projectKey,
            @PathVariable("repoSlug") String repoSlug,
            @PathVariable("pullRequestId") long pullRequestId,
            @RequestParam(value = "start", required = false, defaultValue = "0") int start,
            @RequestParam(value = "limit", required = false, defaultValue = "25") int limit,
            @RequestParam(value = "contents", required = false, defaultValue = "false") boolean contentsOnly) {
        Repository repository = getRepository(projectKey, repoSlug);
        PullRequest pullRequest = getPullRequest(repository, pullRequestId);
        PageRequest pageRequest = PageUtils.newRequest(start, limit);

        if (contentsOnly) {
            Page<Commit> commitPage = pullRequestService.getCommits(repository.getId(), pullRequestId, pageRequest);
            return new StashSoyResponseBuilder(COMMIT_LIST_FRAGMENT)
                    .putRepository(repository)
                    .putPullRequest(pullRequest)
                    .put("commitPage", commitPage)
                    .build();
        } else {
            return getPullRequestViewBuilder(pullRequest, repository)
                    .put("startingTabKey", COMMITS_KEY)
                    .build();
        }
    }

    @RequestMapping(value = "/{pullRequestId}/commits", method = GET, params = "contents")
    public ModelAndView getPullRequestCommitsContents(
            @PathVariable("projectKey") String projectKey,
            @PathVariable("repoSlug") String repoSlug,
            @PathVariable("pullRequestId") long pullRequestId,
            @RequestParam(value = "start", required = false, defaultValue = "0") int start,
            @RequestParam(value = "limit", required = false, defaultValue = "25") int limit) {
        return viewPullRequestCommits(projectKey, repoSlug, pullRequestId, start, limit, true);
    }

    @RequestMapping(value = "/{pullRequestId}/commits/{commitId}", method = GET)
    public ModelAndView viewPullRequestCommitOrCommitRange(
            @PathVariable("projectKey") String projectKey,
            @PathVariable("repoSlug") String repoSlug,
            @PathVariable("pullRequestId") long pullRequestId,
            @PathVariable("commitId") String commitId,
            @RequestParam(value = "since", required = false, defaultValue = "") String sinceId) {
        if (sinceId.isEmpty()) {
            return getPullRequestCommitView(projectKey, repoSlug, pullRequestId, commitId);
        }

        return getPullRequestRangeDiffView(projectKey, repoSlug, pullRequestId, sinceId, commitId);
    }

    @RequestMapping(value = "/{pullRequestId}/diff", method = GET)
    public ModelAndView viewPullRequestDiff(
            @PathVariable("projectKey") String projectKey,
            @PathVariable("repoSlug") String repoSlug,
            @PathVariable("pullRequestId") long pullRequestId) {

        Repository repository = getRepository(projectKey, repoSlug);
        PullRequest pullRequest = getPullRequest(repository, pullRequestId);

        return getPullRequestDiffView(repository, pullRequest);
    }

    /**
     * This GET mutates state. It is intended to make it possible to unwatch a pull request from an e-mail link, which
     * is not capable of running a {@code DELETE} operation against a REST resource.
     */
    @RequestMapping(value = "/{pullRequestId}", method = GET, params = "unwatch")
    public ModelAndView unwatch(
            @PathVariable("projectKey") String projectKey,
            @PathVariable("repoSlug") String repoSlug,
            @PathVariable("pullRequestId") long pullRequestId,
            RedirectAttributes redirectAttributes) {
        Repository repository = getRepository(projectKey, repoSlug);

        redirectAttributes.addFlashAttribute("unwatched", pullRequestService.unwatch(repository.getId(), pullRequestId));

        return viewPullRequest(projectKey, repoSlug, pullRequestId);
    }

    @Override
    protected void handleNullRef(Repository repository, String branch) {
        if (branch != null) {
            throw new NoSuchCommitException(i18nService.createKeyedMessage("bitbucket.web.pullrequest.commitnotfound",
                    repository.getName(), branch), branch);
        }
    }

    private ModelAndView getPullRequestCommitView(String projectKey, String repoSlug, long pullRequestId, String commitId) {
        Repository repository = getRepository(projectKey, repoSlug);
        PullRequest pullRequest = getPullRequest(repository, pullRequestId);

        if (pullRequest.isCrossRepository()) {
            pullRequestService.ensureUpToDate(pullRequest);
        }

        return getPullRequestDiffView(repository, pullRequest, null, commitId);
    }

    private ModelAndView getPullRequestRangeDiffView(String projectKey, String repoSlug, long pullRequestId, String sinceId, String untilId) {
        Repository repository = getRepository(projectKey, repoSlug);
        PullRequest pullRequest = getPullRequest(repository, pullRequestId);

        if (!darkFeatureManager.isFeatureEnabledForAllUsers(ITERATIVE_REVIEW_DARK_FEATURE_KEY)) {
            throw new NoSuchEntityException(i18nService.createKeyedMessage("bitbucket.web.couldnt.find.title"));
        }

        if (pullRequest.isCrossRepository()) {
            pullRequestService.ensureUpToDate(pullRequest);
        }

        return getPullRequestDiffView(repository, pullRequest, sinceId, untilId);
    }

    private ModelAndView getPullRequestDiffView(Repository repository, PullRequest pullRequest) {
        return getPullRequestDiffView(repository, pullRequest, null, null);
    }

    private ModelAndView getPullRequestDiffView(Repository repository, PullRequest pullRequest, String sinceId, String untilId) {

        StashSoyResponseBuilder responseBuilder = getPullRequestViewBuilder(pullRequest, repository)
                .put("relevantContextLines", relevantContextLines)
                .put("startingTabKey", DIFF_KEY);

        if (untilId != null) {
            if (sinceId == null) {
                Commit commit = commitService.getCommit(new CommitRequest.Builder(repository, untilId).build());
                responseBuilder.putCommit(commit);
            } else {
                responseBuilder
                        .put("sinceCommitId", sinceId)
                        .put("untilCommitId", untilId);
            }
        }

        if (darkFeatureManager.isFeatureEnabledForAllUsers(ITERATIVE_REVIEW_DARK_FEATURE_KEY)) {
            responseBuilder.put("showUnreviewedChanges", true);
        }

        return responseBuilder.build();
    }

    private PullRequest getPullRequest(Repository repository, long pullRequestId) {
        PullRequest pullRequest = pullRequestService.getById(repository.getId(), pullRequestId);
        if (pullRequest == null) {
            throw newNoSuchPullRequestException(repository, pullRequestId);
        }
        return pullRequest;
    }

    private PullRequestDirection getPullRequestDirection(String direction) {
        try {
            return StringUtils.isEmpty(direction) ? PullRequestDirection.INCOMING : PullRequestDirection.valueOf(direction.toUpperCase(Locale.US));
        } catch (IllegalArgumentException e) {
            return PullRequestDirection.INCOMING;
        }
    }

    private PullRequestState getPullRequestState(String state) {
        try {
            return StringUtils.isEmpty(state) ? PullRequestState.OPEN : PullRequestState.valueOf(state.toUpperCase(Locale.US));
        } catch (IllegalArgumentException e) {
            return PullRequestState.OPEN;
        }
    }

    @SuppressWarnings("ConstantConditions")
    private Iterable<PullRequestParticipantRequest> getPullRequestParticipants(String author, boolean reviewing) {
        ImmutableList.Builder<PullRequestParticipantRequest> builder = ImmutableList.builder();

        if (StringUtils.isNotBlank(author)) {
            builder.add(new PullRequestParticipantRequest.Builder(author)
                    .role(PullRequestRole.AUTHOR)
                    .build());
        }

        if (reviewing) {
            builder.add(new PullRequestParticipantRequest.Builder(authenticationContext.getCurrentUser().getName())
                    .role(PullRequestRole.REVIEWER)
                    .build());
        }

        return builder.build();
    }

    private StashSoyResponseBuilder getPullRequestViewBuilder(PullRequest pullRequest, Repository repository) {
        Boolean hasRepoWrite = permissionService.hasRepositoryPermission(repository, Permission.REPO_WRITE);

        Set<DiscoverableFeature> undiscovered =
                featureDiscoveryService.getIfUndiscovered(PULL_REQUEST_DISCOVERABLE_FEATURES);
        StashSoyResponseBuilder builder = new StashSoyResponseBuilder(VIEW)
                .putRepository(repository)
                .putPullRequest(pullRequest)
                .put("maxChanges", maxChanges)
                .put("mergeTimeout", mergeTimeout)
                .put("hasRepoWrite", hasRepoWrite)
                .put("seenNeedsWork", !undiscovered.contains(DiscoverableFeature.NEEDS_WORK))
                .put("seenCommitReview", !undiscovered.contains(DiscoverableFeature.COMMIT_REVIEW));

        featureDiscoveryService.setDiscovered(undiscovered);

        return builder;
    }
}
