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

import com.atlassian.bitbucket.avatar.AvatarRequest;
import com.atlassian.bitbucket.comment.Comment;
import com.atlassian.bitbucket.commit.CommitService;
import com.atlassian.bitbucket.content.DiffContentFilter;
import com.atlassian.bitbucket.content.DiffRequest;
import com.atlassian.bitbucket.content.DiffWhitespace;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.RestResource;
import com.atlassian.bitbucket.rest.comment.RestComment;
import com.atlassian.bitbucket.rest.content.RestDiff;
import com.atlassian.bitbucket.rest.enrich.AvatarEnricher;
import com.atlassian.bitbucket.rest.util.*;
import com.atlassian.bitbucket.validation.ArgumentValidationException;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.atlassian.stash.internal.rest.content.JsonDiffContentCallback;
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.function.Function;

import static com.atlassian.bitbucket.rest.util.CachePolicies.getCacheControlForRange;
import static com.atlassian.bitbucket.rest.util.CachePolicies.noCache;

/**
 * @since 2.11
 */
@AnonymousAllowed
@Consumes({MediaType.APPLICATION_JSON})
@Path(ResourcePatterns.COMMIT_URI + "/diff")
@Produces({RestUtils.APPLICATION_JSON_UTF8})
@Singleton
public class CommitDiffResource extends RestResource {

    private final AvatarEnricher avatarEnricher;
    private final CommitService commitService;

    public CommitDiffResource(I18nService i18nService, AvatarEnricher avatarEnricher, CommitService commitService) {
        super(i18nService);

        this.avatarEnricher = avatarEnricher;
        this.commitService = commitService;
    }

    /**
     * Retrieve the diff between two provided revisions.
     * <p>
     * <strong>Note:</strong> This resource is currently <i>not paged</i>. The server will internally apply a hard cap
     * to the streamed lines, and it is not possible to request subsequent pages if that cap is exceeded. In the event
     * that the cap is reached, the diff will be cut short and one or more {@code truncated} flags will be set to
     * {@code true} on the {@code "segments"}, {@code "hunks"} and {@code "diffs"} properties, as well as the top-level
     * object, in the returned JSON response.
     * <p>
     * The authenticated user must have <strong>REPO_READ</strong> permission for the specified repository to call this
     * resource.
     *
     * @param path          the path to the file which should be diffed (optional)
     * @param autoSrcPath   {@code true} to automatically try to find the source path when it's not provided,
     *                      {@code false} otherwise. Requires the {@code path} to be provided.
     * @param avatarSize    if present the service adds avatar URLs for comment authors where the provided value
     *                      specifies the desired avatar size in pixels
     * @param avatarScheme  the security scheme for avatar URLs. If the scheme is not present then it is inherited
     *                      from the request. It can be set to "https" to force the use of secure URLs
     * @param contextLines  the number of context lines to include around added/removed lines in the diff
     * @param filter        text used to filter files and lines (optional)
     * @param pullRequestId when comments should be returned as well ({@code withComments == true}), a pull request ID
     *                      can be provided to only retrieve comments that were made in the context of a pull request.
     *                      If no pull request ID is provided, only comments made outside the context of
     *                      any pull request will be retrieved.
     * @param sinceId       the base revision to diff from. If omitted the parent revision of the until revision is used
     * @param srcPath       the source path for the file, if it was copied, moved or renamed
     * @param whitespace    optional whitespace flag which can be set to {@code ignore-all}
     * @param withComments  {@code true} to embed comments in the diff (the default); otherwise {@code false}
     *                      to stream the diff without comments
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname diff
     * @response.representation.200.example {@link RestDiff#PAGE_EXAMPLE}
     * @response.representation.200.doc A diff between two revisions.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The until parameter was not supplied.
     *
     * @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("{path:.*}")
    public Response streamDiff(@Context Repository repository,
                               @PathParam(ResourcePatterns.COMMIT_ID) String untilId,
                               @PathParam("path") String path,
                               @QueryParam("autoSrcPath") @DefaultValue("false") boolean autoSrcPath,
                               @QueryParam("contextLines") @DefaultValue("-1") int contextLines,
                               @QueryParam("pullRequestId") Long pullRequestId,
                               @QueryParam("since") String sinceId,
                               @QueryParam("srcPath") String srcPath,
                               @QueryParam("whitespace") String whitespace,
                               @QueryParam("withComments") @DefaultValue("true") boolean withComments,
                               @Context DiffContentFilter filter,
                               @Context ContainerRequest request) {
        DiffRequest.Builder builder = new DiffRequest.Builder(repository, untilId)
                .contextLines(contextLines)
                .filter(filter)
                .path(path)
                .pullRequest(pullRequestId)
                .sinceId(sinceId)
                .whitespace(DiffWhitespace.fromString(whitespace))
                .withComments(withComments);

        if (StringUtils.isNotBlank(srcPath)) {
            builder.path(srcPath);
        } else if (autoSrcPath) {
            if (StringUtils.isBlank(path)) {
                throw new ArgumentValidationException(
                        i18nService.createKeyedMessage("bitbucket.rest.commits.autoscrpathrequirespath"));
            }
            builder.autoSrcPath(true);
        }

        DiffRequest diffRequest = builder.build();
        Function<Comment, RestComment> commentTransformer = createAvatarCommentTransformer(request);

        return ResponseFactory.ok(new JsonStreamingOutput() {

            @Override
            public void write(StatefulJsonWriter writer) {
                //Note: The comment transform used here doesn't matter, as these diffs will never contain comments
                commitService.streamDiff(diffRequest, new JsonDiffContentCallback(writer, commentTransformer));
            }
        }, withComments ? noCache() : getCacheControlForRange(untilId, sinceId)).build();
    }

    //This method is intentionally undocumented. The REST doclet generator takes the docs from the first method
    //for overloaded methods and applies it to both.
    @GET
    public Response streamDiff(@Context Repository repository,
                               @PathParam(ResourcePatterns.COMMIT_ID) String untilId,
                               @QueryParam("autoSrcPath") @DefaultValue("false") boolean autoSrcPath,
                               @QueryParam("contextLines") @DefaultValue("-1") int contextLines,
                               @QueryParam("pullRequestId") Long pullRequestId,
                               @QueryParam("since") String sinceId,
                               @QueryParam("srcPath") String srcPath,
                               @QueryParam("whitespace") String whitespace,
                               @QueryParam("withComments") @DefaultValue("true") boolean withComments,
                               @Context DiffContentFilter filter,
                               @Context ContainerRequest request) {
        //This method _only_ exists to get a "nicer" error when no path is specified. Otherwise, the error is
        //something about mappings, rather than an indicator that a path is required
        return streamDiff(repository, untilId, null, autoSrcPath, contextLines, pullRequestId, sinceId, srcPath,
                whitespace, withComments, filter, request);
    }

    /**
     * Returns a {@code Function} which translates a {@link Comment} to a {@link RestComment}.
     * <p>
     * If avatars have been requested, as determined by examining the provided {@code ContainerRequest}, the returned
     * function will automatically populate avatar URLs for comment authors.
     *
     * @param request the container request, used to determine whether avatars were requested
     * @return a function for transforming comments for REST
     */
    private Function<Comment, RestComment> createAvatarCommentTransformer(ContainerRequest request) {
        AvatarRequest avatarRequest = RestUtils.makeAvatarRequest(request);
        if (avatarRequest == null) {
            //If avatars have not been requested, a simple function transforming to a RestComment is enough
            return RestComment.REST_TRANSFORM;
        }

        //If avatars have been requested, return a function which, in addition to transforming to a RestComment, also
        //populates the avatar URL on the comment's author
        return new Function<Comment, RestComment>() {

            @Override
            public RestComment apply(Comment comment) {
                RestComment restComment = new RestComment(comment, this);
                avatarEnricher.enrich(restComment, avatarRequest);

                return restComment;
            }
        };
    }
}
