package org.yamcs.http;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;

import org.yamcs.api.HttpBody;
import org.yamcs.logging.Log;
import org.yamcs.utils.TimeEncoding;

import com.google.protobuf.ByteString;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Message;
import com.google.protobuf.Timestamp;

import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.QueryStringDecoder;

/**
 * Converts HTTP requests to Protobuf messages used in API definitions.
 * <p>
 * This is largely inspired from how Google Cloud transcodes HTTP to gRPC. The advantage of transcoding is that the API
 * implementation can be largely agnostic of HTTP and that it can profit from Protobuf generated code without needing to
 * distinguish between route params, query params, request bodies and so on.
 */
public class HttpTranscoder {

    private static final Log log = new Log(HttpTranscoder.class);

    public static Message transcode(RouteContext ctx) throws HttpTranscodeException {
        QueryStringDecoder qsDecoder = new QueryStringDecoder(ctx.getURI());
        Message requestPrototype = ctx.getRequestPrototype();
        Message.Builder requestb = requestPrototype.newBuilderForType();

        String body = ctx.getBodySpecifier();
        if (body != null && !ctx.isClientStreaming()) {
            if ("*".equals(body)) {
                requestb = ctx.getBodyAsMessage(requestb);
            } else {
                FieldDescriptor field = requestPrototype.getDescriptorForType().findFieldByName(body);
                if (field.getMessageType().equals(HttpBody.getDescriptor())) {
                    HttpBody httpBody = toHttpBody(ctx);
                    requestb.setField(field, httpBody);
                } else {
                    Message.Builder fieldValueb = requestb.getFieldBuilder(field);
                    Message fieldValue = ctx.getBodyAsMessage(fieldValueb).build();
                    requestb.setField(field, fieldValue);
                }
            }
        } else if (!ctx.isClientStreaming() && ctx.hasBody()) {
            log.warn("Received a request with a body, but the method {} does not support request bodies",
                    ctx.getMethod().getFullName());
        }

        for (FieldDescriptor field : requestPrototype.getDescriptorForType().getFields()) {
            if (ctx.hasRouteParam(field.getJsonName())) {
                Object value = toFieldValue(field, ctx.getRouteParam(field.getJsonName()));
                requestb.setField(field, value);
            } else if (body == null) {
                List<String> queryParameter = qsDecoder.parameters().get(field.getJsonName());
                if (queryParameter != null) {
                    Object value = toFieldValue(field, queryParameter);
                    requestb.setField(field, value);
                }
            }
        }

        return requestb.build();
    }

    private static HttpBody toHttpBody(RouteContext ctx) {
        String contentType = ctx.nettyRequest.headers().get(HttpHeaderNames.CONTENT_ENCODING);

        HttpBody.Builder bodyb = HttpBody.newBuilder();
        if (contentType != null) {
            bodyb.setContentType(contentType);
        }
        if (ctx.hasBody()) {
            try (InputStream bufOut = ctx.getBodyAsInputStream()) {
                ByteString data = ByteString.readFrom(bufOut);
                bodyb.setData(data);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        }
        return bodyb.build();
    }

    private static Object toFieldValue(FieldDescriptor field, List<String> parameters) throws HttpTranscodeException {
        if (field.isRepeated()) {
            List<Object> values = new ArrayList<>();
            for (String value : parameters) {
                for (String item : value.split(",")) { // Support both repeated query params and comma-separated
                    values.add(toFieldValue(field, item));
                }
            }
            return values;
        } else {
            return toFieldValue(field, parameters.get(0));
        }
    }

    private static Object toFieldValue(FieldDescriptor field, String parameter) throws HttpTranscodeException {
        String name = field.getJsonName();
        switch (field.getJavaType()) {
        case BOOLEAN:
            return toBoolean(name, parameter);
        case INT:
            return toInt(name, parameter);
        case LONG:
            return toLong(name, parameter);
        case STRING:
            return parameter;
        case ENUM:
            return field.getEnumType().findValueByName(parameter);
        case MESSAGE:
            if (Timestamp.getDescriptor().equals(field.getMessageType())) {
                long instant = TimeEncoding.parse(parameter);
                return TimeEncoding.toProtobufTimestamp(instant);
            }
            throw new UnsupportedOperationException(
                    "No query parameter conversion for message type " + field.getMessageType().getFullName());
        default:
            throw new UnsupportedOperationException(
                    "No query parameter conversion for type " + field.getJavaType());
        }
    }

    private static boolean toBoolean(String name, String parameter) {
        return (parameter == null || "".equals(parameter) || "true".equalsIgnoreCase(parameter)
                || "yes".equalsIgnoreCase(parameter));
    }

    private static int toInt(String name, String parameter) throws HttpTranscodeException {
        try {
            return Integer.parseInt(parameter);
        } catch (NumberFormatException e) {
            throw new HttpTranscodeException(String.format(
                    "Parameter '%s' is not a valid integer value", name));
        }
    }

    private static long toLong(String name, String parameter) throws HttpTranscodeException {
        try {
            return Long.parseLong(parameter);
        } catch (NumberFormatException e) {
            throw new HttpTranscodeException(String.format(
                    "Parameter '%s' is not a valid integer value", name));
        }
    }
}

@@ -103,32 +103,65 @@
 kedIOException;%0A
+import java.text.ParseException;%0A
 import java.util
@@ -172,16 +172,16 @@
 ayList;%0A
-
 import j
@@ -260,45 +260,8 @@
 Log;
-%0Aimport org.yamcs.utils.TimeEncoding;
 %0A%0Aim
@@ -389,16 +389,16 @@
 essage;%0A
-
 import c
@@ -426,16 +426,60 @@
 mestamp;
+%0Aimport com.google.protobuf.util.Timestamps;
 %0A%0Aimport
@@ -4957,119 +4957,242 @@
     
-long instant = TimeEncoding.parse(parameter);%0A                return TimeEncoding.toProtobufTimestamp(instant);
+try %7B%0A                    return Timestamps.parse(parameter);%0A                %7D catch (ParseException e) %7B%0A                    throw new HttpTranscodeException(%22Provided date string does not conform to RFC 3339%22, e);%0A                %7D
 %0A   

Use Protobuf's parser when datestrings are read from URI