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

import com.atlassian.bitbucket.mesh.git.GitProcessCompletionHandler;
import com.atlassian.bitbucket.mesh.git.exception.RefNotFoundException;
import com.atlassian.bitbucket.mesh.process.ProcessSummary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Extends {@link GitProcessCompletionHandler} with behaviour specific to {@code git update-ref -d} and its quirks.
 * <p>
 * {@code git update-ref -d /refs/heads/nonexistent} will return 0 even if the ref does not exist <i>unless</i> an old
 * value is provided, like {@code git update-ref -d /refs/heads/nonexistent 2e10dd2d1d5eea9291b296e78312e8a703964a95}.
 * Then, because it cannot open the ref to verify the old value, it will return non-zero with an error message stating
 * that the ref could not be resolved. This exit handler does <i>not</i> change that behaviour; instead, it throws a
 * more specific exception than what {@link GitProcessCompletionHandler} would have thrown.
 */
public class DeleteUpdateRefCompletionHandler extends GitProcessCompletionHandler {

    /**
     * Matches error messaging from {@code git update-ref -d} when the specified ref does not exist.
     * <p>
     * Prior to Git 2.2.0 (2.1.x and older), the error messaging looked like this:
     * <code><pre>
     * error: unable to resolve reference refs/heads/nonexistent: No such file or directory
     * </pre></code>
     * Starting in 2.2.0, the error message grew an extra line, like this:
     * <code><pre>
     * error: unable to resolve reference refs/heads/nonexistent: No such file or directory
     * error: Cannot lock the ref 'refs/heads/nonexistent'.
     * </pre></code>
     * In 2.4.3 the error message was changed again, collapsing the two lines into a single line:
     * <code><pre>
     * error: Cannot lock ref 'refs/heads/nonexistent': unable to resolve reference refs/heads/nonexistent: No such file or directory
     * </pre></code>
     * In 2.5.0-rc0 the error message was changed again, lower-casing the "c" in "cannot":
     * <code><pre>
     * error: cannot lock ref 'refs/heads/nonexistent': unable to resolve reference refs/heads/nonexistent: No such file or directory
     * </pre></code>
     * 2.10.0 introduced further changes, dropping the OS-specific "No such file or directory" message and
     * enclosing the ref name for "unable to resolve reference" in single quotes:
     * <code><pre>
     * error: cannot lock ref 'refs/heads/nonexistent': unable to resolve reference 'refs/heads/nonexistent'
     * </pre></code>
     * Using {@code MULTILINE} causes "$" in the pattern to match on <i>either</i> end-of-text or end-of-line,
     * rather than just end-of-text, which allows this pattern to match both 2.1.x- and 2.2.x+ output. Using
     * "$" is desirable to help limit the scope of the greedy ".*" so that we only match against a single line.
     * The new "Cannot lock ref" section of the message is optional, to continue matching the error message. If
     * it matches, the ref name is validated to ensure it's the same between the two places it's printed. That
     * additional validation is intended to ensure the parsing is being applied correctly.
     */
    static final Pattern PATTERN_UNABLE_TO_RESOLVE = Pattern.compile(
            //This pattern uses a reluctant quantifier in the last capturing group because the ' after
            //is optional. An eager quantifier prefers to include the ' in the captured name
            "error: (?:cannot lock ref '([^:]+)': )?unable to resolve reference '?([^:]+?)'?:?.*$",
            Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);

    private static final Logger log = LoggerFactory.getLogger(DeleteUpdateRefCompletionHandler.class);

    @Override
    protected void evaluateStderr(@Nonnull ProcessSummary summary, @Nonnull String stderr) {
        Matcher matcher = PATTERN_UNABLE_TO_RESOLVE.matcher(stderr);
        if (matcher.find()) { //There may be more error messaging after the match, so don't use matches()
            String refId = matcher.group(1);
            if (refId == null) {
                //Git 2.2.0 to 2.4.2 output two lines, and Git prior to 2.2.0 only output one, but
                //without the "Cannot lock ref" message. On those versions, we use the second group
                refId = matcher.group(2);
            } else if (!refId.equals(matcher.group(2))) {
                //Git 2.4.3+ merge the two lines into a single line. On those versions, we verify
                //that we parsed out the same ref name on both groups. If they don't, we log a warning
                log.warn("[{}] != [{}]: Unexpected mismatch in ref names after parsing\n{}",
                        refId, matcher.group(2), stderr);
            }

            throw new RefNotFoundException(refId);
        }

        super.evaluateStderr(summary, stderr);
    }
}
