#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <Hash.h>


const char *WIFI_SSID = "seminardemo";
const char *WIFI_PASS = "seminardemo";

const char *MQTT_HOST = "192.168.1.16"; // broker 
const uint16_t MQTT_PORT = 1883;
const char *MQTT_USER = "testuser";     // from test/.env
const char *MQTT_PASS = "testpass";     // from test/.env

const char *MQTT_TOPIC = "lambdaiot";   // main topic

// GPIO
const uint8_t ACTOR_PIN = 14; // digital output

// Publishing interval (ms)
const unsigned long SENSOR_PUBLISH_INTERVAL = 5000;

WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);

String macAddress;
String deviceId;
String sensorId;
String actorId;

unsigned long lastPublishAt = 0;

// DNS namespace UUID bytes: 6ba7b810-9dad-11d1-80b4-00c04fd430c8
const char *DNS_NAMESPACE_UUID = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";

// ---- UUID helpers ----

bool parseUuidBytes(const String &uuidStr, uint8_t out[16]) {
  String hex = uuidStr;
  hex.replace("-", "");
  if (hex.length() != 32) {
    return false;
  }
  for (int i = 0; i < 16; i++) {
    char c1 = hex[i * 2];
    char c2 = hex[i * 2 + 1];
    auto hexVal = [](char c) -> int {
      if (c >= '0' && c <= '9') return c - '0';
      if (c >= 'a' && c <= 'f') return c - 'a' + 10;
      if (c >= 'A' && c <= 'F') return c - 'A' + 10;
      return -1;
    };
    int v1 = hexVal(c1);
    int v2 = hexVal(c2);
    if (v1 < 0 || v2 < 0) {
      return false;
    }
    out[i] = (uint8_t)((v1 << 4) | v2);
  }
  return true;
}

String formatUuid(const uint8_t bytes[16]) {
  char buf[37];
  snprintf(buf, sizeof(buf),
           "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
           bytes[0], bytes[1], bytes[2], bytes[3],
           bytes[4], bytes[5],
           bytes[6], bytes[7],
           bytes[8], bytes[9],
           bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]);
  return String(buf);
}

bool parseHexBytes(const String &hex, uint8_t *out, size_t outLen) {
  if (hex.length() < (int)(outLen * 2)) {
    return false;
  }
  for (size_t i = 0; i < outLen; i++) {
    char c1 = hex[i * 2];
    char c2 = hex[i * 2 + 1];
    auto hexVal = [](char c) -> int {
      if (c >= '0' && c <= '9') return c - '0';
      if (c >= 'a' && c <= 'f') return c - 'a' + 10;
      if (c >= 'A' && c <= 'F') return c - 'A' + 10;
      return -1;
    };
    int v1 = hexVal(c1);
    int v2 = hexVal(c2);
    if (v1 < 0 || v2 < 0) {
      return false;
    }
    out[i] = (uint8_t)((v1 << 4) | v2);
  }
  return true;
}

String uuidV5(const uint8_t namespaceBytes[16], const String &name) {
  // SHA1(namespace + name)
  size_t totalLen = 16 + name.length();
  uint8_t *buf = (uint8_t *)malloc(totalLen);
  if (!buf) {
    return String("");
  }
  memcpy(buf, namespaceBytes, 16);
  memcpy(buf + 16, name.c_str(), name.length());

  String hashHex = sha1(buf, totalLen);
  free(buf);

  uint8_t out[16];
  if (!parseHexBytes(hashHex, out, sizeof(out))) {
    return String("");
  }

  // Set version (5) and variant (RFC 4122)
  out[6] = (out[6] & 0x0F) | 0x50;
  out[8] = (out[8] & 0x3F) | 0x80;

  return formatUuid(out);
}

// ---- MQTT messaging ----

void publishDiscovery() {
  StaticJsonDocument<1024> doc;
  doc["mac_address"] = macAddress;

  JsonObject dev = doc.createNestedObject("device");
  dev["id"] = deviceId;
  dev["name"] = "ESP8266 Demo";
  dev["description"] = "ESP8266 MQTT demo device";
  dev["location"] = "Lab";
  dev["status_id"] = 1;

  JsonArray sensors = doc.createNestedArray("sensors");
  JsonObject s0 = sensors.createNestedObject();
  s0["id"] = sensorId;
  s0["name"] = "Temperature";
  s0["type"] = "ADC";
  s0["data_type_id"] = 2;

  JsonArray actors = doc.createNestedArray("actors");
  JsonObject a0 = actors.createNestedObject();
  a0["id"] = actorId;
  a0["name"] = "DigitalOut";
  a0["type"] = "GPIO";
  a0["data_type_id"] = 1;

  char buf[1024];
  size_t n = serializeJson(doc, buf, sizeof(buf));
  
  Serial.println(n);
  Serial.println(buf);

  String topic = String(MQTT_TOPIC) + "/discovery";
  Serial.println(topic);
  int result = mqtt.publish(topic.c_str(), buf, n);
  Serial.println(result);
}

void publishSensorReading() {
  int adc = analogRead(A0);
  float value = (float)adc; // raw ADC value

  StaticJsonDocument<256> doc;
  doc["type"] = "sensor_reading";
  doc["sensor_id"] = sensorId;
  doc["value"] = value;

  char buf[256];
  size_t n = serializeJson(doc, buf, sizeof(buf));
  mqtt.publish(MQTT_TOPIC, buf, n);
}

void publishDeviceCheckResponse() {
  StaticJsonDocument<256> doc;
  doc["type"] = "device_check_response";
  doc["device_id"] = deviceId;
  doc["status"] = "ok";

  char buf[256];
  size_t n = serializeJson(doc, buf, sizeof(buf));
  mqtt.publish(MQTT_TOPIC, buf, n);
}

void handleActorCommand(JsonObject payload) {
  if (!payload.containsKey("actor_id")) return;
  String id = payload["actor_id"].as<String>();
  if (id != actorId) return;

  int value = 0;
  if (payload.containsKey("value")) {
    if (payload["value"].is<bool>()) {
      value = payload["value"].as<bool>() ? 1 : 0;
    } else if (payload["value"].is<int>()) {
      value = payload["value"].as<int>() != 0 ? 1 : 0;
    } else if (payload["value"].is<float>()) {
      value = payload["value"].as<float>() != 0 ? 1 : 0;
    } else if (payload["value"].is<const char *>()) {
      String v = payload["value"].as<String>();
      v.toLowerCase();
      if (v == "true" || v == "1" || v == "on") value = 1;
      else value = 0;
    }
  }

  digitalWrite(ACTOR_PIN, value ? HIGH : LOW);
}

void handleSensorTrigger(JsonObject payload) {
  if (!payload.containsKey("sensor_id")) return;
  String id = payload["sensor_id"].as<String>();
  if (id != sensorId) return;
  publishSensorReading();
}

void handleDeviceCheckRequest(JsonObject payload) {
  if (!payload.containsKey("device_id")) return;
  String id = payload["device_id"].as<String>();
  if (id != deviceId) return;
  publishDeviceCheckResponse();
}

void mqttCallback(char *topic, uint8_t *payload, unsigned int length) {
  StaticJsonDocument<512> doc;
  DeserializationError err = deserializeJson(doc, payload, length);
  if (err) {
    return;
  }

  const char *type = doc["type"] | "";
  String msgType = String(type);
  msgType.toLowerCase();

  if (msgType == "actor_command") {
    handleActorCommand(doc.as<JsonObject>());
    return;
  }
  if (msgType == "sensor_trigger") {
    handleSensorTrigger(doc.as<JsonObject>());
    return;
  }
  if (msgType == "device_check_request") {
    handleDeviceCheckRequest(doc.as<JsonObject>());
    return;
  }
}

// ---- Setup/loop ----

void ensureMqtt() {
  while (!mqtt.connected()) {
    String clientId = "esp-demo-" + String(ESP.getChipId());
    if (mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASS)) {
      mqtt.subscribe(MQTT_TOPIC);
      delay(1000);
      publishDiscovery();
    } else {
      delay(2000);
    }
  }
}

void setup() {
  pinMode(ACTOR_PIN, OUTPUT);
  digitalWrite(ACTOR_PIN, LOW);
  Serial.begin(9600);

  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }

  macAddress = WiFi.macAddress();
  macAddress.toLowerCase();

  uint8_t dnsNs[16];
  parseUuidBytes(String(DNS_NAMESPACE_UUID), dnsNs);
  String macNoSep = macAddress;
  macNoSep.replace(":", "");
  deviceId = uuidV5(dnsNs, macNoSep);

  uint8_t deviceNs[16];
  parseUuidBytes(deviceId, deviceNs);
  sensorId = uuidV5(deviceNs, "sensor-0");
  actorId = uuidV5(deviceNs, "actor-0");

  mqtt.setBufferSize(4096); // Otherwise discovery fails spectacularly
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(mqttCallback);

  ensureMqtt();
  lastPublishAt = millis();
}

void loop() {
  ensureMqtt();
  mqtt.loop();

  unsigned long now = millis();
  if (now - lastPublishAt >= SENSOR_PUBLISH_INTERVAL) {
    lastPublishAt = now;
    publishSensorReading();
  }
}
