/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.flink.runtime.rest.handler.router;

import org.apache.flink.runtime.rest.handler.AbstractRestHandler;
import org.apache.flink.runtime.rest.handler.util.HandlerUtils;
import org.apache.flink.runtime.rest.messages.ErrorResponseBody;

import org.apache.flink.shaded.netty4.io.netty.channel.ChannelHandler;
import org.apache.flink.shaded.netty4.io.netty.channel.ChannelHandlerContext;
import org.apache.flink.shaded.netty4.io.netty.channel.ChannelInboundHandler;
import org.apache.flink.shaded.netty4.io.netty.channel.ChannelPipeline;
import org.apache.flink.shaded.netty4.io.netty.channel.SimpleChannelInboundHandler;
import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.DefaultFullHttpResponse;
import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpHeaders;
import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpMethod;
import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpRequest;
import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpResponseStatus;
import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpVersion;
import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.QueryStringDecoder;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

import static java.util.Objects.requireNonNull;

/**
 * Inbound handler that converts HttpRequest to Routed and passes Routed to the matched handler.
 *
 * <p>This class replaces the standard error response to be identical with those sent by the {@link AbstractRestHandler}.
 *
 * <p>This class is based on:
 * https://github.com/sinetja/netty-router/blob/1.10/src/main/java/io/netty/handler/codec/http/router/AbstractHandler.java
 * https://github.com/sinetja/netty-router/blob/1.10/src/main/java/io/netty/handler/codec/http/router/Handler.java
 */
public class RouterHandler extends SimpleChannelInboundHandler<HttpRequest> {
	private static final String ROUTER_HANDLER_NAME = RouterHandler.class.getName() + "_ROUTER_HANDLER";
	private static final String ROUTED_HANDLER_NAME = RouterHandler.class.getName() + "_ROUTED_HANDLER";

	private static final Logger LOG = LoggerFactory.getLogger(RouterHandler.class);

	private final Map<String, String> responseHeaders;
	private final Router router;

	public RouterHandler(Router router, final Map<String, String> responseHeaders) {
		this.router = requireNonNull(router);
		this.responseHeaders = requireNonNull(responseHeaders);
	}

	public String getName() {
		return ROUTER_HANDLER_NAME;
	}

	@Override
	protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpRequest httpRequest) {
		if (HttpHeaders.is100ContinueExpected(httpRequest)) {
			channelHandlerContext.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE));
			return;
		}

		// Route
		HttpMethod method = httpRequest.getMethod();
		QueryStringDecoder qsd = new QueryStringDecoder(httpRequest.uri());
		RouteResult<?> routeResult = router.route(method, qsd.path(), qsd.parameters());

		if (routeResult == null) {
			respondNotFound(channelHandlerContext, httpRequest);
			return;
		}

		routed(channelHandlerContext, routeResult, httpRequest);
	}

	private void routed(
			ChannelHandlerContext channelHandlerContext,
			RouteResult<?> routeResult,
			HttpRequest httpRequest) {
		ChannelInboundHandler handler = (ChannelInboundHandler) routeResult.target();

		// The handler may have been added (keep alive)
		ChannelPipeline pipeline     = channelHandlerContext.pipeline();
		ChannelHandler addedHandler = pipeline.get(ROUTED_HANDLER_NAME);
		if (handler != addedHandler) {
			if (addedHandler == null) {
				pipeline.addAfter(ROUTER_HANDLER_NAME, ROUTED_HANDLER_NAME, handler);
			} else {
				pipeline.replace(addedHandler, ROUTED_HANDLER_NAME, handler);
			}
		}

		RoutedRequest<?> request = new RoutedRequest<>(routeResult, httpRequest);
		channelHandlerContext.fireChannelRead(request.retain());
	}

	private void respondNotFound(ChannelHandlerContext channelHandlerContext, HttpRequest request) {
		LOG.trace("Request could not be routed to any handler. Uri:{} Method:{}", request.getUri(), request.getMethod());
		HandlerUtils.sendErrorResponse(
			channelHandlerContext,
			request,
			new ErrorResponseBody("Not found."),
			HttpResponseStatus.NOT_FOUND,
			responseHeaders);
	}
}