package com.krslynx.axiom;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.AsyncResult;
import io.vertx.core.eventbus.Message;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The biller handles setup, connects and disconnects. Therefore, it's purpose is to ensure that setups are well-formed
 * (and eventually, trusted) and that clients connect/disconnect packets can be trusted. General packets are directly
 * passed between the client and the arena.
 */
public class AxiomBiller extends AbstractVerticle {

    /**
     * The Logger
     */
    private static final Logger log = LoggerFactory.getLogger(AxiomBiller.class);

    /* fixed externalised keys */
    private static final String socketIdKey = Messages.getString("key.socket.id");
    private static final String pathKey = Messages.getString("key.path");
    private static final String packetKey = Messages.getString("key.packet");

    /**
     * Called by vertx3.
     *
     * @throws Exception if something has gone wrong.
     */
    @Override
    public void start() throws Exception {
        log.info(Messages.getString("msg.biller.started"));
        arenaSetupConsumer();
    }

    /**
     * Consumes setup packets, and delegates the callback.
     */
    private void arenaSetupConsumer() {
        String arenaSetupAddress = Messages.getString("address.arena.setup");
        log.info(Messages.getString("msg.consumer"), arenaSetupAddress);
        vertx.eventBus().consumer(arenaSetupAddress, this::onArenaSetup);
    }

    /**
     * Called when a setup packet has been received.
     *
     * @param setupMessage the Setup message from the EventBus.
     */
    private void onArenaSetup(Message<JsonObject> setupMessage) {
        String arenaSetupAddress = Messages.getString("address.arena.setup");
        JsonObject setupBody = setupMessage.body();
        log.info(Messages.getString("msg.on.consumer"), arenaSetupAddress, setupBody.encode());
        if(!setupBody.containsKey(pathKey)) {
            log.error(Messages.getString("consumer.body.missing.key"), pathKey);
            return;
        }

        setupMessage.reply("success");
        String arenaPath = setupBody.getString("path");

        consumePing(arenaPath);
        consumeConnect(arenaPath);
        consumeDisconnect(arenaPath);
        sendSetup(arenaPath);
    }

    /**
     * Sends a Setup packet to the web for a given <code>arenaPath</code>
     *
     * @param arenaPath the path of the <code>{@link com.krslynx.axiom.model.Arena}</code>.
     */
    private void sendSetup(String arenaPath) {
        String webSetupAddress = Messages.getString("address.web.setup");
        vertx.eventBus().send(webSetupAddress, new JsonObject().put("path", arenaPath));
    }

    /**
     * Consumes Disconnect packets, and delegates the callback for the given <code>arenaPath</code>.
     *
     * @param arenaPath the path of the <code>{@link com.krslynx.axiom.model.Arena}</code>.
     */
    private void consumeDisconnect(String arenaPath) {
        // disconnects
        String disconnectAddress = Messages.getString("address.srv.disconnect") + arenaPath;
        log.info(Messages.getString("msg.consumer"), disconnectAddress);
        vertx.eventBus().consumer(disconnectAddress, this::onDisconnect);
    }

    /**
     * Called when a Disconnect packet has been received.
     *
     * @param disconnectMessage the Disconnect message from the EventBus.
     */
    private void onDisconnect(Message<JsonObject> disconnectMessage) {
        JsonObject disconnectBody = disconnectMessage.body();
//            log.trace(Messages.getString("msg.on.consumer"), disconnectAddress, disconnectBody.encode());

        if (!disconnectBody.containsKey(socketIdKey) || !disconnectBody.containsKey(pathKey)) {
            log.error(Messages.getString("consumer.body.missing.key"), new JsonArray().add(pathKey).add(socketIdKey).encode());
            return;
        }

        sendDisconnect(disconnectBody);
    }

    /**
     * Sends a Disconnect to the <code>{@link com.krslynx.axiom.arena.AxiomArena}</code>
     * @param disconnectBody the Disconnect message to be sent on the EventBus.
     */
    private void sendDisconnect(JsonObject disconnectBody) {
        String id = disconnectBody.getString(socketIdKey);
        String path = disconnectBody.getString(pathKey);
        JsonObject disconnectPacket = new JsonObject()
                .put(socketIdKey, id);

        String arenaDisconnectAddress = Messages.getString("address.arena.disconnect") + path + ":" + id;
        log.info(Messages.getString("msg.send"), arenaDisconnectAddress, disconnectPacket.encode());
        vertx.eventBus().send(arenaDisconnectAddress, disconnectPacket);
    }

    /**
     * Consumes Connect packets, and delegates the callback for the given <code>arenaPath</code>.
     *
     * @param arenaPath the path of the <code>{@link com.krslynx.axiom.model.Arena}</code>
     */
    private void consumeConnect(String arenaPath) {
        // connects
        String connectAddress = Messages.getString("address.srv.connect") + arenaPath;
        log.info(Messages.getString("msg.consumer"), connectAddress);
        vertx.eventBus().consumer(connectAddress, this::onConnect);
    }

    /**
     * Called when a Connect packet has been received.
     *
     * @param connectMessage the Connect message from the EventBus.
     */
    private void onConnect(Message<JsonObject> connectMessage) {
        JsonObject connectBody = connectMessage.body();
//            log.trace(Messages.getString("msg.on.consumer"), connectAddress, connectBody.encode());

        if (!connectBody.containsKey(socketIdKey)
                || !connectBody.containsKey(pathKey)
                || !connectBody.containsKey(packetKey)) {
            log.error(Messages.getString("consumer.body.missing.key"), new JsonArray().add(pathKey).add(socketIdKey).add(packetKey).encode());
            return;
        }

        sendConnect(connectBody);
    }

    /**
     * Sends a Connect to the <code>{@link com.krslynx.axiom.arena.AxiomArena}</code>.
     * @param connectBody the Connect message to be sent on the EventBus.
     */
    private void sendConnect(JsonObject connectBody) {
        String id = connectBody.getString(socketIdKey);
        String path = connectBody.getString(pathKey);
        JsonArray frame = connectBody.getJsonArray(packetKey);

        JsonObject connectPacket = new JsonObject()
                .put(socketIdKey, id)
                .put(packetKey, frame);

        String arenaConnectAddress = Messages.getString("address.arena.connect") + path;
        log.info(Messages.getString("msg.send"), arenaConnectAddress, connectPacket.encode());
        vertx.eventBus().send(arenaConnectAddress, connectPacket);
    }

    @Deprecated
    private void consumePing(String arenaPath) {
        // pings
        String pingAddress = Messages.getString("address.arena.ping") + arenaPath;
        JsonObject pingPacket = new JsonObject().put("path", arenaPath);
        log.info(Messages.getString("msg.send"), pingAddress, pingPacket.encode());
        vertx.eventBus().send(pingAddress, pingPacket, (AsyncResult<Message<JsonObject>> pingMessage) -> {
            if (pingMessage.failed()) {
                log.warn(Messages.getString("msg.ping.fail"), pingAddress);
            } else {
                JsonObject pingBody = pingMessage.result().body();
                log.info(Messages.getString("msg.on.recv"), pingAddress, pingBody.encode());
                if(pingBody.containsKey("path") && pingBody.getString("path").equals(arenaPath)) {
                    log.info(Messages.getString("msg.ping.success"), arenaPath, pingBody.encode());
                } else {
                    log.warn(Messages.getString("msg.ping.malformed"), pingAddress, arenaPath, pingBody.getString("path"));
                }
            }
        });
    }
}