package client.network;

import java.security.KeyPair;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.Base64;

import client.Client;
import client.gui.GuiConfirm;
import client.gui.GuiConnect;
import client.gui.GuiConnect.ServerInfo;
import common.net.util.concurrent.Future;
import common.net.util.concurrent.GenericFutureListener;
import common.network.IClientLoginHandler;
import common.network.NetConnection;
import common.network.PacketRegistry;
import common.packet.LPacketChallenge;
import common.packet.LPacketPassword;
import common.packet.LPacketPubkey;
import common.packet.LPacketResponse;
import common.packet.LPacketStartEncrypt;
import common.packet.RPacketChallenge;
import common.packet.RPacketDisconnect;
import common.packet.RPacketEnableCompression;
import common.packet.RPacketLoginSuccess;
import common.packet.RPacketRequestEncrypt;
import common.packet.RPacketResponse;
import common.packet.RPacketServerConfig;
import common.util.EncryptUtil;
import common.util.Util;

public class ClientLoginHandler implements IClientLoginHandler {
    private static enum LoginState {
        HANDSHAKE, CONFIRMING, CHALLENGE, ENCRYPTED, PROVING, AUTHENTICATING, DONE;
    }
    
    private static final SecureRandom TOKEN_RNG = new SecureRandom();
    
    private final Client gm;
	private final NetConnection connection;
    private final ServerInfo server;
    private final KeyPair tempKeys;
    
    private LoginState state = LoginState.HANDSHAKE;
    private byte[] token;
    
    public ClientLoginHandler(NetConnection conn, Client gm, ServerInfo server) {
    	this.connection = conn;
        this.gm = gm;
        this.server = server;
        this.tempKeys = EncryptUtil.createDHKeypair();
    }
    
    public void onDisconnect(String reason)
    {
    	this.gm.disconnected(reason);
    }

    public void handleDisconnect(RPacketDisconnect packet)
    {
        this.connection.closeChannel(packet.getReason());
    }
	
	public void handleEncrypt(RPacketRequestEncrypt packet) {
		if(this.state != LoginState.HANDSHAKE) {
			this.connection.closeChannel("Unerwartetes Verschlüsselungs-Paket");
			return;
		}
        this.connection.setConnectionState(PacketRegistry.LOGIN);
        final PublicKey pubkey = packet.getKey();
        final PublicKey tempKey = packet.getTempKey();
        if(this.server.getServerKey() == null) {
        	this.state = LoginState.CONFIRMING;
        	this.gm.schedule(new Runnable() {
				public void run() {
					ClientLoginHandler.this.gm.show(new GuiConfirm(new GuiConfirm.Callback() {
						public void confirm(boolean confirmed) {
							if(confirmed) {
								ClientLoginHandler.this.server.setServerKey(pubkey);
								GuiConnect.INSTANCE.editServer(ClientLoginHandler.this.server);
								ClientLoginHandler.this.gm.displayConnecting(ClientLoginHandler.this.server);
								ClientLoginHandler.this.startEncryption(tempKey);
							}
							else {
								ClientLoginHandler.this.connection.closeChannel("Verbindung wurde abgebrochen");
							}
						}
					}, "Die Identität des Servers ist unbekannt", "Es wurde noch nie mit diesem Server verbunden.\nSoll die Verbindung wirklich fortgesetzt werden?\n\nDie Pubkey-ID des Servers lautet:\n" + EncryptUtil.KEY_ALGO_DISPLAY + " " + EncryptUtil.getXorSha512Hash(pubkey.getEncoded()) + "\n\nDer öffentliche Schlüssel des Servers lautet:\n" + Util.breakString(Base64.getEncoder().encodeToString(pubkey.getEncoded()), 64), "Verbindung herstellen", "Abbrechen und trennen"));
				}
			});
        	return;
        }
        else if(!this.server.getServerKey().equals(pubkey)) {
    		this.connection.closeChannel("Die Identität des Servers hat sich geändert\n\nDer Server hat einen anderen öffentlichen Schlüssel als vorher bekannt war,\ndies kann bedeuten dass der Inhaber jetzt einen anderen Schlüssel verwendet\noder sich ein anderer Server als dieser ausgibt (\"Man-in-the-middle-attack\").\n\nDie Pubkey-ID des Servers lautet: " + EncryptUtil.KEY_ALGO_DISPLAY + " " + EncryptUtil.getXorSha512Hash(pubkey.getEncoded()) + "\n\nDer öffentliche Schlüssel des Servers lautet:\n" + Util.breakString(Base64.getEncoder().encodeToString(pubkey.getEncoded()), 64) + "\n\nDie vertrauenswürdige Pubkey-ID lautet: " + EncryptUtil.KEY_ALGO_DISPLAY + " " + EncryptUtil.getXorSha512Hash(this.server.getServerKey().getEncoded()) + 
    				"\n\nFalls der Server trotzdem vertrauenswürdig wirkt, kann die Server-Identifizierung\nin den Einstellungen von '" + this.server.getName() + "' zurückgesetzt werden.");
        	return;
        }
        this.startEncryption(tempKey);
	}
	
	private void startEncryption(PublicKey serverKey) {
        this.state = LoginState.CHALLENGE;
        this.connection.sendPacket(new LPacketStartEncrypt(this.tempKeys.getPublic()), new GenericFutureListener < Future <? super Void >> () {
            public void operationComplete(Future <? super Void > u) throws Exception {
                ClientLoginHandler.this.connection.startEncryption(EncryptUtil.makeKeyAgreement(ClientLoginHandler.this.tempKeys.getPrivate(), serverKey));
                ClientLoginHandler.this.token = new byte[32];
    			TOKEN_RNG.nextBytes(ClientLoginHandler.this.token);
                ClientLoginHandler.this.connection.sendPacket(new LPacketChallenge(ClientLoginHandler.this.token));
            }
        });
	}

	public void handleResponse(RPacketResponse packet) {
		if(this.state != LoginState.CHALLENGE) {
			this.connection.closeChannel("Unerwartetes Beweis-Paket");
			return;
		}
		if(!packet.verifyToken(this.server.getServerKey(), this.token)) {
			this.connection.closeChannel("Fehlerhaftes Beweis-Token");
			return;
		}
		this.state = LoginState.ENCRYPTED;
	}

	public void handleConfig(final RPacketServerConfig packet) {
		if(this.state == LoginState.HANDSHAKE) {
	        this.connection.setConnectionState(PacketRegistry.LOGIN);
	        if(this.server.requiresEncryption()) {
	        	this.connection.closeChannel("Der Server unterstützt keine verschlüsselte Verbindung, dies ist\nin den Servereinstellungen von '"
	        			+ this.server.getName() + "' als erforderlich\neingestellt, stelle keine ungesicherte Verbindung her.");
	    		return;
	        }
	        if(this.server.getServerKey() != null) {
	        	this.state = LoginState.CONFIRMING;
	        	this.gm.schedule(new Runnable() {
					public void run() {
						ClientLoginHandler.this.gm.show(new GuiConfirm(new GuiConfirm.Callback() {
							public void confirm(boolean confirmed) {
								if(confirmed) {
									ClientLoginHandler.this.server.setServerKey(null);
									GuiConnect.INSTANCE.editServer(ClientLoginHandler.this.server);
									ClientLoginHandler.this.gm.displayConnecting(ClientLoginHandler.this.server);
									ClientLoginHandler.this.state = LoginState.HANDSHAKE;
									ClientLoginHandler.this.handleConfig(packet);
								}
								else {
									ClientLoginHandler.this.connection.closeChannel("Verbindung wurde abgebrochen");
								}
							}
						}, "Der Server hat die Verschlüsselung deaktiviert", "Es wurde bereits verschlüsselt mit diesem Server kommuniziert.\n\nSoll die Verbindung wirklich fortgesetzt werden?\n\nDie Pubkey-Authentifizierung wird nicht verfügbar sein\nund die Server-ID wird ignoriert und zurückgesetzt.", "Unverschlüsselt verbinden", "Abbrechen und trennen"));
					}
				});
	        	return;
	        }
		}
		else if(this.state != LoginState.ENCRYPTED) {
			this.connection.closeChannel("Unerwartetes Konfigurations-Paket");
			return;
		}
		if(packet.hasAccessPassword() && this.server.getAccess().isEmpty()) {
			this.connection.closeChannel("Der Server verlangt ein Zugangs-Passwort zur Authentifizierung");
			return;
		}
        boolean auth = packet.isAuthenticating();
        boolean passwordAuth = packet.canUsePassword();
        boolean pubkeyAuth = packet.canUsePubkey() && this.state == LoginState.ENCRYPTED;
		if(auth && (!passwordAuth || this.server.getPassword().isEmpty()) && (!pubkeyAuth || this.server.getKeypair() == null)) {
			this.connection.closeChannel("Der Server unterstützt keine der vorhandenen\nAuthentifizierungsmethoden\n\nUnterstützt vom Server: " + 
					(!passwordAuth && !pubkeyAuth ? "Keine" : ((passwordAuth ? "Passwort" : "") + (passwordAuth && pubkeyAuth ? " und " : "") + (pubkeyAuth ? "Pubkey" : ""))) +
					"\n\nVorhanden in Konfiguration für '" + this.server.getName() + "': " + (this.server.getPassword().isEmpty() && this.server.getKeypair() == null ? "Keine" : 
					((!this.server.getPassword().isEmpty() ? "Passwort" : "") + 
					(!this.server.getPassword().isEmpty() && this.server.getKeypair() != null ? " und " : "") + (this.server.getKeypair() != null ? "Pubkey" : ""))));
			return;
		}
		if(auth && pubkeyAuth && this.server.getKeypair() != null) {
			this.state = LoginState.PROVING;
	        this.connection.sendPacket(new LPacketPubkey(this.server.getUser(), this.server.getAccess(), this.server.getKeypair().getPublic()));
		}
		else {
			this.state = LoginState.AUTHENTICATING;
	        this.connection.sendPacket(new LPacketPassword(this.server.getUser(), this.server.getAccess(), auth ? this.server.getPassword() : ""));
		}
	}

	public void handleChallenge(RPacketChallenge packet) {
		if(this.state != LoginState.PROVING) {
    		this.connection.closeChannel("Unerwartetes Anforderungs-Paket");
    		return;
    	}
		this.state = LoginState.AUTHENTICATING;
		this.connection.sendPacket(new LPacketResponse(this.server.getKeypair().getPrivate(), packet.getToken()));
	}

    public void handleLoginSuccess(RPacketLoginSuccess packet)
    {
    	if(this.state != LoginState.AUTHENTICATING) {
    		this.connection.closeChannel("Unerwartetes Bestätigungs-Paket");
    		return;
    	}
    	this.state = LoginState.DONE;
        this.connection.setConnectionState(PacketRegistry.PLAY);
        this.connection.setNetHandler(new ClientPlayer(this.gm, this.connection));
    }
	
	public void handleEnableCompression(RPacketEnableCompression packet)
    {
        this.connection.setCompressionTreshold(packet.getValue());
    }
}
