/*
 * IoT SmartBin — ESP32-CAM v2.0
 *
 * REWRITTEN: Pakai esp_http_server.h (bukan WebServer.h yang buggy)
 * Ini cara RESMI dari Espressif, terbukti stabil untuk streaming.
 *
 * Port 80: GET /         → Status page
 *          GET /capture  → Single JPEG
 *          GET /flash    → Toggle flash LED
 * Port 81: GET /stream   → MJPEG live stream (non-blocking, terpisah)
 *
 * Board: AI-Thinker ESP32-CAM + OV2640
 */

#include <Arduino.h>
#include <WiFi.h>
#include "esp_camera.h"
#include "esp_http_server.h"
#include "config_cam.h"

// ==================== AI-Thinker Pin Map ====================
#define PWDN_GPIO_NUM   32
#define RESET_GPIO_NUM  -1
#define XCLK_GPIO_NUM    0
#define SIOD_GPIO_NUM   26
#define SIOC_GPIO_NUM   27
#define Y9_GPIO_NUM     35
#define Y8_GPIO_NUM     34
#define Y7_GPIO_NUM     39
#define Y6_GPIO_NUM     36
#define Y5_GPIO_NUM     21
#define Y4_GPIO_NUM     19
#define Y3_GPIO_NUM     18
#define Y2_GPIO_NUM      5
#define VSYNC_GPIO_NUM  25
#define HREF_GPIO_NUM   23
#define PCLK_GPIO_NUM   22

// ==================== Globals ====================
static httpd_handle_t httpd_main   = NULL;
static httpd_handle_t httpd_stream = NULL;
static bool flashState = false;
static bool cameraRegistered = false;
static unsigned long lastRegisterAttempt = 0;
static unsigned long lastWiFiAttempt = 0;

// MJPEG boundary
#define BOUNDARY "----esp32camboundary"
static const char *STREAM_CT   = "multipart/x-mixed-replace;boundary=" BOUNDARY;
static const char *STREAM_BR   = "\r\n--" BOUNDARY "\r\n";
static const char *STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";

// ==================== Camera Init ====================
bool initCamera() {
  camera_config_t cfg = {};
  cfg.ledc_channel = LEDC_CHANNEL_0;
  cfg.ledc_timer   = LEDC_TIMER_0;
  cfg.pin_d0       = Y2_GPIO_NUM;
  cfg.pin_d1       = Y3_GPIO_NUM;
  cfg.pin_d2       = Y4_GPIO_NUM;
  cfg.pin_d3       = Y5_GPIO_NUM;
  cfg.pin_d4       = Y6_GPIO_NUM;
  cfg.pin_d5       = Y7_GPIO_NUM;
  cfg.pin_d6       = Y8_GPIO_NUM;
  cfg.pin_d7       = Y9_GPIO_NUM;
  cfg.pin_xclk     = XCLK_GPIO_NUM;
  cfg.pin_pclk     = PCLK_GPIO_NUM;
  cfg.pin_vsync    = VSYNC_GPIO_NUM;
  cfg.pin_href     = HREF_GPIO_NUM;
  cfg.pin_sccb_sda = SIOD_GPIO_NUM;
  cfg.pin_sccb_scl = SIOC_GPIO_NUM;
  cfg.pin_pwdn     = PWDN_GPIO_NUM;
  cfg.pin_reset    = RESET_GPIO_NUM;
  cfg.xclk_freq_hz = 20000000;
  cfg.pixel_format = PIXFORMAT_JPEG;
  cfg.grab_mode    = CAMERA_GRAB_LATEST;

  if (psramFound()) {
    cfg.frame_size   = FRAME_SIZE;
    cfg.jpeg_quality = JPEG_QUALITY;
    cfg.fb_count     = 2;
    Serial.println("[CAM] PSRAM detected — high quality mode");
  } else {
    cfg.frame_size   = FRAMESIZE_CIF;   // 400x296 tanpa PSRAM
    cfg.jpeg_quality = 14;
    cfg.fb_count     = 1;
    Serial.println("[CAM] No PSRAM — low quality mode (CIF)");
  }

  esp_err_t err = esp_camera_init(&cfg);
  if (err != ESP_OK) {
    Serial.printf("[CAM] INIT FAILED error=0x%x\n", err);
    return false;
  }

  // Tune sensor
  sensor_t *s = esp_camera_sensor_get();
  if (s) {
    s->set_brightness(s, 1);
    s->set_contrast(s, 1);
    s->set_saturation(s, 0);
    s->set_whitebal(s, 1);
    s->set_awb_gain(s, 1);
    s->set_wb_mode(s, 0);
    s->set_exposure_ctrl(s, 1);
    s->set_aec2(s, 1);
    s->set_gain_ctrl(s, 1);
    s->set_gainceiling(s, (gainceiling_t)6);
    s->set_vflip(s, 0);
    s->set_hmirror(s, 0);
    Serial.println("[CAM] Sensor tuned OK");
  }

  // Quick test: capture one frame
  camera_fb_t *fb = esp_camera_fb_get();
  if (fb) {
    Serial.printf("[CAM] Test capture OK: %u bytes, %dx%d\n", fb->len, fb->width, fb->height);
    esp_camera_fb_return(fb);
  } else {
    Serial.println("[CAM] WARNING: Test capture failed!");
  }

  return true;
}

// ==================== HANDLER: GET /stream (port 81) ====================
static esp_err_t handler_stream(httpd_req_t *req) {
  Serial.println("[STREAM] Client connected — starting MJPEG");

  esp_err_t res = httpd_resp_set_type(req, STREAM_CT);
  if (res != ESP_OK) return res;

  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
  httpd_resp_set_hdr(req, "X-Framerate", "25");

  char part_buf[80];
  unsigned long frames = 0;

  while (true) {
    camera_fb_t *fb = esp_camera_fb_get();
    if (!fb) {
      Serial.println("[STREAM] Frame failed, skip");
      delay(50);
      continue;
    }

    size_t hlen = snprintf(part_buf, sizeof(part_buf), STREAM_PART, fb->len);

    res = httpd_resp_send_chunk(req, STREAM_BR, strlen(STREAM_BR));
    if (res == ESP_OK)
      res = httpd_resp_send_chunk(req, part_buf, hlen);
    if (res == ESP_OK)
      res = httpd_resp_send_chunk(req, (const char *)fb->buf, fb->len);

    esp_camera_fb_return(fb);
    frames++;

    if (res != ESP_OK) {
      Serial.printf("[STREAM] Ended after %lu frames\n", frames);
      break;
    }

    delay(40); // ~25 FPS
  }

  return res;
}

// ==================== HANDLER: GET /capture (port 80) ====================
static esp_err_t handler_capture(httpd_req_t *req) {
  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("[CAPTURE] Failed!");
    httpd_resp_send_500(req);
    return ESP_FAIL;
  }

  httpd_resp_set_type(req, "image/jpeg");
  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg");
  httpd_resp_set_hdr(req, "Cache-Control", "no-cache, no-store, must-revalidate");

  size_t len = fb->len;
  esp_err_t res = httpd_resp_send(req, (const char *)fb->buf, fb->len);
  esp_camera_fb_return(fb);

  Serial.printf("[CAPTURE] Sent %u bytes\n", len);
  return res;
}

// ==================== HANDLER: GET /flash (port 80) ====================
static esp_err_t handler_flash(httpd_req_t *req) {
  flashState = !flashState;
  digitalWrite(PIN_FLASH_LED, flashState ? HIGH : LOW);

  char resp[50];
  snprintf(resp, sizeof(resp), "{\"flash\":%s}", flashState ? "true" : "false");

  httpd_resp_set_type(req, "application/json");
  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  httpd_resp_send(req, resp, strlen(resp));

  Serial.printf("[FLASH] %s\n", flashState ? "ON" : "OFF");
  return ESP_OK;
}

// ==================== HANDLER: GET / (port 80) ====================
static esp_err_t handler_index(httpd_req_t *req) {
  String ip = WiFi.localIP().toString();

  String html = "<!DOCTYPE html><html><head>"
    "<meta charset='utf-8'><meta name='viewport' content='width=device-width'>"
    "<title>SmartBin Camera</title>"
    "<style>"
    "body{font-family:sans-serif;background:#111;color:#eee;margin:0;padding:20px;text-align:center}"
    "h1{color:#4285f4;font-size:22px}"
    "img{max-width:100%;border-radius:8px;border:2px solid #333}"
    ".info{font-size:13px;color:#888;margin:8px 0}"
    "a{color:#4285f4;text-decoration:none;padding:8px 16px;border:1px solid #4285f4;"
    "border-radius:6px;display:inline-block;margin:4px}"
    "a:hover{background:#4285f4;color:#fff}"
    "</style></head><body>"
    "<h1>SmartBin Camera — " + String(DEVICE_ID) + "</h1>"
    "<div class='info'>IP: " + ip + " | Main: port 80 | Stream: port 81</div>"
    "<div style='margin:16px 0'>"
    "<img src='http://" + ip + ":81/stream' width='640'/>"
    "</div><div>"
    "<a href='/capture' target='_blank'>Capture JPEG</a> "
    "<a href='http://" + ip + ":81/stream' target='_blank'>Open Stream</a> "
    "<a href='#' onclick='fetch(\"/flash\");return false;'>Toggle Flash</a>"
    "</div>"
    "<div class='info' style='margin-top:16px'>"
    "Di Dashboard SmartBin tab Camera, masukkan: <b>http://" + ip + "</b>"
    "</div></body></html>";

  httpd_resp_set_type(req, "text/html");
  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  httpd_resp_send(req, html.c_str(), html.length());
  return ESP_OK;
}

// ==================== Start HTTP Servers ====================
void startServers() {
  // --- Main server (port 80): /, /capture, /flash ---
  httpd_config_t cfg80 = HTTPD_DEFAULT_CONFIG();
  cfg80.server_port = 80;
  cfg80.ctrl_port   = 32768;

  if (httpd_start(&httpd_main, &cfg80) == ESP_OK) {
    httpd_uri_t u1 = {}; u1.uri = "/";        u1.method = HTTP_GET; u1.handler = handler_index;   u1.user_ctx = NULL;
    httpd_uri_t u2 = {}; u2.uri = "/capture";  u2.method = HTTP_GET; u2.handler = handler_capture; u2.user_ctx = NULL;
    httpd_uri_t u3 = {}; u3.uri = "/flash";    u3.method = HTTP_GET; u3.handler = handler_flash;   u3.user_ctx = NULL;
    httpd_register_uri_handler(httpd_main, &u1);
    httpd_register_uri_handler(httpd_main, &u2);
    httpd_register_uri_handler(httpd_main, &u3);
    Serial.println("[HTTP] Port 80 OK -> /, /capture, /flash");
  } else {
    Serial.println("[HTTP] Port 80 FAILED!");
  }

  // --- Stream server (port 81): /stream ---
  httpd_config_t cfg81 = HTTPD_DEFAULT_CONFIG();
  cfg81.server_port      = 81;
  cfg81.ctrl_port        = 32769;
  cfg81.max_uri_handlers = 1;
  cfg81.stack_size       = 8192;  // more stack for streaming

  if (httpd_start(&httpd_stream, &cfg81) == ESP_OK) {
    httpd_uri_t us = {}; us.uri = "/stream"; us.method = HTTP_GET; us.handler = handler_stream; us.user_ctx = NULL;
    httpd_register_uri_handler(httpd_stream, &us);
    Serial.println("[HTTP] Port 81 OK -> /stream (MJPEG)");
  } else {
    Serial.println("[HTTP] Port 81 FAILED!");
  }
}

// ==================== Register Camera IP with Backend ====================
void registerCameraWithBackend(const char *cameraIp) {
  Serial.printf("[CAM-REG] Registering camera at %s...\n", cameraIp);
  cameraRegistered = false;
  
  WiFiClient client;
  client.setTimeout(2000);
  if (!client.connect(BACKEND_HOST, BACKEND_PORT)) {
    Serial.println("[CAM-REG] Failed to connect to backend!");
    return;
  }

  // Prepare JSON payload
  String payload = "{\"tenantId\":\"" + String(TENANT_ID) + "\","
                   "\"siteId\":\"" + String(SITE_ID) + "\","
                   "\"deviceId\":\"" + String(DEVICE_ID) + "\","
                   "\"cameraIp\":\"http://" + String(cameraIp) + "\"}";

  // Send POST request
  String request = String("POST /api/v1/ingest/camera HTTP/1.1\r\n")
                   + "Host: " + String(BACKEND_HOST) + ":" + String(BACKEND_PORT) + "\r\n"
                   "Content-Type: application/json\r\n"
                   "Content-Length: " + String(payload.length()) + "\r\n"
                   "Connection: close\r\n"
                   "\r\n" + payload;

  client.print(request);

  // Read response
  unsigned long deadline = millis() + 2500;
  while (client.connected() && millis() < deadline) {
    if (client.available()) {
      String line = client.readStringUntil('\n');
      if (line.indexOf("200") > 0 || line.indexOf("\"ok\":true") > 0) {
        Serial.println("[CAM-REG] ✓ Camera registered successfully!");
        cameraRegistered = true;
        break;
      }
    }
    delay(10);
  }

  client.stop();
}

void retryCameraRegistration() {
  if (cameraRegistered || WiFi.status() != WL_CONNECTED) return;
  if (millis() - lastRegisterAttempt < 10000) return;
  lastRegisterAttempt = millis();
  String ip = WiFi.localIP().toString();
  registerCameraWithBackend(ip.c_str());
}

// ==================== WiFi ====================
void connectWiFi() {
  Serial.printf("[WiFi] Connecting to '%s'...\n", WIFI_SSID);
  WiFi.mode(WIFI_STA);
  WiFi.setAutoReconnect(true);
  WiFi.persistent(true);
  WiFi.setSleep(false);

  // === STATIC IP (PERMANENT — tidak berubah setelah restart) ===
#if USE_STATIC_IP
  IPAddress ip(STATIC_IP);
  IPAddress gw(STATIC_GATEWAY);
  IPAddress sn(STATIC_SUBNET);
  IPAddress dns(STATIC_DNS);
  if (!WiFi.config(ip, gw, sn, dns)) {
    Serial.println("[WiFi] Static IP config GAGAL!");
  } else {
    Serial.printf("[WiFi] Static IP: %s  GW: %s\n", ip.toString().c_str(), gw.toString().c_str());
  }
#else
  Serial.println("[WiFi] DHCP mode");
#endif

  WiFi.begin(WIFI_SSID, WIFI_PASS);

  int n = 0;
  while (WiFi.status() != WL_CONNECTED && n < 30) {
    delay(500);
    Serial.print(".");
    n++;
  }

  if (WiFi.status() == WL_CONNECTED) {
    String ipStr = WiFi.localIP().toString();
    Serial.printf("\n[WiFi] CONNECTED! IP=%s RSSI=%d dBm\n", ipStr.c_str(), WiFi.RSSI());
  } else {
    Serial.println("\n[WiFi] GAGAL KONEK!");
  }
}

// ==================== SETUP ====================
void setup() {
  Serial.begin(115200);
  delay(1000);

  Serial.println("\n=============================================");
  Serial.println("  IoT SmartBin — ESP32-CAM v2.0");
  Serial.println("  esp_http_server — MJPEG Streaming");
  Serial.println("=============================================");
  Serial.printf("Device: %s | WiFi: %s\n", DEVICE_ID, WIFI_SSID);
  Serial.printf("PSRAM: %s | Free heap: %d bytes\n",
    psramFound() ? "YES" : "NO", ESP.getFreeHeap());

  // Flash LED setup
  pinMode(PIN_FLASH_LED, OUTPUT);
  digitalWrite(PIN_FLASH_LED, LOW);

  // Init camera
  Serial.println("\n[CAM] Initializing camera...");
  if (!initCamera()) {
    Serial.println("[FATAL] Camera init GAGAL!");
    Serial.println("[FATAL] Cek ribbon cable / board type.");
    // Restart setelah 5 detik (jangan blink flash LED — silau!)
    delay(5000);
    ESP.restart();
  }

  // Connect WiFi
  connectWiFi();
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[WiFi] Router belum siap. Camera tetap hidup dan akan mencoba lagi.");
  }

  // Start HTTP servers before backend registration so the camera remains usable
  // even if the dashboard backend is offline or slow to respond.
  startServers();

  String ip = WiFi.localIP().toString();
  if (WiFi.status() == WL_CONNECTED) {
    lastRegisterAttempt = millis();
    registerCameraWithBackend(ip.c_str());
  }

  // Print access info
  Serial.println("\n=============================================");
  Serial.println("  CAMERA READY!");
  Serial.printf("  Status:  http://%s\n", ip.c_str());
  Serial.printf("  Stream:  http://%s:81/stream\n", ip.c_str());
  Serial.printf("  Capture: http://%s/capture\n", ip.c_str());
  Serial.printf("  Flash:   http://%s/flash\n", ip.c_str());
  Serial.printf("  Backend: %s\n", BACKEND_URL);
  Serial.println("=============================================\n");

  // Ready — flash LED tetap OFF
  Serial.println("[INIT] Camera READY — flash LED OFF");
}

// ==================== LOOP ====================
void loop() {
  // Status print every 10 seconds
  static unsigned long lastPrint = 0;
  if (millis() - lastPrint > 10000) {
    lastPrint = millis();
    Serial.printf("[CAM] uptime=%lus WiFi=%s IP=%s RSSI=%d heap=%d\n",
      millis() / 1000,
      WiFi.status() == WL_CONNECTED ? "OK" : "LOST",
      WiFi.localIP().toString().c_str(),
      WiFi.RSSI(),
      ESP.getFreeHeap());
  }

  // WiFi recovery
  if (WiFi.status() != WL_CONNECTED) {
    if (millis() - lastWiFiAttempt >= 10000) {
      lastWiFiAttempt = millis();
      cameraRegistered = false;
      Serial.println("[WiFi] Belum terhubung; mencoba kembali tanpa restart camera...");
      WiFi.reconnect();
    }
  } else {
    retryCameraRegistration();
  }

  delay(100);
}
