// 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 "spotmarket/spotmarketmanager.h"
#include "types/chargingconfiguration.h"
#include "nymeaenergyjsonhandler.h"
#include "smartchargingmanager.h"
#include "types/chargingstate.h"
#include "types/scoreentry.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<ChargingConfiguration::ChargingMode>();
    registerEnum<ChargingAction::ChargingActionIssuer>();
    registerEnum<ChargingState::Mode>();

    registerObject<ChargingState, ChargingStates>();
    registerObject<ChargingConfiguration, ChargingConfigurations>();

    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 charging configuration for all or a single EV charger.";
    params.insert("o:evChargerId", enumValueName(Uuid));
    returns.insert("chargingConfigurations", QVariantList() << objectRef<ChargingConfiguration>());
    registerMethod("GetChargingConfigurations", description, params, returns, Types::PermissionScopeControlThings);

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

    params.clear(); returns.clear();
    description = "Get the charging states for all or a single EV charger.";
    returns.insert("chargingStates", QVariantList() << objectRef<ChargingState>());
    registerMethod("GetChargingStates", 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 a charging configuration is added.";
    params.insert("chargingConfiguration", objectRef<ChargingConfiguration>());
    registerNotification("ChargingConfigurationAdded", description, params);

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

    params.clear();
    description = "Emitted whenever a charging configuration changes.";
    params.insert("chargingConfiguration", objectRef<ChargingConfiguration>());
    registerNotification("ChargingConfigurationChanged", description, params);


    params.clear();
    description = "Emitted whenever a charging state has been added.";
    params.insert("chargingState", objectRef<ChargingState>());
    registerNotification("ChargingStateAdded", description, params);

    params.clear();
    description = "Emitted whenever a charging state has been removed.";
    params.insert("evChargerThingId", enumValueName(Uuid));
    registerNotification("ChargingStateRemoved", description, params);

    params.clear();
    description = "Emitted whenever a charging state has changed.";
    params.insert("chargingState", objectRef<ChargingState>());
    registerNotification("ChargingStateChanged", 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>());
    registerNotification("ChargingSchedulesChanged", description, params);

    // Charing manager
    connect(m_smartChargingManager, &SmartChargingManager::chargingConfigurationAdded, this, [=](const ChargingConfiguration &chargingConfiguration) {
        QVariantMap params;
        params.insert("chargingConfiguration", pack(chargingConfiguration));
        emit ChargingConfigurationAdded(params);
    });
    connect(m_smartChargingManager, &SmartChargingManager::chargingConfigurationRemoved, this, [=](const ThingId &evChargerThingId){
        QVariantMap params;
        params.insert("evChargerThingId", evChargerThingId);
        emit ChargingConfigurationRemoved(params);
    });
    connect(m_smartChargingManager, &SmartChargingManager::chargingConfigurationChanged, this, [=](const ChargingConfiguration chargingConfiguration){
        QVariantMap params;
        params.insert("chargingConfiguration", pack(chargingConfiguration));
        emit ChargingConfigurationChanged(params);
    });


    connect(m_smartChargingManager, &SmartChargingManager::chargingStateAdded, this, [=](const ChargingState &chargingState) {
        QVariantMap params;
        params.insert("chargingState", pack(chargingState));
        emit ChargingStateAdded(params);
    });
    connect(m_smartChargingManager, &SmartChargingManager::chargingStateRemoved, this, [=](const ThingId &evChargerThingId){
        QVariantMap params;
        params.insert("evChargerThingId", evChargerThingId);
        emit ChargingStateRemoved(params);
    });
    connect(m_smartChargingManager, &SmartChargingManager::chargingStateChanged, this, [=](const ChargingState &chargingState){
        QVariantMap params;
        params.insert("chargingState", pack(chargingState));
        emit ChargingStateChanged(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);
        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::GetChargingConfigurations(const QVariantMap &params)
{
    Q_UNUSED(params)
    QVariantMap returns;
    returns.insert("chargingConfigurations", pack(m_smartChargingManager->chargingConfigurations()));
    return createReply(returns);
}

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

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

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);
}

