package protocolsupport.protocol.packet.handler;

import java.math.BigInteger;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.UUID;
import java.util.logging.Level;
import java.util.stream.Collectors;

import javax.crypto.SecretKey;

import com.google.common.base.Preconditions;

import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.EncryptionUtil;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.Util;
import net.md_5.bungee.api.AbstractReconnectHandler;
import net.md_5.bungee.api.Callback;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.LoginEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.event.PreLoginEvent;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.connection.LoginResult;
import net.md_5.bungee.connection.UpstreamBridge;
import net.md_5.bungee.http.HttpClient;
import net.md_5.bungee.jni.cipher.BungeeCipher;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.netty.cipher.CipherDecoder;
import net.md_5.bungee.netty.cipher.CipherEncoder;
import net.md_5.bungee.protocol.Protocol;
import net.md_5.bungee.protocol.packet.EncryptionRequest;
import net.md_5.bungee.protocol.packet.EncryptionResponse;
import net.md_5.bungee.protocol.packet.LoginRequest;
import net.md_5.bungee.protocol.packet.LoginSuccess;
import protocolsupport.api.ProtocolType;
import protocolsupport.api.ProtocolVersion;
import protocolsupport.api.events.PlayerLoginFinishEvent;
import protocolsupport.api.events.PlayerLoginStartEvent;
import protocolsupport.api.events.PlayerProfileCompleteEvent;
import protocolsupport.api.utils.Profile;
import protocolsupport.api.utils.ProfileProperty;
import protocolsupport.protocol.ConnectionImpl;
import protocolsupport.protocol.utils.GameProfile;

public class PSInitialHandler extends InitialHandler {

	public PSInitialHandler(BungeeCord bungee, ListenerInfo listener) {
		super(bungee, listener);
	}

	protected ChannelWrapper channel;
	protected ConnectionImpl connection;

	@Override
	public void connected(ChannelWrapper channel) throws Exception {
		super.connected(channel);
		this.channel = channel;
		this.connection = ConnectionImpl.getFromChannel(channel.getHandle());
	}

	public ChannelWrapper getChannelWrapper() {
		return channel;
	}

	protected LoginRequest loginRequest;

	@Override
	public LoginRequest getLoginRequest() {
		return loginRequest;
	}

	protected LoginResult loginProfile;

	@Override
	public LoginResult getLoginProfile() {
		return loginProfile;
	}

	protected LoginState state = LoginState.HELLO;

	protected String username;

	@Override
	public String getName() {
		return username;
	}

	protected void updateUsername(String newName, boolean original) {
		username = newName;
		if (original) {
			connection.getProfile().setOriginalName(newName);
		} else {
			connection.getProfile().setName(newName);
		}
	}

	protected UUID uuid;

	@Override
	public void setUniqueId(UUID uuid) {
		Preconditions.checkState((state == LoginState.HELLO) || (state == LoginState.ONLINEMODERESOLVE), "Can only set uuid while state is username");
		Preconditions.checkState(!isOnlineMode(), "Can only set uuid when online mode is false");
		updateUUID(uuid, true);
	}

	@Override
	public UUID getUniqueId() {
		return uuid;
	}

	@Override
	public String getUUID() {
		return getUniqueId().toString().replaceAll("-", "");
	}

	protected void updateUUID(UUID newUUID, boolean original) {
		uuid = newUUID;
		if (original) {
			connection.getProfile().setOriginalUUID(newUUID);
		} else {
			connection.getProfile().setUUID(newUUID);
		}
	}

	@Override
	public void setOnlineMode(boolean onlineMode) {
		Preconditions.checkState((state == LoginState.HELLO) || (state == LoginState.ONLINEMODERESOLVE), "Can only set uuid while state is username");
		connection.getProfile().setOnlineMode(onlineMode);
	}

	@Override
	public boolean isOnlineMode() {
		return connection.getProfile().isOnlineMode();
	}

	@Override
	public void handle(LoginRequest lLoginRequest) throws Exception {
		Preconditions.checkState(state == LoginState.HELLO, "Not expecting USERNAME");
		state = LoginState.ONLINEMODERESOLVE;

		loginRequest = lLoginRequest;

		updateUsername(lLoginRequest.getData(), true);

		if (getName().contains(".")) {
			disconnect(BungeeCord.getInstance().getTranslation("name_invalid"));
			return;
		}
		if (getName().length() > 16) {
			disconnect(BungeeCord.getInstance().getTranslation("name_too_long"));
			return;
		}
		int limit = BungeeCord.getInstance().config.getPlayerLimit();
		if ((limit > 0) && (BungeeCord.getInstance().getOnlineCount() > limit)) {
			disconnect(BungeeCord.getInstance().getTranslation("proxy_full"));
			return;
		}
		if (!isOnlineMode() && (BungeeCord.getInstance().getPlayer(getUniqueId()) != null)) {
			disconnect(BungeeCord.getInstance().getTranslation("already_connected_proxy"));
			return;
		}

		BungeeCord.getInstance().getPluginManager().callEvent(new PreLoginEvent(this, new Callback<PreLoginEvent>() {
			@Override
			public void done(PreLoginEvent result, Throwable error) {
				if (result.isCancelled()) {
					disconnect(result.getCancelReasonComponents());
					return;
				}
				if (!isConnected()) {
					return;
				}
				processLoginStart();
			}
		}));
	}

	protected EncryptionRequest request;

	protected void processLoginStart() {
		PlayerLoginStartEvent event = new PlayerLoginStartEvent(connection, getHandshake().getHost());
		ProxyServer.getInstance().getPluginManager().callEvent(event);
		if (event.isLoginDenied()) {
			disconnect(event.getDenyLoginMessage());
			return;
		}

		connection.getProfile().setOnlineMode(event.isOnlineMode());

		switch (connection.getVersion().getProtocolType()) {
			case PC: {
				if (isOnlineMode()) {
					state = LoginState.KEY;
					unsafe().sendPacket((request = EncryptionUtil.encryptRequest()));
				} else {
					offlineuuid = Profile.generateOfflineModeUUID(getName());
					updateUUID(offlineuuid, true);
					finishLogin();
				}
				return;
			}
			default: {
				throw new IllegalArgumentException(MessageFormat.format("Unknown protocol type {0}", connection.getVersion().getProtocolType()));
			}
		}
	}

	@Override
	public void handle(EncryptionResponse encryptResponse) throws Exception {
		Preconditions.checkState(state == LoginState.KEY, "Not expecting ENCRYPT");
		state = LoginState.AUTHENTICATING;
		SecretKey sharedKey = EncryptionUtil.getSecret(encryptResponse, request);
		BungeeCipher decrypt = EncryptionUtil.getCipher(false, sharedKey);
		channel.addBefore(PipelineUtils.FRAME_DECODER, PipelineUtils.DECRYPT_HANDLER, new CipherDecoder(decrypt));
		if (isFullEncryption(connection.getVersion())) {
			BungeeCipher encrypt = EncryptionUtil.getCipher(true, sharedKey);
			channel.addBefore(PipelineUtils.FRAME_PREPENDER, PipelineUtils.ENCRYPT_HANDLER, new CipherEncoder(encrypt));
		}
		String encName = URLEncoder.encode(getName(), "UTF-8");
		MessageDigest sha = MessageDigest.getInstance("SHA-1");
		for (byte[] bit : new byte[][] { request.getServerId().getBytes("ISO_8859_1"), sharedKey.getEncoded(), EncryptionUtil.keys.getPublic().getEncoded() }) {
			sha.update(bit);
		}
		String encodedHash = URLEncoder.encode(new BigInteger(sha.digest()).toString(16), "UTF-8");
		String preventProxy = BungeeCord.getInstance().config.isPreventProxyConnections() ? ("&ip=" + URLEncoder.encode(getAddress().getAddress().getHostAddress(), "UTF-8")) : "";
		String authURL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" + encName + "&serverId=" + encodedHash + preventProxy;
		Callback<String> handler = new Callback<String>() {
			@Override
			public void done(String result, Throwable error) {
				if (error == null) {
					LoginResult obj = BungeeCord.getInstance().gson.fromJson(result, LoginResult.class);
					if ((obj != null) && (obj.getId() != null)) {
						loginProfile = obj;
						updateUsername(obj.getName(), true);
						updateUUID(Util.getUUID(obj.getId()), true);
						Arrays.stream(loginProfile.getProperties())
						.forEach(bProperty -> connection.getProfile().addProperty(
							new ProfileProperty(bProperty.getName(), bProperty.getValue(), bProperty.getSignature())
						));
						finishLogin();
						return;
					}
					disconnect(BungeeCord.getInstance().getTranslation("offline_mode_player"));
				} else {
					disconnect(BungeeCord.getInstance().getTranslation("mojang_fail"));
					BungeeCord.getInstance().getLogger().log(Level.SEVERE, "Error authenticating " + getName() + " with minecraft.net", error);
				}
			}
		};
		HttpClient.get(authURL, channel.getHandle().eventLoop(), handler);
	}

	protected UUID offlineuuid;

	@Override
	public UUID getOfflineId() {
		return offlineuuid;
	}

	@SuppressWarnings("deprecation")
	protected void finishLogin() {
		GameProfile profile = connection.getProfile();

		PlayerProfileCompleteEvent profileCompleteEvent = new PlayerProfileCompleteEvent(connection);
		BungeeCord.getInstance().getPluginManager().callEvent(profileCompleteEvent);
		if (profileCompleteEvent.isLoginDenied()) {
			disconnect(profileCompleteEvent.getDenyLoginMessage());
			return;
		}

		if (profileCompleteEvent.getForcedName() != null) {
			updateUsername(profileCompleteEvent.getForcedName(), false);
		}
		if (profileCompleteEvent.getForcedUUID() != null) {
			updateUUID(profileCompleteEvent.getForcedUUID(), false);
		}
		profile.setProperties(profileCompleteEvent.getProperties());

		loginProfile = new LoginResult(
			getName(), getUUID(),
			profile.getProperties().values().stream()
			.flatMap(Collection::stream)
			.map(psprop -> new LoginResult.Property(psprop.getName(), psprop.getValue(), psprop.getSignature()))
			.collect(Collectors.toList()).toArray(new LoginResult.Property[0])
		);

		ProxiedPlayer oldName = BungeeCord.getInstance().getPlayer(getName());
		if (oldName != null) {
			oldName.disconnect(BungeeCord.getInstance().getTranslation("already_connected_proxy"));
		}
		ProxiedPlayer oldID = BungeeCord.getInstance().getPlayer(getUniqueId());
		if (oldID != null) {
			oldID.disconnect(BungeeCord.getInstance().getTranslation("already_connected_proxy"));
		}

		Callback<LoginEvent> complete = new Callback<LoginEvent>() {
			@Override
			public void done(LoginEvent result, Throwable error) {
				if (result.isCancelled()) {
					disconnect(result.getCancelReasonComponents());
					return;
				}
				if (!isConnected()) {
					return;
				}
				channel.getHandle().eventLoop().execute(() -> processLoginFinish());
			}
		};
		BungeeCord.getInstance().getPluginManager().callEvent(new LoginEvent(this, complete));
	}

	@SuppressWarnings("deprecation")
	protected void processLoginFinish() {
		if (!channel.isClosing()) {
			UserConnection userCon = new UserConnection(BungeeCord.getInstance(), channel, getName(), PSInitialHandler.this);
			if (hasCompression(connection.getVersion())) {
				userCon.setCompressionThreshold(BungeeCord.getInstance().config.getCompressionThreshold());
			}
			userCon.init();
			unsafe().sendPacket(new LoginSuccess(getUniqueId().toString(), getName()));
			channel.setProtocol(Protocol.GAME);

			PlayerLoginFinishEvent loginFinishEvent = new PlayerLoginFinishEvent(connection);
			BungeeCord.getInstance().getPluginManager().callEvent(loginFinishEvent);
			if (loginFinishEvent.isLoginDenied()) {
				disconnect(loginFinishEvent.getDenyLoginMessage());
				return;
			}

			channel.getHandle().pipeline().get(HandlerBoss.class).setHandler(new UpstreamBridge(BungeeCord.getInstance(), userCon));
			BungeeCord.getInstance().getPluginManager().callEvent(new PostLoginEvent(userCon));

			ServerInfo server;
			if (BungeeCord.getInstance().getReconnectHandler() != null) {
				server = BungeeCord.getInstance().getReconnectHandler().getServer(userCon);
			} else {
				server = AbstractReconnectHandler.getForcedHost(PSInitialHandler.this);
			}
			if (server == null) {
				server = BungeeCord.getInstance().getServerInfo(getListener().getDefaultServer());
			}
			userCon.connect(server, null, true);
		}
	}

	public enum LoginState {
		HELLO, ONLINEMODERESOLVE, KEY, AUTHENTICATING;
	}

	protected static boolean hasCompression(ProtocolVersion version) {
		return (version.getProtocolType() == ProtocolType.PC) && version.isAfterOrEq(ProtocolVersion.MINECRAFT_1_8);
	}

	protected static boolean isFullEncryption(ProtocolVersion version) {
		return (version.getProtocolType() == ProtocolType.PC) && version.isAfterOrEq(ProtocolVersion.MINECRAFT_1_7_5);
	}

}