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

import com.atlassian.bitbucket.mesh.annotation.Profiled;
import com.atlassian.bitbucket.mesh.config.ProcessConfig;
import com.atlassian.bitbucket.mesh.event.EventPublisher;
import com.atlassian.bitbucket.mesh.git.GitAgent;
import com.atlassian.bitbucket.mesh.git.GitConstants;
import com.atlassian.bitbucket.mesh.git.auth.GitAuthenticationHelper;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.builder.fetch.FetchStdioHandler;
import com.atlassian.bitbucket.mesh.git.builder.fetch.GitFetchBuilder;
import com.atlassian.bitbucket.mesh.git.builder.foreachref.AllRefsForEachRefStdioHandler;
import com.atlassian.bitbucket.mesh.git.ref.Ref;
import com.atlassian.bitbucket.mesh.process.ProcessSummary;
import com.atlassian.bitbucket.mesh.repository.RefsUpdatedEvent;
import com.atlassian.bitbucket.mesh.repository.Repository;

import javax.annotation.Nonnull;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

@Profiled
public class DefaultFetchManager implements FetchManager {

    private final GitAgent agent;
    private final GitAuthenticationHelper authenticationHelper;
    private final GitProcessBuilderFactory builderFactory;
    private final ProcessConfig config;
    private final EventPublisher eventPublisher;

    public DefaultFetchManager(GitAgent agent, GitAuthenticationHelper authenticationHelper,
                               GitProcessBuilderFactory builderFactory, ProcessConfig config,
                               EventPublisher eventPublisher) {
        this.agent = agent;
        this.authenticationHelper = authenticationHelper;
        this.builderFactory = builderFactory;
        this.config = config;
        this.eventPublisher = eventPublisher;
    }

    @Override
    public void fetch(@Nonnull FetchParameters parameters, @Nonnull FetchCallback callback) {
        Repository repository = parameters.getRepository();
        FetchStdioHandler handler = new FetchStdioHandler(agent, callback, getCurrentRefs(repository),
                parameters.getSourceUrl(), repository);

        GitFetchBuilder builder = builderFactory.builder(parameters.getRepository())
                .fetch()
                .atomic(parameters.isAtomic())
                .author(parameters.getAuthor().getName(), parameters.getAuthor().getEmail())
                .force(true)
                .prune(parameters.isPrune())
                .progress(true)
                .repository(parameters.getSourceUrl())
                .tags(parameters.getTagMode())
                .withConfiguration(GitConstants.CONFIG_CORE_ABBREV, "40") // try to get full hashes for updated refs
                .withConfiguration(GitConstants.CONFIG_GC_AUTO, "0") // don't trigger gc when fetching to prevent timeouts
                .withConfiguration(GitConstants.CONFIG_PROTOCOL_VERSION, "0") // force protocol v0 for consistency
                .withExecutionInterval(Duration.ofSeconds(parameters.getExecutionTimeout()))
                .withIdleInterval(Duration.ofSeconds(parameters.getIdleTimeout()));
        parameters.getEnvironment().forEach(builder::withEnvironment);

        // Configure git to use the provided credentials when authentication is required
        builder = authenticationHelper.configure(builder, parameters.getSourceUrl(), parameters.getCredentials());
        int maxCommandLineLength = config.getCommandLineSize() - 250; //250 character buffer for other arguments

        ProcessSummary summary = null;
        RuntimeException exception = null;
        try {
            callback.onStart();
            for (List<String> refspecs : partitionByMaxCombinedLength(parameters.getRefspecs(), maxCommandLineLength)) {
                summary = builder.clearRefspecs()
                        .refspecs(refspecs)
                        .build(handler)
                        .run();
            }
        } catch (RuntimeException e) {
            exception = e;
        } finally {
            if (handler.isAnyRefUpdated()) {
                eventPublisher.publish(new RefsUpdatedEvent(parameters.getRepository()));
            }
            if (exception != null) {
                eventPublisher.publish(new FetchFailedEvent(parameters.getRepository()));
            }
            fireOnEnd(callback, summary, exception);
        }
    }

    private static void fireOnEnd(@Nonnull FetchCallback callback, ProcessSummary summary, Throwable thrown) {
        if (summary == null) {
            callback.onEnd(new ProcessSummary.Builder("git fetch").thrown(thrown).build());
        } else {
            callback.onEnd(new ProcessSummary.Builder(summary).thrown(thrown).build());
        }
    }

    private static List<List<String>> partitionByMaxCombinedLength(List<String> values, int maxLength) {
        List<List<String>> partitions = new ArrayList<>();
        int start = 0;
        int index = 0;
        int partitionLength = 0;
        for (String value : values) {
            int length = value.length();
            if (partitionLength + length > maxLength && start != index) {
                // exceeded the max and we have at least one item in the partition
                partitions.add(values.subList(start, index));
                start = index;
                partitionLength = 0;
            }
            partitionLength += length;
            ++index;
        }
        if (start < values.size()) {
            partitions.add(values.subList(start, values.size()));
        }
        return partitions;
    }

    private List<Ref> getCurrentRefs(Repository repository) {
        List<Ref> refs = new ArrayList<>();
        builderFactory.builder(repository).forEachRef()
                .format(AllRefsForEachRefStdioHandler.FORMAT)
                .build(new AllRefsForEachRefStdioHandler(ref -> {
                    refs.add(ref);
                    return false;
                }, agent.getHead(repository)))
                .run();
        return refs;
    }
}
