#include <ModbusRTU.h>
#include <Arduino.h>
#include <Preferences.h>
#include <time.h>
#include <sys/time.h>
#include "config.h"
#include "state.h"
#include "debug_log.h"

static ModbusRTU mb;

static uint32_t intervalleJour = INTERVALLE_MODBUS;   // ms — mode soleil
static uint32_t intervalleNuit = 30000UL;             // ms — mode veille

static uint32_t intervalCourant() {
    if (state.last_update == 0) return intervalleJour; // première lecture toujours rapide
    return state.sun ? intervalleJour : intervalleNuit;
}

void setIntervallesModbus(uint32_t jour_ms, uint32_t nuit_ms) {
    intervalleJour = jour_ms;
    intervalleNuit = nuit_ms;
    Preferences p; p.begin("modbus", false);
    p.putUInt("jour", jour_ms);
    p.putUInt("nuit", nuit_ms);
    p.end();
    Serial.printf("[Modbus] Intervalles — jour:%ums nuit:%ums\n", jour_ms, nuit_ms);
}

void getIntervallesModbus(uint32_t &jour_ms, uint32_t &nuit_ms) {
    jour_ms = intervalleJour;
    nuit_ms = intervalleNuit;
}

// Buffers de réception — un par groupe de registres
static uint16_t bufPV[8];        // 0x3100..0x3107 : PV, batterie, courant/puissance charge
static uint16_t bufLoad[5];      // 0x310C..0x3110 : load + température batterie
static uint16_t bufSOC[1];       // 0x311A : SOC %
static uint16_t bufStatus[2];    // 0x3200 : Battery status  |  0x3201 : Charging status
static uint16_t bufEnergie[16];  // 0x3304..0x3313 : kWh consommés/générés
static bool     bufJourNuit[1];  // 0x200C : jour/nuit (FC02 discrete input)

static unsigned long tDerniereLecture = 0;
static unsigned long tDebutRequete    = 0;
static bool          lectureEnCours   = false;
static uint32_t      nbLecturesOK     = 0;
static uint32_t      nbErreurs        = 0;
static uint8_t       derniereErreur   = 0;
static const char   *derniereEtape    = "aucune";
static unsigned long tDerniereSyncRtc = 0;
static bool          rtcSyncedOnce    = false;
static const unsigned long INTERVALLE_SYNC_RTC = 21600000UL; // 6h
static bool          dernierSun       = false;
static bool          dernierSunValide = false;

static void finaliserLecture();

static uint16_t crc16Modbus(const uint8_t *buf, size_t len) {
    uint16_t crc = 0xFFFF;
    for (size_t i = 0; i < len; i++) {
        crc ^= buf[i];
        for (uint8_t bit = 0; bit < 8; bit++) {
            crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
        }
    }
    return crc;
}

static void dumpHex(const char *prefix, const uint8_t *buf, size_t len) {
    String ligne;
    ligne.reserve(40 + len * 3);
    ligne += prefix;
    ligne += " (";
    ligne += (unsigned)len;
    ligne += " octets):";
    for (size_t i = 0; i < len; i++) {
        char hex[4];
        snprintf(hex, sizeof(hex), " %02X", buf[i]);
        ligne += hex;
    }
    Serial.println(ligne);
    debugLogLine(ligne.c_str());
}

static void viderRx(const char *raison) {
    uint8_t buf[MODBUS_DEBUG_RX_MAX];
    size_t n = 0;
    while (Serial2.available() && n < sizeof(buf)) {
        buf[n++] = (uint8_t)Serial2.read();
    }
    while (Serial2.available()) Serial2.read();
    if (n > 0) {
        debugLogf("[Modbus][debug] RX vidé avant %s", raison);
        dumpHex("[Modbus][debug] octets parasites", buf, n);
    }
}

static bool probeRegistreBatterie(uint32_t baudrate) {
#if MODBUS_DEBUG_BOOT
    debugLogf("[Modbus][probe] Test direct 0x3104 à %u bauds, esclave %d",
                  baudrate, MODBUS_ADRESSE);

    Serial2.end();
    delay(20);
    Serial2.begin(baudrate, SERIAL_8N1, PIN_RS485_RX, PIN_RS485_TX);
    delay(50);
    viderRx("probe");

    uint8_t req[8] = {
        MODBUS_ADRESSE,
        0x04,
        0x31, 0x04,
        0x00, 0x01,
        0x00, 0x00
    };
    uint16_t crc = crc16Modbus(req, 6);
    req[6] = crc & 0xFF;
    req[7] = crc >> 8;

    dumpHex("[Modbus][probe] TX", req, sizeof(req));
    Serial2.write(req, sizeof(req));
    Serial2.flush();

    uint8_t resp[MODBUS_DEBUG_RX_MAX];
    size_t n = 0;
    unsigned long t0 = millis();
    unsigned long dernierOctet = t0;

    while ((millis() - t0) < 700 && n < sizeof(resp)) {
        while (Serial2.available() && n < sizeof(resp)) {
            resp[n++] = (uint8_t)Serial2.read();
            dernierOctet = millis();
        }
        if (n >= 7 && (millis() - dernierOctet) > 20) break;
        delay(1);
    }

    if (n == 0) {
        debugLogf("[Modbus][probe] Aucun octet reçu à %u bauds", baudrate);
        debugLogf("[Modbus][probe] Causes probables: A/B inversés, GND absent, mauvais baudrate, mauvais ID, Epever non alimenté.");
        return false;
    }

    dumpHex("[Modbus][probe] RX", resp, n);

    if (n < 5) {
        debugLogf("[Modbus][probe] Réponse trop courte: bruit RS485 ou baudrate incorrect probable.");
        return false;
    }
    if (resp[0] != MODBUS_ADRESSE) {
        debugLogf("[Modbus][probe] Adresse inattendue: reçu %u, attendu %u",
                      resp[0], MODBUS_ADRESSE);
        return false;
    }
    if (resp[1] & 0x80) {
        debugLogf("[Modbus][probe] Exception Modbus fonction 0x%02X code 0x%02X",
                      resp[1], n > 2 ? resp[2] : 0);
        return false;
    }
    if (resp[1] != 0x04 || resp[2] != 0x02 || n < 7) {
        debugLogf("[Modbus][probe] Format inattendu pour lecture input register 0x3104.");
        return false;
    }

    uint16_t crcCalc = crc16Modbus(resp, 5);
    uint16_t crcRx = (uint16_t)resp[5] | ((uint16_t)resp[6] << 8);
    if (crcCalc != crcRx) {
        debugLogf("[Modbus][probe] CRC invalide: calcul 0x%04X, reçu 0x%04X", crcCalc, crcRx);
        return false;
    }

    uint16_t brut = ((uint16_t)resp[3] << 8) | resp[4];
    debugLogf("[Modbus][probe] OK à %u bauds: batterie %.2f V (brut 0x%04X)",
                  baudrate, brut * 0.01f, brut);
    return true;
#else
    (void)baudrate;
    return false;
#endif
}

static bool lireRegistresBrutsFc(uint8_t fonction, uint16_t registre, uint16_t quantite,
                                 uint16_t *dest, uint16_t timeoutMs, bool logSucces = false) {
    if (quantite == 0 || quantite > 24) return false;

    viderRx("lecture brute");

    uint8_t req[8] = {
        MODBUS_ADRESSE,
        fonction,
        (uint8_t)(registre >> 8), (uint8_t)(registre & 0xFF),
        (uint8_t)(quantite >> 8), (uint8_t)(quantite & 0xFF),
        0x00, 0x00
    };
    uint16_t crc = crc16Modbus(req, 6);
    req[6] = crc & 0xFF;
    req[7] = crc >> 8;

    Serial2.write(req, sizeof(req));
    Serial2.flush();

    const size_t attendu = 5 + (size_t)quantite * 2;
    uint8_t resp[MODBUS_DEBUG_RX_MAX];
    size_t n = 0;
    unsigned long t0 = millis();
    unsigned long dernierOctet = t0;

    while ((millis() - t0) < timeoutMs && n < sizeof(resp)) {
        while (Serial2.available() && n < sizeof(resp)) {
            resp[n++] = (uint8_t)Serial2.read();
            dernierOctet = millis();
        }
        if (n >= attendu && (millis() - dernierOctet) > 5) break;
        delay(1);
    }

    if (n == 0) {
        nbErreurs++;
        derniereErreur = 0xE0;
        derniereEtape = "Brut";
        debugLogf("[Modbus][brut] Timeout FC%02u registre 0x%04X, aucun octet reçu", fonction, registre);
        dumpHex("[Modbus][brut] TX", req, sizeof(req));
        return false;
    }

    if (n < attendu) {
        nbErreurs++;
        derniereErreur = 0xE2;
        derniereEtape = "Brut court";
        debugLogf("[Modbus][brut] Réponse courte FC%02u registre 0x%04X: reçu %u, attendu %u",
                  fonction, registre, (unsigned)n, (unsigned)attendu);
        dumpHex("[Modbus][brut] TX", req, sizeof(req));
        dumpHex("[Modbus][brut] RX", resp, n);
        return false;
    }

    uint16_t crcCalc = crc16Modbus(resp, attendu - 2);
    uint16_t crcRx = (uint16_t)resp[attendu - 2] | ((uint16_t)resp[attendu - 1] << 8);
    if (crcCalc != crcRx) {
        nbErreurs++;
        derniereErreur = 0xE1;
        derniereEtape = "Brut CRC";
        debugLogf("[Modbus][brut] CRC invalide FC%02u registre 0x%04X: calcul 0x%04X, reçu 0x%04X",
                  fonction, registre, crcCalc, crcRx);
        dumpHex("[Modbus][brut] TX", req, sizeof(req));
        dumpHex("[Modbus][brut] RX", resp, n);
        return false;
    }

    if (resp[0] != MODBUS_ADRESSE || resp[1] != fonction || resp[2] != quantite * 2) {
        nbErreurs++;
        derniereErreur = resp[1];
        derniereEtape = "Brut format";
        debugLogf("[Modbus][brut] Format inattendu FC%02u registre 0x%04X", fonction, registre);
        dumpHex("[Modbus][brut] TX", req, sizeof(req));
        dumpHex("[Modbus][brut] RX", resp, n);
        return false;
    }

    for (uint16_t i = 0; i < quantite; i++) {
        dest[i] = ((uint16_t)resp[3 + i * 2] << 8) | resp[4 + i * 2];
    }

    if (logSucces) {
        debugLogf("[Modbus][brut] OK FC%02u registre 0x%04X x%u", fonction, registre, quantite);
    }
    return true;
}

static bool lireRegistresBruts(uint16_t registre, uint16_t quantite, uint16_t *dest,
                               uint16_t timeoutMs, bool logSucces = false) {
    return lireRegistresBrutsFc(0x04, registre, quantite, dest, timeoutMs, logSucces);
}

static bool lireHoldingBruts(uint16_t registre, uint16_t quantite, uint16_t *dest,
                             uint16_t timeoutMs, bool logSucces = false) {
    return lireRegistresBrutsFc(0x03, registre, quantite, dest, timeoutMs, logSucces);
}

static bool ecrireHoldingMultiplesBrut(uint16_t registre, uint16_t quantite,
                                       const uint16_t *valeurs, uint16_t timeoutMs) {
    if (quantite == 0 || quantite > 12) return false;
    viderRx("écriture holding brute");

    uint8_t req[MODBUS_DEBUG_RX_MAX];
    size_t len = 7 + (size_t)quantite * 2 + 2;
    if (len > sizeof(req)) return false;

    req[0] = MODBUS_ADRESSE;
    req[1] = 0x10;
    req[2] = registre >> 8;
    req[3] = registre & 0xFF;
    req[4] = quantite >> 8;
    req[5] = quantite & 0xFF;
    req[6] = quantite * 2;
    for (uint16_t i = 0; i < quantite; i++) {
        req[7 + i * 2] = valeurs[i] >> 8;
        req[8 + i * 2] = valeurs[i] & 0xFF;
    }
    uint16_t crc = crc16Modbus(req, len - 2);
    req[len - 2] = crc & 0xFF;
    req[len - 1] = crc >> 8;

    dumpHex("[Modbus][write] TX", req, len);
    Serial2.write(req, len);
    Serial2.flush();

    uint8_t resp[8];
    size_t n = 0;
    unsigned long t0 = millis();
    while ((millis() - t0) < timeoutMs && n < sizeof(resp)) {
        while (Serial2.available() && n < sizeof(resp)) resp[n++] = (uint8_t)Serial2.read();
        if (n >= 8) break;
        delay(1);
    }

    if (n < 8) {
        debugLogf("[Modbus][write] Réponse courte FC16 registre 0x%04X: %u octets", registre, (unsigned)n);
        if (n) dumpHex("[Modbus][write] RX", resp, n);
        return false;
    }

    uint16_t crcCalc = crc16Modbus(resp, 6);
    uint16_t crcRx = (uint16_t)resp[6] | ((uint16_t)resp[7] << 8);
    bool ok = resp[0] == MODBUS_ADRESSE && resp[1] == 0x10 &&
              resp[2] == (registre >> 8) && resp[3] == (registre & 0xFF) &&
              resp[4] == (quantite >> 8) && resp[5] == (quantite & 0xFF) &&
              crcCalc == crcRx;
    dumpHex("[Modbus][write] RX", resp, n);
    debugLogf("[Modbus][write] FC16 0x%04X x%u -> %s", registre, quantite, ok ? "OK" : "ERREUR");
    return ok;
}

static bool lireEntreesDiscretesBrut(uint16_t adresse, bool *dest, uint16_t timeoutMs) {
    viderRx("lecture discrete brute");

    uint8_t req[8] = {
        MODBUS_ADRESSE,
        0x02,
        (uint8_t)(adresse >> 8), (uint8_t)(adresse & 0xFF),
        0x00, 0x01,
        0x00, 0x00
    };
    uint16_t crc = crc16Modbus(req, 6);
    req[6] = crc & 0xFF;
    req[7] = crc >> 8;

    Serial2.write(req, sizeof(req));
    Serial2.flush();

    uint8_t resp[8];
    size_t n = 0;
    unsigned long t0 = millis();
    while ((millis() - t0) < timeoutMs && n < 6) {
        while (Serial2.available() && n < sizeof(resp)) resp[n++] = (uint8_t)Serial2.read();
        if (n >= 6) break;
        delay(1);
    }

    if (n < 6) {
        debugLogf("[Modbus][brut] Jour/nuit 0x%04X ignoré: pas de réponse FC02", adresse);
        return false;
    }

    uint16_t crcCalc = crc16Modbus(resp, 4);
    uint16_t crcRx = (uint16_t)resp[4] | ((uint16_t)resp[5] << 8);
    if (resp[0] != MODBUS_ADRESSE || resp[1] != 0x02 || resp[2] != 1 || crcCalc != crcRx) {
        debugLogf("[Modbus][brut] Jour/nuit 0x%04X ignoré: format/CRC invalide", adresse);
        dumpHex("[Modbus][brut] RX FC02", resp, n);
        return false;
    }

    *dest = (resp[3] & 0x01) != 0;
    debugLogf("[Modbus][brut] FC02 0x%04X = %u", adresse, *dest ? 1 : 0);
    return true;
}

static float u32x100(const uint16_t *reg, uint8_t indexL) {
    // Le PDF EPEVER indique les 32 bits en deux registres: L puis H.
    return (((uint32_t)reg[indexL + 1] << 16) | reg[indexL]) * 0.01f;
}

static void calerHorlogeEspDepuisEpever() {
    struct tm tmRtc = {};
    tmRtc.tm_year = state.epeverYear - 1900;
    tmRtc.tm_mon  = state.epeverMonth - 1;
    tmRtc.tm_mday = state.epeverDay;
    tmRtc.tm_hour = state.epeverHour;
    tmRtc.tm_min  = state.epeverMinute;
    tmRtc.tm_sec  = state.epeverSecond;

    time_t epoch = mktime(&tmRtc);
    if (epoch <= 0) {
        state.espClockOk = false;
        debugLogf("[RTC] Impossible de convertir l'heure Epever en epoch");
        return;
    }

    timeval tv = { epoch, 0 };
    settimeofday(&tv, nullptr);
    state.espClockOk = true;
    debugLogf("[RTC] Horloge ESP32 calée depuis Epever");
}

static void lireHorlogeEpever(bool force) {
    unsigned long maintenant = millis();
    if (!force && rtcSyncedOnce && (maintenant - tDerniereSyncRtc) < INTERVALLE_SYNC_RTC) return;

    uint16_t rtc[3];
    if (!lireHoldingBruts(0x9013, 3, rtc, 700, true)) {
        state.epeverClockOk = false;
        debugLogf("[Modbus][rtc] Horloge Epever indisponible");
        return;
    }

    state.epeverSecond = rtc[0] & 0xFF;
    state.epeverMinute = (rtc[0] >> 8) & 0xFF;
    state.epeverHour   = rtc[1] & 0xFF;
    state.epeverDay    = (rtc[1] >> 8) & 0xFF;
    state.epeverMonth  = rtc[2] & 0xFF;
    state.epeverYear   = 2000 + ((rtc[2] >> 8) & 0xFF);

    bool valide = state.epeverSecond < 60 && state.epeverMinute < 60 &&
                  state.epeverHour < 24 && state.epeverDay >= 1 &&
                  state.epeverDay <= 31 && state.epeverMonth >= 1 &&
                  state.epeverMonth <= 12;
    state.epeverClockOk = valide;

    debugLogf("[Modbus][rtc] %04u-%02u-%02u %02u:%02u:%02u (%s)",
              state.epeverYear, state.epeverMonth, state.epeverDay,
              state.epeverHour, state.epeverMinute, state.epeverSecond,
              valide ? "OK" : "invalide");

    if (valide) {
        tDerniereSyncRtc = maintenant;
        rtcSyncedOnce = true;
        calerHorlogeEspDepuisEpever();
    }
}

static void enregistrerChangementSoleil(bool nouveauSun) {
    if (!dernierSunValide) {
        dernierSun = nouveauSun;
        dernierSunValide = true;
        return;
    }
    if (nouveauSun == dernierSun) return;

    dernierSun = nouveauSun;
    uint8_t idx = state.sunHistoryHead;
    state.sunHistoryState[idx] = nouveauSun;

    if (state.espClockOk) {
        time_t now = time(nullptr);
        struct tm tmNow;
        localtime_r(&now, &tmNow);
        snprintf(state.sunHistoryTime[idx], sizeof(state.sunHistoryTime[idx]),
                 "%04d-%02d-%02d %02d:%02d:%02d",
                 tmNow.tm_year + 1900, tmNow.tm_mon + 1, tmNow.tm_mday,
                 tmNow.tm_hour, tmNow.tm_min, tmNow.tm_sec);
    } else if (state.epeverClockOk) {
        snprintf(state.sunHistoryTime[idx], sizeof(state.sunHistoryTime[idx]),
                 "%04u-%02u-%02u %02u:%02u:%02u",
                 state.epeverYear, state.epeverMonth, state.epeverDay,
                 state.epeverHour, state.epeverMinute, state.epeverSecond);
    } else {
        snprintf(state.sunHistoryTime[idx], sizeof(state.sunHistoryTime[idx]),
                 "uptime %lus", millis() / 1000);
    }

    state.sunHistoryHead = (state.sunHistoryHead + 1) % 5;
    if (state.sunHistoryCount < 5) state.sunHistoryCount++;
    state.sunHistoryValid = true;
    debugLogf("[SUN] Changement état -> %s à %s",
              nouveauSun ? "JOUR" : "NUIT", state.sunHistoryTime[idx]);
}

static bool effectuerLectureBruteEpever() {
    uint16_t pv[8];
    uint16_t load[5];
    uint16_t soc[1];
    uint16_t status[2];
    uint16_t energie[18];
    bool nuit = false;

    debugLogf("[Modbus][brut] Début cycle lecture Epever");

    if (!lireRegistresBruts(0x3100, 8, pv, 700, true)) return false;
    if (!lireRegistresBruts(0x310C, 5, load, 700, true)) return false;
    if (!lireRegistresBruts(0x311A, 1, soc, 700, true)) return false;
    if (!lireRegistresBruts(0x3200, 2, status, 700, true)) return false;
    lireHorlogeEpever(false);

    if (lireRegistresBruts(0x3302, 18, energie, 900, false)) {
        // Base 0x3302 selon MODBUS-Protocol-v25.pdf:
        // 0x3304/05 conso jour, 0x330A/0B conso totale,
        // 0x330C/0D production jour, 0x3312/13 production totale.
        state.energieConJour  = u32x100(energie, 2);
        state.energieConTotal = u32x100(energie, 8);
        state.energieGenJour  = u32x100(energie, 10);
        state.energieGenTotal = u32x100(energie, 16);
        debugLogf("[Modbus][brut] Energie: genJ=%.2fkWh consoJ=%.2fkWh genTot=%.2fkWh consoTot=%.2fkWh",
                  state.energieGenJour, state.energieConJour,
                  state.energieGenTotal, state.energieConTotal);
    } else {
        debugLogf("[Modbus][brut] Energie ignorée, les valeurs précédentes sont conservées");
    }

    state.pv        = pv[0] * 0.01f;
    state.pvCurrent = pv[1] * 0.01f;
    state.battery   = pv[4] * 0.01f;

    state.loadVoltage    = load[0] * 0.01f;
    state.loadCurrent    = load[1] * 0.01f;
    state.loadPower      = u32x100(load, 2);       // 0x310E/0x310F L/H
    state.batTemperature = (int16_t)load[4] * 0.01f;

    state.batSOC = (uint8_t)constrain((int)soc[0], 0, 100);

    uint8_t batVoltStatus = status[0] & 0x0F;
    state.batSousVoltage = (batVoltStatus == 2);
    state.batSurVoltage  = (batVoltStatus == 1);
    state.batStatut = (status[1] >> 2) & 0x03;

    if (lireEntreesDiscretesBrut(0x200C, &nuit, 500)) {
        // Le registre officiel dit 1=Nuit, 0=Jour. Si le PV est clairement
        // présent, on force jour pour éviter un état incohérent côté UI.
        state.sun = !nuit || state.pv > 2.0f;
    } else {
        state.sun = state.pv > 2.0f;
    }
    enregistrerChangementSoleil(state.sun);

    debugLogf("[Modbus][brut] Bruts: 3110(temp)=0x%04X 311A(SOC)=%u 3200=0x%04X 3201=0x%04X sun=%u",
              load[4], soc[0], status[0], status[1], state.sun ? 1 : 0);

    finaliserLecture();
    return true;
}

bool reglerHorlogeEpever(uint16_t annee, uint8_t mois, uint8_t jour,
                         uint8_t heure, uint8_t minute, uint8_t seconde) {
    if (lectureEnCours) {
        debugLogf("[Modbus][rtc] Réglage refusé: cycle lecture en cours");
        return false;
    }
    if (annee < 2000 || annee > 2099 || mois < 1 || mois > 12 || jour < 1 || jour > 31 ||
        heure > 23 || minute > 59 || seconde > 59) {
        debugLogf("[Modbus][rtc] Réglage refusé: date/heure invalide");
        return false;
    }

    uint16_t regs[3];
    regs[0] = ((uint16_t)minute << 8) | seconde;          // 0x9013
    regs[1] = ((uint16_t)jour << 8) | heure;              // 0x9014
    regs[2] = ((uint16_t)(annee - 2000) << 8) | mois;     // 0x9015

    lectureEnCours = true;
    bool ok = ecrireHoldingMultiplesBrut(0x9013, 3, regs, 1000);
    lectureEnCours = false;

    if (ok) {
        rtcSyncedOnce = false;
        lireHorlogeEpever(true);
    }
    return ok;
}

static void debugBootModbus() {
#if MODBUS_DEBUG_BOOT
    debugLogf("--- Diagnostic RS485 boot ---");
    debugLogf("  UART ESP32      : Serial2");
    debugLogf("  RX GPIO         : %d", PIN_RS485_RX);
    debugLogf("  TX GPIO         : %d", PIN_RS485_TX);
    debugLogf("  Adresse Epever  : %d", MODBUS_ADRESSE);
    debugLogf("  Baud principal  : %u", (uint32_t)MODBUS_BAUDRATE);
    debugLogf("  Trame test      : FC04 registre 0x3104 tension batterie");
    debugLogf("  Rappel câblage  : Epever A/D+ vers KC868 A, Epever B/D- vers KC868 B, GND recommandé");

    bool okPrincipal = probeRegistreBatterie(MODBUS_BAUDRATE);
    if (!okPrincipal && MODBUS_BAUDRATE != 9600) probeRegistreBatterie(9600);
    if (!okPrincipal && MODBUS_BAUDRATE != 115200) probeRegistreBatterie(115200);

    debugLogf("--- Fin diagnostic RS485 boot ---");
#endif
}

// Fin de chaîne — appelé après la dernière lecture
static void finaliserLecture() {
    state.rs485_ok    = true;
    state.last_update = millis();
    lectureEnCours    = false;
    nbLecturesOK++;
    debugLogf("Modbus OK #%u — Bat:%.2fV %d%%  PV:%.2fV %.2fA  Load:%.1fW  %s",
                  nbLecturesOK,
                  state.battery, state.batSOC, state.pv, state.pvCurrent,
                  state.loadPower, state.sun ? "JOUR" : "NUIT");
}

static const char* codeModbus(uint8_t code) {
    switch (code) {
        case 0x01: return "Fonction non supportée";
        case 0x02: return "Adresse registre invalide";
        case 0x03: return "Valeur invalide";
        case 0x04: return "Erreur matérielle esclave";
        case 0xE0: return "Timeout (pas de réponse)";
        case 0xE1: return "CRC invalide";
        case 0xE2: return "Exception générale";
        default:   return "Inconnu";
    }
}

static void erreurLecture(const char *etape, uint8_t code) {
    state.rs485_ok = false;
    lectureEnCours = false;
    nbErreurs++;
    derniereErreur = code;
    derniereEtape = etape;
    debugLogf("Modbus ERREUR #%u [%s] code=0x%02X (%s), ok=%u, uptime=%lums",
                  nbErreurs, etape, code, codeModbus(code), nbLecturesOK, millis());
    debugLogf("[Modbus][aide] Si timeout: vérifier A/B, GND, baudrate, ID=%d, RJ45 Epever non branché sur Ethernet.",
                  MODBUS_ADRESSE);
}

// Chaîne de lectures : PV → Load → SOC → Status → Energie → JourNuit → fin

static bool cbJourNuit(Modbus::ResultCode ev, uint16_t, void*) {
    if (ev == Modbus::EX_SUCCESS) {
        // 0x200C FC02 : bit D0 = 1 → Nuit, 0 → Jour
        state.sun = !bufJourNuit[0];
    } else {
        Serial.printf("Modbus [JourNuit] : 0x%02X — ignoré\n", ev);
    }
    finaliserLecture();  // non-fatal : on finalise dans tous les cas
    return true;
}

static bool cbEnergie(Modbus::ResultCode ev, uint16_t, void*) {
    if (ev == Modbus::EX_SUCCESS) {
        // Lecture depuis 0x3300, registres 32 bits little-endian (L word first)
        state.energieGenJour  = ((uint32_t)bufEnergie[1]  << 16 | bufEnergie[0])  * 0.01f; // 0x3300-01
        state.energieGenTotal = ((uint32_t)bufEnergie[7]  << 16 | bufEnergie[6])  * 0.01f; // 0x3306-07
        state.energieConJour  = ((uint32_t)bufEnergie[9]  << 16 | bufEnergie[8])  * 0.01f; // 0x3308-09
        state.energieConTotal = ((uint32_t)bufEnergie[15] << 16 | bufEnergie[14]) * 0.01f; // 0x330E-0F
        tDebutRequete = millis();
        mb.readIsts(MODBUS_ADRESSE, 0x200C, bufJourNuit, 1, cbJourNuit); // FC02 discrete input
    } else {
        Serial.printf("Modbus [Energie] : 0x%02X — ignoré\n", ev);
        finaliserLecture();  // non-fatal
    }
    return true;
}

static bool cbStatus(Modbus::ResultCode ev, uint16_t, void*) {
    if (ev != Modbus::EX_SUCCESS) { erreurLecture("Status", ev); return true; }
    // 0x3200 D3-D0 : 00=Normal, 01=Over voltage, 02=Under voltage, 03=Over discharge
    uint8_t batVoltStatus = bufStatus[0] & 0x0F;
    state.batSousVoltage  = (batVoltStatus == 2);
    state.batSurVoltage   = (batVoltStatus == 1);
    // 0x3201 D3-D2 : 00=No charge, 01=Float, 02=Boost, 03=Equalization
    state.batStatut = (bufStatus[1] >> 2) & 0x03;
    tDebutRequete = millis();
    mb.readIreg(MODBUS_ADRESSE, 0x3300, bufEnergie, 16, cbEnergie);
    return true;
}

static bool cbSOC(Modbus::ResultCode ev, uint16_t, void*) {
    if (ev != Modbus::EX_SUCCESS) { erreurLecture("SOC", ev); return true; }
    state.batSOC = (uint8_t)bufSOC[0];
    tDebutRequete = millis();
    mb.readIreg(MODBUS_ADRESSE, 0x3200, bufStatus, 2, cbStatus); // 0x3200 + 0x3201
    return true;
}

static bool cbLoad(Modbus::ResultCode ev, uint16_t, void*) {
    if (ev != Modbus::EX_SUCCESS) { erreurLecture("Load", ev); return true; }
    state.loadVoltage   = bufLoad[0] * 0.01f;  // 0x310C
    state.loadCurrent   = bufLoad[1] * 0.01f;  // 0x310D
    state.loadPower     = bufLoad[2] * 0.01f;  // 0x310E
    // bufLoad[3] = 0x310F réservé
    state.batTemperature = (int16_t)bufLoad[4] * 0.01f;  // 0x3110 signé
    tDebutRequete = millis();
    mb.readIreg(MODBUS_ADRESSE, 0x311A, bufSOC, 1, cbSOC);
    return true;
}

static bool cbPV(Modbus::ResultCode ev, uint16_t, void*) {
    if (ev != Modbus::EX_SUCCESS) { erreurLecture("PV", ev); return true; }
    state.pv        = bufPV[0] * 0.01f;  // 0x3100 Tension PV
    state.pvCurrent = bufPV[1] * 0.01f;  // 0x3101 Courant PV
    state.battery   = bufPV[4] * 0.01f;  // 0x3104 Tension batterie
    tDebutRequete = millis();
    mb.readIreg(MODBUS_ADRESSE, 0x310C, bufLoad, 5, cbLoad);
    return true;
}

void initModbus() {
    Preferences p; p.begin("modbus", true);
    intervalleJour = p.getUInt("jour", INTERVALLE_MODBUS);
    intervalleNuit = p.getUInt("nuit", 30000UL);
    p.end();

    debugLogf("--- Modbus init ---");
    debugLogf("  Adresse esclave : %d", MODBUS_ADRESSE);
    debugLogf("  Baud rate       : %u", (uint32_t)MODBUS_BAUDRATE);
    debugLogf("  TX GPIO         : %d", PIN_RS485_TX);
    debugLogf("  RX GPIO         : %d", PIN_RS485_RX);
    debugLogf("  Timeout requête : %d ms", TIMEOUT_MODBUS);
    debugLogf("  Intervalle jour : %u ms", intervalleJour);
    debugLogf("  Intervalle nuit : %u ms", intervalleNuit);

    debugBootModbus();

    Serial2.end();
    delay(20);
    Serial2.begin(MODBUS_BAUDRATE, SERIAL_8N1, PIN_RS485_RX, PIN_RS485_TX);
    viderRx("démarrage ModbusRTU");
    mb.begin(&Serial2);
    mb.master();
    debugLogf("  → Serial2 + Modbus master démarrés");
    debugLogf("-------------------");
}

void gererModbus() {
    unsigned long maintenant = millis();

    if (!lectureEnCours && (maintenant - tDerniereLecture) >= intervalCourant()) {
        tDerniereLecture = maintenant;
        tDebutRequete    = maintenant;
        lectureEnCours   = true;
        debugLogf("[Modbus] Début lecture brute — uptime=%lus, baud=%u, ID=%d, erreurs=%u, dernière=%s/0x%02X",
                      maintenant / 1000, (uint32_t)MODBUS_BAUDRATE, MODBUS_ADRESSE,
                      nbErreurs, derniereEtape, derniereErreur);
        if (!effectuerLectureBruteEpever()) {
            state.rs485_ok = false;
            lectureEnCours = false;
            debugLogf("[Modbus][brut] Cycle échoué — dernière=%s/0x%02X, erreurs=%u",
                      derniereEtape, derniereErreur, nbErreurs);
        }
    }
}
