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

import com.atlassian.bitbucket.scm.Command;
import com.atlassian.bitbucket.scm.CommandFailedException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.stash.internal.scm.git.GitHookUtils;
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.scm.git.command.config.GitConfigSetBuilder;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * A base class for constructing {@link Command commands} which create repositories on disk.
 */
public abstract class AbstractCreateCommand extends SimpleGitCommand<Void> {

    public static final Map<String, String> CORE_CONFIGURATION = ImmutableMap.<String, String>builder()
            .put(GitConfig.HTTP_RECEIVE_PACK, "true")
            .put(GitConfig.CORE_PACKED_GIT_LIMIT, "256m")
            .put(GitConfig.CORE_PACKED_GIT_WINDOW_SIZE, "32m")
            .build();
    public static final Map<String, String> GC_CONFIGURATION = ImmutableMap.<String, String>builder()
            .put(GitConfig.GC_PR_REFS_EXPIRE, "never")
            .put(GitConfig.GC_PR_REFS_EXPIRE_UNREACHABLE, "never")
            .build();
    public static final Map<String, String> DEFAULT_CONFIGURATION = ImmutableMap.<String, String>builder()
            .putAll(CORE_CONFIGURATION)
            .putAll(GC_CONFIGURATION)
            .build();

    protected final GitScmConfig config;
    protected final I18nService i18nService;
    protected final Logger log;
    protected final Repository repository;

    private final GitCommandBuilderFactory builderFactory;

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

        this.builderFactory = checkNotNull(builderFactory, "builderFactory");
        this.config = checkNotNull(config, "config");
        this.i18nService = checkNotNull(i18nService, "i18nService");
        this.repository = checkNotNull(repository, "repository");

        log = LoggerFactory.getLogger(getClass());
    }

    @Override
    public Void call() {
        String name = repository.getProject().getKey() + "/" + repository.getSlug() + " (" + repository.getId() + ")";

        boolean delete = false;
        try {
            // create the directory
            log.debug("Creating repository for {}", name);
            createRepository(builderFactory.builder());
            delete = true;

            // set config options
            log.debug("Configuring repository for {}", name);
            configureRepository();

            // install hook scripts
            log.debug("Configuring hooks for {}", name);
            configureHooks();

            log.info("Repository {} has been created and configured successfully", name);
            delete = false;
        } finally {
            if (delete) {
                try {
                    log.debug("Cleaning up partially-created repository for {}", name);

                    FileUtils.deleteDirectory(config.getRepositoryDir(repository));
                } catch (Exception e) {
                    log.error("Could not delete partially-created repository " + name, e);
                }
            }
        }

        return null;
    }

    protected void applyConfiguration(Repository repository, Map<String, String> configuration) {
        log.debug("Applying {} configuration setting(s) to {}/{} ({})",
                configuration.size(), repository.getProject().getKey(), repository.getSlug(), repository.getId());

        GitConfig configBuilder = builderFactory.builder(repository).config();
        for (Map.Entry<String, String> entry : configuration.entrySet()) {
            log.trace("Configuring {} -> {}", entry.getKey(), entry.getValue());

            configBuilder.set(entry.getKey(), entry.getValue()).build().call();
        }
    }

    protected GitScmCommandBuilder builder() {
        return builderFactory.builder(repository);
    }

    protected void configureHooks() {
        try {
            File repositoryDir = config.getRepositoryDir(repository);
            GitHookUtils.prepareHookScripts(repositoryDir);
            GitHookUtils.installHookScripts(repositoryDir);
        } catch (IOException e) {
            log.error(String.format("Could not install hook scripts for repository %1$s/%2$s (%3$d)",
                    repository.getProject().getKey(), repository.getSlug(), repository.getId()), e);

            throw newCommandFailedException(e);
        }
    }

    /**
     * Applies configuration settings to the created repository. The default implementation provided here calls
     * {@link #getConfiguration()} to get a map of configuration settings to apply, then uses {@link GitConfigSetBuilder}
     * to apply them.
     */
    protected void configureRepository() {
        ImmutableMap<String, String> configuration = ImmutableMap.<String, String>builder()
                .putAll(getConfiguration())
                .put(GitConfig.HIERARCHY_ID, repository.getHierarchyId())
                .build();

        applyConfiguration(repository, configuration);
    }

    /**
     * When implemented in a derived class, uses whatever mechanism is appropriate in order to create the repository
     * on disk and initialise it.
     */
    protected abstract void createRepository(GitScmCommandBuilder builder);

    /**
     * Retrieves a map of configuration settings to be {@link #configureRepository() applied} when a repository is
     * created. The iteration order of the returned map controls the order in which the settings are applied.
     *
     * @return a map of configuration settings to apply
     */
    protected Map<String, String> getConfiguration() {
        return DEFAULT_CONFIGURATION;
    }

    protected abstract CommandFailedException newCommandFailedException(Throwable cause);
}
