package com.atlassian.bitbucket.commit;

import com.atlassian.bitbucket.comment.*;
import com.atlassian.bitbucket.comment.commit.AddCommitFileCommentRequest;
import com.atlassian.bitbucket.comment.commit.AddCommitLineCommentRequest;
import com.atlassian.bitbucket.commit.graph.TraversalCallback;
import com.atlassian.bitbucket.commit.graph.TraversalRequest;
import com.atlassian.bitbucket.content.*;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.validation.ArgumentValidationException;
import com.atlassian.bitbucket.watcher.Watcher;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public interface CommitService {

    /**
     * Adds a general comment on the specified commit in the specified repository.
     *
     * @param repository  the ID of the repository containing the commit
     * @param commitId    the ID of the commit within the repository
     * @param commentText the comment text
     * @return the created comment
     */
    @Nonnull
    Comment addComment(@Nonnull Repository repository, @Nonnull String commitId, @Nonnull String commentText);

    /**
     * Adds a line comment to a commit diff in the specified repository.
     * <p>
     * The request's {@link AddDiffCommentRequest#getToHash() toHash} should generally be omitted. If it is provided,
     * it must match the {@code commitId}. For merge commits, which have a diff for each parent, the desired diff can
     * be selected by providing the parent's ID in the {@link AddDiffCommentRequest#getFromHash() fromHash}. If the
     * {@code fromHash} is {@code null}, the first parent will be assumed.
     *
     * @param repository the ID of the repository containing the commit
     * @param commitId   the ID of the commit within the repository
     * @param request    describes the comment to create
     * @return the created line comment
     * @throws ArgumentValidationException if the {@code request} contains invalid data
     * @deprecated in 4.7 for removal in 5.0. Use {@link #addLineComment(AddCommitLineCommentRequest)} instead
     */
    @Deprecated
    @Nonnull
    Comment addDiffComment(@Nonnull Repository repository, @Nonnull String commitId,
                           @Nonnull AddDiffCommentRequest request);

    /**
     * Adds a file-level comment to a commit diff in the specified repository.
     * <p>
     * For merge commits, which have a diff for each parent, the desired diff can be selected by providing
     * the parent's ID in the {@link AddCommitFileCommentRequest#getFromHash() fromHash}.
     * If the {@code fromHash} is {@code null}, the first parent will be assumed.
     *
     * @param request describes the comment to create
     * @return the created file comment
     * @since 4.7
     */
    @Nonnull
    Comment addFileComment(@Nonnull AddCommitFileCommentRequest request);

    /**
     * Adds a file-level comment to a commit diff in the specified repository.
     * <p>
     * The request's {@link AddFileCommentRequest#getToHash() toHash} should generally be omitted. If it is provided,
     * it must match the {@code commitId}. For merge commits, which have a diff for each parent, the desired diff can
     * be selected by providing the parent's ID in the {@link AddFileCommentRequest#getFromHash() fromHash}. If the
     * {@code fromHash} is {@code null}, the first parent will be assumed.
     *
     * @param repository the ID of the repository containing the commit
     * @param commitId   the ID of the commit within the repository
     * @param request    describes the comment to create
     * @return the created file comment
     * @throws ArgumentValidationException if the {@code request} contains invalid data
     * @deprecated in 4.7 for removal in 5.0. Use {@link #addFileComment(AddCommitFileCommentRequest)} instead.
     */
    @Nonnull
    @Deprecated
    Comment addFileComment(@Nonnull Repository repository, @Nonnull String commitId,
                           @Nonnull AddFileCommentRequest request);

    /**
     * Adds a line comment to a commit diff in the specified repository.
     * <p>
     * For merge commits, which have a diff for each parent, the desired diff can be selected by providing
     * the parent's ID in the {@link AddCommitLineCommentRequest#getFromHash() fromHash}.
     * If the {@code fromHash} is {@code null}, the first parent will be assumed.
     *
     * @param request describes the comment to create
     * @return the created line comment
     * @since 4.7
     */
    @Nonnull
    Comment addLineComment(@Nonnull AddCommitLineCommentRequest request);

    /**
     * Adds a reply to a comment.
     *
     * @param repository  the ID of the repository containing the commit
     * @param commitId    the ID of the commit within the repository
     * @param commentId   the ID of the comment to reply to
     * @param commentText the comment text
     * @return the created comment
     * @throws NoSuchCommentException if no comment exists with the specified ID
     */
    @Nonnull
    Comment addReply(@Nonnull Repository repository, @Nonnull String commitId,
                     long commentId, @Nonnull String commentText);

    /**
     * Deletes a comment.
     *
     * @param repository     the ID of the repository containing the commit
     * @param commitId       the ID of the commit within the repository
     * @param commentId      the ID of the comment to delete
     * @param commentVersion the expected version of the comment
     * @return {@code true} if the comment existed (and thus was deleted); otherwise {@code false}
     * @throws CommentDeletionException if the comment has replies
     * @throws CommentOutOfDateException if the version of the specified comment does not
     *                                                               match the expected version
     */
    boolean deleteComment(@Nonnull Repository repository, @Nonnull String commitId,
                          long commentId, int commentVersion);

    /**
     * Finds {@link DiffCommentAnchor anchors} for {@link DiffCommentAnchor#isFileComment() file} and
     * {@link DiffCommentAnchor#isLineComment() line} comments using the search criteria specified in the request.
     *
     * @param request a request with the anchor search criteria
     * @return the anchors which match the search criteria or an empty iterable if none match.
     * @since 4.7
     */
    Iterable<DiffCommentAnchor> findCommentAnchors(@Nonnull CommitCommentAnchorSearchRequest request);

    /**
     * Finds {@link DiffCommentAnchor anchors} for {@link DiffCommentAnchor#isFileComment() file} and
     * {@link DiffCommentAnchor#isLineComment() line} comments on the specified path in the commit.
     * For merge commits, which have a diff for each parent, the parent ID can also be provided to
     * select the diff to retrieve comments from.
     *
     * @param repository the ID of the repository containing the commit
     * @param commitId   the ID of the commit within the repository
     * @param parentId   the ID of the parent, used to select the desired diff for merge commits. If the parent ID is
     *                   not provided the commit's first parent is automatically used if it has one
     * @param path       the path of the file for which the anchors should be retrieved
     * @return all anchors (representing comments) for the specified modified {@code path} in the commit
     * @deprecated in 4.7 for removal in 5.0. Use {@link #findCommentAnchors(CommitCommentAnchorSearchRequest)} instead.
     */
    @Nonnull
    @Deprecated
    Iterable<DiffCommentAnchor> findCommentAnchors(@Nonnull Repository repository, @Nonnull String commitId,
                                                   @Nullable String parentId, @Nonnull String path);

    /**
     * Retrieves a page of {@link Change changes} between two commits. If the {@link ChangesRequest#getSinceId() since}
     * commit is not specified, the {@link ChangesRequest#getUntilId() until} commit's <i>first</i> parent is used.
     * <p>
     * Note: The implementation will apply a hard cap ({@code page.max.changes}) and it is not
     * possible to request subsequent content when that cap is exceeded.
     *
     * @param request     describes the two commits to retrieve changes between and the repository which contains
     *                    <i>both commits</i>
     * @param pageRequest the start and limit for the page being requested
     * @return a page of changes within the repository between the specified IDs
     */
    @Nonnull
    Page<Change> getChanges(@Nonnull ChangesRequest request, @Nonnull PageRequest pageRequest);

    /**
     * Retrieves changesets for each of the requested {@link ChangesetsRequest#getCommitIds()} commits}, including the
     * first page of changes between each commit and its first parent.
     * <p>
     * If the {@link PageRequest#getLimit() page size} is less than the size of the collection of IDs, a subset of the
     * collection will be returned. Otherwise, changesets for all IDs in the requested collection are returned.
     *
     * @param request     request parameters, specifying the repository and the desired commits
     * @param pageRequest the start and limit of the page being request
     * @return a page containing changesets for a subset of the requested IDs
     * @throws NoSuchCommitException if {@link ChangesetsRequest#isIgnoreMissing() ignoreMissing} is not set and any
     *         of the requested commits cannot be found
     */
    @Nonnull
    Page<Changeset> getChangesets(@Nonnull ChangesetsRequest request, @Nonnull PageRequest pageRequest);

    /**
     * Retrieves a the specified comment by its {@link Comment#getId() ID}.
     *
     * @param repository the ID of the repository containing the commit
     * @param commitId   the ID of the commit within the repository
     * @param commentId  the ID of the comment to retrieve
     * @return the retrieved comment
     * @throws NoSuchCommentException if no comment exists with the specified ID
     */
    @Nonnull
    Comment getComment(@Nonnull Repository repository, @Nonnull String commitId, long commentId);

    /**
     * Retrieves a commit based on te provided {@link CommitRequest request}.
     * <p>
     * If only a {@link CommitRequest#getCommitId() commit ID} is specified, that commit will be returned if it exists.
     * If a {@link CommitRequest#getPath() path} is also provided, the first commit <i>starting from</i> the specified
     * commit ID which modifies that path is returned. If the specified commit does not exist, or if no commit in the
     * ancestry of that commit modifies the specified path, a {@link NoSuchCommitException} is thrown.
     * <p>
     * When retrieving a commit, {@link CommitRequest#getPropertyKeys() extra properties} can also be loaded.
     * Properties are metadata associated with commits by {@code CommitIndexer}s. Until 4.0, the extra properties
     * requested also control the attributes that are loaded. Attributes will be removed in 4.0.
     *
     * @param request describes the commit to retrieve, and the repository to retrieve it from
     * @return the first commit matching the request within the specified repository
     * @throws NoSuchCommitException if no matching commit can be found
     */
    @Nonnull
    Commit getCommit(@Nonnull CommitRequest request);

    /**
     * Retrieves a page of commits, starting from a given branch, tag or commit, optionally filtered to only return
     * commits which modify one or more paths. Commits are returned starting from (and including) the specified
     * {@link CommitsRequest#getCommitId()}, unless one or more {@link CommitsRequest#getPaths() paths} are provided.
     * Then the first commit returned, if any are returned, will be the first commit in the ancestry of the specified
     * starting commit which modifies <i>at least one of</i> the specified files.
     * <p>
     * This service call may be used to:
     * <ul>
     *     <li>Retrieve pages of commits on a given branch or tag</li>
     *     <li>Retrieve pages of ancestors starting from a given commit</li>
     *     <li>Retrieve pages of commits where at least one file from a specified set of files has been modified</li>
     * </ul>
     * Each request is for a <i>single starting point</i>. This means a single branch, tag or commit. For example,
     * retrieving all commits across all of a repository's branches where a given file has been modified in a single
     * request is not possible.
     * <p>
     * When retrieving commits, {@link CommitsRequest#getPropertyKeys() extra properties} can also be loaded.
     * Properties are metadata associated with commits by {@code CommitIndexer}s. Until 4.0, the extra properties
     * requested also control the attributes that are loaded. Attributes will be removed in 4.0.
     * <p>
     * Note: If any of the provided paths is renamed at some point in its history, commits will <i>not</i> follow the
     * path across the rename. In other words, only commits for exact paths provided will be included.
     *
     * @param request     describes the commits to retrieve, and the repository to retrieve them from
     * @param pageRequest the start and limit for the page being requested
     * @return a page of commits, which may be empty but will never be {@code null}
     */
    @Nonnull
    Page<Commit> getCommits(@Nonnull CommitsRequest request, @Nonnull PageRequest pageRequest);

    /**
     * Retrieves a page of commits between the {@link CommitsBetweenRequest#getIncludes() included commits} and the
     * {@link CommitsBetweenRequest#getExcludes() excluded commits}. Included and excluded commits may be specified
     * by branch or tag name or by commit ID. The commits returned are those which are reachable an included commit
     * and not reachable from any excluded commit. While this may imply an ancestral relationship between included
     * and excluded IDs, that is not strictly required. When the IDs are unrelated, all commits reachable from the
     * included commits are candidates for the returned page, and excluded commits are effectively ignored. One or
     * more paths may be provided to further narrow the results, restricting the returned commits to only which modify
     * <i>one or more</i> of the specified paths.
     * <p>
     * Which commits are included and which are excluded is important here. Consider the following example:
     * <ol>
     *     <li>Branch "feature-A" was created from "master" when "master" was at commit A</li>
     *     <li>Commits FA1, FA2 and FA3 were created on the "feature-A" branch</li>
     *     <li>Commits B and C were made on "master" after the branch was created</li>
     * </ol>
     * The graph would look like this:
     * <pre>
     * ---- A ---- B ---- C
     *       \
     *        \
     *        FA1 ---- FA2 ---- FA3
     * </pre>
     * If "master" is excluded and "feature-A" is included, commits FA1, FA2 and FA3 will be returned because they
     * are reachable from "feature-A" but not from "master". If the two IDs were reversed, commits B and C would be
     * returned, because they are reachable from "master" but not from "feature-A". This approach of swapping includes
     * and excludes can be used to determine how two branches have diverged over time.
     * <p>
     * When retrieving commits, {@link CommitRequest#getPropertyKeys() extra properties} can also be loaded.
     * Properties are metadata associated with commits by {@code CommitIndexer}s. Until 4.0, the extra properties
     * requested also control the attributes that are loaded. Attributes will be removed in 4.0.
     * <p>
     * <b>Warning:</b> Commits specified using branch or tag names will be resolved against the <b>primary</b>
     * {@link CommitsBetweenRequest#getRepository() repository}. When retrieving commits between repositories,
     * commits in the {@link CommitsBetweenRequest#getSecondaryRepository() secondary repository} <i>may only be
     * specified by commit ID</i>. This means requesting a page like this will not work:
     * <pre><code>
     *     CommitsBetweenRequest request = new CommitsBetweenRequest.Builder(repository)
     *             .exclude("master")
     *             .include("master")
     *             .secondaryRepository(forkOfRepository)
     *             .build();
     *     Page&lt;Commit&gt; page = commitService.getCommitsBetween(request, PageUtils.newRequest(0, 25));
     * </code></pre>
     * The returned page will always be empty because "master" was resolved on both sides using the same repository,
     * producing the same commit to include and exclude. This also demonstrates that <i>excludes take precedence</i>
     * over includes, as one might expect.
     *
     * @param request     describes the commits to include/exclude, paths to filter by and which repository, or
     *                    repositories, contain the commits
     * @param pageRequest the start and limit for the page being requested
     * @return a page containing 0 or more commits reachable from any included commit and not reachable from
     *         any excluded ones
     */
    @Nonnull
    Page<Commit> getCommitsBetween(@Nonnull CommitsBetweenRequest request, @Nonnull PageRequest pageRequest);

    /**
     * Streams changes between the {@link ChangesRequest#getUntilId() until} and {@link ChangesRequest#getSinceId()
     * since} commits. If the {@link ChangesRequest#getSinceId() since} commit is not specified, the
     * {@link ChangesRequest#getUntilId() until} commit's <i>first</i> parent is used automatically.
     * <p>
     * Note: The implementation will apply a hard cap ({@code page.max.changes}) and it is not
     * possible to request subsequent content when that cap is exceeded.
     *
     * @param request  describes the two commits to retrieve changes between and the repository which contains
     *                 <i>both commits</i>
     * @param callback the callback for receiving the changes
     */
    void streamChanges(@Nonnull ChangesRequest request, @Nonnull ChangeCallback callback);

    /**
     * Streams commits, starting from a given branch, tag or commit, optionally filtered to only include commits which
     * modify one or more paths. Commits are streamed starting from (and including) the specified
     * {@link CommitsRequest#getCommitId()}, unless one or more {@link CommitsRequest#getPaths() paths} are provided.
     * Then the first commit streamed, if any are, will be the first commit in the ancestry of the specified starting
     * commit which modifies <i>at least one of</i> the specified files.
     * <p>
     * This service call may be used to:
     * <ul>
     *     <li>Stream the commits on a given branch or tag</li>
     *     <li>Stream ancestors starting from a given commit</li>
     *     <li>Stream commits where at least one file from a specified set of files has been modified</li>
     * </ul>
     * Each request is for a <i>single starting point</i>. This means a single branch, tag or commit. For example,
     * streaming all commits across all of a repository's branches where a given file has been modified in a single
     * request is not possible.
     * <p>
     * When streaming commits, {@link CommitsRequest#getPropertyKeys() extra properties} can also be loaded.
     * Properties are metadata associated with commits by {@code CommitIndexer}s. Until 4.0, the extra properties
     * requested also control the attributes that are loaded. Attributes will be removed in 4.0.
     * <p>
     * Note: If any of the provided paths is renamed at some point in its history, commits will <i>not</i> follow the
     * path across the rename. In other words, only commits for exact paths provided will be included.
     *
     * @param request  describes the commits to stream, and the repository to stream them from
     * @param callback a callback to receive the commits
     */
    void streamCommits(@Nonnull CommitsRequest request, @Nonnull CommitCallback callback);

    /**
     * Streams commits between the {@link CommitsBetweenRequest#getIncludes() included commits} and the
     * {@link CommitsBetweenRequest#getExcludes() excluded commits}.
     * <p>
     * This method promises to call {@link CommitCallback#onStart(CommitContext)} on the supplied callback exactly once,
     * followed by zero or more calls to {@link CommitCallback#onCommit(Commit)}, one for each commit to be streamed
     * (until {@link CommitCallback#onCommit(Commit) onCommit(...)} returns {@code false} or there are no more commits),
     * followed by exactly one call to {@link CommitCallback#onEnd(CommitSummary)}.
     * <p>
     * See {@link #getCommitsBetween(CommitsBetweenRequest, PageRequest) getCommitsBetween(...)} for a
     * description of the semantics of the commits returned.
     *
     * @param request  describes the commits to include/exclude, paths to filter by and which repository, or
     *                 repositories, contain the commits
     * @param callback a callback to receive the commits
     */
    void streamCommitsBetween(@Nonnull CommitsBetweenRequest request, @Nonnull CommitCallback callback);

    /**
     * Streams diff output for the specified {@link DiffRequest#getPaths() paths} at the specified commit.
     * <p>
     * Note: This interface is currently <i>not paged</i>. The implementation will apply a hard cap
     * ({@code page.max.diff.lines}) and it is not possible to request subsequent content when that cap is exceeded.
     *
     * @param request  the repository, starting and terminating commits, and paths to filter by
     * @param callback the callback for receiving the diff details
     */
    void streamDiff(@Nonnull DiffRequest request, @Nonnull DiffContentCallback callback);

    /**
     * Streams the last modification for files under the requested {@link LastModifiedRequest#getPath path}, starting
     * from the specified {@link LastModifiedRequest#getCommitId() commit}.
     * <p>
     * The {@link LastModifiedRequest#getCommitId commit ID} serves as a <i>starting point</i>. For any files under
     * the requested path, commits are traversed from that starting point until the most recent commit to <i>directly
     * modify</i> each file is found. Because the exact number of commits which must be traversed is unknown, this
     * operation may be very expensive.
     *
     * @param request  the repository, path and starting commit
     * @param callback the callback for receiving last modification data
     * @since 4.6
     */
    void streamLastModified(@Nonnull LastModifiedRequest request, @Nonnull LastModifiedCallback callback);

    /**
     * Traverse the graph of commits in topological order, optionally supplying included commits to start from
     * and excluded commits to ignore.
     *
     * @param request  the traversal request describing the repository and the commits to include and exclude
     * @param callback the callback to receive the traversed commits
     */
    void traverse(@Nonnull TraversalRequest request, @Nonnull TraversalCallback callback);

    /**
     * Removes the current user as a watcher for the specified commit, which will prevent further notifications from
     * being dispatched to that user.
     *
     * @param repository the ID of the repository containing the commit
     * @param commitId   the ID of the commit within the repository
     * @return {@code true} if the watcher existed (and thus was removed); otherwise {@code false}
     */
    boolean unwatch(@Nonnull Repository repository, @Nonnull String commitId);

    /**
     * Updates the {@link Comment#getText() text} for the specified comment.
     *
     * @param repository     the ID of the repository containing the commit
     * @param commitId       the ID of the commit within the repository
     * @param commentId      the ID of the comment to update
     * @param commentVersion the expected version of the comment
     * @param commentText    the new text for the comment
     * @return the updated comment
     * @throws CommentOutOfDateException if the version of the specified comment does not
     *                                                               match the expected version
     * @throws NoSuchCommentException if no comment exists with the specified ID
     */
    @Nonnull
    Comment updateComment(@Nonnull Repository repository, @Nonnull String commitId,
                          long commentId, int commentVersion, @Nonnull String commentText);

    /**
     * Adds the current user as a watcher for the specified commit. Watchers receive notifications when new comments
     * are added to the commit.
     *
     * @param repository the ID of the repository containing the commit
     * @param commitId   the ID of the commit within the repository
     * @return the created watcher
     */
    @Nonnull
    Watcher watch(@Nonnull Repository repository, @Nonnull String commitId);
}
