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

import com.atlassian.bitbucket.util.PageStartOutOfBoundsException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.Command;
import com.atlassian.bitbucket.scm.CommandOutputHandler;
import com.atlassian.bitbucket.scm.git.command.blame.GitBlameBuilder;
import com.atlassian.bitbucket.scm.git.command.GitCommandExitHandler;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 *
 */
public class BlameCommandExitHandler extends GitCommandExitHandler {

    /**
     * Defines a simple regular expression which will match very specifically against the output from {@code git blame}
     * when the line range requested is beyond the file's line count. The total line count is extracted as group 1.
     */
    public static final Pattern FILE_LENGTH_PATTERN = Pattern.compile("fatal: file .+ has only (\\d+) lines");

    protected static final Logger log = LoggerFactory.getLogger(BlameCommandExitHandler.class);

    private final GitBlameBuilder builder;
    private final CommandOutputHandler<?> outputHandler;
    private final int start;

    public BlameCommandExitHandler(I18nService i18nService, Repository repository, GitBlameBuilder builder,
                                   CommandOutputHandler outputHandler, int start) {
        super(i18nService, repository);

        this.builder = builder;
        this.outputHandler = outputHandler;
        this.start = start;
    }

    @Override
    protected void onError(String command, int exitCode, String stdErr, Throwable throwable) {
        if (StringUtils.isNotBlank(stdErr)) {
            log.debug("{}: Processing error output for possible retry: ({}) {}", command, exitCode, stdErr);

            //Before we begin: I know this is terrible. However, it's simple reality with git blame. Whenever using -L,
            //                 if the end of the range (whether an explicit line number or +offset) would go beyond the
            //                 end of the file, the command fails rather than returning fewer lines. Because incremental
            //                 output (blame-only) is returned out of order, though, using -L is important to be able to
            //                 performantly get subsections of blame for large files. As a result, this command needs to
            //                 be able to adjust its paging and retry itself.
            //
            //Next, to document assumptions:
            //1. git blame is fail-fast--no output will be sent to the output handler if the line range is unacceptable
            //2. git blame's output is not localised, so the response will always be in English
            //3. git blame outputs the total line count as part of the failure message when failing because of range
            //
            //With that all duly noted, let's begin:
            Matcher matcher = FILE_LENGTH_PATTERN.matcher(stdErr);
            if (matcher.matches()) {
                log.debug("{}: [{}] looks to be paging-related. Attempting to calculate an acceptable line range",
                        command, stdErr);

                int totalLines = Integer.parseInt(matcher.group(1));
                if (start <= totalLines) {
                    //If there are enough lines to satisfy the requested start index, then we know the end of the range
                    //was simply too far. Re-execute the command with an exact ending line
                    log.debug("{}: Trying again with new range [{},{}]", command, start, totalLines);
                    builder.range(start, totalLines, false);
                } else if (start > 1) {
                    //If the request is beyond the number of lines in the file, there's no successful way to run the
                    //the command that will generate any relevant output
                    log.debug("{}: Requested starting index {} cannot be satisfied; only {} lines are available",
                            command, start, totalLines);

                    throw new PageStartOutOfBoundsException(i18nService.createKeyedMessage("bitbucket.scm.git.pagerequest.outofbounds", start));
                } else {
                    //If start == 1 and totalLines == 0 we clear the requested range because, for empty files, there is
                    //still blame but no possible way to use -L
                    log.debug("{}: File appears to be empty; retrying without any range", command);
                    builder.clearRange();
                }
                //Use the standard git exit handler, because we do not want to attempt another retry regardless
                //of whether the error looks like it may be paging-related. This protects us from command loops.
                builder.exitHandler(new GitCommandExitHandler(i18nService, repository));

                Command<?> blame = builder.build(outputHandler);
                log.debug("{} failed, but will be retried as {}", command, blame.toString());
                blame.call(); //Run the command again. This time it shouldn't fail for paging

                return;
            }
        }

        super.onError(command, exitCode, stdErr, throwable);
    }
}
