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

#include "ecsrelayadapter.h"
#include "plugininfo.h"

#include <QDateTime>
#include <integrations/thingmanager.h>
#include <integrations/thing.h>
#include <types/action.h>
#include <types/param.h>

EcsRelayAdapter::EcsRelayAdapter(ThingManager *thingManager,
                                  const QString &id,
                                  const QString &label,
                                  const QList<int> &stages,
                                  const QList<QList<QString>> &relayMapping,
                                  int minOnS,
                                  int minOffS,
                                  int priority,
                                  QObject *parent)
    : QObject(parent)
    , m_thingManager(thingManager)
    , m_id(id)
    , m_label(label)
    , m_stages(stages)
    , m_relayMapping(relayMapping)
    , m_minOnS(minOnS)
    , m_minOffS(minOffS)
    , m_priority(priority)
{
    Q_ASSERT(!m_stages.isEmpty() && m_stages.first() == 0);
    Q_ASSERT(m_relayMapping.size() == m_stages.size());
}

LoadDescriptor EcsRelayAdapter::descriptor() const
{
    LoadDescriptor d;
    d.id       = m_id;
    d.label    = m_label;
    d.adapter  = QStringLiteral("relay-stages");
    d.priority = m_priority;

    d.declared.stages  = m_stages;
    d.limits.minOnS    = m_minOnS;
    d.limits.minOffS   = m_minOffS;

    d.supportedKinds   = { LoadAction::Stage };
    return d;
}

LoadTelemetry EcsRelayAdapter::telemetry() const
{
    LoadTelemetry t;
    t.available   = true;
    t.lastActionAt = m_lastActionAt;

    // Source de currentPowerW (consommée par le recrédit anti-clignotement du waterfall,
    // RuleBasedScheduler::buildEcsStageAction — correction B) :
    //   - MESURÉE dès qu'au moins un relais du stage expose un state "currentPower" :
    //     somme des mesures. Un thermostat interne coupé (relais ON mais 0 W réel) renvoie
    //     alors 0 → recrédit 0, jamais de puissance fantôme.
    //   - DÉCLARÉE (repli) : stages[stage] UNIQUEMENT si aucun relais actif ne mesure
    //     (relais nus / mock sans powermetering). Approximation = palier à sa nominale.
    double power = 0;
    bool   metered = false;
    const QList<QString> &activeRelays = m_currentStage < m_relayMapping.size()
                                          ? m_relayMapping.at(m_currentStage)
                                          : QList<QString>();
    for (const QString &thingId : activeRelays) {
        Thing *t2 = m_thingManager->findConfiguredThing(ThingId(thingId));
        if (!t2)
            continue;
        if (!t2->thingClass().stateTypes().findByName("currentPower").id().isNull()) {
            metered = true;
            power  += t2->stateValue("currentPower").toDouble();
        }
    }
    // Repli déclaré : seulement si la mesure n'est pas disponible (pas si elle vaut 0).
    if (!metered && m_currentStage > 0 && m_currentStage < m_stages.size())
        power = m_stages.at(m_currentStage);

    t.currentPowerW = power;
    return t;
}

LoadContext EcsRelayAdapter::toLoadContext(const QDateTime &now) const
{
    LoadContext ctx;
    ctx.id      = m_id;
    ctx.adapter = QStringLiteral("relay-stages");
    ctx.label   = m_label;
    ctx.priority = m_priority;
    ctx.declared = descriptor().declared;
    ctx.limits   = descriptor().limits;

    const LoadTelemetry tel = telemetry();
    ctx.telemetry.currentPowerW = tel.currentPowerW;
    ctx.telemetry.stage         = m_currentStage;
    ctx.telemetry.lastSwitch    = m_lastSwitch;
    // Fenêtre de verrou évaluée au temps de cycle : le scheduler y borne son choix
    // (plancher = puissance engagée non-coupable sous minOn ; plafond = redémarrage minOff).
    lockWindow(now, ctx.telemetry.minStage, ctx.telemetry.maxStage);
    return ctx;
}

LoadAction EcsRelayAdapter::applyAction(const LoadAction &action, const QDateTime &now)
{
    if (action.kind != LoadAction::Stage)
        return action;

    if (action.reason.isEmpty()) {
        qCWarning(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label
                                   << "— LoadAction sans reason rejetée.";
        return action;
    }

    const int newStage = qBound(0, action.stage, m_stages.size() - 1);

    if (newStage == m_currentStage)
        return action;  // Aucun changement → idempotent

    // Verrous anti-rebond évalués au temps de cycle (même fenêtre que le scheduler) —
    // bypassés si force == true (L2 watchdog).
    if (!action.force && lockActive(newStage, now)) {
        qCDebug(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label
                                  << "— verrou anti-rebond actif, stage" << newStage << "ignoré.";
        return action;
    }

    qCDebug(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label
                              << "→ stage" << newStage
                              << "(" << (m_currentStage < m_stages.size() ? m_stages.at(m_currentStage) : 0) << "W"
                              << "→" << m_stages.at(newStage) << "W)"
                              << "|" << action.reason;

    applyRelayStage(newStage);

    m_currentStage = newStage;
    m_lastSwitch   = now;   // estampille au temps de cycle (cohérent avec la fenêtre de verrou)
    m_lastActionAt = now;

    LoadAction applied = action;
    applied.stage          = newStage;
    applied.estimatedPowerW = m_stages.at(newStage);
    return applied;
}

// ---- privé ---------------------------------------------------------------

void EcsRelayAdapter::lockWindow(const QDateTime &now, int &minStage, int &maxStage) const
{
    const int topStage = m_stages.size() - 1;
    const bool valid   = m_lastSwitch.isValid();
    const qint64 elapsed = valid ? m_lastSwitch.secsTo(now) : 0;

    // Plancher : si ON et minOn non écoulé → interdit de descendre sous le palier courant
    // (puissance engagée non-coupable, protection compresseur / anti court-cycling).
    minStage = (m_currentStage > 0 && valid && elapsed < m_minOnS) ? m_currentStage : 0;

    // Plafond : si à l'arrêt et minOff non écoulé → interdit de redémarrer.
    maxStage = (m_currentStage == 0 && valid && elapsed < m_minOffS) ? 0 : topStage;
}

bool EcsRelayAdapter::lockActive(int newStage, const QDateTime &now) const
{
    // MÊME calcul que la fenêtre exposée au scheduler → décision et exécution coïncident.
    int minStage, maxStage;
    lockWindow(now, minStage, maxStage);
    return newStage < minStage || newStage > maxStage;
}

void EcsRelayAdapter::applyRelayStage(int stage)
{
    // Ensemble des relais ON pour le nouveau stage
    const QSet<QString> wantOn = [&]() {
        QSet<QString> s;
        if (stage < m_relayMapping.size())
            for (const QString &id : m_relayMapping.at(stage))
                s.insert(id);
        return s;
    }();

    // Union de tous les relais connus
    QSet<QString> allRelays;
    for (const auto &list : m_relayMapping)
        for (const QString &id : list)
            allRelays.insert(id);

    for (const QString &thingId : allRelays) {
        Thing *relay = m_thingManager->findConfiguredThing(ThingId(thingId));
        if (!relay) {
            qCWarning(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label
                                       << "— relais non trouvé:" << thingId;
            continue;
        }
        const bool targetOn = wantOn.contains(thingId);
        StateType powerStateType = relay->thingClass().stateTypes().findByName("power");
        if (!powerStateType.id().isNull()) {
            Action powerAction(powerStateType.id(), relay->id(), Action::TriggeredByRule);
            powerAction.setParams(ParamList() << Param(powerStateType.id(), targetOn));
            m_thingManager->executeAction(powerAction);
        } else {
            // Fallback mock : setStateValue direct (Things sans actionType "power")
            relay->setStateValue("power", targetOn);
        }
    }
}
