/*
 * Copyright 2018 Netflix, Inc.
 *
 *      Licensed under the Apache License, Version 2.0 (the "License");
 *      you may not use this file except in compliance with the License.
 *      You may obtain a copy of the License at
 *
 *          http://www.apache.org/licenses/LICENSE-2.0
 *
 *      Unless required by applicable law or agreed to in writing, software
 *      distributed under the License is distributed on an "AS IS" BASIS,
 *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *      See the License for the specific language governing permissions and
 *      limitations under the License.
 */

package com.netflix.netty.common;

import com.netflix.spectator.api.Counter;
import com.netflix.zuul.passport.CurrentPassport;
import com.netflix.zuul.passport.PassportState;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.timeout.ReadTimeoutHandler;

import java.util.concurrent.TimeUnit;

/**
 * This handler times from the point a HttpRequest is read until the LastHttpContent is read,
 * and fires a HttpRequestTimeoutEvent if that time has exceed the configured timeout.
 *
 * Unlike ReadTimeoutHandler, this impl does NOT close the channel on a timeout. Only fires the
 * event.
 *
 * @author michaels
 */
public class HttpRequestReadTimeoutHandler extends ChannelInboundHandlerAdapter
{
    private static final String HANDLER_NAME = "http_request_read_timeout_handler";
    private static final String INTERNAL_HANDLER_NAME = "http_request_read_timeout_internal";

    private final long timeout;
    private final TimeUnit unit;
    private final Counter httpRequestReadTimeoutCounter;

    protected HttpRequestReadTimeoutHandler(long timeout, TimeUnit unit, Counter httpRequestReadTimeoutCounter)
    {
        this.timeout = timeout;
        this.unit = unit;
        this.httpRequestReadTimeoutCounter = httpRequestReadTimeoutCounter;
    }

    /**
     * Factory which ensures that this handler is added to the pipeline using the
     * correct name.
     */
    public static void addLast(ChannelPipeline pipeline, long timeout, TimeUnit unit, Counter httpRequestReadTimeoutCounter)
    {
        HttpRequestReadTimeoutHandler handler = new HttpRequestReadTimeoutHandler(timeout, unit, httpRequestReadTimeoutCounter);
        pipeline.addLast(HANDLER_NAME, handler);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
    {
        if (msg instanceof LastHttpContent) {
            removeInternalHandler(ctx);
        }
        else if (msg instanceof HttpRequest) {
            // Start timeout handler.
            InternalReadTimeoutHandler handler = new InternalReadTimeoutHandler(timeout, unit);
            ctx.pipeline().addBefore(HANDLER_NAME, INTERNAL_HANDLER_NAME, handler);
        }

        super.channelRead(ctx, msg);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception
    {
        if (evt instanceof HttpRequestReadTimeoutEvent) {
            CurrentPassport.fromChannel(ctx.channel()).add(PassportState.IN_REQ_READ_TIMEOUT);
            removeInternalHandler(ctx);
            httpRequestReadTimeoutCounter.increment();
        }

        super.userEventTriggered(ctx, evt);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception
    {
        removeInternalHandler(ctx);
        super.handlerRemoved(ctx);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception
    {
        removeInternalHandler(ctx);
        super.channelInactive(ctx);
    }

    protected void removeInternalHandler(ChannelHandlerContext ctx)
    {
        // Remove timeout handler if not already removed.
        ChannelHandlerContext handlerContext = ctx.pipeline().context(INTERNAL_HANDLER_NAME);
        if (handlerContext != null && ! handlerContext.isRemoved()) {
            ctx.pipeline().remove(INTERNAL_HANDLER_NAME);
        }
    }


    static class InternalReadTimeoutHandler extends ReadTimeoutHandler
    {
        public InternalReadTimeoutHandler(long timeout, TimeUnit unit)
        {
            super(timeout, unit);
        }

        @Override
        protected void readTimedOut(ChannelHandlerContext ctx) throws Exception
        {
            ctx.fireUserEventTriggered(HttpRequestReadTimeoutEvent.INSTANCE);
        }
    }
}
