package gcbatch.svc.reverseproxy;

import gcbatch.base.exception.BusinessException;
import gcbatch.core.gccmd.ChannelContext;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.util.ReferenceCounted;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

public class HttpReserveProxyHandler extends SimpleChannelInboundHandler<HttpObject> {
    private Vector<RouteRule> routes;
    private ChannelContext ctx;
    private Logger log;

    private String channelName;
    private final Map<String, ChannelFuture> backChannelMap = new HashMap<>();

    private ChannelFuture backChannelFuture;

    public HttpReserveProxyHandler(ChannelContext ctx, Vector<RouteRule> routes) {
        this.ctx = ctx;
        this.channelName = ctx.getName();
        this.log = ctx.getLogger();
        this.routes = routes;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject httpObject) {
        if(httpObject instanceof HttpRequest) {
            HttpRequest request = (HttpRequest) httpObject;
            log.debug(channelName + ": from client message:" + request.toString());
            String srcUrl = request.uri();

            Channel channel = channelHandlerContext.channel();
            InetSocketAddress remoteAddress = ((InetSocketAddress) channelHandlerContext.channel().remoteAddress());
            String host = remoteAddress.getAddress().getHostAddress();

            RouteRule route = RouteRule.chooseRoute(routes, host, srcUrl);
            if(route == null) {
                DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR, Unpooled.copiedBuffer( "no route pattern matched!".getBytes(StandardCharsets.UTF_8)));
                channelHandlerContext.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
                backChannelFuture = null;
                return;
            }

            String tgtUrl = route.getTargetUrl(srcUrl);

            request.setUri(tgtUrl);
            request.headers().set(HttpHeaderNames.HOST, route.getTargetHost() + ":" + route.getTargetPort());

            ChannelFuture backChannel = backChannelMap.get(route.getName());
            if(backChannel == null) {
                Bootstrap b = new Bootstrap();
                b.group(channel.eventLoop()).channel(channel.getClass()).handler(
                        new ChannelInitializer<SocketChannel>() {
                            @Override public void initChannel(SocketChannel innerCh) {
                                innerCh.pipeline().addLast(new HttpClientCodec(), new HttpReserveProxyBackHandler(ctx, channel, route));
                            }
                        });// .option(ChannelOption.AUTO_READ, false);
                backChannelFuture = b.connect(route.getTargetHost(), route.getTargetPort());
                backChannelMap.put(route.getName(), backChannelFuture);
            } else {
                backChannelFuture = backChannel;
            }
        }

        if(backChannelFuture != null) {
            addHttpObjectWrite(httpObject);
        } else {
            DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR, Unpooled.copiedBuffer("ERR!".getBytes(StandardCharsets.UTF_8)));
            channelHandlerContext.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext channelHandlerContext) {
        log.info("channel colse!");
        for(ChannelFuture backChannel: backChannelMap.values()) {
            if(backChannel != null && backChannel.channel().isActive()) backChannel.channel().close();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext channelHandlerContext, Throwable cause) {
        for(ChannelFuture backChannel : backChannelMap.values()) {
            if(backChannel != null && backChannel.channel().isActive()) backChannel.channel().close();
        }

        if(cause instanceof IOException) log.error(channelName + ": err: " + cause.getMessage());
        else log.error(channelName + ": http reserveProxy exception!", cause);

        DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR, Unpooled.copiedBuffer( "route error!".getBytes(StandardCharsets.UTF_8)));
        channelHandlerContext.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    /**
     * proxy backend
     */
    private class HttpReserveProxyBackHandler extends SimpleChannelInboundHandler<HttpObject> {
        private String name;
        private Logger log;
        private RouteRule route;

        private final Channel inboundChannel;

        public HttpReserveProxyBackHandler(ChannelContext ctx,Channel inboundChannel, RouteRule route) {
            this.log = ctx.getLogger();
            this.inboundChannel = inboundChannel;
            this.name = route.getName();
            this.route = route;
        }
        @Override
        public void channelActive(ChannelHandlerContext channelHandlerContext) {
            log.info(channelName + ": backend channel [" + this.name + "] active! ip:" + route.getTargetHost() + " port:" + route.getTargetPort());
        }

        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject httpResponse) {
            log.info(channelName + ": Target server returns data:" + httpResponse.toString());

            inboundChannel.writeAndFlush(getRetainedObject(httpResponse)).addListener((ChannelFutureListener) future -> {
                if (!future.isSuccess()) {
                    log.error(channelName + ": backend channel [" + this.name + "] write back error!", future.cause());
                }
            });
        }

        @Override
        public void channelInactive(ChannelHandlerContext channelHandlerContext) {
            log.info(channelName + ": backend channel [" + this.name + "] inactive!");
            backChannelMap.remove(name);
            if(backChannelMap.size() == 0) inboundChannel.close();
            closeOnFlush(inboundChannel);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext channelHandlerContext, Throwable cause) {
            log.error(channelName + ": http reserveProxy backend  [" + this.name + "]  exception!", cause);
            closeOnFlush(channelHandlerContext.channel());
        }
    }

    private HttpObject getRetainedObject(HttpObject obj) {
        if(obj instanceof ReferenceCounted) return (HttpObject) ((ReferenceCounted) obj).retain();
        return obj;
    }


//    private void addHttpObjectWrite(HttpObject httpObject) {
//        backChannelFuture.addListener((ChannelFutureListener) f -> {
//            if (f.isSuccess()) {
//                if(httpObject instanceof LastHttpContent) f.channel().writeAndFlush(getRetainedObject(httpObject));
//                else f.channel().write(getRetainedObject(httpObject));
//            } else {
//                log.error(f.cause());
//            }
//        });
//    }
    private void addHttpObjectWrite(HttpObject httpObject) {
        HttpObject retainedHttpObject = getRetainedObject(httpObject);

        backChannelFuture.addListener((ChannelFutureListener) f -> {
            if (f.isSuccess()) {
                if(retainedHttpObject instanceof LastHttpContent) f.channel().writeAndFlush(retainedHttpObject);
                else f.channel().write(getRetainedObject(retainedHttpObject));
            } else {
                log.error(f.cause());
            }
        });
    }
    private static void closeOnFlush(Channel ch) {
        // if(ch.isActive())
        ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
    }
}
