/*
 * terraview — ESP-IDF firmware (the real one).
 * On each wake: read KY-015 (DHT11) -> snap a JPEG -> WiFi -> POST to the Worker
 * -> deep-sleep. Same pipeline as the Arduino sketch we designed, in C.
 *
 * The camera works because this project vendors the PATCHED esp32-camera driver
 * (retry marginal SCCB writes + tolerate the OV3660 clone's NAKs) — see
 * components/esp32-camera/driver/sccb.c.
 *
 * Talks to api/src/index.ts:
 *   POST {WORKER_URL}/frame?temp=<C>&rh=<%>   Bearer token, image/jpeg body.
 *   The Worker stamps the time; the ESP has no clock and never needs one.
 */
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "esp_rom_sys.h"           // esp_rom_delay_us
#include "esp_sleep.h"
#include "nvs_flash.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_http_client.h"
#include "esp_crt_bundle.h"
#include "driver/gpio.h"
#include "esp_camera.h"
#include "config.h"

static const char *TAG = "terraview";

// ---- AI-Thinker ESP32-CAM pins ----
#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

// ---- deep sleep ----
static void deep_sleep(void)
{
    uint64_t us = (uint64_t)SLEEP_MINUTES * 60ULL * 1000000ULL;
    esp_sleep_enable_timer_wakeup(us);
    ESP_LOGI(TAG, "sleeping %d min", SLEEP_MINUTES);
    esp_deep_sleep_start();  // never returns; next wake restarts app_main
}

// ---- KY-015 / DHT11 bit-bang read ----
// Returns true on a good read. The KY-015 module has an onboard pull-up.
static bool dht11_read(gpio_num_t pin, float *temp_c, float *rh)
{
    uint8_t d[5] = {0};

    // Start: pull low >=18ms, high ~30us, then release to input.
    gpio_set_direction(pin, GPIO_MODE_OUTPUT);
    gpio_set_level(pin, 0);
    esp_rom_delay_us(20000);
    gpio_set_level(pin, 1);
    esp_rom_delay_us(30);
    gpio_set_direction(pin, GPIO_MODE_INPUT);

    portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
    portENTER_CRITICAL(&mux);
    bool ok = true;
    int64_t t0;

    // Response: DHT drives ~80us low then ~80us high. Wait through both.
    t0 = esp_timer_get_time(); while (gpio_get_level(pin) == 1) if (esp_timer_get_time() - t0 > 200) { ok = false; goto done; }
    t0 = esp_timer_get_time(); while (gpio_get_level(pin) == 0) if (esp_timer_get_time() - t0 > 200) { ok = false; goto done; }
    t0 = esp_timer_get_time(); while (gpio_get_level(pin) == 1) if (esp_timer_get_time() - t0 > 200) { ok = false; goto done; }

    // 40 bits: each is ~50us low, then high 26-28us (0) or ~70us (1).
    for (int i = 0; i < 40; i++) {
        t0 = esp_timer_get_time(); while (gpio_get_level(pin) == 0) if (esp_timer_get_time() - t0 > 200) { ok = false; goto done; }
        t0 = esp_timer_get_time(); while (gpio_get_level(pin) == 1) if (esp_timer_get_time() - t0 > 200) break;
        int64_t high_us = esp_timer_get_time() - t0;
        d[i / 8] <<= 1;
        if (high_us > 45) d[i / 8] |= 1;   // long high pulse = 1
    }
done:
    portEXIT_CRITICAL(&mux);
    if (!ok) return false;

    if ((uint8_t)(d[0] + d[1] + d[2] + d[3]) != d[4]) return false;  // checksum
    *rh     = d[0] + d[1] * 0.1f;
    *temp_c = d[2] + d[3] * 0.1f;
    return true;
}

// ---- camera ----
static bool init_camera(void)
{
    camera_config_t config = {
        .pin_pwdn = PWDN_GPIO_NUM, .pin_reset = RESET_GPIO_NUM,
        .pin_xclk = XCLK_GPIO_NUM,
        .pin_sccb_sda = SIOD_GPIO_NUM, .pin_sccb_scl = SIOC_GPIO_NUM,
        .pin_d7 = Y9_GPIO_NUM, .pin_d6 = Y8_GPIO_NUM, .pin_d5 = Y7_GPIO_NUM, .pin_d4 = Y6_GPIO_NUM,
        .pin_d3 = Y5_GPIO_NUM, .pin_d2 = Y4_GPIO_NUM, .pin_d1 = Y3_GPIO_NUM, .pin_d0 = Y2_GPIO_NUM,
        .pin_vsync = VSYNC_GPIO_NUM, .pin_href = HREF_GPIO_NUM, .pin_pclk = PCLK_GPIO_NUM,
        .xclk_freq_hz = 10000000,       // 10MHz: slower pixel clock, cleaner DVP sampling
        .ledc_timer = LEDC_TIMER_0, .ledc_channel = LEDC_CHANNEL_0,
        .pixel_format = PIXFORMAT_JPEG,
        .frame_size = FRAMESIZE_VGA,    // 640x480: comfortable for the ESP32's DMA
        .jpeg_quality = 12,
        .fb_count = 2,
        .grab_mode = CAMERA_GRAB_LATEST,
        .fb_location = CAMERA_FB_IN_PSRAM,
    };
    esp_err_t err = esp_camera_init(&config);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "camera init failed: 0x%x (%s)", err, esp_err_to_name(err));
        return false;
    }
    return true;
}

// ---- WiFi ----
static EventGroupHandle_t s_wifi_events;
#define WIFI_OK_BIT   BIT0
#define WIFI_FAIL_BIT BIT1
static int s_retries = 0;

static void wifi_evt(void *arg, esp_event_base_t base, int32_t id, void *data)
{
    if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
        if (s_retries++ < 10) esp_wifi_connect();
        else xEventGroupSetBits(s_wifi_events, WIFI_FAIL_BIT);
    } else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
        s_retries = 0;
        xEventGroupSetBits(s_wifi_events, WIFI_OK_BIT);
    }
}

static bool wifi_connect(void)
{
    s_wifi_events = xEventGroupCreate();
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_evt, NULL, NULL));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_evt, NULL, NULL));
    wifi_config_t wc = {0};
    strncpy((char *)wc.sta.ssid, WIFI_SSID, sizeof(wc.sta.ssid) - 1);
    strncpy((char *)wc.sta.password, WIFI_PASS, sizeof(wc.sta.password) - 1);
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wc));
    ESP_ERROR_CHECK(esp_wifi_start());
    EventBits_t bits = xEventGroupWaitBits(s_wifi_events, WIFI_OK_BIT | WIFI_FAIL_BIT,
                                           pdFALSE, pdFALSE, pdMS_TO_TICKS(20000));
    if (bits & WIFI_OK_BIT) { ESP_LOGI(TAG, "wifi connected"); return true; }
    ESP_LOGW(TAG, "wifi failed — will retry next wake");
    return false;
}

// ---- upload ----
static void upload_frame(camera_fb_t *fb, float temp, float rh, bool have_dht)
{
    char url[192];
    if (have_dht)
        snprintf(url, sizeof(url), "%s/frame?temp=%.1f&rh=%.0f", WORKER_URL, temp, rh);
    else
        snprintf(url, sizeof(url), "%s/frame", WORKER_URL);

    char auth[96];
    snprintf(auth, sizeof(auth), "Bearer %s", INGEST_TOKEN);

    esp_http_client_config_t cfg = {
        .url = url,
        .method = HTTP_METHOD_POST,
        .crt_bundle_attach = esp_crt_bundle_attach,   // validate Cloudflare's cert
        .timeout_ms = 15000,
    };
    esp_http_client_handle_t c = esp_http_client_init(&cfg);
    esp_http_client_set_header(c, "Content-Type", "image/jpeg");
    esp_http_client_set_header(c, "Authorization", auth);
    esp_http_client_set_post_field(c, (const char *)fb->buf, fb->len);

    esp_err_t err = esp_http_client_perform(c);
    int status = esp_http_client_get_status_code(c);
    ESP_LOGI(TAG, "POST -> err=%s status=%d", esp_err_to_name(err), status);
    esp_http_client_cleanup(c);
}

void app_main(void)
{
    ESP_LOGI(TAG, "== terraview wake ==");

    // Camera FIRST — nothing runs before init (the DHT read's critical section
    // and nvs_flash_init were disturbing the camera clock/DMA setup and causing
    // capture timeouts; the proven probe inits the camera before anything else).
    // Marginal OV3660 clone: init is a per-boot gamble, AND the first frame is
    // dark until auto-exposure settles. So retry init until the sensor streams,
    // then turn on auto exposure/gain/white-balance, let it converge, discard a
    // few warm-up frames, and keep a properly-exposed one.
    camera_fb_t *fb = NULL;
    for (int attempt = 1; attempt <= 8 && !fb; attempt++) {
        if (!init_camera()) { esp_camera_deinit(); vTaskDelay(pdMS_TO_TICKS(200)); continue; }

        camera_fb_t *probe = esp_camera_fb_get();   // confirm the sensor streams
        if (!probe) {
            ESP_LOGW(TAG, "camera attempt %d not streaming; re-init", attempt);
            esp_camera_deinit(); vTaskDelay(pdMS_TO_TICKS(200));
            continue;
        }
        esp_camera_fb_return(probe);

        sensor_t *s = esp_camera_sensor_get();
        if (s) {
            s->set_gain_ctrl(s, 1);      // auto gain
            s->set_exposure_ctrl(s, 1);  // auto exposure (brightens the dim tank)
            s->set_whitebal(s, 1);       // auto white balance
            s->set_brightness(s, 1);     // nudge a touch brighter (-2..2)
        }
        vTaskDelay(pdMS_TO_TICKS(800));  // let AEC/AGC converge
        for (int i = 0; i < 5; i++) {    // throw away warm-up frames
            camera_fb_t *w = esp_camera_fb_get();
            if (w) esp_camera_fb_return(w);
        }
        fb = esp_camera_fb_get();        // the keeper
        if (!fb) { esp_camera_deinit(); vTaskDelay(pdMS_TO_TICKS(200)); }
    }
    if (!fb) { ESP_LOGE(TAG, "capture failed after 8 tries"); deep_sleep(); }
    ESP_LOGI(TAG, "frame: %u bytes (bigger = better-exposed)", (unsigned)fb->len);

    // Now read the sensor and go online, with the image already in hand.
    esp_err_t r = nvs_flash_init();   // WiFi needs NVS
    if (r == ESP_ERR_NVS_NO_FREE_PAGES || r == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        nvs_flash_erase();
        nvs_flash_init();
    }

    float temp = 0, rh = 0;
    bool have_dht = false;
    for (int i = 0; i < 4 && !have_dht; i++) {   // DHT11 is flaky on a single read
        have_dht = dht11_read(DHT_GPIO, &temp, &rh);
        if (!have_dht) vTaskDelay(pdMS_TO_TICKS(1200));  // it needs ~1s between reads
    }
    if (have_dht) ESP_LOGI(TAG, "sensor: %.1f C  %.0f %%", temp, rh);
    else          ESP_LOGW(TAG, "dht read failed after retries (frame still uploads, temp/rh null)");

    if (wifi_connect()) upload_frame(fb, temp, rh, have_dht);
    esp_camera_fb_return(fb);

    deep_sleep();
}
