package com.atlassian.bitbucket.scm.pull;

import com.atlassian.bitbucket.pull.PullRequest;
import com.atlassian.bitbucket.pull.PullRequestState;
import com.atlassian.bitbucket.pull.RescopeDetails;

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

/**
 * Provides a context for SCMs to detect {@link PullRequestRescope pull request rescopes} when a repository is updated.
 * <p>
 * For the SCM it does not matter what pull request is being rescoped by each of the
 * {@link PullRequestRescope rescopes}. To keep the SPI as simple and flexible as possible, only the repositories and
 * the hashes are made available. The pull requests are not provided. The provided rescopes may be subsequent rescopes
 * of the same pull request, rescopes for different pull requests or a mix. At the SCM level it's irrelevant.
 * <p>
 * The rescopes are <i>guaranteed</i> to:
 * <ul>
 *     <li>Be non-empty: the {@code oldFromHash} will differ from the {@code newFromHash}, the {@code oldToHash} will
 *         differ from the {@code newToHash}, or both</li>
 *     <li>Never have {@code oldFromHash}, {@code oldToHash} or {@code newToHash} equal to {@code null}. Only
 *         {@code newFromHash} can ever be {@code null}. A {@code null newToHash} would always result in a decline
 *         of the pull request, which the system can detect without the SCM. A {@code null newFromHash} on the other
 *         hand could result in a decline or a merge, depending on whether the from-branch has been merged into the
 *         to-branch before it was deleted.</li>
 * </ul>
 * The SCM is required to analyse each of the rescopes and classify it as producing <i>one</i> of four possible
 * outcomes for the associated pull request. Each outcome has a specific contract which SCMs are expected to honour:
 * <ul>
 *     <li>No analysis result: If the result of applying the rescope can not be determined, the SCM <i>shall not</i>
 *         call any of the callback methods for it</li>
 *     <li>If the {@link PullRequestRescope#getNewFromHash newFromHash} is {@code null}, the SCM <i>shall</i>
 *     <ul>
 *       <li>{@link #merge Merge} the pull request if the SCM allows updating multiple branches in a single push
 *           <i>and</i> the {@link PullRequestRescope#getOldFromHash oldFromHash} is now reachable from the
 *           {@link PullRequestRescope#getNewToHash newToHash}, or</li>
 *       <li>{@link #decline Decline} the pull request if its commits have not been merged to the target</li>
 *     </ul>
 *     </li>
 *     <li>{@link #merge(PullRequestRescope, String) Merge}: If the source or target ref for the pull request has been
 *         updated in such a way that there are no commits on the source that are not also on the target, the SCM
 *         <i>shall</i> classify the rescope as merging the pull request, with 1 exception (detailed below)
 *     <ul>
 *         <li>SCMs which allow rewriting history <i>must</i> detect cases where such actions effectively remove all
 *             of the pending commits from a pull request</li>
 *         <li>For example, force pushing a historical commit to the source ref may result in it referencing the same
 *             commit as the target ref, or one of its ancestors, effectively making the pull request "merged"</li>
 *         <li>If the SCM can detect the commit in which the pull request was merged, the SCM can <i>optionally</i>
 *             provide the merge commit ID to the {@link #merge(PullRequestRescope, String) merge} callback.</li>
 *         <li>If the source ref has been reset to an ancestor of the original target ref, the pull request is
 *             "emptied" without being merged and the pull request <i>may</i> be declined.</li>
 *     </ul>
 *     </li>
 *     <li>{@link #update(PullRequestRescope, RescopeDetails, RescopeDetails) Update}: If new commits have been added
 *         to the {@link MinimalPullRequest#getFromRef() source} or {@link MinimalPullRequest#getToRef() target} refs,
 *         the SCM <i>shall</i> classify the rescope as updating the pull request. The SCM <i>shall</i> also detect
 *         how many commits, if any, have been added to and removed from the scope of the pull request, which is defined
 *         as the commits that are reachable from the source ref, but not the target ref. The SCM <i>shall not</i>
 *         provide more than {@link #getMaxCommitIds()} commit IDs in the {@code addedCommitIds} or
 *         {@code removedCommitIds} lists.
 *     <ul>
 *         <li>There <i>must</i> be at least one commit to be merged, or the pull request <i>shall</i> be marked
 *             as {@link #merge(PullRequestRescope, String) merged} or {@link #decline(PullRequestRescope) declined},
 *             not updated</li>
 *         <li>SCMs which allow rewriting history <i>must</i> detect when commits are <i>removed</i> from a pull
 *             request's source or target refs and update or {@link #merge(PullRequestRescope, String) merge}
 *             or {@link #decline(PullRequestRescope) decline} the pull request accordingly</li>
 *     </ul>
 *     </li>
 * </ul>
 * Each {@link PullRequestRescope rescope} in the context may be classified <i>exactly once</i>. Additionally,
 * <i>only</i> {@link PullRequestRescope rescopes} retrieved from the context may be provided back
 * to it for {@link #decline(PullRequestRescope) declining}, {@link #merge(PullRequestRescope, String) merging} or
 * {@link #update(PullRequestRescope, RescopeDetails, RescopeDetails)}  updating}.
 *
 * @since 4.5
 */
public interface BulkRescopeContext extends Iterable<PullRequestRescope> {

    /**
     * Marks the rescope as {@link PullRequestState#DECLINED declining} the associated pull request.
     *
     * @param rescope the rescope
     * @throws IllegalArgumentException if the provided {@code rescope} was not returned by this context
     */
    void decline(@Nonnull PullRequestRescope rescope);

    /**
     * @return the maximum number of commit IDs to provide in
     *         {@link #update(PullRequestRescope, RescopeDetails, RescopeDetails)}
     */
    int getMaxCommitIds();

    /**
     * Marks the rescope as {@link PullRequestState#MERGED remotely merging} the associated pull request.
     * <p>
     * When a pull request is remotely merged, the SCM can <i>optionally</i> detect and provide the commit which
     * actually carried out the merge. It is only required to detect that the pull request's refs have changed in a
     * way which removes all of the pull request's incoming commits.
     *
     * @param rescope the rescope
     * @throws IllegalArgumentException if the provided {@code rescope} was not returned by this context
     */
    void merge(@Nonnull PullRequestRescope rescope, @Nullable String mergeHash);

    /**
     * Marks the rescope as updating one or both of the associated pull request's
     * {@link PullRequest#getFromRef() from} and {@link PullRequest#getToRef() to} refs.
     * <p>
     * The SCM is required to not only detect that the rescope does not merge or decline the pull request, it is also
     * <i>required</i> to detect how many and which commits were added and/or removed from the scope of the pull
     * request.
     * <p>
     * For performance reasons, only a limited number of commit IDs should be returned. Implementors should call
     * {@link #getMaxCommitIds()} to discover how many commit IDs can be returned.
     *
     * @param rescope the rescope
     * @param addedCommits details about how many and what commits were added to the the pull request
     * @param removedCommits details about how many and what commits were removed from the pull request
     * @throws IllegalArgumentException if the provided {@code rescope} was not returned by this context
     */
    void update(@Nonnull PullRequestRescope rescope, @Nonnull RescopeDetails addedCommits,
                @Nonnull RescopeDetails removedCommits);
}
