package net.glowstone.net.handler.login;

import com.flowpowered.network.MessageHandler;
import lombok.AllArgsConstructor;
import net.glowstone.EventFactory;
import net.glowstone.GlowServer;
import net.glowstone.entity.meta.profile.PlayerProfile;
import net.glowstone.entity.meta.profile.PlayerProperty;
import net.glowstone.net.GlowSession;
import net.glowstone.net.http.HttpCallback;
import net.glowstone.net.http.HttpClient;
import net.glowstone.net.message.login.EncryptionKeyResponseMessage;
import net.glowstone.util.UuidUtils;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent.Result;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;

public final class EncryptionKeyResponseHandler implements MessageHandler<GlowSession, EncryptionKeyResponseMessage> {

    private static final String BASE_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined";
    private static final JSONParser PARSER = new JSONParser();

    @Override
    public void handle(GlowSession session, EncryptionKeyResponseMessage message) {
        PrivateKey privateKey = session.getServer().getKeyPair().getPrivate();

        // create rsaCipher
        Cipher rsaCipher;
        try {
            rsaCipher = Cipher.getInstance("RSA");
        } catch (GeneralSecurityException ex) {
            GlowServer.logger.log(Level.SEVERE, "Could not initialize RSA cipher", ex);
            session.disconnect("Unable to initialize RSA cipher.");
            return;
        }

        // decrypt shared secret
        SecretKey sharedSecret;
        try {
            rsaCipher.init(Cipher.DECRYPT_MODE, privateKey);
            sharedSecret = new SecretKeySpec(rsaCipher.doFinal(message.getSharedSecret()), "AES");
        } catch (Exception ex) {
            GlowServer.logger.log(Level.WARNING, "Could not decrypt shared secret", ex);
            session.disconnect("Unable to decrypt shared secret.");
            return;
        }

        // decrypt verify token
        byte[] verifyToken;
        try {
            rsaCipher.init(Cipher.DECRYPT_MODE, privateKey);
            verifyToken = rsaCipher.doFinal(message.getVerifyToken());
        } catch (Exception ex) {
            GlowServer.logger.log(Level.WARNING, "Could not decrypt verify token", ex);
            session.disconnect("Unable to decrypt verify token.");
            return;
        }

        // check verify token
        if (!Arrays.equals(verifyToken, session.getVerifyToken())) {
            session.disconnect("Invalid verify token.");
            return;
        }

        // initialize stream encryption
        session.enableEncryption(sharedSecret);

        // create hash for auth
        String hash;
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-1");
            digest.update(session.getSessionId().getBytes());
            digest.update(sharedSecret.getEncoded());
            digest.update(session.getServer().getKeyPair().getPublic().getEncoded());

            // BigInteger takes care of sign and leading zeroes
            hash = new BigInteger(digest.digest()).toString(16);
        } catch (NoSuchAlgorithmException ex) {
            GlowServer.logger.log(Level.SEVERE, "Unable to generate SHA-1 digest", ex);
            session.disconnect("Failed to hash login data.");
            return;
        }

        String url = BASE_URL + "?username=" + session.getVerifyUsername() + "&serverId=" + hash;
        HttpClient.connect(url, session.getChannel().eventLoop(), new ClientAuthCallback(session));
    }

    @AllArgsConstructor
    private static class ClientAuthCallback implements HttpCallback {

        private final GlowSession session;

        @Override
        public void done(String response) {
            JSONObject json;
            try {
                json = (JSONObject) PARSER.parse(response); // TODO gson here
            } catch (ParseException e) {
                GlowServer.logger.warning("Username \"" + session.getVerifyUsername() + "\" failed to authenticate!");
                session.disconnect("Failed to verify username!");
                return;
            }

            String name = (String) json.get("name");
            String id = (String) json.get("id");

            // parse UUID
            UUID uuid;
            try {
                uuid = UuidUtils.fromFlatString(id);
            } catch (IllegalArgumentException ex) {
                GlowServer.logger.log(Level.SEVERE, "Returned authentication UUID invalid: " + id, ex);
                session.disconnect("Invalid UUID.");
                return;
            }

            JSONArray propsArray = (JSONArray) json.get("properties");

            // parse properties
            List<PlayerProperty> properties = new ArrayList<>(propsArray.size());
            for (Object obj : propsArray) {
                JSONObject propJson = (JSONObject) obj;
                String propName = (String) propJson.get("name");
                String value = (String) propJson.get("value");
                String signature = (String) propJson.get("signature");
                properties.add(new PlayerProperty(propName, value, signature));
            }

            AsyncPlayerPreLoginEvent event = EventFactory.onPlayerPreLogin(name, session.getAddress(), uuid);
            if (event.getLoginResult() != Result.ALLOWED) {
                session.disconnect(event.getKickMessage(), true);
                return;
            }

            // spawn player
            session.getServer().getScheduler().runTask(null, () -> session.setPlayer(new PlayerProfile(name, uuid, properties)));
        }

        @Override
        public void error(Throwable t) {
            GlowServer.logger.log(Level.SEVERE, "Error in authentication thread", t);
            session.disconnect("Internal error during authentication.", true);
        }
    }
}
