package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilder;
import com.atlassian.bitbucket.mesh.git.exception.AbstractGitException;
import com.atlassian.bitbucket.mesh.git.exception.DetachedHeadException;
import com.atlassian.bitbucket.mesh.git.exception.InvalidRefNameException;
import com.atlassian.bitbucket.mesh.git.exception.NoDefaultBranchException;
import com.atlassian.bitbucket.mesh.git.ref.Branch;
import com.atlassian.bitbucket.mesh.git.ref.Ref;
import com.atlassian.bitbucket.mesh.repository.Repository;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Map;

/**
 * Describes an interface for performing operations against a {@code git} repository. The exact techniques used by
 * implementations are not mandated here beyond stating that they should not invoke {@code git} processes. All tasks
 * which rely on the {@code git} executable should be implemented by adding a type-safe builder interface which is
 * exposed by {@link GitProcessBuilder}.
 */
public interface GitAgent {

    /**
     * The name of the symbolic reference which contains the head of the git repository, also known as the default
     * branch. By default, this is "refs/heads/master". However, it may be updated by either directly updating the
     * file to a new reference, or by executing {@code git symbolic-ref HEAD &lt;new-branch&gt;}.
     */
    String HEAD = "HEAD";
    /**
     * The name of the default remote added when a repository is cloned.
     */
    String ORIGIN = "origin";

    /**
     * Adds an alternate in the specified {@code repositoryDir}, allowing it to "borrow" objects from the provided
     * {@code alternate}.
     * <p>
     * {@code repositoryDir} is <i>required</i> to be a {@link Repository#getPath()} repository directory}.
     * {@code alternate} is <i>required</i> to be an object directory; it cannot point to a repository
     * directory or it will not work.
     *
     * @param repositoryDir the <i>root</i> of the repository to which the alternate should be added
     * @param alternate     the <i>object directory</i> of the repository from which objects should be borrowed
     * @throws AbstractGitException if the contents of the the repository do not appear to follow normal layout rules for
     *                              {@code git}, or if the alternates file cannot be created/replaced
     */
    void addAlternate(@Nonnull Path repositoryDir, @Nonnull String alternate);

    /**
     * Creates a reference with the specified {@code refName} pointing to the specified {@code hash}.
     * <p>
     * Reference names are assumed to be fully-qualified, in git parlance. This allows the creation of references that
     * are not in the standard git locations, but it also means trying to create a reference in a normal location, like
     * refs/heads, requires a qualified name like "refs/heads/new-branch" rather than simply specifying "new-branch".
     * <p>
     * The value provided <i>must</i> be a full 40-byte hash. It cannot be a short hash, nor can it be the name of a
     * reference, such as "master".
     *
     * @param repository the repository in which the new ref should be created
     * @param refName    the fully-qualified ref name
     * @param hash       the hash for the created ref
     */
    void createRef(@Nonnull Repository repository, @Nonnull String refName, @Nonnull String hash);

    /**
     * Creates a symbolic reference from the specified {@code refName} to the specified {@code targetRef}. Both names
     * must be <i>fully-qualified</i>.
     *
     * @param repository the repository in which the new symbolic ref should be created
     * @param refName    the fully-qualified ref name
     * @param targetRef  the fully-qualified name of the target ref
     */
    void createSymbolicRef(@Nonnull Repository repository, @Nonnull String refName, @Nonnull String targetRef);

    /**
     * Deletes the loose reference for {@code refName}, which must be <i>fully-qualified</i>.
     *
     * @param repository the repository in which the new loose ref should be deleted.
     * @param refName    the fully-qualified ref name
     * @return {@code true} if the loose ref was found and deleted
     * @throws com.atlassian.bitbucket.mesh.git.exception.DeleteRefFailedException if the loose ref was found but the
     *                                                                             delete failed.
     */
    boolean deleteLooseRef(@Nonnull Repository repository, @Nonnull String refName);

    /**
     * Enables {@code git reflog} for the specified {@code refName}, if it has not already been enabled. If it has,
     * nothing is done.
     * <p>
     * In a non-bare clone, git enables logging for all refs by default. In a bare clone, it's possible to enable
     * logging the same way, but that is not always desirable. However, git internally automatically checks for the
     * existence of the reflog file and it uses that to determine whether to update it (it uses the enablement flag
     * to tell it to create that file when it creates a new ref, not whether to update the log). That allows logging
     * to be enabled for selective refs simply by "touching" their target log.
     *
     * @param repository the repository in which logging should be enabled for the specified ref
     * @param refName    the fully-qualified ref name
     */
    void enableReflog(@Nonnull Repository repository, @Nonnull String refName);

    /**
     * Retrieves the HEAD reference for the specified repository.
     * <p>
     * Note: The HEAD file is a standard part of git and, even for an empty repository, will never be empty. However,
     * for empty repositories it is possible that the reference pointed to by HEAD may not actually exist. This method
     * <i>does not</i> actually resolve the HEAD reference--it simply returns what it is. If the reference should be
     * resolved, thereby ensuring it actually exists, use {@link #resolveHead(Repository)} instead.
     *
     * @param repository the repository from which the HEAD should be retrieved
     * @return the HEAD for the specified repository
     * @throws DetachedHeadException    Thrown if the specified repository has a HEAD which is not a reference.
     * @throws NoDefaultBranchException Throws if the specified repository has no HEAD.
     */
    @Nonnull
    String getHead(@Nonnull Repository repository) throws DetachedHeadException, NoDefaultBranchException;

    /**
     * Examines the specified repository to determine if it contains any packs or loose objects, regardless of whether
     * the repository has any refs. A repository can have refs but no objects if all of its objects is located in an
     * alternate (e.g. shared-objects for managed repositories), or if the repository is being constructed through
     * migration.
     *
     * @param repository the repository to check
     * @return {@code true} if the provided repository has any packs or loose objects; otherwise, {@code false}
     */
    boolean hasObjects(@Nonnull Repository repository);

    /**
     * Examines the specified repository to determine if it is empty. A repository is considered to be empty if it has
     * no branches or tags, with the assumption being that any objects pushed to a repository must have a reference
     * pointing to them.
     *
     * @param repository the repository to check for emptiness
     * @return {@code true} if the provided repository has no branches or tags; otherwise, {@code false}
     */
    boolean isEmpty(@Nonnull Repository repository);

    /**
     * Resolves the {@link Branch} within the specified repository identified by the provided name. It is assumed that
     * the provided name is <i>not</i> absolute.
     *
     * @param repository the repository to resolve the branch in
     * @param refName    the name of the branch to resolve
     * @return the resolved branch, or {@code null} if no such branch exists
     * @see #resolveBranch(Repository, String, boolean)
     */
    @Nullable
    Branch resolveBranch(@Nonnull Repository repository, @Nonnull String refName);

    /**
     * Resolves the {@link Branch} within the specified repository identified by the provided name, optionally applying
     * git precedence rules if the provided name is not absolute.
     * <p>
     * An example of an absolute name is "refs/heads/master". Absolute names must be matched exactly how they are; the
     * rules of ref precedence in git will not be applied. When dealing with non-absolute names, such as "master", git
     * applies a standard precedence in searching for the matching reference. Those same rules will be applied here.
     *
     * @param repository the repository to resolve the branch in
     * @param refName    the name of the branch to resolve
     * @param absolute   {@code true} if the ref name is already absolute, meaning there is no need to apply precedence
     *                   rules; otherwise, {@code false} to apply standard precedence rules
     * @return the resolved branch, or {@code null} if no such branch exists
     * @throws InvalidRefNameException thrown if the specified {@code refName} contains characters that are illegal for
     *                                 the path
     */
    @Nullable
    Branch resolveBranch(@Nonnull Repository repository, @Nonnull String refName, boolean absolute);

    /**
     * Resolves the {@link Branch} denoted by the {@link #getHead(Repository) HEAD} of the specified repository.
     * <p>
     * Note: For a repository which has had no commits, or which has had only commits targeting branches other than the
     * HEAD branch, attempting to resolve the HEAD will throw a {@link NoDefaultBranchException}.
     *
     * @param repository the repository to resolve the HEAD reference for
     * @return the resolved branch
     * @throws DetachedHeadException    Thrown if the specified repository has a HEAD which is not a reference.
     * @throws NoDefaultBranchException Thrown if the configured default branch for the specified repository does not
     *                                  exist. This may happen in {@link #isEmpty(Repository) empty} repositories, or
     *                                  repositories which do not use standard git layout (meaning they have no master
     *                                  branch).
     */
    @Nonnull
    Branch resolveHead(@Nonnull Repository repository) throws DetachedHeadException, NoDefaultBranchException;

    /**
     * Performs <i>basic</i> revision parsing on the provided value. If {@code value} isn't a ref, similar to the
     * behavior of {@code git rev-parse}, it is returned as-is.
     * <p>
     * Unlike {@link #resolveBranch} or {@link #resolveHead}, which attempt to return a full {@link Branch}, or
     * {@link #getHead}, which will return a ref ID rather than "peeling" to a commit, this is more akin to a
     * symbolic ref-aware read. If the provided {@code value} is a symbolic ref, it will be "peeled". The first
     * non-symbolic value is returned <i>without validation</i>.
     * <p>
     * One possible use case for this is to parse a detached {@code HEAD}. Where both {@link #getHead} and
     * {@link #resolveHead} would throw, this method will return whatever value is present. If {@code HEAD} is
     * <i>not</i> detached, it will be "peeled" to find the current value of its target.
     *
     * @param repositoryDir the <i>root</i> of the repository
     * @param value         the value to parse, which may be a ref name
     * @return the provided {@code value}, if it can't be found as a ref, or the <i>peeled target</i> of the
     *         the specified ref
     */
    @Nonnull
    String revParse(@Nonnull Path repositoryDir, @Nonnull String value);

    /**
     * Sets the HEAD reference for the specified {@code repositoryDir}.
     * <p>
     * Note: The HEAD file is a standard part of git and, even for an empty repository, will never be empty. However,
     * for empty repositories it is possible that the reference pointed to by HEAD may not actually exist. This method
     * <i>does not</i> actually resolve the HEAD reference--it simply sets it to the provided value..
     *
     * @param repositoryDir the <i>root</i> of the repository in which the HEAD should be set
     * @param refId         the fully qualified ref ID to set the HEAD reference to
     */
    void setHead(@Nonnull Path repositoryDir, @Nonnull String refId);

    /**
     * Resolves the {@link Ref}s within the specified repository identified by the provided refIds. Where
     * {@link #resolveBranch(Repository, String)} only resolves branches, this method will also resolve tags. However,
     * it will <em>not</em> 'peel' the tags. Any tags that are returned, are returned as simple {@link Ref} objects,
     * where {@link Ref#getLatestCommit()} refers to the tag <em>object</em>, and <em>not</em> the commit it refers to.
     *
     * @param repository the repository to resolve the refs in
     * @param refIds     the IDs to the refs to resolve
     * @param absolute   whether the provided refIds are absolute
     * @return a {@link Map} of refId to resolved {@link Ref}
     */
    @Nonnull
    Map<String, Ref> shallowResolveRefs(@Nonnull Repository repository, @Nonnull Collection<String> refIds,
                                        boolean absolute);
}
