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

import com.atlassian.bitbucket.comment.Comment;
import com.atlassian.bitbucket.comment.pull.AddPullRequestFileCommentRequest;
import com.atlassian.bitbucket.comment.pull.AddPullRequestLineCommentRequest;
import com.atlassian.bitbucket.content.DiffFileType;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.pull.PullRequest;
import com.atlassian.bitbucket.pull.PullRequestService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.rest.BadRequestException;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.comment.RestComment;
import com.atlassian.bitbucket.rest.comment.RestDiffCommentAnchor;
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.PageUtils;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
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 javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.List;

import static com.atlassian.bitbucket.rest.util.ResourcePatterns.*;
import static com.atlassian.bitbucket.util.MoreCollectors.toImmutableList;
import static com.atlassian.bitbucket.util.MoreStreams.streamIterable;
import static java.util.Optional.ofNullable;

@AnonymousAllowed
@Consumes(MediaType.APPLICATION_JSON)
@Path(PULL_REQUEST_URI + COMMENT_URI)
@Produces(RestUtils.APPLICATION_JSON_UTF8)
@Singleton
public class PullRequestCommentResource extends AbstractPullRequestResource {

    public PullRequestCommentResource(I18nService i18nService, PullRequestService pullRequestService) {
        super(i18nService, pullRequestService);
    }

    /**
     * Add a new comment.
     * <p>
     * Comments can be added in a few places by setting different attributes:
     * <p>
     * General pull request comment:
     *
     * <pre>
     {
         "text": "An insightful general comment on a pull request."
     }
     </pre>
     *
     * Reply to a comment:
     *
     * <pre>
     {
         "text": "A measured reply.",
         "parent": {
             "id": 1
         }
     }
     </pre>
     *
     * General file comment:
     *
     * <pre>
     {
         "text": "An insightful general comment on a file.",
         "anchor": {
             "path": "path/to/file",
             "srcPath": "path/to/file"
         }
     }
     </pre>
     *
     * File line comment:
     *
     * <pre>
     {
         "text": "A pithy comment on a particular line within a file.",
         "anchor": {
             "line": 1,
             "lineType": "CONTEXT",
             "fileType": "FROM"
             "path": "path/to/file",
             "srcPath": "path/to/file"
         }
     }
     </pre>
     * <strong>Note: general file comments are an experimental feature and may change in the near future!</strong>
     * <p>
     * For file and line comments, 'path' refers to the path of the file to which the comment should be applied and
     * 'srcPath' refers to the path the that file used to have (only required for copies and moves).
     * <p>
     * For line comments, 'line' refers to the line in the diff that the comment should apply to. 'lineType' refers to
     * the type of diff hunk, which can be:
     * <ul>
     *     <li>'ADDED' - for an added line;</li>
     *     <li>'REMOVED' - for a removed line; or</li>
     *     <li>'CONTEXT' - for a line that was unmodified but is in the vicinity of the diff.</li>
     * </ul>
     * 'fileType' refers to the file of the diff to which the anchor should be attached - which is of relevance when
     * displaying the diff in a side-by-side way. Currently the supported values are:
     * <ul>
     *     <li>'FROM' - the source file of the diff</li>
     *     <li>'TO' - the destination file of the diff</li>
     * </ul>
     * If the current user is not a participant the user is added as a watcher of the 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 pullRequest the pull request within the repository
     *
     * @request.representation.example {@link RestComment#REQUEST_EXAMPLE}
     * @response.representation.201.mediaType application/json
     * @response.representation.201.qname comment
     * @response.representation.201.example {@link RestComment#RESPONSE_EXAMPLE}
     * @response.representation.201.doc The newly created comment.
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The comment was not created due to a validation error.
     * @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, create a comment or watch 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 Unable to find the supplied project, repository, pull request or parent comment
     */
    @POST
    public Response createComment(@Context Repository repository,
                                  @Context PullRequest pullRequest,
                                  @Context UriInfo uriInfo, RestComment comment) {
        String commentText = comment.getText();
        if (StringUtils.isBlank(commentText)) {
            String message = i18nService.getMessage("bitbucket.rest.comment.addrequirestext");
            throw new BadRequestException(message);
        }

        Comment created;
        if (comment.isReply()) {
            created = pullRequestService.addReply(repository.getId(), pullRequest.getId(),
                    comment.getParent().getId(), commentText);
        } else if (comment.isAnchored()) {
            RestDiffCommentAnchor anchor = comment.getAnchor();
            if (anchor.isLineComment()) {
                validateLineAnchor(anchor);

                created = pullRequestService.addLineComment(new AddPullRequestLineCommentRequest.Builder(pullRequest)
                        .fileType(ofNullable(anchor.getFileType())
                                .orElse(DiffFileType.forSegmentType(anchor.getLineType())))
                        .line(anchor.getLine())
                        .lineType(anchor.getLineType())
                        .path(anchor.getPath().toString())
                        .srcPath(ofNullable(anchor.getSrcPath()).map(Object::toString).orElse(null))
                        .text(commentText)
                        .build());
            } else {
                created = pullRequestService.addFileComment(new AddPullRequestFileCommentRequest.Builder(pullRequest)
                        .path(anchor.getPath().toString())
                        .srcPath(ofNullable(anchor.getSrcPath()).map(Object::toString).orElse(null))
                        .text(commentText)
                        .build());
            }
        } else {
            created = pullRequestService.addComment(repository.getId(), pullRequest.getId(), commentText);
        }
        URI uri = uriInfo.getRequestUriBuilder()
                .path(String.valueOf(created.getId()))
                .build();

        return ResponseFactory.created(uri)
                .entity(new RestComment(created))
                .build();
    }

    /**
     * Delete a pull request comment. Anyone can delete their own comment. Only users with <strong>REPO_ADMIN</strong>
     * and above may delete comments created by other users.
     * <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 commentId     the id of the comment
     * @param version       The expected version of the comment. This must match the server's version of the comment or
     *                      the delete will fail. To determine the current version of the comment, the comment should be
     *                      fetched from the server prior to the delete. Look for the 'version' attribute in the
     *                      returned JSON structure.
     *
     * @response.representation.204.doc The operation was successful
     * @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 delete the comment.
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc Unable to find the supplied project, repository or pull request.
     * @response.representation.409.mediaType application/json
     * @response.representation.409.qname errors
     * @response.representation.409.example {@link RestErrors#EXAMPLE}
     * @response.representation.409.doc The comment has replies or the version supplied does not match the current version.
     */
    @DELETE
    @Path(COMMENT_ID_PATH)
    public Response deleteComment(@Context Repository repository,
                                  @PathParam(PULL_REQUEST_ID) long pullRequestId,
                                  @PathParam(COMMENT_ID) long commentId,
                                  @QueryParam("version") @DefaultValue("-1") int version) {
        pullRequestService.deleteComment(repository.getId(), pullRequestId, commentId, version);

        return ResponseFactory.noContent().build();
    }

    /**
     * Retrieves a pull request comment.
     * <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 commentId     the id of the comment to retrieve
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname comment
     * @response.representation.200.example {@link RestComment#RESPONSE_EXAMPLE}
     * @response.representation.200.doc The requested comment.
     * @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 comment
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc Unable to find the supplied project, repository, pull request or comment
     */
    @GET
    @Path(COMMENT_ID_PATH)
    public Response getComment(@Context Repository repository,
                               @PathParam(PULL_REQUEST_ID) long pullRequestId,
                               @PathParam(COMMENT_ID) long commentId) {
        Comment comment = pullRequestService.getComment(repository.getId(), pullRequestId, commentId);

        return ResponseFactory.ok(new RestComment(comment)).build();
    }

    @GET
    public Response getComments(@Context Repository repository,
                                @PathParam(PULL_REQUEST_ID) long pullRequestId,
                                @QueryParam("path") String path) {
        if (StringUtils.isBlank(path)) {
            throw new BadRequestException(i18nService.getMessage("bitbucket.rest.comments.findrequirespath"));
        }
        List<RestComment> comments = streamIterable(pullRequestService.findCommentAnchors(repository.getId(), pullRequestId, path))
                .map(RestComment.FROM_ANCHOR)
                .collect(toImmutableList());
        Page<RestComment> page = PageUtils.createPage(comments, true,
                PageUtils.newRequest(0, Math.max(25, comments.size())));

        return ResponseFactory.ok(new RestPage<>(page)).build();
    }

    /**
     * Update the text of a comment. Only the user who created a comment may update it.
     * <p>
     * <strong>Note:</strong> the supplied supplied JSON object must contain a <code>version</code> that must match the
     * server's version of the comment or the update will fail. To determine the current version of
     * the comment, the comment should be fetched from the server prior to the update. Look for the
     * 'version' attribute in the returned JSON structure.
     * <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 commentId     the id of the comment to update
     *
     * @request.representation.example {@link RestComment#REQUEST_EXISTING_EXAMPLE}
     * @response.representation.201.mediaType application/json
     * @response.representation.201.qname comment
     * @response.representation.201.example {@link RestComment#RESPONSE_EXAMPLE}
     * @response.representation.201.doc The newly updated comment.
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The comment was not updated due to a validation error.
     * @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, update a comment or watch 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 Unable to find the supplied project, repository, pull request or comment
     * @response.representation.409.mediaType application/json
     * @response.representation.409.qname errors
     * @response.representation.409.example {@link RestErrors#EXAMPLE}
     * @response.representation.409.doc The comment version supplied does not match the current version.
     */
    @PUT
    @Path(COMMENT_ID_PATH)
    public Response updateComment(@Context Repository repository,
                                  @PathParam(PULL_REQUEST_ID) long pullRequestId,
                                  @PathParam(COMMENT_ID) long commentId,
                                  RestComment comment) {
        if (StringUtils.isBlank(comment.getText())) {
            String message = i18nService.getMessage("bitbucket.rest.comment.updaterequirestext");
            throw new BadRequestException(message);
        }
        Comment updated = pullRequestService.updateComment(repository.getId(), pullRequestId, commentId,
                comment.getVersion(), comment.getText());

        return ResponseFactory.ok(new RestComment(updated)).build();
    }

    private void validateLineAnchor(RestDiffCommentAnchor anchor) {
        if (anchor.getLine() < 1) {
            throw new BadRequestException("anchor.line",
                    i18nService.getMessage("bitbucket.rest.comment.anchor.invalidline", anchor.getLine()));
        }

        try {
            if (anchor.getLineType() == null) {
                throw new BadRequestException("anchor.lineType",
                        i18nService.getMessage("bitbucket.rest.comment.anchor.emptylinetype"));
            }
        } catch (IllegalArgumentException e) {
            throw new BadRequestException("anchor.lineType",
                    i18nService.getMessage("bitbucket.rest.comment.anchor.invalidlinetype",
                            anchor.get(RestDiffCommentAnchor.LINE_TYPE)));
        }

        try {
            anchor.getFileType();
        } catch (IllegalArgumentException e) {
            throw new BadRequestException("anchor.fileType",
                    i18nService.getMessage("bitbucket.rest.comment.anchor.invalidfiletype",
                            anchor.get(RestDiffCommentAnchor.FILE_TYPE)));
        }
    }
}
