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

#include "energyarbitrator.h"
#include "adapters/evadapter.h"
#include "adapters/ecsrelayadapter.h"
#include "scheduler/rulebasedscheduler.h"
#include "types/surpluscontext.h"
#include "types/plan.h"
#include "../rootmeter.h"
#include "../evcharger.h"

#include "plugininfo.h"

#include <energymanager.h>
#include <QTimer>

namespace {
//! Période du watchdog L2 (SAFETY.md §L2) : tick indépendant des signaux compteur.
constexpr int MeterWatchdogPeriodMs  = 30 * 1000;  // 30 s
//! Seuil de silence compteur au-delà duquel le mode dégradé L2 est déclenché.
constexpr int MeterSilenceThresholdS = 90;         // 90 s
}

EnergyArbitrator::EnergyArbitrator(EnergyManager *em, ThingManager *tm,
                                    SpotMarketManager *sm,
                                    EnergyManagerConfiguration *conf,
                                    QObject *parent)
    : SmartChargingManager(em, tm, sm, conf, parent)
    , m_scheduler(new RuleBasedScheduler(this, this))
{
    // --- L2 : watchdog fraîcheur compteur (SAFETY.md §L2) ---
    // La LOGIQUE (recordMeterUpdate / evaluateMeterFreshness) prend le temps en paramètre
    // et reste testable par injection (symétrique de simulationCallUpdate). Seuls les
    // DÉCLENCHEURS RÉELS (signal + QTimer, horloge murale) sont câblés ici, et exclus en
    // simulation — comme les connexions amont powerBalanceEntryAdded→update() (SCM l.108-130).
#ifndef ENERGY_SIMULATION
    m_lastMeterUpdate = QDateTime::currentDateTime();  // grâce au démarrage (évite un dégradé immédiat)
    // Fraîcheur picotée sur powerBalanceChanged (en plus de la connexion amont L4).
    connect(em, &EnergyManager::powerBalanceChanged, this, [this]() {
        recordMeterUpdate(QDateTime::currentDateTime());
    });
    // QTimer (et non signal) : doit rester actif quand le compteur est muet.
    m_meterWatchdog = new QTimer(this);
    m_meterWatchdog->setInterval(MeterWatchdogPeriodMs);
    connect(m_meterWatchdog, &QTimer::timeout, this, &EnergyArbitrator::onMeterWatchdogTick);
    m_meterWatchdog->start();
#else
    Q_UNUSED(em)
#endif

    qCDebug(dcNymeaEnergy()) << "[EnergyArbitrator] Arbitre ETM initialisé.";
}

void EnergyArbitrator::runSurplusPlanning(const QDateTime &now)
{
    planSurplusCharging(now);
}

void EnergyArbitrator::runSpotMarketPlanning(const QDateTime &now)
{
    planSpotMarketCharging(now);
}

const QHash<EvCharger *, ChargingActions> &EnergyArbitrator::scheduledActions() const
{
    return internalChargingActions();
}

void EnergyArbitrator::doExecuteChargingAction(EvCharger *charger,
                                                const ChargingAction &action,
                                                const QDateTime &now)
{
    executeChargingAction(charger, action, now);
}

const QHash<ThingId, EvCharger *> &EnergyArbitrator::registeredEvChargers() const
{
    return internalEvChargers();
}

RootMeter *EnergyArbitrator::registeredRootMeter() const
{
    return internalRootMeter();
}

void EnergyArbitrator::registerEcsAdapter(EcsRelayAdapter *adapter)
{
    const QString id = adapter->descriptor().id;
    if (m_ecsAdapters.contains(id)) {
        qCWarning(dcNymeaEnergy()) << "[EnergyArbitrator] EcsRelayAdapter déjà enregistré:" << id;
        return;
    }
    adapter->setParent(this);
    m_ecsAdapters[id] = adapter;
    qCDebug(dcNymeaEnergy()) << "[EnergyArbitrator] EcsRelayAdapter enregistré:" << adapter->descriptor().label;
}

void EnergyArbitrator::update(const QDateTime &currentDateTime)
{
    qCDebug(dcNymeaEnergy()) << "Updating smart charging";
    // Ordre IDENTIQUE à SmartChargingManager::update() — INTERDIT de réordonner.
    // SCM : 1.updateManual  2.prepareInfo  3.verifyOverload  4.verifyRecovery
    //        5.planSpot     6.planSurplus  7.adjustEv
    // ETM : idem 1-4 ; insertions ETM entre 4 et 7 ;
    //        planSpot + planSurplus appelés via m_scheduler->getPlan() (position 5-6).

    // 1-4 : préparation + sécurité (même ordre que l'amont)
    updateManualSoCsWithoutMeter(currentDateTime);
    prepareInformation(currentDateTime);
    verifyOverloadProtection(currentDateTime);
    verifyOverloadProtectionRecovery(currentDateTime);

    // Mode dégradé L2 : la sécurité (L4 ci-dessus) reste active, mais on SUSPEND la
    // planification et le dispatch. Replanifier sur le cache d'un compteur mort
    // rallumerait les charges que le watchdog vient de couper → oscillation. Les
    // consignes de repli (posées à la transition) tiennent jusqu'au retour du compteur.
    if (m_degradedMode) {
        qCDebug(dcNymeaEnergy()) << "[Arbitre] Mode dégradé L2 actif — planification suspendue.";
        return;
    }

    // ETM-only : sync adapters + proxy planification → log [Arbitre]
    // getPlan() appelle planSpotMarketCharging() + planSurplusCharging() (position 5-6 amont).
    syncAdapters();
    SurplusContext ctx = buildContext(currentDateTime);
    Plan plan = m_scheduler->getPlan(ctx);
    Slot slot = plan.slotCovering(currentDateTime);

    for (const LoadAction &action : slot.actions) {
        qCInfo(dcNymeaEnergy()) << "[Arbitre]"
            << action.loadId << "→" << action.reason
            << "| activé:" << action.chargingEnabled
            << "| courant:" << action.currentA << "A"
            << "| phases:" << action.phaseCount
            << "| stratégie:" << plan.strategy;
    }

    // 7 : dispatch matériel (même position que l'amont — m_chargingActions rempli par getPlan())
    applyActionsToAdapters(slot, currentDateTime);  // ECS (kind==Stage) → m_ecsAdapters
    adjustEvChargers(currentDateTime);              // EV (kind==Setpoint) → proxy amont jusqu'à 3g
}

SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const
{
    SurplusContext ctx;
    ctx.timestamp = now;

    // --- Compteur principal (AGENTS invariant 8 : mesure brute, aucune déduction) ---
    RootMeter *meter = internalRootMeter();
    if (meter) {
        // currentPower() < 0 → export ; > 0 → import (convention amont SCM l.1141)
        const double p = meter->currentPower();
        ctx.meter.importW = qMax(0.0,  p);
        ctx.meter.exportW = qMax(0.0, -p);
        ctx.meter.perPhaseA = {
            meter->currentPhaseA(),
            meter->currentPhaseB(),
            meter->currentPhaseC()
        };
    }
    // SurplusPv : interface inverter — déféré (remplissage prévu en 3d)
    // SurplusBattery : déféré 3f

    // --- loads[] : EV adapters --- (now = ctx.timestamp : source unique des verrous)
    for (auto it = m_adapters.constBegin(); it != m_adapters.constEnd(); ++it)
        ctx.loads.append(it.value()->toLoadContext(now));

    // --- loads[] : ECS relay adapters ---
    for (auto it = m_ecsAdapters.constBegin(); it != m_ecsAdapters.constEnd(); ++it)
        ctx.loads.append(it.value()->toLoadContext(now));

    return ctx;
}

void EnergyArbitrator::syncAdapters()
{
    // Crée les adapters manquants
    for (auto it = internalEvChargers().constBegin(); it != internalEvChargers().constEnd(); ++it) {
        const QString id = it.key().toString();
        if (!m_adapters.contains(id))
            m_adapters[id] = new EvAdapter(it.value(), this);
    }
    // Supprime les adapters obsolètes
    for (const QString &id : m_adapters.keys()) {
        if (!internalEvChargers().contains(ThingId(id)))
            m_adapters.take(id)->deleteLater();
    }
}

void EnergyArbitrator::applyActionsToAdapters(const Slot &slot, const QDateTime &now)
{
    for (const LoadAction &action : slot.actions) {
        // EV (Setpoint) : dispatché par adjustEvChargers() amont jusqu'à 3g.
        if (action.kind != LoadAction::Stage)
            continue;

        EcsRelayAdapter *adapter = m_ecsAdapters.value(action.loadId);
        if (!adapter) {
            qCWarning(dcNymeaEnergy()) << "[Arbitre] action Stage sans adaptateur ECS:" << action.loadId;
            continue;
        }
        // L'adaptateur applique, écrête et verrouille (anti-rebond) — il ne décide pas.
        adapter->applyAction(action, now);
    }
}

void EnergyArbitrator::onMeterWatchdogTick()
{
    // Déclencheur réel (QTimer, horloge murale) → délègue à la logique injectable.
    evaluateMeterFreshness(QDateTime::currentDateTime());
}

void EnergyArbitrator::recordMeterUpdate(const QDateTime &now)
{
    m_lastMeterUpdate = now;
    if (m_degradedMode) {
        qCInfo(dcNymeaEnergy()) << "[Arbitre] Compteur de nouveau actif — sortie du mode dégradé L2.";
        m_degradedMode = false;
        emit chargingSchedulesChanged();  // pousse degradedMode=false (planif reprend au cycle suivant)
    }
}

void EnergyArbitrator::evaluateMeterFreshness(const QDateTime &now)
{
    if (!m_lastMeterUpdate.isValid())
        return;  // Aucune mesure reçue (démarrage) — pas de dégradé (invariant root meter absent).

    const qint64 silentS = m_lastMeterUpdate.secsTo(now);
    if (silentS <= MeterSilenceThresholdS)
        return;

    if (m_degradedMode)
        return;  // Déjà en repli — les consignes tiennent, pas de ré-émission (anti-oscillation).

    qCWarning(dcNymeaEnergy()) << "[Arbitre] Compteur muet depuis" << silentS
                               << "s (>" << MeterSilenceThresholdS << "s) — mode dégradé L2.";
    applyDegradedMode(now);
}

void EnergyArbitrator::applyDegradedMode(const QDateTime &now)
{
    m_degradedMode = true;
    emit chargingSchedulesChanged();  // pousse degradedMode=true (notification client L2)
    const QString reason =
        QStringLiteral("Compteur muet depuis >90 s — consigne de repli (L2 watchdog)");

    // ECS : tous paliers coupés (stage 0), force=true → bypass des verrous anti-rebond.
    for (EcsRelayAdapter *adapter : m_ecsAdapters) {
        LoadAction la;
        la.loadId = adapter->descriptor().id;
        la.kind   = LoadAction::Stage;
        la.stage  = 0;
        la.force  = true;
        la.reason = reason;
        adapter->applyAction(la, now);   // force=true → bypass verrous ; now = temps de cycle
    }

    // EV : repli CONSERVATEUR — n'initie aucune charge. On clampe seulement une charge
    // DÉJÀ en cours au courant minimum (force=true, bypass lock). Une borne branchée mais
    // non chargeante reste off (off volontaire possible : HC/spot à venir) ; débranchée →
    // aucune action. La garantie "jamais 0 A si branché" relève du failsafe L1 de la borne.
    for (auto it = internalEvChargers().constBegin(); it != internalEvChargers().constEnd(); ++it) {
        EvCharger *ev = it.value();
        if (ev->available() && ev->charging())
            ev->setMaxChargingCurrent(ev->maxChargingCurrentMinValue(), now, true);
    }

    // SG-Ready (état 1) / Batterie (aucune charge réseau) : repli ajouté avec leurs
    // adaptateurs (3e/3f). Le flag degradedMode + notification client arrivent en 3c-6.
}
