// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync

#include "rulebasedscheduler.h"
#include "../energyarbitrator.h"
#include "../../evcharger.h"
#include "../../types/chargingaction.h"
#include "../../types/charginginfo.h"

#include "plugininfo.h"
#include <QUuid>
#include <algorithm>

RuleBasedScheduler::RuleBasedScheduler(EnergyArbitrator *arbitrator, QObject *parent)
    : QObject(parent)
    , m_arbitrator(arbitrator)
{
}

Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx)
{
    // Planification (même logique que l'amont — écrit dans m_chargingActions)
    m_arbitrator->runSpotMarketPlanning(ctx.timestamp);
    m_arbitrator->runSurplusPlanning(ctx.timestamp);

    Slot slot;
    slot.from = ctx.timestamp;
    slot.to   = ctx.timestamp.addSecs(60);

    const auto &cas    = m_arbitrator->scheduledActions();
    const auto &evs    = m_arbitrator->registeredEvChargers();

    // Même priorité que adjustEvChargers() — iso-fonctionnel 3b
    for (auto it = evs.constBegin(); it != evs.constEnd(); ++it) {
        EvCharger *ev = it.value();
        if (!ev->available() || !ev->pluggedIn())
            continue;

        const ChargingActions &actions = cas.value(ev);
        LoadAction la;

        if (actions.value(ChargingAction::ChargingActionIssuerTimeRequirement).chargingEnabled()) {
            la = buildTimeRequirementAction(
                ev, actions.value(ChargingAction::ChargingActionIssuerTimeRequirement));

        } else if (actions.value(ChargingAction::ChargingActionIssuerSurplusCharging).chargingEnabled()) {
            const auto &ca = actions.value(ChargingAction::ChargingActionIssuerSurplusCharging);
            la.loadId          = ev->thing()->id().toString();
            la.kind            = LoadAction::Setpoint;
            la.funding         = LoadAction::Surplus;
            la.chargingEnabled = true;
            la.currentA        = ca.maxChargingCurrent();
            la.phaseCount      = ca.desiredPhaseCount();
            la.reason          = QStringLiteral("Surplus PV disponible — recharge solaire");
            la.estimatedPowerW = la.currentA * 230.0 * la.phaseCount;

        } else if (actions.value(ChargingAction::ChargingActionIssuerSpotMarketCharging).chargingEnabled()) {
            const auto &ca = actions.value(ChargingAction::ChargingActionIssuerSpotMarketCharging);
            la.loadId          = ev->thing()->id().toString();
            la.kind            = LoadAction::Setpoint;
            la.funding         = LoadAction::Grid;
            la.chargingEnabled = true;
            la.currentA        = ca.maxChargingCurrent();
            la.phaseCount      = ca.desiredPhaseCount();
            la.reason          = QStringLiteral("Tarif aWATTar favorable — recharge heure creuse");
            la.estimatedPowerW = la.currentA * 230.0 * la.phaseCount;

        } else {
            const ChargingInfo::ChargingMode mode =
                m_arbitrator->chargingInfo(ev->id()).chargingMode();
            if (mode == ChargingInfo::ChargingModeEcoWithMinCurrent
                || mode == ChargingInfo::ChargingModeEcoMinWithTargetTime) {
                la = buildMinCurrentAction(ev);
            } else {
                la = buildIdleAction(ev);
            }
        }

        slot.actions.append(la);
    }

    // ---- Waterfall ECS (charges non-EV à paliers) ---------------------------------
    // Correction A — déduction EV unique : ctx.meter.exportW est la mesure BRUTE
    // (invariant 8). Les consignes EV de CE cycle ne sont pas encore visibles au
    // compteur ; on réserve donc leur puissance commandée non encore mesurée.
    double evReservedW = 0;
    for (const LoadAction &la : slot.actions) {
        if (la.kind != LoadAction::Setpoint || !la.chargingEnabled)
            continue;
        EvCharger *ev = evs.value(ThingId(la.loadId));
        const double measuredW = ev ? ev->currentPower() : 0.0;
        evReservedW += qMax(0.0, la.estimatedPowerW - measuredW);
    }
    // Surplus net SIGNÉ : exportW − importW = −puissance compteur. Négatif en import →
    // l'ECS déleste (budget < palier) au lieu de rester allumé sur le réseau. (Le maintien
    // transitoire sous minOn est géré par le verrou de l'adaptateur, pas par le budget.)
    double remainingSurplusW = (ctx.meter.exportW - ctx.meter.importW) - evReservedW;

    // Charges ECS triées par priorité ASCENDANTE : rang 1 = premier servi
    // (OPTIMIZER_PROTOCOL §5 + annexe C — la priorité est un rang, pas un poids).
    QList<LoadContext> ecsLoads;
    for (const LoadContext &lc : ctx.loads) {
        if (lc.adapter == QStringLiteral("relay-stages"))
            ecsLoads.append(lc);
    }
    std::sort(ecsLoads.begin(), ecsLoads.end(),
              [](const LoadContext &a, const LoadContext &b) { return a.priority < b.priority; });

    for (const LoadContext &lc : ecsLoads)
        slot.actions.append(buildEcsStageAction(lc, remainingSurplusW));

    // Grid funding ECS : dormant jusqu'à 3f (waterfall réseau) — non implémenté ici.

    Plan plan;
    plan.planId   = QUuid::createUuid().toString(QUuid::WithoutBraces);
    plan.strategy = QStringLiteral("rule-based");
    plan.timeSlots.append(slot);
    return plan;
}

LoadAction RuleBasedScheduler::buildTimeRequirementAction(EvCharger *ev,
                                                           const ChargingAction &ca) const
{
    // Le courant final est affiné par adjustEvChargers() (allowance root-meter).
    // En 3b on log la valeur brute de la planification — iso-fonctionnel.
    LoadAction la;
    la.loadId          = ev->thing()->id().toString();
    la.kind            = LoadAction::Setpoint;
    la.funding         = LoadAction::Grid;
    la.chargingEnabled = true;
    la.currentA        = ca.maxChargingCurrent();
    la.phaseCount      = ca.desiredPhaseCount();
    la.reason          = QStringLiteral("Deadline VE approchante — recharge prioritaire");
    la.estimatedPowerW = la.currentA * 230.0 * la.phaseCount;
    return la;
}

LoadAction RuleBasedScheduler::buildMinCurrentAction(EvCharger *ev) const
{
    const uint minA    = qMax(EcoMinChargingCurrent, ev->maxChargingCurrentMinValue());
    const uint phases  = ev->phaseCount();

    LoadAction la;
    la.loadId          = ev->thing()->id().toString();
    la.kind            = LoadAction::Setpoint;
    la.funding         = LoadAction::Surplus;
    la.chargingEnabled = true;
    la.currentA        = minA;
    la.phaseCount      = phases;
    la.reason          = QStringLiteral("Aucun surplus — courant minimum maintenu (mode EcoMin)");
    la.estimatedPowerW = la.currentA * 230.0 * la.phaseCount;
    return la;
}

LoadAction RuleBasedScheduler::buildIdleAction(EvCharger *ev) const
{
    LoadAction la;
    la.loadId          = ev->thing()->id().toString();
    la.kind            = LoadAction::Setpoint;
    la.funding         = LoadAction::Surplus;
    la.chargingEnabled = false;
    la.currentA        = 0;
    la.phaseCount      = 0;
    la.reason          = QStringLiteral("Aucun surplus disponible — recharge suspendue");
    la.estimatedPowerW = 0;
    return la;
}

LoadAction RuleBasedScheduler::buildEcsStageAction(const LoadContext &lc,
                                                   double &remainingSurplusW) const
{
    // Correction B (anti-clignotement) : on recrédite la conso actuelle de l'ECS au budget.
    // Sans ce recrédit, allumer un palier ferait chuter l'export mesuré → palier 0 → oscillation.
    const double budgetChargeW = remainingSurplusW + lc.telemetry.currentPowerW;

    // Palier déclaré le plus haut qui tient dans le budget. stages[0] == 0 par construction
    // (Q_ASSERT EcsRelayAdapter).
    const QList<int> &stages = lc.declared.stages;
    const int topStage = stages.size() - 1;
    int budgetStage = 0;
    for (int i = 0; i < stages.size(); ++i) {
        if (stages.at(i) <= budgetChargeW)
            budgetStage = i;
    }

    // Verrou (minOn/minOff) : le palier choisi est borné par la fenêtre déclarée par
    // l'adaptateur (calculée au MÊME ctx.timestamp). Une charge verrouillée ON garde son
    // palier (puissance engagée non-coupable) ; à l'arrêt sous minOff elle ne redémarre pas.
    const int lo = qBound(0, lc.telemetry.minStage, topStage);
    const int hi = qBound(lo, lc.telemetry.maxStage, topStage);
    const int bestStage = qBound(lo, budgetStage, hi);

    LoadAction la;
    la.loadId          = lc.id;
    la.kind            = LoadAction::Stage;
    la.funding         = LoadAction::Surplus;
    la.stage           = bestStage;
    la.estimatedPowerW = stages.at(bestStage);

    if (bestStage > budgetStage)
        // Maintenu au-dessus du budget : protection compresseur (minOn non écoulé).
        la.reason = QStringLiteral("Verrou minOn — %1 maintenu palier %2 (%3 W, surplus %4 W)")
                        .arg(lc.label).arg(bestStage).arg(stages.at(bestStage)).arg(qRound(budgetChargeW));
    else if (bestStage < budgetStage)
        // Empêché de monter/redémarrer par minOff malgré le surplus.
        la.reason = QStringLiteral("Verrou minOff — %1 maintenu palier %2 (redémarrage trop tôt)")
                        .arg(lc.label).arg(bestStage);
    else if (bestStage > 0)
        la.reason = QStringLiteral("Surplus PV %1 W — %2 palier %3 (%4 W)")
                        .arg(qRound(budgetChargeW)).arg(lc.label)
                        .arg(bestStage).arg(stages.at(bestStage));
    else
        la.reason = QStringLiteral("Surplus insuffisant (%1 W) — %2 éteint")
                        .arg(qRound(budgetChargeW)).arg(lc.label);

    // Budget restant pour les charges ECS suivantes (rang supérieur / priorité plus basse).
    remainingSurplusW = budgetChargeW - la.estimatedPowerW;
    return la;
}
