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

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.scm.Command;
import com.atlassian.bitbucket.scm.CommandOutputHandler;
import com.atlassian.bitbucket.scm.git.command.GitCommand;
import com.atlassian.bitbucket.scm.git.command.GitCommandExitHandler;
import com.atlassian.bitbucket.scm.git.command.reset.GitResetBuilder;
import com.atlassian.utils.process.LineOutputHandler;
import com.atlassian.utils.process.ProcessException;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Handles a special situation when attempting to perform the {@code git reset} part of a conflict-handling merge so
 * that an exception is not thrown unnecessarily and the merge can complete successfully.
 * <p>
 * Specifically it handles the case when the following three things all occur after the {@code git reset} process has
 * exited:
 * <ul>
 *      <li>a non-zero exit result for the command is returned</li>
 *      <li>the line "some_file: needs merge" appears in stdout</li>
 *      <li>stderr does not contain any content that appears to be an error message as defined by
 *      {@link GitCommandExitHandler#isErrorOutput(String, String)}</li>
 * </ul>
 * <p>
 * The above three things indicate that a pull request with multiple conflicts, including at least one rename/add or
 * directory/file conflict, is being merged. In that situation, the exit code, exception and stderr combination that
 * is described above is not indicative of an error and the merge process should continue.
 * <p>
 * See <a href="https://jira.atlassian.com/browse/STASHDEV-1781">STASHDEV-1781</a> for more details.
 */
public class ResetCommandHandler  {

    private static final Pattern PATTERN_NEEDS_MERGE = Pattern.compile("(\\S+): needs merge");

    private final I18nService i18nService;

    private boolean needsMerge;

    public ResetCommandHandler(I18nService i18nService) {
        this.i18nService = i18nService;
    }

    public GitCommand<Void> build(GitResetBuilder resetBuilder) {
        ExitHandler exitHandler = new ExitHandler(i18nService);
        OutputHandler outputHandler = new OutputHandler();

        GitCommand<Void> command = resetBuilder.exitHandler(exitHandler).build(outputHandler);
        outputHandler.setCommand(command);

        return command;
    }

    @VisibleForTesting
    class ExitHandler extends GitCommandExitHandler {

        public ExitHandler(I18nService i18nService) {
            super(i18nService, null);
        }

        @Override
        protected boolean isError(String command, int exitCode, String stdErr, Throwable thrown) {
            if (needsMerge && (exitCode != 0 || isNonZeroExitCodeException(thrown))) {
                //If needs merge msg seen then only flag a non-zero exit code as an error if the stderr output
                //actually looks like an error
                return isErrorOutput(command, stdErr);
            }
            return super.isError(command, exitCode, stdErr, thrown);
        }
    }

    @VisibleForTesting
    class OutputHandler extends LineOutputHandler implements CommandOutputHandler<Void> {

        private Command<?> command;
        private Logger log;
        private List<String> lines = new ArrayList<>();

        @Override
        public Void getOutput() {
            return null;
        }

        @Override
        protected void processLine(int lineNum, String line) {
            Matcher matcher = PATTERN_NEEDS_MERGE.matcher(line);
            if (matcher.matches()) {
                needsMerge = true;
                if (log.isDebugEnabled()) {
                    log.debug("Merge needed on file \"{}\". Ignoring any non-zero exit code of git command where " +
                            "stderr does not otherwise indicate an error.", matcher.group(1));
                }
            }
            lines.add(line);
        }

        @Override
        protected void processInputEnd(int lineCount) throws ProcessException {
            if (lines.isEmpty()) {
                log.trace("{}: Completed with no output", command);
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("{}: Completed with the following output:\n{}", command, StringUtils.join(lines, "\n"));
                }
            }
        }

        @VisibleForTesting
        void setCommand(GitCommand<Void> command) {
            this.command = command;
            this.log = LoggerFactory.getLogger(command.getClass());
        }
    }
}
