package com.atlassian.bitbucket.mesh.git.pull;

import com.atlassian.bitbucket.mesh.git.GitConstants;
import com.atlassian.bitbucket.mesh.repository.BitbucketServerRepository;
import com.atlassian.bitbucket.mesh.repository.Repository;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.Nonnull;
import java.nio.file.Path;
import java.util.stream.Stream;

import static com.atlassian.bitbucket.mesh.git.GitConstants.*;
import static java.util.Objects.requireNonNull;

/**
 * A reusable builder for crafting pull request refs, either dark or public. The builder allows the various pieces,
 * like the pull request ID and the {@link PullRequestRefType type}, to be assembled (and replaced), and then converted
 * <i>either</i> into a {@link #toRef() fully-qualified ref}, or to a {@link #toPath(Path) path} as needed.
 */
public class PullRequestRefBuilder {

    private static final String[] PATH_BBS_DARK_REFLOGS = {PATH_LOGS, PATH_STASH_REFS, PATH_PULL_REQUESTS};
    private static final String[] PATH_BBS_DARK_REFS = {PATH_STASH_REFS, PATH_PULL_REQUESTS};
    private static final String[] PATH_DARK_REFLOGS = {PATH_LOGS, GitConstants.PATH_DARK_REFS, PATH_PULL_REQUESTS};
    private static final String[] PATH_DARK_REFS = {GitConstants.PATH_DARK_REFS, PATH_PULL_REQUESTS};
    private static final String[] PATH_PUBLIC_REFLOGS = {PATH_LOGS, PATH_REFS, PATH_PULL_REQUESTS};
    private static final String[] PATH_PUBLIC_REFS = {PATH_REFS, PATH_PULL_REQUESTS};

    private final String[] prefix;

    private Long id;
    private String name;

    private PullRequestRefBuilder(String[] prefix) {
        this.prefix = prefix;
    }

    /**
     * Constructs a new builder for creating dark refs for {@link BitbucketServerRepository classic Bitbucket Server
     * repositories}, used for pull request auto-merges. These refs will not be included in ref advertisements;
     * they will only be available locally.
     *
     * @return a builder for constructing dark refs
     */
    @Nonnull
    public static PullRequestRefBuilder bitbucketServerDarkRef() {
        return new PullRequestRefBuilder(PATH_BBS_DARK_REFS);
    }

    /**
     * Constructs a new builder for creating dark refs, used for pull request auto-merges. These refs will not be
     * included in ref advertisements; they will only be available locally.
     *
     * @return a builder for constructing dark refs
     */
    @Nonnull
    public static PullRequestRefBuilder darkRef(@Nonnull Repository repository) {
        return isBitbucketServer(repository) ? bitbucketServerDarkRef() : darkRef();
    }

    /**
     * Constructs a new builder for creating dark refs, used for pull request auto-merges. These refs will not be
     * included in ref advertisements; they will only be available locally.
     *
     * @return a builder for constructing dark refs
     */
    @Nonnull
    public static PullRequestRefBuilder darkRef() {
        return new PullRequestRefBuilder(PATH_DARK_REFS);
    }

    /**
     * Constructs a new builder for managing reflogs for dark refs, which are used to protect commits previously
     * involved in the pull request.
     *
     * @return a builder for interacting with dark reflogs
     */
    @Nonnull
    public static PullRequestRefBuilder darkReflogs(@Nonnull Repository repository) {
        return new PullRequestRefBuilder(isBitbucketServer(repository) ? PATH_BBS_DARK_REFLOGS : PATH_DARK_REFLOGS);
    }

    /**
     * Constructs a new builder for creating public refs, which will be included in ref advertisements, to allow pull
     * request commits to be fetched.
     *
     * @return a builder for constructing public refs
     */
    @Nonnull
    public static PullRequestRefBuilder publicRef() {
        return new PullRequestRefBuilder(PATH_PUBLIC_REFS);
    }

    /**
     * Constructs a new builder for managing reflogs for public refs. These reflogs were only created by legacy
     * versions of the system, prior to Bitbucket Server 2.9.0. However, a builder is provided for managing them
     * anyway, because long-lived repositories may have been upgraded from such legacy versions.
     *
     * @return a builder for interacting with public reflogs
     */
    @Nonnull
    public static PullRequestRefBuilder publicReflogs() {
        return new PullRequestRefBuilder(PATH_PUBLIC_REFLOGS);
    }

    /**
     * Streams builders for all of the refs associated with the specified {@link PullRequest}.
     * <p>
     * Builders are streamed in the following order:
     * <ol>
     *     <li>{@link #publicRef()}</li>
     *     <li>{@link #darkRef(Repository)}</li>
     *     <li>{@link #publicReflogs()}</li>
     *     <li>{@link #darkReflogs(Repository)}</li>
     * </ol>
     *
     * @param pullRequest the pull request to stream builders for
     * @return a stream of builders for <i>all</i> of the refs associated with the specified {@link PullRequest}
     */
    @Nonnull
    public static Stream<PullRequestRefBuilder> stream(@Nonnull PullRequest pullRequest) {
        Repository repository = pullRequest.getRepository();
        return Stream.of(publicRef(), darkRef(repository), publicReflogs(), darkReflogs(repository))
                .map(builder -> builder.id(pullRequest));
    }

    @Nonnull
    public PullRequestRefBuilder id(@Nonnull PullRequest pullRequest) {
        return id(requireNonNull(pullRequest, "pullRequest").getId());
    }

    @Nonnull
    public PullRequestRefBuilder id(long value) {
        id = value;

        return this;
    }

    @Nonnull
    public PullRequestRefBuilder name(@Nonnull String value) {
        name = requireNonNull(value, "name");

        return this;
    }

    @Nonnull
    public String[] toArray() {
        if (id == null) {
            //name is ignored if the id isn't set, so just return the prefix
            return prefix;
        }

        String[] path = new String[prefix.length + (name == null ? 1 : 2)]; //+1 for id, maybe +1 more for name
        System.arraycopy(prefix, 0, path, 0, prefix.length);

        path[prefix.length] = String.valueOf(id);
        if (name != null) {
            path[prefix.length + 1] = name;
        }

        return path;
    }

    /**
     * Builds a {@code Path} which can be used to access the ref on disk.
     *
     * @param repositoryDir the repository directory, used as the prefix for the returned {@code Path}
     * @return a path for the pull request ref
     */
    @Nonnull
    public Path toPath(@Nonnull Path repositoryDir) {
        return requireNonNull(repositoryDir, "repositoryDir").resolve(toRef());
    }

    /**
     * Builds a <i>fully-qualified</i> ref from the provided values.
     *
     * @return a fully-qualified pull request ref
     */
    @Nonnull
    public String toRef() {
        return StringUtils.join(toArray(), "/");
    }

    /**
     * @return {@link #toRef()}
     */
    @Override
    public String toString() {
        return toRef();
    }

    @Nonnull
    public PullRequestRefBuilder type(@Nonnull PullRequestRefType value) {
        return name(requireNonNull(value, "type").getRefName());
    }

    /**
     * Appends the pull request's version as the {@link #name(String) name}. The pull request's {@link #id(long) ID}
     * must be appended separately.
     *
     * @param version the version
     * @return {@code this}
     */
    @Nonnull
    public PullRequestRefBuilder version(int version) {
        return name("v" + version);
    }

    private static boolean isBitbucketServer(Repository repository) {
        return repository instanceof BitbucketServerRepository;
    }
}
