// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.server.core.io.netty;

import com.hypixel.hytale.common.util.FormatUtil;
import io.netty.handler.timeout.WriteTimeoutException;
import io.netty.handler.timeout.ReadTimeoutException;
import io.netty.handler.timeout.TimeoutException;
import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.util.concurrent.atomic.AtomicBoolean;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import javax.annotation.Nonnull;
import io.netty.channel.ChannelHandlerContext;
import com.hypixel.hytale.server.core.HytaleServerConfig;
import java.time.Duration;
import io.netty.handler.codec.quic.QuicChannel;
import com.hypixel.hytale.server.core.io.PacketHandler;
import com.hypixel.hytale.server.core.io.handlers.InitialPacketHandler;
import com.hypixel.hytale.protocol.io.netty.PacketDecoder;
import com.hypixel.hytale.server.core.HytaleServer;
import io.netty.util.AttributeKey;
import com.hypixel.hytale.protocol.io.PacketStatsRecorder;
import com.hypixel.hytale.server.core.io.PacketStatsRecorderImpl;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
import com.hypixel.hytale.protocol.io.netty.ProtocolUtil;
import com.hypixel.hytale.protocol.packets.connection.Disconnect;
import com.hypixel.hytale.protocol.packets.connection.DisconnectType;
import io.netty.channel.ChannelHandler;
import com.hypixel.hytale.protocol.io.netty.PacketEncoder;
import com.hypixel.hytale.server.core.io.transport.QUICTransport;
import java.util.logging.Level;
import com.hypixel.hytale.logger.HytaleLogger;
import io.netty.handler.codec.quic.QuicStreamChannel;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;

public class HytaleChannelInitializer extends ChannelInitializer<Channel>
{
    @Override
    protected void initChannel(final Channel channel) {
        if (channel instanceof final QuicStreamChannel quicStreamChannel) {
            HytaleLogger.getLogger().at(Level.INFO).log("Received stream %s to %s", NettyUtil.formatRemoteAddress(channel), NettyUtil.formatLocalAddress(channel));
            final QuicChannel parentChannel = quicStreamChannel.parent();
            final Integer rejectErrorCode = parentChannel.attr(QUICTransport.ALPN_REJECT_ERROR_CODE_ATTR).get();
            if (rejectErrorCode != null) {
                HytaleLogger.getLogger().at(Level.INFO).log("Rejecting stream from %s: client outdated (ALPN mismatch)", NettyUtil.formatRemoteAddress(channel));
                channel.config().setAutoRead(false);
                channel.pipeline().addLast("packetEncoder", new PacketEncoder());
                channel.writeAndFlush(new Disconnect("Your game client needs to be updated.", DisconnectType.Disconnect)).addListener(future -> channel.eventLoop().schedule(() -> ProtocolUtil.closeApplicationConnection(channel, rejectErrorCode), 100L, TimeUnit.MILLISECONDS));
                return;
            }
            final X509Certificate clientCert = parentChannel.attr(QUICTransport.CLIENT_CERTIFICATE_ATTR).get();
            if (clientCert != null) {
                channel.attr(QUICTransport.CLIENT_CERTIFICATE_ATTR).set(clientCert);
                HytaleLogger.getLogger().at(Level.FINE).log("Copied client certificate to stream: %s", clientCert.getSubjectX500Principal().getName());
            }
        }
        else {
            HytaleLogger.getLogger().at(Level.INFO).log("Received connection from %s to %s", NettyUtil.formatRemoteAddress(channel), NettyUtil.formatLocalAddress(channel));
        }
        final PacketStatsRecorderImpl statsRecorder = new PacketStatsRecorderImpl();
        channel.attr((AttributeKey<PacketStatsRecorderImpl>)PacketStatsRecorder.CHANNEL_KEY).set(statsRecorder);
        final Duration initialTimeout = HytaleServer.get().getConfig().getConnectionTimeouts().getInitial();
        channel.attr(ProtocolUtil.PACKET_TIMEOUT_KEY).set(initialTimeout);
        channel.pipeline().addLast("packetDecoder", new PacketDecoder());
        final HytaleServerConfig.RateLimitConfig rateLimitConfig = HytaleServer.get().getConfig().getRateLimitConfig();
        if (rateLimitConfig.isEnabled()) {
            channel.pipeline().addLast("rateLimit", new RateLimitHandler(rateLimitConfig.getBurstCapacity(), rateLimitConfig.getPacketsPerSecond()));
        }
        channel.pipeline().addLast("packetEncoder", new PacketEncoder());
        channel.pipeline().addLast("packetArrayEncoder", NettyUtil.PACKET_ARRAY_ENCODER_INSTANCE);
        if (NettyUtil.PACKET_LOGGER.getLevel() != Level.OFF) {
            channel.pipeline().addLast("logger", NettyUtil.LOGGER);
        }
        final InitialPacketHandler playerConnection = new InitialPacketHandler(channel);
        channel.pipeline().addLast("handler", new PlayerChannelHandler(playerConnection));
        channel.pipeline().addLast(new ExceptionHandler());
        playerConnection.registered(null);
    }
    
    @Override
    public void exceptionCaught(@Nonnull final ChannelHandlerContext ctx, final Throwable cause) {
        HytaleLogger.getLogger().at(Level.WARNING).withCause(cause).log("Got exception from netty pipeline in HytaleChannelInitializer!");
        if (ctx.channel().isWritable()) {
            ctx.channel().writeAndFlush(new Disconnect("Internal server error!", DisconnectType.Crash)).addListener((GenericFutureListener<? extends Future<? super Void>>)ProtocolUtil.CLOSE_ON_COMPLETE);
        }
        else {
            ProtocolUtil.closeApplicationConnection(ctx.channel());
        }
    }
    
    @Override
    public void channelInactive(@Nonnull final ChannelHandlerContext ctx) throws Exception {
        ProtocolUtil.closeApplicationConnection(ctx.channel());
        super.channelInactive(ctx);
    }
    
    private static class ExceptionHandler extends ChannelInboundHandlerAdapter
    {
        private static final HytaleLogger LOGGER;
        private final AtomicBoolean handled;
        
        private ExceptionHandler() {
            this.handled = new AtomicBoolean();
        }
        
        @Override
        public void exceptionCaught(@Nonnull final ChannelHandlerContext ctx, final Throwable cause) {
            if (cause instanceof ClosedChannelException) {
                return;
            }
            final ChannelHandler handler = ctx.pipeline().get("handler");
            String identifier;
            if (handler instanceof final PlayerChannelHandler playerChannelHandler) {
                identifier = playerChannelHandler.getHandler().getIdentifier();
            }
            else {
                identifier = NettyUtil.formatRemoteAddress(ctx.channel());
            }
            if (this.handled.getAndSet(true)) {
                if (cause instanceof IOException && cause.getMessage() != null) {
                    final String message = cause.getMessage();
                    switch (message) {
                        case "Broken pipe":
                        case "Connection reset by peer":
                        case "An existing connection was forcibly closed by the remote host": {
                            return;
                        }
                    }
                }
                ExceptionHandler.LOGGER.at(Level.WARNING).withCause(cause).log("Already handled exception in ExceptionHandler but got another!");
                return;
            }
            if (cause instanceof TimeoutException) {
                this.handleTimeout(ctx, cause, identifier);
                return;
            }
            ExceptionHandler.LOGGER.at(Level.SEVERE).withCause(cause).log("Got exception from netty pipeline in ExceptionHandler: %s", cause.getMessage());
            this.gracefulDisconnect(ctx, identifier, "Internal server error!");
        }
        
        private void handleTimeout(@Nonnull final ChannelHandlerContext ctx, final Throwable cause, final String identifier) {
            final boolean readTimeout = cause instanceof ReadTimeoutException;
            final boolean writeTimeout = cause instanceof WriteTimeoutException;
            final String timeoutType = readTimeout ? "Read" : (writeTimeout ? "Write" : "Connection");
            final NettyUtil.TimeoutContext context = ctx.channel().attr(NettyUtil.TimeoutContext.KEY).get();
            final String stage = (context != null) ? context.stage() : "unknown";
            final String duration = (context != null) ? FormatUtil.nanosToString(System.nanoTime() - context.connectionStartNs()) : "unknown";
            ExceptionHandler.LOGGER.at(Level.INFO).log("%s timeout for %s at stage '%s' after %s connected", timeoutType, identifier, stage, duration);
            NettyUtil.CONNECTION_EXCEPTION_LOGGER.at(Level.FINE).withCause(cause).log("%s timeout for %s at stage '%s' after %s connected", timeoutType, identifier, stage, duration);
            this.gracefulDisconnect(ctx, identifier, timeoutType + " timeout");
        }
        
        private void gracefulDisconnect(@Nonnull final ChannelHandlerContext ctx, final String identifier, final String reason) {
            final Channel channel = ctx.channel();
            if (channel.isWritable()) {
                channel.writeAndFlush(new Disconnect(reason, DisconnectType.Disconnect)).addListener(future -> ProtocolUtil.closeApplicationConnection(channel, 4));
                channel.eventLoop().schedule(() -> {
                    if (channel.isOpen()) {
                        ExceptionHandler.LOGGER.at(Level.FINE).log("Force closing %s after graceful disconnect attempt", identifier);
                        ProtocolUtil.closeApplicationConnection(channel, 4);
                    }
                }, 1L, TimeUnit.SECONDS);
            }
            else {
                ProtocolUtil.closeApplicationConnection(channel, 4);
            }
        }
        
        static {
            LOGGER = HytaleLogger.forEnclosingClass();
        }
    }
}
