/*
	20.06.2025
	WiFi Manager für IoT  ohne WebServer
	siehe RandomNerdTutorial und DeepSeek
  mit Built-In-LED und Taster an D5 (GPIO_14)

  Bei Tastendruck an D5 (GPIO_14) zu Masse mit Restart, wird der Wifi_Manager Start erzwungen.
  192.168.4.1 lebt nur 5 Min.

  mit zusätzlichen MQTT Parameter
*/

#include <WiFiManager.h>    // https://github.com/tzapu/WiFiManager
#include <PubSubClient.h>   // Für MQTT
#include <ArduinoJson.h>    // https://arduinojson.org/
#include <LittleFS.h>       // nicht flüchtiger Speicher


#define BUILTIN_LED   2     // GPIO for D1mini
//#define BUILTIN_LED 1     // GPIO for ESP01

#define WIFI_BUTTON_PIN   14     // Taster an GPIO_14 (D5)
//#define TASTER_PIN 0     // D3 = GPIO0 (⚠️ Vorsicht beim Boot!)
//#define TASTER_PIN 4      // GPIO4 (Safe)

WiFiManager wifiManager;

// MQTT stuff siehe my_MQTT.h
WiFiClient   espClient;
PubSubClient mqttClient(espClient);

#include "myMQTT_Data.h"    // für den MQTT Datenaustausch

const char* nameAP = "ESP_MQTT_Config";
IPAddress   brokerIP;   // IP-Adresse vom Broker

// MQTT-Struktur (in EEPROM/Preferences gespeichert)
struct Config {
  char mqtt_server[40] = "test.mosquitto.org";
  char mqtt_port[6] = "1883";  // Standard: "1883"
  char mqtt_user[32] = "";
  char mqtt_pass[32] = "";
};  
Config config;

// ==================== Forward Declaration ====================
void loadConfig();
void saveConfig();

// ==================== startWiFi_Manager ====================
void startWiFi_Manager() {
  // Timeout für den WiFiClient setzen
  espClient.setTimeout(10000); // 10 Sekunden, Abbruch wenn der Broker nicht innerhalb antwortet.
  
  // 1. Dateisystem initialisieren
  //LittleFS.format();  // Nur beim ersten Mal oder bei Problemen  
  
  // Starten und Prüft LittleFS
  if (!LittleFS.begin()) {
    Serial.println("LittleFS Mount Failed");
    snprintf(line4, sizeof(line4), "LittleFS failed");  // 
    showTextOLED(lines, 5);       // siehe "myOLED_u8g2.h"    
    return;
  }

  // Konfiguration laden aus internem Speicher, siehe unten
  loadConfig();
  
  pinMode(WIFI_BUTTON_PIN, INPUT_PULLUP);  // WiFi Taster mit internem Pull-up

  // 2. WiFiManager mit MQTT-Parametern einstellen
  WiFiManagerParameter custom_mqtt_server("server", "MQTT Server", config.mqtt_server, 40);
  WiFiManagerParameter custom_mqtt_port("port", "MQTT Port", config.mqtt_port, 6);
  WiFiManagerParameter custom_mqtt_user("user", "MQTT User", config.mqtt_user, 32);
  WiFiManagerParameter custom_mqtt_pass("pass", "MQTT Pass", config.mqtt_pass, 32);

  wifiManager.addParameter(&custom_mqtt_server);
  wifiManager.addParameter(&custom_mqtt_port);
  wifiManager.addParameter(&custom_mqtt_user);
  wifiManager.addParameter(&custom_mqtt_pass);

  // Uncomment and run it once, if you want to erase all the stored information
  //wifiManager.resetSettings();

  // set custom ip for portal
  //wifiManager.setAPConfig(IPAddress(10,0,1,1), IPAddress(10,0,1,1), IPAddress(255,255,255,0));

  // Timeout für das Konfigurationsportal setzen (300 Sekunden)
  wifiManager.setConfigPortalTimeout(300);  // Timeout: 5 Minuten

  // Callback für den AP-Modus die beim Start des AP-Modus aufgerufen wird.
  wifiManager.setAPCallback([](WiFiManager * wm) {
    Serial.println("Config-Portal gestartet! SSID: " + wm->getConfigPortalSSID());
    
    snprintf(line1, sizeof(line1), "Start AccessPoint:");  // Überschreibt line1 , Zeichenlänge 30 wird geprüft
    snprintf(line2, sizeof(line2), "%s", wm->getConfigPortalSSID().c_str() );
    IPAddress apIP = WiFi.softAPIP();         // IP-Adresse des AP formatieren
    snprintf(line3, sizeof(line3), "AP-IP: %s", apIP.toString().c_str() );      // AP - IP 192.168.4.1
    snprintf(line4, sizeof(line4), "");  // 
    snprintf(line5, sizeof(line5), "");  // 
    showTextOLED(lines, 5);       // siehe "myOLED_u8g2.h"  
  });

  // Callback nach erfolgreicher Konfiguration
  wifiManager.setSaveConfigCallback([]() {
    Serial.println("WLAN-Daten + MQTT-Daten gespeichert!");

    snprintf(line3, sizeof(line3), "WLAN + MQTT - Daten");  // 
    snprintf(line4, sizeof(line4), "gespeichert.");  // 
    snprintf(line5, sizeof(line5), "");  // 
    showTextOLED(lines, 5);       // siehe "myOLED_u8g2.h"   
  });

  // 3. WiFi verbinden oder Config-Portal starten
  // Wenn Taster an WIFI_BUTTON_PIN gedrückt (LOW), starte Config-Portal
  if (digitalRead(WIFI_BUTTON_PIN) == LOW) {
    delay(500);  // Entprellen 0,5 sec
    if (digitalRead(WIFI_BUTTON_PIN) == LOW) {
      snprintf(line5, sizeof(line5), "** Release Button **");  // 
      showTextOLED(lines, 5);       // siehe "myOLED_u8g2.h"         
      while(digitalRead(WIFI_BUTTON_PIN) == LOW)  // Warten bis Taste wieder losgelassen.
        delay(500);  // Kurze Verzögerung zur CPU-Entlastung
       
      LittleFS.remove("/config.json");          // entfernt die config.json
      wifiManager.resetSettings();          // if you want to erase all the stored information
      ESP.restart();
    }
  }
  

  // Automatische Verbindung mit ssid und pass versuchen, sonst start mit Access Point (AP)
  if (!wifiManager.autoConnect(nameAP)) { 
    Serial.println("Fehler: Keine Verbindung und Timeout erreicht!");

    snprintf(line4, sizeof(line4), "Keine WiFi-Verbindg");  // 
    snprintf(line5, sizeof(line5), "    Press Button");  // 
    showTextOLED(lines, 5);       // siehe "myOLED_u8g2.h"   
        
    ESP.restart();  // Neustart bei Fehler
  }

  // 4. Neue Werte speichern im internen Speicher
  strncpy(config.mqtt_server, custom_mqtt_server.getValue(), sizeof(config.mqtt_server));
  strncpy(config.mqtt_port,   custom_mqtt_port.getValue(), sizeof(config.mqtt_port));
  strncpy(config.mqtt_user,   custom_mqtt_user.getValue(), sizeof(config.mqtt_user));
  strncpy(config.mqtt_pass,   custom_mqtt_pass.getValue(), sizeof(config.mqtt_pass));
  saveConfig();   // siehe unten

  // 5. MQTT Verbindung initialisieren
  mqttClient.setServer(config.mqtt_server, atoi(config.mqtt_port));
  mqttClient.connect(clientId.c_str(), config.mqtt_user, config.mqtt_pass, mqtt_LWT_Topic, qos, retain, "offline");
  mqttClient.setCallback(callbackMessage);   // für einkommende Nachrichten callback Funktion registrieren siehe myMQTT_Data.h

  publishMessage(mqtt_LWT_Topic, "online", retain);
  publishMessage(mqtt_DeviceName_Topic, deviceName, retain);
  publishMessage(mqtt_DevicePlace_Topic, devicePlace, retain);

  brokerIP = espClient.remoteIP();
  Serial.print("MQTT verbunden mit: " );
  Serial.print("MQTT Broker: ");
  Serial.print(config.mqtt_server);
  Serial.print(":");
  Serial.println(config.mqtt_port);
  Serial.print("\t MQTT-IP: ");
  Serial.println(brokerIP.toString());
  Serial.println("Erfolgreich verbunden mit: " + WiFi.SSID());
  
  snprintf(line1, sizeof(line1), "SSID: %s", WiFi.SSID().c_str());  // SSID ausgeben
  snprintf(line2, sizeof(line2), "IP: %s", WiFi.localIP().toString().c_str());  // IP ausgeben
  snprintf(line3, sizeof(line3), "MQTT-Broker:");   // MQTT-Label
  snprintf(line4, sizeof(line4), "%s", config.mqtt_server);  // MQTT-Server-Adresse
  snprintf(line5, sizeof(line5), "MQTT: %s", brokerIP.toString().c_str() );  // 
  showTextOLED(lines, 5);       // siehe "myOLED_u8g2.h"     
}

// ==================== HELPER ====================
void loadConfig() {
  if (LittleFS.exists("/config.json")) {
    File file = LittleFS.open("/config.json", "r");
    if (file) {
      DynamicJsonDocument doc(512);
      deserializeJson(doc, file);
      
      strlcpy(config.mqtt_server, doc["mqtt_server"] | "test.mosquitto.org", sizeof(config.mqtt_server));
      strlcpy(config.mqtt_port, doc["mqtt_port"] | "1883", sizeof(config.mqtt_port));
      strlcpy(config.mqtt_user, doc["mqtt_user"] | "user", sizeof(config.mqtt_user));
      strlcpy(config.mqtt_pass, doc["mqtt_pass"] | "pass", sizeof(config.mqtt_pass));
      
      file.close();
    }
  }
}

void saveConfig() {
  DynamicJsonDocument doc(512);
  doc["mqtt_server"] = config.mqtt_server;
  doc["mqtt_port"] = config.mqtt_port;
  doc["mqtt_user"] = config.mqtt_user;
  doc["mqtt_pass"] = config.mqtt_pass;

  File file = LittleFS.open("/config.json", "w");
  if (file) {
    serializeJson(doc, file);
    file.close();
 
    Serial.print("Saved config: ");
    Serial.print(config.mqtt_server); Serial.print(":");
    Serial.println(config.mqtt_port);
  } else {
    Serial.println("Config not saved, because file troubles. ");
  }
}

//==================== für MQTT connection ====================
bool validateMQTTConfig();          // Forward Declaration
bool testDomainResolution(const char* domain);

//================ Fehlercode der MQTT Verbindung
void printMQTTError(int state) {
  switch (state) {
    case -4: Serial.print("Timeout (Broker nicht erreichbar)"); break;
    case -3: Serial.print("Verbindung verloren"); break;
    case -2: Serial.print("Connect fehlgeschlagen"); break;
    case -1: Serial.print("Disconnected"); break;
    case 1: Serial.print("Falsches Protokoll"); break;
    case 2: Serial.print("Client-ID abgelehnt"); break;
    case 3: Serial.print("Broker nicht verfügbar"); break;
    case 4: Serial.print("Falscher Benutzername/Passwort"); break;
    case 5: Serial.print("Nicht autorisiert"); break;
    default: Serial.print("Unbekannter Fehler"); break;
  }
}

//================ Prüft die Verbindung zum MQTT Broker und baut diese wieder auf, wenn disconnected
void reconnectMQTT() {
  snprintf(line5, sizeof(line5), "MQTT reconnect");  // 
  showTextOLED(lines, 5);       // siehe "myOLED_u8g2.h"  

  // Timeout für den WiFiClient setzen
  espClient.setTimeout(5000); // 5 Sekunden

  // Schleife solange bis MQTT verbunden ist  
  while (!mqttClient.connected()) {
    Serial.print("Versuche MQTT-Verbindung... ");
    // prüfe die MQTT-Einstellungen
    if (!validateMQTTConfig()) {
      Serial.println("Ungültige MQTT-Konfiguration, warte 10s...");
      snprintf(line5, sizeof(line5), "MQTT Config Fehler");  // 
      showTextOLED(lines, 5);       // siehe "myOLED_u8g2.h"  
         
      delay(10000);
      continue;
    }
  
    // Verbindung zum MQTT-Broker herstellen und prüfen
    if (mqttClient.connect(clientId.c_str(), config.mqtt_user, config.mqtt_pass, mqtt_LWT_Topic, qos, retain, "offline")) {
      brokerIP = espClient.remoteIP();
      Serial.println("MQTT verbunden!");
      Serial.print(config.mqtt_server);
      Serial.print(" on port: ");
      Serial.print(config.mqtt_port);
      Serial.print("\t MQTT-IP: ");
      Serial.println(brokerIP.toString());

      snprintf(line5, sizeof(line5), "%s", config.mqtt_server);  // MQTT-Server-Adresse
      showTextOLED(lines, 5);       // siehe "myOLED_u8g2.h"

      mqttClient.subscribe(mqtt_main_Topic);  //SUBSCRIBE TO TOPIC HERE, wenn Nachrichten empfangen werden sollen.

      publishMessage(mqtt_LWT_Topic, "online", retain);
      publishMessage(mqtt_DeviceName_Topic, deviceName, retain);
      publishMessage(mqtt_DevicePlace_Topic, devicePlace, retain);
      //publishMessage(msgWiFi_topic, String(ssid).c_str(), retain);
      //publishMessage(msgWiFi_ip_topic, str_WiFi_data.c_str(), retain);

    } else {
      Serial.print("MQTT fehlgeschlagen, Fehler= ");
      snprintf(line5, sizeof(line5), "MQTT fehlgeschlagen!");  // 
      showTextOLED(lines, 5);       // siehe "myOLED_u8g2.h"   
      
      printMQTTError(mqttClient.state());   // MQTT Fehler Klartext

      Serial.println("\n...try again in 5 seconds\n");  // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

// ==================== Erweiterte Sicherheitsprüfung der MQTT-Einstellungen
bool validateMQTTConfig() {
  if (strlen(config.mqtt_server) == 0) {
    Serial.println("Kein MQTT-Server konfiguriert!");
    snprintf(line5, sizeof(line5), "Kein MQTT-Broker");  // 
    showTextOLED(lines, 5);       // siehe "myOLED_u8g2.h"     

    return false;
  }
  
  // Einfache IP-Adressen-Validierung
  IPAddress ip;
  if (ip.fromString(config.mqtt_server)) {
    Serial.println("Gültige IP-Adresse erkannt");
    return true;
  }else {
    // Domain-Name (wie test.mosquitto.org) - wir müssen die Erreichbarkeit anders prüfen
    Serial.println("Domain-Name erkannt, Erreichbarkeit wird geprüft...");
    return testDomainResolution(config.mqtt_server);
  }
  return true;
}

// ====================
bool testDomainResolution(const char* domain) {
  WiFiClient client;
  
  // 1. DNS-Auflösung testen
  IPAddress resolvedIP;
  if (!WiFi.hostByName(domain, resolvedIP)) {
    Serial.println("DNS-Auflösung fehlgeschlagen!");
    snprintf(line5, sizeof(line5), "DNS fehlgeschlagen");  // 
    showTextOLED(lines, 5);       // siehe "myOLED_u8g2.h"     
    return false;
  }
  Serial.print("Erfolgreich aufgelöst: ");
  Serial.print(domain);
  Serial.print(" -> ");
  Serial.println(resolvedIP);

  // 2. Port-Test (optional)
  if (!client.connect(domain, 1883)) {
    Serial.println("Verbindung zum Port 1883 fehlgeschlagen");
    snprintf(line5, sizeof(line5), "port fehlgeschlagen");  // 
    showTextOLED(lines, 5);       // siehe "myOLED_u8g2.h"     
    return false;
  }
  client.stop();
  return true;
}

