package com.atlassian.stash.internal.scm.git.command;

import com.atlassian.bitbucket.scm.git.command.clone.GitCloneBuilder;
import com.atlassian.bitbucket.scm.CommandFailedException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.bitbucket.scm.git.command.GitScmCommandBuilder;
import com.atlassian.bitbucket.scm.git.GitScmConfig;
import com.atlassian.bitbucket.scm.git.command.config.GitConfig;
import com.atlassian.bitbucket.util.FileUtils;
import com.atlassian.stash.internal.scm.git.InternalGitConstants;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ExecutorService;

/**
 * Uses {@link GitCloneBuilder git clone} to create a fork of an existing repository.
 * <p>
 * A unidirectional alternate is setup between the fork and its parent. This allows the parent's objects to be used by
 * the fork, substantially reducing the disk usage for the fork. Since alternates are transitive, that one-way link to
 * the parent will eventually link back through any number of grandparents to the root of the hierarchy.
 * <p>
 * Because it is unknown which repositories in an overall fork hierarchy still have references reaching a given object,
 * pruning during garbage collection must be disabled. It is disabled both for the parent repository <i>and</i> for the
 * new fork. Garbage collection itself is not disabled, so the repositories will still repack periodically, but objects
 * will never be deleted, even if a given repository believes they are unreachable.
 *
 * @since 2.4
 */
public class SharedForkCommand extends AbstractForkCommand {

    /**
     * Defines the maximum number of times repositories can be recursively forked.
     * <p>
     * {@code git} internally has a recursion limit of 5, when processing alternates, which forms the basis for this
     * limit. <i>However, due to how Git's limit is applied, the functional limit there is 6.</i> The limit here is
     * <i>intentionally</i> one less than Git's limit to account for the temporary clone which is used to merge pull
     * requests. That clone uses alternates back to the pull request's repository (or repositories). As a result, if
     * the limit here matched {@code git}'s limit, pull requests created between repositories at the limit would not
     * be mergeable--they would be one alternate too deep.
     *
     * @see #calculateDepth()
     */
    public static final int MAX_DEPTH = 5;

    /**
     * Used to reconfigure the fork's parent, not the fork itself, to disable pruning objects during GC. Due to the
     * use of alternates, repositories which have been forked no longer know which objects are or are not in use.
     */
    private static final Map<String, String> ORIGIN_CONFIG = ImmutableMap.<String, String>builder()
            .put(GitConfig.GC_AUTO, InternalGitConstants.CONFIG_GC_AUTO_DISABLED)
            .put(GitConfig.GC_PRUNE_EXPIRE, InternalGitConstants.CONFIG_GC_PRUNE_EXPIRE_NEVER)
            .build();
    /**
     * Defines the path <i>below the {@code objects} directory</i> to the alternates file, within a repository.
     */
    private static final String[] PATH_ALTERNATES = {"info", "alternates"};

    private boolean disablePrune;

    public SharedForkCommand(@Nonnull ExecutorService executorService, @Nonnull GitCommandBuilderFactory builderFactory,
                             @Nonnull GitScmConfig config, @Nonnull I18nService i18nService,
                             @Nonnull Repository repository, @Nonnull Repository parent) {
        super(executorService, builderFactory, config, i18nService, repository, parent);

        disablePrune = true;
    }

    @Override
    protected void configureRepository() {
        super.configureRepository();

        if (disablePrune) {
            //Apply additional configuration to parent to disable pruning unreachable objects. This is necessitated by
            //the use of alternates; parent repositories don't know which of their objects forks are borrowing, so they
            //cannot remove any objects. Note that the _fork_ still garbage collects like normal.
            applyConfiguration(parent, SharedForkCommand.ORIGIN_CONFIG);
        }
    }

    @Override
    protected void createRepository(GitScmCommandBuilder builder) {
        //Before running git clone, ensure the new fork will not exceed the maximum depth for forks
        checkDepth();

        //If the fork is retried, it means we're using Cygwin. That means an alternate was not added, due to how
        //Cygwin paths are formatted, so there is no reason to disable pruning, because the fork doesn't depend on
        //its parent's objects
        disablePrune = fork(builder, true); //Use a shared clone, to create an alternate to the origin in the fork
    }

    /**
     * Walks recursive alternates to calculate the <i>current</i> fork depth. {@code git} has an internal <i>hard-
     * coded</i> limit of walking no more than 5 layers of alternates. That means objects that are only available
     * in repositories above that depth will not be reachable from the fork, resulting in the creation of a "broken"
     * fork.
     * <p>
     * <i>Direct</i> alternates (those listed within the fork repository) are "free". The parent {@link Repository}
     * will be a direct alternate, so the initial depth here is considered to be 0, not 1. This means forks of a
     * repository with no {@link Repository#getOrigin() origin} (a "root" repository) will have a calculated depth
     * of 0. The first fork of a fork will have depth 1, a fork of a fork of a fork will have depth 2, and so on.
     * <p>
     * For more details, see {@code sha1_file.c} in Git. {@code link_alt_odb_entries} applies the recursion limit.
     * Due to how the functions call each other, the maximum <i>depth</i> ends up being 7 object stores:
     * <ol>
     *     <li>"My" object store has no calculated depth</li>
     *     <li>Parent's object store has a depth of 0</li>
     *     <li>Grandparent's object store has a depth of 1</li>
     *     <li>Great grandparent's object store has a depth of 2</li>
     *     <li>Great great grandparent's object store has a depth of 3</li>
     *     <li>Great great great grandparent's object store has a depth of 4</li>
     *     <li>Great great great great grandparent's object store has a depth of 5</li>
     * </ol>
     * Git's processing blocks strictly greater than the limit, so alternates <i>at</i> 5 deep <i>are</i> valid.
     *
     * @return the calculated fork depth
     */
    private int calculateDepth() {
        int depth = 0;

        File alternates = FileUtils.construct(config.getObjectsDir(parent), PATH_ALTERNATES);
        while (alternates.isFile()) {
            //If an alternate exists, it points to the objects directory for a "parent". This may _not_ align with
            //origins in the database, so those are ignored here. Instead, this will traverse up all the alternates
            //to compute the _real_ depth.
            ++depth;

            try {
                //Alternates point to object directories, so they can be used as the "base" part of a construct
                //call to assemble a path to their recursive alternates, if present.
                String first = Files.readFirstLine(alternates, Charsets.UTF_8);
                if (StringUtils.isBlank(first)) {
                    //The system should never trigger the creation of empty alternates files, but just in case a
                    //customer ends up with one, if the first line is blank (empty or null), break out
                    break;
                }

                alternates = FileUtils.construct(new File(first), PATH_ALTERNATES);
            } catch (IOException e) {
                throw newCommandFailedException(e);
            }
        }

        return depth;
    }

    /**
     * Ensures the depth of the would-be fork does not exceed the {@link #MAX_DEPTH maximum}.
     *
     * @throws CommandFailedException if the fork would exceed the {@link #MAX_DEPTH maximum} depth
     * @see #calculateDepth()
     */
    private void checkDepth() {
        //Git's application of the recursion limit is strictly greater than, meaning the limit itself is a valid
        //depth. As a result, we apply less-than-or-equal to our limit. This will leave one more layer of forks
        //before hitting Git's hard-coded limit, allowing room for the temporary clone used to merge pull requests.
        int depth = calculateDepth();
        if (depth < MAX_DEPTH) {
            return;
        }

        //To ensure we don't trigger recursive alternates failures in git, this fork cannot be created. Depending
        //on whether the proposed parent has a database-visible origin, construct different messages.
        Repository grandparent = parent.getOrigin();
        if (grandparent == null) {
            //This is going to be hard to explain, because, to the user, it's not going to look like they're as
            //deep in the hierarchy as the system is telling them they are. This is fallout from the fact that,
            //sometimes, "deleted" repositories are not actually removed on disk.
            throw new CommandFailedException(i18nService.createKeyedMessage("bitbucket.git.fork.toodeep.hiddenparents",
                    parent.getProject().getKey(), parent.getSlug(), MAX_DEPTH));
        } else {
            //Otherwise, if the proposed parent has a database-visible origin, suggest that the user fork from
            //that repository instead. Note that this does _not_ take permissions into consideration, so it's
            //possible the current user can't fork any higher.
            throw new CommandFailedException(i18nService.createKeyedMessage("bitbucket.git.fork.toodeep.useparent",
                    parent.getProject().getKey(), parent.getSlug(), MAX_DEPTH,
                    grandparent.getProject().getKey(), grandparent.getSlug()));
        }
    }
}
