#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <FS.h>
#include <ArduinoJson.h>
#include <PubSubClient.h>
#include <ArduinoOTA.h>
#include <Ticker.h>
#include <ESP8266HTTPClient.h>
#include <map>
#include <time.h>
#include <WiFiClientSecure.h>  // Para HTTPS

ESP8266WebServer server(80);
WiFiClient espClient;
WiFiClientSecure secureClient;
PubSubClient client(espClient);
Ticker watchdogReset;
Ticker verificadorWiFi;
Ticker verificadorMQTT;
Ticker tickerA0;

std::map<String, uint8_t> pines;
std::map<String, uint8_t> modosPines;
std::map<String, int> ultimoEstadoEntrada;
std::map<String, bool> esAnalogico;

String ssid, password, mqttUser, mqttPass, mqttServer, url_puertos_json;
String wifiSSID = "";

unsigned long ultimoIntentoMQTT = 0;
const unsigned long intervaloReintentoMQTT = 5000;
int mqttPort = 1883;


struct TopicsMQTT {
  String state;
  String comando;
  String ip;
  String valores;
  String reboot;
};

TopicsMQTT topics;

bool estadoInicialPublicado = false;
int ultimoValorA0 = -1;    // Valor inicial imposible para detectar primer lectura
const int UMBRAL_A0 = 10;  // Diferencia mínima para publicar


String getChipID() {
  String id = String(ESP.getChipId(), HEX);
  id.toUpperCase();
  return id;
}

void inicializarTopicsMQTT() {
  topics.state = "dispositivo/" + getChipID() + "/state";
  topics.comando = "dispositivo/" + getChipID() + "/comando";
  topics.ip = "dispositivo/" + getChipID() + "/ip";
  topics.reboot = topics.comando + "/reboot";
}

void resetWatchdog() {
  ESP.wdtFeed();
}

void setupOTA() {
  ArduinoOTA.setHostname("ESP-Dispositivo");
  ArduinoOTA.onStart([]() {
    Serial.println("\n🔄 OTA iniciado...");
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\n✅ OTA completado");
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("❌ OTA Error[%u]\n", error);
  });
  ArduinoOTA.begin();
}

void configurarHora() {
  configTime(-3 * 3600, 0, "pool.ntp.org", "time.nist.gov");  // UTC-3 (Argentina
  Serial.print("⏳ Sincronizando hora");
  while (time(nullptr) < 100000) {
    Serial.print(".");
    delay(500);
  }
  Serial.println("\n✅ Hora sincronizada");
}

void leerConfig() {
  if (!SPIFFS.exists("/config.json")) {
    Serial.println("config.json no existe. Creando valores por defecto.");
    ssid = "";
    password = "";
    mqttServer = "";
    mqttUser = "";
    mqttPass = "";
    url_puertos_json = "";
    guardarConfig();
    return;
  }

  File file = SPIFFS.open("/config.json", "r");
  if (!file) {
    Serial.println("No se pudo abrir config.json");
    return;
  }

  StaticJsonDocument<512> doc;
  DeserializationError error = deserializeJson(doc, file);
  file.close();

  if (error) {
    Serial.println("Error al parsear JSON");
    return;
  }

  ssid = doc["ssid"] | "";
  password = doc["password"] | "";
  mqttServer = doc["mqttServer"] | "";
  mqttUser = doc["mqttUser"] | "";
  mqttPass = doc["mqttPass"] | "";
  mqttPort = doc["mqttPort"] | 1883;
  url_puertos_json = doc["url_puertos_json"] | "";
}

void guardarConfig() {
  StaticJsonDocument<512> doc;
  doc["ssid"] = ssid;
  doc["password"] = password;
  doc["mqttServer"] = mqttServer;
  doc["mqttUser"] = mqttUser;
  doc["mqttPass"] = mqttPass;
  doc["mqttPort"] = mqttPort;
  doc["url_puertos_json"] = url_puertos_json;

  File file = SPIFFS.open("/config.json", "w");
  serializeJson(doc, file);
  file.close();
}

bool descargarYActualizarPuertos() {
  String urlCompleta = "https://esp.penki.com.ar/api/get_puertos.php?chipid=" + getChipID();
  Serial.println("📡 Solicitando: " + urlCompleta);

  // Eliminar esta línea: BearSSL::WiFiClientSecure secureClient;
  secureClient.setInsecure();  // Temporalmente ignorar verificación SSL (más compatible)

  HTTPClient http;
  http.setAuthorization("api", "API#2025");
  if (!http.begin(secureClient, urlCompleta)) {
    Serial.println("❌ Error al iniciar conexión HTTPS");
    return false;
  }

  int httpCode = http.GET();
  if (httpCode > 0) {
    Serial.printf("📥 HTTP %d recibido\n", httpCode);
  } else {
    Serial.printf("❌ Error HTTP al intentar descargar: %d\n", httpCode);
    http.end();
    return false;
  }

  if (httpCode == HTTP_CODE_OK) {
    String contenidoRemoto = http.getString();
    http.end();

    // --- Parsear JSON remoto ---
    StaticJsonDocument<4096> docRemoto;
    DeserializationError errorRemoto = deserializeJson(docRemoto, contenidoRemoto);
    if (errorRemoto) {
      Serial.println("❌ Error al parsear JSON remoto");
      return false;  // No actualizar ni sobreescribir el local
    }

    const char* fechaRemota = docRemoto["update"] | "";
    Serial.printf("📅 Fecha remota: %s\n", fechaRemota);

    // --- Leer archivo local (si existe) ---
    if (SPIFFS.exists("/puertos.json")) {
      File fileLocal = SPIFFS.open("/puertos.json", "r");
      StaticJsonDocument<4096> docLocal;
      DeserializationError errorLocal = deserializeJson(docLocal, fileLocal);
      fileLocal.close();

      if (!errorLocal) {
        const char* fechaLocal = docLocal["update"] | "";
        Serial.printf("📅 Fecha local: %s\n", fechaLocal);

        // --- Comparar fechas ---
        if (strcmp(fechaRemota, fechaLocal) <= 0) {
          Serial.println("✅ El archivo local está actualizado o es más reciente.");
          return true;  // No se necesita actualizar
        }
      }
    }

    // --- Guardar archivo remoto en /puertos.json ---
    File fileNuevo = SPIFFS.open("/puertos.json", "w");
    if (!fileNuevo) {
      Serial.println("❌ No se pudo abrir puertos.json para guardar");
      return false;
    }
    fileNuevo.print(contenidoRemoto);
    fileNuevo.close();
    Serial.println("✅ Archivo puertos.json actualizado desde servidor remoto");
    return true;

  } else {
    Serial.printf("❌ Error HTTP al intentar descargar: %d\n", httpCode);
    http.end();
    return false;  // Falló la descarga
  }
}

void leerPuertos() {
  pines.clear();
  modosPines.clear();
  ultimoEstadoEntrada.clear();
  esAnalogico.clear();

  if (!SPIFFS.exists("/puertos.json")) {
    Serial.println("❌ puertos.json no existe");
    return;
  }

  File file = SPIFFS.open("/puertos.json", "r");
  if (!file) {
    Serial.println("❌ No se pudo abrir puertos.json");
    return;
  }

  StaticJsonDocument<4096> doc;
  DeserializationError error = deserializeJson(doc, file);
  file.close();

  if (error) {
    Serial.println("❌ Error al parsear puertos.json");
    return;
  }

  for (JsonPair kv : doc.as<JsonObject>()) {
    String key = kv.key().c_str();
    if (key == "update") continue;

    JsonObject obj = kv.value().as<JsonObject>();
    if (!obj.containsKey("gpio") || !obj.containsKey("modo")) continue;

    String gpioStr = obj["gpio"].as<String>();
    uint8_t gpio;

    if (gpioStr == "A0") {
      gpio = A0;
    } else {
      gpio = gpioStr.toInt();
    }

    String alias = key;  // Usamos "D0", "D1", "A0", etc.
    String modoStr = obj["modo"].as<String>();

    uint8_t modoPin;
    if (modoStr == "INPUT") {
      modoPin = INPUT;
    } else if (modoStr == "OUTPUT") {
      modoPin = OUTPUT;
    } else if (modoStr == "INPUT_PULLUP") {
      modoPin = INPUT_PULLUP;
    } else {
      Serial.printf("⚠️ Modo desconocido para %s: %s, usando INPUT_PULLUP\n", alias.c_str(), modoStr.c_str());
      modoPin = INPUT_PULLUP;
    }

    pines[alias] = gpio;
    modosPines[alias] = modoPin;
    esAnalogico[alias] = (gpio == A0);  // 📌 Marca como analógico si corresponde

    Serial.printf("🔧 Alias %s => GPIO %d, modo %s\n", alias.c_str(), gpio, modoStr.c_str());
  }

  Serial.println("✅ Pines y modos cargados dinámicamente");
}

void imprimirPuertosJson() {
  if (!SPIFFS.begin()) {
    Serial.println("Error al montar SPIFFS");
    return;
  }

  if (!SPIFFS.exists("/puertos.json")) {
    Serial.println("Archivo puertos.json no existe");
    return;
  }

  File file = SPIFFS.open("/puertos.json", "r");
  if (!file) {
    Serial.println("No se pudo abrir puertos.json");
    return;
  }

  Serial.println("Contenido de puertos.json:");
  while (file.available()) {
    Serial.write(file.read());
  }
  file.close();
  Serial.println();  // Nueva línea al final
}

void conectarWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid.c_str(), password.c_str());
  Serial.print("Conectando a WiFi");

  int intentos = 20;
  while (WiFi.status() != WL_CONNECTED && intentos-- > 0) {
    delay(500);
    Serial.print(".");
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\n✅ WiFi conectado!");
    Serial.print("IP: ");
    Serial.println(WiFi.localIP());
    wifiSSID = WiFi.SSID();
  } else {
    Serial.println("\n❌ No se pudo conectar. Modo AP.");
    WiFi.softAP("ESP_Config", "12345678");
    Serial.print("AP IP: ");
    Serial.println(WiFi.softAPIP());
    server.begin();
  }
}

void conectarMQTT() {
  if (client.connected()) return;
  if ((long)(millis() - ultimoIntentoMQTT) < intervaloReintentoMQTT) return;
  ultimoIntentoMQTT = millis();

  client.setServer(mqttServer.c_str(), mqttPort);
  client.setCallback(callback);

  String clientId = "WemosClient-" + String(ESP.getChipId());
  Serial.print("Conectando a MQTT: ");
  Serial.println(mqttServer);

  if (client.connect(clientId.c_str(), mqttUser.c_str(), mqttPass.c_str(),

                     topics.state.c_str(), 1, true, "offline")) {
    Serial.println("✅ Conectado al broker MQTT");
    estadoInicialPublicado = false;  // para asegurarte que se publique

    String ip = WiFi.localIP().toString();

    client.publish(topics.state.c_str(), "online", true);
    client.publish(topics.ip.c_str(), ip.c_str(), true);
    
    client.subscribe(topics.comando.c_str());
    client.subscribe(topics.reboot.c_str());
    client.publish(topics.reboot.c_str(), "0", true);
  } else {
    Serial.print("❌ MQTT falló. Código: ");
    Serial.println(client.state());
  }
}

void callback(char* topic, byte* payload, unsigned int length) {
  payload[length] = '\0';
  String mensaje = String((char*)payload);

  Serial.printf("Mensaje recibido en topic: %s -> %s\n", topic, mensaje.c_str());

  // Verificar si es el topic de reboot
  if (String(topic) == topics.reboot.c_str()) {
    if (mensaje == "1") {
      Serial.println("Reiniciando dispositivo...");
      delay(100);  // Corto retardo para que se imprima el mensaje
      ESP.restart();
      return;
    }
  }
  if (String(topic) == topics.comando) {
    StaticJsonDocument<256> doc;
    DeserializationError err = deserializeJson(doc, mensaje);
    if (err) {
      Serial.println("Error al parsear comando JSON");
      return;
    }
    String alias = doc["alias"] | "";
    String valor = doc["valor"] | "";

    if (pines.count(alias)) {
      Serial.printf("Alias '%s' existe en pines\n", alias.c_str());
      if (modosPines[alias] == OUTPUT) {
        Serial.printf("Modo de '%s' es OUTPUT\n", alias.c_str());
      } else {
        Serial.printf("Modo de '%s' NO es OUTPUT (modo=%d)\n", alias.c_str(), modosPines[alias]);
      }
    } else {
      Serial.printf("Alias '%s' NO existe en pines\n", alias.c_str());
    }

    if (pines.count(alias) && (modosPines[alias] == OUTPUT)) {
      uint8_t gpio = pines[alias];
      if (valor == "ON") {
        digitalWrite(gpio, HIGH);
        Serial.printf("Pin %d (%s) puesto ON\n", gpio, alias.c_str());
      } else if (valor == "OFF") {
        digitalWrite(gpio, LOW);
        Serial.printf("Pin %d (%s) puesto OFF\n", gpio, alias.c_str());
      } else {
        Serial.println("Valor desconocido, usar ON u OFF");
      }
    } else {
      Serial.println("Alias no encontrado o no es salida");
    }
  }
}

void publicarEstadoInicialEntradas() {
  for (auto const& [alias, gpio] : pines) {
    if (esAnalogico[alias]) continue;  // Saltar A0

    int val = digitalRead(gpio);
    ultimoEstadoEntrada[alias] = val;

    String estado = val == HIGH ? "ON" : "OFF";
    String modoStr;

    switch (modosPines[alias]) {
      case INPUT: modoStr = "INPUT"; break;
      case OUTPUT: modoStr = "OUTPUT"; break;
      case INPUT_PULLUP: modoStr = "INPUT_PULLUP"; break;
      default: modoStr = "DESCONOCIDO"; break;
    }

    // Crear JSON: {"valor": "ON", "modo": "INPUT"}
    StaticJsonDocument<128> doc;
    doc["valor"] = estado;
    doc["modo"] = modoStr;

    char buffer[128];
    serializeJson(doc, buffer, sizeof(buffer));

    String topic = "dispositivo/" + getChipID() + "/valores/" + alias;
    client.publish(topic.c_str(), buffer, true);

    Serial.printf("Publicado estado inicial %s => %s / %s\n", alias.c_str(), estado.c_str(), modoStr.c_str());
  }
}

void publicarCambiosEntradas() {
  for (auto const& [alias, gpio] : pines) {
    if (esAnalogico[alias]) continue;  // Salta A0

    int val = digitalRead(gpio);
    if (!ultimoEstadoEntrada.count(alias) || ultimoEstadoEntrada[alias] != val) {
      ultimoEstadoEntrada[alias] = val;

      String estado = val == HIGH ? "ON" : "OFF";
      String modoStr;

      switch (modosPines[alias]) {
        case INPUT: modoStr = "INPUT"; break;
        case OUTPUT: modoStr = "OUTPUT"; break;
        case INPUT_PULLUP: modoStr = "INPUT_PULLUP"; break;
        default: modoStr = "DESCONOCIDO"; break;
      }

      StaticJsonDocument<128> doc;
      doc["valor"] = estado;
      doc["modo"] = modoStr;

      char buffer[128];
      serializeJson(doc, buffer, sizeof(buffer));

      String topic = "dispositivo/" + getChipID() + "/valores/" + alias;
      client.publish(topic.c_str(), buffer, true);

      Serial.printf("📤 Cambio en %s (digital): %s / %s\n", alias.c_str(), estado.c_str(), modoStr.c_str());
    }
  }
}

void manejarRoot() {
  String pagina = "<html><head><meta name='viewport' content='width=device-width'><link rel='stylesheet' href='/bootstrap.min.css'></head><body class='d-flex justify-content-center align-items-center vh-100'><div class='container'><div class='card p-4 shadow'><h3>Configuración WiFi</h3><form method='POST' action='/guardar'>";

  pagina += "<label>SSID:</label><select name='ssid' class='form-select'>";
  int n = WiFi.scanNetworks();
  for (int i = 0; i < n; i++) {
    String net = WiFi.SSID(i);
    pagina += "<option value='" + net + "'";
    if (net == ssid) pagina += " selected";
    pagina += ">" + net + "</option>";
  }
  pagina += "</select><label>Password:</label><input name='password' class='form-control' value='" + password + "'>";
  pagina += "<label>MQTT Server:</label><input name='mqttServer' class='form-control' value='" + mqttServer + "'>";
  pagina += "<label>MQTT User:</label><input name='mqttUser' class='form-control' value='" + mqttUser + "'>";
  pagina += "<label>MQTT Pass:</label><input name='mqttPass' class='form-control' value='" + mqttPass + "'><br>";
  pagina += "<label>MQTT Port:</label><input name='mqttPort' class='form-control' type='number' value='" + String(mqttPort) + "'><br>";
  pagina += "<label>URL Base para puertos.json (host sin ruta):</label>";
  pagina += "<input name='url_puertos_json' class='form-control' value='" + url_puertos_json + "'>";
  pagina += "<button type='submit' class='btn btn-primary'>Guardar</button></form></div></div></body></html>";

  server.send(200, "text/html", pagina);
}

void manejarGuardar() {
  ssid = server.arg("ssid");
  password = server.arg("password");
  mqttServer = server.arg("mqttServer");
  mqttUser = server.arg("mqttUser");
  mqttPass = server.arg("mqttPass");
  mqttPort = server.arg("mqttPort").toInt();
  mqttPort = constrain(mqttPort, 1, 65535);  // validación
  String nuevo_url = server.arg("url_puertos_json");
  if (nuevo_url.length() > 0) {
    url_puertos_json = nuevo_url;
  }

  guardarConfig();
  server.send(200, "text/html", "<html><body><h2>Guardado con éxito</h2><p>Reinicia el ESP para aplicar cambios.</p><a href='/'>Volver</a></body></html>");
}

void manejarBootstrap() {
  if (SPIFFS.exists("/bootstrap.min.css")) {
    File file = SPIFFS.open("/bootstrap.min.css", "r");
    server.streamFile(file, "text/css");
    file.close();
  } else {
    server.send(404, "text/plain", "No encontrado");
  }
}

void verificarConexionWiFi() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("⚠️ WiFi desconectado. Intentando reconectar...");

    WiFi.disconnect();
    WiFi.begin(ssid.c_str(), password.c_str());

    unsigned long inicio = millis();
    while (WiFi.status() != WL_CONNECTED && millis() - inicio < 10000) {
      delay(500);
      Serial.print(".");
    }

    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("\n✅ Reconectado al WiFi!");
    } else {
      Serial.println("\n❌ No se pudo reconectar. Reiniciando ESP...");
      ESP.restart();
    }
  }
}

int intentosMQTTFallidos = 0;
const int MAX_INTENTOS_MQTT = 5;

void verificarConexionMQTT() {
  if (WiFi.status() != WL_CONNECTED) return;  // Solo verifica MQTT si hay WiFi

  if (!client.connected()) {
    Serial.println("⚠️ MQTT desconectado. Intentando reconectar...");
    conectarMQTT();

    if (client.connected()) {
      Serial.println("✅ Reconexion MQTT exitosa");
      intentosMQTTFallidos = 0;
    } else {
      intentosMQTTFallidos++;
      Serial.printf("❌ Fallo reconexión MQTT (%d/%d)\n", intentosMQTTFallidos, MAX_INTENTOS_MQTT);
      if (intentosMQTTFallidos >= MAX_INTENTOS_MQTT) {
        Serial.println("🚨 Demasiados fallos MQTT. Reiniciando ESP...");
        ESP.restart();
      }
    }
  } else {
    intentosMQTTFallidos = 0;  // Resetea contador si está conectado
  }
}

void imprimirConfigJSON() {
  if (!SPIFFS.exists("/config.json")) {
    Serial.println("❌ El archivo config.json no existe.");
    return;
  }

  File file = SPIFFS.open("/config.json", "r");
  if (!file) {
    Serial.println("❌ No se pudo abrir config.json.");
    return;
  }

  Serial.println("📄 Contenido de config.json:");
  while (file.available()) {
    Serial.write(file.read());
  }
  file.close();
}

void publicarA0() {
  int valorActual = analogRead(A0);
  if (ultimoValorA0 == -1 || abs(valorActual - ultimoValorA0) > UMBRAL_A0) {
    ultimoValorA0 = valorActual;

    String topic = "dispositivo/" + getChipID() + "/valores/A0";
    String payload = String(valorActual);  // 👈 conversión de int a String
    client.publish(topic.c_str(), payload.c_str(), true);

    Serial.printf("Publicado cambio A0: %d\n", valorActual);
  }
}

void setup() {
  Serial.begin(115200);
  ESP.wdtDisable();
  ESP.wdtEnable(8000);

  if (!SPIFFS.begin()) {
    Serial.println("❌ Error al montar SPIFFS");
    // Aquí podés decidir reiniciar o modo seguro
  } else {
    Serial.println("✅ SPIFFS montado correctamente");
  }

  leerConfig();
  imprimirConfigJSON();
  imprimirPuertosJson();

  inicializarTopicsMQTT();

  conectarWiFi();

  if (WiFi.status() == WL_CONNECTED) {
    if (!descargarYActualizarPuertos()) {
      Serial.println("⚠️ Usando puertos.json local");
    }
    leerPuertos();
  } else {
    Serial.println("⚠️ No hay WiFi, usando puertos.json local");
  }

  // Configurar pines según modos cargados
  for (auto const& [alias, gpio] : pines) {
    if (modosPines.count(alias)) {
      pinMode(gpio, modosPines[alias]);
      Serial.printf("Pin %d (%s) configurado modo %d\n", gpio, alias.c_str(), modosPines[alias]);
    } else {
      pinMode(gpio, INPUT);
      Serial.printf("Pin %d (%s) configurado modo por defecto INPUT\n", gpio, alias.c_str());
    }
  }

  // Inicializar salidas en LOW
  for (auto const& [alias, modo] : modosPines) {
    if (modo == OUTPUT) {
      digitalWrite(pines[alias], LOW);
    }
  }

  if (WiFi.status() == WL_CONNECTED) {
    conectarMQTT();
  }

  setupOTA();

  // Servidor web
  server.on("/", manejarRoot);
  server.on("/guardar", HTTP_POST, manejarGuardar);
  server.on("/bootstrap.min.css", HTTP_GET, manejarBootstrap);
  server.begin();

  // Watchdog y verificadores
  watchdogReset.attach_ms(1000, resetWatchdog);
  verificadorWiFi.attach(60, verificarConexionWiFi);  // Chequeo WiFi cada 60s
  verificadorMQTT.attach(15, verificarConexionMQTT);  // Chequeo MQTT cada 15s
  tickerA0.attach_ms(10, publicarA0);
}

// LOOP principal
void loop() {
  if (WiFi.status() == WL_CONNECTED) {
    client.loop();   // Mantener MQTT vivo
    conectarMQTT();  // Reintentar conexión si está desconectado
  }

  server.handleClient();
  ArduinoOTA.handle();

  publicarCambiosEntradas();  // Leer entradas digitales y publicar solo si cambian

  // (Opcional) Podés agregar publicación periódica de A0 separado, si quieres:

  // publicarAnalogicoA0();

  // Reset watchdog
  watchdogReset.attach_ms(1000, resetWatchdog);
  verificadorWiFi.attach(60, verificarConexionWiFi);
  verificadorMQTT.attach(15, verificarConexionMQTT);
}
