/*
 * UV Wearable — rev 4 firmware skeleton
 *
 * Target:   ESP32-C3 SuperMini (Arduino ESP32 core 2.x+)
 * Sensors:  VEML6075 UVA/UVB over I2C
 * Storage:  micro SD (SPI) — append-only CSV
 * Sync:     gesture-selected — 1 tap = BLE (iOS), 2 taps = WiFi (HTTP POST to Pi)
 * Sleep:    light sleep between samples
 *
 * CSV format on SD:
 *   boot_id,ms_since_boot,uva,uvb,comp1,comp2,batt_mv
 *   EVENT rows:  boot_id,ms_since_boot,EVENT,<label>
 *
 * Time is reconstructed server-side: each sync call carries the
 * device's current millis(), and the server's wall-clock at sync
 * time anchors the whole run. No RTC on the device.
 */

#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <Preferences.h>
#include "esp_sleep.h"
#include "ble_sync.h"

// ---- Pinout (matches rev4 schematic) ----
#define I2C_SDA        8
#define I2C_SCL        9
#define SPI_SCK        4
#define SPI_MISO       5
#define SPI_MOSI       6
#define SD_CS          7
#define PIN_BUTTON    10
#define PIN_BAT_ADC    3
#define PIN_LED_STATUS 20 // active-high pink status LED via 100Ω to GND

// ---- Timing ----
// Sync is button-only — the device never wakes the radio on its own. Gestures:
//   single short tap (release < EVENT_MS, no second tap)  : BLE advertise window
//   double tap (second press within DOUBLE_TAP_MS)        : WiFi sync to Pi
//   long press (held >= EVENT_MS)                         : log user_mark, one blink
// Tap count chooses radio because iOS-side BLE bring-up doesn't want a 15 s
// WiFi timeout in front of every BLE window, and home WiFi sync doesn't want
// to be gated behind a BLE pairing prompt that won't come.
#define SAMPLE_INTERVAL_MS   (5UL  * 60UL * 1000UL)   // 5 min
#define BUTTON_EVENT_MS      3000
#define BUTTON_DEBOUNCE_MS   50
#define BUTTON_DOUBLE_TAP_MS 600   // window after first release to register a second tap
#define WIFI_TIMEOUT_MS      15000
#define BLE_WINDOW_MS        60000   // bumped from 20s: enough headroom for a full SD-log drain at MTU=23 (~50 chunks × ~1s = ~50s worst case)

// ---- Secrets ----
// Real values live in secrets.h (gitignored). Copy secrets.h.example to
// secrets.h and fill in before flashing.
#include "secrets.h"

// ---- VEML6075 registers ----
#define VEML_ADDR      0x10
#define VEML_REG_CONF  0x00
#define VEML_REG_UVA   0x07
#define VEML_REG_UVB   0x09
#define VEML_REG_COMP1 0x0A
#define VEML_REG_COMP2 0x0B

// ---- State ----
Preferences prefs;
uint32_t boot_id      = 0;
uint32_t last_samp_ms = 0;

// ===================================================================
void setup() {
  Serial.begin(115200);
  delay(100);

  // Increment boot_id in NVS (survives power-off, distinguishes runs)
  prefs.begin("uvwear", false);
  boot_id = prefs.getUInt("boot_id", 0) + 1;
  prefs.putUInt("boot_id", boot_id);
  prefs.end();
  Serial.printf("Boot #%u\n", boot_id);

  pinMode(PIN_BUTTON, INPUT_PULLUP);
  pinMode(PIN_LED_STATUS, OUTPUT);
  ledOff();

  // C3 ADC defaults to 10-bit at boot; force 12-bit so readBatteryMv()'s
  // /4095 divisor matches the actual range. Without this, batt_mv reads ~1/4
  // of true voltage (raw 0..1023 over a /4095 denominator).
  analogReadResolution(12);

  Wire.begin(I2C_SDA, I2C_SCL);
  Wire.setClock(100000);
  vemlInit();

  SPI.begin(SPI_SCK, SPI_MISO, SPI_MOSI, SD_CS);
  if (!SD.begin(SD_CS)) {
    Serial.println("SD init FAILED — check wiring/card");
  } else {
    Serial.println("SD OK");
  }

  bleSyncInit(boot_id, "/uv_log.csv", UV_API_KEY);
}

// ===================================================================
void loop() {
  // Print the resting button state every loop — if this reads 0 with nothing
  // pressed, the line is stuck LOW (broken switch, short to GND, defeated
  // pull-up) and handleButton() would otherwise hang forever in its press
  // loop. Flush so we see this even if something downstream crashes.
  Serial.printf("[T] loop top  btn=%d\n", digitalRead(PIN_BUTTON));
  Serial.flush();
  handleButton();

  uint32_t now = millis();
  if (now - last_samp_ms >= SAMPLE_INTERVAL_MS || last_samp_ms == 0) {
    Serial.println("[T] sample begin");
    ledOff();  // VEML6075 COMP1 is visible-light — keep LED dark while sampling
    sampleAndLog();
    Serial.println("[T] sample end");
    last_samp_ms = millis();
  }

  // No background sync — the device only talks to the network when the
  // user holds the button for >= BUTTON_SYNC_MS. Sleep until the next
  // sample. Button press wakes via gpio_wakeup.
  uint32_t until_samp = SAMPLE_INTERVAL_MS - (millis() - last_samp_ms);
  Serial.printf("[T] sleep_ms=%lu\n", (unsigned long)until_samp);
  Serial.flush();  // make sure the trace lands before we sleep
  ledOff();  // light_sleep holds GPIO state — don't burn current driving the LED
  if (until_samp > 100) lightSleep(until_samp);
  Serial.println("[T] woke");
}

// ===================================================================
// VEML6075 — minimal direct I2C (no library dependency).
// Active draw is ~85µA continuously. Across 24h that's ~7 mAh, which
// is small but free to recover: we hold it in shutdown between samples
// (SD=1, <1µA) and wake it just before each read. The 150ms wait in
// vemlWake() covers one 100ms integration cycle so the four channel
// registers latch real values before we read them.
// ===================================================================
void vemlShutdown() {
  Wire.beginTransmission(VEML_ADDR);
  Wire.write(VEML_REG_CONF);
  Wire.write(0x11);  // UV_IT=001 (100ms), SD=1 (shutdown)
  Wire.write(0x00);
  Wire.endTransmission();
}

void vemlWake() {
  Wire.beginTransmission(VEML_ADDR);
  Wire.write(VEML_REG_CONF);
  Wire.write(0x10);  // UV_IT=001, SD=0 (active)
  Wire.write(0x00);
  Wire.endTransmission();
  delay(150);
}

void vemlInit() {
  // Configure timing and immediately drop to shutdown — first sample
  // (last_samp_ms == 0 in loop) will wake it for the read.
  vemlShutdown();
}

// Returns true and writes the channel value to *out on success. On any I2C
// failure (no ACK, short read) returns false and leaves *out untouched, so the
// caller can skip the whole sample instead of logging garbage.
//
// The (uint8_t) casts are load-bearing: Wire.read() returns int -1 when no byte
// is available, and -1 stuffed into a uint16_t is 0xFFFF (65535) — that's the
// exact railing bug that painted the flat 131-UVI line. Checking
// endTransmission()/requestFrom() catches the failure; the casts are a second
// line of defense so a stray -1 can never reach *out.
bool vemlRead(uint8_t reg, uint16_t* out) {
  Wire.beginTransmission(VEML_ADDR);
  Wire.write(reg);
  if (Wire.endTransmission(false) != 0) return false;   // address/repeated-start NACK
  if (Wire.requestFrom(VEML_ADDR, (uint8_t)2) < 2) return false;
  if (Wire.available() < 2) return false;
  uint16_t lo = (uint8_t)Wire.read();
  uint16_t hi = (uint8_t)Wire.read();
  *out = (uint16_t)((hi << 8) | lo);
  return true;
}

// ===================================================================
// Status LED — active-high on GP20. Blink state must survive across
// calls inside tight wait loops, so the cadence state is file-scope.
// ledOff() resets it so the next ledTick() starts cleanly from off.
//
// led_enabled gates ledSolid/ledTick — attemptSync() flips it on at
// entry and off at exit, so the LED only animates during the button-
// initiated sync session. Sampling and idle stay dark. ledOff() remains
// unconditional so callers can force the pin low regardless of state.
// ledBlinkOnce() also bypasses the flag for one-shot confirmations.
// ===================================================================
static uint32_t led_last_ms  = 0;
static bool     led_blink_on = false;
static bool     led_enabled  = false;

void ledOff() {
  digitalWrite(PIN_LED_STATUS, LOW);
  led_blink_on = false;
}

void ledEnable(bool on) {
  led_enabled = on;
  if (!on) ledOff();
}

void ledSolid() {
  if (!led_enabled) return;
  digitalWrite(PIN_LED_STATUS, HIGH);
}

void ledTick(uint32_t half_period_ms) {
  if (!led_enabled) {
    ledOff();
    return;
  }
  uint32_t now = millis();
  if (now - led_last_ms >= half_period_ms) {
    led_last_ms  = now;
    led_blink_on = !led_blink_on;
    digitalWrite(PIN_LED_STATUS, led_blink_on ? HIGH : LOW);
  }
}

// One-shot blink used as confirmation feedback (e.g. event mark). Bypasses
// led_enabled because this is the *acknowledgement* — it should always show
// regardless of whether a sync session is active.
void ledBlinkOnce(uint32_t on_ms) {
  digitalWrite(PIN_LED_STATUS, HIGH);
  delay(on_ms);
  digitalWrite(PIN_LED_STATUS, LOW);
}

// ===================================================================
// Battery monitor (100k/100k divider → GPIO3 ADC)
// ===================================================================
uint32_t readBatteryMv() {
  int raw = analogRead(PIN_BAT_ADC);
  // C3 ADC default: 12-bit, ~3.3V full-scale with 11dB attenuation
  // V_bat = 2 × V_adc (divider is 1:1 with two 100k)
  return (uint32_t)raw * 3300UL * 2UL / 4095UL;
}

// ===================================================================
// Logging
// ===================================================================
void sampleAndLog() {
  vemlWake();
  uint16_t uva, uvb, comp1, comp2;
  // All four reads share one bus burst; if any fails, the whole burst is
  // suspect. Short-circuit && stops at the first failure.
  bool ok = vemlRead(VEML_REG_UVA,   &uva)
         && vemlRead(VEML_REG_UVB,   &uvb)
         && vemlRead(VEML_REG_COMP1, &comp1)
         && vemlRead(VEML_REG_COMP2, &comp2);
  uint32_t batmv = readBatteryMv();
  vemlShutdown();

  if (!ok) {
    // I2C read failed — skip the sample rather than log 0xFFFF railing. Leave a
    // marker row so the fault is visible in the data (query event_label) rather
    // than showing up as a silent gap.
    Serial.println("[W] VEML read failed — sample skipped");
    logEvent("sensor_i2c_fail");
    return;
  }

  File f = SD.open("/uv_log.csv", FILE_APPEND);
  if (f) {
    f.printf("%u,%lu,%u,%u,%u,%u,%lu\n",
             boot_id, millis(), uva, uvb, comp1, comp2, batmv);
    f.close();
  }
}

void logEvent(const char* label) {
  File f = SD.open("/uv_log.csv", FILE_APPEND);
  if (f) {
    f.printf("%u,%lu,EVENT,%s\n", boot_id, millis(), label);
    f.close();
  }
}

// ===================================================================
// Button — tap-count gesture detector:
//   long press (held >= EVENT_MS)               : log user_mark, one blink, done
//   single short tap                            : BLE advertise window (iOS sync)
//   double tap (2nd press within DOUBLE_TAP_MS) : WiFi sync to Pi
//
// Blocks for the full duration of the press (and the double-tap wait window).
// loop() drops straight back into light_sleep after one handleButton() call,
// and release (LOW → HIGH) does not wake the device (wake is GPIO_INTR_LOW_LEVEL).
// So we have to stay in this function until the gesture fully resolves —
// otherwise the second tap of a double-tap would be a fresh wake with no
// memory of the first.
//
// Radio mode is chosen by tap count rather than press length so iOS BLE
// bring-up doesn't wait out the 15 s WiFi timeout each press, and home WiFi
// sync isn't gated behind a BLE pairing prompt that won't come.
// ===================================================================
// Upper bound on how long we'll wait for a press to release. Real presses are
// <10 s (BUTTON_EVENT_MS is 3 s). If we exceed this, the line is stuck LOW —
// bail out instead of hanging the device. Picked well above EVENT_MS so a
// genuinely long press still completes its event-mark logging path.
#define BUTTON_HOLD_MAX_MS 10000

void handleButton() {
  if (digitalRead(PIN_BUTTON) != LOW) return;

  // Debounce — re-check after the noise window before committing to a press.
  delay(BUTTON_DEBOUNCE_MS);
  if (digitalRead(PIN_BUTTON) != LOW) return;

  Serial.println("[BTN] press detected");
  // Drive LED solid as immediate visual feedback — "device sees your press".
  // Bypasses led_enabled because the sync session hasn't started yet.
  digitalWrite(PIN_LED_STATUS, HIGH);

  uint32_t press_start = millis();
  bool     event_fired = false;
  bool     stuck       = false;

  while (digitalRead(PIN_BUTTON) == LOW) {
    uint32_t held = millis() - press_start;
    if (!event_fired && held >= BUTTON_EVENT_MS) {
      logEvent("user_mark");
      ledBlinkOnce(200);
      digitalWrite(PIN_LED_STATUS, HIGH);  // back to solid after the blink
      event_fired = true;
    }
    if (held >= BUTTON_HOLD_MAX_MS) {
      stuck = true;
      break;
    }
    delay(20);
  }

  digitalWrite(PIN_LED_STATUS, LOW);

  if (stuck) {
    Serial.printf("[BTN] STUCK LOW — bailing after %lu ms (check wiring on GP%d)\n",
                  (unsigned long)(millis() - press_start), PIN_BUTTON);
    return;
  }

  uint32_t held = millis() - press_start;
  Serial.printf("[BTN] release after %lu ms (event_fired=%d)\n",
                (unsigned long)held, event_fired ? 1 : 0);

  if (event_fired) return;  // long press already handled, ignore release

  // First tap released — watch for a second tap to upgrade BLE → WiFi.
  uint32_t wait_start = millis();
  bool second_tap = false;
  while (millis() - wait_start < BUTTON_DOUBLE_TAP_MS) {
    if (digitalRead(PIN_BUTTON) == LOW) {
      delay(BUTTON_DEBOUNCE_MS);
      if (digitalRead(PIN_BUTTON) == LOW) {
        second_tap = true;
        // Wait for release with the same stuck-LOW guard.
        uint32_t t = millis();
        while (digitalRead(PIN_BUTTON) == LOW) {
          if (millis() - t >= BUTTON_HOLD_MAX_MS) {
            Serial.println("[BTN] 2nd tap STUCK LOW — bailing");
            return;
          }
          delay(20);
        }
        break;
      }
    }
    delay(10);
  }

  if (second_tap) {
    Serial.println("[BTN] double tap → WiFi");
    attemptWiFiSync();
  } else {
    Serial.println("[BTN] single tap → BLE");
    attemptBleSync();
  }
}

// ===================================================================
// Sync — split by gesture. Each mode is strict: single tap runs BLE
// only, double tap runs WiFi only. No fallback between them — if a
// mode fails, the user picks again with the next gesture. No retry
// backoff either: the user is the retry trigger.
// ===================================================================
void attemptBleSync() {
  Serial.println("BLE sync (1 tap)…");
  ledEnable(true);
  bool ble_ok = bleSyncRunWindow(BLE_WINDOW_MS);
  (void)ble_ok;
  ledEnable(false);
}

void attemptWiFiSync() {
  Serial.println("WiFi sync (2 taps)…");
  ledEnable(true);

  WiFi.mode(WIFI_STA);
  Serial.printf("  Connecting to SSID '%s'…\n", WIFI_SSID);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  uint32_t t0 = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - t0 < WIFI_TIMEOUT_MS) {
    ledTick(250);  // 2Hz blink while associating
    delay(250);
    Serial.print(".");
  }
  Serial.println();

  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("  WiFi up, IP=%s, RSSI=%d dBm, took %lu ms\n",
                  WiFi.localIP().toString().c_str(), WiFi.RSSI(),
                  (unsigned long)(millis() - t0));
    ledSolid();  // solid through the drain
    syncViaWiFi();
    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
  } else {
    // Status codes worth knowing:
    //   1 WL_NO_SSID_AVAIL   — network not visible (out of range / wrong SSID)
    //   4 WL_CONNECT_FAILED  — usually wrong password
    //   6 WL_DISCONNECTED    — gave up / no response
    Serial.printf("  WiFi unreachable, status=%d after %lu ms\n",
                  WiFi.status(), (unsigned long)(millis() - t0));
  }

  ledEnable(false);
}

bool syncViaWiFi() {
  // Drain the SD log in 4KB chunks until EOF, hitting one of:
  //   - file exhausted (good — fully caught up)
  //   - per-cycle chunk count cap (8 chunks = 32KB ≈ 2 days of samples)
  //   - per-cycle wall-clock cap (30s — keep radio-on time bounded)
  //   - a chunk POST fails (next cycle resumes from same sync_pos)
  // Each chunk is its own POST and advances sync_pos only on HTTP 200, so a
  // mid-drain failure costs at most one chunk's worth of resend.
  // Returns true if at least one chunk uploaded OR the file was already drained.
  //
  // TLS without cert verification — encrypts the wire so the bearer token
  // doesn't travel cleartext over the public internet, while skipping the
  // root-CA maintenance burden. Acceptable for a personal device; not for
  // production multi-user deployments.
  WiFiClientSecure client;
  client.setInsecure();

  prefs.begin("uvwear", false);

  const int      MAX_CHUNKS  = 8;
  const uint32_t MAX_WALL_MS = 30000;
  uint32_t t0 = millis();
  bool any_uploaded = false;
  bool drained      = false;

  for (int chunk = 0; chunk < MAX_CHUNKS; chunk++) {
    if (millis() - t0 > MAX_WALL_MS) {
      Serial.println("  sync wall-clock cap hit");
      break;
    }

    File f = SD.open("/uv_log.csv", FILE_READ);
    if (!f) break;

    uint32_t last_pos = prefs.getUInt("sync_pos", 0);
    f.seek(last_pos);

    String payload;
    payload.reserve(4096);
    while (f.available() && payload.length() < 4096) {
      payload += (char)f.read();
    }
    uint32_t new_pos = f.position();
    f.close();

    if (payload.length() == 0) {
      drained = true;
      break;
    }

    HTTPClient http;
    http.begin(client, SYNC_URL);
    http.addHeader("Content-Type", "text/csv");
    http.addHeader("Authorization", String("Bearer ") + UV_API_KEY);
    http.addHeader("X-Boot-Id", String(boot_id));
    http.addHeader("X-Device-Ms", String(millis()));
    int code = http.POST(payload);
    http.end();
    Serial.printf("  chunk %d: %u bytes -> HTTP %d\n",
                  chunk, (unsigned)payload.length(), code);

    if (code != 200) {
      Serial.println("  chunk failed — resume next sync");
      break;
    }

    prefs.putUInt("sync_pos", new_pos);
    any_uploaded = true;
  }

  prefs.end();
  return any_uploaded || drained;
}

// ===================================================================
// Light sleep — timer + button wake
// ===================================================================
void lightSleep(uint32_t ms) {
  esp_sleep_enable_timer_wakeup((uint64_t)ms * 1000ULL);
  gpio_wakeup_enable((gpio_num_t)PIN_BUTTON, GPIO_INTR_LOW_LEVEL);
  esp_sleep_enable_gpio_wakeup();
  esp_light_sleep_start();
}
