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

package com.hypixel.hytale.server.core.io.handlers.login;

import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;
import com.hypixel.hytale.server.core.auth.PlayerAuthentication;
import java.security.SecureRandom;
import com.hypixel.hytale.protocol.packets.auth.PasswordRejected;
import com.hypixel.hytale.protocol.packets.auth.PasswordAccepted;
import java.security.MessageDigest;
import com.hypixel.hytale.protocol.io.netty.ProtocolUtil;
import com.hypixel.hytale.protocol.packets.auth.PasswordResponse;
import com.hypixel.hytale.protocol.packets.connection.Disconnect;
import com.hypixel.hytale.protocol.Packet;
import com.hypixel.hytale.server.core.HytaleServerConfig;
import java.util.logging.Level;
import com.hypixel.hytale.server.core.HytaleServer;
import com.hypixel.hytale.server.core.io.PacketHandler;
import com.hypixel.hytale.server.core.io.netty.NettyUtil;
import javax.annotation.Nullable;
import com.hypixel.hytale.server.core.io.ProtocolVersion;
import javax.annotation.Nonnull;
import io.netty.channel.Channel;
import com.hypixel.hytale.protocol.HostAddress;
import java.util.UUID;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.server.core.io.handlers.GenericConnectionPacketHandler;

public class PasswordPacketHandler extends GenericConnectionPacketHandler
{
    private static final HytaleLogger LOGGER;
    private static final int MAX_PASSWORD_ATTEMPTS = 3;
    private static final int CHALLENGE_LENGTH = 32;
    private final UUID playerUuid;
    private final String username;
    private final byte[] referralData;
    private final HostAddress referralSource;
    private byte[] passwordChallenge;
    private final SetupHandlerSupplier setupHandlerSupplier;
    private int attemptsRemaining;
    
    public PasswordPacketHandler(@Nonnull final Channel channel, @Nonnull final ProtocolVersion protocolVersion, @Nonnull final String language, @Nonnull final UUID playerUuid, @Nonnull final String username, @Nullable final byte[] referralData, @Nullable final HostAddress referralSource, @Nullable final byte[] passwordChallenge, @Nonnull final SetupHandlerSupplier setupHandlerSupplier) {
        super(channel, protocolVersion, language);
        this.attemptsRemaining = 3;
        this.playerUuid = playerUuid;
        this.username = username;
        this.referralData = referralData;
        this.referralSource = referralSource;
        this.passwordChallenge = passwordChallenge;
        this.setupHandlerSupplier = setupHandlerSupplier;
    }
    
    @Nonnull
    @Override
    public String getIdentifier() {
        return "{Password(" + NettyUtil.formatRemoteAddress(this.channel) + "), " + this.username;
    }
    
    public void registered0(final PacketHandler oldHandler) {
        final HytaleServerConfig.TimeoutProfile timeouts = HytaleServer.get().getConfig().getConnectionTimeouts();
        if (this.passwordChallenge == null || this.passwordChallenge.length == 0) {
            PasswordPacketHandler.LOGGER.at(Level.FINE).log("No password required for %s, proceeding to setup", this.username);
            this.proceedToSetup();
        }
        else {
            PasswordPacketHandler.LOGGER.at(Level.FINE).log("Waiting for password response from %s", this.username);
            this.enterStage("password", timeouts.getPassword(), () -> !this.registered);
        }
    }
    
    @Override
    public void accept(@Nonnull final Packet packet) {
        switch (packet.getId()) {
            case 1: {
                this.handle((Disconnect)packet);
                break;
            }
            case 15: {
                this.handle((PasswordResponse)packet);
                break;
            }
            default: {
                this.disconnect("Protocol error: unexpected packet " + packet.getId());
                break;
            }
        }
    }
    
    public void handle(@Nonnull final Disconnect packet) {
        this.disconnectReason.setClientDisconnectType(packet.type);
        PasswordPacketHandler.LOGGER.at(Level.INFO).log("%s (%s) at %s left with reason: %s - %s", this.playerUuid, this.username, NettyUtil.formatRemoteAddress(this.channel), packet.type.name(), packet.reason);
        ProtocolUtil.closeApplicationConnection(this.channel);
    }
    
    public void handle(@Nonnull final PasswordResponse packet) {
        this.clearTimeout();
        if (this.passwordChallenge == null || this.passwordChallenge.length == 0) {
            PasswordPacketHandler.LOGGER.at(Level.WARNING).log("Received unexpected PasswordResponse from %s - no password required", NettyUtil.formatRemoteAddress(this.channel));
            this.disconnect("Protocol error: unexpected PasswordResponse");
            return;
        }
        final byte[] clientHash = packet.hash;
        if (clientHash == null || clientHash.length == 0) {
            PasswordPacketHandler.LOGGER.at(Level.WARNING).log("Received empty password hash from %s", NettyUtil.formatRemoteAddress(this.channel));
            this.disconnect("Invalid password response");
            return;
        }
        final String password = HytaleServer.get().getConfig().getPassword();
        if (password == null || password.isEmpty()) {
            PasswordPacketHandler.LOGGER.at(Level.SEVERE).log("Password validation failed - no password configured but challenge was sent");
            this.disconnect("Server configuration error");
            return;
        }
        final byte[] expectedHash = computePasswordHash(this.passwordChallenge, password);
        if (expectedHash == null) {
            PasswordPacketHandler.LOGGER.at(Level.SEVERE).log("Failed to compute password hash");
            this.disconnect("Server error");
            return;
        }
        if (MessageDigest.isEqual(expectedHash, clientHash)) {
            PasswordPacketHandler.LOGGER.at(Level.INFO).log("Password accepted for %s (%s)", this.username, this.playerUuid);
            this.write(new PasswordAccepted());
            this.proceedToSetup();
            return;
        }
        --this.attemptsRemaining;
        PasswordPacketHandler.LOGGER.at(Level.WARNING).log("Invalid password from %s (%s), %d attempts remaining", this.username, NettyUtil.formatRemoteAddress(this.channel), this.attemptsRemaining);
        if (this.attemptsRemaining <= 0) {
            this.disconnect("Too many failed password attempts");
            return;
        }
        this.passwordChallenge = generateChallenge();
        this.write(new PasswordRejected(this.passwordChallenge, this.attemptsRemaining));
        final HytaleServerConfig.TimeoutProfile timeouts = HytaleServer.get().getConfig().getConnectionTimeouts();
        this.continueStage("password", timeouts.getPassword(), () -> !this.registered);
    }
    
    private static byte[] generateChallenge() {
        final byte[] challenge = new byte[32];
        new SecureRandom().nextBytes(challenge);
        return challenge;
    }
    
    private void proceedToSetup() {
        this.auth = new PlayerAuthentication(this.playerUuid, this.username);
        if (this.referralData != null) {
            this.auth.setReferralData(this.referralData);
        }
        if (this.referralSource != null) {
            this.auth.setReferralSource(this.referralSource);
        }
        PasswordPacketHandler.LOGGER.at(Level.INFO).log("Connection complete for %s (%s), transitioning to setup", this.username, this.playerUuid);
        NettyUtil.setChannelHandler(this.channel, this.setupHandlerSupplier.create(this.channel, this.protocolVersion, this.language, this.auth));
    }
    
    @Nullable
    private static byte[] computePasswordHash(final byte[] challenge, final String password) {
        try {
            final MessageDigest digest = MessageDigest.getInstance("SHA-256");
            digest.update(challenge);
            digest.update(password.getBytes(StandardCharsets.UTF_8));
            return digest.digest();
        }
        catch (final NoSuchAlgorithmException e) {
            return null;
        }
    }
    
    static {
        LOGGER = HytaleLogger.forEnclosingClass();
    }
    
    @FunctionalInterface
    public interface SetupHandlerSupplier
    {
        PacketHandler create(final Channel p0, final ProtocolVersion p1, final String p2, final PlayerAuthentication p3);
    }
}
