package org.rakam.server.http;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Throwables;

import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.util.ArrayList;
import java.util.List;

import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static java.lang.String.format;
import static org.rakam.server.http.HttpServer.*;

public class JsonParametrizedRequestHandler implements HttpRequestHandler {
    private final ObjectMapper mapper;
    private final List<IRequestParameter> bodyParams;
    private final MethodHandle methodHandle;
    private final HttpService service;
    private final boolean isAsync;
    private final List<RequestPreprocessor<ObjectNode>> jsonPreprocessors;
    private final List<RequestPreprocessor<RakamHttpRequest>> requestPreprocessors;
    private final boolean bodyExtractionRequired;

    static final String bodyError;

    static {
        try {
            bodyError =  DEFAULT_MAPPER.writeValueAsString(errorMessage("Body must be an json object.", BAD_REQUEST));
        } catch (JsonProcessingException e) {
            throw Throwables.propagate(e);
        }
    }

    public JsonParametrizedRequestHandler(ObjectMapper mapper, ArrayList<IRequestParameter> bodyParams, MethodHandle methodHandle, HttpService service, List<RequestPreprocessor<ObjectNode>> jsonPreprocessors, List<RequestPreprocessor<RakamHttpRequest>> requestPreprocessors, boolean isAsync) {
        this.mapper = mapper;
        this.bodyParams = bodyParams;
        this.methodHandle = methodHandle;
        this.service = service;
        this.isAsync = isAsync;
        this.jsonPreprocessors = jsonPreprocessors;
        this.requestPreprocessors = requestPreprocessors;
        this.bodyExtractionRequired = bodyParams.stream().anyMatch(a -> a.in().equals("body"));
    }

    @Override
    public void handle(RakamHttpRequest request) {
        if(bodyExtractionRequired) {
            request.bodyHandler(body -> {
                ObjectNode node;
                try {
                    // TODO: use custom deserialization to avoid the overhead of garbage generated by Jackson
                    node = (ObjectNode) mapper.readTree(body);
                } catch (ClassCastException e) {
                    request.response(bodyError, BAD_REQUEST).end();
                    return;
                } catch (UnrecognizedPropertyException e) {
                    returnError(request, "Unrecognized field: " + e.getPropertyName(), BAD_REQUEST);
                    return;
                } catch (InvalidFormatException e) {
                    returnError(request, format("Field value couldn't validated: %s ", e.getOriginalMessage()), BAD_REQUEST);
                    return;
                } catch (JsonMappingException e) {
                    returnError(request, e.getCause() != null ? e.getCause().getMessage() : e.getMessage(), BAD_REQUEST);
                    return;
                } catch (JsonParseException e) {
                    returnError(request, format("Couldn't parse json: %s ", e.getOriginalMessage()), BAD_REQUEST);
                    return;
                } catch (IOException e) {
                    returnError(request, format("Error while mapping json: ", e.getMessage()), BAD_REQUEST);
                    return;
                }

                handleInternal(request, node);
            });
        } else {
            handleInternal(request, null);
        }
    }

    private void handleInternal(RakamHttpRequest request, ObjectNode node) {
        try {
            if(!jsonPreprocessors.isEmpty()) {
                for (RequestPreprocessor<ObjectNode> preprocessor : jsonPreprocessors) {
                    preprocessor.handle(request.headers(), node);
                }
            }
        } catch (Throwable e) {
            requestError(e, request);
            return;
        }

        Object[] values = new Object[bodyParams.size() + 1];
        values[0] = service;
        for (int i = 0; i < bodyParams.size(); i++) {
            IRequestParameter param = bodyParams.get(i);
            Object value = param.extract(node, request);
            if (param.required() && (value == null || value == NullNode.getInstance())) {
                returnError(request, param.name() + " " + param.in() + " parameter is required", BAD_REQUEST);
                return;
            }

            values[i + 1] = value;
        }

        Object invoke;
        try {
            invoke = methodHandle.invokeWithArguments(values);
        } catch (Throwable e) {
            requestError(e, request);
            return;
        }

        handleRequest(mapper, isAsync, invoke, request);
    }
}

@@ -4588,40 +4588,197 @@
 alue
- = param.extract(node, request);
+;%0A            try %7B%0A                value = param.extract(node, request);%0A            %7D catch (Exception e) %7B%0A                requestError(e, request);%0A                return;%0A            %7D
 %0A   

return user-friendly error when parameter type is not valid