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

import com.atlassian.bitbucket.scm.git.GitAgent;
import com.atlassian.bitbucket.scm.git.GitScm;
import com.atlassian.bitbucket.scm.git.GitScmConfig;
import com.atlassian.bitbucket.scm.git.command.GitCommandFactory;
import com.atlassian.event.api.EventListener;
import com.atlassian.bitbucket.event.repository.RepositoryDefaultBranchModifiedEvent;
import com.atlassian.bitbucket.event.repository.RepositoryRefsChangedEvent;
import com.atlassian.bitbucket.repository.NoDefaultBranchException;
import com.atlassian.bitbucket.NoSuchEntityException;
import com.atlassian.bitbucket.ServerException;
import com.atlassian.bitbucket.util.MoreSuppliers;
import com.atlassian.bitbucket.repository.RefChange;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.RawFileCommandParameters;
import com.google.common.collect.Iterables;
import com.google.common.io.Files;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;

/**
 * Adds support for .mailmap author and committer mappings by copying the {@code .mailmap} file from the
 * default branch to the root of the (bare) repository directory. With the .mailmap file in this spot, git will
 * automatically use it to map commiter and authors (if the correct format options are provided).
 * <p>
 * Any existing {@code .mailmap} file will be deleted if no default branch is configured, or no  {@code .mailmap}
 * file is present on the default branch.
 *
 * @since 2.5
 */
public class GitMailmapSupport {

    private static final Logger log = LoggerFactory.getLogger(GitMailmapSupport.class);

    private final GitCommandFactory commandFactory;
    private final GitScmConfig config;
    private final GitAgent gitAgent;

    public GitMailmapSupport(GitCommandFactory commandFactory, GitScmConfig config, GitAgent gitAgent) {
        this.commandFactory = commandFactory;
        this.config = config;
        this.gitAgent = gitAgent;
    }

    @EventListener
    public void onRefsChanged(RepositoryRefsChangedEvent event) {
        if (!GitScm.ID.equals(event.getRepository().getScmId())) {
            return;
        }

        try {
            String defaultBranch = gitAgent.getHead(event.getRepository());
            if (isUpdated(defaultBranch, event.getRefChanges())) {
                updateMailmapFile(event.getRepository(), defaultBranch);
            }
        } catch (NoDefaultBranchException e) {
            // if there's no default branch, there's no mailmap file to maintain. Delete any mailmap file that
            // may be present.
            deleteMailmapIfPresent(event.getRepository());
        }
    }

    @EventListener
    public void onDefaultBranchChanged(RepositoryDefaultBranchModifiedEvent event) {
        if (!GitScm.ID.equals(event.getRepository().getScmId())) {
            return;
        }

        updateMailmapFile(event.getRepository(), event.getNewValue());
    }

    private boolean isUpdated(final String branchId, Iterable<RefChange> changes) {
        return Iterables.any(changes, refChange -> refChange.getRef().getId().equals(branchId));
    }

    private void deleteMailmapIfPresent(Repository repository) {
        File mailmapFile = new File(config.getRepositoryDir(repository), ".mailmap");
        if (mailmapFile.exists() && !mailmapFile.delete()) {
            log.info("Failed to delete .mailmap for {}. Will retry on next push.", repository);
        }
    }

    private void updateMailmapFile(final Repository repository, String defaultBranchId) {
        // Don't simply overwrite any existing .mailmap file. Simply opening the FileOutputStream already
        // overwrites the exiting file with an empty version. If getting the current mailmap fails for any
        // reason, we may want to retain the old version.
        File newMailmapFile = new File(config.getRepositoryDir(repository), ".mailmap.new");

        try (OutputStream outputStream = new FileOutputStream(newMailmapFile)) {
            commandFactory.rawFile(repository, new RawFileCommandParameters.Builder()
                    .commitId(defaultBranchId)
                    .path(".mailmap")
                    .build(), MoreSuppliers.newTypeAwareOutputSupplierOf(outputStream)).call();
        } catch (NoSuchEntityException e) {
            // the repository has no .mailmap file in the default branch, or the configured default branch does
            // not exist
            deleteMailmapIfPresent(repository);
            return;
        } catch (IOException e) {
            // FileNotFoundException will also be caught by this clause
            log.debug("Could not open .mailmap.new for writing in repository {} ({})", repository, e.getMessage());
            return;
        } catch (ServerException e) {
            log.debug("Could not update .mailmap file in " + repository, e);
            return;
        }

        try {
            // only attempt to move the file after we've closed the outputstream
            Files.move(newMailmapFile, new File(config.getRepositoryDir(repository), ".mailmap"));
        } catch (IOException e) {
            log.debug("Could not overwrite the .mailmap file in {} ({})", repository, e.getMessage());
        }
    }
}
