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

import com.atlassian.bitbucket.repository.Ref;
import com.atlassian.bitbucket.repository.Tag;
import com.atlassian.utils.process.ProcessException;
import com.atlassian.utils.process.Watchdog;
import com.google.common.collect.ImmutableList;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.InputStream;
import java.util.*;

import static java.util.Optional.empty;
import static java.util.Optional.ofNullable;

/**
 * @since 4.4
 */
public class ResolveRefsForEachRefOutputHandler implements ForEachRefOutputHandler<Map<String, Ref>> {

    private final ForEachRefOutputHandler<Void> delegate;

    private final Map<String, Ref> refsByCommitId;
    private final Map<String, Ref> refsByName;
    private final Map<String, List<String>> unresolved;

    public ResolveRefsForEachRefOutputHandler(String defaultBranch, Set<String> ids) {
        delegate = new AllRefsForEachRefOutputHandler(this::onRef, defaultBranch);
        refsByCommitId = new HashMap<>();
        refsByName = new HashMap<>();
        unresolved = new HashMap<>();

        // this can't be done with Maps.asMap because the unresolved map must be mutable
        ids.stream().forEach(refId -> unresolved.put(refId, getSearchOrder(refId)));
    }

    @Override
    public void complete() throws ProcessException {
        delegate.complete();
    }

    @Override
    @Nonnull
    public String getFormat() {
        return delegate.getFormat();
    }

    @Override
    @Nullable
    public Map<String, Ref> getOutput() {
        // only return commit ID matches if the refId in question did not match a branch or tag
        refsByCommitId.keySet().removeAll(refsByName.keySet());
        refsByName.putAll(refsByCommitId);

        return refsByName;
    }

    @Override
    public void process(InputStream inputStream) throws ProcessException {
        delegate.process(inputStream);
    }

    @Override
    public void setWatchdog(Watchdog watchdog) {
        delegate.setWatchdog(watchdog);
    }

    private static List<String> getSearchOrder(String refId) {
        return ImmutableList.of(refId, "refs/" + refId, "refs/tags/" + refId, "refs/heads/" + refId);
    }

    private static Optional<String> getTagHash(Ref ref) {
        return ref instanceof Tag ? ofNullable(((Tag) ref).getHash()) : empty();
    }

    private boolean onRef(@Nonnull Ref ref) {
        Iterator<Map.Entry<String, List<String>>> it = unresolved.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, List<String>> entry = it.next();
            for (String candidate : entry.getValue()) {
                if (ref.getId().equals(candidate)) {
                    // exact hit found
                    refsByName.put(entry.getKey(), ref);

                    it.remove();
                    break;
                } else if (ref.getLatestCommit().startsWith(candidate) ||
                        getTagHash(ref).map(hash -> hash.startsWith(candidate)).orElse(false)) {
                    // if it's a hash-match, record the match but don't remove the ref ID from the iterator. If
                    // there is a better match (e.g. a ref/heads/abc123 branch), we want to return that instead.
                    refsByCommitId.putIfAbsent(entry.getKey(), ref);
                }
            }
        }

        return !unresolved.isEmpty();
    }
}
