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

#include "../smartchargingmanager.h"
#include "scheduler/ischeduler.h"
#include "types/surpluscontext.h"
#include "types/plan.h"

#include <QDateTime>

class QTimer;

class EvAdapter;
class EcsRelayAdapter;
class RuleBasedScheduler;

/*!
 * \brief Arbitre central ETM — remplace SmartChargingManager::update() (ETM_ARBITRATOR).
 *
 * Hérite de SmartChargingManager pour conserver la compatibilité API complète avec
 * NymeaEnergyJsonHandler sans modifier le code amont.
 * Seul update() est surchargé : préparation → sécurité → planificateur → adapters.
 *
 * \invariant UN seul arbitre : EnergyArbitrator décide, les EvAdapter exécutent (règle 1).
 * \invariant verifyOverloadProtection() est toujours appelée avant la planification (règle 4).
 * \invariant Toute LoadAction transmise aux adapters a un \c reason non vide (règle 7).
 * \invariant L'absence du root meter n'empêche pas le démarrage — cycle ignoré silencieusement.
 */
class EnergyArbitrator : public SmartChargingManager
{
    Q_OBJECT
public:
    explicit EnergyArbitrator(EnergyManager *energyManager, ThingManager *thingManager,
                               SpotMarketManager *spotMarketManager,
                               EnergyManagerConfiguration *configuration,
                               QObject *parent = nullptr);

    /*!
     * \brief Déclenche planSurplusCharging() (protégée) — appelé par RuleBasedScheduler.
     * \param now Instant courant du cycle.
     */
    void runSurplusPlanning(const QDateTime &now);

    /*!
     * \brief Déclenche planSpotMarketCharging() (protégée) — appelé par RuleBasedScheduler.
     * \param now Instant courant du cycle.
     */
    void runSpotMarketPlanning(const QDateTime &now);

    /*!
     * \brief Actions planifiées (résultat de runSurplus/SpotMarket).
     * \return Référence constante vers la table EvCharger* → ChargingActions.
     * \note Valide seulement après runSurplusPlanning() / runSpotMarketPlanning().
     */
    const QHash<EvCharger *, ChargingActions> &scheduledActions() const;

    /*!
     * \brief Pont d'exécution pour EvAdapter — délègue à executeChargingAction() protégée.
     * \param charger Borne EV cible.
     * \param action  ChargingAction à appliquer.
     * \param now     Instant de l'action (pour les locks anti-rebond).
     */
    void doExecuteChargingAction(EvCharger *charger, const ChargingAction &action, const QDateTime &now);

    /*!
     * \brief Liste des EvCharger enregistrés (lecture seule).
     * \return Table ThingId → EvCharger*.
     */
    const QHash<ThingId, EvCharger *> &registeredEvChargers() const;

    /*!
     * \brief Root meter courant.
     * \return Pointeur ou nullptr si aucun compteur principal n'est enregistré.
     */
    RootMeter *registeredRootMeter() const;

    /*!
     * \brief Enregistre un EcsRelayAdapter pour inclusion dans le contexte et le dispatch.
     *
     * Appelé par le test (setup) ou la configuration de production.
     * L'adaptateur est adopté comme enfant Qt de l'arbitre.
     * \param adapter Adaptateur à enregistrer. Son \c descriptor().id doit être unique.
     */
    void registerEcsAdapter(EcsRelayAdapter *adapter);

    /*!
     * \brief Mode dégradé L2 actif (compteur muet > 90 s) — override de SmartChargingManager.
     * \return \c true tant que les consignes de repli L2 tiennent ; \c false en régime normal.
     * \note Exposé dans la notification \c NymeaEnergy.ChargingSchedulesChanged (champ
     *   \c degradedMode), émise aussi aux transitions de ce flag.
     */
    bool degradedMode() const override { return m_degradedMode; }

    /*!
     * \brief Enregistre une mesure fraîche du compteur à l'instant \p now (logique L2).
     *
     * Met à jour \c m_lastMeterUpdate et, si le mode dégradé était actif, en sort
     * (\c degradedMode=false + notification). \p now = temps de cycle.
     * \note Logique injectable (temps en paramètre) — en production appelée par le
     *   handler \c powerBalanceChanged ; en simulation/test appelée directement. Le
     *   déclencheur réel (signal) est câblé sous \c \#ifndef ENERGY_SIMULATION.
     */
    void recordMeterUpdate(const QDateTime &now);

    /*!
     * \brief Évalue la fraîcheur du compteur à \p now et bascule en mode dégradé si muet >90 s.
     *
     * Si \c now − \c m_lastMeterUpdate > 90 s et pas déjà dégradé → \c applyDegradedMode().
     * Appliqué à la TRANSITION uniquement (idempotent ensuite). \p now = temps de cycle.
     * \note Logique injectable — en production appelée par \c onMeterWatchdogTick() (QTimer
     *   horloge murale, indépendant car le compteur muet fige aussi \c update()) ; en
     *   simulation/test appelée directement avec le temps simulé. Symétrique de
     *   \c simulationCallUpdate : déclencheur réel en prod, logique testable par injection.
     */
    void evaluateMeterFreshness(const QDateTime &now);

protected:
    /*!
     * \brief Boucle principale ETM — surcharge SmartChargingManager::update().
     *
     * Ordre garanti :
     *  1. updateManualSoCsWithoutMeter()
     *  2. prepareInformation()
     *  3. verifyOverloadProtection() + verifyOverloadProtectionRecovery()
     *     (si \c m_degradedMode actif : retour immédiat — planification/dispatch suspendus, L2)
     *  4. m_scheduler->getPlan() → log des decisionReason
     *  5. applyActionsToAdapters() (ECS Stage) + adjustEvChargers() (EV) → dispatch matériel
     *
     * \param currentDateTime Instant courant (timer ou simulation).
     */
    void update(const QDateTime &currentDateTime) override;

private:
    /*!
     * \brief Construit le SurplusContext §5 : meter brut + loads EV + loads ECS.
     *
     * \c ctx.meter.exportW = mesure brute du compteur (AGENTS invariant 8 — aucune
     * déduction interne). La déduction evReservedW est faite dans le scheduler.
     */
    SurplusContext buildContext(const QDateTime &now) const;

    /*!
     * \brief Synchronise m_adapters avec les EvCharger actuellement enregistrés.
     * Crée les adapters manquants, supprime les adapters obsolètes.
     * \note Découverte ECS via interface 'ecsrelay' ThingManager — déféré 3g config.
     *   En beta, les EcsRelayAdapters sont enregistrés via registerEcsAdapter().
     */
    void syncAdapters();

    /*!
     * \brief Applique les actions d'un Slot aux LoadAdapters non-EV (ECS en 3c).
     *
     * Itère \c slot.actions et dispatche chaque action \c kind==Stage vers le
     * EcsRelayAdapter correspondant (\c m_ecsAdapters[action.loadId]). Les actions EV
     * (\c kind==Setpoint) restent dispatchées par \c adjustEvChargers() amont jusqu'à 3g.
     * L'adaptateur écrête/verrouille lui-même (anti-rebond) et ignore toute action sans
     * \c reason ou de kind non supporté — aucune décision ici (règle 2).
     * \param slot Créneau courant retourné par le scheduler.
     * \param now  Temps de cycle (\c ctx.timestamp) — transmis aux adaptateurs pour une
     *   évaluation des verrous cohérente avec celle vue par le scheduler.
     */
    void applyActionsToAdapters(const Slot &slot, const QDateTime &now);

    /*!
     * \brief Tick du watchdog L2 (SAFETY.md §L2) — piloté par \c m_meterWatchdog (QTimer 30 s).
     *
     * Si \c QDateTime::currentDateTime() − \c m_lastMeterUpdate dépasse 90 s, déclenche
     * \c applyDegradedMode(). Indépendant des signaux compteur : reste actif précisément
     * quand le compteur est muet (le signal \c powerBalanceChanged ne fire plus).
     * \note Tant que \c m_lastMeterUpdate est invalide (aucune mesure reçue depuis le
     *   démarrage), aucun mode dégradé n'est déclenché (invariant root meter absent).
     */
    void onMeterWatchdogTick();

    /*!
     * \brief Applique les consignes de repli L2 (SAFETY.md §L2, Variante B).
     *
     * Repli CONSERVATEUR (n'initie aucune charge) : ECS → palier 0 \c force=true (bypass
     * anti-rebond) ; EV en charge → clamp courant minimum borne ; EV branché non chargeant
     * ou débranché → aucune action (planification en cours respectée ; "jamais 0 A si
     * branché" relève du failsafe L1). SG-Ready/Batterie : repli ajouté à l'arrivée de
     * leurs adaptateurs (3e/3f). Positionne \c m_degradedMode.
     *
     * \note Appelé une seule fois à la TRANSITION vers le mode dégradé. Ensuite \c update()
     *   suspend la planification, donc les consignes tiennent sans ré-émission par tick.
     * \param now Instant courant (locks anti-rebond des bornes EV).
     */
    void applyDegradedMode(const QDateTime &now);

    RuleBasedScheduler               *m_scheduler = nullptr;
    QHash<QString, EvAdapter *>       m_adapters;     //!< loadId (ThingId string) → EvAdapter*.
    QHash<QString, EcsRelayAdapter *> m_ecsAdapters;  //!< loadId → EcsRelayAdapter*.

    // --- L2 watchdog fraîcheur compteur (SAFETY.md §L2) ---
    QTimer    *m_meterWatchdog  = nullptr;  //!< Tick 30 s, indépendant des signaux compteur.
    QDateTime  m_lastMeterUpdate;           //!< Horodatage du dernier powerBalanceChanged.
    bool       m_degradedMode   = false;    //!< Vrai si les consignes de repli L2 sont actives.
};
