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

import com.atlassian.bitbucket.comment.Comment;
import com.atlassian.bitbucket.comment.commit.AddCommitFileCommentRequest;
import com.atlassian.bitbucket.comment.commit.AddCommitLineCommentRequest;
import com.atlassian.bitbucket.commit.Commit;
import com.atlassian.bitbucket.commit.CommitCommentAnchorSearchRequest;
import com.atlassian.bitbucket.commit.CommitService;
import com.atlassian.bitbucket.content.DiffFileType;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.rest.BadRequestException;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.RestResource;
import com.atlassian.bitbucket.rest.comment.RestComment;
import com.atlassian.bitbucket.rest.comment.RestDiffCommentAnchor;
import com.atlassian.bitbucket.rest.pull.RestPullRequest;
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.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;

/**
 * @since 2.11
 */
@AnonymousAllowed
@Consumes(MediaType.APPLICATION_JSON)
@Path(COMMIT_URI + COMMENT_URI)
@Produces(RestUtils.APPLICATION_JSON_UTF8)
@Singleton
public class CommitCommentResource extends RestResource {

    private final CommitService commitService;

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

        this.commitService = commitService;
    }

    /**
     * Add a new comment.
     * <p>
     * Comments can be added in a few places by setting different attributes:
     * <p>
     * General commit comment:
     *
     * <pre>
     {
         "text": "An insightful general comment on a commit."
     }
     </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 one and updated to watch the commit.
     * <p>
     * The authenticated user must have <strong>REPO_READ</strong> permission for the repository that the commit
     * is in to call this resource.
     *
     * @param commitId the <i>full {@link Commit#getId() ID}</i> of the commit 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
     *                                  commit, create a comment or watch the commit.
     * @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, commit or parent comment.
     *                                  The missing entity will be specified in the error details.
     */
    @POST
    public Response createComment(@Context Repository repository,
                                  @PathParam(COMMIT_ID) String commitId,
                                  @QueryParam("since") String parentId,
                                  @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 = commitService.addReply(repository, commitId,
                    comment.getParent().getId(), commentText);
        } else if (comment.isAnchored()) {
            RestDiffCommentAnchor anchor = comment.getAnchor();
            if (anchor.isLineComment()) {
                validateLineAnchor(anchor);

                created = commitService.addLineComment(new AddCommitLineCommentRequest.Builder(repository, commitId)
                        .fileType(ofNullable(anchor.getFileType())
                                .orElse(DiffFileType.forSegmentType(anchor.getLineType())))
                        .fromHash(parentId)
                        .line(anchor.getLine())
                        .lineType(anchor.getLineType())
                        .path(anchor.getPath().toString())
                        .pullRequestId(ofNullable(anchor.getPullRequest())
                                .map(RestPullRequest::getId)
                                .orElse(null))
                        .srcPath(ofNullable(anchor.getSrcPath()).map(Object::toString).orElse(null))
                        .text(commentText)
                        .build());
            } else {
                created = commitService.addFileComment(new AddCommitFileCommentRequest.Builder(repository, commitId)
                        .fromHash(parentId)
                        .path(anchor.getPath().toString())
                        .pullRequestId(ofNullable(anchor.getPullRequest())
                                .map(RestPullRequest::getId)
                                .orElse(null))
                        .srcPath(ofNullable(anchor.getSrcPath()).map(Object::toString).orElse(null))
                        .text(commentText)
                        .build());
            }
        } else {
            created = commitService.addComment(repository, commitId, commentText);
        }
        URI uri = uriInfo.getRequestUriBuilder()
                .path(String.valueOf(created.getId()))
                .build();

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

    /**
     * Delete a commit comment. Anyone can delete their own comment. Only users with <strong>REPO_ADMIN</strong>
     * and above may delete comments created by other users. Comments which have replies <i>may not be deleted</i>,
     * regardless of the user's granted permissions.
     * <p>
     * The authenticated user must have <strong>REPO_READ</strong> permission for the repository that the commit
     * is in to call this resource.
     *
     * @param commitId  the <i>full {@link Commit#getId() ID}</i> of the commit 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 commit. The missing
     *                                  entity will be specified in the error details.
     * @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 comment's
     *                                  current version.
     */
    @DELETE
    @Path(COMMENT_ID_PATH)
    public Response deleteComment(@Context Repository repository,
                                  @PathParam(COMMIT_ID) String commitId,
                                  @PathParam(COMMENT_ID) long commentId,
                                  @QueryParam("version") @DefaultValue("-1") int version) {
        commitService.deleteComment(repository, commitId, commentId, version);

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

    /**
     * Retrieves a commit discussion comment.
     * <p>
     * The authenticated user must have <strong>REPO_READ</strong> permission for the repository that the commit
     * is in to call this resource.
     *
     * @param commitId  the <i>full {@link Commit#getId() ID}</i> of the commit 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, commit or comment. The
     *                                  missing entity will be specified in the error details.
     */
    @GET
    @Path(COMMENT_ID_PATH)
    public Response getComment(@Context Repository repository,
                               @PathParam(COMMIT_ID) String commitId,
                               @PathParam(COMMENT_ID) long commentId) {
        Comment comment = commitService.getComment(repository, commitId, commentId);

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

    /**
     * Retrieves the commit discussion comments that match the specified search criteria.
     * <p>
     * The authenticated user must have <strong>REPO_READ</strong> permission for the repository that the commit
     * is in to call this resource.
     *
     * @param repository    the repository which contains the commit
     * @param commitId      the commit to which the comments must be anchored
     * @param path          the path to the file on which comments were made
     * @param parentId      for merge commits a parent can be provided to specify which diff the comments must be on
     * @param pullRequestId 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.
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestPullRequest#PAGE_EXAMPLE}
     * @response.representation.200.doc A page of comments that match the search criteria
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#VALIDATION_EXAMPLE}
     * @response.representation.400.doc The request was malformed.
     *
     * @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, or commit. The
     *                                  missing entity will be specified in the error details.
     */
    @GET
    public Response getComments(@Context Repository repository,
                                @PathParam(COMMIT_ID) String commitId,
                                @QueryParam("path") String path,
                                @QueryParam("pullRequestId") Long pullRequestId,
                                @QueryParam("since") String parentId) {
        if (StringUtils.isBlank(path)) {
            throw new BadRequestException(i18nService.getMessage("bitbucket.rest.comments.findrequirespath"));
        }

        List<RestComment> comments = streamIterable(commitService.findCommentAnchors(new CommitCommentAnchorSearchRequest.Builder(repository, commitId, path)
                .parentId(parentId)
                .pullRequestId(pullRequestId)
                .build()))
                .map(RestComment.FROM_ANCHOR)
                .collect(toImmutableList());

        return ResponseFactory.ok(new RestPage<>(PageUtils.createPage(comments, true,
                PageUtils.newRequest(0, Math.max(25, comments.size()))))).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 the commit
     * is in to call this resource.
     *
     * @param commitId  the <i>full {@link Commit#getId() ID}</i> of the commit 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
     *                                  commit, update the comment or watch the commit.
     * @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, commit or comment. The
     *                                  missing entity will be specified in the error details.
     * @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(COMMIT_ID) String commitId,
                                  @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 = commitService.updateComment(repository, commitId, 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)));
        }
    }
}
