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

#include "nymeaenergyjsonhandler.h"
#include "types/charginginfo.h"
#include "smartchargingmanager.h"
#include "spotmarket/spotmarketmanager.h"

#include <energymanager.h>

#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)

NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketManager, SmartChargingManager *smartChargingManager, QObject *parent):
    JsonHandler{parent},
    m_spotMarketManager{spotMarketManager},
    m_smartChargingManager{smartChargingManager}
{

    registerEnum<ChargingInfo::ChargingMode>();
    registerEnum<ChargingInfo::ChargingState>();
    registerEnum<ChargingAction::ChargingActionIssuer>();

    registerObject<ChargingInfo, ChargingInfos>();
    registerObject<SpotMarketProviderInfo, SpotMarketProviderInfos>();
    registerObject<ScoreEntry, ScoreEntries>();
    registerObject<ChargingAction>();
    registerObject<ChargingSchedule, ChargingSchedules>();

    QVariantMap params, returns;
    QString description;

    params.clear(); returns.clear();
    description = "Get the phase power consumption limit. 0 if unset. This needs to be set in order for the smart charging to do anything.";
    returns.insert("phasePowerLimit", enumValueName(Uint));
    registerMethod("GetPhasePowerLimit", description, params, returns, Types::PermissionScopeControlThings);

    params.clear(); returns.clear();
    description = "Set the phase power consumption limit. This is the maximum allowed Ampere per phase. A value of 0 means unset. All smart charging will be disabled.";
    params.insert("phasePowerLimit", enumValueName(Uint));
    returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
    registerMethod("SetPhasePowerLimit", description, params, returns);

    params.clear(); returns.clear();
    description = "Get the acquisition tolerance. This value will control how much solar surplus is required to start the charging.";
    returns.insert("acquisitionTolerance", enumValueName(Double));
    registerMethod("GetAcquisitionTolerance", description, params, returns, Types::PermissionScopeControlThings);

    params.clear(); returns.clear();
    description = "Set the acquisition tolerance for the charging.";
    params.insert("acquisitionTolerance", enumValueName(Double));
    returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
    registerMethod("SetAcquisitionTolerance", description, params, returns, Types::PermissionScopeControlThings);

    params.clear(); returns.clear();
    description = "Get the battery level consideration. This value will control how much the energy storage is taking in account";
    returns.insert("batteryLevelConsideration", enumValueName(Double));
    registerMethod("GetBatteryLevelConsideration", description, params, returns);

    params.clear(); returns.clear();
    description = "Set the battery level consideration for the charging.";
    params.insert("batteryLevelConsideration", enumValueName(Double));
    returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
    registerMethod("SetBatteryLevelConsideration", description, params, returns);

    params.clear(); returns.clear();
    description = "Get the lock on unplug setting.";
    returns.insert("lockOnUnplug", enumValueName(Bool));
    registerMethod("GetLockOnUnplug", description, params, returns, Types::PermissionScopeControlThings);

    params.clear(); returns.clear();
    description = "Set the lock on unplug setting.";
    params.insert("lockOnUnplug", enumValueName(Bool));
    returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
    registerMethod("SetLockOnUnplug", description, params, returns, Types::PermissionScopeControlThings);

    params.clear(); returns.clear();
    description = "Get the charging info for all or a single EV charger";
    params.insert("o:evChargerId", enumValueName(Uuid));
    returns.insert("chargingInfos", QVariantList() << objectRef<ChargingInfo>());
    registerMethod("GetChargingInfos", description, params, returns, Types::PermissionScopeControlThings);

    params.clear(); returns.clear();
    description = "Set the charging info for an EV charger. Only given properties will be set, others will be untouched.";
    params.insert("chargingInfo", objectRef<ChargingInfo>());
    returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
    registerMethod("SetChargingInfo", description, params, returns, Types::PermissionScopeControlThings);

    params.clear(); returns.clear();
    description = "Get the list of available spot market data providers.";
    returns.insert("providers", QVariantList() << objectRef<SpotMarketProviderInfo>());
    registerMethod("GetAvailableSpotMarketProviders", description, params, returns, Types::PermissionScopeControlThings);

    params.clear(); returns.clear();
    description = "Set the current spotmarket configuration. If enabled, the provideId must be valid. You can get the available providers using GetAvailableSpotMarketProviders.";
    params.insert("enabled", enumValueName(Bool));
    params.insert("o:providerId", enumValueName(Uuid));
    returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
    registerMethod("SetSpotMarketConfiguration", description, params, returns, Types::PermissionScopeControlThings);

    params.clear(); returns.clear();
    description = "Get the current spotmarket configuration.";
    returns.insert("o:providerId", enumValueName(Uuid));
    returns.insert("enabled", enumValueName(Bool));
    returns.insert("available", enumValueName(Bool));
    registerMethod("GetSpotMarketConfiguration", description, params, returns, Types::PermissionScopeControlThings);


    params.clear(); returns.clear();
    description = "Get the current score entries from the current spot market. If spot market is not enabled or not available, the list will be empty. The weighting of the available score entries goes from 0 (worst) to 1.0 (best).";
    returns.insert("spotMarketScoreEntries", QVariantList() << objectRef<ScoreEntry>());
    registerMethod("GetSpotMarketScoreEntries", description, params, returns, Types::PermissionScopeControlThings);


    params.clear(); returns.clear();
    description = "Get the planed charging schedules.";
    returns.insert("chargingSchedules", QVariantList() << objectRef<ChargingSchedule>());
    registerMethod("GetChargingSchedules", description, params, returns, Types::PermissionScopeControlThings);


    // Notifications
    params.clear();
    description = "Emitted whenever the phase power limit configuration changes.";
    params.insert("phasePowerLimit", enumValueName(Uint));
    registerNotification("PhasePowerLimitChanged", description, params);
    connect(m_smartChargingManager, &SmartChargingManager::phasePowerLimitChanged, this, [=](int phasePowerLimit){
        emit PhasePowerLimitChanged({{"phasePowerLimit", phasePowerLimit}});
    });

    params.clear();
    description = "Emitted whenever the acquisition tolerance changes.";
    params.insert("acquisitionTolerance", enumValueName(Double));
    registerNotification("AcquisitionToleranceChanged", description, params);
    connect(m_smartChargingManager, &SmartChargingManager::acquisitionToleranceChanged, this, [=](double acquisitionTolerance){
        emit AcquisitionToleranceChanged({{"acquisitionTolerance", acquisitionTolerance}});
    });

    params.clear();
    description = "Emitted whenever the battery level consideration changes.";
    params.insert("batteryLevelConsideration", enumValueName(Double));
    registerNotification("BatteryLevelConsiderationChanged", description, params);
    connect(m_smartChargingManager, &SmartChargingManager::batteryLevelConsiderationChanged, this, [=](double batteryLevelConsideration){
        emit BatteryLevelConsiderationChanged({{"batteryLevelConsideration", batteryLevelConsideration}});
    });

    params.clear();
    description = "Emitted whenever the lock on unplug setting changes.";
    params.insert("lockOnUnplug", enumValueName(Bool));
    registerNotification("LockOnUnplugChanged", description, params);
    connect(m_smartChargingManager, &SmartChargingManager::lockOnUnplugChanged, this, [=](bool lockOnUnplug){
        emit LockOnUnplugChanged({{"lockOnUnplug", lockOnUnplug}});
    });

    params.clear();
    description = "Emitted whenever a charging info is added.";
    params.insert("chargingInfo", objectRef<ChargingInfo>());
    registerNotification("ChargingInfoAdded", description, params);

    params.clear();
    description = "Emitted whenever a charging info is removed.";
    params.insert("evChargerThingId", enumValueName(Uuid));
    registerNotification("ChargingInfoRemoved", description, params);

    params.clear();
    description = "Emitted whenever a charging info changes.";
    params.insert("chargingInfo", objectRef<ChargingInfo>());
    registerNotification("ChargingInfoChanged", description, params);

    params.clear();
    description = "Emitted whenever a spot market configuration has changed.";
    params.insert("o:providerId", enumValueName(Uuid));
    params.insert("enabled", enumValueName(Bool));
    params.insert("available", enumValueName(Bool));
    registerNotification("SpotMarketConfigurationChanged", description, params);

    params.clear();
    description = "Emitted whenever the planed charging schedules have changed.";
    params.insert("chargingSchedules", QVariantList() << objectRef<ChargingSchedule>());
    // [ETM] degradedMode : true quand le watchdog L2 a basculé en repli (compteur muet).
    //   Optionnel (additif, rétro-compatible) ; émis aussi aux transitions du mode dégradé.
    params.insert("o:degradedMode", enumValueName(Bool));
    registerNotification("ChargingSchedulesChanged", description, params);

    // Charing manager
    connect(m_smartChargingManager, &SmartChargingManager::chargingInfoAdded, this, [=](const ChargingInfo &chargingInfo) {
        QVariantMap params;
        params.insert("chargingInfo", pack(chargingInfo));
        emit ChargingInfoAdded(params);
    });
    connect(m_smartChargingManager, &SmartChargingManager::chargingInfoRemoved, this, [=](const ThingId &evChargerThingId){
        QVariantMap params;
        params.insert("evChargerThingId", evChargerThingId);
        emit ChargingInfoRemoved(params);
    });
    connect(m_smartChargingManager, &SmartChargingManager::chargingInfoChanged, this, [=](const ChargingInfo chargingInfo){
        QVariantMap params;
        params.insert("chargingInfo", pack(chargingInfo));
        emit ChargingInfoChanged(params);
    });

    connect(m_smartChargingManager, &SmartChargingManager::chargingSchedulesChanged, this, [=](){
        QVariantMap params;
        QVariantList schedules;
        foreach (const ChargingSchedule &schedule, m_smartChargingManager->chargingSchedules()) {
            schedules << pack<ChargingSchedule>(schedule);
        }
        params.insert("chargingSchedules", schedules);
        params.insert("degradedMode", m_smartChargingManager->degradedMode());  // [ETM] L2
        emit ChargingSchedulesChanged(params);
    });

    // Spot market manager
    params.clear();
    description = "Emitted whenever the spot market manager status changed.";
    params.insert("enabled", enumValueName(Bool));
    params.insert("available", enumValueName(Bool));
    registerNotification("SpotMarketStatusChanged", description, params);
    connect(m_spotMarketManager, &SpotMarketManager::enabledChanged, this, [this](bool){
        sendSpotMarketConfigurationChangedNotification();
    });

    connect(m_spotMarketManager, &SpotMarketManager::availableChanged, this, [this](bool){
        sendSpotMarketConfigurationChangedNotification();
    });

    connect(m_spotMarketManager, &SpotMarketManager::currentProviderChanged, this, [this](SpotMarketDataProvider *){
        sendSpotMarketConfigurationChangedNotification();
    });

    params.clear();
    description = "Emitted whenever the score entries of the current spot market provider changed. The weighting of the available score entries goes from 0 (worst) to 1.0 (best).";
    params.insert("spotMarketScoreEntries", QVariantList() << objectRef<ScoreEntry>());
    registerNotification("SpotMarketScoreEntriesChanged", description, params);
    connect(m_spotMarketManager, &SpotMarketManager::scoreEntriesUpdated, this, [=](){

        QVariantList entries;
        if (m_spotMarketManager->currentProvider() && m_spotMarketManager->enabled()) {
            // Get all scores weighted (we want all available)
            ScoreEntries weightedEntries = m_spotMarketManager->weightedScoreEntries();
            foreach (const ScoreEntry &entry, weightedEntries) {
                entries << pack<ScoreEntry>(entry);
            }
        }

        QVariantMap params;
        params.insert("spotMarketScoreEntries", entries);
        emit SpotMarketScoreEntriesChanged(params);
    });
}

QString NymeaEnergyJsonHandler::name() const
{
    return "NymeaEnergy";
}

JsonReply *NymeaEnergyJsonHandler::GetPhasePowerLimit(const QVariantMap &params)
{
    Q_UNUSED(params)
    QVariantMap returns;
    returns.insert("phasePowerLimit", m_smartChargingManager->phasePowerLimit());
    return createReply(returns);
}

JsonReply *NymeaEnergyJsonHandler::SetPhasePowerLimit(const QVariantMap &params)
{
    int phasePowerLimit = params.value("phasePowerLimit").toUInt();
    m_smartChargingManager->setPhasePowerLimit(phasePowerLimit);
    QVariantMap returns;
    returns.insert("energyError", enumValueName(EnergyManager::EnergyErrorNoError));
    return createReply(returns);
}

JsonReply *NymeaEnergyJsonHandler::GetAcquisitionTolerance(const QVariantMap &params)
{
    Q_UNUSED(params)
    return createReply({{"acquisitionTolerance", m_smartChargingManager->acquisitionTolerance()}});
}

JsonReply *NymeaEnergyJsonHandler::SetAcquisitionTolerance(const QVariantMap &params)
{
    double acquisitionTolerance = params.value("acquisitionTolerance").toDouble();
    if (acquisitionTolerance < 0.0 || acquisitionTolerance > 1.0)
        return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});

    m_smartChargingManager->setAcquisitionTolerance(acquisitionTolerance);
    return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
}

JsonReply *NymeaEnergyJsonHandler::GetBatteryLevelConsideration(const QVariantMap &params)
{
    Q_UNUSED(params)
    return createReply({{"batteryLevelConsideration", m_smartChargingManager->batteryLevelConsideration()}});
}

JsonReply *NymeaEnergyJsonHandler::SetBatteryLevelConsideration(const QVariantMap &params)
{
    double batteryLevelConsideration = params.value("batteryLevelConsideration").toDouble();
    if (batteryLevelConsideration < 0.0 || batteryLevelConsideration > 1.0)
        return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});

    m_smartChargingManager->setBatteryLevelConsideration(batteryLevelConsideration);
    return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
}

JsonReply* NymeaEnergyJsonHandler::GetChargingInfos(const QVariantMap &params)
{
    Q_UNUSED(params)
    QVariantMap returns;
    returns.insert("chargingInfos", pack(m_smartChargingManager->chargingInfos()));
    return createReply(returns);
}

JsonReply* NymeaEnergyJsonHandler::SetChargingInfo(const QVariantMap &params, const JsonContext &context)
{
    ChargingInfo chargingInfo = unpack<ChargingInfo>(params.value("chargingInfo"));
    chargingInfo.setLocale(context.locale());
    EnergyManager::EnergyError status = m_smartChargingManager->setChargingInfo(chargingInfo);
    QVariantMap returns;
    returns.insert("energyError", enumValueName(status));
    return createReply(returns);
}

JsonReply *NymeaEnergyJsonHandler::GetLockOnUnplug(const QVariantMap &params)
{
    Q_UNUSED(params)
    return createReply({{"lockOnUnplug", m_smartChargingManager->lockOnUnplug()}});
}

JsonReply *NymeaEnergyJsonHandler::SetLockOnUnplug(const QVariantMap &params)
{
    bool lockOnUnplug = params.value("lockOnUnplug").toBool();
    m_smartChargingManager->setLockOnUnplug(lockOnUnplug);
    return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
}

JsonReply *NymeaEnergyJsonHandler::GetAvailableSpotMarketProviders(const QVariantMap &params)
{
    Q_UNUSED(params)
    QVariantMap returns;
    returns.insert("providers", pack(m_spotMarketManager->availableProviders()));
    return createReply(returns);
}

JsonReply *NymeaEnergyJsonHandler::GetSpotMarketConfiguration(const QVariantMap &params)
{
    Q_UNUSED(params)

    QVariantMap returns;
    returns.insert("enabled", m_spotMarketManager->enabled());
    returns.insert("available", m_spotMarketManager->available());
    if (!m_spotMarketManager->currentProviderId().isNull())
        returns.insert("providerId", m_spotMarketManager->currentProviderId());

    return createReply(returns);
}

JsonReply *NymeaEnergyJsonHandler::SetSpotMarketConfiguration(const QVariantMap &params)
{
    bool enable = params.value("enabled").toBool();
    QUuid providerId = params.value("providerId").toUuid();

    EnergyManager::EnergyError error = EnergyManager::EnergyErrorNoError;

    if (enable && providerId.isNull()) {
        qCWarning(dcNymeaEnergy()) << "Could not enable spot market because ther is no valid provider id given.";
        error = EnergyManager::EnergyErrorInvalidParameter;
    } else if (enable && !providerId.isNull()) {
        if (!m_spotMarketManager->changeProvider(providerId)) {
            error = EnergyManager::EnergyErrorInvalidParameter;
        }
    }

    if (error == EnergyManager::EnergyErrorNoError) {
        m_spotMarketManager->setEnabled(enable);
    }

    QVariantMap returns;
    returns.insert("energyError", enumValueName(error));
    return createReply(returns);
}

JsonReply *NymeaEnergyJsonHandler::GetSpotMarketScoreEntries(const QVariantMap &params)
{
    Q_UNUSED(params)

    QVariantMap returns;
    if (m_spotMarketManager->currentProvider() && m_spotMarketManager->enabled()) {
        QVariantList entries;
        if (m_spotMarketManager->currentProvider() && m_spotMarketManager->enabled()) {
            ScoreEntries weightedEntries = SpotMarketManager::weightScoreEntries(m_spotMarketManager->currentProvider()->scoreEntries());
            foreach (const ScoreEntry &entry, weightedEntries) {
                entries << pack<ScoreEntry>(entry);
            }
        }
        returns.insert("spotMarketScoreEntries", entries);
    } else {
        returns.insert("spotMarketScoreEntries", QVariantList());
    }
    return createReply(returns);
}

JsonReply *NymeaEnergyJsonHandler::GetChargingSchedules(const QVariantMap &params)
{
    Q_UNUSED(params)

    QVariantMap returns;
    QVariantList schedules;
    foreach (const ChargingSchedule &schedule, m_smartChargingManager->chargingSchedules()) {
        schedules << pack<ChargingSchedule>(schedule);
    }
    returns.insert("chargingSchedules", schedules);
    return createReply(returns);
}

void NymeaEnergyJsonHandler::sendSpotMarketConfigurationChangedNotification()
{
    QVariantMap params;
    params.insert("enabled", m_spotMarketManager->enabled());
    params.insert("available", m_spotMarketManager->available());
    if (m_spotMarketManager->enabled())
        params.insert("providerId", m_spotMarketManager->currentProviderId());

    emit SpotMarketConfigurationChanged(params);
}

