// 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 "schedulermanager.h"
#include "schedulingstrategies/rulebasedstrategy.h"
#include "schedulingstrategies/aistrategy.h"
#include "schedulingstrategies/manualstrategy.h"
#include "schedulersettings.h"
#include "spotmarket/spotmarketmanager.h"

#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)

SchedulerManager::SchedulerManager(
    SpotMarketManager *spotMarketManager,
    EnergyManager     *energyManager,
    ThingManager      *thingManager,
    QObject           *parent)
    : QObject(parent),
      m_spotMarketManager(spotMarketManager),
      m_energyManager(energyManager),
      m_thingManager(thingManager)
{
    m_settings = new SchedulerSettings(this);

    // Register built-in strategies
    RuleBasedStrategy *ruleStrategy = new RuleBasedStrategy(this);
    AIStrategy        *aiStrategy   = new AIStrategy(this);
    registerStrategy(ruleStrategy);
    registerStrategy(aiStrategy);

    // Default to rule-based
    m_activeStrategy = ruleStrategy;

    // Recompute timer — fires every recomputeIntervalMin
    connect(&m_recomputeTimer, &QTimer::timeout, this, &SchedulerManager::onRecomputeTimer);
    m_recomputeTimer.start(m_config.recomputeIntervalMin * 60 * 1000);

    // Slot execution timer
    connect(&m_slotTimer, &QTimer::timeout, this, &SchedulerManager::onSlotExecutionTimer);

    // Initial compute
    QTimer::singleShot(0, this, &SchedulerManager::forceRecompute);
}

// --- Strategy management ---

void SchedulerManager::setStrategy(ISchedulingStrategy *strategy)
{
    if (!strategy || !m_strategies.contains(strategy)) {
        qCWarning(dcNymeaEnergy()) << "SchedulerManager: unknown strategy, ignoring setStrategy()";
        return;
    }
    if (m_activeStrategy == strategy)
        return;

    m_activeStrategy = strategy;
    qCDebug(dcNymeaEnergy()) << "SchedulerManager: strategy changed to" << strategy->strategyId();
    emit strategyChanged(strategy->strategyId());
    forceRecompute();
}

ISchedulingStrategy *SchedulerManager::currentStrategy() const
{
    return m_activeStrategy;
}

QList<ISchedulingStrategy *> SchedulerManager::availableStrategies() const
{
    return m_strategies;
}

void SchedulerManager::registerStrategy(ISchedulingStrategy *strategy)
{
    if (!strategy)
        return;
    // Ensure no duplicate ids
    foreach (ISchedulingStrategy *s, m_strategies) {
        if (s->strategyId() == strategy->strategyId())
            return;
    }
    strategy->setParent(this);
    m_strategies.append(strategy);
    qCDebug(dcNymeaEnergy()) << "SchedulerManager: registered strategy" << strategy->strategyId();

    // Hydrate ManualStrategy with persisted slots as soon as it is registered
    if (strategy->strategyId() == QLatin1String("manual") && m_settings) {
        ManualStrategy *ms = qobject_cast<ManualStrategy *>(strategy);
        if (ms) {
            foreach (const ManualSlotConfig &cfg, m_settings->manualSlots())
                ms->setManualSlot(cfg);
            qCDebug(dcNymeaEnergy()) << "SchedulerManager: loaded"
                                     << m_settings->manualSlots().size()
                                     << "manual slot(s) from settings";
        }
    }
}

// --- Timeline access ---

QList<EnergyTimeSlot> SchedulerManager::currentTimeline() const
{
    return m_timeline;
}

QDateTime SchedulerManager::lastComputedAt() const
{
    return m_lastComputedAt;
}

QDateTime SchedulerManager::nextRecomputeAt() const
{
    return m_nextRecomputeAt;
}

QString SchedulerManager::planHealth() const
{
    if (!m_activeStrategy || !m_activeStrategy->isAvailable())
        return QStringLiteral("degraded");
    if (m_timeline.isEmpty())
        return QStringLiteral("no_forecast");

    // Check if any slot has a missing decisionReason (should not happen)
    foreach (const EnergyTimeSlot &slot, m_timeline) {
        if (slot.decisionReason.isEmpty())
            return QStringLiteral("degraded");
    }
    return QStringLiteral("ok");
}

// --- Configuration ---

SchedulerConfig SchedulerManager::config() const
{
    return m_config;
}

void SchedulerManager::setConfig(const SchedulerConfig &config)
{
    m_config = config;
    // Restart recompute timer with new interval
    m_recomputeTimer.start(m_config.recomputeIntervalMin * 60 * 1000);
    emit configChanged(m_config);
    forceRecompute();
}

// --- Flexible loads ---

QList<FlexibleLoad> SchedulerManager::flexibleLoads() const
{
    return m_loads;
}

void SchedulerManager::updateLoad(const FlexibleLoad &load)
{
    for (int i = 0; i < m_loads.size(); ++i) {
        if (m_loads.at(i).thingId == load.thingId) {
            m_loads[i] = load;
            emit loadUpdated(load);
            return;
        }
    }
    m_loads.append(load);
    emit loadRegistered(load);
}

// --- Manual override ---

void SchedulerManager::overrideSlot(const QDateTime &slotStart,
                                     LoadSource source,
                                     double powerW,
                                     const QString &reason)
{
    for (int i = 0; i < m_timeline.size(); ++i) {
        EnergyTimeSlot &slot = m_timeline[i];
        if (slot.start == slotStart) {
            slot.manualOverride  = true;
            slot.overrideReason  = reason;
            // Apply override to the relevant allocation
            switch (source) {
            case LoadSource::SmartCharging: slot.allocatedToEV      = powerW; break;
            case LoadSource::HeatPump:      slot.allocatedToHP      = powerW; break;
            case LoadSource::DHW:           slot.allocatedToDHW     = powerW; break;
            case LoadSource::Battery:       slot.allocatedToBattery = powerW; break;
            case LoadSource::FeedIn:        slot.allocatedToFeedIn  = powerW; break;
            case LoadSource::External:      break;
            }
            slot.decisionReason = QStringLiteral("Décision manuelle : ") + reason;
            slot.decisionRules  = QStringList() << QStringLiteral("ManualOverride");

            qCDebug(dcNymeaEnergy()) << "SchedulerManager: override applied to slot"
                                     << slotStart << ":" << reason;
            emit timelineUpdated(m_timeline);
            return;
        }
    }
    qCWarning(dcNymeaEnergy()) << "SchedulerManager: no slot found for override at" << slotStart;
}

int SchedulerManager::activeOverridesCount() const
{
    int count = 0;
    foreach (const EnergyTimeSlot &slot, m_timeline) {
        if (slot.manualOverride)
            count++;
    }
    return count;
}

// --- Manual slot management ---

QList<ManualSlotConfig> SchedulerManager::manualSlots() const
{
    ManualStrategy *ms = findManualStrategy();
    if (!ms)
        return {};
    return ms->manualSlots();
}

void SchedulerManager::setManualSlot(const ManualSlotConfig &config)
{
    ManualStrategy *ms = findManualStrategy();
    if (!ms) {
        qCWarning(dcNymeaEnergy())
            << "SchedulerManager::setManualSlot: no ManualStrategy registered";
        return;
    }
    ms->setManualSlot(config);
    if (m_settings)
        m_settings->setManualSlot(config);
    emit manualSlotsChanged();
    forceRecompute();
}

void SchedulerManager::removeManualSlot(const QDateTime &start)
{
    ManualStrategy *ms = findManualStrategy();
    if (!ms)
        return;
    ms->removeManualSlot(start);
    if (m_settings)
        m_settings->removeManualSlot(start);
    emit manualSlotsChanged();
    forceRecompute();
}

void SchedulerManager::clearManualSlots()
{
    ManualStrategy *ms = findManualStrategy();
    if (!ms)
        return;
    ms->clearAllManualSlots();
    if (m_settings)
        m_settings->clearManualSlots();
    emit manualSlotsChanged();
    forceRecompute();
}

// --- Force recompute ---

void SchedulerManager::forceRecompute()
{
    if (!m_activeStrategy) {
        qCWarning(dcNymeaEnergy()) << "SchedulerManager: no active strategy, skipping recompute";
        return;
    }

    QList<EnergyTimeSlot> forecast = buildForecast();
    QList<FlexibleLoad>   loads    = collectLoads();

    // Preserve existing manual overrides
    QHash<QDateTime, EnergyTimeSlot> overrides;
    foreach (const EnergyTimeSlot &slot, m_timeline) {
        if (slot.manualOverride)
            overrides.insert(slot.start, slot);
    }

    // Re-inject overrides into the new forecast
    for (int i = 0; i < forecast.size(); ++i) {
        if (overrides.contains(forecast.at(i).start))
            forecast[i] = overrides.value(forecast.at(i).start);
    }

    m_timeline = m_activeStrategy->computeSchedule(forecast, loads, m_config);
    m_lastComputedAt  = QDateTime::currentDateTimeUtc();
    m_nextRecomputeAt = m_lastComputedAt.addSecs(m_config.recomputeIntervalMin * 60);

    qCDebug(dcNymeaEnergy()) << "SchedulerManager: recomputed" << m_timeline.size()
                             << "slots, health=" << planHealth();

    // Apply current slot immediately
    QDateTime now = QDateTime::currentDateTimeUtc();
    foreach (const EnergyTimeSlot &slot, m_timeline) {
        if (slot.isActive(now)) {
            applyCurrentSlot(slot);
            break;
        }
    }

    scheduleNextSlotTimer();
    emit timelineUpdated(m_timeline);
}

// --- Private slots ---

void SchedulerManager::onRecomputeTimer()
{
    forceRecompute();
}

void SchedulerManager::onSlotExecutionTimer()
{
    QDateTime now = QDateTime::currentDateTimeUtc();
    foreach (const EnergyTimeSlot &slot, m_timeline) {
        if (slot.isActive(now)) {
            qCDebug(dcNymeaEnergy()) << "SchedulerManager: executing slot" << slot.start
                                     << slot.decisionReason;
            applyCurrentSlot(slot);
            emit slotExecuted(slot, true);
            break;
        }
    }
    scheduleNextSlotTimer();
}

// --- Private helpers ---

QList<EnergyTimeSlot> SchedulerManager::buildForecast() const
{
    // Stub PredictionProvider: build slots for the planning horizon (every 60 min).
    // All predictions are 0. TariffManager (SpotMarketManager) provides electricityPrice.
    QList<EnergyTimeSlot> forecast;
    QDateTime start = QDateTime::currentDateTimeUtc();
    // Round down to the current hour
    start.setTime(QTime(start.time().hour(), 0, 0));

    int horizon = m_config.planningHorizonHours;

    // Get spot price entries if available
    ScoreEntries scoreEntries;
    if (m_spotMarketManager && m_spotMarketManager->enabled()
            && m_spotMarketManager->currentProvider()) {
        scoreEntries = m_spotMarketManager->weightedScoreEntries();
    }

    for (int h = 0; h < horizon; ++h) {
        EnergyTimeSlot slot;
        slot.start = start.addSecs(h * 3600);
        slot.end   = start.addSecs((h + 1) * 3600);

        // Fill electricity price from spot market if available
        if (!scoreEntries.isEmpty()) {
            ScoreEntry entry = scoreEntries.getScoreEntry(slot.start);
            if (!entry.isNull())
                slot.electricityPrice = entry.value() / 1000.0; // mEUR/kWh → EUR/kWh
        }

        // All other predictions are 0 (stub — replaced by PredictionManager later)
        forecast.append(slot);
    }

    qCDebug(dcNymeaEnergy()) << "SchedulerManager: built forecast of" << forecast.size() << "slots";
    return forecast;
}

QList<FlexibleLoad> SchedulerManager::collectLoads() const
{
    // Return the currently registered loads.
    // In Phase 1, loads are registered externally (by managers calling updateLoad()).
    return m_loads;
}

void SchedulerManager::applyCurrentSlot(const EnergyTimeSlot &slot)
{
    // In Phase 1, we log the decision. Actual hardware commands are issued by
    // each specialist manager (SmartChargingManager, etc.) which listens to
    // the timelineUpdated() signal and reads the current slot.
    qCDebug(dcNymeaEnergy()) << "SchedulerManager: applying slot:"
                             << slot.decisionReason
                             << "EV=" << slot.allocatedToEV << "W"
                             << "HP=" << slot.allocatedToHP << "W"
                             << "DHW=" << slot.allocatedToDHW << "W"
                             << "Bat=" << slot.allocatedToBattery << "W";
}

ManualStrategy *SchedulerManager::findManualStrategy() const
{
    foreach (ISchedulingStrategy *s, m_strategies) {
        if (s->strategyId() == QLatin1String("manual"))
            return qobject_cast<ManualStrategy *>(s);
    }
    return nullptr;
}

void SchedulerManager::scheduleNextSlotTimer()
{
    m_slotTimer.stop();

    QDateTime now  = QDateTime::currentDateTimeUtc();
    QDateTime next;

    foreach (const EnergyTimeSlot &slot, m_timeline) {
        if (slot.start > now) {
            next = slot.start;
            break;
        }
    }

    if (next.isValid()) {
        qint64 msUntilNext = now.msecsTo(next);
        if (msUntilNext > 0) {
            m_slotTimer.setSingleShot(true);
            m_slotTimer.start(static_cast<int>(qMin(msUntilNext, static_cast<qint64>(INT_MAX))));
            qCDebug(dcNymeaEnergy()) << "SchedulerManager: next slot execution in"
                                     << msUntilNext / 60000 << "min at" << next;
        }
    }
}
