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

#include "lifxcloud.h"
#include "extern-plugininfo.h"

#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUrl>
#include <QUrlQuery>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>

LifxCloud::LifxCloud(NetworkAccessManager *networkManager, QObject *parent) :
    QObject(parent),
    m_networkManager(networkManager)
{

}

void LifxCloud::setAuthorizationToken(const QByteArray &token)
{
    m_authorizationToken = token;
}

bool LifxCloud::cloudAuthenticated()
{
    return m_authenticated;
}

bool LifxCloud::cloudConnected()
{
    return m_connected;
}

void LifxCloud::listLights()
{
    if (m_authorizationToken.isEmpty()) {
        qCWarning(dcLifx()) << "Authorization token is not set";
        return;
    }
    QNetworkRequest request;
    request.setUrl(QUrl("https://api.lifx.com/v1/lights/all"));
    request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
    request.setRawHeader("Authorization","Bearer "+m_authorizationToken);

    QNetworkReply *reply = m_networkManager->get(request);
    connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [reply,  this] {
        if(!checkHttpStatusCode(reply)) {
            return;
        }
        QByteArray rawData = reply->readAll();

        QJsonDocument data; QJsonParseError error;
        data = QJsonDocument::fromJson(rawData, &error);
        if (error.error != QJsonParseError::NoError) {
            qCDebug(dcLifx()) << "List lights: Received invalide JSON object" << error.errorString();
            return;
        }

        if (!data.isArray())
            qCWarning(dcLifx()) << "Data is not an array";

        QJsonArray array = data.array();
        QList<Light> descriptors;
        foreach (QJsonValue jsonValue, array) {

            QJsonObject object = jsonValue.toObject();
            qCDebug(dcLifx()) << "Light object:" << object;
            Light light;
            light.id = object["id"].toString().toUtf8();
            light.uuid = object["uuid"].toString().toUtf8();
            if (object["power"].toString() == "on") {
                light.power = true;
            } else {
                light.power = false;
            }
            light.label = object["label"].toString();
            light.connected = object["connected"].toBool();
            light.brightness = object["brightness"].toDouble();
            int hue = object["hue"].toObject().value("saturation").toDouble();
            int saturation = object["color"].toObject().value("saturation").toDouble();
            light.colorTemperature = object["color"].toObject().value("kelvin").toDouble();
            light.color = QColor::fromHsv(hue, saturation, light.brightness);
            Group group;
            group.name = object["group"].toObject().value("name").toString();
            group.id = object["group"].toObject().value("id").toString().toUtf8();
            light.group = group;
            Location location;
            location.name = object["location"].toObject().value("name").toString();
            location.id = object["location"].toObject().value("id").toString().toUtf8();
            light.location = location;
            Product product;
            QJsonObject productObject = object["product"].toObject();
            product.name = productObject["name"].toString();
            product.identifier = productObject["identifier"].toString();
            product.manufacturer = productObject["manufacturer"].toString();
            product.secondsSinceLastSeen = productObject["seconds_since_seen"].toInt();
            Capabilities capabilities;
            QJsonObject capabilitiesObject = productObject["capabilities"].toObject();
            capabilities.color = capabilitiesObject["has_color"].toBool();
            capabilities.colorTemperature = capabilitiesObject["has_variable_color_temp"].toBool();
            capabilities.ir = capabilitiesObject["has_ir"].toBool();
            capabilities.chain = capabilitiesObject["has_chain"].toBool();
            capabilities.multizone = capabilitiesObject["has_multizone"].toBool();
            capabilities.minKelvin= capabilitiesObject["min_kelvin"].toInt();
            capabilities.maxKelvin = capabilitiesObject["max_kelvin"].toInt();
            product.capabilities = capabilities;
            light.product = product;
            descriptors.append(light);
        }
        emit lightsListReceived(descriptors);
    });
}

void LifxCloud::listScenes()
{
    if (m_authorizationToken.isEmpty()) {
        qCWarning(dcLifx()) << "Authorization token is not set";
        return;
    }
    QNetworkRequest request;
    request.setUrl(QUrl("https://api.lifx.com/v1/scenes"));
    request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
    request.setRawHeader("Authorization","Bearer " + m_authorizationToken);

    QNetworkReply *reply = m_networkManager->get(request);
    connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [reply,  this] {
        if(!checkHttpStatusCode(reply)) {
            return;
        }
        QByteArray rawData = reply->readAll();
        qCDebug(dcLifx()) << "Got list scenes reply" << rawData;
        QJsonDocument data; QJsonParseError error;
        data = QJsonDocument::fromJson(rawData, &error);
        if (error.error != QJsonParseError::NoError) {
            qCDebug(dcLifx()) << "List scenes: Received invalide JSON object" << error.errorString();
            return;
        }
        if (!data.isArray())
            qCWarning(dcLifx()) << "Data is not an array";

        QJsonArray array = data.array();
        QList<Scene> scenes;
        foreach (QJsonValue value, array) {
            Scene scene;
            scene.id = value.toObject().value("uuid").toString().toUtf8();
            scene.name = value.toObject().value("name").toString();
            scenes.append(scene);
        }
        emit scenesListReceived(scenes);
    });
}

int LifxCloud::setPower(const QString &lightId, bool power, int duration)
{
    return setState("id:"+lightId, StatePower, power, duration);
}

int LifxCloud::setBrightnesss(const QString &lightId, int brightness, int duration)
{
    return setState("id:"+lightId, StateBrightness, brightness/100.00, duration);
}

int LifxCloud::setColor(const QString &lightId, QColor color, int duration)
{
    return setState("id:"+lightId, StateColor, color.name(), duration);
}

int LifxCloud::setColorTemperature(const QString &lightId, int kelvin, int duration)
{
    return setState("id:"+lightId, StateColorTemperature, kelvin, duration);
}

int LifxCloud::setInfrared(const QString &lightId, int infrared, int duration)
{
    return setState("id:"+lightId, StateColor, infrared/100.00, duration);
}

int LifxCloud::activateScene(const QString &sceneId)
{
    if (m_authorizationToken.isEmpty()) {
        qCWarning(dcLifx()) << "Authorization token is not set";
        return -1;
    }

    int requestId = std::rand();

    QNetworkRequest request;
    request.setUrl(QUrl(QString("https://api.lifx.com/v1/scenes/scene_id:%1/activate").arg(sceneId)));
    request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
    request.setRawHeader("Authorization","Bearer "+m_authorizationToken);
    QNetworkReply *reply = m_networkManager->put(request, "");
    connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
        emit requestExecuted(requestId, checkHttpStatusCode(reply));
        QByteArray rawData = reply->readAll();
        qCDebug(dcLifx()) << "Got activate scene reply" << rawData;
    });
    return requestId;
}

int LifxCloud::setEffect(const QString &lightId, LifxCloud::Effect effect, QColor color)
{
    if (m_authorizationToken.isEmpty()) {
        qCWarning(dcLifx()) << "Authorization token is not set";
        return -1;
    }
    int requestId = std::rand();
    QNetworkRequest request;
    QUrlQuery params;
    switch (effect) {
    case LifxCloud::EffectNone:
        request.setUrl(QUrl(QString("https://api.lifx.com/v1/lights/id:%1/effects/off").arg(lightId)));
        break;
    case LifxCloud::EffectBreathe:
        request.setUrl(QUrl(QString("https://api.lifx.com/v1/lights/id:%1/effects/breathe").arg(lightId)));
        params.addQueryItem("color", color.name().trimmed());
        params.addQueryItem("period", "2");
        params.addQueryItem("cycles", "3");
        break;
    case LifxCloud::EffectMove:
        request.setUrl(QUrl(QString("https://api.lifx.com/v1/lights/id:%1/effects/move").arg(lightId)));
        break;
    case LifxCloud::EffectMorph:
        request.setUrl(QUrl(QString("https://api.lifx.com/v1/lights/id:%1/effects/morph").arg(lightId)));
        break;
    case LifxCloud::EffectFlame:
        request.setUrl(QUrl(QString("https://api.lifx.com/v1/lights/id:%1/effects/flame").arg(lightId)));
        break;
    case LifxCloud::EffectPulse:
        request.setUrl(QUrl(QString("https://api.lifx.com/v1/lights/id:%1/effects/pulse").arg(lightId)));
        params.addQueryItem("color", color.name().trimmed());
        params.addQueryItem("period", "2");
        params.addQueryItem("cycles", "3");
        break;
    }
    request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/x-www-form-urlencoded.");
    request.setRawHeader("Authorization","Bearer "+m_authorizationToken);
    qCDebug(dcLifx()) << "Set effect request" << request.url() << params.toString().toUtf8();

    QNetworkReply *reply = m_networkManager->post(request, params.toString().toUtf8());
    connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [requestId, reply,  this] {

        QByteArray rawData = reply->readAll();
        qCDebug(dcLifx()) << "Got set effect reply" << rawData;
        emit requestExecuted(requestId, checkHttpStatusCode(reply));
    });
    return requestId;
}

int LifxCloud::setState(const QString &selector, State state, QVariant stateValue, int duration)
{
    if (m_authorizationToken.isEmpty()) {
        qCWarning(dcLifx()) << "Authorization token is not set";
        return -1;
    }
    int requestId = std::rand();
    QNetworkRequest request;
    request.setUrl(QUrl(QString("https://api.lifx.com/v1/lights/%1/state").arg(selector)));
    request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
    request.setRawHeader("Authorization","Bearer "+m_authorizationToken);
    QJsonDocument doc;
    QJsonObject payload;
    payload["duration"] = duration;
    payload["fast"] = false;
    switch (state) {
    case StatePower:
        if (stateValue.toBool())
            payload["power"] = "on";
        else
            payload["power"] = "off";

        qCDebug(dcLifx()) << "Set state power" << stateValue.toBool();
        break;
    case StateBrightness:
        payload["brightness"] = stateValue.toDouble();
        qCDebug(dcLifx()) << "Set state brightness" << stateValue;
        break;
    case StateColor:
        payload["color"] = stateValue.toString();
        qCDebug(dcLifx()) << "Set state color" << stateValue;
        break;
    case StateColorTemperature:
        payload["color"] = "kelvin:"+stateValue.toString();
        qCDebug(dcLifx()) << "Set state color" << stateValue;
        break;
    case StateInfrared:
        payload["infrared"] = stateValue.toDouble();
        qCDebug(dcLifx()) << "Set state infrared" << stateValue;
    }

    doc.setObject(payload);
    qCDebug(dcLifx()) << "Set state request" << request.url() << doc.toJson();
    QNetworkReply *reply = m_networkManager->put(request, doc.toJson());
    connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [requestId, duration,reply,  this] {

        QByteArray rawData = reply->readAll();
        qCDebug(dcLifx()) << "Got set state reply" << rawData;
        if (checkHttpStatusCode(reply)) {
            emit requestExecuted(requestId, true);
            QTimer::singleShot(duration*1000+500, this, [=] {listLights();});
        } else {
            emit requestExecuted(requestId, false);
        }
    });
    return requestId;
}

bool LifxCloud::checkHttpStatusCode(QNetworkReply *reply)
{
    int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();

    if (reply->error() != QNetworkReply::NoError) {
        qCWarning(dcLifx()) << "Request error:" << status << reply->errorString();
        if (m_connected) {
            m_connected = false;
            emit connectionChanged(false);
        }
        return false;
    }
    // check HTTP status code
    if (status == 401 || status == 403) {
        if (m_authenticated) {
            m_authenticated = false;
            emit authenticationChanged(false);
        }
    }
    if (status > 207) {
        qCWarning(dcLifx()) << "Error get scene list" << status;
        return false;
    }
    if (!m_authenticated) {
        m_authenticated = true;
        emit authenticationChanged(true);
    }
    if (!m_connected) {
        m_connected = true;
        emit connectionChanged(true);
    }
    return true;
}
