package com.atlassian.bitbucket.mesh.logback;

import ch.qos.logback.classic.pattern.DateConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Context;
import ch.qos.logback.core.encoder.Encoder;
import ch.qos.logback.core.encoder.EncoderBase;
import com.atlassian.bitbucket.mesh.request.RequestContext;
import org.apache.commons.lang3.StringUtils;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Set;

import static com.atlassian.bitbucket.mesh.logback.AccessLog.PHASE_END;
import static com.google.common.base.MoreObjects.firstNonNull;
import static org.apache.commons.lang3.StringUtils.defaultString;

/**
 * A Logback {@link Encoder} which, when paired with access logging in the {@link AccessLog}, formats "i" (start)
 * and "o" (end) lines for the access log. Rather than relying on the MDC, or any thread-local state (which can easily
 * end up cleared by the time "o" lines are written), this encoder relies on the {@link RequestContext} and the "i" or
 * "o" value being provided as arguments on the {@link ILoggingEvent}, and then assembles the log entry accordingly.
 * <p>
 * Note that this appender <i>fully ignores</i> the message on the {@link ILoggingEvent}. It assembles its own message
 * in a fixed format based on the {@link RequestContext} it receives.
 */
public class AccessLogEncoder extends EncoderBase<ILoggingEvent> {

    //The repeat constant here is coupled to the number of fields in the "o" handling below
    static final String START_PLACEHOLDERS = StringUtils.repeat(" | -", 7);

    private final DateConverter dateConverter;

    public AccessLogEncoder() {
        dateConverter = new DateConverter();
    }

    @Override
    public byte[] encode(ILoggingEvent event) {
        Object[] arguments = event.getArgumentArray();
        if (arguments == null || arguments.length != 2 ||
                !(arguments[0] instanceof RequestContext && arguments[1] instanceof String)) {
            addError("Unexpected arguments for access log: " + Arrays.toString(arguments));
            return "-".getBytes(StandardCharsets.UTF_8);
        }

        RequestContext ctx = (RequestContext) arguments[0];
        String phase = (String) arguments[1];

        String[] common = new String[]{
                ctx.getRemoteAddress(),
                ctx.getProtocol(),
                phase + "@" + ctx.getId(),
                ctx.getCorrelationId(),
                ctx.getUser(),
                dateConverter.convert(event), //Use Logback's standard date handling
                ctx.getAction(),
                ctx.getAgent(),
        };

        //The first common element is handled outside the loop to simplify the loop so it doesn't need
        //to continuously check if it's on the first item or not
        StringBuilder builder = new StringBuilder(defaultString(common[0], "-"));
        for (int i = 1; i < common.length; ++i) {
            builder.append(" | ").append(defaultString(common[i], "-"));
        }

        if (PHASE_END.equals(phase)) {
            //For end lines, we want to include details about the request
            Object[] details = new Object[]{
                    firstNonNull(ctx.getResult(), "-"), //Should always be set by now, but...
                    ctx.getMessagesReceived(),
                    ctx.getMessagesSent(),
                    ctx.getBytesReceived(),
                    ctx.getBytesSent(),
                    formatLabels(ctx.getLabels()),
                    ctx.getDuration().toMillis()
                    //If you add or remove fields update START_PLACEHOLDERS
            };
            for (Object detail : details) {
                builder.append(" | ").append(detail);
            }
        } else {
            //For start lines, append placeholders for the detail fields because they're not available yet
            builder.append(START_PLACEHOLDERS);
        }

        return builder.append("\n")
                .toString()
                .getBytes(StandardCharsets.UTF_8);
    }

    @Override
    public byte[] footerBytes() {
        //No footer on the access log
        return null;
    }

    @Override
    public byte[] headerBytes() {
        //No header on the access log
        return null;
    }

    @Override
    public void setContext(Context context) {
        dateConverter.setContext(context);

        super.setContext(context);
    }

    @Override
    public void start() {
        dateConverter.start();

        super.start();
    }

    private static String formatLabels(Set<String> labels) {
        if (labels.isEmpty()) {
            return "-";
        }

        return String.join(", ", labels).replace("|", "\\|"); //Escape any separators
    }
}
