package com.nike.riposte.server.handler;

import com.nike.riposte.server.channelpipeline.ChannelAttributes;
import com.nike.riposte.server.error.exception.MissingRequiredContentException;
import com.nike.riposte.server.error.validation.RequestValidator;
import com.nike.riposte.server.handler.base.BaseInboundHandlerWithTracingAndMdcSupport;
import com.nike.riposte.server.handler.base.PipelineContinuationBehavior;
import com.nike.riposte.server.http.Endpoint;
import com.nike.riposte.server.http.HttpProcessingState;
import com.nike.riposte.server.http.RequestInfo;

import org.jetbrains.annotations.NotNull;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.function.Function;

import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.LastHttpContent;

import static com.nike.riposte.util.AsyncNettyHelper.runnableWithTracingAndMdc;

/**
 * Looks at the current channel state's {@link HttpProcessingState#getEndpointForExecution()} and {@link
 * HttpProcessingState#getRequestInfo()} to see if (1) the endpoint wants the incoming request content validated, and
 * (2) the incoming request's {@link RequestInfo#getContent()} is populated. If both those things are true then {@link
 * #validationService} will be run on the request's content, which will throw an appropriate exception with validation
 * violation details. If {@link #validationService} is null then no validation will be performed.
 * <p/>
 * NOTE: Both the payload deserialization and validation may be done asynchronously if {@link
 * Endpoint#shouldValidateAsynchronously(RequestInfo)} returns true. If asynchronous processing is requested then the
 * deserialization and validation will be done via {@link CompletableFuture} running in a separate {@link Executor}.
 * That future is added to {@link HttpProcessingState#addPreEndpointExecutionWorkChainSegment(Function)} so that the
 * endpoint handler can make sure the future completes successfully before the endpoint execution happens. If the future
 * throws an exception then the endpoint should not be executed.
 * <p/>
 * This must come after {@link com.nike.riposte.server.handler.RequestContentDeserializerHandler} in the pipeline to
 * make sure that the {@link com.nike.riposte.server.http.RequestInfo#getContent()} has had a chance to be populated.
 *
 * @author Nic Munroe
 */
public class RequestContentValidationHandler extends BaseInboundHandlerWithTracingAndMdcSupport {

    private static final Executor ASYNC_VALIDATION_EXECUTOR =
        Executors.newWorkStealingPool(Runtime.getRuntime().availableProcessors() * 2);

    private final RequestValidator validationService;

    public RequestContentValidationHandler(RequestValidator validationService) {
        if (validationService == null) {
            throw new IllegalArgumentException(
                "validationService cannot be null. If you don't have a validationService to pass in, don't register "
                + "this handler in the pipeline"
            );
        }

        this.validationService = validationService;
    }

    @Override
    public PipelineContinuationBehavior doChannelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof LastHttpContent) {
            HttpProcessingState state = ChannelAttributes.getHttpProcessingStateForChannel(ctx).get();
            Endpoint<?> endpoint = state.getEndpointForExecution();
            RequestInfo<?> requestInfo = state.getRequestInfo();

            if (endpoint != null && requestInfo.isCompleteRequestWithAllChunks()) {
                if (endpoint.isRequireRequestContent() && requestInfo.getRawContentLengthInBytes() == 0) {
                    throw new MissingRequiredContentException(requestInfo, endpoint);
                }

                if (endpoint.isValidateRequestContent(requestInfo)
                        //TODO: Is this actually necessary? Does false indicate a misconfigured endpoint?
                        && requestInfo.isContentDeserializerSetup()     // Content deserialization must be possible
                        ) {
                    if (endpoint.shouldValidateAsynchronously(requestInfo)) {
                        // The endpoint has requested asynchronous validation, so split it off into the
                        //      pre-endpoint-execution-work-chain.
                        state.addPreEndpointExecutionWorkChainSegment(aVoid -> CompletableFuture.runAsync(
                                () -> executeValidation(requestInfo, endpoint, ctx),
                                ASYNC_VALIDATION_EXECUTOR)
                        );
                    }
                    else {
                        // This request can be validated synchronously, so do it now.
                        executeValidation(requestInfo, endpoint, ctx);
                    }
                }
            }
        }

        return PipelineContinuationBehavior.CONTINUE;
    }

    @Override
    protected boolean argsAreEligibleForLinkingAndUnlinkingDistributedTracingInfo(
        HandlerMethodToExecute methodToExecute, ChannelHandlerContext ctx, Object msgOrEvt, Throwable cause
    ) {
        // To save on extraneous linking/unlinking, we'll do it as-necessary in this class.
        return false;
    }

    @SuppressWarnings("WeakerAccess")
    protected void executeValidation(
        @NotNull RequestInfo<?> requestInfo,
        @NotNull Endpoint<?> endpoint,
        ChannelHandlerContext ctx
    ) {
        runnableWithTracingAndMdc(() -> {
            // NOTE: The requestInfo.getContent() call should be here in this method because this is the first time
            //      getContent() is called and will deserialize the payload here. For very large payloads it may take a
            //      while to deserialize, and if we're validating asynchronously we should be deserializing
            //      asynchronously as well.
            // TODO: Is null check required here before validation now that we are doing a missing content check?
            if (requestInfo.getContent() != null) {
                // We have an endpoint and validation is requested and the content has been deserialized.
                //      Perform the validation.
                Class<?>[] validationGroups = endpoint.validationGroups(requestInfo);
                if (validationGroups == null)
                    validationService.validateRequestContent(requestInfo);
                else
                    validationService.validateRequestContent(requestInfo, validationGroups);
            }
        }, ctx).run();
    }
}
