#include <Arduino.h>
#include <WiFi.h>
#include <WebSocketsClient.h>
#include <ArduinoJson.h>
#include <time.h>
#include <esp_netif_sntp.h>
#include "secrets.h"
#ifndef HOMECORE_LED_ENABLED
#define HOMECORE_LED_ENABLED 0
#endif
#ifndef HOMECORE_PIR_ENABLED
#define HOMECORE_PIR_ENABLED 0
#endif
#ifndef HOMECORE_TEMPERATURE_ENABLED
// Temperature enabled by default; set to 0 only for explicit isolation tests.
#define HOMECORE_TEMPERATURE_ENABLED 1
#endif
#if HOMECORE_PIR_ENABLED && !HOMECORE_LED_ENABLED
#error "PIR requiere HOMECORE_LED_ENABLED 1"
#endif
#if HOMECORE_PIR_ENABLED
#include <Preferences.h>
#include "MotionLight.h"
const uint8_t PIR_INPUT_PIN=26;
MotionLight motionLight;
SemaphoreHandle_t lightMutex=nullptr;
Preferences lightPreferences;
bool preferencesReady=false;
void lightTask(void*) {
  bool previousMotion=false,previousReady=false;
  uint32_t lastDiagnostic=0;
  for(;;) {
    xSemaphoreTake(lightMutex,portMAX_DELAY);
    uint32_t sampledAt=millis();
    int raw=digitalRead(PIR_INPUT_PIN);
    uint8_t duty=motionLight.tick(sampledAt,raw==HIGH);
    bool motion=motionLight.motion,ready=motionLight.ready,automatic=motionLight.automatic;
    ledcWrite(23,duty);
    xSemaphoreGive(lightMutex);
    if(ready && !previousReady)Serial.println("[PIR] Listo tras estabilizacion.");
    if(motion!=previousMotion)Serial.println(motion?"[PIR] Movimiento detectado.":"[PIR] Sin movimiento; comienza espera.");
    if(uint32_t(sampledAt-lastDiagnostic)>=2000) {
      lastDiagnostic=sampledAt;
      Serial.printf("[PIR DIAG] uptime=%lu s | GPIO26=%s | listo=%s | movimiento=%s | modo=%s | brillo=%u%%\n",
        (unsigned long)(sampledAt/1000),raw==HIGH?"HIGH":"LOW",ready?"SI":"NO",motion?"SI":"NO",automatic?"AUTO":"MANUAL",unsigned(duty)*100/255);
    }
    previousMotion=motion;previousReady=ready;
    vTaskDelay(pdMS_TO_TICKS(20));
  }
}
bool validAutomation(JsonVariantConst a) {
  if(!a.is<JsonObjectConst>() || !a["mode"].is<const char*>() || !a["hold_seconds"].is<unsigned int>() || !a["fade_ms"].is<unsigned int>() || !a["max_brightness"].is<unsigned int>())return false;
  String mode=a["mode"].as<String>();
  return (mode=="auto" || mode=="manual") && a["hold_seconds"].as<unsigned>()>=5 && a["hold_seconds"].as<unsigned>()<=3600 && a["fade_ms"].as<unsigned>()>=100 && a["fade_ms"].as<unsigned>()<=10000 && a["max_brightness"].as<unsigned>()>=1 && a["max_brightness"].as<unsigned>()<=100;
}
unsigned brightnessSetting=100;
void appendLightState(JsonDocument &doc) {
  xSemaphoreTake(lightMutex,portMAX_DELAY);
  doc["led_on"]=motionLight.value>0;
  doc["brightness"]=unsigned(motionLight.value)*100/255;
  doc["motion"]=motionLight.motion;doc["pir_ready"]=motionLight.ready;
  doc["automation"]["mode"]=motionLight.automatic?"auto":"manual";
  doc["automation"]["hold_seconds"]=motionLight.holdMs/1000;
  doc["automation"]["fade_ms"]=motionLight.fadeMs;
  doc["automation"]["max_brightness"]=brightnessSetting;
  xSemaphoreGive(lightMutex);
}
void saveLightPreferences() {
  if(!preferencesReady)return;
  JsonDocument state;appendLightState(state);
  String serialized;serializeJson(state["automation"],serialized);
  if(lightPreferences.getString("settings","")!=serialized)lightPreferences.putString("settings",serialized);
}
void configureLight(JsonVariantConst a) {
  xSemaphoreTake(lightMutex,portMAX_DELAY);
  brightnessSetting=a["max_brightness"].as<unsigned>();
  motionLight.configure(a["mode"].as<String>()=="auto",a["hold_seconds"].as<unsigned>()*1000,a["fade_ms"].as<unsigned>(),255*brightnessSetting/100,millis());
  xSemaphoreGive(lightMutex);
}
#endif

const uint8_t LED_CONTROL_PIN = 23;
bool ledOn = false;
String lastCommandId;
uint32_t lastServerMessage = 0;
void setLed(bool on) {
#if HOMECORE_PIR_ENABLED
  if(lightMutex){xSemaphoreTake(lightMutex,portMAX_DELAY);motionLight.manual(on,millis());xSemaphoreGive(lightMutex);}
#elif HOMECORE_LED_ENABLED
  digitalWrite(LED_CONTROL_PIN, on ? HIGH : LOW);
#endif
  ledOn = on;
}
void networkLightLost() {
#if HOMECORE_PIR_ENABLED
  if(lightMutex){xSemaphoreTake(lightMutex,portMAX_DELAY);motionLight.networkLost(millis());xSemaphoreGive(lightMutex);}
#else
  setLed(false);
#endif
}
// Compatibilidad con secrets.h de la primera version.
#ifndef HOMECORE_SENSOR_TYPE
  #if defined(HOMECORE_USE_DHT22) && HOMECORE_USE_DHT22
    #define HOMECORE_SENSOR_TYPE 22
  #else
    #define HOMECORE_SENSOR_TYPE 0
  #endif
#endif
#if HOMECORE_SENSOR_TYPE != 0 && HOMECORE_SENSOR_TYPE != 11 && HOMECORE_SENSOR_TYPE != 22
  #error "HOMECORE_SENSOR_TYPE debe ser 0 (simulado), 11 (DHT11) o 22 (DHT22)"
#endif
#if HOMECORE_TEMPERATURE_ENABLED && HOMECORE_SENSOR_TYPE != 0
#include <DHT.h>
DHT sensor(HOMECORE_DHT_PIN, HOMECORE_SENSOR_TYPE);
#endif

WebSocketsClient socketClient;
bool configured = false, started = false, authenticated = false, ntpStarted = false;
uint32_t lastNtpAttempt = 0;
uint32_t lastHeartbeat = 0, lastSample = 0, lastPending = 0, lastWifi = 0, connectedAt = 0;
uint32_t sequenceNumber = 0, reconnectDelay = 1000;
String bootId, pendingId, pendingJson;
uint8_t retryCount = 0;

void sendDocument(JsonDocument &doc) {
  String json;
  serializeJson(doc, json);
  socketClient.sendTXT(json);
}
void event(WStype_t type, uint8_t *payload, size_t length) {
  if (type == WStype_DISCONNECTED) {
    authenticated = false; networkLightLost();
    pendingId = ""; pendingJson = ""; // No cola de lecturas antiguas tras reconexión.
    socketClient.setReconnectInterval(reconnectDelay + esp_random() % 500);
    reconnectDelay = min(reconnectDelay * 2, (uint32_t)30000);
    Serial.println("Conexion cerrada. Reintentando con espera.");
  } else if (type == WStype_CONNECTED) {
    connectedAt = millis();
    JsonDocument auth;
    auth["protocol"] = 1; auth["type"] = "device.auth";
    auth["device_uid"] = HOMECORE_DEVICE_UID; auth["token"] = HOMECORE_DEVICE_TOKEN;
    auth["firmware"] = HOMECORE_PIR_ENABLED ? "esp32-pir-3" : HOMECORE_LED_ENABLED ? "esp32-led-1" : "esp32-monitor-2";
#if HOMECORE_LED_ENABLED
    auth["capabilities"]["led"] = true; auth["led_on"] = ledOn;
#if HOMECORE_PIR_ENABLED
    auth["capabilities"]["pir"] = true;appendLightState(auth);
#endif
#endif
    sendDocument(auth);
  } else if (type == WStype_TEXT) {
    if (length > 4096) { socketClient.disconnect(); return; }
    JsonDocument response;
    if (deserializeJson(response, payload, length) || response["protocol"].as<int>() != 1) return;
    lastServerMessage = millis();
    String messageType = response["type"].as<String>();
    if (messageType == "device.auth.result" && response["success"].as<bool>()) {
      authenticated = true; reconnectDelay = 1000;
      lastHeartbeat = millis(); lastSample = millis() - 20000;
      Serial.println("HomeCore conectado y autenticado.");
    } else if (messageType == "telemetry.ack" && response["stored"].as<bool>() && response["message_id"].as<String>() == pendingId) {
      pendingId = ""; pendingJson = "";
      Serial.println("Lectura confirmada por HomeCore.");
    } else if (messageType == "command.set" && authenticated) {
#if HOMECORE_LED_ENABLED
      String id = response["command_id"].as<String>();
      if (id.length() != 32 || !response["on"].is<bool>() || !response["expires_at"].is<unsigned long>()) return;
      time_t expires = response["expires_at"].as<unsigned long>();
      if (expires <= time(nullptr) || expires > time(nullptr) + 30) return;
      // Absolute on/off commands are idempotent; no toggle or arbitrary GPIO.
      if (id != lastCommandId) { setLed(response["on"].as<bool>()); lastCommandId = id; }
      JsonDocument result;
      result["protocol"] = 1; result["type"] = "command.result";
      result["command_id"] = id; result["on"] = ledOn;
      #if HOMECORE_PIR_ENABLED
      saveLightPreferences();appendLightState(result);
      #endif
      sendDocument(result);
      Serial.printf("[LED] GPIO23: %s. Confirmacion enviada.\n", ledOn ? "ENCENDIDA" : "APAGADA");
#endif
    } else if (messageType == "command.automation" && authenticated) {
#if HOMECORE_PIR_ENABLED
      String id=response["command_id"].as<String>();
      if(id.length()!=32 || !response["expires_at"].is<unsigned long>() || !validAutomation(response["automation"]))return;
      time_t expires=response["expires_at"].as<unsigned long>();
      if(expires<=time(nullptr) || expires>time(nullptr)+30)return;
      if(id!=lastCommandId){configureLight(response["automation"]);saveLightPreferences();lastCommandId=id;}
      JsonDocument ack;ack["protocol"]=1;ack["type"]="command.automation.result";ack["command_id"]=id;
      appendLightState(ack);sendDocument(ack);
      Serial.println("[PIR] Configuracion aplicada y guardada.");
#endif
    } else if (messageType == "error") {
      Serial.println("HomeCore rechazo el mensaje. Revisa la configuracion.");
      socketClient.disconnect();
    }

  }
}
void sample() {
#if !HOMECORE_TEMPERATURE_ENABLED
  return; // No DHT calls and no invented telemetry while isolating the PIR.
#else
  float temperature, humidity;
#if HOMECORE_TEMPERATURE_ENABLED && HOMECORE_SENSOR_TYPE != 0
  temperature = sensor.readTemperature(); humidity = sensor.readHumidity();
  if (!isfinite(temperature) || !isfinite(humidity)) {
    Serial.println("Sensor sin lectura valida. No se envian datos ficticios."); return;
  }
#else
  temperature = 24.0f + (esp_random() % 200) / 100.0f;
  humidity = 50.0f + (esp_random() % 1000) / 100.0f;
#endif
#if HOMECORE_SENSOR_TYPE == 11
  // Limites publicados por Steren para ARD-360.
  if (temperature < 0 || temperature > 50 || humidity < 20 || humidity > 90) {
    Serial.println("ARD-360: lectura fuera del rango especificado. No se envia."); return;
  }
#endif
  Serial.printf("[Telemetry] %s: %.2f C | %.2f %%\n", HOMECORE_SENSOR_TYPE == 0 ? "Simulada" : "Sensor real", temperature, humidity);
  if (temperature < -40 || temperature > 85 || humidity < 0 || humidity > 100) return;
  JsonDocument telemetry;
  pendingId = bootId + "-" + String(++sequenceNumber);
  telemetry["protocol"] = 1; telemetry["type"] = "telemetry.publish";
  telemetry["message_id"] = pendingId;
  telemetry["source"] = HOMECORE_SENSOR_TYPE != 0 ? "sensor" : "simulated";
  telemetry["readings"]["temperature"] = temperature;
  telemetry["readings"]["humidity"] = humidity;
  pendingJson = ""; serializeJson(telemetry, pendingJson);
  socketClient.sendTXT(pendingJson); lastPending = millis(); retryCount = 0;
#endif
}

void setup() {
  Serial.begin(115200);
#if HOMECORE_PIR_ENABLED
  digitalWrite(23,LOW);pinMode(23,OUTPUT);pinMode(PIR_INPUT_PIN, INPUT);
  if(!ledcAttach(23,1000,8)){Serial.println("Error al iniciar PWM.");return;}
  ledcWrite(23,0);lightMutex=xSemaphoreCreateMutex();
  if(!lightMutex){Serial.println("Error al iniciar control local.");return;}
  preferencesReady=lightPreferences.begin("hc-light",false);
  JsonDocument saved;
  if(preferencesReady && !deserializeJson(saved,lightPreferences.getString("settings","")) && validAutomation(saved.as<JsonVariantConst>()))configureLight(saved.as<JsonVariantConst>());
  if(xTaskCreate(lightTask,"hc-motion",4096,nullptr,1,nullptr)!=pdPASS){Serial.println("Error al iniciar tarea PIR.");return;}
  Serial.println("HomeCore PIR v3 | OUT GPIO26 INPUT | PWM GPIO23 | Estabilizando 60 s");
#elif HOMECORE_LED_ENABLED
  digitalWrite(LED_CONTROL_PIN, LOW);
  pinMode(LED_CONTROL_PIN, OUTPUT);
  setLed(false);
#endif
  if (HOMECORE_LED_ENABLED && !HOMECORE_PIR_ENABLED) Serial.println("HomeCore LED v1 | GPIO23 | Arranque apagado");
  Serial.printf("HomeCore monitor v2 | Sensor: %d | GPIO: %d\n", HOMECORE_SENSOR_TYPE, HOMECORE_DHT_PIN);
  if (!HOMECORE_TEMPERATURE_ENABLED) Serial.println("[DHT] Desactivado: no se lee ni se envia temperatura simulada.");
  configured = strlen(HOMECORE_DEVICE_UID) == 35 && strlen(HOMECORE_DEVICE_TOKEN) == 64 && strstr(HOMECORE_ROOT_CA, "-----BEGIN CERTIFICATE-----");
  if (!configured) { Serial.println("Completa secrets.h con dominio, WiFi, credenciales y CA valida."); return; }
  bootId = String(esp_random(), HEX) + String(esp_random(), HEX);
  WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(true);
  WiFi.begin(HOMECORE_WIFI_SSID, HOMECORE_WIFI_PASSWORD);
  socketClient.onEvent(event);
#if HOMECORE_TEMPERATURE_ENABLED && HOMECORE_SENSOR_TYPE != 0
  sensor.begin();
#endif
}
void loop() {
  if (!configured) { delay(100); return; }
  uint32_t now = millis();
  if (WiFi.status() != WL_CONNECTED) {
    authenticated = false; networkLightLost();
    if ((uint32_t)(now - lastWifi) >= 10000) { lastWifi = now; WiFi.reconnect(); }
    delay(10); return;
  }
  // Initialize SNTP only with an assigned IP, through the TCP/IP-safe ESP-NETIF API.
  if (WiFi.localIP() == IPAddress(0,0,0,0)) { delay(20); return; }
  if (!ntpStarted && (lastNtpAttempt == 0 || uint32_t(now-lastNtpAttempt) >= 10000)) {
    lastNtpAttempt = now == 0 ? 1 : now;
    esp_sntp_config_t ntpConfig = ESP_NETIF_SNTP_DEFAULT_CONFIG("pool.ntp.org");
    esp_err_t result = esp_netif_sntp_init(&ntpConfig);
    ntpStarted = result == ESP_OK;
    Serial.printf("[NTP] Inicio seguro: %s\n", esp_err_to_name(result));
  }
  if (!ntpStarted) { delay(20); return; }
  // TLS requiere hora correcta; no degradar a una conexion sin validacion.
  if (time(nullptr) < 1700000000) { delay(20); return; }
  if (!started) {
    socketClient.beginSslWithCA(HOMECORE_WS_HOST, HOMECORE_WS_PORT, HOMECORE_WS_PATH, HOMECORE_ROOT_CA, "");
    socketClient.setReconnectInterval(1000);
    started = true;
  }
  socketClient.loop();
  now = millis(); // TLS/event callbacks may take time and update connectedAt.
  if (socketClient.isConnected() && !authenticated && (uint32_t)(now - connectedAt) > 6000) socketClient.disconnect();
  if (authenticated && (uint32_t)(now - lastServerMessage) > 45000) {
    networkLightLost(); authenticated = false; socketClient.disconnect();
  }
  if (authenticated) {
    if ((uint32_t)(now - lastHeartbeat) >= (HOMECORE_PIR_ENABLED ? 2000 : 20000)) {
      JsonDocument heartbeat;
      heartbeat["protocol"] = 1; heartbeat["type"] = "device.heartbeat";
#if HOMECORE_LED_ENABLED
      heartbeat["led_on"] = ledOn;
#if HOMECORE_PIR_ENABLED
      appendLightState(heartbeat);
#endif
#endif
      sendDocument(heartbeat); lastHeartbeat = now;
    }
    if (pendingJson.length() && (uint32_t)(now - lastPending) >= 10000) {
      if (++retryCount > 2) socketClient.disconnect();
      else { socketClient.sendTXT(pendingJson); lastPending = now; }
    } else if (!pendingJson.length() && (uint32_t)(now - lastSample) >= 20000) {
      lastSample = now; sample();
    }
  }
  delay(2);
}
