// 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 "schedulersettings.h"
#include "types/manualslotconfig.h"

#include <nymeasettings.h>

#include <QSettings>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLoggingCategory>

Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)

SchedulerSettings::SchedulerSettings(QObject *parent)
    : QObject(parent)
{
    load();
}

QString SchedulerSettings::settingsFilePath() const
{
    return NymeaSettings::settingsPath() + QStringLiteral("/scheduler.conf");
}

// --- Strategy ---

QString SchedulerSettings::activeStrategyId() const
{
    return m_activeStrategyId;
}

void SchedulerSettings::setActiveStrategyId(const QString &id)
{
    if (m_activeStrategyId == id)
        return;
    m_activeStrategyId = id;
    save();
}

// --- SchedulerConfig ---

SchedulerConfig SchedulerSettings::schedulerConfig() const
{
    return m_config;
}

void SchedulerSettings::setSchedulerConfig(const SchedulerConfig &config)
{
    m_config = config;
    save();
}

// --- LoadConfig ---

SchedulerSettings::LoadConfig SchedulerSettings::loadConfig(const ThingId &thingId) const
{
    return m_loadConfigs.value(thingId.toString());
}

void SchedulerSettings::setLoadConfig(const ThingId &thingId, const LoadConfig &config)
{
    m_loadConfigs.insert(thingId.toString(), config);
    save();
}

QList<ThingId> SchedulerSettings::configuredLoadIds() const
{
    QList<ThingId> ids;
    foreach (const QString &key, m_loadConfigs.keys())
        ids.append(ThingId(key));
    return ids;
}

// --- Overrides ---

QList<QDateTime> SchedulerSettings::overrideSlotStarts() const
{
    QList<QDateTime> starts;
    foreach (const QString &key, m_overrides.keys())
        starts.append(QDateTime::fromString(key, Qt::ISODateWithMs).toUTC());
    return starts;
}

SchedulerSettings::OverrideEntry SchedulerSettings::overrideEntry(const QDateTime &slotStart) const
{
    return m_overrides.value(slotStart.toUTC().toString(Qt::ISODateWithMs));
}

void SchedulerSettings::setOverride(const QDateTime &slotStart, const OverrideEntry &entry)
{
    m_overrides.insert(slotStart.toUTC().toString(Qt::ISODateWithMs), entry);
    save();
}

void SchedulerSettings::removeOverride(const QDateTime &slotStart)
{
    m_overrides.remove(slotStart.toUTC().toString(Qt::ISODateWithMs));
    save();
}

void SchedulerSettings::clearExpiredOverrides()
{
    QDateTime now = QDateTime::currentDateTimeUtc();
    QStringList toRemove;
    for (auto it = m_overrides.constBegin(); it != m_overrides.constEnd(); ++it) {
        if (it.value().expiresAt.isValid() && it.value().expiresAt <= now)
            toRemove.append(it.key());
    }
    foreach (const QString &key, toRemove)
        m_overrides.remove(key);
    if (!toRemove.isEmpty())
        save();
}

// --- Manual slots ---

QList<ManualSlotConfig> SchedulerSettings::manualSlots() const
{
    return m_manualSlots;
}

void SchedulerSettings::setManualSlot(const ManualSlotConfig &config)
{
    const QString key = config.start.toUTC().toString(Qt::ISODateWithMs);
    for (int i = 0; i < m_manualSlots.size(); ++i) {
        if (m_manualSlots.at(i).start.toUTC().toString(Qt::ISODateWithMs) == key) {
            m_manualSlots[i] = config;
            save();
            return;
        }
    }
    m_manualSlots.append(config);
    save();
}

void SchedulerSettings::removeManualSlot(const QDateTime &start)
{
    const QString key = start.toUTC().toString(Qt::ISODateWithMs);
    for (int i = 0; i < m_manualSlots.size(); ++i) {
        if (m_manualSlots.at(i).start.toUTC().toString(Qt::ISODateWithMs) == key) {
            m_manualSlots.removeAt(i);
            save();
            return;
        }
    }
}

void SchedulerSettings::clearManualSlots()
{
    if (m_manualSlots.isEmpty())
        return;
    m_manualSlots.clear();
    save();
}

// --- Persistence ---

void SchedulerSettings::load()
{
    QSettings s(settingsFilePath(), QSettings::IniFormat);

    s.beginGroup(QStringLiteral("scheduler"));
    m_activeStrategyId = s.value("strategyId", QStringLiteral("rule-based")).toString();
    m_config.chargePriceThreshold   = s.value("chargePriceThreshold",   m_config.chargePriceThreshold).toDouble();
    m_config.solarSurplusThresholdW = s.value("solarSurplusThresholdW", m_config.solarSurplusThresholdW).toDouble();
    m_config.planningHorizonHours   = s.value("planningHorizonHours",   m_config.planningHorizonHours).toInt();
    m_config.recomputeIntervalMin   = s.value("recomputeIntervalMin",   m_config.recomputeIntervalMin).toInt();
    m_config.selfSufficiencyTarget  = s.value("selfSufficiencyTarget",  m_config.selfSufficiencyTarget).toDouble();
    s.endGroup();

    // Load per-thing load configs
    int loadCount = s.beginReadArray(QStringLiteral("loads"));
    for (int i = 0; i < loadCount; ++i) {
        s.setArrayIndex(i);
        QString key = s.value("thingId").toString();
        if (key.isEmpty()) continue;
        LoadConfig lc;
        lc.priority    = s.value("priority",    0.5).toDouble();
        lc.targetValue = s.value("targetValue", 0.0).toDouble();
        lc.enabled     = s.value("enabled",    true).toBool();
        QString dl = s.value("deadline").toString();
        if (!dl.isEmpty())
            lc.deadline = QDateTime::fromString(dl, Qt::ISODateWithMs).toUTC();
        m_loadConfigs.insert(key, lc);
    }
    s.endArray();

    // Load overrides
    int overrideCount = s.beginReadArray(QStringLiteral("overrides"));
    for (int i = 0; i < overrideCount; ++i) {
        s.setArrayIndex(i);
        QString slotKey = s.value("slotStart").toString();
        if (slotKey.isEmpty()) continue;
        OverrideEntry entry;
        entry.source = loadSourceFromString(s.value("source").toString());
        entry.powerW = s.value("powerW", 0.0).toDouble();
        entry.reason = s.value("reason").toString();
        QString exp  = s.value("expiresAt").toString();
        if (!exp.isEmpty())
            entry.expiresAt = QDateTime::fromString(exp, Qt::ISODateWithMs).toUTC();
        m_overrides.insert(slotKey, entry);
    }
    s.endArray();

    // Load manual slots
    m_manualSlots.clear();
    int manualCount = s.beginReadArray(QStringLiteral("manualSlots"));
    for (int i = 0; i < manualCount; ++i) {
        s.setArrayIndex(i);
        ManualSlotConfig cfg;
        cfg.start     = QDateTime::fromString(s.value("start").toString(), Qt::ISODateWithMs).toUTC();
        cfg.end       = QDateTime::fromString(s.value("end").toString(),   Qt::ISODateWithMs).toUTC();
        cfg.label     = s.value("label").toString();
        cfg.repeating = s.value("repeating", false).toBool();
        const QString expStr = s.value("expiresAt").toString();
        if (!expStr.isEmpty())
            cfg.expiresAt = QDateTime::fromString(expStr, Qt::ISODateWithMs).toUTC();

        // Allocations stored as alloc_ev, alloc_battery, …
        const QStringList allocKeys = {
            QStringLiteral("ev"), QStringLiteral("battery"),
            QStringLiteral("dhw"), QStringLiteral("heatpump"), QStringLiteral("feedin")
        };
        foreach (const QString &key, allocKeys) {
            double val = s.value(QStringLiteral("alloc_") + key, 0.0).toDouble();
            if (val != 0.0)
                cfg.powerAllocations.insert(manualSlotSourceFromKey(key), val);
        }

        if (!cfg.start.isValid()) continue;

        if (cfg.isExpired()) {
            qCDebug(dcNymeaEnergy()) << "SchedulerSettings: discarding expired manual slot"
                                     << cfg.label << "(expired" << cfg.expiresAt << ")";
            continue;
        }
        m_manualSlots.append(cfg);
    }
    s.endArray();

    qCDebug(dcNymeaEnergy()) << "SchedulerSettings: loaded from" << settingsFilePath()
                              << "—" << m_manualSlots.size() << "manual slot(s)";
}

void SchedulerSettings::save()
{
    QSettings s(settingsFilePath(), QSettings::IniFormat);

    s.beginGroup(QStringLiteral("scheduler"));
    s.setValue("strategyId",             m_activeStrategyId);
    s.setValue("chargePriceThreshold",   m_config.chargePriceThreshold);
    s.setValue("solarSurplusThresholdW", m_config.solarSurplusThresholdW);
    s.setValue("planningHorizonHours",   m_config.planningHorizonHours);
    s.setValue("recomputeIntervalMin",   m_config.recomputeIntervalMin);
    s.setValue("selfSufficiencyTarget",  m_config.selfSufficiencyTarget);
    s.endGroup();

    s.beginWriteArray(QStringLiteral("loads"), m_loadConfigs.size());
    int idx = 0;
    for (auto it = m_loadConfigs.constBegin(); it != m_loadConfigs.constEnd(); ++it, ++idx) {
        s.setArrayIndex(idx);
        s.setValue("thingId",     it.key());
        s.setValue("priority",    it.value().priority);
        s.setValue("targetValue", it.value().targetValue);
        s.setValue("enabled",     it.value().enabled);
        if (it.value().deadline.isValid())
            s.setValue("deadline", it.value().deadline.toUTC().toString(Qt::ISODateWithMs));
    }
    s.endArray();

    s.beginWriteArray(QStringLiteral("overrides"), m_overrides.size());
    idx = 0;
    for (auto it = m_overrides.constBegin(); it != m_overrides.constEnd(); ++it, ++idx) {
        s.setArrayIndex(idx);
        s.setValue("slotStart", it.key());
        s.setValue("source",    loadSourceToString(it.value().source));
        s.setValue("powerW",    it.value().powerW);
        s.setValue("reason",    it.value().reason);
        if (it.value().expiresAt.isValid())
            s.setValue("expiresAt", it.value().expiresAt.toUTC().toString(Qt::ISODateWithMs));
    }
    s.endArray();

    s.beginWriteArray(QStringLiteral("manualSlots"), m_manualSlots.size());
    idx = 0;
    foreach (const ManualSlotConfig &cfg, m_manualSlots) {
        s.setArrayIndex(idx++);
        s.setValue("start",     cfg.start.toUTC().toString(Qt::ISODateWithMs));
        s.setValue("end",       cfg.end.toUTC().toString(Qt::ISODateWithMs));
        s.setValue("label",     cfg.label);
        s.setValue("repeating", cfg.repeating);
        if (cfg.expiresAt.isValid())
            s.setValue("expiresAt", cfg.expiresAt.toUTC().toString(Qt::ISODateWithMs));
        // Allocations
        for (auto it = cfg.powerAllocations.constBegin();
             it != cfg.powerAllocations.constEnd(); ++it) {
            s.setValue(QStringLiteral("alloc_") + manualSlotAllocationKey(it.key()),
                       it.value());
        }
    }
    s.endArray();
}
