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

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-experience-plugin-evdash.
*
* nymea-experience-plugin-evdash is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-experience-plugin-evdash 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-experience-plugin-evdash. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "evdashengine.h"
#include "chargingsessionsdbusinterfaceclient.h"
#include "energymanagerdbusclient.h"
#include "evdashsettings.h"
#include "evdashwebserverresource.h"

#include <integrations/thingmanager.h>
#include <logging/logengine.h>

#include <QHostAddress>
#include <QWebSocket>
#include <QWebSocketProtocol>
#include <QWebSocketServer>

#include <QDateTime>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QUuid>

#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(dcEvDashExperience)

EvDashEngine::EvDashEngine(ThingManager *thingManager, LogEngine *logEngine, EvDashWebServerResource *webServerResource, QObject *parent)
    : QObject{parent}
    , m_thingManager{thingManager}
    , m_logEngine{logEngine}
    , m_webServerResource{webServerResource}
{
    Things configuredThings = m_thingManager->configuredThings();
    foreach (Thing *thing, configuredThings) {
        if (isChargerThing(thing)) {
            m_chargers.append(thing);
            monitorChargerThing(thing);
        }

        if (isCarThing(thing)) {
            m_cars.append(thing);
            monitorCarThing(thing);
        }
    }

    connect(m_thingManager, &ThingManager::thingAdded, this, &EvDashEngine::onThingAdded);
    connect(m_thingManager, &ThingManager::thingRemoved, this, &EvDashEngine::onThingRemoved);
    connect(m_thingManager, &ThingManager::thingChanged, this, &EvDashEngine::onThingChanged);

    // Setup websocket server
    m_webSocketServer = new QWebSocketServer(QStringLiteral("EvDashEngine"), QWebSocketServer::NonSecureMode, this);

    connect(m_webSocketServer, &QWebSocketServer::newConnection, this, [this]() {
        QWebSocket *socket = m_webSocketServer->nextPendingConnection();
        if (!socket) {
            qCWarning(dcEvDashExperience()) << "Interface: Received new connection but socket was null";
            return;
        }

        connect(socket, &QWebSocket::textMessageReceived, this, [this, socket](const QString &message) { processTextMessage(socket, message); });

        connect(socket, &QWebSocket::disconnected, this, [this, socket]() {
            m_clients.removeAll(socket);
            m_authenticatedClients.remove(socket);
            qCDebug(dcEvDashExperience()) << "WebSocket client disconnected" << socket->peerAddress().toString() << "Remaining clients:" << m_clients.count();
            socket->deleteLater();
        });

        m_clients.append(socket);
        m_authenticatedClients.insert(socket, QString());
        qCDebug(dcEvDashExperience()) << "WebSocket client connected" << socket->peerAddress().toString() << "Total clients:" << m_clients.count();
    });

    connect(m_webSocketServer, &QWebSocketServer::acceptError, this, [this](QAbstractSocket::SocketError error) {
        qCWarning(dcEvDashExperience()) << "WebSocket accept error" << error << m_webSocketServer->errorString();
    });

    EvDashSettings settings;
    settings.beginGroup("General");
    m_webSocketPort = settings.value("webSocketServerPort", 4449).toUInt();
    bool enabled = settings.value("enabled", false).toBool();
    settings.endGroup();

    // ChargingSessions client for fetching charging sessions
    m_chargingSessionsClient = new ChargingSessionsDBusInterfaceClient(this);
    connect(m_chargingSessionsClient, &ChargingSessionsDBusInterfaceClient::sessionsReceived, this, &EvDashEngine::onSessionsReceived);

    connect(m_chargingSessionsClient, &ChargingSessionsDBusInterfaceClient::errorOccurred, this, &EvDashEngine::onSessionsError);

    // Energy manager client for associated cars and current mode
    m_energyManagerClient = new EnergyManagerDbusClient(this);
    connect(m_energyManagerClient, &EnergyManagerDbusClient::chargingConfigurationsUpdated, this, [](const QVariantList &chargingConfigurations) {
        qCDebug(dcEvDashExperience()) << "ChargingConfigurations:";
        foreach (const QVariant &ciVariant, chargingConfigurations) {
            qCDebug(dcEvDashExperience()) << "-->" << ciVariant.toMap();
        }
    });

    connect(m_energyManagerClient, &EnergyManagerDbusClient::chargingConfigurationAdded, this, [this](const QVariantMap &chargingConfiguration) {
        qCDebug(dcEvDashExperience()) << "ChargingConfiguration added:" << chargingConfiguration;
        Thing *charger = m_thingManager->findConfiguredThing(chargingConfiguration.value("evChargerId").toUuid());
        if (charger) {
            onThingChanged(charger);
        }
    });

    connect(m_energyManagerClient, &EnergyManagerDbusClient::chargingConfigurationChanged, this, [this](const QVariantMap &chargingConfiguration) {
        qCDebug(dcEvDashExperience()) << "ChargingConfiguration changed:" << chargingConfiguration;
        Thing *charger = m_thingManager->findConfiguredThing(chargingConfiguration.value("evChargerId").toUuid());
        if (charger) {
            onThingChanged(charger);
        }
    });

    connect(m_energyManagerClient, &EnergyManagerDbusClient::chargingConfigurationRemoved, this, [](const QString &evChargerId) {
        qCDebug(dcEvDashExperience()) << "ChargingConfiguration removed:" << evChargerId;
    });

    connect(m_energyManagerClient, &EnergyManagerDbusClient::errorOccurred, this, [](const QString &errorMessage) {
        qCWarning(dcEvDashExperience()) << "Energy manager DBus client error occurred:" << errorMessage;
    });

    qCDebug(dcEvDashExperience()) << "ChargingConfigurations:" << m_energyManagerClient->chargingConfigurations();
    foreach (const QVariant &ccVariant, m_energyManagerClient->chargingConfigurations()) {
        qCDebug(dcEvDashExperience()) << "-->" << ccVariant.toMap();
    }

    // Start the service if enabled
    setEnabled(enabled);
}

EvDashEngine::~EvDashEngine()
{
    stopWebSocketServer();
}

bool EvDashEngine::enabled() const
{
    return m_enabled;
}

bool EvDashEngine::setEnabled(bool enabled)
{
    m_enabled = enabled;

    if (m_enabled) {
        if (!startWebSocketServer(m_webSocketPort)) {
            return false;
        }
    } else {
        stopWebSocketServer();
    }

    qCDebug(dcEvDashExperience()) << "The EV Dash service is now" << (enabled ? "enabled" : "disabled");
    m_webServerResource->setEnabled(m_enabled);

    EvDashSettings settings;
    settings.beginGroup("General");
    settings.setValue("enabled", enabled);
    settings.endGroup();

    emit enabledChanged(m_enabled);
    return true;
}

void EvDashEngine::onThingAdded(Thing *thing)
{
    if (isChargerThing(thing)) {
        m_chargers.append(thing);
        monitorChargerThing(thing);
        sendNotification("ChargerAdded", packCharger(thing));
        verifyChargerStatusChanged(thing);
    }

    if (isCarThing(thing)) {
        m_cars.append(thing);
        monitorCarThing(thing);
        sendNotification("CarAdded", packCar(thing));
    }
}

void EvDashEngine::onThingRemoved(const ThingId &thingId)
{
    foreach (Thing *thing, m_chargers) {
        if (thing->id() == thingId) {
            qCDebug(dcEvDashExperience()) << "Charger has been removed.";
            sendNotification("ChargerRemoved", packCharger(thing));
            m_chargers.removeAll(thing);
            m_chargersStatusChangedCache.remove(thing);
            break;
        }
    }

    foreach (Thing *thing, m_cars) {
        if (thing->id() == thingId) {
            qCDebug(dcEvDashExperience()) << "Car has been removed.";
            m_cars.removeAll(thing);
            sendNotification("CarRemoved", packCar(thing));
            break;
        }
    }
}

void EvDashEngine::onThingChanged(Thing *thing)
{
    if (isChargerThing(thing)) {
        sendNotification("ChargerChanged", packCharger(thing));
        verifyChargerStatusChanged(thing);
    }

    if (isCarThing(thing))
        sendNotification("CarChanged", packCar(thing));
}

void EvDashEngine::monitorChargerThing(Thing *thing)
{
    connect(thing,
            &Thing::stateValueChanged,
            this,
            [this, thing](const StateTypeId &stateTypeId, const QVariant &value, const QVariant &minValue, const QVariant &maxValue, const QVariantList &possibleValues) {
                Q_UNUSED(stateTypeId)
                Q_UNUSED(value)
                Q_UNUSED(minValue)
                Q_UNUSED(maxValue)
                Q_UNUSED(possibleValues)

                onThingChanged(thing);
            });
}

void EvDashEngine::monitorCarThing(Thing *thing)
{
    connect(thing,
            &Thing::stateValueChanged,
            this,
            [this, thing](const StateTypeId &stateTypeId, const QVariant &value, const QVariant &minValue, const QVariant &maxValue, const QVariantList &possibleValues) {
                Q_UNUSED(stateTypeId)
                Q_UNUSED(value)
                Q_UNUSED(minValue)
                Q_UNUSED(maxValue)
                Q_UNUSED(possibleValues)

                onThingChanged(thing);
            });
}

void EvDashEngine::verifyChargerStatusChanged(Thing *charger)
{
    QString stateName = "status";
    QString source = QString("state-%1-%2").arg(charger->id().toString(QUuid::WithBraces), stateName);
    LogFetchJob *job = m_logEngine->fetchLogEntries({source}, {stateName}, {}, {}, {}, Types::SampleRateAny, Qt::DescendingOrder, 0, 1);
    connect(job, &LogFetchJob::finished, charger, [this, charger, stateName](const LogEntries &entries) {
        if (entries.isEmpty()) {
            qCDebug(dcEvDashExperience()) << "Last state change of" << charger->name() << stateName << "unknown";
            // Forget any cached values, the database did not return any information...
            m_chargersStatusChangedCache.remove(charger);
            return;
        }

        qint64 lastChangeTimestamp = entries.first().timestamp().toSecsSinceEpoch();
        qCDebug(dcEvDashExperience()) << "Last state change" << charger->name() << stateName << entries.first().timestamp().toString() << entries.first().values().first();

        if (!m_chargersStatusChangedCache.contains(charger)) {
            m_chargersStatusChangedCache.insert(charger, lastChangeTimestamp);
            sendNotification("ChargerChanged", packCharger(charger));
            return;
        }

        if (m_chargersStatusChangedCache.value(charger) != lastChangeTimestamp) {
            m_chargersStatusChangedCache[charger] = lastChangeTimestamp;
            sendNotification("ChargerChanged", packCharger(charger));
            return;
        }
    });
}

bool EvDashEngine::startWebSocketServer(quint16 port)
{
    if (m_webSocketServer->isListening()) {
        if (m_webSocketServer->serverPort() == port && port != 0)
            return true;

        m_webSocketServer->close();
    }

    const bool listening = m_webSocketServer->listen(QHostAddress::AnyIPv4, port);
    if (listening) {
        qCDebug(dcEvDashExperience()) << "WebSocket server listening on" << m_webSocketServer->serverAddress() << m_webSocketServer->serverPort();
    } else {
        qCWarning(dcEvDashExperience()) << "Failed to start WebSocket server" << m_webSocketServer->errorString();
    }

    emit webSocketListeningChanged(listening);
    return listening;
}

void EvDashEngine::stopWebSocketServer()
{
    if (m_webSocketServer->isListening())
        m_webSocketServer->close();

    for (QWebSocket *client : qAsConst(m_clients)) {
        if (client->state() == QAbstractSocket::ConnectedState)
            client->close(QWebSocketProtocol::CloseCodeGoingAway, QStringLiteral("Server shutting down"));

        client->deleteLater();
    }

    m_clients.clear();
    m_authenticatedClients.clear();
}

void EvDashEngine::processTextMessage(QWebSocket *socket, const QString &message)
{
    if (!socket)
        return;

    QJsonParseError parseError;
    const QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8(), &parseError);

    if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
        qCWarning(dcEvDashExperience()) << "Invalid WebSocket payload" << parseError.errorString();
        QJsonObject errorReply = createErrorResponse(QString(), QStringLiteral("invalidPayload"));
        sendReply(socket, errorReply);
        return;
    }

    qCDebug(dcEvDashExperience()) << "-->" << qUtf8Printable(doc.toJson(QJsonDocument::Compact));

    const QJsonObject requestObject = doc.object();
    const QString requestId = requestObject.value(QStringLiteral("requestId")).toString();
    const QString action = requestObject.value(QStringLiteral("action")).toString();

    if (action.isEmpty()) {
        QJsonObject response = createErrorResponse(requestId, QStringLiteral("invalidAction"));
        sendReply(socket, response);
        return;
    }

    const bool isAuthenticateAction = action.compare(QStringLiteral("authenticate"), Qt::CaseInsensitive) == 0;
    if (!isAuthenticateAction) {
        const QString token = m_authenticatedClients.value(socket);
        if (token.isEmpty()) {
            QJsonObject response = createErrorResponse(requestId, QStringLiteral("unauthenticated"));
            sendReply(socket, response);
            socket->close(QWebSocketProtocol::CloseCodePolicyViolated, QStringLiteral("Authentication required"));
            m_authenticatedClients.remove(socket);
            return;
        }
    }

    QJsonObject response = handleApiRequest(socket, requestObject);
    if (!response.isEmpty()) {
        sendReply(socket, response);

        if (isAuthenticateAction && !response.value(QStringLiteral("success")).toBool()) {
            socket->close(QWebSocketProtocol::CloseCodePolicyViolated, QStringLiteral("Authentication failed"));
            m_authenticatedClients.remove(socket);
        }
    }
}

QJsonObject EvDashEngine::handleApiRequest(QWebSocket *socket, const QJsonObject &request)
{
    qCDebug(dcEvDashExperience()) << "Handle API request" << request;

    const QString requestId = request.value(QStringLiteral("requestId")).toString();
    const QString action = request.value(QStringLiteral("action")).toString();

    if (action.compare(QStringLiteral("authenticate"), Qt::CaseInsensitive) == 0) {
        const QJsonObject payload = request.value(QStringLiteral("payload")).toObject();
        const QString token = payload.value(QStringLiteral("token")).toString();

        if (token.isEmpty())
            return createErrorResponse(requestId, QStringLiteral("missingToken"));

        if (!m_webServerResource || !m_webServerResource->validateToken(token)) {
            m_authenticatedClients.remove(socket);
            return createErrorResponse(requestId, QStringLiteral("unauthorized"));
        }

        m_authenticatedClients.insert(socket, token);

        QJsonObject responsePayload{{QStringLiteral("authenticated"), true}, {QStringLiteral("timestamp"), QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs)}};
        return createSuccessResponse(requestId, responsePayload);
    }

    if (action.compare(QStringLiteral("ping"), Qt::CaseInsensitive) == 0) {
        QJsonObject payload;
        payload.insert(QStringLiteral("timestamp"), QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs));

        const QJsonObject requestPayload = request.value(QStringLiteral("payload")).toObject();
        if (!requestPayload.isEmpty())
            payload.insert(QStringLiteral("echo"), requestPayload);

        return createSuccessResponse(requestId, payload);
    }

    if (action.compare(QStringLiteral("GetChargers"), Qt::CaseInsensitive) == 0) {
        QJsonObject payload;
        QJsonArray chargerList;
        foreach (Thing *thing, m_thingManager->configuredThings()) {
            if (isChargerThing(thing))
                chargerList.append(packCharger(thing));
        }

        payload.insert(QStringLiteral("chargers"), chargerList);
        return createSuccessResponse(requestId, payload);
    }

    if (action.compare(QStringLiteral("GetCars"), Qt::CaseInsensitive) == 0) {
        QJsonObject payload;
        QJsonArray carList;
        foreach (Thing *thing, m_thingManager->configuredThings()) {
            if (isCarThing(thing))
                carList.append(packCar(thing));
        }

        payload.insert(QStringLiteral("cars"), carList);
        return createSuccessResponse(requestId, payload);
    }

    if (action.compare(QStringLiteral("GetChargingSessions"), Qt::CaseInsensitive) == 0) {
        if (!m_chargingSessionsClient)
            return createErrorResponse(requestId, QStringLiteral("chargingSessionsUnavailable"));

        const QJsonObject payload = request.value(QStringLiteral("payload")).toObject();
        QStringList carThingIds;

        const QString carId = payload.value(QStringLiteral("carId")).toString();
        if (!carId.isEmpty()) {
            const QUuid carUuid = QUuid::fromString(carId);
            if (carUuid.isNull())
                return createErrorResponse(requestId, QStringLiteral("invalidCarId"));
            carThingIds.append(carUuid.toString(QUuid::WithoutBraces));
        } else {
            const QString chargerId = payload.value(QStringLiteral("chargerId")).toString();
            if (!chargerId.isEmpty())
                carThingIds = carThingIdsForCharger(chargerId);
        }

        m_pendingChargingSessionsRequests.insert(requestId, QPointer<QWebSocket>(socket));
        m_chargingSessionsClient->getSessions(carThingIds);
        return {};
    }

    return createErrorResponse(requestId, QStringLiteral("unknownAction"));
}

void EvDashEngine::sendReply(QWebSocket *socket, QJsonObject response) const
{
    if (!socket)
        return;

    const QJsonDocument replyDoc(response);
    qCDebug(dcEvDashExperience()) << "<--" << qUtf8Printable(replyDoc.toJson(QJsonDocument::Compact));
    socket->sendTextMessage(QString::fromUtf8(replyDoc.toJson(QJsonDocument::Compact)));
}

void EvDashEngine::sendNotification(const QString &notification, QJsonObject payload) const
{
    // Send to all active clients
    for (QWebSocket *client : qAsConst(m_clients)) {
        if (m_authenticatedClients.value(client).isEmpty())
            continue;

        QJsonObject notificationObject;
        notificationObject.insert(QStringLiteral("requestId"), QUuid::createUuid().toString(QUuid::WithoutBraces));
        notificationObject.insert("event", notification);
        notificationObject.insert("payload", payload);
        const QJsonDocument notificationDoc(notificationObject);
        qCDebug(dcEvDashExperience()) << "<--" << qUtf8Printable(notificationDoc.toJson(QJsonDocument::Compact));
        client->sendTextMessage(QString::fromUtf8(notificationDoc.toJson(QJsonDocument::Compact)));
    }
}

QJsonObject EvDashEngine::createSuccessResponse(const QString &requestId, const QJsonObject &payload) const
{
    QJsonObject response;
    if (!requestId.isEmpty())
        response.insert(QStringLiteral("requestId"), requestId);

    response.insert(QStringLiteral("success"), true);
    response.insert(QStringLiteral("payload"), payload.isEmpty() ? QJsonObject{} : payload);
    return response;
}

QJsonObject EvDashEngine::createErrorResponse(const QString &requestId, const QString &errorMessage) const
{
    QJsonObject response;
    if (!requestId.isEmpty())
        response.insert(QStringLiteral("requestId"), requestId);

    response.insert(QStringLiteral("success"), false);
    response.insert(QStringLiteral("error"), errorMessage);
    return response;
}

QJsonObject EvDashEngine::packCharger(Thing *charger) const
{
    QJsonObject chargerObject;
    chargerObject.insert("id", charger->id().toString(QUuid::WithoutBraces));
    chargerObject.insert("name", charger->name());

    foreach (const QVariant &chargingConfigurationVariant, m_energyManagerClient->chargingConfigurations()) {
        QVariantMap chargingConfiguration = chargingConfigurationVariant.toMap();
        if (chargingConfiguration.value("evChargerId").toUuid() == charger->id()) {
            // Set assigned car name
            if (chargingConfiguration.value("assignedCarId").toString().isEmpty()) {
                chargerObject.insert("assignedCar", "");
            } else {
                Thing *car = m_thingManager->findConfiguredThing(chargingConfiguration.value("assignedCarId").toUuid());
                if (car) {
                    chargerObject.insert("assignedCar", car->name());
                } else {
                    chargerObject.insert("assignedCar", "");
                }
            }

            // Set energyManagerMode
            chargerObject.insert("energyManagerMode", chargingConfiguration.value("chargingMode").toInt());
        }
    }

    chargerObject.insert("connected", charger->stateValue("connected").toBool());
    chargerObject.insert("chargingCurrent", charger->stateValue("maxChargingCurrent").toDouble());
    chargerObject.insert("currentPower", charger->stateValue("currentPower").toDouble());
    chargerObject.insert("pluggedIn", charger->stateValue("pluggedIn").toBool());
    chargerObject.insert("chargingAllowed", charger->stateValue("power").toBool());

    if (charger->hasState("currentVersion"))
        chargerObject.insert("version", charger->stateValue("currentVersion").toDouble());

    if (charger->hasState("sessionEnergy"))
        chargerObject.insert("sessionEnergy", charger->stateValue("sessionEnergy").toDouble());

    if (charger->hasState("desiredPhaseCount"))
        chargerObject.insert("chargingPhases", charger->stateValue("desiredPhaseCount").toInt());

    // PCE specific
    if (charger->hasState("temperature"))
        chargerObject.insert("temperature", charger->stateValue("temperature").toDouble());

    if (charger->hasState("error"))
        chargerObject.insert("error", charger->stateValue("error").toString());

    if (charger->hasState("status"))
        chargerObject.insert("status", charger->stateValue("status").toString());

    if (m_chargersStatusChangedCache.contains(charger))
        chargerObject.insert("lastStatusUpdate", m_chargersStatusChangedCache.value(charger));

    if (charger->hasState("digitalInputMode"))
        chargerObject.insert("digitalInputMode", charger->stateValue("digitalInputMode").toInt());

    return chargerObject;
}

QJsonObject EvDashEngine::packCar(Thing *car) const
{
    QJsonObject carObject;
    if (!car)
        return carObject;

    carObject.insert("id", car->id().toString(QUuid::WithoutBraces));
    carObject.insert("name", car->name());

    return carObject;
}

bool EvDashEngine::isChargerThing(Thing *thing) const
{
    if (!thing)
        return false;

    const QStringList interfaces = thing->thingClass().interfaces();
    return interfaces.contains(QStringLiteral("evcharger"));
}

bool EvDashEngine::isCarThing(Thing *thing) const
{
    if (!thing)
        return false;

    const QStringList interfaces = thing->thingClass().interfaces();
    return interfaces.contains(QStringLiteral("electricvehicle"));
}

QStringList EvDashEngine::carThingIdsForCharger(const QString &chargerId) const
{
    QStringList carThingIds;
    if (!m_energyManagerClient || chargerId.isEmpty())
        return carThingIds;

    const QUuid chargerUuid = QUuid::fromString(chargerId);
    if (chargerUuid.isNull())
        return carThingIds;

    for (const QVariant &ciVariant : m_energyManagerClient->chargingConfigurations()) {
        const QVariantMap chargingConfiguration = ciVariant.toMap();
        if (chargingConfiguration.value(QStringLiteral("evChargerId")).toUuid() != chargerUuid)
            continue;

        const QString assignedCarId = chargingConfiguration.value(QStringLiteral("assignedCarId")).toString();
        if (!assignedCarId.isEmpty())
            carThingIds.append(assignedCarId);

        break;
    }

    return carThingIds;
}

void EvDashEngine::onSessionsReceived(const QList<QVariantMap> &sessions)
{
    qCDebug(dcEvDashExperience()) << "ChargingSessions received:" << sessions.count();

    QJsonArray sessionArray;
    for (const QVariantMap &session : sessions)
        sessionArray.append(QJsonObject::fromVariantMap(session));

    QJsonObject payload;
    payload.insert(QStringLiteral("sessions"), sessionArray);

    const QList<QString> pendingRequestIds = m_pendingChargingSessionsRequests.keys();
    for (const QString &requestId : pendingRequestIds) {
        QPointer<QWebSocket> socket = m_pendingChargingSessionsRequests.take(requestId);
        if (!socket)
            continue;

        sendReply(socket, createSuccessResponse(requestId, payload));
    }

    sendNotification(QStringLiteral("chargingSessionsUpdated"), payload);
}

void EvDashEngine::onSessionsError(const QString &errorMessage)
{
    qCWarning(dcEvDashExperience()) << "Charging sessions DBus client error occurred:" << errorMessage;

    const QList<QString> pendingRequestIds = m_pendingChargingSessionsRequests.keys();
    for (const QString &requestId : pendingRequestIds) {
        QPointer<QWebSocket> socket = m_pendingChargingSessionsRequests.take(requestId);
        if (!socket)
            continue;

        sendReply(socket, createErrorResponse(requestId, errorMessage));
    }
}
