/*
 * Copyright (c) 2019 the Eclipse Milo Authors
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */

package org.eclipse.milo.opcua.stack.server.transport.http;

import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Objects;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.ClientAuth;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.eclipse.milo.opcua.stack.core.Stack;
import org.eclipse.milo.opcua.stack.core.util.EndpointUtil;
import org.eclipse.milo.opcua.stack.server.UaStackServer;
import org.eclipse.milo.opcua.stack.server.transport.RateLimitingHandler;
import org.eclipse.milo.opcua.stack.server.transport.websocket.OpcServerWebSocketFrameHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OpcServerHttpChannelInitializer extends ChannelInitializer<SocketChannel> {

    private SslContext sslContext = null;

    private final UaStackServer stackServer;

    public OpcServerHttpChannelInitializer(UaStackServer stackServer) {
        this.stackServer = stackServer;

        KeyPair keyPair = stackServer.getConfig().getHttpsKeyPair().orElse(null);
        X509Certificate[] httpsCertificate = stackServer.getConfig().getHttpsCertificateChain().orElse(null);

        if (keyPair != null && httpsCertificate != null) {
            try {
                PrivateKey privateKey = keyPair.getPrivate();

                sslContext = SslContextBuilder
                    .forServer(privateKey, httpsCertificate)
                    .clientAuth(ClientAuth.NONE)
                    .trustManager(InsecureTrustManagerFactory.INSTANCE)
                    .build();
            } catch (Exception e) {
                LoggerFactory.getLogger(OpcServerHttpChannelInitializer.class)
                    .error("Error configuration SslContext: {}", e.getMessage(), e);
            }
        } else {
            LoggerFactory.getLogger(OpcServerHttpChannelInitializer.class)
                .warn("HTTPS KeyPair and/or Certificate not configured; falling back to plaintext...");
        }
    }


    @Override
    protected void initChannel(SocketChannel channel) {
        stackServer.registerConnectedChannel(channel);

        channel.closeFuture().addListener(future -> stackServer.unregisterConnectedChannel(channel));

        channel.pipeline().addLast(RateLimitingHandler.getInstance());

        if (sslContext != null) {
            channel.pipeline().addLast(sslContext.newHandler(channel.alloc()));
        }

        channel.pipeline().addLast(new LoggingHandler(LogLevel.TRACE));
        channel.pipeline().addLast(new HttpServerCodec());

        // TODO configure maxContentLength based on MaxRequestSize?
        channel.pipeline().addLast(new HttpObjectAggregator(Integer.MAX_VALUE));
        channel.pipeline().addLast(new OpcHttpTransportInterceptor(stackServer));
    }

    private static class OpcHttpTransportInterceptor extends SimpleChannelInboundHandler<FullHttpRequest> {

        private final Logger logger = LoggerFactory.getLogger(getClass());

        private final UaStackServer stackServer;

        public OpcHttpTransportInterceptor(UaStackServer stackServer) {
            this.stackServer = stackServer;
        }

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest httpRequest) {
            String host = httpRequest.headers().get(HttpHeaderNames.HOST);
            String uri = httpRequest.uri();

            logger.debug("host={} uri={}", host, uri);

            boolean endpointMatch = stackServer.getEndpointDescriptions()
                .stream()
                .anyMatch(endpoint ->
                    Objects.equals(
                        uri,
                        EndpointUtil.getPath(endpoint.getEndpointUrl()))
                );

            if (!endpointMatch) {
                logger.debug("unrecognized endpoint URL: " + uri);

                HttpResponse response = new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1,
                    HttpResponseStatus.NOT_FOUND
                );

                ctx.channel().writeAndFlush(response)
                    .addListener(future -> ctx.close());

                return;
            }

            if (Objects.equals(httpRequest.method(), HttpMethod.GET) &&
                "websocket".equalsIgnoreCase(httpRequest.headers().get(HttpHeaderValues.UPGRADE))) {

                logger.debug("intercepted WebSocket upgrade");

                ctx.channel().pipeline().remove(this);

                ctx.channel().pipeline().addLast(new WebSocketServerCompressionHandler());

                // TODO configure webSocketPath based on path component of endpoint URL?
                ctx.channel().pipeline().addLast(new WebSocketServerProtocolHandler(
                    "/ws",
                    String.format("%s, %s", Stack.WSS_PROTOCOL_BINARY, Stack.WSS_PROTOCOL_JSON),
                    true
                ));

                ctx.channel().pipeline().addLast(new OpcServerWebSocketFrameHandler(stackServer));

                httpRequest.retain();
                ctx.executor().execute(() -> ctx.fireChannelRead(httpRequest));
            } else if (Objects.equals(httpRequest.method(), HttpMethod.POST)) {
                logger.debug("intercepted HTTP POST");

                ctx.channel().pipeline().remove(this);
                ctx.channel().pipeline().addLast(new OpcServerHttpRequestHandler(stackServer));

                httpRequest.retain();
                ctx.executor().execute(() -> ctx.pipeline().fireChannelRead(httpRequest));
            } else {
                HttpResponse response = new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1,
                    HttpResponseStatus.BAD_REQUEST
                );

                ctx.channel().writeAndFlush(response)
                    .addListener(future -> ctx.close());
            }
        }
    }

}
