package com.atlassian.bitbucket.event.pull;

import com.atlassian.bitbucket.commit.MinimalCommit;
import com.atlassian.bitbucket.event.repository.RepositoryRefsChangedEvent;
import com.atlassian.bitbucket.pull.PullRequest;
import com.atlassian.bitbucket.pull.PullRequestAction;
import com.atlassian.bitbucket.repository.RefChange;
import com.atlassian.bitbucket.repository.RefChangeType;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.SimpleRefChange;
import com.atlassian.event.api.AsynchronousPreferred;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;

import static java.util.Objects.requireNonNull;

/**
 * Event raised when a pull request is merged via the web UI or REST, or when a remote merge is detected.
 * <p>
 * If the pull request was merged by the system, the {@link #getCommit() commit} resulting from the merge (which
 * may or may not be a literal merge commit, depending on the configured merge strategy) will be included. Otherwise,
 * if the pull request was {@link #isMergedRemotely() merged remotely}, meaning a commit which is a descendant of the
 * {@link PullRequest#getFromRef() from ref} was pushed to the {@link PullRequest#getToRef() to ref}, the commit
 * <i>will not be provided</i>.
 * <p>
 * Listeners that are interested in all repository changes (and not just pull request merges) should listen for
 * {@link RepositoryRefsChangedEvent}s.
 */
@AsynchronousPreferred
public class PullRequestMergedEvent extends PullRequestEvent implements RepositoryRefsChangedEvent {

    private final MinimalCommit commit;
    private final Map<String, Object> context;
    private final String message;
    private final Collection<RefChange> refChanges;
    private final String strategyId;

    /**
     * Constructs a new {@code PullRequestMergedEvent}. This is a convenience constructor for remotely-merged pull
     * requests, where the {@link MinimalCommit merge commit} is not known.
     *
     * @param source      the entity raising the event
     * @param pullRequest the pull request that was merged
     */
    public PullRequestMergedEvent(@Nonnull Object source, @Nonnull PullRequest pullRequest) {
        this(source, pullRequest, null, null, null, Collections.emptyMap());
    }

    /**
     * Constructs a new {@code PullRequestMergedEvent}. The {@link MinimalCommit merge commit} should be provided if
     * the pull request was merged by the system, and should be left {@code null} if the merge was pushed by a user.
     *
     * @param source      the entity raising the event
     * @param pullRequest the pull request that was merged
     * @param commit      the commit that resulted from the merging the pull request via the web UI or REST, which may
     *                    by {@code null} if the pull request was merged remotely and pushed up
     * @param message     the commit message, which may be {@code null}
     * @param context     additional context provided when merging the pull request, intended to allow plugins to
     *                    add their own functionality around the merge operation
     */
    public PullRequestMergedEvent(@Nonnull Object source, @Nonnull PullRequest pullRequest,
                                  @Nullable MinimalCommit commit, @Nullable String message,
                                  @Nonnull Map<String, Object> context) {
        this(source, pullRequest, commit, message, null, context);
    }

    /**
     * Constructs a new {@code PullRequestMergedEvent}. The {@link MinimalCommit merge commit} should be provided if
     * the pull request was merged by the system, and should be left {@code null} if the merge was pushed by a user.
     *
     * @param source      the entity raising the event
     * @param pullRequest the pull request that was merged
     * @param commit      the commit that resulted from the merging the pull request via the web UI or REST, which may
     *                    by {@code null} if the pull request was merged remotely and pushed up
     * @param message     the commit message, which may be {@code null}
     * @param strategyId  the strategy used to merge the pull request
     * @param context     additional context provided when merging the pull request, intended to allow plugins to
     *                    add their own functionality around the merge operation
     * @since 4.9
     */
    public PullRequestMergedEvent(@Nonnull Object source, @Nonnull PullRequest pullRequest,
                                  @Nullable MinimalCommit commit, @Nullable String message,
                                  @Nullable String strategyId, @Nonnull Map<String, Object> context) {
        super(source, pullRequest, PullRequestAction.MERGED);

        this.commit = commit;
        this.context = ImmutableMap.copyOf(requireNonNull(context, "context"));
        this.message = message;
        this.strategyId = strategyId;

        //Note: the PR may have been merged remotely, these cases can be detected by checking for commit == null
        refChanges = commit == null ?
                Collections.emptyList() :
                Collections.singleton(new SimpleRefChange.Builder()
                        .from(pullRequest.getToRef())
                        .toHash(commit.getId())
                        .type(RefChangeType.UPDATE)
                        .build());
    }

    /**
     * @return the merge commit, which may be {@code null} if the pull request was merged remotely
     */
    @Nullable
    public MinimalCommit getCommit() {
        return commit;
    }

    /**
     * @return additional context provided when the merge was requested
     */
    @Nonnull
    public Map<String, Object> getContext() {
        return context;
    }

    /**
     * @return the proposed commit message, which may be {@code null}
     */
    @Nullable
    public String getMessage() {
        return message;
    }

    /**
     * @return a collection of {@link RefChange updated refs} which may be empty, if the pull request was merged
     *         remotely, or contain a single updated ref for the pull request's {@link PullRequest#getToRef target
     *         branch}, if the pull request was merged via the web UI or REST
     */
    @Nonnull
    @Override
    public Collection<RefChange> getRefChanges() {
        return refChanges;
    }

    /**
     * @return the repository <i>into which</i> the pull request was merged
     */
    @Nonnull
    @Override
    public Repository getRepository() {
        return getPullRequest().getToRef().getRepository();
    }

    /**
     * @return the ID of the strategy used to merge the pull request
     * @since 4.9
     */
    @Nullable
    public String getStrategyId() {
        return strategyId;
    }

    /**
     * @return {@code true} if the pull request was merged remotely and pushed up; otherwise, {@code false}
     *         if the pull request was merged via the web UI or REST
     */
    public boolean isMergedRemotely() {
        return commit == null;
    }
}
