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

import com.atlassian.bitbucket.mesh.annotation.Profiled;
import com.atlassian.bitbucket.mesh.config.ProcessConfig;
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.foreachref.AllRefsForEachRefStdioHandler;
import com.atlassian.bitbucket.mesh.git.builder.push.GitPushBuilder;
import com.atlassian.bitbucket.mesh.git.builder.push.PushStdioHandler;
import com.atlassian.bitbucket.mesh.git.ref.Ref;
import com.atlassian.bitbucket.mesh.process.ProcessSummary;
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 DefaultPushManager implements PushManager {

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

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

    @Override
    public void push(@Nonnull PushParameters parameters, @Nonnull PushCallback callback) {
        Repository repository = parameters.getRepository();
        PushStdioHandler handler = new PushStdioHandler(agent, callback, getCurrentRefs(repository),
                parameters.getTargetUrl(), repository);

        GitPushBuilder builder = builderFactory.builder(parameters.getRepository())
                .push()
                .author(parameters.getAuthor().getName(), parameters.getAuthor().getEmail())
                .force(parameters.isForce())
                .prune(parameters.isPrune())
                .progress(true)
                .porcelain(true) // Push supports porcelain, which simplifies parsing
                .repository(parameters.getTargetUrl())
                .withConfiguration(GitConstants.CONFIG_CORE_ABBREV, "40") // try to get full hashes for updated refs
                .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.getTargetUrl(), 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 {
            fireOnEnd(callback, summary, exception);
        }
    }

    private static void fireOnEnd(@Nonnull PushCallback callback, ProcessSummary summary, Throwable thrown) {
        if (summary == null) {
            callback.onEnd(new ProcessSummary.Builder("git push").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;
    }
}
