package com.atlassian.stash.internal.scm.git.protocol.http;

import com.atlassian.bitbucket.scm.AuthenticationState;
import com.atlassian.bitbucket.scm.git.GitUtils;
import com.atlassian.bitbucket.scm.http.HttpScmRequest;
import com.atlassian.bitbucket.util.RequestUtils;
import com.google.common.base.Charsets;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;

/**
 * Utility methods for working with {@code git} over HTTP.
 */
public class GitHttpUtils {

    private GitHttpUtils() {
        throw new UnsupportedOperationException(getClass().getName() +
                " is a utility class and should not be instantiated");
    }

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

    /**
     * The maximum hex-encoded length for a packet. This constant takes into account the 4 bytes which are reserved
     * for prepending the length to the packet, so it provides the maximum number of bytes which may be included in
     * one packet.
     */
    private static final int MAX_PACKET_LENGTH = Integer.decode("0xFFFF") - 4; //-4 for the 4-byte length

    /**
     * Construct a String that when, when flushed to the Git client over HTTP, will display in the user's terminal as:
     * <pre>
     * fatal: remote error: &lt;title&gt;
     * &lt;message&gt;
     * </pre>
     *
     * @param title   a brief summary of the error
     * @param message a detailed description of the error. Messages will be wrapped at 80 chars.
     * @param service the service that was the target of the request that failed
     * @return a String suitable for writing directly to the HttpServletResponse's output stream, that will trigger the
     *         git client to display the supplied title and message.
     */
    public static String asRefError(String title, String message, String service) {
        StringBuilder messageBuilder = new StringBuilder();
        //First packet: <length># service=<service> -- This is required by git to indicate the server is "smart"
        messageBuilder.append(createPacket("# service=" + service));
        //Next, empty packet -- This separates "metadata" from the response body
        messageBuilder.append("0000");
        //Lastly, an ERR packet -- This indicates to git that the command did not succeed. It will display the
        //packet contents after "ERR" as an error message on the client.
        messageBuilder.append(createPacket("ERR " + title + "\n" + GitUtils.wordWrap(message)));
        return messageBuilder.toString();
    }

    /**
     * Creates a "packet" from the provided {@code line} to be sent over the 'error' sideband as part of pack file
     * transmission, as part of git-upload-pack or git-receive-pack.
     * <p>
     * Packets are only sent over sidebands as part of transmission of git packfile data. Three sidebands are supported:
     * <ul>
     *     <li>1 for packfile data</li>
     *     <li>2 for progress information</li>
     *     <li>3 for error information</li>
     * </ul>
     * Sideband packets are similar to normal packets, with one difference: the line starts with the channel byte. The
     * total packet consists of a 4-byte hex-encoded length, followed by the channel byte, followed by the line. The
     * length <em>includes</em> the channel byte.
     * <p>
     * For simplicity, this method always appends a newline to the provided line before prepending the channel byte and
     * the length bytes.
     * <p>
     * If the provided line plus the band byte exceeds {@link #MAX_PACKET_LENGTH}, it is truncated and only what will
     * fit in a packet is returned. git does not support any form of "continuation" marker to indicate that a packet
     * was not large enough to include all of the desired contents, so there's no "better" approach. For this reason,
     * it's best to keep lines which will be sent to git brief.
     *
     * @param line the line to encode
     * @return the provided line with a hex-encoded 4 byte length and a channel byte prepended and a newline appended
     */
    public static byte[] createErrorSidebandPacket(String line) {
        line = trimLine(line + "\n", MAX_PACKET_LENGTH - 1); //-1 to reserve space for the channel byte

        //+5 is for the four byte length and the channel byte we're prepending to the line. We left-pad with 0s to
        // ensure the length is always 4 bytes, even for shorter lines.
        String length = StringUtils.leftPad(Integer.toHexString(line.length() + 5), 4, "0");
        byte[] packet = new byte[line.length() + 5];
        System.arraycopy(length.getBytes(StandardCharsets.UTF_8), 0, packet, 0, 4);
        packet[4] = (byte) 3;
        System.arraycopy(line.getBytes(StandardCharsets.UTF_8), 0, packet, 5, line.length());
        return packet;
    }

    /**
     * Creates a "packet" from the provided {@code line}.
     * <p>
     * In git parlance, a packet is a 4-byte hex-encoded length followed by a line containing that many characters.
     * For simplicity, this method always appends a newline to the provided line before prepending the length bytes.
     * <p>
     * If the provided line exceeds {@link #MAX_PACKET_LENGTH}, it is truncated and only what will fit in a packet is
     * returned. git does not support any form of "continuation" marker to indicate that a packet was not large
     * enough to include all of the desired contents, so there's no "better" approach. For this reason, it's best
     * to keep lines which will be sent to git brief.
     *
     * @param line the line to transform into a packet
     * @return the provided line with a hex-encoded 4 byte length prepended and a newline appended
     */
    public static String createPacket(String line) {
        line = trimLine(line + "\n", MAX_PACKET_LENGTH);

        //+4 is for the four byte length we're prepending to the line. We left-pad with 0s to ensure the length is
        //always 4 bytes, even for shorter lines.
        String length = StringUtils.leftPad(Integer.toHexString(line.length() + 4), 4, "0");

        return length + line;
    }

    public static void sendError(HttpServletRequest request, HttpServletResponse response,
                                 String summary, String detailMessage) throws IOException {
        String service = request.getParameter("service");

        //Git only processes the content of the response if the status code is 200 OK. Because we want to display a
        //useful error message, we want to use git's internal ability to send error messages. Unfortunately, this
        //means our code has to be 200 even though technically it shouldn't be.
        response.setStatus(HttpServletResponse.SC_OK);
        if (service != null) {
            //JGit does a verification on the content type (which normal git does not do). If we do not set the expected,
            //content type, JGit assumes we're dumb without even bothering to look at the content of the response.
            response.setContentType("application/x-" + service + "-advertisement");

            sendPacket(response, asRefError(summary, detailMessage, service));
        } else {
            sendPacket(response, createPacket(summary + "\n" + GitUtils.wordWrap(detailMessage)));
        }
    }

    /**
     * If the user needs to authenticate, send a basic auth challenge to the client. If the user's account has been
     * captcha'd, send a message to the client entreating them to journey to the Stash web UI and solve a CAPTCHA
     * challenge to unlock their account. If some other authentication error has occurred, simply flush the message to
     * the client.
     */
    public static void sendAuthenticationError(HttpServletRequest request, HttpServletResponse response,
                                               AuthenticationState state, String summary, String detailMessage) throws IOException {
        // todo build a specific mechanism into OAuth from adding an Authorization header here (STASHDEV-681)

        if (state == AuthenticationState.NOT_AUTHENTICATED) {
            sendAuthenticationChallenge(response);
        } else {
            // display the message in the same manner as other validation errors
            sendError(request, response, summary, detailMessage);
        }
    }

    /**
     * Send authentication challenge, no body is sent as the Git client doesn't support displaying a message for a 401.
     *
     * @param response the response to write to
     */
    public static void sendAuthenticationChallenge(HttpServletResponse response) {
        HttpServletResponse unwrappedResponse = RequestUtils.unwrap(response);
        unwrappedResponse.setHeader("WWW-Authenticate", "Basic realm=\"" + HttpScmRequest.BITBUCKET_REALM + "\"");
        unwrappedResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    }

    /**
     * Writes directly to the response outputstream, because Tomcat 7 automatically appends the charset to the
     * Content-Type when you use HttpServletResponse.getWriter(). This breaks JGit because JGit only does an equals
     * check against the content-type.
     *
     * @param response the response to write to
     * @param packet a well-formed git protocol "packet" (see {@link #createPacket(String)} for details)
     * @throws IOException if writing to the output stream caused an exception
     */
    public static void sendPacket(HttpServletResponse response, String packet) throws IOException {
        Writer writer = new OutputStreamWriter(response.getOutputStream(), Charsets.UTF_8);
        writer.write(packet);
        writer.flush();
        response.flushBuffer();
    }

    /**
     * Writes a package to the response outputstream
     *
     * @param response the response to write to
     * @param packet a well-formed git protocol "packet" (see {@link #createPacket(String)} for details)
     * @throws IOException if writing to the output stream caused an exception
     */
    public static void sendPacket(HttpServletResponse response, byte[] packet) throws IOException {
        OutputStream os = response.getOutputStream();
        os.write(packet);
        os.flush();
        response.flushBuffer();
    }

    /**
     * Write the supplied errorMessage to the response in the Git {@link #createPacket packet format}. This can be used
     * as a last resort to get the client to display a meaningful error message when something goes wrong, as git client
     * commands often write protocol violations from the server to stderr.
     *
     * @param response the response to write to
     * @param errorMessage an error message String
     */
    public static void sendErrorAsRawPacket(HttpServletResponse response, String errorMessage) throws IOException {
        sendPacket(response, createPacket(errorMessage));
    }

    /**
     * Trims the line to {@code maxLength} so it will fit in a git packet. If the line is too long and is trimmed,
     * a {@code \n} character will be appended. The resulting line including the \n character will be
     * {@code maxLength} characters long.
     *
     * @param line the line
     * @param maxLength maximum line length
     * @return the trimmed line if {@code line} was too long, otherwise {@code line}
     */
    private static String trimLine(String line, int maxLength) {
        if (line.length() > maxLength) {
            //If the hex-encoded length exceeds 0xFFFF, we can't send it to git (which has no support for continuation
            //of a line in multiple packets). Log a warning and truncate the message
            log.warn("The provided line will not fit in a packet to send to git. Its length is {}, which is greater " +
                    "than the max length supported by git ({})", line.length(), maxLength);

            return line.substring(0, maxLength) + "\n"; //-1 leaves room for trailing newline
        }
        return line;
    }
}
