// SPDX-License-Identifier: LGPL-3.0-or-later

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea.
*
* nymea is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* nymea is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with nymea. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

/*!
    \class nymeaserver::JsonRPCServer
    \brief This class provides a JSON-RPC API interface to the \l{TransportInterface}{TransportInterfaces}.

    \ingroup server
    \inmodule core

    The \l{JsonRPCServer} class provides the server interface for a JSON-RPC API call. This class
    communicates with \l{TransportInterface}{TransportInterfaces} and processes the
    JSON-RPC request in the corresponding \l{JsonHandler}. The \l{JsonRPCServer} it self is also
    an \l{JsonHandler} and provides the introspection, version and notification control methods
    for the \l{JSON-RPC API}.

    \sa ServerManager, TransportInterface, TcpServer, WebSocketServer
*/


#include "jsonrpcserverimplementation.h"
#include "jsonrpc/jsonhandler.h"
#include "jsonvalidator.h"
#include "nymeacore.h"
#include "usermanager/usermanager.h"
#include "loggingcategories.h"
#include "version.h"

#include "integrationshandler.h"
#include "ruleshandler.h"
#include "logginghandler.h"
#include "configurationhandler.h"
#include "networkmanagerhandler.h"
#include "tagshandler.h"
#include "appdatahandler.h"
#include "systemhandler.h"
#include "usershandler.h"
#include "zigbeehandler.h"
#include "zwavehandler.h"
#include "modbusrtuhandler.h"

#include <QJsonDocument>
#include <QStringList>
#include <QSslConfiguration>
#include <QRegularExpression>

namespace nymeaserver {

/*! Constructs a \l{JsonRPCServer} with the given \a sslConfiguration and \a parent. */
JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration &sslConfiguration, QObject *parent):
    JsonHandler(parent),
    m_notificationId(0)
{
    Q_UNUSED(sslConfiguration)
    // First, define our own JSONRPC API

    // Enums
    registerEnum<BasicType>();
    registerEnum<UserManager::UserError>();
    registerFlag<Types::PermissionScope, Types::PermissionScopes>();

    // Objects
    registerObject<TokenInfo>();

    QVariantMap experiece;
    experiece.insert("name", enumValueName(String));
    experiece.insert("version", enumValueName(String));
    registerObject("Experience", experiece);

    QVariantMap cacheHash;
    cacheHash.insert("method", enumValueName(String));
    cacheHash.insert("hash", enumValueName(String));
    registerObject("CacheHash", cacheHash);

    // Methods
    QString description; QVariantMap returns; QVariantMap params;
    description = "Initiates a connection. Use this method to perform an initial handshake of the "
                  "connection. Optionally, a parameter \"locale\" is can be passed to set up the used "
                  "locale for this connection. Strings such as ThingClass displayNames etc will be "
                  "localized to this locale. If this parameter is omitted, the default system locale "
                  "(depending on the configuration) is used. The reply of this method contains information "
                  "about this core instance such as version information, uuid and its name. The locale value"
                  "indicates the locale used for this connection. Note: This method can be called multiple "
                  "times. The locale used in the last call for this connection will be used. Other values, "
                  "like initialSetupRequired might change if the setup has been performed in the meantime.\n "
                  "The field cacheHashes may contain a map of methods and MD5 hashes. As long as the hash for "
                  "a method does not change, a client may use a previously cached copy of the call instead of "
                  "fetching the content again. While the Hello call doesn't necessarily require a token, this "
                  "can be called with a token. If a token is provided, it will be verified and the reply contains "
                  "information about the tokens validity and the user and permissions for the given token.";
    params.insert("o:locale", enumValueName(String));
    returns.insert("server", enumValueName(String));
    returns.insert("name", enumValueName(String));
    returns.insert("version", enumValueName(String));
    returns.insert("uuid", enumValueName(Uuid));
    returns.insert("language", enumValueName(String));
    returns.insert("locale", enumValueName(String));
    returns.insert("protocol version", enumValueName(String));
    returns.insert("initialSetupRequired", enumValueName(Bool));
    returns.insert("authenticationRequired", enumValueName(Bool));
    returns.insert("pushButtonAuthAvailable", enumValueName(Bool));
    returns.insert("o:experiences", QVariantList() << objectRef("Experience"));
    returns.insert("o:cacheHashes", QVariantList() << objectRef("CacheHash"));
    returns.insert("o:authenticated", enumValueName(Bool));
    returns.insert("o:permissionScopes", flagRef<Types::PermissionScopes>());
    returns.insert("o:username", enumValueName(String));
    registerMethod("Hello", description, params, returns, Types::PermissionScopeNone);

    params.clear(); returns.clear();
    description = "Introspect this API.";
    returns.insert("methods", enumValueName(Object));
    returns.insert("notifications", enumValueName(Object));
    returns.insert("types", enumValueName(Object));
    registerMethod("Introspect", description, params, returns, Types::PermissionScopeNone);

    params.clear(); returns.clear();
    description = "Version of this nymea/JSONRPC interface.";
    returns.insert("version", enumValueName(String));
    returns.insert("protocol version", enumValueName(String));
    returns.insert("qtVersion", enumValueName(String));
    returns.insert("qtBuildVersion", enumValueName(String));
    registerMethod("Version", description, params, returns, Types::PermissionScopeNone);

    params.clear(); returns.clear();
    description = "Enable/Disable notifications for this connections. Either \"enabled\" or """
                  "\"namespaces\" needs to be given but not both of them. The boolean based "
                  "\"enabled\" parameter will enable/disable all notifications at once. If "
                  "instead the list-based \"namespaces\" parameter is provided, all given namespaces"
                  "will be enabled, the others will be disabled. The return value of \"success\" will "
                  "indicate success of the operation. The \"enabled\" property in the return value is "
                  "deprecated and used for legacy compatibilty only. It will be set to true if at least "
                  "one namespace has been enabled.";
    params.insert("o:namespaces", enumValueName(StringList));
    params.insert("d:o:enabled", enumValueName(Bool));
    returns.insert("namespaces", enumValueName(StringList));
    returns.insert("d:enabled", enumValueName(Bool));
    registerMethod("SetNotificationStatus", description, params, returns, Types::PermissionScopeNone);

    params.clear(); returns.clear();
    description = "Create a new user in the API. This is only allowed to be called when the initial setup is required. "
                  "To create additional users, use Users.CreateUser instead. Call Authenticate after this to obtain a "
                  "device token for the newly created user.";
    params.insert("username", enumValueName(String));
    params.insert("password", enumValueName(String));
    params.insert("o:displayName", enumValueName(String));
    params.insert("o:email", enumValueName(String));
    returns.insert("error", enumRef<UserManager::UserError>());
    registerMethod("CreateUser", description, params, returns, Types::PermissionScopeAdmin);

    params.clear(); returns.clear();
    description = "Authenticate a client to the api via user & password challenge. Provide "
                  "a device name which allows the user to identify the client and revoke the token in case "
                  "the device is lost or stolen. This will return a new token to be used to authorize a "
                  "client at the API.";
    params.insert("username", enumValueName(String));
    params.insert("password", enumValueName(String));
    params.insert("deviceName", enumValueName(String));
    returns.insert("success", enumValueName(Bool));
    returns.insert("o:token", enumValueName(String));
    returns.insert("o:username", enumValueName(String));
    returns.insert("o:scopes", flagRef<Types::PermissionScopes>());
    registerMethod("Authenticate", description, params, returns, Types::PermissionScopeNone);

    params.clear(); returns.clear();
    description = "Authenticate a client to the api via Push Button method. "
                  "Provide a device name which allows the user to identify the client and revoke the "
                  "token in case the device is lost or stolen. If push button hardware is available, "
                  "this will return with success and start listening for push button presses. When the "
                  "push button is pressed, the PushButtonAuthFinished notification will be sent to the "
                  "requesting client. The procedure will be cancelled when the connection is interrupted. "
                  "If another client requests push button authentication while a procedure is still going "
                  "on, the second call will take over and the first one will be notified by the "
                  "PushButtonAuthFinished signal about the error. The application should make it clear "
                  "to the user to not press the button when the procedure fails as this can happen for 2 "
                  "reasons: a) a second user is trying to auth at the same time and only the currently "
                  "active user should press the button or b) it might indicate an attacker trying to take "
                  "over and snooping in for tokens.";
    params.insert("deviceName", enumValueName(String));
    returns.insert("success", enumValueName(Bool));
    returns.insert("transactionId", enumValueName(Int));
    registerMethod("RequestPushButtonAuth", description, params, returns, Types::PermissionScopeNone);

    params.clear(); returns.clear();
    description = "This is basically a Ping/Pong mechanism a client app may use to check server connectivity. Currently, the server does not actually do anything with this information and will return the call providing the given sessionId back to the caller. It is up to the client whether to use this or not and not required by the server to keep the connection alive.";
    params.insert("sessionId", enumValueName(String));
    returns.insert("success", enumValueName(Bool));
    returns.insert("sessionId", enumValueName(String));
    registerMethod("KeepAlive", description, params, returns, Types::PermissionScopeNone);

    // Notifications
    params.clear();
    description = "Emitted when a push button authentication reaches final state. NOTE: This notification is special. It will only be emitted to connections that did actively request a push button authentication, but also it will be emitted regardless of the notification settings. ";
    params.insert("success", enumValueName(Bool));
    params.insert("transactionId", enumValueName(Int));
    params.insert("o:token", enumValueName(String));
    registerNotification("PushButtonAuthFinished", description, params);

    connect(NymeaCore::instance()->userManager(), &UserManager::pushButtonAuthFinished, this, &JsonRPCServerImplementation::onPushButtonAuthFinished);



    m_connectionLockdownTimer.setSingleShot(true);
    m_connectionLockdownTimer.setInterval(3000);
}

/*! Returns the \e namespace of \l{JsonHandler}. */
QString JsonRPCServerImplementation::name() const
{
    return QStringLiteral("JSONRPC");
}

JsonReply *JsonRPCServerImplementation::Hello(const QVariantMap &params, const JsonContext &context)
{
    TransportInterface *interface = reinterpret_cast<TransportInterface*>(property("transportInterface").toLongLong());

    qCDebug(dcJsonRpc()) << params;
    QUuid clientId = context.clientId();
    if (params.contains("locale")) {
        m_clientLocales.insert(clientId, QLocale(params.value("locale").toString()));
    }

    qCDebug(dcJsonRpc()) << "Client" << clientId << "initiated handshake." << m_clientLocales.value(clientId);

    // If we waited for the handshake, here it is. Remove the timer...
    if (m_newConnectionWaitTimers.contains(clientId)) {
        delete m_newConnectionWaitTimers.take(clientId);
    }

    // Compose the reply
    QVariantMap handshake;
    handshake.insert("server", "nymea");
    handshake.insert("name", NymeaCore::instance()->configuration()->serverName());
    handshake.insert("version", NYMEA_VERSION_STRING);
    handshake.insert("uuid", NymeaCore::instance()->configuration()->serverUuid().toString());
    // "language" is deprecated
    handshake.insert("language", m_clientLocales.value(clientId).name());
    handshake.insert("locale", m_clientLocales.value(clientId).name());
    handshake.insert("protocol version", JSON_PROTOCOL_VERSION);
    handshake.insert("initialSetupRequired", (interface->configuration().authenticationEnabled ? NymeaCore::instance()->userManager()->initRequired() : false));
    handshake.insert("authenticationRequired", interface->configuration().authenticationEnabled);
    handshake.insert("pushButtonAuthAvailable", NymeaCore::instance()->userManager()->pushButtonAuthAvailable());
    if (!m_experiences.isEmpty()) {
        QVariantList experiences;
        foreach (JsonHandler* handler, m_experiences.keys()) {
            QVariantMap experience;
            experience.insert("name", handler->name());
            experience.insert("version", m_experiences.value(handler));
            experiences.append(experience);
        }
        handshake.insert("experiences", experiences);
    }
    QVariantList cacheHashes;
    foreach (const QString &handlerName, m_handlers.keys()) {
        QHash<QString, QString> hashes = m_handlers.value(handlerName)->cacheHashes();
        foreach (const QString &hashName, hashes.keys()) {
            QVariantMap cacheHash;
            cacheHash.insert("method", handlerName + "." + hashName);
            cacheHash.insert("hash", hashes.value(hashName));
            cacheHashes.append(cacheHash);
        }
    }
    if (!cacheHashes.isEmpty()) {
        handshake.insert("cacheHashes", cacheHashes);
    }

    m_clientTokens[context.clientId()] = context.token();

    bool badToken = false;
    if (!context.token().isEmpty()) {
        TokenInfo tokenInfo = NymeaCore::instance()->userManager()->tokenInfo(context.token());
        UserInfo userInfo = NymeaCore::instance()->userManager()->userInfo(tokenInfo.username());
        badToken = tokenInfo.id().isNull();
        handshake.insert("authenticated", !badToken);
        handshake.insert("permissionScopes", Types::scopesToStringList(userInfo.scopes()));
        handshake.insert("username", userInfo.username());
    }

    // If the connection is locked down already (because of a previous failed attempt) and authentication failed
    // again, drop the client. He won't be able to reconnect until the lockdown timer runs out.
    // This will give at max 2 attempts to present a valid token per lockdown period.
    if (m_connectionLockdownTimer.isActive() && badToken) {
        qCWarning(dcJsonRpc()) << "Dropping client because of repeated bad token authentication.";
        interface->terminateClientConnection(clientId);
    }

    if (badToken) {
        qCWarning(dcJsonRpc()) << "Staring connection lockdown timer";
        m_connectionLockdownTimer.start();
    }

    return createReply(handshake);;
}

JsonReply* JsonRPCServerImplementation::Introspect(const QVariantMap &params) const
{
    Q_UNUSED(params)
    return createReply(m_api);
}

JsonReply* JsonRPCServerImplementation::Version(const QVariantMap &params) const
{
    Q_UNUSED(params)

    QVariantMap data;
    data.insert("version", NYMEA_VERSION_STRING);
    data.insert("protocol version", JSON_PROTOCOL_VERSION);
    data.insert("qtVersion", qVersion());
    data.insert("qtBuildVersion", QT_VERSION_STR);
    return createReply(data);
}

JsonReply* JsonRPCServerImplementation::SetNotificationStatus(const QVariantMap &params, const JsonContext &context)
{
    QUuid clientId = context.clientId();
    Q_ASSERT_X(m_clientTransports.contains(clientId), "JsonRPCServer", "Invalid client ID.");

    QStringList enabledNamespaces;
    foreach (const QString &namespaceName, m_handlers.keys()) {
        if (params.contains("enabled")) {
            if (params.value("enabled").toBool()) {
                enabledNamespaces.append(namespaceName);
            }
        } else {
            if (params.value("namespaces").toList().contains(namespaceName)) {
                enabledNamespaces.append(namespaceName);
            }
        }
    }
    qCDebug(dcJsonRpc()) << "Notification settings for client" << clientId << ":" << enabledNamespaces;
    m_clientNotifications[clientId] = enabledNamespaces;

    QVariantMap returns;
    returns.insert("namespaces", m_clientNotifications[clientId]);
    // legacy, deprecated
    returns.insert("enabled", m_clientNotifications[clientId].count() > 0);
    return createReply(returns);
}

JsonReply *JsonRPCServerImplementation::CreateUser(const QVariantMap &params)
{
    QString username = params.value("username").toString();
    QString password = params.value("password").toString();
    QString email = params.value("email").toString();
    QString displayName = params.value("displayName").toString();

    UserManager::UserError status = NymeaCore::instance()->userManager()->createUser(username, password, email, displayName, Types::PermissionScopeAdmin);

    QVariantMap returns;
    returns.insert("error", enumValueName<UserManager::UserError>(status));
    return createReply(returns);
}

JsonReply *JsonRPCServerImplementation::Authenticate(const QVariantMap &params, const JsonContext &context)
{
    QString username = params.value("username").toString();
    QString password = params.value("password").toString();
    QString deviceName = params.value("deviceName").toString();

    QByteArray token = NymeaCore::instance()->userManager()->authenticate(username, password, deviceName);
    QVariantMap ret;
    ret.insert("success", !token.isEmpty());
    if (!token.isEmpty()) {
        ret.insert("token", token);
        TokenInfo tokenInfo = NymeaCore::instance()->userManager()->tokenInfo(token);
        UserInfo userInfo = NymeaCore::instance()->userManager()->userInfo(tokenInfo.username());
        ret.insert("username", userInfo.username());
        ret.insert("scopes", Types::scopesToStringList(userInfo.scopes()));
    }

    // If the connection is locked down already (because of a previous failed attempt) and authentication failed
    // again, drop the client. He won't be able to reconnect until the lockdown timer runs out.
    // This will give at max 2 attempts to present a valid token per lockdown period.
    if (m_connectionLockdownTimer.isActive() && token.isEmpty()) {
        qCWarning(dcJsonRpc()) << "Dropping client because of repeated bad user/password authentication.";
        TransportInterface *interface = reinterpret_cast<TransportInterface*>(property("transportInterface").toLongLong());
        interface->terminateClientConnection(context.clientId());
    }

    if (token.isEmpty()) {
        qCWarning(dcJsonRpc()) << "Staring connection lockdown timer";
        m_connectionLockdownTimer.start();
    }

    m_clientTokens[context.clientId()] = token;

    return createReply(ret);
}

JsonReply *JsonRPCServerImplementation::RequestPushButtonAuth(const QVariantMap &params, const JsonContext &context)
{
    QString deviceName = params.value("deviceName").toString();
    QUuid clientId = context.clientId();

    int transactionId = NymeaCore::instance()->userManager()->requestPushButtonAuth(deviceName);
    m_pushButtonTransactions.insert(transactionId, clientId);

    QVariantMap data;
    data.insert("transactionId", transactionId);
    // TODO: return false if pushbutton auth is disabled in settings
    data.insert("success", true);
    return createReply(data);
}

/*! A client may use this as a ping/pong mechanism to check server connectivity. */
JsonReply *JsonRPCServerImplementation::KeepAlive(const QVariantMap &params)
{
    QString sessionId = params.value("sessionId").toString();
    qCDebug(dcJsonRpc()) << "KeepAlive received" << sessionId;
    QVariantMap resultMap;
    resultMap.insert("success", true);
    resultMap.insert("sessionId", sessionId);
    return createReply(resultMap);
}

/*! Returns the list of registered \l{JsonHandler}{JsonHandlers} and their name.*/
QHash<QString, JsonHandler *> JsonRPCServerImplementation::handlers() const
{
    return m_handlers;
}

/*! Register a new \l{TransportInterface} to the JSON server. If the given interface is already registered, just the authenticationRequired flag will be updated. */
void JsonRPCServerImplementation::registerTransportInterface(TransportInterface *interface)
{
    connect(interface, &TransportInterface::clientConnected, this, &JsonRPCServerImplementation::clientConnected);
    connect(interface, &TransportInterface::clientDisconnected, this, &JsonRPCServerImplementation::clientDisconnected);
    connect(interface, &TransportInterface::dataAvailable, this, &JsonRPCServerImplementation::processData);
}

void JsonRPCServerImplementation::unregisterTransportInterface(TransportInterface *interface)
{
    disconnect(interface, &TransportInterface::clientConnected, this, &JsonRPCServerImplementation::clientConnected);
    disconnect(interface, &TransportInterface::clientDisconnected, this, &JsonRPCServerImplementation::clientDisconnected);
    disconnect(interface, &TransportInterface::dataAvailable, this, &JsonRPCServerImplementation::processData);
    foreach (const QUuid &clientId, m_clientTransports.keys(interface)) {
        interface->terminateClientConnection(clientId);
        clientDisconnected(clientId);
    }
}

bool JsonRPCServerImplementation::registerExperienceHandler(JsonHandler *handler, int majorVersion, int minorVersion)
{
    bool ret = registerHandler(handler);
    if (ret) {
        m_experiences.insert(handler, QString("%1.%2").arg(majorVersion).arg(minorVersion));
    }
    return ret;
}

/*! Send a JSON success response to the client with the given \a clientId,
 * \a commandId and \a params to the inerted \l{TransportInterface}.
 */
void JsonRPCServerImplementation::sendResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QVariantMap &params, const QString &deprecationWarning)
{
    QVariantMap response;
    response.insert("id", commandId);
    response.insert("status", "success");
    response.insert("params", params);

    if (!deprecationWarning.isEmpty()) {
        response.insert("deprecationWarning", deprecationWarning);
    }

    QByteArray data = QJsonDocument::fromVariant(response).toJson(QJsonDocument::Compact);
    qCDebug(dcJsonRpcTraffic()) << "Sending data:" << data;
    interface->sendData(clientId, data);
}

/*! Send a JSON error response to the client with the given \a clientId,
 * \a commandId and \a error to the inerted \l{TransportInterface}.
 */
void JsonRPCServerImplementation::sendErrorResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error)
{
    QVariantMap errorResponse;
    errorResponse.insert("id", commandId);
    errorResponse.insert("status", "error");
    errorResponse.insert("error", error);

    QByteArray data = QJsonDocument::fromVariant(errorResponse).toJson(QJsonDocument::Compact);
    qCDebug(dcJsonRpcTraffic()) << "Sending data:" << data;
    interface->sendData(clientId, data);
}

void JsonRPCServerImplementation::sendUnauthorizedResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error)
{
    QVariantMap errorResponse;
    errorResponse.insert("id", commandId);
    errorResponse.insert("status", "unauthorized");
    errorResponse.insert("error", error);

    QByteArray data = QJsonDocument::fromVariant(errorResponse).toJson(QJsonDocument::Compact);
    qCDebug(dcJsonRpcTraffic()) << "Sending data:" << data;
    interface->sendData(clientId, data);
}

void JsonRPCServerImplementation::setup()
{
    registerHandler(this);
    registerHandler(new IntegrationsHandler(NymeaCore::instance()->thingManager(), this));
    registerHandler(new RulesHandler(NymeaCore::instance()->ruleEngine(), this));
    registerHandler(new LoggingHandler(NymeaCore::instance()->logEngine(), this));
    registerHandler(new ConfigurationHandler(this));
    registerHandler(new NetworkManagerHandler(NymeaCore::instance()->networkManager(), this));
    registerHandler(new TagsHandler(this));
    registerHandler(new AppDataHandler(this));
    registerHandler(new SystemHandler(NymeaCore::instance()->platform(), this));
    registerHandler(new UsersHandler(NymeaCore::instance()->userManager(), this));
    registerHandler(new ZigbeeHandler(NymeaCore::instance()->zigbeeManager(), this));
    registerHandler(new ZWaveHandler(NymeaCore::instance()->zwaveManager(), this));
    registerHandler(new ModbusRtuHandler(NymeaCore::instance()->modbusRtuManager(), this));
}

void JsonRPCServerImplementation::processData(const QUuid &clientId, const QByteArray &data)
{
    qCDebug(dcJsonRpcTraffic()) << "Incoming data:" << data;

    TransportInterface *interface = qobject_cast<TransportInterface *>(sender());

    // Handle packet fragmentation
    QByteArray buffer = m_clientBuffers[clientId];
    buffer.append(data);
    int splitIndex = buffer.indexOf("}\n{");
    while (splitIndex > -1) {
        processJsonPacket(interface, clientId, buffer.left(splitIndex + 1));
        buffer = buffer.right(buffer.length() - splitIndex - 2);
        splitIndex = buffer.indexOf("}\n{");
    }
    if (buffer.trimmed().endsWith("}")) {
        processJsonPacket(interface, clientId, buffer);
        buffer.clear();
    }
    m_clientBuffers[clientId] = buffer;

    if (buffer.size() > 1024 * 1024) {
        qCWarning(dcJsonRpc()) << "Client buffer larger than 1MB and no valid data. Dropping client connection.";
        interface->terminateClientConnection(clientId);
    }
}

void JsonRPCServerImplementation::processJsonPacket(TransportInterface *interface, const QUuid &clientId, const QByteArray &data)
{
    QJsonParseError error;
    QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);

    if(error.error != QJsonParseError::NoError) {
        qCWarning(dcJsonRpc()) << "Failed to parse JSON data" << data << ":" << error.errorString();
        sendErrorResponse(interface, clientId, -1, QString("Failed to parse JSON data: %1").arg(error.errorString()));
        return;
    }

    QVariantMap message = jsonDoc.toVariant().toMap();

    bool success;
    int commandId = message.value("id").toInt(&success);
    if (!success) {
        qCWarning(dcJsonRpc()) << "Error parsing command. Missing \"id\":" << message;
        sendErrorResponse(interface, clientId, commandId, "Error parsing command. Missing 'id'");
        return;
    }

    QString methodString = message.value("method").toString();
    QStringList commandList = methodString.split('.');
    if (commandList.count() != 2) {
        qCWarning(dcJsonRpc()) << "Error parsing method.\nGot:" << message.value("method").toString() << "\nExpected: \"Namespace.method\"";
        sendErrorResponse(interface, clientId, commandId, QString("Error parsing method. Got: '%1'', Expected: 'Namespace.method'").arg(message.value("method").toString()));
        return;
    }
    QString targetNamespace = commandList.first();
    QString method = commandList.last();

    // We'll only allow setting a new token in the hello call. All other calls must match the token given in the hello
    QByteArray token = m_clientTokens.value(clientId);
    if (methodString == "JSONRPC.Hello") {
        token = message.value("token").toByteArray();
    } else if (message.value("token").toByteArray() != token) {
        qCWarning(dcJsonRpc()) << "Client changed token without redoing the handshake.";
        qCDebug(dcJsonRpc()) << "Old token:" << token << "new token:" << message.value("token").toByteArray();
        sendUnauthorizedResponse(interface, clientId, commandId, "Changing the user (token) requires a new handshake. Call JSONRPC.Hello.");
        interface->terminateClientConnection(clientId);
        qCWarning(dcJsonRpc()) << "Staring connection lockdown timer";
        m_connectionLockdownTimer.start();
        return;
    }

    // check if authentication is required for this transport
    if (interface->configuration().authenticationEnabled) {
        QStringList authExemptMethodsNoUser = {"JSONRPC.Hello", "JSONRPC.RequestPushButtonAuth", "JSONRPC.CreateUser"};
        QStringList authExemptMethodsWithUser = {"JSONRPC.Hello", "JSONRPC.Authenticate", "JSONRPC.RequestPushButtonAuth"};
        // if there is no user in the system yet, let's fail unless this is a special method for authentication itself
        if (NymeaCore::instance()->userManager()->initRequired()) {
            if (!authExemptMethodsNoUser.contains(methodString) && (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token))) {
                sendUnauthorizedResponse(interface, clientId, commandId, "Initial setup required. Call JSONRPC.CreateUser first.");
                qCWarning(dcJsonRpc()) << "Initial setup required but client does not call the setup. Dropping connection.";
                interface->terminateClientConnection(clientId);
                qCWarning(dcJsonRpc()) << "Staring connection lockdown timer";
                m_connectionLockdownTimer.start();
                return;
            }
        } else {
            // Ok, we have a user. If there isn't a valid token, let's fail unless this is an authentication related call
            if (!authExemptMethodsWithUser.contains(methodString)) {
                if (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token)) {
                    sendUnauthorizedResponse(interface, clientId, commandId, "Forbidden: Invalid token.");
                    qCWarning(dcJsonRpc()) << "Client did not not present a valid token. Dropping connection.";
                    interface->terminateClientConnection(clientId);
                    qCWarning(dcJsonRpc()) << "Staring connection lockdown timer";
                    m_connectionLockdownTimer.start();
                    return;
                }
                // Check if the user has the required permissions
                TokenInfo tokenInfo = NymeaCore::instance()->userManager()->tokenInfo(token);
                UserInfo userInfo = NymeaCore::instance()->userManager()->userInfo(tokenInfo.username());
                Types::PermissionScope methodScope = Types::scopeFromString(m_api.value("methods").toMap().value(methodString).toMap().value("permissionScope").toString());
                if (methodScope != Types::PermissionScopeNone && !userInfo.scopes().testFlag(Types::PermissionScopeAdmin) && !userInfo.scopes().testFlag(methodScope)) {
                    qCWarning(dcJsonRpc()) << "Method" << methodString << "requires" << Types::scopeToString(methodScope) << "but client token has:" << Types::scopesToStringList(userInfo.scopes());
                    sendErrorResponse(interface, clientId, commandId, "Permission denied.");
                    return;
                }
            }
        }
    }
    // At this point we can assume all the calls are authorized

    JsonHandler *handler = m_handlers.value(targetNamespace);
    if (!handler) {
        qCWarning(dcJsonRpc()) << "JSON RPC method called for invalid namespace:" << targetNamespace;
        sendErrorResponse(interface, clientId, commandId, "No such namespace");
        return;
    }
    if (!handler->jsonMethods().contains(method)) {
        qCWarning(dcJsonRpc()) << QString("JSON RPC method called for invalid method: %1.%2").arg(targetNamespace, method);
        sendErrorResponse(interface, clientId, commandId, "No such method");
        return;
    }

    QVariantMap params = message.value("params").toMap();

    QVariantMap definition = handler->jsonMethods().value(method).toMap().value("params").toMap();
    JsonValidator validator;
    JsonValidator::Result validationResult = validator.validateParams(params, targetNamespace + '.' + method, m_api);
    if (!validationResult.success()) {
        qCWarning(dcJsonRpc()) << "JSON RPC parameter verification failed for method" << targetNamespace + '.' + method;
        qCWarning(dcJsonRpc()) << validationResult.errorString() << "in" << validationResult.where();
        qCWarning(dcJsonRpc()) << "Call params:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
        sendErrorResponse(interface, clientId, commandId, "Invalid params: " + validationResult.errorString() + " in " + validationResult.where());
        return;
    }

    if (!(targetNamespace == "JSONRPC" && method == "Hello")) {
        // This is not the handshake message. If we've waited for it, consider this a protocol violation and drop connection
        if (m_newConnectionWaitTimers.contains(clientId)) {
            sendErrorResponse(interface, clientId, commandId, "Handshake required. Call JSONRPC.Hello first.");
            qCWarning(dcJsonRpc()) << "Connection requires a handshake but client did not initiate handshake. Dropping connection";
            interface->terminateClientConnection(clientId);
            return;
        }
    }

    // Attach the transportInterface if this call is for ourselves
    if (handler == this) {
        handler->setProperty("transportInterface", reinterpret_cast<qint64>(interface));
    }

    JsonContext callContext(clientId, m_clientLocales.value(clientId), interface->configuration().authenticationEnabled);
    callContext.setToken(token);

    qCDebug(dcJsonRpc()) << "Invoking method" << targetNamespace + '.' +  method << "from client" << clientId;

    JsonReply *reply = nullptr;
    if (handler->metaObject()->indexOfMethod(method.toUtf8() + "(QVariantMap,JsonContext)") >= 0) {
        QMetaObject::invokeMethod(handler, method.toUtf8().data(), Q_RETURN_ARG(JsonReply*, reply), Q_ARG(QVariantMap, params), Q_ARG(JsonContext, callContext));
    } else {
        QMetaObject::invokeMethod(handler, method.toUtf8().data(), Q_RETURN_ARG(JsonReply*, reply), Q_ARG(QVariantMap, params));
    }

    if (reply->type() == JsonReply::TypeAsync) {
        m_asyncReplies.insert(reply, interface);
        reply->setClientId(clientId);
        reply->setCommandId(commandId);
        connect(reply, &JsonReply::finished, this, &JsonRPCServerImplementation::asyncReplyFinished);
        reply->startWait();
    } else {
        JsonValidator validator;
        Q_ASSERT_X((targetNamespace == "JSONRPC" && method == "Introspect") || validator.validateReturns(reply->data(), targetNamespace + '.' + method, m_api).success(),
                   validator.result().where().toUtf8(),
                   validator.result().errorString().toUtf8() + "\nReturn value:\n" + QJsonDocument::fromVariant(reply->data()).toJson());

        QString deprecationWarning;
        if (m_api.value("methods").toMap().value(targetNamespace + '.' + method).toMap().contains("deprecated")) {
            deprecationWarning = m_api.value("methods").toMap().value(targetNamespace + '.' + method).toMap().value("deprecated").toString();
            qCWarning(dcJsonRpc()) << "Client uses deprecated API. Please update client implementation!";
            qCWarning(dcJsonRpc()) << targetNamespace + '.' + method + ':' << deprecationWarning;
        }

        sendResponse(interface, clientId, commandId, reply->data(), deprecationWarning);
        reply->deleteLater();
    }
}

void JsonRPCServerImplementation::sendNotification(const QVariantMap &params)
{
    JsonHandler *handler = qobject_cast<JsonHandler *>(sender());
    QMetaMethod method = handler->metaObject()->method(senderSignalIndex());

    QVariantMap notification;
    notification.insert("id", m_notificationId++);
    notification.insert("notification", handler->name() + "." + method.name());

    foreach (const QUuid &clientId, m_clientNotifications.keys()) {

        // Check if this client wants to be notified
        if (!m_clientNotifications.value(clientId).contains(handler->name())) {
            continue;
        }

        // Add deprecation warning if necessary
        if (m_api.value("notifications").toMap().value(handler->name() + '.' + method.name()).toMap().contains("deprecated")) {
            QString deprecationMessage = m_api.value("notifications").toMap().value(handler->name() + '.' + method.name()).toMap().value("deprecated").toString();
            qCWarning(dcJsonRpc()) << "Client" << clientId << "uses deprecated API. Please update client implementation!";
            qCWarning(dcJsonRpc()) << handler->name() + '.' + method.name() + ':' << deprecationMessage;
            notification.insert("deprecationWarning", deprecationMessage);
        }

        QLocale locale = m_clientLocales.value(clientId);
        QVariantMap translatedParams = handler->translateNotification(method.name(), params, locale);

        JsonValidator validator;
        Q_ASSERT_X(validator.validateNotificationParams(translatedParams, handler->name() + '.' + method.name(), m_api).success(),
                   validator.result().where().toUtf8(),
                   validator.result().errorString().toUtf8() + "\nGot:" + QJsonDocument::fromVariant(translatedParams).toJson(QJsonDocument::Indented));

        notification.insert("params", translatedParams);

        QByteArray data = QJsonDocument::fromVariant(notification).toJson(QJsonDocument::Compact);

        qCDebug(dcJsonRpc()) << "Sending notification" << handler->name() + "." + method.name() << "to client" << clientId;
        qCDebug(dcJsonRpcTraffic()) << "Notification content:" << data;

        m_clientTransports.value(clientId)->sendData(clientId, data);
    }
}

void JsonRPCServerImplementation::sendClientNotification(const QUuid &clientId, const QVariantMap &params)
{
    JsonHandler *handler = qobject_cast<JsonHandler *>(sender());
    QMetaMethod method = handler->metaObject()->method(senderSignalIndex());

    if (!m_clientTransports.contains(clientId)) {
        qCWarning(dcJsonRpc()) << "No client with id" << clientId << ". Not sending client notification.";
        return;
    }

    QVariantMap notification;
    notification.insert("id", m_notificationId++);
    notification.insert("notification", handler->name() + "." + method.name());
    notification.insert("params", params);

    JsonValidator validator;
    Q_ASSERT_X(validator.validateNotificationParams(params, handler->name() + '.' + method.name(), m_api).success(),
               validator.result().where().toUtf8(),
               validator.result().errorString().toUtf8() + "\nGot:" + QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented));

    if (m_api.value("notifications").toMap().value(handler->name() + '.' + method.name()).toMap().contains("deprecated")) {
        QString deprecationMessage = m_api.value("notifications").toMap().value(handler->name() + '.' + method.name()).toMap().value("deprecated").toString();
        qCWarning(dcJsonRpc()) << "Client uses deprecated API. Please update client implementation!";
        qCWarning(dcJsonRpc()) << handler->name() + '.' + method.name() + ':' << deprecationMessage;
        notification.insert("deprecationWarning", deprecationMessage);
    }

    QByteArray data = QJsonDocument::fromVariant(notification).toJson(QJsonDocument::Compact);
    qCDebug(dcJsonRpcTraffic()) << "Notification content:" << data;
    qCDebug(dcJsonRpc()) << "Sending notification:" << handler->name() + "." + method.name();
    m_clientTransports.value(clientId)->sendData(clientId, data);
}

void JsonRPCServerImplementation::sendClientNotification(const QVariantMap &params, const ThingId &thingId)
{
    JsonHandler *handler = qobject_cast<JsonHandler *>(sender());
    QMetaMethod method = handler->metaObject()->method(senderSignalIndex());

    QVariantMap notification;
    notification.insert("id", m_notificationId++);
    notification.insert("notification", handler->name() + "." + method.name());

    foreach (const QUuid &clientId, m_clientNotifications.keys()) {

        // Check if this client wants to be notified
        if (!m_clientNotifications.value(clientId).contains(handler->name()))
            continue;

        // Make sure this client is allowed to receive this notification
        TransportInterface *transport = m_clientTransports.value(clientId, nullptr);
        const bool authEnabled = transport ? transport->configuration().authenticationEnabled : true;
        if (authEnabled && m_clientTokens.contains(clientId)) {
            const QByteArray token = m_clientTokens.value(clientId);
            if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, token)) {
                qCDebug(dcJsonRpc()) << "Not sending notification to client" << "to client" << clientId.toString()
                << "due to missing thing permissions" << handler->name() + "." + method.name();
                continue;
            }
        }

        // Add deprecation warning if necessary
        if (m_api.value("notifications").toMap().value(handler->name() + '.' + method.name()).toMap().contains("deprecated")) {
            QString deprecationMessage = m_api.value("notifications").toMap().value(handler->name() + '.' + method.name()).toMap().value("deprecated").toString();
            qCWarning(dcJsonRpc()) << "Client" << clientId << "uses deprecated API. Please update client implementation!";
            qCWarning(dcJsonRpc()) << handler->name() + '.' + method.name() + ':' << deprecationMessage;
            notification.insert("deprecationWarning", deprecationMessage);
        }

        QLocale locale = m_clientLocales.value(clientId);
        QVariantMap translatedParams = handler->translateNotification(method.name(), params, locale);

        JsonValidator validator;
        Q_ASSERT_X(validator.validateNotificationParams(translatedParams, handler->name() + '.' + method.name(), m_api).success(),
                   validator.result().where().toUtf8(),
                   validator.result().errorString().toUtf8() + "\nGot:" + QJsonDocument::fromVariant(translatedParams).toJson(QJsonDocument::Indented));

        notification.insert("params", translatedParams);

        QByteArray data = QJsonDocument::fromVariant(notification).toJson(QJsonDocument::Compact);

        qCDebug(dcJsonRpc()) << "Sending notification" << handler->name() + "." + method.name() << "to client" << clientId;
        qCDebug(dcJsonRpcTraffic()) << "Notification content:" << data;

        m_clientTransports.value(clientId)->sendData(clientId, data);
    }
}

void JsonRPCServerImplementation::sendClientNotification(const QVariantMap &params, const UserInfo &userInfo)
{
    // Send client specific notifications
    qCDebug(dcJsonRpc()) << "Sending notification to client" << userInfo.username() << "connections...";
    for (auto it = m_clientTokens.constBegin(); it != m_clientTokens.constEnd(); ++it) {
        const QUuid clientId = it.key();
        const QByteArray token = it.value();

        TransportInterface *transport = m_clientTransports.value(clientId, nullptr);
        const bool authEnabled = transport ? transport->configuration().authenticationEnabled : true;

        if (!authEnabled) {
            sendClientNotification(clientId, params);
            continue;
        }

        if (token.isEmpty()) {
            continue;
        }

        if (NymeaCore::instance()->userManager()->tokenInfo(token).username() == userInfo.username()) {
            sendClientNotification(clientId, params);
        }
    }
}

void JsonRPCServerImplementation::asyncReplyFinished()
{
    JsonReply *reply = qobject_cast<JsonReply *>(sender());
    TransportInterface *interface = m_asyncReplies.take(reply);
    if (!interface) {
        qCWarning(dcJsonRpc()) << "Got an async reply but the requesting connection has vanished.";
        reply->deleteLater();
        return;
    }
    if (!reply->timedOut()) {
        JsonValidator validator;
        QString method = reply->handler()->name() + '.' + reply->method();
        Q_ASSERT_X(validator.validateReturns(reply->data(), method, m_api).success()
                   ,validator.result().where().toUtf8()
                   ,validator.result().errorString().toUtf8() + "\nReturn value:\n" + QJsonDocument::fromVariant(reply->data()).toJson());

        QString deprecationWarning;
        if (m_api.value("methods").toMap().value(method).toMap().contains("deprecated")) {
            deprecationWarning = m_api.value("methods").toMap().value(method).toMap().value("deprecated").toString();
            qCWarning(dcJsonRpc()) << "Client uses deprecated API. Please update client implementation!";
            qCWarning(dcJsonRpc()) << method + ':' << deprecationWarning;
        }

        sendResponse(interface, reply->clientId(), reply->commandId(), reply->data(), deprecationWarning);
    } else {
        qCWarning(dcJsonRpc()) << "RPC call timed out:" << reply->handler()->name() << ":" << reply->method();
        sendErrorResponse(interface, reply->clientId(), reply->commandId(), "Command timed out");
    }

    reply->deleteLater();
}

void JsonRPCServerImplementation::onPushButtonAuthFinished(int transactionId, bool success, const QByteArray &token)
{
    QUuid clientId = m_pushButtonTransactions.take(transactionId);
    if (clientId.isNull()) {
        qCDebug(dcJsonRpc()) << "Received a PushButton reply but wasn't expecting it.";
        return;
    }

    QVariantMap params;
    params.insert("transactionId", transactionId);
    params.insert("success", success);
    if (success) {
        params.insert("token", token);
        m_clientTokens[clientId] = token;
    }

    emit PushButtonAuthFinished(clientId, params);
}

bool JsonRPCServerImplementation::registerHandler(JsonHandler *handler)
{
    // Sanity checks on API:
    // * Make sure all $ref: entries are valid.
    // * A handler must not register a type name that is already registered by a previously loaded handler with different content.

    qCDebug(dcJsonRpc()).nospace() << "Registering JSON RPC handler: " << handler->name() << ". Performing sanity check";

    QVariantMap enums = m_api.value("enums").toMap();
    QVariantMap flags = m_api.value("flags").toMap();
    QVariantMap types = m_api.value("types").toMap();
    QVariantMap methods = m_api.value("methods").toMap();
    QVariantMap notifications = m_api.value("notifications").toMap();

    QVariantMap apiIncludingThis = m_api;

    // Verify enums name clash
    foreach (const QString &enumName, handler->jsonEnums().keys()) {
        QVariantList list = handler->jsonEnums().value(enumName).toList();
        if (enums.contains(enumName) && enums.value(enumName) != list) {
            qCWarning(dcJsonRpc()) << "Enum type" << enumName << "is already registered. Not registering handler" << handler->name();
            return false;
        }
        enums.insert(enumName, list);
    }
    apiIncludingThis["enums"] = enums;

    foreach (const QString &flagName, handler->jsonFlags().keys()) {
        QVariant flagDescription = handler->jsonFlags().value(flagName);
        if (enums.contains(flagName)) {
            qCWarning(dcJsonRpc()) << "Enum with name" << flagName << "is already registered. Not registering handler" << handler->name();
            return false;
        }
        if (flags.contains(flagName) && flags.value(flagName) != handler->jsonFlags().value(flagName)) {
            qCWarning(dcJsonRpc()) << "Flags with name" << flagName << "is already registered. Not registering handler" << handler->name();
            return false;
        }
        flags.insert(flagName, flagDescription);
    }
    apiIncludingThis["flags"] = flags;

    // Verify objects
    QVariantMap typesIncludingThis = types;
    foreach (const QString &objectName, handler->jsonObjects().keys()) {
        if (types.contains(objectName) && types.value(objectName) != handler->jsonObjects().value(objectName)) {
            qCWarning(dcJsonRpc()) << "Object type" << objectName << "is already registered and not the same. Not registering handler" << handler->name();
            qCWarning(dcJsonRpc()) << types.value(objectName);
            qCWarning(dcJsonRpc()) << handler->jsonObjects().value(objectName);
            return false;
        } else {
            typesIncludingThis.insert(objectName, handler->jsonObjects().value(objectName));
        }
    }

    apiIncludingThis["types"] = typesIncludingThis;
    foreach (const QString &objectName, handler->jsonObjects().keys()) {
        QVariantMap object = handler->jsonObjects().value(objectName).toMap();
        // Check for invalid $ref: entries
        if (!JsonValidator::checkRefs(object, apiIncludingThis)) {
            qCWarning(dcJsonRpc()).nospace() << "Invalid reference in object type " << objectName << ". Not registering handler " << handler->name();
            return false;
        }
    }

    // Verify methods
    QVariantMap newMethods;
    foreach (const QString &methodName, handler->jsonMethods().keys()) {
        QVariantMap method = handler->jsonMethods().value(methodName).toMap();

        if (handler->metaObject()->indexOfMethod(methodName.toUtf8() + "(QVariantMap)") < 0
            && handler->metaObject()->indexOfMethod(methodName.toUtf8() + "(QVariantMap,JsonContext)") < 0) {
            qCWarning(dcJsonRpc()).nospace().noquote() << "Invalid method \"" << methodName << "\". Method \"JsonReply* " + methodName + "(QVariantMap,JsonContext)\" does not exist. Not registering handler " << handler->name();
            return false;
        }
        if (!JsonValidator::checkRefs(method.value("params").toMap(), apiIncludingThis)) {
            qCWarning(dcJsonRpc()).nospace() << "Invalid reference in params of method " << methodName << ". Not registering handler " << handler->name();
            return false;
        }
        if (!JsonValidator::checkRefs(method.value("returns").toMap(), apiIncludingThis)) {
            qCWarning(dcJsonRpc()).nospace() << "Invalid reference in return value of method " << methodName << ". Not registering handler " << handler->name();
            return false;
        }
        newMethods.insert(handler->name() + '.' + methodName, method);
    }

    foreach (const QString &methodName, newMethods.keys()) {
        if (methods.contains(methodName)) {
            qCWarning(dcJsonRpc()).nospace() << "Method " << methodName << " has already been registered. Not registering handler " << handler->name();
            return false;
        }

        methods.insert(methodName, newMethods.value(methodName));
    }

    apiIncludingThis["methods"] = methods;

    // Verify notifications
    QVariantMap newNotifications;
    foreach (const QString &notificationName, handler->jsonNotifications().keys()) {
        QVariantMap notification = handler->jsonNotifications().value(notificationName).toMap();
        if (!JsonValidator::checkRefs(notification.value("params").toMap(), apiIncludingThis)) {
            qCWarning(dcJsonRpc()).nospace() << "Invalid reference in params of notification " << notificationName << ". Not registering handler " << handler->name();
            return false;
        }
        newNotifications.insert(handler->name() + '.' + notificationName, notification);
    }


    foreach (const QString &notificationName, newNotifications.keys()) {
        if (notifications.contains(notificationName)) {
            qCWarning(dcJsonRpc()).nospace() << "Notification " << notificationName << " has already been registered. Not registering handler " << handler->name();
            return false;
        }

        notifications.insert(notificationName, newNotifications.value(notificationName));
    }

    apiIncludingThis["notifications"] = notifications;

    // Checks completed. Store new API
    qCDebug(dcJsonRpc()) << "Registering JSON RPC handler:" << handler->name();
    m_api = apiIncludingThis;

    m_handlers.insert(handler->name(), handler);
    for (int i = 0; i < handler->metaObject()->methodCount(); ++i) {
        QMetaMethod method = handler->metaObject()->method(i);
        if (method.methodType() == QMetaMethod::Signal && QString(method.name()).contains(QRegularExpression("^[A-Z]"))) {
            if (method.parameterCount() == 1 && method.parameterType(0) == QMetaType::QVariantMap) {
                // Generic notification for all subscribed clients
                QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendNotification(QVariantMap)")));
            } else if (method.parameterCount() == 2 && method.parameterType(0) == QMetaType::QUuid && method.parameterType(1) == QMetaType::QVariantMap) {
                // Notifications for a specific client with the given UUID
                QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendClientNotification(QUuid,QVariantMap)")));
            } else if (method.parameterCount() == 2 && method.parameterType(0) == QMetaType::QVariantMap && method.parameterType(1) == QMetaType::type("ThingId")) {
                // Notifications which contains thing specific information which might be restricted for certain clients
                QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendClientNotification(QVariantMap,ThingId)")));
            } else if (method.parameterCount() == 2 && method.parameterType(0) == QMetaType::QVariantMap && method.parameterType(1) == QMetaType::type("nymeaserver::UserInfo")) {
                // Notifications for a specific user
                QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendClientNotification(QVariantMap,nymeaserver::UserInfo)")));
            }
        }
    }
    return true;
}

void JsonRPCServerImplementation::clientConnected(const QUuid &clientId)
{
    qCDebug(dcJsonRpc()) << "Client connected with uuid" << clientId.toString();
    TransportInterface *interface = qobject_cast<TransportInterface *>(sender());

    if (m_connectionLockdownTimer.isActive()) {
        qCWarning(dcJsonRpc()) << "Connection is locked down. Rejecting new client connection.";
        interface->terminateClientConnection(clientId);
        return;
    }

    m_clientTransports.insert(clientId, interface);

    // Initialize the connection locale to the settings default
    m_clientLocales.insert(clientId, NymeaCore::instance()->configuration()->locale());

    QTimer *timer = new QTimer(this);
    connect(timer, &QTimer::timeout, this, [this, timer, clientId, interface](){
        qCDebug(dcJsonRpc()) << "Client" << clientId << "did not initiate the handshake within the required timeout. Dropping connection.";
        timer->deleteLater();
        m_newConnectionWaitTimers.remove(clientId);
        interface->terminateClientConnection(clientId);
    });

    m_newConnectionWaitTimers.insert(clientId, timer);
    timer->start(10000);
}

void JsonRPCServerImplementation::clientDisconnected(const QUuid &clientId)
{
    qCDebug(dcJsonRpc()) << "Client disconnected:" << clientId;
    m_clientTransports.remove(clientId);
    m_clientNotifications.remove(clientId);
    m_clientBuffers.remove(clientId);
    m_clientLocales.remove(clientId);
    m_clientTokens.remove(clientId);

    if (m_pushButtonTransactions.values().contains(clientId)) {
        NymeaCore::instance()->userManager()->cancelPushButtonAuth(m_pushButtonTransactions.key(clientId));
    }

    if (m_newConnectionWaitTimers.contains(clientId)) {
        delete m_newConnectionWaitTimers.take(clientId);
    }
}

}
