package com.atlassian.bitbucket.mesh.repair;

import com.atlassian.bitbucket.mesh.client.FetchObjectsParameters;
import com.atlassian.bitbucket.mesh.client.MeshClient;
import com.atlassian.bitbucket.mesh.git.GitPackFile;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.ref.ProposedRefUpdate;
import com.atlassian.bitbucket.mesh.network.MeshNode;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.util.profiling.Ticker;
import com.atlassian.util.profiling.Timer;
import com.atlassian.util.profiling.Timers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Closeable;
import java.time.Duration;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;

public class MissingObjectsFetcher implements Closeable {

    private static final Timer FETCH_TIMER = Timers.timerWithMetric(
            MissingObjectsFetcher.class.getName() + ".maybeFetch", "replication.FetchMissingObjects");
    private static final Logger log = LoggerFactory.getLogger(MissingObjectsFetcher.class);

    private final FetchParametersBuilder builder;
    private final MeshClient meshClient;
    private final Repository repository;
    private final MeshNode source;

    public MissingObjectsFetcher(GitProcessBuilderFactory builderFactory, MeshClient meshClient,
                                 Repository repository, MeshNode source) {
        this.meshClient = meshClient;
        this.repository = repository;
        this.source = source;

        builder = new FetchParametersBuilder(builderFactory, repository, false, 100);
        builder.keep(true);
    }

    @Override
    public void close() {
        builder.close();
    }

    public void includeCommitsIfMissing(@Nonnull ProposedRefUpdate refUpdate) {
        builder.includeCommitsIfMissing(refUpdate);
    }

    public void includeIfMissing(@Nonnull String rev) {
        builder.includeIfMissing(rev);
    }

    public boolean isMissingAnyObjects() {
        return builder.isMissingAnyObjects();
    }

    /**
     * @return the pack file if any objects were missing, otherwise {@code null}
     */
    @Nullable
    public GitPackFile maybeFetch() {
        try {
            FetchObjectsParameters parameters = builder.maybeBuild();
            if (parameters == null) {
                return null;
            }
            if (log.isDebugEnabled()) {
                Set<String> excludes = parameters.getExcludes();
                Set<String> includes = parameters.getIncludes();
                log.debug("[{}] Fetching {} missing objects ({}) with {} excludes ({})", repository,
                        includes.size(), joinAbbreviated(includes, 5), excludes.size(), joinAbbreviated(excludes, 5));
            }
            try (Ticker ignored = FETCH_TIMER.start(repository)) {
                return meshClient.fetchObjects(source, repository, parameters);
            }
        } finally {
            //Terminate the cat-file batch if  is running
            close();
        }
    }

    public void setTimeout(@Nonnull Duration timeout) {
        builder.setTimeout(timeout);
    }

    private static String joinAbbreviated(Collection<String> items, int count) {
        return items.stream()
                .limit(count)
                .collect(Collectors.joining(", ", "", items.size() > count ? ", ..." : ""));
    }
}
