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

import com.atlassian.bitbucket.rest.RestResource;
import com.atlassian.bitbucket.rest.content.RestDiff;
import com.atlassian.bitbucket.rest.BadRequestException;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.util.*;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.atlassian.bitbucket.commit.CommitService;
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.comment.RestComment;
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.io.IOException;

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

@AnonymousAllowed
@Consumes({MediaType.APPLICATION_JSON})
@Path(ResourcePatterns.REPOSITORY_URI + "/diff")
@Produces({RestUtils.APPLICATION_JSON_UTF8})
@Singleton
public class DiffResource extends RestResource {

    private final CommitService commitService;

    public DiffResource(I18nService i18nService, CommitService commitService) {
        super(i18nService);

        this.commitService = commitService;
    }

    /**
     * Retrieve the diff for a specified file path 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</code> flags will be set to
     * <code>true</code> on the segments, hunks and diffs substructures 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 contextLines the number of context lines to include around added/removed lines in the diff
     * @param path         the path to the file which should be diffed (required)
     * @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 untilId      the target revision to diff to (required)
     * @param whitespace   optional whitespace flag which can be set to <code>ignore-all</code>
     * @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 of a file path.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The path or until parameters were 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("path") String path,
                               @QueryParam("contextLines") @DefaultValue("-1") int contextLines,
                               @QueryParam("since") String sinceId,
                               @QueryParam("srcPath") String srcPath,
                               @QueryParam("until") String untilId,
                               @QueryParam("whitespace") String whitespace) {
        if (StringUtils.isBlank(path)) {
            throw new BadRequestException(i18nService.getMessage("bitbucket.rest.diff.path.required"));
        }
        if (StringUtils.isBlank(untilId)) {
            throw new BadRequestException(i18nService.getMessage("bitbucket.rest.diff.until.required"));
        }

        DiffRequest.Builder builder = new DiffRequest.Builder(repository, untilId)
                .contextLines(contextLines)
                .path(path)
                .sinceId(sinceId)
                .whitespace(DiffWhitespace.fromString(whitespace))
                .withComments(false); //For the legacy endpoint, _never return comments_
        if (StringUtils.isNotBlank(srcPath)) {
            builder.path(srcPath);
        }

        return ResponseFactory.ok(new JsonStreamingOutput() {

            @Override
            public void write(StatefulJsonWriter writer) throws IOException, WebApplicationException {
                commitService.streamDiff(builder.build(), new JsonDiffContentCallback(writer, RestComment.REST_TRANSFORM));
            }
        }, 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,
                               @QueryParam("contextLines") @DefaultValue("-1") int contextLines,
                               @QueryParam("since") String sinceId,
                               @QueryParam("srcPath") String srcPath,
                               @QueryParam("until") String untilId,
                               @QueryParam("whitespace") String whitespace) {
        //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, null, contextLines, sinceId, srcPath, untilId, whitespace);
    }
}
