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

import com.atlassian.bitbucket.ServiceException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.repository.RefChange;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.git.GitUtils;
import com.atlassian.bitbucket.scm.git.command.GitCommand;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.bitbucket.scm.git.command.updateref.GitUpdateRef;
import com.atlassian.bitbucket.scm.mirror.MirrorUpdateRefsCommandFailedException;
import com.atlassian.bitbucket.scm.mirror.MirrorUpdateRefsCommandParameters;
import com.atlassian.bitbucket.util.MoreCollectors;
import com.atlassian.stash.internal.scm.git.command.SimpleGitCommand;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;

/**
 * Updates a set of refs in a repository, validating that the ref is in the pre-refchange state before applying the
 * ref change. This bulk command will apply all {@link RefChange ref changes} that can be safely. If one or more
 * {@link RefChange changes} fail to apply, an error will be thrown after the last {@link RefChange} is attempted.
 * <p>
 * Note: git 1.8.5+ supports a {@code --stdin} argument on the {@code update-ref} command. Since the minimum supported
 * version is currently 1.8.0, this cannot be used on universally. The --stdin version also has the problem that any
 * failure immediately aborts the command, which makes it less suitable for our use.
 */
public class MirrorUpdateRefsCommand extends SimpleGitCommand<Void> {

    private final GitCommandBuilderFactory builderFactory;
    private final I18nService i18nService;
    private final MirrorUpdateRefsCommandParameters parameters;
    private final Repository repository;

    public MirrorUpdateRefsCommand(@Nonnull ExecutorService executorService,
                                   @Nonnull GitCommandBuilderFactory builderFactory,
                                   @Nonnull I18nService i18nService,
                                   @Nonnull Repository repository,
                                   MirrorUpdateRefsCommandParameters parameters) {
        super(executorService);

        this.builderFactory = builderFactory;
        this.i18nService = i18nService;
        this.parameters = parameters;
        this.repository = repository;
    }

    @Nullable
    @Override
    public Void call() {
        List<RefUpdateError> errors = new ArrayList<>();
        for (RefChange change : parameters.getChanges()) {
            try {
                applyRefChange(change);
            } catch (ServiceException e) {
                errors.add(new RefUpdateError(change, e.getLocalizedMessage()));
            } catch (RuntimeException e) {
                errors.add(new RefUpdateError(change, e.getMessage()));
            }
        }

        if (!errors.isEmpty()) {
            String errorDetails = errors.stream()
                    .map(RefUpdateError::toString)
                    .collect(Collectors.joining(",\n\t"));
            List<RefChange> failed = errors.stream()
                    .map(RefUpdateError::getChange)
                    .collect(MoreCollectors.toImmutableList());
            throw new MirrorUpdateRefsCommandFailedException(
                    i18nService.createKeyedMessage("bitbucket.git.ref.bulk.update.failed", repository, errorDetails),
                    failed);
        }
        return null;
    }

    private void applyRefChange(RefChange refChange) {
        GitUpdateRef updateRef = builderFactory.builder(repository).updateRef();
        GitCommand<Void> command;
        switch (refChange.getType()) {
            case ADD:
                command = updateRef.set(refChange.getRef().getId(), refChange.getToHash())
                        .oldValue(GitUtils.NULL_SHA1).build();
                break;
            case DELETE:
                command = updateRef.delete(refChange.getRef().getId())
                        .oldValue(refChange.getFromHash()).build();
                break;
            case UPDATE:
                command = updateRef.set(refChange.getRef().getId(), refChange.getToHash())
                        .oldValue(refChange.getFromHash()).build();
                break;
            default:
                throw new IllegalArgumentException("Unsupported RefChange type " + refChange.getType());
        }

        configureAndCall(command);
    }

    private static class RefUpdateError {
        private final RefChange change;
        private final String message;

        RefUpdateError(RefChange change, String message) {
            this.change = change;
            this.message = message;
        }

        public RefChange getChange() {
            return change;
        }

        public String toString() {
            return change.getRef().getId() + ": \"" + message + "\"";
        }
    }
}
