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

import com.atlassian.bitbucket.commit.*;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.repository.RefService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.annotation.SecondaryRepository;
import com.atlassian.bitbucket.rest.commit.RestCommit;
import com.atlassian.bitbucket.rest.enrich.AvatarEnricher;
import com.atlassian.bitbucket.rest.enrich.LinkEnricher;
import com.atlassian.bitbucket.rest.util.*;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.validation.ArgumentValidationException;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.atlassian.stash.internal.rest.pull.JsonCommitCallback;
import com.sun.jersey.spi.container.ContainerRequest;
import com.sun.jersey.spi.resource.Singleton;
import org.apache.commons.lang3.StringUtils;

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.List;
import java.util.Locale;
import java.util.function.Function;

import static com.atlassian.bitbucket.util.MoreCollectors.toImmutableList;
import static java.util.Arrays.stream;

@AnonymousAllowed
@Consumes({MediaType.APPLICATION_JSON})
@Path(ResourcePatterns.REPOSITORY_URI + "/commits")
@Produces({RestUtils.APPLICATION_JSON_UTF8})
@Singleton
public class CommitResource {

    private final AvatarEnricher avatarEnricher;
    private final CommitService commitService;
    private final I18nService i18nService;
    private final LinkEnricher linkEnricher;
    private final RefService refService;

    public CommitResource(AvatarEnricher avatarEnricher, CommitService commitService, I18nService i18nService,
                          LinkEnricher linkEnricher, RefService refService) {
        this.avatarEnricher = avatarEnricher;
        this.commitService = commitService;
        this.i18nService = i18nService;
        this.linkEnricher = linkEnricher;
        this.refService = refService;
    }

    /**
     * Retrieve a page of commits from a given starting commit or "between" two commits. If no explicit commit is
     * specified, the tip of the repository's default branch is assumed. commits may be identified by branch or tag
     * name or by ID. A path may be supplied to restrict the returned commits to only those which affect that path.
     * <p>
     * The authenticated user must have <b>REPO_READ</b> permission for the specified repository to call this
     * resource.
     *
     * @param avatarSize if present the service adds avatar URLs for commit authors. Should be an integer specifying
     *                   the desired size in pixels. If the parameter is not present, avatar URLs will not be set
     * @param avatarScheme the desired scheme for the avatar URL. If the parameter is not present URLs will use the
     *                     same scheme as this request
     * @param commitId the commit ID (SHA1) or ref (inclusively) to retrieve commits before
     * @param followRenames if {@code true}, the commit history of the specified file will be followed past renames.
     *                      Only valid for a path to a single file.
     * @param ignoreMissing {@code true} to ignore missing commits, {@code false} otherwise
     * @param merges if present, controls how merge commits should be filtered. Can be either {@code exclude}, to
     *               exclude merge commits, {@code include}, to include both merge commits and non-merge commits or
     *               {@code only}, to only return merge commits.
     * @param parentId the commit ID or ref (exclusively) to retrieve commits after
     * @param path an optional path to filter commits by
     * @param withCounts optionally include the total number of commits and total number of unique authors
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestCommit#PAGE_EXAMPLE}
     * @response.representation.200.doc A page of commits.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc One of the supplied commit IDs or refs was invalid.
     *
     * @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 repository.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The repository does not exist.
     */
    @GET
    public Response getCommits(@Context Repository repository,
                               @QueryParam("followRenames") @DefaultValue("false") boolean followRenames,
                               @QueryParam("ignoreMissing") @DefaultValue("false") boolean ignoreMissing,
                               @QueryParam("merges") String merges,
                               @QueryParam("path") String path,
                               @QueryParam("since") String parentId,
                               @QueryParam("until") String commitId,
                               @QueryParam("withCounts") @DefaultValue("false") boolean withCounts,
                               @Context PageRequest pageRequest,
                               @Context ContainerRequest request,
                               @SecondaryRepository Repository secondaryRepository) {
        String untilOrAt = chooseInclude(repository, commitId); //Called first because it may throw
        if (followRenames && StringUtils.isBlank(path)) {
            throw new ArgumentValidationException(
                    i18nService.createKeyedMessage("bitbucket.rest.commits.followrequirespath"));
        }

        //After the top-level transformer creates the RestCommit from the Commit, this applies links
        Function<RestCommit, RestCommit> enrich = commit -> {
            linkEnricher.enrich(commit);

            return commit;
        };

        Function<Commit, RestCommit> commitTransformer =
                //Depending on whether avatars were requested, this transformer will apply avatar URLs
                RestCommit.createTransformer(request, avatarEnricher).andThen(enrich);

        CommitsBetweenRequest.Builder builder = new CommitsBetweenRequest.Builder(repository);

        if (merges != null) {
            CommitListMergeFilter mergeFilter = CommitListMergeFilter.fromString(merges, null);

            if (mergeFilter == null) {
                throw new ArgumentValidationException(
                        i18nService.createKeyedMessage("bitbucket.rest.commits.mergefilter.invalid", merges,
                                toLowercaseNames(CommitListMergeFilter.values())));
            }

            builder.merges(mergeFilter);
        }

        return ResponseFactory.ok(new JsonStreamingOutput() {

            @Override
            public void write(StatefulJsonWriter writer) throws WebApplicationException {
                CommitsBetweenRequest request = builder.exclude(parentId)
                        .followRenames(followRenames)
                        .ignoreMissing(ignoreMissing)
                        .include(untilOrAt)
                        .path(path)
                        .secondaryRepository(secondaryRepository)
                        .build();

                commitService.streamCommitsBetween(request,
                        new JsonCommitCallback(writer, commitTransformer, withCounts, pageRequest));
            }
        }).build();
    }

    /**
     * Retrieve a single commit <i>identified by its ID</i>>. In general, that ID is a SHA1. <u>From 2.11, ref names
     * like "refs/heads/master" are no longer accepted by this resource.</u>
     * <p>
     * The authenticated user must have <strong>REPO_READ</strong> permission for the specified repository to call this
     * resource.
     *
     * @param commitId the commit ID to retrieve
     * @param path an optional path to filter the commit by. If supplied the details returned <i>may not</i>
     *             be for the specified commit. Instead, starting from the specified commit, they will be the
     *             details for the first commit affecting the specified path.
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname commit
     * @response.representation.200.example {@link RestCommit#EXAMPLE}
     * @response.representation.200.doc A commit.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The supplied commit ID was invalid.
     *
     * @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 repository.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The repository does not exist.
     */
    @GET
    @Path("{commitId}")
    public Response getCommit(@Context Repository repository,
                              @PathParam("commitId") String commitId,
                              @QueryParam("path") String path) {
        Commit commit = commitService.getCommit(new CommitRequest.Builder(repository, commitId)
                .path(path)
                .build());

        return ResponseFactory.ok(new RestCommit(commit)).build();
    }

    private String chooseInclude(Repository repository, String commitId) {
        if (StringUtils.isBlank(commitId)) { //If not available, use the default branch
            commitId = refService.getDefaultBranch(repository).getLatestCommit();
        }

        return commitId;
    }

    private List<String> toLowercaseNames(Enum[] values) {
        return stream(values)
                .map(value -> value.name().toLowerCase(Locale.ROOT))
                .collect(toImmutableList());
    }
}
