/**
 * @file projet_horloge.ino
 * @brief Horloge District Côte d'Ivoire & Statut des Projets PNUD
 * @author Antigravity (Google DeepMind Advanced Agentic Coding)
 * 
 * Ce code est conçu pour un ESP32 connecté à :
 * - Un écran TFT LCD (contrôlé par TFT_eSPI et affiché avec LVGL v8)
 * - 14 LEDs NeoPixel WS2812B représentant les 14 districts de Côte d'Ivoire
 * - Un module RTC DS1302 pour l'heure temps réel
 * - Deux boutons poussoirs (BP1 = GPIO17, BP2 = GPIO4)
 * - Un buzzer actif (GPIO16)
 * 
 * L'ESP32 démarre en mode Point d'Accès (AP) avec un serveur web pour configurer
 * l'heure, 3 alarmes, et l'état des projets PNUD par district. Il intègre ElegantOTA.
 */

#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ElegantOTA.h>
#include <Preferences.h>
#include <Adafruit_NeoPixel.h>
#include <ThreeWire.h>
#include <RtcDS1302.h>
#include <lvgl.h>
#include <TFT_eSPI.h>

// ============================================================================
// CONFIGURATION DU MATÉRIEL & PINS
// ============================================================================

// Connexions DS1302 (RST/CE, DAT/IO, CLK/SCLK)
#define RTC_IO_PIN   26
#define RTC_SCLK_PIN 25
#define RTC_CE_PIN   27

// Connexions WS2812B NeoPixel
#define LED_PIN      13
#define NUM_LEDS     14

// Boutons & Buzzer
#define PIN_BUTTON_1 17  // Bouton vert (Suivant / Arrêt Alarme / Hold: Mode AUTO)
#define PIN_BUTTON_2 4   // Bouton jaune (Afficher / Hold: Mode Aléatoire)
#define PIN_BUZZER   16  // Buzzer actif

// Configuration Wi-Fi AP
const char* AP_SSID = "PNUD_District_Clock";
const char* AP_PASS = "pnud1234";

// Orientation de l'écran
#define SCREEN_ROTATION 3 // 3 = Paysage orienté à 180° (Landscape rotated 180)

// Définitions LVGL
#define LV_TICK_PERIOD_MS 10

// Instanciation des périphériques
TFT_eSPI tft = TFT_eSPI();
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
ThreeWire myWire(RTC_IO_PIN, RTC_SCLK_PIN, RTC_CE_PIN); // IO, SCLK, CE
RtcDS1302<ThreeWire> Rtc(myWire);
AsyncWebServer server(80);
Preferences prefs;

// ============================================================================
// STRUCTURES & CONSTANTES DE DONNÉES
// ============================================================================

// Définition des états des projets
enum ProjectStatus {
  STATUS_INEXISTANT = 0, // Rouge
  STATUS_EN_COURS = 1,   // Bleu
  STATUS_REALISE = 2     // Vert
};

// Informations sur les 14 districts
struct District {
  const char* name;
  int ledIndex;
  const char* fullName;
};

// Mapping exact selon la topologie physique et le câblage de la carte (noms sans accents pour l'affichage LCD)
const District districts[NUM_LEDS] = {
  {"Bas-Sassandra", 0, "Bas-Sassandra"},
  {"Montagnes", 1, "Montagnes"},
  {"Woroba", 2, "Woroba"},
  {"Sassandra-Marahoue", 3, "Sassandra-Marahoue"},
  {"Denguele", 4, "Denguele"},
  {"Savanes", 5, "Savanes"},
  {"Vallee du Bandama", 6, "Vallee du Bandama"},
  {"Zanzan", 7, "Zanzan"},
  {"Lacs", 8, "Lacs"},
  {"Yamoussoukro", 9, "Yamoussoukro"},
  {"Lagunes", 10, "Lagunes"},
  {"Comoe", 11, "Comoe"},
  {"Abidjan", 12, "Abidjan"},
  {"Goh-Djiboua", 13, "Goh-Djiboua"}
};

// Couleurs correspondantes aux statuts
const uint32_t STATUS_COLORS[3] = {
  strip.Color(255, 0, 0),   // Rouge: Inexistant
  strip.Color(0, 0, 255),   // Bleu: En cours
  strip.Color(0, 255, 0)    // Vert: Réalisé
};

// Structure de configuration des alarmes
struct AlarmConfig {
  int hour;
  int minute;
  bool enabled;
};
AlarmConfig alarms[3];

// États du système
enum SystemState {
  STATE_CLOCK,
  STATE_DISTRICT,
  STATE_AUTO,
  STATE_WALKTHROUGH
};
SystemState currentState = STATE_CLOCK;

// Variables de contrôle globales
int activeDistrictIndex = 0;       // District en cours d'affichage
int walkthroughIndex = 0;          // Index pour le parcours aléatoire
int walkthroughOrder[NUM_LEDS];    // Ordre de parcours mélangé
int districtProjectStatus[NUM_LEDS]; // États des projets (chargés depuis Preferences)

unsigned long stateTimer = 0;      // Timer pour le retour à l'horloge
unsigned long stateDuration = 0;   // Durée de l'état actuel en ms
unsigned long lastTickTime = 0;    // Pour le rafraîchissement LVGL
unsigned long lastBuzzerToggle = 0; // Timer pour le beeping du buzzer
bool buzzerOn = false;

// Variables pour les alarmes
bool alarmTriggered = false;       // Si l'alarme sonne actuellement
int ringingAlarmIndex = -1;        // Quelle alarme sonne
int lastTriggeredMinute = -1;      // Pour éviter de re-déclencher dans la même minute

// Variables pour le traitement des boutons
struct Button {
  int pin;
  bool lastState;
  unsigned long pressTime;
  bool isHeld;
};
Button btn1 = {PIN_BUTTON_1, HIGH, 0, false};
Button btn2 = {PIN_BUTTON_2, HIGH, 0, false};

// ============================================================================
// CONTEXTE ET WIDGETS LVGL
// ============================================================================
uint16_t screenWidth;
uint16_t screenHeight;
#if LVGL_VERSION_MAJOR < 9
static lv_disp_draw_buf_t draw_buf;
#endif
static lv_color_t *disp_draw_buf;

// Widgets LVGL
lv_obj_t *clock_screen;
lv_obj_t *clock_label;
lv_obj_t *date_label;
lv_obj_t *ap_info_label;
lv_obj_t *alarm_status_label;

lv_obj_t *card_container;
lv_obj_t *card_title;
lv_obj_t *card_subtitle;
lv_obj_t *card_badge;
lv_obj_t *card_progress_bar;
lv_obj_t *pnud_logo_label;

lv_obj_t *auto_screen;
lv_obj_t *auto_title;
lv_obj_t *auto_stats_label;

lv_obj_t *alarm_alert_box = NULL;

// ============================================================================
// ACCÈS MÉMOIRE NON VOLATILE (PREFERENCES)
// ============================================================================

void loadSettings() {
  prefs.begin("pnud_clock", false);
  
  // Charger les alarmes (Alarm 0, 1, 2)
  for (int i = 0; i < 3; i++) {
    char keyH[8], keyM[8], keyE[8];
    snprintf(keyH, sizeof(keyH), "a%dh", i);
    snprintf(keyM, sizeof(keyM), "a%dm", i);
    snprintf(keyE, sizeof(keyE), "a%de", i);
    
    alarms[i].hour = prefs.getInt(keyH, 8 + i * 4); // Par défaut: 8h, 12h, 16h
    alarms[i].minute = prefs.getInt(keyM, 0);
    alarms[i].enabled = prefs.getBool(keyE, false);
  }
  
  // Charger le statut des projets des districts
  for (int i = 0; i < NUM_LEDS; i++) {
    char keyD[8];
    snprintf(keyD, sizeof(keyD), "d%d", i);
    // Statut par défaut : 0 (Inexistant), 1 (En cours), ou 2 (Réalisé) réparti
    districtProjectStatus[i] = prefs.getInt(keyD, i % 3);
  }
  
  prefs.end();
}

void saveAlarm(int index) {
  prefs.begin("pnud_clock", false);
  char keyH[8], keyM[8], keyE[8];
  snprintf(keyH, sizeof(keyH), "a%dh", index);
  snprintf(keyM, sizeof(keyM), "a%dm", index);
  snprintf(keyE, sizeof(keyE), "a%de", index);
  
  prefs.putInt(keyH, alarms[index].hour);
  prefs.putInt(keyM, alarms[index].minute);
  prefs.putBool(keyE, alarms[index].enabled);
  prefs.end();
}

void saveDistrictStatus(int index, int status) {
  prefs.begin("pnud_clock", false);
  char keyD[8];
  snprintf(keyD, sizeof(keyD), "d%d", index);
  prefs.putInt(keyD, status);
  districtProjectStatus[index] = status;
  prefs.end();
}

// ============================================================================
// INTERFACE WEB HTML / CSS
// ============================================================================

const char INDEX_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>UNDP - Clock & Projects Configuration</title>
  <style>
    :root {
      --bg-color: #0f172a;
      --card-bg: rgba(30, 41, 59, 0.75);
      --border-color: rgba(255, 255, 255, 0.1);
      --text-color: #f8fafc;
      --text-muted: #94a3b8;
      --primary: #f97316;       /* Côte d'Ivoire Orange */
      --primary-hover: #ea580c;
      --success: #22c55e;       /* Côte d'Ivoire Green */
      --warning: #f59e0b;
      --danger: #ef4444;
    }
    * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
    body {
      background: radial-gradient(circle at top right, #1e1b4b, var(--bg-color));
      color: var(--text-color);
      min-height: 100vh;
      padding: 20px;
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    header {
      width: 100%;
      max-width: 900px;
      text-align: center;
      margin-bottom: 30px;
      padding: 25px 20px;
      background: var(--card-bg);
      backdrop-filter: blur(10px);
      border-radius: 16px;
      border: 1px solid var(--border-color);
      box-shadow: 0 4px 30px rgba(0, 0, 0, 0.4);
    }
    header h1 { font-size: 2.1rem; color: #fff; text-shadow: 0 0 10px rgba(249, 115, 22, 0.3); margin-bottom: 5px;}
    header p { font-size: 0.95rem; color: var(--text-muted); }
    main { width: 100%; max-width: 900px; display: flex; flex-direction: column; gap: 20px; }
    
    /* Flag top-border style for premium cards */
    .card-flag-top {
      position: relative;
      overflow: hidden;
    }
    .card-flag-top::before {
      content: "";
      position: absolute;
      top: 0; left: 0; right: 0;
      height: 4px;
      background: linear-gradient(to right, #f97316 33.3%, #ffffff 33.3%, #ffffff 66.6%, #22c55e 66.6%);
    }
    
    .card {
      background: var(--card-bg);
      backdrop-filter: blur(10px);
      border: 1px solid var(--border-color);
      border-radius: 16px;
      padding: 25px;
      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
    }
    .card h2 { font-size: 1.4rem; border-bottom: 1px solid var(--border-color); padding-bottom: 10px; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;}
    .grid-2 { display: grid; grid-template-columns: 1fr; gap: 20px; }
    @media (min-width: 768px) { .grid-2 { grid-template-columns: 1fr 1fr; } }
    .form-group { display: flex; flex-direction: column; gap: 8px; margin-bottom: 15px; }
    label { font-size: 0.9rem; font-weight: 600; color: var(--text-muted); }
    input[type="text"], input[type="time"], input[type="datetime-local"], select {
      background: rgba(15, 23, 42, 0.6);
      border: 1px solid var(--border-color);
      padding: 12px;
      border-radius: 8px;
      color: #fff;
      font-size: 1rem;
      transition: all 0.3s;
    }
    input:focus, select:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(249, 115, 22, 0.2); }
    .btn {
      background: var(--primary);
      color: white;
      border: none;
      padding: 12px 20px;
      border-radius: 8px;
      font-weight: 600;
      cursor: pointer;
      font-size: 1rem;
      transition: all 0.2s;
      display: inline-flex;
      justify-content: center;
      align-items: center;
      gap: 10px;
    }
    .btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
    .btn-secondary { background: rgba(255, 255, 255, 0.1); color: #fff; }
    .btn-secondary:hover { background: rgba(255, 255, 255, 0.2); }
    .alarm-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 15px;
      background: rgba(15, 23, 42, 0.4);
      border-radius: 10px;
      margin-bottom: 10px;
      border: 1px solid var(--border-color);
    }
    .alarm-info { display: flex; align-items: center; gap: 15px; }
    .alarm-time { font-size: 1.6rem; font-weight: 700; }
    .switch {
      position: relative;
      display: inline-block;
      width: 50px;
      height: 26px;
    }
    .switch input { opacity: 0; width: 0; height: 0; }
    .slider {
      position: absolute;
      cursor: pointer;
      top: 0; left: 0; right: 0; bottom: 0;
      background-color: #475569;
      transition: .3s;
      border-radius: 34px;
    }
    .slider:before {
      position: absolute;
      content: "";
      height: 18px; width: 18px;
      left: 4px; bottom: 4px;
      background-color: white;
      transition: .3s;
      border-radius: 50%;
    }
    input:checked + .slider { background-color: var(--success); }
    input:focus + .slider { box-shadow: 0 0 1px var(--success); }
    input:checked + .slider:before { transform: translateX(24px); }
    
    .district-table-container { overflow-x: auto; }
    .district-table { width: 100%; border-collapse: collapse; text-align: left; }
    .district-table th, .district-table td { padding: 12px 15px; border-bottom: 1px solid var(--border-color); }
    .district-table th { color: var(--text-muted); font-weight: 600; font-size: 0.9rem; }
    .status-badge {
      display: inline-block;
      padding: 6px 12px;
      border-radius: 20px;
      font-size: 0.8rem;
      font-weight: 600;
      text-transform: uppercase;
    }
    .badge-red { background: rgba(239, 68, 68, 0.2); color: var(--danger); border: 1px solid var(--danger); }
    .badge-blue { background: rgba(59, 130, 246, 0.2); color: #60a5fa; border: 1px solid #3b82f6; }
    .badge-green { background: rgba(16, 185, 129, 0.2); color: var(--success); border: 1px solid var(--success); }
    
    .toast {
      position: fixed;
      bottom: 20px; right: 20px;
      background: var(--success);
      color: white;
      padding: 15px 25px;
      border-radius: 8px;
      font-weight: 600;
      box-shadow: 0 5px 15px rgba(0,0,0,0.3);
      transform: translateY(100px);
      opacity: 0;
      transition: all 0.3s;
      z-index: 1000;
    }
    .toast.show { transform: translateY(0); opacity: 1; }
    .ota-link {
      display: block;
      text-align: center;
      margin-top: 30px;
      color: var(--text-muted);
      text-decoration: none;
      font-size: 0.9rem;
      transition: color 0.2s;
    }
    .ota-link:hover { color: #fff; text-decoration: underline; }
  </style>
</head>
<body>

  <!-- Header with flag and English UNDP title -->
  <header class="card-flag-top">
    <div style="display: flex; width: 60px; height: 36px; border-radius: 4px; overflow: hidden; margin: 0 auto 15px auto; box-shadow: 0 4px 10px rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1);">
      <div style="flex: 1; background-color: #f97316;"></div>
      <div style="flex: 1; background-color: #ffffff;"></div>
      <div style="flex: 1; background-color: #22c55e;"></div>
    </div>
    <h1>UNDP Côte d'Ivoire</h1>
    <p>Integrated RTC Clock & District Project Dashboard</p>
  </header>

  <main>
    <!-- Section Clock -->
    <section class="card card-flag-top">
      <h2>⏰ Clock Settings</h2>
      <div class="grid-2">
        <div class="form-group">
          <label for="datetime">Date and Time</label>
          <input type="datetime-local" id="datetime">
        </div>
        <div class="form-group" style="justify-content: flex-end; flex-direction: row; gap: 10px; align-items: flex-end;">
          <button class="btn btn-secondary" onclick="syncWithDevice()">📍 Sync with PC</button>
          <button class="btn" onclick="saveTime()">💾 Save</button>
        </div>
      </div>
    </section>

    <!-- Section Alarms -->
    <section class="card card-flag-top">
      <h2>🔔 Alarm Configuration</h2>
      <div>
        <!-- Alarm 1 -->
        <div class="alarm-row">
          <div class="alarm-info">
            <span class="alarm-time" id="a0-disp">07:00</span>
            <div>
              <div style="font-weight:600;">Alarm 1</div>
              <input type="time" id="a0-time" value="07:00" style="padding:4px; font-size:0.9rem; margin-top:5px;" onchange="updateAlarmDisp(0)">
            </div>
          </div>
          <label class="switch">
            <input type="checkbox" id="a0-enable" onchange="toggleAlarm(0)">
            <span class="slider"></span>
          </label>
        </div>
        <!-- Alarm 2 -->
        <div class="alarm-row">
          <div class="alarm-info">
            <span class="alarm-time" id="a1-disp">12:00</span>
            <div>
              <div style="font-weight:600;">Alarm 2</div>
              <input type="time" id="a1-time" value="12:00" style="padding:4px; font-size:0.9rem; margin-top:5px;" onchange="updateAlarmDisp(1)">
            </div>
          </div>
          <label class="switch">
            <input type="checkbox" id="a1-enable" onchange="toggleAlarm(1)">
            <span class="slider"></span>
          </label>
        </div>
        <!-- Alarm 3 -->
        <div class="alarm-row">
          <div class="alarm-info">
            <span class="alarm-time" id="a2-disp">20:00</span>
            <div>
              <div style="font-weight:600;">Alarm 3</div>
              <input type="time" id="a2-time" value="20:00" style="padding:4px; font-size:0.9rem; margin-top:5px;" onchange="updateAlarmDisp(2)">
            </div>
          </div>
          <label class="switch">
            <input type="checkbox" id="a2-enable" onchange="toggleAlarm(2)">
            <span class="slider"></span>
          </label>
        </div>
      </div>
    </section>

    <!-- Section Districts -->
    <section class="card card-flag-top">
      <h2>🇨🇮 District Projects</h2>
      <div class="district-table-container">
        <table class="district-table">
          <thead>
            <tr>
              <th>District</th>
              <th>Current Status</th>
              <th>Action / Change</th>
            </tr>
          </thead>
          <tbody id="districts-tbody">
            <!-- Dynamically populated in JS -->
          </tbody>
        </table>
      </div>
    </section>
  </main>

  <a href="/update" class="ota-link">⚙️ Open ElegantOTA for software update</a>

  <div id="toast" class="toast">Configuration saved!</div>

  <script>
    const districtNames = [
      "Bas-Sassandra", "Montagnes", "Woroba", "Sassandra-Marahoue", "Denguele",
      "Savanes", "Vallee du Bandama", "Zanzan", "Lacs", "Yamoussoukro",
      "Lagunes", "Comoe", "Abidjan", "Goh-Djiboua"
    ];

    window.onload = function() {
      const now = new Date();
      now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
      document.getElementById('datetime').value = now.toISOString().slice(0, 16);
      fetchData();
    };

    function showToast(message, isError = false) {
      const toast = document.getElementById('toast');
      toast.innerText = message;
      toast.style.background = isError ? 'var(--danger)' : 'var(--success)';
      toast.classList.add('show');
      setTimeout(() => toast.classList.remove('show'), 3000);
    }

    function fetchData() {
      fetch('/api/status')
        .then(res => res.json())
        .then(data => {
          data.alarms.forEach((alarm, i) => {
            const timeStr = String(alarm.hour).padStart(2, '0') + ':' + String(alarm.minute).padStart(2, '0');
            document.getElementById(`a${i}-time`).value = timeStr;
            document.getElementById(`a${i}-disp`).innerText = timeStr;
            document.getElementById(`a${i}-enable`).checked = alarm.enabled;
          });

          const tbody = document.getElementById('districts-tbody');
          tbody.innerHTML = '';
          data.districts.forEach((status, i) => {
            const tr = document.createElement('tr');
            
            let badgeClass = 'badge-red';
            let statusText = 'Non-existent';
            if (status === 1) { badgeClass = 'badge-blue'; statusText = 'In Progress'; }
            else if (status === 2) { badgeClass = 'badge-green'; statusText = 'Completed'; }

            tr.innerHTML = `
              <td><strong>${districtNames[i]}</strong></td>
              <td><span class="status-badge ${badgeClass}">${statusText}</span></td>
              <td>
                <select onchange="updateDistrict(${i}, this.value)" style="padding: 6px 12px; font-size: 0.9rem;">
                  <option value="0" ${status === 0 ? 'selected' : ''}>Non-existent (Red)</option>
                  <option value="1" ${status === 1 ? 'selected' : ''}>In Progress (Blue)</option>
                  <option value="2" ${status === 2 ? 'selected' : ''}>Completed (Green)</option>
                </select>
              </td>
            `;
            tbody.appendChild(tr);
          });
        })
        .catch(err => console.error("Error loading status data", err));
    }

    function syncWithDevice() {
      const now = new Date();
      now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
      document.getElementById('datetime').value = now.toISOString().slice(0, 16);
      showToast("Time synchronized with your device.");
    }

    function saveTime() {
      const dt = document.getElementById('datetime').value;
      if (!dt) return;
      const d = new Date(dt);
      
      const url = `/api/set_time?y=${d.getFullYear()}&m=${d.getMonth()+1}&d=${d.getDate()}&h=${d.getHours()}&min=${d.getMinutes()}&s=${d.getSeconds()}`;
      fetch(url)
        .then(res => res.json())
        .then(data => {
          if (data.success) showToast("RTC Date and Time updated!");
          else showToast("Error saving time settings.", true);
        });
    }

    function updateAlarmDisp(index) {
      const timeVal = document.getElementById(`a${index}-time`).value;
      document.getElementById(`a${index}-disp`).innerText = timeVal;
      saveAlarmConfig(index);
    }

    function toggleAlarm(index) {
      saveAlarmConfig(index);
    }

    function saveAlarmConfig(index) {
      const timeVal = document.getElementById(`a${index}-time`).value;
      const enabled = document.getElementById(`a${index}-enable`).checked ? 1 : 0;
      const parts = timeVal.split(':');
      const hour = parts[0];
      const minute = parts[1];

      const url = `/api/set_alarm?id=${index}&h=${hour}&m=${minute}&e=${enabled}`;
      fetch(url)
        .then(res => res.json())
        .then(data => {
          if (data.success) showToast(`Alarm ${index + 1} config saved.`);
          else showToast("Error saving alarm config.", true);
        });
    }

    function updateDistrict(index, status) {
      const url = `/api/set_district?id=${index}&s=${status}`;
      fetch(url)
        .then(res => res.json())
        .then(data => {
          if (data.success) {
            showToast("District status updated!");
            fetchData();
          } else {
            showToast("Error updating district status.", true);
          }
        });
    }
  </script>
</body>
</html>
)rawliteral";

// ============================================================================
// SERVEUR WEB ET APPELS D'API
// ============================================================================

void handleRoot(AsyncWebServerRequest *request) {
  request->send_P(200, "text/html", INDEX_HTML);
}

void handleStatus(AsyncWebServerRequest *request) {
  String json = "{";
  
  // Renvoyer les alarmes
  json += "\"alarms\":[";
  for (int i = 0; i < 3; i++) {
    json += "{";
    json += "\"hour\":" + String(alarms[i].hour) + ",";
    json += "\"minute\":" + String(alarms[i].minute) + ",";
    json += "\"enabled\":" + String(alarms[i].enabled ? "true" : "false");
    json += "}";
    if (i < 2) json += ",";
  }
  json += "],";

  // Renvoyer les districts
  json += "\"districts\":[";
  for (int i = 0; i < NUM_LEDS; i++) {
    json += String(districtProjectStatus[i]);
    if (i < NUM_LEDS - 1) json += ",";
  }
  json += "]";
  
  json += "}";
  request->send(200, "application/json", json);
}

void handleSetTime(AsyncWebServerRequest *request) {
  if (request->hasParam("y") && request->hasParam("m") && request->hasParam("d") && 
      request->hasParam("h") && request->hasParam("min") && request->hasParam("s")) {
    
    int year = request->getParam("y")->value().toInt();
    int month = request->getParam("m")->value().toInt();
    int day = request->getParam("d")->value().toInt();
    int hour = request->getParam("h")->value().toInt();
    int minute = request->getParam("min")->value().toInt();
    int second = request->getParam("s")->value().toInt();
    
    RtcDateTime newTime(year, month, day, hour, minute, second);
    Rtc.SetDateTime(newTime);
    
    request->send(200, "application/json", "{\"success\":true}");
  } else {
    request->send(400, "application/json", "{\"success\":false,\"error\":\"Parametres manquants\"}");
  }
}

void handleSetAlarm(AsyncWebServerRequest *request) {
  if (request->hasParam("id") && request->hasParam("h") && request->hasParam("m") && request->hasParam("e")) {
    int id = request->getParam("id")->value().toInt();
    if (id >= 0 && id < 3) {
      alarms[id].hour = request->getParam("h")->value().toInt();
      alarms[id].minute = request->getParam("m")->value().toInt();
      alarms[id].enabled = (request->getParam("e")->value().toInt() == 1);
      
      saveAlarm(id);
      request->send(200, "application/json", "{\"success\":true}");
    } else {
      request->send(400, "application/json", "{\"success\":false,\"error\":\"ID invalide\"}");
    }
  } else {
    request->send(400, "application/json", "{\"success\":false,\"error\":\"Parametres manquants\"}");
  }
}

void handleSetDistrict(AsyncWebServerRequest *request) {
  if (request->hasParam("id") && request->hasParam("s")) {
    int id = request->getParam("id")->value().toInt();
    int status = request->getParam("s")->value().toInt();
    if (id >= 0 && id < NUM_LEDS && status >= 0 && status <= 2) {
      saveDistrictStatus(id, status);
      request->send(200, "application/json", "{\"success\":true}");
    } else {
      request->send(400, "application/json", "{\"success\":false,\"error\":\"ID ou statut invalide\"}");
    }
  } else {
    request->send(400, "application/json", "{\"success\":false,\"error\":\"Parametres manquants\"}");
  }
}

// ============================================================================
// CALLBACKS ET PILOTAGE DE L'ÉCRAN (TFT_eSPI & LVGL)
// ============================================================================

/* Flush display sur l'écran via TFT_eSPI */
#if LVGL_VERSION_MAJOR >= 9
void my_disp_flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
  uint32_t w = (area->x2 - area->x1 + 1);
  uint32_t h = (area->y2 - area->y1 + 1);

  tft.startWrite();
  tft.setAddrWindow(area->x1, area->y1, w, h);
  tft.pushColors((uint16_t *)px_map, w * h, true);
  tft.endWrite();

  lv_display_flush_ready(disp);
}
#else
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) {
  uint32_t w = (area->x2 - area->x1 + 1);
  uint32_t h = (area->y2 - area->y1 + 1);

  tft.startWrite();
  tft.setAddrWindow(area->x1, area->y1, w, h);
  tft.pushColors((uint16_t *)&color_p->full, w * h, true);
  tft.endWrite();

  lv_disp_flush_ready(disp);
}
#endif

// Bouton OK sur l'alerte Alarme cliqué
static void alarm_ok_event_handler(lv_event_t * e) {
  lv_event_code_t code = lv_event_get_code(e);
  if(code == LV_EVENT_CLICKED) {
    alarmTriggered = false;
    digitalWrite(PIN_BUZZER, LOW);
    if(alarm_alert_box) {
      lv_obj_del(alarm_alert_box);
      alarm_alert_box = NULL;
    }
  }
}

// Créer la boîte d'alerte lors du déclenchement de l'alarme
void createAlarmAlertBox(int index) {
  if (alarm_alert_box != NULL) return; // Déjà ouverte

  alarm_alert_box = lv_obj_create(lv_scr_act());
  lv_obj_set_size(alarm_alert_box, screenWidth - 40, screenHeight - 60);
  lv_obj_align(alarm_alert_box, LV_ALIGN_CENTER, 0, 0);
  lv_obj_set_style_bg_color(alarm_alert_box, lv_color_hex(0x7f1d1d), 0); // Rouge sombre
  lv_obj_set_style_border_color(alarm_alert_box, lv_color_hex(0xef4444), 0);
  lv_obj_set_style_border_width(alarm_alert_box, 3, 0);
  lv_obj_set_style_radius(alarm_alert_box, 15, 0);

  lv_obj_t * title = lv_label_create(alarm_alert_box);
  lv_label_set_text_fmt(title, "🚨 ALARM %d 🚨", index + 1);
  lv_obj_set_style_text_color(title, lv_color_hex(0xffffff), 0);
  lv_obj_set_style_text_font(title, &lv_font_montserrat_16, 0);
  lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);

  lv_obj_t * desc = lv_label_create(alarm_alert_box);
  lv_label_set_text(desc, "Time is up!\nPress BP1 or OK\nto stop.");
  lv_obj_set_style_text_color(desc, lv_color_hex(0xfca5a5), 0);
  lv_obj_set_style_text_align(desc, LV_TEXT_ALIGN_CENTER, 0);
  lv_obj_set_style_text_font(desc, &lv_font_montserrat_14, 0);
  lv_obj_align(desc, LV_ALIGN_CENTER, 0, -10);

  lv_obj_t * btn = lv_btn_create(alarm_alert_box);
  lv_obj_align(btn, LV_ALIGN_BOTTOM_MID, 0, -10);
  lv_obj_set_size(btn, 100, 40);
  lv_obj_set_style_bg_color(btn, lv_color_hex(0x10b981), 0); // Vert
  lv_obj_add_event_cb(btn, alarm_ok_event_handler, LV_EVENT_ALL, NULL);

  lv_obj_t * btn_lbl = lv_label_create(btn);
  lv_label_set_text(btn_lbl, "OK");
  lv_obj_set_style_text_color(btn_lbl, lv_color_hex(0xffffff), 0);
  lv_obj_align(btn_lbl, LV_ALIGN_CENTER, 0, 0);
}

// Initialisation de la charte graphique LVGL (Styles)
void initUI() {
  // 1. Écran Principal Horloge
  clock_screen = lv_obj_create(NULL);
  lv_obj_set_style_bg_color(clock_screen, lv_color_hex(0x0f172a), 0); // Deep slate blue/black
  
  // Bordure décorative Orange/Blanc/Vert (Côte d'Ivoire) en haut
  lv_obj_t *flag_orange = lv_obj_create(clock_screen);
  lv_obj_set_size(flag_orange, screenWidth / 3, 4);
  lv_obj_align(flag_orange, LV_ALIGN_TOP_LEFT, 0, 0);
  lv_obj_set_style_bg_color(flag_orange, lv_color_hex(0xf97316), 0); // Orange
  lv_obj_set_style_border_width(flag_orange, 0, 0);

  lv_obj_t *flag_white = lv_obj_create(clock_screen);
  lv_obj_set_size(flag_white, screenWidth / 3, 4);
  lv_obj_align(flag_white, LV_ALIGN_TOP_MID, 0, 0);
  lv_obj_set_style_bg_color(flag_white, lv_color_hex(0xffffff), 0); // Blanc
  lv_obj_set_style_border_width(flag_white, 0, 0);

  lv_obj_t *flag_green = lv_obj_create(clock_screen);
  lv_obj_set_size(flag_green, screenWidth / 3, 4);
  lv_obj_align(flag_green, LV_ALIGN_TOP_RIGHT, 0, 0);
  lv_obj_set_style_bg_color(flag_green, lv_color_hex(0x22c55e), 0); // Vert
  lv_obj_set_style_border_width(flag_green, 0, 0);

  // Label Titre PNUD
  lv_obj_t * title_label = lv_label_create(clock_screen);
  lv_label_set_text(title_label, "UNDP COTE D'IVOIRE");
  lv_obj_set_style_text_color(title_label, lv_color_hex(0x3b82f6), 0); // Bleu PNUD
  lv_obj_set_style_text_font(title_label, &lv_font_montserrat_14, 0);
  lv_obj_align(title_label, LV_ALIGN_TOP_MID, 0, 15);

  // Label Horloge
  clock_label = lv_label_create(clock_screen);
  lv_label_set_text(clock_label, "00:00:00");
  lv_obj_set_style_text_color(clock_label, lv_color_hex(0xffffff), 0);
  lv_obj_set_style_text_font(clock_label, &lv_font_montserrat_40, 0); // Grand format
  lv_obj_align(clock_label, LV_ALIGN_CENTER, 0, -20);

  // Label Date
  date_label = lv_label_create(clock_screen);
  lv_label_set_text(date_label, "Chargement date...");
  lv_obj_set_style_text_color(date_label, lv_color_hex(0x94a3b8), 0);
  lv_obj_set_style_text_font(date_label, &lv_font_montserrat_14, 0);
  lv_obj_align(date_label, LV_ALIGN_CENTER, 0, 15);

  // Info Point d'accès
  ap_info_label = lv_label_create(clock_screen);
  lv_label_set_text_fmt(ap_info_label, "AP: %s | http://192.168.4.1", AP_SSID);
  lv_obj_set_style_text_color(ap_info_label, lv_color_hex(0x475569), 0);
  lv_obj_set_style_text_font(ap_info_label, &lv_font_montserrat_12, 0);
  lv_obj_align(ap_info_label, LV_ALIGN_BOTTOM_MID, 0, -30);

  // Status des 3 alarmes (format court pour tenir en largeur 320px)
  alarm_status_label = lv_label_create(clock_screen);
  lv_label_set_text(alarm_status_label, "A1: OFF  A2: OFF  A3: OFF");
  lv_obj_set_style_text_color(alarm_status_label, lv_color_hex(0xf59e0b), 0);
  lv_obj_set_style_text_font(alarm_status_label, &lv_font_montserrat_12, 0);
  lv_obj_align(alarm_status_label, LV_ALIGN_BOTTOM_MID, 0, -10);

  // 2. Conteneur/Carte pour l'affichage d'un district
  card_container = lv_obj_create(clock_screen);
  lv_obj_set_size(card_container, screenWidth - 40, screenHeight - 60);
  lv_obj_align(card_container, LV_ALIGN_CENTER, 0, 0);
  lv_obj_set_style_bg_color(card_container, lv_color_hex(0x1e293b), 0);
  lv_obj_set_style_border_color(card_container, lv_color_hex(0x3b82f6), 0);
  lv_obj_set_style_border_width(card_container, 2, 0);
  lv_obj_set_style_radius(card_container, 16, 0);
  lv_obj_add_flag(card_container, LV_OBJ_FLAG_HIDDEN); // Caché par défaut

  pnud_logo_label = lv_label_create(card_container);
  lv_label_set_text(pnud_logo_label, "UNDP DISTRICT PROJECT");
  lv_obj_set_style_text_color(pnud_logo_label, lv_color_hex(0x3b82f6), 0);
  lv_obj_set_style_text_font(pnud_logo_label, &lv_font_montserrat_12, 0);
  lv_obj_align(pnud_logo_label, LV_ALIGN_TOP_LEFT, 10, 10);

  card_title = lv_label_create(card_container);
  lv_label_set_text(card_title, "Nom du District");
  lv_obj_set_style_text_color(card_title, lv_color_hex(0xffffff), 0);
  lv_obj_set_style_text_font(card_title, &lv_font_montserrat_16, 0);
  lv_obj_align(card_title, LV_ALIGN_TOP_MID, 0, 30);

  card_subtitle = lv_label_create(card_container);
  lv_label_set_text(card_subtitle, "Project Completed / In Progress / Non-existent");
  lv_obj_set_style_text_color(card_subtitle, lv_color_hex(0xe2e8f0), 0);
  lv_obj_set_style_text_font(card_subtitle, &lv_font_montserrat_14, 0);
  lv_obj_align(card_subtitle, LV_ALIGN_CENTER, 0, 10);

  // Badge indicateur de couleur de l'état
  card_badge = lv_obj_create(card_container);
  lv_obj_set_size(card_badge, 40, 15);
  lv_obj_align(card_badge, LV_ALIGN_CENTER, 0, 40);
  lv_obj_set_style_radius(card_badge, 8, 0);
  lv_obj_set_style_border_width(card_badge, 0, 0);

  // Progress Bar pour le timeout de 5 secondes
  card_progress_bar = lv_bar_create(card_container);
  lv_obj_set_size(card_progress_bar, screenWidth - 80, 8);
  lv_obj_align(card_progress_bar, LV_ALIGN_BOTTOM_MID, 0, -10);
  lv_bar_set_range(card_progress_bar, 0, 100);
  lv_bar_set_value(card_progress_bar, 100, LV_ANIM_OFF);
  lv_obj_set_style_bg_color(card_progress_bar, lv_color_hex(0x475569), LV_PART_MAIN);
  lv_obj_set_style_bg_color(card_progress_bar, lv_color_hex(0x3b82f6), LV_PART_INDICATOR);

  // 3. Écran Mode Auto
  auto_screen = lv_obj_create(clock_screen);
  lv_obj_set_size(auto_screen, screenWidth - 40, screenHeight - 60);
  lv_obj_align(auto_screen, LV_ALIGN_CENTER, 0, 0);
  lv_obj_set_style_bg_color(auto_screen, lv_color_hex(0x0f172a), 0);
  lv_obj_set_style_border_color(auto_screen, lv_color_hex(0x10b981), 0); // Vert indicateur
  lv_obj_set_style_border_width(auto_screen, 2, 0);
  lv_obj_set_style_radius(auto_screen, 16, 0);
  lv_obj_add_flag(auto_screen, LV_OBJ_FLAG_HIDDEN); // Caché par défaut

  auto_title = lv_label_create(auto_screen);
  lv_label_set_text(auto_title, "AUTO REVIEW MODE");
  lv_obj_set_style_text_color(auto_title, lv_color_hex(0x10b981), 0);
  lv_obj_set_style_text_font(auto_title, &lv_font_montserrat_16, 0);
  lv_obj_align(auto_title, LV_ALIGN_TOP_MID, 0, 15);

  auto_stats_label = lv_label_create(auto_screen);
  lv_label_set_text(auto_stats_label, "Initialisation...");
  lv_obj_set_style_text_color(auto_stats_label, lv_color_hex(0xffffff), 0);
  lv_obj_set_style_text_font(auto_stats_label, &lv_font_montserrat_12, 0);
  lv_obj_set_style_text_align(auto_stats_label, LV_TEXT_ALIGN_CENTER, 0);
  lv_obj_align(auto_stats_label, LV_ALIGN_CENTER, 0, 10);

  lv_scr_load(clock_screen);
}

// Met à jour les éléments d'affichage de l'horloge
void updateClockView(const RtcDateTime& dt) {
  char time_str[10];
  snprintf(time_str, sizeof(time_str), "%02d:%02d:%02d", dt.Hour(), dt.Minute(), dt.Second());
  lv_label_set_text(clock_label, time_str);

  const char* jours[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
  const char* mois[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
  
  // Calcul du jour de la semaine
  int dayOfWeek = dt.DayOfWeek();
  if(dayOfWeek < 0 || dayOfWeek > 6) dayOfWeek = 0;

  char date_str[40];
  snprintf(date_str, sizeof(date_str), "%s %d %s %d", jours[dayOfWeek], dt.Day(), mois[dt.Month() - 1], dt.Year());
  lv_label_set_text(date_label, date_str);

  // Mettre à jour l'état des alarmes affiché (format compact)
  char alarm_str[80];
  snprintf(alarm_str, sizeof(alarm_str), "A1: %02d:%02d (%s)  A2: %02d:%02d (%s)  A3: %02d:%02d (%s)",
           alarms[0].hour, alarms[0].minute, alarms[0].enabled ? "ON" : "OFF",
           alarms[1].hour, alarms[1].minute, alarms[1].enabled ? "ON" : "OFF",
           alarms[2].hour, alarms[2].minute, alarms[2].enabled ? "ON" : "OFF");
  lv_label_set_text(alarm_status_label, alarm_str);
}

// Change le contenu de la carte district affichée à l'écran
void updateDistrictCard(int index, int timeLeftMs) {
  if (index < 0 || index >= NUM_LEDS) return;
  
  lv_label_set_text(card_title, districts[index].name); // Utilise le nom court pour éviter la troncature
  
  int status = districtProjectStatus[index];
  lv_color_t color;
  const char* statusStr = "";
  
  if (status == STATUS_REALISE) {
    statusStr = "UNDP Project Completed";
    color = lv_color_hex(0x22c55e); // Vert
  } else if (status == STATUS_EN_COURS) {
    statusStr = "UNDP Project In Progress";
    color = lv_color_hex(0x3b82f6); // Bleu
  } else {
    statusStr = "No UNDP Project";
    color = lv_color_hex(0xef4444); // Rouge
  }
  
  lv_label_set_text(card_subtitle, statusStr);
  lv_obj_set_style_bg_color(card_badge, color, 0);
  
  // Barre de progression
  int progress = (timeLeftMs * 100) / 5000;
  if(progress < 0) progress = 0;
  if(progress > 100) progress = 100;
  lv_bar_set_value(card_progress_bar, progress, LV_ANIM_OFF);
}

// Met à jour l'écran des statistiques du mode auto
void updateAutoScreenStats() {
  int countInexistant = 0;
  int countEnCours = 0;
  int countRealise = 0;
  
  for(int i = 0; i < NUM_LEDS; i++) {
    if(districtProjectStatus[i] == STATUS_INEXISTANT) countInexistant++;
    else if(districtProjectStatus[i] == STATUS_EN_COURS) countEnCours++;
    else if(districtProjectStatus[i] == STATUS_REALISE) countRealise++;
  }
  
  char stats_str[150];
  snprintf(stats_str, sizeof(stats_str), 
           "Reviewing all districts...\n\n"
           "Completed Projects: %d\n"
           "In Progress Projects: %d\n"
           "Non-existent Projects: %d\n\n"
           "Total review duration: 6 seconds", 
           countRealise, countEnCours, countInexistant);
  lv_label_set_text(auto_stats_label, stats_str);
}

// ============================================================================
// GESTION DES TRANSITIONS D'ÉTATS & PIXELS LED
// ============================================================================

void changeState(SystemState newState) {
  currentState = newState;
  stateTimer = millis();
  
  // Cacher par défaut toutes les couches spéciales
  lv_obj_add_flag(card_container, LV_OBJ_FLAG_HIDDEN);
  lv_obj_add_flag(auto_screen, LV_OBJ_FLAG_HIDDEN);
  
  // Éteindre les leds
  strip.clear();
  strip.show();
  
  switch(currentState) {
    case STATE_CLOCK:
      // Rien à faire, l'horloge tourne par défaut en tâche de fond.
      break;
      
    case STATE_DISTRICT:
      lv_obj_clear_flag(card_container, LV_OBJ_FLAG_HIDDEN);
      // Allumer la LED spécifique avec sa couleur
      strip.setPixelColor(districts[activeDistrictIndex].ledIndex, STATUS_COLORS[districtProjectStatus[activeDistrictIndex]]);
      strip.show();
      stateDuration = 5000; // 5 secondes
      break;
      
    case STATE_AUTO:
      lv_obj_clear_flag(auto_screen, LV_OBJ_FLAG_HIDDEN);
      updateAutoScreenStats();
      // Allumer toutes les LEDs avec leur couleur respective
      for (int i = 0; i < NUM_LEDS; i++) {
        strip.setPixelColor(districts[i].ledIndex, STATUS_COLORS[districtProjectStatus[i]]);
      }
      strip.show();
      stateDuration = 6000; // 6 secondes
      break;
      
    case STATE_WALKTHROUGH:
      lv_obj_clear_flag(card_container, LV_OBJ_FLAG_HIDDEN);
      // Mélanger l'ordre des districts
      for (int i = 0; i < NUM_LEDS; i++) {
        walkthroughOrder[i] = i;
      }
      for (int i = NUM_LEDS - 1; i > 0; i--) {
        int r = random(0, i + 1);
        int temp = walkthroughOrder[i];
        walkthroughOrder[i] = walkthroughOrder[r];
        walkthroughOrder[r] = temp;
      }
      walkthroughIndex = 0;
      activeDistrictIndex = walkthroughOrder[0];
      
      // Activer l'affichage du premier district du parcours
      strip.setPixelColor(districts[activeDistrictIndex].ledIndex, STATUS_COLORS[districtProjectStatus[activeDistrictIndex]]);
      strip.show();
      stateTimer = millis(); // restart timer
      stateDuration = 5000;
      break;
  }
}

// Bip bref du buzzer pour feedback utilisateur
void triggerBeep(int durationMs = 80) {
  digitalWrite(PIN_BUZZER, HIGH);
  delay(durationMs);
  digitalWrite(PIN_BUZZER, LOW);
}

// ============================================================================
// DÉTECTION ET TRAITEMENT DES BOUTONS
// ============================================================================

void processButtons() {
  unsigned long now = millis();
  
  // --- BOUTON 1 (BP1 - VERT) ---
  bool b1State = digitalRead(btn1.pin);
  if (b1State != btn1.lastState) {
    delay(20); // Debounce simple
    if (digitalRead(btn1.pin) == b1State) {
      btn1.lastState = b1State;
      if (b1State == LOW) { // Pressé
        btn1.pressTime = now;
        btn1.isHeld = false;
      } else { // Relâché
        if (!btn1.isHeld) {
          // CLIC BOUTON 1
          if (alarmTriggered) {
            // Arrêter l'alarme
            alarmTriggered = false;
            digitalWrite(PIN_BUZZER, LOW);
            if(alarm_alert_box) { lv_obj_del(alarm_alert_box); alarm_alert_box = NULL; }
            triggerBeep(100);
          } else {
            // Passer au district suivant
            if (currentState == STATE_CLOCK) {
              activeDistrictIndex = 0;
              changeState(STATE_DISTRICT);
            } else if (currentState == STATE_DISTRICT) {
              activeDistrictIndex = (activeDistrictIndex + 1) % NUM_LEDS;
              changeState(STATE_DISTRICT);
            } else if (currentState == STATE_WALKTHROUGH) {
              // Aller directement au district suivant dans le parcours
              walkthroughIndex++;
              if (walkthroughIndex >= NUM_LEDS) {
                changeState(STATE_CLOCK);
              } else {
                activeDistrictIndex = walkthroughOrder[walkthroughIndex];
                changeState(STATE_WALKTHROUGH); // Relance le timer
              }
            } else if (currentState == STATE_AUTO) {
              // Quitter le mode auto
              changeState(STATE_CLOCK);
            }
            triggerBeep(50);
          }
        }
      }
    }
  }
  
  // Détection appui long BP1 (6 secondes)
  if (b1State == LOW && !btn1.isHeld && (now - btn1.pressTime >= 6000)) {
    btn1.isHeld = true;
    // Lancer le mode AUTO
    if (!alarmTriggered) {
      changeState(STATE_AUTO);
      triggerBeep(300);
    }
  }

  // --- BOUTON 2 (BP2 - JAUNE) ---
  bool b2State = digitalRead(btn2.pin);
  if (b2State != btn2.lastState) {
    delay(20);
    if (digitalRead(btn2.pin) == b2State) {
      btn2.lastState = b2State;
      if (b2State == LOW) {
        btn2.pressTime = now;
        btn2.isHeld = false;
      } else {
        if (!btn2.isHeld) {
          // CLIC BOUTON 2
          if (!alarmTriggered) {
            if (currentState == STATE_CLOCK) {
              // Affiche le district sélectionné actuel
              changeState(STATE_DISTRICT);
            } else if (currentState == STATE_DISTRICT) {
              // Réinitialise le timer de 5s pour le district actuel
              changeState(STATE_DISTRICT);
            } else if (currentState == STATE_WALKTHROUGH || currentState == STATE_AUTO) {
              // Quitter et retourner à l'horloge
              changeState(STATE_CLOCK);
            }
            triggerBeep(50);
          }
        }
      }
    }
  }

  // Détection appui long BP2 (6 secondes)
  if (b2State == LOW && !btn2.isHeld && (now - btn2.pressTime >= 6000)) {
    btn2.isHeld = true;
    // Lancer le mode ALÉATOIRE (WALKTHROUGH)
    if (!alarmTriggered) {
      changeState(STATE_WALKTHROUGH);
      triggerBeep(300);
    }
  }
}

// ============================================================================
// CONTRÔLE DES ALARMES DE FOND
// ============================================================================

void checkAlarms(const RtcDateTime& dt) {
  if (alarmTriggered) {
    // Si l'alarme sonne, on fait clignoter le buzzer à 1Hz
    unsigned long curMs = millis();
    if (curMs - lastBuzzerToggle >= 500) {
      lastBuzzerToggle = curMs;
      buzzerOn = !buzzerOn;
      digitalWrite(PIN_BUZZER, buzzerOn ? HIGH : LOW);
    }
    
    // Sécurité: éteindre automatiquement après 60 secondes pour ménager le matériel
    if (dt.Second() == 0 && dt.Minute() != alarms[ringingAlarmIndex].minute) {
      alarmTriggered = false;
      digitalWrite(PIN_BUZZER, LOW);
      if(alarm_alert_box) { lv_obj_del(alarm_alert_box); alarm_alert_box = NULL; }
    }
    return;
  }

  // Vérifier chaque minute si une alarme configurée se déclenche
  if (dt.Second() == 0 && dt.Minute() != lastTriggeredMinute) {
    for (int i = 0; i < 3; i++) {
      if (alarms[i].enabled && alarms[i].hour == dt.Hour() && alarms[i].minute == dt.Minute()) {
        alarmTriggered = true;
        ringingAlarmIndex = i;
        lastTriggeredMinute = dt.Minute();
        lastBuzzerToggle = millis();
        buzzerOn = true;
        digitalWrite(PIN_BUZZER, HIGH);
        
        // Afficher l'alerte sur l'écran LCD
        createAlarmAlertBox(i);
        break;
      }
    }
  }
}

// ============================================================================
// SETUP & LOOP
// ============================================================================

void setup() {
  Serial.begin(115200);
  
  // Initialiser les E/S
  pinMode(PIN_BUTTON_1, INPUT_PULLUP);
  pinMode(PIN_BUTTON_2, INPUT_PULLUP);
  pinMode(PIN_BUZZER, OUTPUT);
  digitalWrite(PIN_BUZZER, LOW);

  // Initialiser les Leds NeoPixel
  strip.begin();
  strip.setBrightness(60); // Luminosité moyenne confortable
  strip.clear();
  strip.show();

  // Charger les paramètres depuis Preferences (flash)
  loadSettings();

  // Initialisation RTC DS1302
  Rtc.Begin();
  RtcDateTime compiled = RtcDateTime(__DATE__, __TIME__);
  
  if (!Rtc.IsDateTimeValid()) {
    Serial.println("RTC a perdu l'heure. Configuration par defaut...");
    Rtc.SetDateTime(compiled);
  }
  
  if (Rtc.GetIsWriteProtected()) {
    Serial.println("RTC protege en ecriture. Desactivation...");
    Rtc.SetIsWriteProtected(false);
  }
  
  if (!Rtc.GetIsRunning()) {
    Serial.println("RTC n'est pas actif. Lancement...");
    Rtc.SetIsRunning(true);
  }

  // Initialisation Wi-Fi Point d'Accès
  WiFi.softAP(AP_SSID, AP_PASS);
  Serial.print("Point d'Accès WiFi activé. IP: ");
  Serial.println(WiFi.softAPIP());

  // Configuration du Serveur Web et API
  server.on("/", HTTP_GET, handleRoot);
  server.on("/api/status", HTTP_GET, handleStatus);
  server.on("/api/set_time", HTTP_GET, handleSetTime);
  server.on("/api/set_alarm", HTTP_GET, handleSetAlarm);
  server.on("/api/set_district", HTTP_GET, handleSetDistrict);
  
  // Initialisation d'ElegantOTA
  ElegantOTA.begin(&server);
  server.begin();
  Serial.println("Serveur HTTP asynchrone démarré.");

  // Configuration de l'écran TFT_eSPI
  tft.begin();
  tft.setRotation(SCREEN_ROTATION);
  screenWidth = tft.width();
  screenHeight = tft.height();

  // Initialisation de LVGL
  lv_init();
  
  // Allouer le buffer de dessin LVGL
#if LVGL_VERSION_MAJOR >= 9
  // Allocation du buffer de dessin LVGL (v9, taille passée en octets)
  size_t buf_size = screenWidth * 10 * sizeof(lv_color_t);
  disp_draw_buf = (lv_color_t *)malloc(buf_size);
  if (disp_draw_buf == NULL) {
    Serial.println("Erreur d'allocation mémoire LVGL !");
    while(1) delay(1000);
  }
  
  lv_display_t * disp = lv_display_create(screenWidth, screenHeight);
  lv_display_set_flush_cb(disp, my_disp_flush);
  lv_display_set_buffers(disp, disp_draw_buf, NULL, buf_size, LV_DISPLAY_RENDER_MODE_PARTIAL);
#else
  // Allocation du buffer de dessin LVGL (v8)
  disp_draw_buf = (lv_color_t *)malloc(sizeof(lv_color_t) * screenWidth * 10);
  if (disp_draw_buf == NULL) {
    Serial.println("Erreur d'allocation mémoire LVGL !");
    while(1) delay(1000);
  }
  lv_disp_draw_buf_init(&draw_buf, disp_draw_buf, NULL, screenWidth * 10);

  // Enregistrer le display driver dans LVGL (v8)
  static lv_disp_drv_t disp_drv;
  lv_disp_drv_init(&disp_drv);
  disp_drv.hor_res = screenWidth;
  disp_drv.ver_res = screenHeight;
  disp_drv.flush_cb = my_disp_flush;
  disp_drv.draw_buf = &draw_buf;
  lv_disp_drv_register(&disp_drv);
#endif

  // Initialiser les interfaces graphiques LVGL
  initUI();
  
  lastTickTime = millis();
  
  // Bip de démarrage réussi
  triggerBeep(150);
}

void loop() {
  unsigned long now = millis();
  
  // 1. Mise à jour de l'horloge LVGL
  lv_tick_inc(now - lastTickTime);
  lastTickTime = now;
  lv_timer_handler();

  // 2. Traiter les requêtes du serveur Web
  // Non requis avec le serveur web asynchrone (géré en tâche de fond)

  // 3. Lire l'heure du DS1302
  RtcDateTime dt = Rtc.GetDateTime();
  if (dt.IsValid()) {
    if (currentState == STATE_CLOCK) {
      updateClockView(dt);
    }
    checkAlarms(dt);
  }

  // 4. Traiter les boutons
  processButtons();

  // 5. Gérer la machine à états temporelle (Transitions automatiques)
  if (currentState != STATE_CLOCK && !alarmTriggered) {
    long elapsed = now - stateTimer;
    long remaining = stateDuration - elapsed;

    if (currentState == STATE_DISTRICT) {
      if (remaining <= 0) {
        changeState(STATE_CLOCK); // Retour à l'horloge
      } else {
        updateDistrictCard(activeDistrictIndex, remaining);
      }
    } 
    else if (currentState == STATE_AUTO) {
      if (remaining <= 0) {
        changeState(STATE_CLOCK);
      }
    } 
    else if (currentState == STATE_WALKTHROUGH) {
      if (remaining <= 0) {
        // Passer au district suivant du parcours aléatoire
        walkthroughIndex++;
        if (walkthroughIndex >= NUM_LEDS) {
          changeState(STATE_CLOCK); // Fini
          triggerBeep(200);
        } else {
          activeDistrictIndex = walkthroughOrder[walkthroughIndex];
          // Relancer le timer pour le district actuel
          strip.clear();
          strip.setPixelColor(districts[activeDistrictIndex].ledIndex, STATUS_COLORS[districtProjectStatus[activeDistrictIndex]]);
          strip.show();
          stateTimer = millis();
          triggerBeep(40);
        }
      } else {
        updateDistrictCard(activeDistrictIndex, remaining);
      }
    }
  }

  delay(2); // Petit délai pour laisser respirer l'ESP32 et le WiFi
}
