package com.atlassian.bitbucket.mesh.git.builder.foreachref;

import com.atlassian.bitbucket.mesh.git.ref.Ref;
import com.atlassian.bitbucket.mesh.git.ref.Tag;
import com.atlassian.bitbucket.mesh.process.handler.AbstractProcessStdioHandler;
import com.google.common.collect.ImmutableList;

import javax.annotation.Nonnull;
import java.nio.ByteBuffer;
import java.util.*;

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

public class ResolveRefsForEachRefStdioHandler extends AbstractProcessStdioHandler<Map<String, Ref>>
        implements ForEachRefStdioHandler<Map<String, Ref>> {

    private final ForEachRefStdioHandler<Void> delegate;

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

    public ResolveRefsForEachRefStdioHandler(String defaultBranch, Set<String> ids) {
        delegate = new AllRefsForEachRefStdioHandler(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.forEach(refId -> unresolved.put(refId, getSearchOrder(refId)));
    }

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

    @Override
    public Map<String, Ref> getResult() {
        // 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 onStdout(@Nonnull ByteBuffer buffer, boolean closed) {
        delegate.onStdout(buffer, closed);
    }

    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();
    }
}
