// 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-networkmanager.
*
* libnymea-networkmanager 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-networkmanager 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-networkmanager. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*!
    \class BluetoothServer
    \brief Represents a bluetooth LE server for network-manager remote configuration.
    \inmodule nymea-networkmanager
    \ingroup networkmanager-bluetooth

*/

#include "bluetoothserver.h"
#include "../networkmanager.h"
#include "../networkmanagerutils.h"
#include "bluetoothuuids.h"

#include <QFile>
#include <QSysInfo>
#include <QBluetoothUuid>

BluetoothServer::BluetoothServer(NetworkManager *networkManager) :
    QObject(networkManager),
    m_networkManager(networkManager)
{

}

BluetoothServer::~BluetoothServer()
{
    qCDebug(dcNetworkManagerBluetoothServer()) << "Destroy bluetooth server.";
    if (m_controller)
        m_controller->stopAdvertising();

    if (m_localDevice)
        m_localDevice->setHostMode(QBluetoothLocalDevice::HostConnectable);

}

QString BluetoothServer::advertiseName() const
{
    return m_advertiseName;
}

void BluetoothServer::setAdvertiseName(const QString &advertiseName, bool forceFullName)
{
    m_advertiseName = advertiseName;
    m_forceFullName = forceFullName;
}

QString BluetoothServer::modelName() const
{
    return m_modelName;
}

void BluetoothServer::setModelName(const QString &modelName)
{
    m_modelName = modelName;
}

QString BluetoothServer::softwareVersion() const
{
    return m_softwareVersion;
}

void BluetoothServer::setSoftwareVersion(const QString &softwareVersion)
{
    m_softwareVersion = softwareVersion;
}

QString BluetoothServer::hardwareVersion() const
{
    return m_hardwareVersion;
}

void BluetoothServer::setHardwareVersion(const QString &hardwareVersion)
{
    m_hardwareVersion = hardwareVersion;
}

QString BluetoothServer::serialNumber() const
{
    return m_serialNumber;
}

void BluetoothServer::setSerialNumber(const QString &serialNumber)
{
    m_serialNumber = serialNumber;
}

bool BluetoothServer::running() const
{
    return m_running;
}

bool BluetoothServer::connected() const
{
    return m_connected;
}

QLowEnergyServiceData BluetoothServer::deviceInformationServiceData()
{
    QLowEnergyServiceData serviceData;
    serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary);
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
    serviceData.setUuid(QBluetoothUuid::ServiceClassUuid::DeviceInformation);
#else
    serviceData.setUuid(QBluetoothUuid::DeviceInformation);
#endif

    // Model number string 0x2a24
    QLowEnergyCharacteristicData modelNumberCharData;
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
    modelNumberCharData.setUuid(QBluetoothUuid::CharacteristicType::ModelNumberString);
#else
    modelNumberCharData.setUuid(QBluetoothUuid::ModelNumberString);
#endif
    if (m_modelName.isEmpty()) {
        modelNumberCharData.setValue(QString("N.A.").toUtf8());
    } else {
        modelNumberCharData.setValue(m_modelName.toUtf8());
    }

    modelNumberCharData.setProperties(QLowEnergyCharacteristic::Read);
    serviceData.addCharacteristic(modelNumberCharData);

    // Serial number string 0x2a25
    QLowEnergyCharacteristicData serialNumberCharData;
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
    serialNumberCharData.setUuid(QBluetoothUuid::CharacteristicType::SerialNumberString);
#else
    serialNumberCharData.setUuid(QBluetoothUuid::SerialNumberString);
#endif
    if (m_serialNumber.isNull()) {
        // Note: if no serialnumber specified use the system uuid from /etc/machine-id
        qCDebug(dcNetworkManagerBluetoothServer()) << "Serial number not specified. Using system uuid from /etc/machine-id as serialnumber.";
        m_serialNumber = readMachineId().toString();
    }
    serialNumberCharData.setValue(m_serialNumber.toUtf8());
    serialNumberCharData.setProperties(QLowEnergyCharacteristic::Read);
    serviceData.addCharacteristic(serialNumberCharData);

    // Firmware revision string 0x2a26
    QLowEnergyCharacteristicData firmwareRevisionCharData;
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
    firmwareRevisionCharData.setUuid(QBluetoothUuid::CharacteristicType::FirmwareRevisionString);
#else
    firmwareRevisionCharData.setUuid(QBluetoothUuid::FirmwareRevisionString);
#endif
    firmwareRevisionCharData.setValue(QString(VERSION_STRING).toUtf8());
    firmwareRevisionCharData.setProperties(QLowEnergyCharacteristic::Read);
    serviceData.addCharacteristic(firmwareRevisionCharData);

    // Hardware revision string 0x2a27
    QLowEnergyCharacteristicData hardwareRevisionCharData;
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
    hardwareRevisionCharData.setUuid(QBluetoothUuid::CharacteristicType::HardwareRevisionString);
#else
    hardwareRevisionCharData.setUuid(QBluetoothUuid::HardwareRevisionString);
#endif
    hardwareRevisionCharData.setValue(m_hardwareVersion.toUtf8());
    hardwareRevisionCharData.setProperties(QLowEnergyCharacteristic::Read);
    serviceData.addCharacteristic(hardwareRevisionCharData);

    // Software revision string 0x2a28
    QLowEnergyCharacteristicData softwareRevisionCharData;
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
    softwareRevisionCharData.setUuid(QBluetoothUuid::CharacteristicType::SoftwareRevisionString);
#else
    softwareRevisionCharData.setUuid(QBluetoothUuid::SoftwareRevisionString);
#endif
    softwareRevisionCharData.setValue(m_softwareVersion.toUtf8());
    softwareRevisionCharData.setProperties(QLowEnergyCharacteristic::Read);
    serviceData.addCharacteristic(softwareRevisionCharData);

    // Manufacturer name string 0x2a29
    QLowEnergyCharacteristicData manufacturerNameCharData;
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
    manufacturerNameCharData.setUuid(QBluetoothUuid::CharacteristicType::ManufacturerNameString);
#else
    manufacturerNameCharData.setUuid(QBluetoothUuid::ManufacturerNameString);
#endif
    manufacturerNameCharData.setValue(QString("nymea GmbH").toUtf8());
    manufacturerNameCharData.setProperties(QLowEnergyCharacteristic::Read);
    serviceData.addCharacteristic(manufacturerNameCharData);

    return serviceData;
}

QLowEnergyServiceData BluetoothServer::genericAccessServiceData()
{
    QLowEnergyServiceData serviceData;
    serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary);
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
    serviceData.setUuid(QBluetoothUuid::ServiceClassUuid::GenericAccess);
#else
    serviceData.setUuid(QBluetoothUuid::GenericAccess);
#endif

    // Device name 0x2a00
    QLowEnergyCharacteristicData nameCharData;
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
    nameCharData.setUuid(QBluetoothUuid::CharacteristicType::DeviceName);
#else
    nameCharData.setUuid(QBluetoothUuid::DeviceName);
#endif
    if (m_advertiseName.isNull()) {
        qCWarning(dcNetworkManagerBluetoothServer()) << "Advertise name not specified. Using system host name as device name.";
        m_advertiseName = QSysInfo::machineHostName();
    }
    nameCharData.setValue(m_advertiseName.toUtf8());
    nameCharData.setProperties(QLowEnergyCharacteristic::Read);
    serviceData.addCharacteristic(nameCharData);

    // Appearance 0x2a01
    QLowEnergyCharacteristicData appearanceCharData;
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
    appearanceCharData.setUuid(QBluetoothUuid::CharacteristicType::Appearance);
#else
    appearanceCharData.setUuid(QBluetoothUuid::Appearance);
#endif
    appearanceCharData.setValue(QByteArray(4, 0));
    appearanceCharData.setProperties(QLowEnergyCharacteristic::Read);
    serviceData.addCharacteristic(appearanceCharData);

    // Peripheral Privacy Flag 0x2a02
    QLowEnergyCharacteristicData privacyFlagCharData;
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
    privacyFlagCharData.setUuid(QBluetoothUuid::CharacteristicType::PeripheralPrivacyFlag);
#else
    privacyFlagCharData.setUuid(QBluetoothUuid::PeripheralPrivacyFlag);
#endif
    privacyFlagCharData.setValue(QByteArray(2, 0));
    privacyFlagCharData.setProperties(QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::Write);
    serviceData.addCharacteristic(privacyFlagCharData);

    // Reconnection Address 0x2a03
    QLowEnergyCharacteristicData reconnectionAddressCharData;
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
    reconnectionAddressCharData.setUuid(QBluetoothUuid::CharacteristicType::ReconnectionAddress);
#else
    reconnectionAddressCharData.setUuid(QBluetoothUuid::ReconnectionAddress);
#endif
    reconnectionAddressCharData.setValue(QByteArray());
    reconnectionAddressCharData.setProperties(QLowEnergyCharacteristic::Write);
    serviceData.addCharacteristic(reconnectionAddressCharData);

    return serviceData;
}

QLowEnergyServiceData BluetoothServer::genericAttributeServiceData()
{
    QLowEnergyServiceData serviceData;
    serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary);
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
    serviceData.setUuid(QBluetoothUuid::ServiceClassUuid::GenericAttribute);
#else
    serviceData.setUuid(QBluetoothUuid::GenericAttribute);
#endif

    QLowEnergyCharacteristicData charData;
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
    charData.setUuid(QBluetoothUuid::CharacteristicType::ServiceChanged);
#else
    charData.setUuid(QBluetoothUuid::ServiceChanged);
#endif
    charData.setProperties(QLowEnergyCharacteristic::Indicate);

    serviceData.addCharacteristic(charData);

    return serviceData;
}

void BluetoothServer::setRunning(bool running)
{
    if (m_running == running)
        return;
    qCDebug(dcNetworkManagerBluetoothServer()) << "Set running" << running;
    m_running = running;
    emit runningChanged(m_running);
}

void BluetoothServer::setConnected(bool connected)
{
    if (m_connected == connected)
        return;

    qCDebug(dcNetworkManagerBluetoothServer()) << "Set connected" << connected;
    m_connected = connected;
    emit connectedChanged(m_connected);
}

QUuid BluetoothServer::readMachineId()
{
    QUuid systemUuid;
    QFile systemUuidFile("/etc/machine-id");
    if (systemUuidFile.open(QFile::ReadOnly)) {
        QString tmpId = QString::fromLatin1(systemUuidFile.readAll()).trimmed();
        tmpId.insert(8, "-");
        tmpId.insert(13, "-");
        tmpId.insert(18, "-");
        tmpId.insert(23, "-");
        systemUuid = QUuid(tmpId);
    } else {
        qCWarning(dcNetworkManagerBluetoothServer()) << "Failed to open /etc/machine-id for reading the system uuid as device information serialnumber.";
    }
    systemUuidFile.close();

    return systemUuid;
}

void BluetoothServer::onHostModeStateChanged(const QBluetoothLocalDevice::HostMode mode)
{
    switch (mode) {
    case QBluetoothLocalDevice::HostConnectable:
        qCDebug(dcNetworkManagerBluetoothServer()) << "Bluetooth host in connectable mode.";
        break;
    case QBluetoothLocalDevice::HostDiscoverable:
        qCDebug(dcNetworkManagerBluetoothServer()) << "Bluetooth host in discoverable mode.";
        break;
    case QBluetoothLocalDevice::HostPoweredOff:
        qCDebug(dcNetworkManagerBluetoothServer()) << "Bluetooth host in power off mode.";
        setRunning(false);
        break;
    case QBluetoothLocalDevice::HostDiscoverableLimitedInquiry:
        qCDebug(dcNetworkManagerBluetoothServer()) << "Bluetooth host in discoverable limited inquiry mode.";
        break;
    }
}

void BluetoothServer::onDeviceConnected(const QBluetoothAddress &address)
{
    qCDebug(dcNetworkManagerBluetoothServer()) << "Device connected" << address.toString();
}

void BluetoothServer::onDeviceDisconnected(const QBluetoothAddress &address)
{
    qCDebug(dcNetworkManagerBluetoothServer()) << "Device disconnected" << address.toString();
    setConnected(false);
}

void BluetoothServer::onError(QLowEnergyController::Error error)
{
    qCWarning(dcNetworkManagerBluetoothServer()) << "Bluetooth error occurred:" << error << m_controller->errorString();
}

void BluetoothServer::onConnected()
{
    qCDebug(dcNetworkManagerBluetoothServer()) << "Client connected" << m_controller->remoteName() << m_controller->remoteAddress();
    setConnected(true);
}

void BluetoothServer::onDisconnected()
{
    qCDebug(dcNetworkManagerBluetoothServer()) << "Client disconnected";
    setConnected(false);
    stop();
}

void BluetoothServer::onControllerStateChanged(QLowEnergyController::ControllerState state)
{
    switch (state) {
    case QLowEnergyController::UnconnectedState:
        qCDebug(dcNetworkManagerBluetoothServer()) << "Controller state disonnected.";
        setConnected(false);
        setRunning(false);
        break;
    case QLowEnergyController::ConnectingState:
        qCDebug(dcNetworkManagerBluetoothServer()) << "Controller state connecting...";
        setConnected(false);
        break;
    case QLowEnergyController::ConnectedState:
        qCDebug(dcNetworkManagerBluetoothServer()) << "Controller state connected." << m_controller->remoteName() << m_controller->remoteAddress();
        setConnected(true);
        break;
    case QLowEnergyController::DiscoveringState:
        qCDebug(dcNetworkManagerBluetoothServer()) << "Controller state discovering...";
        break;
    case QLowEnergyController::DiscoveredState:
        qCDebug(dcNetworkManagerBluetoothServer()) << "Controller state discovered.";
        break;
    case QLowEnergyController::ClosingState:
        qCDebug(dcNetworkManagerBluetoothServer()) << "Controller state closing...";
        break;
    case QLowEnergyController::AdvertisingState:
        qCDebug(dcNetworkManagerBluetoothServer()) << "Controller state advertising...";
        setRunning(true);
        break;
    }
}

void BluetoothServer::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &value)
{
    qCDebug(dcNetworkManagerBluetoothServer()) << "Service characteristic changed" << characteristic.uuid() << value;
}

void BluetoothServer::characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &value)
{
    qCDebug(dcNetworkManagerBluetoothServer()) << "Service characteristic read" << characteristic.uuid() << value;
}

void BluetoothServer::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &value)
{
    qCDebug(dcNetworkManagerBluetoothServer()) << "Service characteristic written" << characteristic.uuid() << value;
}

void BluetoothServer::descriptorRead(const QLowEnergyDescriptor &descriptor, const QByteArray &value)
{
    qCDebug(dcNetworkManagerBluetoothServer()) << "Descriptor read" << descriptor.uuid() << value;
}

void BluetoothServer::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &value)
{
    qCDebug(dcNetworkManagerBluetoothServer()) << "Descriptor written" << descriptor.uuid() << value;
}

void BluetoothServer::serviceError(QLowEnergyService::ServiceError error)
{
    QString errorString;
    switch (error) {
    case QLowEnergyService::NoError:
        errorString = "No error";
        break;
    case QLowEnergyService::OperationError:
        errorString = "Operation error";
        break;
    case QLowEnergyService::CharacteristicReadError:
        errorString = "Characteristic read error";
        break;
    case QLowEnergyService::CharacteristicWriteError:
        errorString = "Characteristic write error";
        break;
    case QLowEnergyService::DescriptorReadError:
        errorString = "Descriptor read error";
        break;
    case QLowEnergyService::DescriptorWriteError:
        errorString = "Descriptor write error";
        break;
    case QLowEnergyService::UnknownError:
        errorString = "Unknown error";
        break;
    }

    qCWarning(dcNetworkManagerBluetoothServer()) << "Service error:" << errorString;
}

void BluetoothServer::start()
{
    if (running()) {
        qCDebug(dcNetworkManagerBluetoothServer()) << "Start Bluetooth server called but the server is already running.  Doing nothing.";
        return;
    }

    if (connected()) {
        qCDebug(dcNetworkManagerBluetoothServer()) << "Start Bluetooth server called but the server is running and a client is connected. Doing nothing.";
        return;
    }

    qCDebug(dcNetworkManagerBluetoothServer()) << "-------------------------------------";
    qCDebug(dcNetworkManagerBluetoothServer()) << "Starting bluetooth server...";
    qCDebug(dcNetworkManagerBluetoothServer()) << "-------------------------------------";

    // Local bluetooth device
    m_localDevice = new QBluetoothLocalDevice(this);
    if (!m_localDevice->isValid()) {
        qCWarning(dcNetworkManagerBluetoothServer()) << "Local bluetooth device is not valid.";
        delete m_localDevice;
        m_localDevice = nullptr;
        return;
    }

    connect(m_localDevice, &QBluetoothLocalDevice::hostModeStateChanged, this, &BluetoothServer::onHostModeStateChanged);
    connect(m_localDevice, &QBluetoothLocalDevice::deviceConnected, this, &BluetoothServer::onDeviceConnected);
    connect(m_localDevice, &QBluetoothLocalDevice::deviceDisconnected, this, &BluetoothServer::onDeviceDisconnected);

    qCDebug(dcNetworkManagerBluetoothServer()) << "Local device" << m_localDevice->name() << m_localDevice->address().toString();
    m_localDevice->setHostMode(QBluetoothLocalDevice::HostDiscoverable);
    m_localDevice->powerOn();

    // Bluetooth low energy periperal controller
    m_controller = QLowEnergyController::createPeripheral(this);
    connect(m_controller, &QLowEnergyController::stateChanged, this, &BluetoothServer::onControllerStateChanged);
    connect(m_controller, &QLowEnergyController::connected, this, &BluetoothServer::onConnected);
    connect(m_controller, &QLowEnergyController::disconnected, this, &BluetoothServer::onDisconnected);

#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
    connect(m_controller,  &QLowEnergyController::errorOccurred, this, &BluetoothServer::onError);
#else
    connect(m_controller, static_cast<void(QLowEnergyController::*)(QLowEnergyController::Error)>
            (&QLowEnergyController::error), this, &BluetoothServer::onError);
#endif

    // Note: https://www.bluetooth.com/specifications/gatt/services
    m_deviceInfoService = m_controller->addService(deviceInformationServiceData(), m_controller);
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
    m_genericAccessService = m_controller->addService(genericAccessServiceData(), m_controller);
    m_genericAttributeService = m_controller->addService(genericAttributeServiceData(), m_controller);
#endif

    // Create custom services
    m_networkService = new NetworkService(m_controller->addService(NetworkService::serviceData(m_networkManager), m_controller),
                                          m_networkManager, m_controller);
    m_wirelessService = new WirelessService(m_controller->addService(WirelessService::serviceData(m_networkManager), m_controller),
                                            m_networkManager, m_controller);

    QLowEnergyAdvertisingData advertisingData;
    advertisingData.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral);
    // Setting the wireless service UUID to the advertise data.
    // Given that nymea-networkmanager doesn't have a registered service UUID (yet), we need
    // to write the full UUID which takes up most of the advertise data space. Because of
    // this, we can only fit 8 character device name and nothing else.
    // Registering a service UUID with the BT SIG would allow to use shortened uuids and leave
    // more space for device name, multiple uuids, or other info (in that case we'd likely also have manufacturer
    // data to add here).
    // Anyhow, for now, 8 characters device name it is with the single wireless service UUID.
    advertisingData.setServices({wirelessServiceUuid});

    if (m_forceFullName || m_advertiseName.length() <= 8) {
        advertisingData.setLocalName(m_advertiseName);
    } else {
        qCWarning(dcNetworkManagerBluetoothServer()) << "Truncating local host name to" << m_advertiseName.left(8) << "(maximum length of 8 characters exceeded)";
        advertisingData.setLocalName(m_advertiseName.left(8));
    }

    // Note: start advertising in 100 ms interval, this makes the device better discoverable on certain phones
    QLowEnergyAdvertisingParameters advertisingParameters;
    advertisingParameters.setInterval(100, 100);

    qCDebug(dcNetworkManagerBluetoothServer()) << "Start advertising" << m_advertiseName << m_localDevice->address().toString();
    m_controller->startAdvertising(advertisingParameters, advertisingData, advertisingData);
    // Note: setRunning(true) will be called when the service is really advertising, see onControllerStateChanged()
}

void BluetoothServer::stop()
{
    // Prevent printing the stop message twice in case of different shutdown reasons
    if (!m_controller && !m_localDevice)
        return;

    qCDebug(dcNetworkManagerBluetoothServer()) << "-------------------------------------";
    qCDebug(dcNetworkManagerBluetoothServer()) << "Stopping bluetooth server.";
    qCDebug(dcNetworkManagerBluetoothServer()) << "-------------------------------------";

    if (m_controller) {
        qCDebug(dcNetworkManagerBluetoothServer()) << "Stop advertising.";
        m_controller->stopAdvertising();
        m_controller->deleteLater();
        m_controller = nullptr;
    }

    if (m_localDevice) {
        qCDebug(dcNetworkManagerBluetoothServer()) << "Set host mode to connectable.";
        m_localDevice->setHostMode(QBluetoothLocalDevice::HostConnectable);
        m_localDevice->deleteLater();
        m_localDevice = nullptr;
    }

    // Let the events set the state connected and running
}
