// 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 libnymea-app.
*
* libnymea-app 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.
*
* libnymea-app 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 libnymea-app. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "networkmanager.h"
#include "types/networkdevices.h"
#include "types/networkdevice.h"
#include "types/wirelessaccesspoint.h"
#include "types/wirelessaccesspoints.h"

#include "engine.h"
#include "jsonrpc/jsonrpcclient.h"

#include <QMetaEnum>
#include <QJsonDocument>

#include "logging.h"

NYMEA_LOGGING_CATEGORY(dcNetworkManagement, "NetworkManagement")

NetworkManager::NetworkManager(QObject *parent):
    QObject(parent),
    m_wiredNetworkDevices(new WiredNetworkDevices(this)),
    m_wirelessNetworkDevices(new WirelessNetworkDevices(this))
{
}

NetworkManager::~NetworkManager()
{
    if (m_engine) {
        m_engine->jsonRpcClient()->unregisterNotificationHandler(this);
    }
}

void NetworkManager::setEngine(Engine *engine)
{
    if (m_engine && m_engine != engine) {
        // clean up
        m_engine->jsonRpcClient()->unregisterNotificationHandler(this);
    }

    m_engine = engine;
    emit engineChanged();

    m_engine->jsonRpcClient()->registerNotificationHandler(this, "NetworkManager", "notificationReceived");
    init();

    connect(m_engine->jsonRpcClient(), &JsonRpcClient::connectedChanged, this, &NetworkManager::init);
}

Engine *NetworkManager::engine() const
{
    return m_engine;
}

bool NetworkManager::loading()
{
    return m_loading;
}

void NetworkManager::init()
{
    m_wiredNetworkDevices->clear();
    m_wirelessNetworkDevices->clear();

    if (!m_engine->jsonRpcClient()->connected()) {
        // Not ready yet...
        return;
    }

    m_loading = true;
    emit loadingChanged();

    m_engine->jsonRpcClient()->sendCommand("NetworkManager.GetNetworkStatus", QVariantMap(), this, "getStatusResponse");
    m_engine->jsonRpcClient()->sendCommand("NetworkManager.GetNetworkDevices", QVariantMap(), this, "getDevicesResponse");
}

bool NetworkManager::available() const
{
    return m_available;
}

NetworkManager::NetworkManagerState NetworkManager::state() const
{
    return m_state;
}

bool NetworkManager::networkingEnabled() const
{
    return m_networkingEnabled;
}

bool NetworkManager::wirelessNetworkingEnabled() const
{
    return m_wirelessNetworkingEnabled;
}

WiredNetworkDevices *NetworkManager::wiredNetworkDevices() const
{
    return m_wiredNetworkDevices;
}

WirelessNetworkDevices *NetworkManager::wirelessNetworkDevices() const
{
    return m_wirelessNetworkDevices;
}

int NetworkManager::enableNetworking(bool enable)
{
    QVariantMap params;
    params.insert("enable", enable);
    return m_engine->jsonRpcClient()->sendCommand("NetworkManager.EnableNetworking", params, this, "enableNetworkingResponse");
}

int NetworkManager::enableWirelessNetworking(bool enable)
{
    QVariantMap params;
    params.insert("enable", enable);
    return m_engine->jsonRpcClient()->sendCommand("NetworkManager.EnableWirelessNetworking", params, this, "enableWirelessNetworkingResponse");
}

void NetworkManager::refreshWifis(const QString &interface)
{
    QVariantMap params;
    params.insert("interface", interface);
    int requestId = m_engine->jsonRpcClient()->sendCommand("NetworkManager.GetWirelessAccessPoints", params, this, "getAccessPointsResponse");
    m_apRequests.insert(requestId, interface);
}

int NetworkManager::connectToWiFi(const QString &interface, const QString &ssid, const QString &passphrase)
{
    QVariantMap params;
    params.insert("interface", interface);
    params.insert("ssid", ssid);
    params.insert("password", passphrase);
    return m_engine->jsonRpcClient()->sendCommand("NetworkManager.ConnectWifiNetwork", params, this, "connectToWiFiResponse");
}

int NetworkManager::startAccessPoint(const QString &interface, const QString &ssid, const QString &passphrase)
{
    QVariantMap params;
    params.insert("interface", interface);
    params.insert("ssid", ssid);
    params.insert("password", passphrase);
    return m_engine->jsonRpcClient()->sendCommand("NetworkManager.StartAccessPoint", params, this, "startAccessPointResponse");
}

int NetworkManager::createWiredAutoConnection(const QString &interface)
{
    QVariantMap params {
        {"interface", interface},
        {"type", "WiredNetworkConnectionTypeDHCP"}
    };
    return m_engine->jsonRpcClient()->sendCommand("NetworkManager.CreateWiredConnection", params, this, "createWiredAutoConnectionResponse");
}

int NetworkManager::createWiredManualConnection(const QString &interface, const QString &ip, quint8 prefix, const QString &gateway, const QString &dns)
{
    qCDebug(dcNetworkManagement()) << "Creating manual connection" << interface << ip << prefix << gateway << dns;
    QVariantMap params {
        {"interface", interface},
        {"type", "WiredNetworkConnectionTypeManual"},
        {"ip", ip},
        {"prefix", prefix},
        {"gateway", gateway},
        {"dns", dns}
    };
    return m_engine->jsonRpcClient()->sendCommand("NetworkManager.CreateWiredConnection", params, this, "createWiredManualConnectionResponse");
}

int NetworkManager::createWiredSharedConnection(const QString &interface, const QString &ip, quint8 prefix)
{
    qCDebug(dcNetworkManagement()) << "Creating shared connection" << interface << ip << prefix;
    QVariantMap params {
        {"interface", interface},
        {"type", "WiredNetworkConnectionTypeShared"},
        {"ip", ip},
        {"prefix", prefix}
    };
    return m_engine->jsonRpcClient()->sendCommand("NetworkManager.CreateWiredConnection", params, this, "createWiredSharedConnectionResponse");
}

int NetworkManager::disconnectInterface(const QString &interface)
{
    QVariantMap params;
    params.insert("interface", interface);
    return m_engine->jsonRpcClient()->sendCommand("NetworkManager.DisconnectInterface", params, this, "disconnectResponse");
}

void NetworkManager::getStatusResponse(int /*commandId*/, const QVariantMap &params)
{
    m_loading = false;
    emit loadingChanged();

    if (params.value("networkManagerError").toString() != "NetworkManagerErrorNoError") {
        qWarning() << "NetworkManager error:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented));
        m_available = false;
        emit availableChanged();
        return;
    }

    m_available = true;
    emit availableChanged();

    QVariantMap statusMap = params.value("status").toMap();

    QMetaEnum stateEnum = QMetaEnum::fromType<NetworkManager::NetworkManagerState>();
    NetworkManagerState state = static_cast<NetworkManager::NetworkManagerState>(stateEnum.keyToValue(statusMap.value("state").toString().toUtf8()));
    if (m_state != state) {
        m_state = state;
        emit stateChanged();
    }

    bool networkingEnabled = statusMap.value("networkingEnabled").toBool();
    if (m_networkingEnabled != networkingEnabled) {
        m_networkingEnabled = networkingEnabled;
        emit networkingEnabledChanged();
    }
    bool wirelessNetworkingEnabled = statusMap.value("wirelessNetworkingEnabled").toBool();
    if (m_wirelessNetworkingEnabled != wirelessNetworkingEnabled) {
        m_wirelessNetworkingEnabled = wirelessNetworkingEnabled;
        emit wirelessNetworkingEnabledChanged();
    }
}

void NetworkManager::getDevicesResponse(int /*commandId*/, const QVariantMap &params)
{
    qCDebug(dcNetworkManagement) << "Devices reply" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented));

    foreach (const QVariant &deviceVariant, params.value("wiredNetworkDevices").toList()) {
        QVariantMap deviceMap = deviceVariant.toMap();
        WiredNetworkDevice *device = new WiredNetworkDevice(deviceMap.value("macAddress").toString(), deviceMap.value("interface").toString(), this);
        device->setIpv4Addresses(deviceMap.value("ipv4Addresses").toStringList());
        device->setIpv6Addresses(deviceMap.value("ipv6Addresses").toStringList());
        device->setBitRate(deviceMap.value("bitRate").toString());
        QMetaEnum stateEnum = QMetaEnum::fromType<NetworkDevice::NetworkDeviceState>();
        device->setState(static_cast<NetworkDevice::NetworkDeviceState>(stateEnum.keyToValue(deviceMap.value("state").toString().toUtf8())));
        device->setPluggedIn(deviceMap.value("pluggedIn").toBool());
        m_wiredNetworkDevices->addNetworkDevice(device);
    }
    foreach (const QVariant &deviceVariant, params.value("wirelessNetworkDevices").toList()) {
        QVariantMap deviceMap = deviceVariant.toMap();
        WirelessNetworkDevice *device = new WirelessNetworkDevice(deviceMap.value("macAddress").toString(), deviceMap.value("interface").toString(), this);
        device->setIpv4Addresses(deviceMap.value("ipv4Addresses").toStringList());
        device->setIpv6Addresses(deviceMap.value("ipv6Addresses").toStringList());
        device->setBitRate(deviceMap.value("bitRate").toString());
        QMetaEnum stateEnum = QMetaEnum::fromType<NetworkDevice::NetworkDeviceState>();
        device->setState(static_cast<NetworkDevice::NetworkDeviceState>(stateEnum.keyToValue(deviceMap.value("state").toString().toUtf8())));
        QMetaEnum modeEnum = QMetaEnum::fromType<WirelessNetworkDevice::WirelessMode>();
        device->setWirelessMode(static_cast<WirelessNetworkDevice::WirelessMode>(modeEnum.keyToValue(deviceMap.value("mode").toString().toUtf8())));

        // Note: capabilities have been aaded in API 8.2
        if (m_engine->jsonRpcClient()->jsonRpcVersion() >= "8.2") {
            QMetaEnum capabilityEnum = QMetaEnum::fromType<WirelessNetworkDevice::WirelessCapability>();
            WirelessNetworkDevice::WirelessCapabilities wirelessCapabilities;
            foreach (const QVariant &capabilityVariant, deviceMap.value("capabilities").toList()) {
                QString capabilityString = capabilityVariant.toString();
                wirelessCapabilities.setFlag(static_cast<WirelessNetworkDevice::WirelessCapability>(capabilityEnum.keyToValue(capabilityString.toUtf8())));
            }
            device->setWirelessCapabilities(wirelessCapabilities);
        }


        QVariantMap currentApMap = deviceMap.value("currentAccessPoint").toMap();
        device->currentAccessPoint()->setSsid(currentApMap.value("ssid").toString());
        device->currentAccessPoint()->setMacAddress(currentApMap.value("macAddress").toString());
        device->currentAccessPoint()->setProtected(currentApMap.value("protected").toBool());
        device->currentAccessPoint()->setSignalStrength(currentApMap.value("signalStrength").toInt());
        device->currentAccessPoint()->setFrequency(currentApMap.value("frequency").toDouble());
        m_wirelessNetworkDevices->addNetworkDevice(device);
    }
}

void NetworkManager::getAccessPointsResponse(int commandId, const QVariantMap &params)
{
    qCDebug(dcNetworkManagement) << "Access points reply" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented));

    if (!m_apRequests.contains(commandId)) {
        qWarning() << "NetworkManager received a reply for a request we don't know!";
        return;
    }
    QString interface = m_apRequests.take(commandId);

    WirelessNetworkDevice *dev = m_wirelessNetworkDevices->getWirelessNetworkDevice(interface);
    if (!dev) {
        qWarning() << "NetworkManager received wifi list for" << interface << "but device disappeared";
        return;
    }

    dev->accessPoints()->clearModel();

    foreach (const QVariant &apVariant, params.value("wirelessAccessPoints").toList()) {
        QVariantMap apMap = apVariant.toMap();
        WirelessAccessPoint* ap = new WirelessAccessPoint(this);
        ap->setMacAddress(apMap.value("macAddress").toString());
        ap->setSsid(apMap.value("ssid").toString());
        ap->setProtected(apMap.value("protected").toBool());
        ap->setSignalStrength(apMap.value("signalStrength").toInt());
        ap->setFrequency(apMap.value("frequency").toDouble());
        dev->accessPoints()->addWirelessAccessPoint(ap);
    }

}

void NetworkManager::connectToWiFiResponse(int commandId, const QVariantMap &params)
{
    qCDebug(dcNetworkManagement) << "connect to wifi reply" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented));
    QString status = params.value("networkManagerError").toString();
    emit connectToWiFiReply(commandId, status);
}

void NetworkManager::disconnectResponse(int commandId, const QVariantMap &params)
{
    qCDebug(dcNetworkManagement) << "disconnect reply" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented));
    QString status = params.value("networkManagerError").toString();
    emit disconnectReply(commandId, status);
}

void NetworkManager::enableNetworkingResponse(int commandId, const QVariantMap &params)
{
    qCDebug(dcNetworkManagement) << "enable networking reply" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented));
    QString status = params.value("networkManagerError").toString();
    emit enableNetworkingReply(commandId, status);
}

void NetworkManager::enableWirelessNetworkingResponse(int commandId, const QVariantMap &params)
{
    qCDebug(dcNetworkManagement) << "enable wireless networking reply" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented));
    QString status = params.value("networkManagerError").toString();
    emit enableWirelessNetworkingReply(commandId, status);
}

void NetworkManager::startAccessPointResponse(int commandId, const QVariantMap &params)
{
    qCDebug(dcNetworkManagement) << "Start access point reply" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented));
    QString status = params.value("networkManagerError").toString();
    emit startAccessPointReply(commandId, status);
}

void NetworkManager::createWiredAutoConnectionResponse(int commandId, const QVariantMap &params)
{
    QString status = params.value("networkManagerError").toString();
    emit createWiredAutoConnectionReply(commandId, status);
}

void NetworkManager::createWiredManualConnectionResponse(int commandId, const QVariantMap &params)
{
    QString status = params.value("networkManagerError").toString();
    emit createWiredManualConnectionReply(commandId, status);
}

void NetworkManager::createWiredSharedConnectionResponse(int commandId, const QVariantMap &params)
{
    QString status = params.value("networkManagerError").toString();
    emit createWiredSharedConnectionReply(commandId, status);
}

void NetworkManager::notificationReceived(const QVariantMap &params)
{
    qCDebug(dcNetworkManagement) << "Network management Notification:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());

    QString notification = params.value("notification").toString();
    if (notification == "NetworkManager.WirelessNetworkDeviceAdded") {
        QVariantMap deviceMap = params.value("params").toMap().value("wirelessNetworkDevice").toMap();

        WirelessNetworkDevice *device = new WirelessNetworkDevice(deviceMap.value("macAddress").toString(), deviceMap.value("interface").toString(), this);
        device->setIpv4Addresses(deviceMap.value("ipv4Addresses").toStringList());
        device->setIpv6Addresses(deviceMap.value("ipv6Addresses").toStringList());
        device->setBitRate(deviceMap.value("bitRate").toString());
        QMetaEnum stateEnum = QMetaEnum::fromType<NetworkDevice::NetworkDeviceState>();
        device->setState(static_cast<NetworkDevice::NetworkDeviceState>(stateEnum.keyToValue(deviceMap.value("state").toString().toUtf8())));
        QMetaEnum modeEnum = QMetaEnum::fromType<WirelessNetworkDevice::WirelessMode>();
        device->setWirelessMode(static_cast<WirelessNetworkDevice::WirelessMode>(modeEnum.keyToValue(deviceMap.value("mode").toString().toUtf8())));

        QVariantMap currentApMap = deviceMap.value("currentAccessPoint").toMap();
        device->currentAccessPoint()->setSsid(currentApMap.value("ssid").toString());
        device->currentAccessPoint()->setMacAddress(currentApMap.value("macAddress").toString());
        device->currentAccessPoint()->setProtected(currentApMap.value("protected").toBool());
        device->currentAccessPoint()->setSignalStrength(currentApMap.value("signalStrength").toInt());
        device->currentAccessPoint()->setFrequency(currentApMap.value("frequency").toDouble());
        m_wirelessNetworkDevices->addNetworkDevice(device);

    } else if (notification == "NetworkManager.WirelessNetworkDeviceRemoved") {
        QString interface = params.value("params").toMap().value("interface").toString();
        m_wirelessNetworkDevices->removeNetworkDevice(interface);

    } else if (notification == "NetworkManager.WirelessNetworkDeviceChanged") {
        QVariantMap deviceMap = params.value("params").toMap().value("wirelessNetworkDevice").toMap();
        WirelessNetworkDevice* device = m_wirelessNetworkDevices->getWirelessNetworkDevice(deviceMap.value("interface").toString());
        if (!device) {
            qWarning() << "Received a notification for a WiFi device we don't know" << deviceMap;
            return;
        }
        device->setBitRate(deviceMap.value("bitRate").toString());
        device->setIpv4Addresses(deviceMap.value("ipv4Addresses").toStringList());
        device->setIpv6Addresses(deviceMap.value("ipv6Addresses").toStringList());
        QMetaEnum stateEnum = QMetaEnum::fromType<NetworkDevice::NetworkDeviceState>();
        device->setState(static_cast<NetworkDevice::NetworkDeviceState>(stateEnum.keyToValue(deviceMap.value("state").toString().toUtf8())));
        QMetaEnum modeEnum = QMetaEnum::fromType<WirelessNetworkDevice::WirelessMode>();
        device->setWirelessMode(static_cast<WirelessNetworkDevice::WirelessMode>(modeEnum.keyToValue(deviceMap.value("mode").toString().toUtf8())));

        QVariantMap currentApMap = deviceMap.value("currentAccessPoint").toMap();
        device->currentAccessPoint()->setSsid(currentApMap.value("ssid").toString());
        device->currentAccessPoint()->setMacAddress(currentApMap.value("macAddress").toString());
        device->currentAccessPoint()->setProtected(currentApMap.value("protected").toBool());
        device->currentAccessPoint()->setSignalStrength(currentApMap.value("signalStrength").toInt());

    } else if (notification == "NetworkManager.WiredNetworkDeviceAdded") {
        QVariantMap deviceMap = params.value("params").toMap().value("wiredNetworkDevice").toMap();
        WiredNetworkDevice *device = new WiredNetworkDevice(deviceMap.value("macAddress").toString(), deviceMap.value("interface").toString(), this);
        device->setIpv4Addresses(deviceMap.value("ipv4Addresses").toStringList());
        device->setIpv6Addresses(deviceMap.value("ipv6Addresses").toStringList());
        device->setBitRate(deviceMap.value("bitRate").toString());
        device->setPluggedIn(deviceMap.value("pluggedIn").toBool());
        QMetaEnum stateEnum = QMetaEnum::fromType<NetworkDevice::NetworkDeviceState>();
        device->setState(static_cast<NetworkDevice::NetworkDeviceState>(stateEnum.keyToValue(deviceMap.value("state").toString().toUtf8())));
        m_wiredNetworkDevices->addWiredNetworkDevice(device);

    } else if (notification == "NetworkManager.WiredNetworkDeviceRemoved") {
        QString interface = params.value("params").toMap().value("interface").toString();
        m_wiredNetworkDevices->removeNetworkDevice(interface);

    } else if (notification == "NetworkManager.WiredNetworkDeviceChanged") {
        QVariantMap deviceMap = params.value("params").toMap().value("wiredNetworkDevice").toMap();
        WiredNetworkDevice* device = m_wiredNetworkDevices->getWiredNetworkDevice(deviceMap.value("interface").toString());
        if (!device) {
            qCWarning(dcNetworkManagement) << "Received a notification for a network device we don't know" << deviceMap;
            return;
        }
        device->setIpv4Addresses(deviceMap.value("ipv4Addresses").toStringList());
        device->setIpv6Addresses(deviceMap.value("ipv6Addresses").toStringList());
        device->setBitRate(deviceMap.value("bitRate").toString());
        device->setPluggedIn(deviceMap.value("pluggedIn").toBool());
        QMetaEnum stateEnum = QMetaEnum::fromType<NetworkDevice::NetworkDeviceState>();
        device->setState(static_cast<NetworkDevice::NetworkDeviceState>(stateEnum.keyToValue(deviceMap.value("state").toString().toUtf8())));
    } else if (notification == "NetworkManager.NetworkStatusChanged") {
        QMetaEnum stateEnum = QMetaEnum::fromType<NetworkManager::NetworkManagerState>();
        NetworkManagerState state = static_cast<NetworkManager::NetworkManagerState>(stateEnum.keyToValue(params.value("params").toMap().value("status").toMap().value("state").toString().toUtf8()));
        bool networkingEnabled = params.value("params").toMap().value("status").toMap().value("networkingEnabled").toBool();
        bool wirelessNetworkingEnabled = params.value("params").toMap().value("status").toMap().value("wirelessNetworkingEnabled").toBool();
        if (m_state != state) {
            m_state = state;
            emit stateChanged();
        }
        if (m_networkingEnabled != networkingEnabled) {
            m_networkingEnabled = networkingEnabled;
            emit networkingEnabledChanged();
        }
        if (m_wirelessNetworkingEnabled != wirelessNetworkingEnabled) {
            m_wirelessNetworkingEnabled = wirelessNetworkingEnabled;
            emit wirelessNetworkingEnabledChanged();
        }

    } else {
        qCWarning(dcNetworkManagement) << "Unhandled notification received" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented));
    }
}
