// 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 "manualstrategy.h"

#include <QLoggingCategory>

Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)

ManualStrategy::ManualStrategy(QObject *parent)
    : ISchedulingStrategy(parent)
{
}

// ---------------------------------------------------------------------------
// computeSchedule
// ---------------------------------------------------------------------------

QList<EnergyTimeSlot> ManualStrategy::computeSchedule(
    const QList<EnergyTimeSlot> &forecast,
    const QList<FlexibleLoad>   &loads,
    const SchedulerConfig       &config)
{
    Q_UNUSED(config)

    QList<EnergyTimeSlot> timeline = forecast;

    for (int i = 0; i < timeline.size(); ++i) {
        EnergyTimeSlot &slot = timeline[i];

        if (slot.manualOverride)
            continue; // respect human decisions

        // Clear previous allocations
        slot.allocatedToEV      = 0;
        slot.allocatedToHP      = 0;
        slot.allocatedToDHW     = 0;
        slot.allocatedToBattery = 0;
        slot.allocatedToFeedIn  = 0;
        slot.decisionReason.clear();
        slot.decisionRules.clear();

        // Look for a matching manual config
        const ManualSlotConfig *matched      = nullptr;
        bool                    expiredMatch = false;

        foreach (const ManualSlotConfig &cfg, m_manualSlots) {
            if (!cfg.matchesSlotIgnoreExpiry(slot.start))
                continue;

            if (cfg.isExpired()) {
                expiredMatch = true;
                qCDebug(dcNymeaEnergy())
                    << "ManualStrategy: expired config" << cfg.label
                    << "— skipped for slot" << slot.start;
            } else {
                matched = &cfg;
                break;
            }
        }

        if (matched) {
            // Apply exactly the user-defined allocations
            slot.allocatedToEV      = matched->powerAllocations.value(LoadSource::SmartCharging, 0);
            slot.allocatedToHP      = matched->powerAllocations.value(LoadSource::HeatPump, 0);
            slot.allocatedToDHW     = matched->powerAllocations.value(LoadSource::DHW, 0);
            slot.allocatedToBattery = matched->powerAllocations.value(LoadSource::Battery, 0);
            slot.allocatedToFeedIn  = matched->powerAllocations.value(LoadSource::FeedIn, 0);

            if (!matched->label.isEmpty())
                slot.decisionReason =
                    QString("Créneau manuel '%1' configuré par l'utilisateur")
                    .arg(matched->label);
            else
                slot.decisionReason =
                    QStringLiteral("Créneau manuel configuré par l'utilisateur");

            slot.decisionRules.append(QStringLiteral("ManualSlot"));

        } else if (expiredMatch) {
            // Apply safe fallback first, then override reason to show expiry
            applyInflexibleLoads(slot, loads);
            slot.decisionReason = QStringLiteral("Créneau expiré — ignoré");
            if (!slot.decisionRules.contains(QStringLiteral("ExpiredSlot")))
                slot.decisionRules.prepend(QStringLiteral("ExpiredSlot"));

        } else {
            applyInflexibleLoads(slot, loads);
            // applyInflexibleLoads sets reason if critical loads were found;
            // fall back to generic message if still empty.
            if (slot.decisionReason.isEmpty()) {
                slot.decisionReason =
                    QStringLiteral("Aucune configuration — charges critiques uniquement");
                slot.decisionRules.append(QStringLiteral("ManualDefault"));
            }
        }
    }

    return timeline;
}

// ---------------------------------------------------------------------------
// explainDecision
// ---------------------------------------------------------------------------

QString ManualStrategy::explainDecision(
    const EnergyTimeSlot &slot,
    const FlexibleLoad   &load) const
{
    Q_UNUSED(load)

    if (slot.manualOverride)
        return QStringLiteral("Décision manuelle : ") + slot.overrideReason;

    if (slot.decisionRules.contains(QStringLiteral("ManualSlot")))
        return slot.decisionReason;

    if (slot.decisionRules.contains(QStringLiteral("ExpiredSlot")))
        return QStringLiteral("La configuration de ce créneau a expiré — "
                              "seules les charges critiques restent actives.");

    return QStringLiteral("Aucune configuration manuelle pour ce créneau.");
}

// ---------------------------------------------------------------------------
// Manual slot management
// ---------------------------------------------------------------------------

void ManualStrategy::setManualSlot(const ManualSlotConfig &slotConfig)
{
    for (int i = 0; i < m_manualSlots.size(); ++i) {
        if (m_manualSlots.at(i).start == slotConfig.start) {
            m_manualSlots[i] = slotConfig;
            return;
        }
    }
    m_manualSlots.append(slotConfig);
}

void ManualStrategy::removeManualSlot(const QDateTime &slotStart)
{
    for (int i = 0; i < m_manualSlots.size(); ++i) {
        if (m_manualSlots.at(i).start == slotStart) {
            m_manualSlots.removeAt(i);
            return;
        }
    }
}

void ManualStrategy::clearAllManualSlots()
{
    m_manualSlots.clear();
}

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

// ---------------------------------------------------------------------------
// Private helper
// ---------------------------------------------------------------------------

void ManualStrategy::applyInflexibleLoads(EnergyTimeSlot &slot,
                                           const QList<FlexibleLoad> &loads) const
{
    foreach (const FlexibleLoad &load, loads) {
        if (load.type != LoadType::Inflexible)
            continue;

        if (load.source == LoadSource::HeatPump && load.priority >= 0.9) {
            slot.allocatedToHP = qMax(slot.allocatedToHP, load.currentPowerW);
            if (slot.decisionReason.isEmpty())
                slot.decisionReason =
                    QStringLiteral("Chauffage critique — PAC toujours active");
            if (!slot.decisionRules.contains(QStringLiteral("CriticalHeating")))
                slot.decisionRules.append(QStringLiteral("CriticalHeating"));
        }

        if (load.source == LoadSource::DHW && load.priority >= 0.9) {
            slot.allocatedToDHW = qMax(slot.allocatedToDHW, load.currentPowerW);
            if (slot.decisionReason.isEmpty())
                slot.decisionReason =
                    QStringLiteral("ECS critique (sécurité légionellose)");
            if (!slot.decisionRules.contains(QStringLiteral("CriticalDHW")))
                slot.decisionRules.append(QStringLiteral("CriticalDHW"));
        }
    }
}
