﻿#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <TinyGPSPlus.h>
#include <time.h>

// ====== CONFIG ======
const char* WIFI_SSID = "ESP";
const char* WIFI_PASSWORD = "12345678";

const char* FIREBASE_API_KEY = "AIzaSyAEP6kxjgffI-dgdnlJXPF0pCk7VkFqOB4";
const char* FIREBASE_DB_URL = "https://water-monitoring-74d04-default-rtdb.firebaseio.com";
const char* DEVICE_ID = "esp32-gps-1";

// NEO-8M wiring (ESP32):
// NEO TX -> ESP32 RX2 (GPIO16)
// NEO RX -> ESP32 TX2 (GPIO17) [optional]
#define GPS_RX_PIN 16
#define GPS_TX_PIN 17
#define GPS_BAUD 9600

const unsigned long REQUEST_POLL_MS = 1200;
const char* GPS_REQUEST_PATH = "/gpsRequests/current.json";
const char* GPS_RESPONSE_PATH = "/gpsResponses/current.json";

HardwareSerial GPSSerial(2);
TinyGPSPlus gps;

String idToken = "";
String refreshToken = "";
unsigned long tokenExpiryMs = 0;
unsigned long lastPollMs = 0;
String lastHandledRequestId = "";

bool httpPOST(const String& url, const String& body, const String& contentType, String& response, int& code) {
  WiFiClientSecure client;
  client.setInsecure();

  HTTPClient http;
  if (!http.begin(client, url)) return false;
  http.addHeader("Content-Type", contentType);
  code = http.POST(body);
  response = http.getString();
  http.end();
  return true;
}

bool httpPUT(const String& url, const String& body, String& response, int& code) {
  WiFiClientSecure client;
  client.setInsecure();

  HTTPClient http;
  if (!http.begin(client, url)) return false;
  http.addHeader("Content-Type", "application/json");
  code = http.PUT(body);
  response = http.getString();
  http.end();
  return true;
}

bool httpGET(const String& url, String& response, int& code) {
  WiFiClientSecure client;
  client.setInsecure();

  HTTPClient http;
  if (!http.begin(client, url)) return false;
  code = http.GET();
  response = http.getString();
  http.end();
  return true;
}

bool firebaseAnonLogin() {
  String url = String("https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=") + FIREBASE_API_KEY;
  String body = "{\"returnSecureToken\":true}";
  String resp;
  int code = 0;

  if (!httpPOST(url, body, "application/json", resp, code)) return false;
  if (code < 200 || code >= 300) {
    Serial.printf("Anon login failed: %d\n%s\n", code, resp.c_str());
    return false;
  }

  DynamicJsonDocument doc(2048);
  deserializeJson(doc, resp);

  idToken = doc["idToken"].as<String>();
  refreshToken = doc["refreshToken"].as<String>();
  int expiresInSec = doc["expiresIn"].as<int>();
  tokenExpiryMs = millis() + (unsigned long)(expiresInSec - 60) * 1000UL;

  return idToken.length() > 0;
}

bool firebaseRefresh() {
  String url = String("https://securetoken.googleapis.com/v1/token?key=") + FIREBASE_API_KEY;
  String body = "grant_type=refresh_token&refresh_token=" + refreshToken;
  String resp;
  int code = 0;

  if (!httpPOST(url, body, "application/x-www-form-urlencoded", resp, code)) return false;
  if (code < 200 || code >= 300) {
    Serial.printf("Token refresh failed: %d\n%s\n", code, resp.c_str());
    return false;
  }

  DynamicJsonDocument doc(2048);
  deserializeJson(doc, resp);

  idToken = doc["id_token"].as<String>();
  refreshToken = doc["refresh_token"].as<String>();
  int expiresInSec = doc["expires_in"].as<int>();
  tokenExpiryMs = millis() + (unsigned long)(expiresInSec - 60) * 1000UL;

  return idToken.length() > 0;
}

bool ensureFirebaseToken() {
  if (idToken.length() == 0) return firebaseAnonLogin();
  if (millis() >= tokenExpiryMs) return firebaseRefresh();
  return true;
}

bool forceReauth() {
  idToken = "";
  refreshToken = "";
  tokenExpiryMs = 0;
  return firebaseAnonLogin();
}

double round6(double value) {
  return floor(value * 1000000.0 + (value >= 0 ? 0.5 : -0.5)) / 1000000.0;
}

bool fetchGpsRequest(String& requestId, String& beaconId) {
  requestId = "";
  beaconId = "";

  if (!ensureFirebaseToken()) return false;

  String url = String(FIREBASE_DB_URL) + GPS_REQUEST_PATH + "?auth=" + idToken;
  String resp;
  int code = 0;

  if (!httpGET(url, resp, code)) return false;
  if (code == 401) {
    Serial.println("RTDB 401 on request fetch. Re-authenticating and retrying...");
    if (!forceReauth()) return false;
    url = String(FIREBASE_DB_URL) + GPS_REQUEST_PATH + "?auth=" + idToken;
    if (!httpGET(url, resp, code)) return false;
  }

  if (code == 404) return true;
  if (code < 200 || code >= 300) {
    Serial.printf("Request fetch failed: %d\n%s\n", code, resp.c_str());
    if (code == 401 || code == 403) {
      Serial.println("Check: Firebase Anonymous Auth enabled and RTDB rules allow auth != null.");
    }
    return false;
  }

  DynamicJsonDocument doc(1024);
  DeserializationError err = deserializeJson(doc, resp);
  if (err) {
    Serial.printf("Request parse failed: %s\n", err.c_str());
    return false;
  }

  if (doc.isNull()) return true;

  requestId = doc["requestId"].as<String>();
  beaconId = doc["beaconId"].as<String>();
  requestId.trim();
  beaconId.trim();
  return true;
}

bool sendGpsResponse(const String& requestId, const String& beaconId) {
  if (!ensureFirebaseToken()) return false;

  unsigned long nowSec = (unsigned long)time(nullptr);
  if (nowSec < 100000) nowSec = millis() / 1000;

  bool hasFix = gps.location.isValid() && gps.location.age() < 5000;

  DynamicJsonDocument doc(512);
  doc["requestId"] = requestId;
  doc["beaconId"] = beaconId;
  doc["timestamp"] = nowSec;
  doc["deviceId"] = DEVICE_ID;
  doc["hasFix"] = hasFix;
  if (gps.satellites.isValid()) {
    doc["satellites"] = gps.satellites.value();
  }
  if (gps.hdop.isValid()) {
    doc["hdop"] = gps.hdop.hdop();
  }

  if (hasFix) {
    doc["lat"] = round6(gps.location.lat());
    doc["lng"] = round6(gps.location.lng());
  }

  String json;
  serializeJson(doc, json);

  String url = String(FIREBASE_DB_URL) + GPS_RESPONSE_PATH + "?auth=" + idToken;
  String resp;
  int code = 0;
  if (!httpPUT(url, json, resp, code)) return false;
  if (code == 401) {
    Serial.println("RTDB 401 on response write. Re-authenticating and retrying...");
    if (!forceReauth()) return false;
    url = String(FIREBASE_DB_URL) + GPS_RESPONSE_PATH + "?auth=" + idToken;
    if (!httpPUT(url, json, resp, code)) return false;
  }

  if (code < 200 || code >= 300) {
    Serial.printf("GPS response write failed: %d\n%s\n", code, resp.c_str());
    if (code == 401 || code == 403) {
      Serial.println("Check: Firebase Anonymous Auth enabled and RTDB rules allow auth != null.");
    }
    return false;
  }

  if (hasFix) {
    Serial.printf(
      "GPS sent for %s -> %.6f, %.6f\n",
      beaconId.c_str(),
      gps.location.lat(),
      gps.location.lng()
    );
  } else {
    Serial.printf("GPS sent for %s but no valid fix yet.\n", beaconId.c_str());
  }

  return true;
}

void connectWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("Connecting WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(400);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected.");
}

void setup() {
  Serial.begin(115200);
  delay(500);

  connectWiFi();
  configTime(0, 0, "pool.ntp.org", "time.google.com");

  GPSSerial.begin(GPS_BAUD, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
  Serial.println("NEO-8M GPS responder ready.");

  if (!firebaseAnonLogin()) {
    Serial.println("Firebase login failed at startup, will retry in loop.");
  }
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
  }

  while (GPSSerial.available()) {
    gps.encode((char)GPSSerial.read());
  }

  if (millis() - lastPollMs < REQUEST_POLL_MS) {
    delay(20);
    return;
  }
  lastPollMs = millis();

  String requestId;
  String beaconId;
  if (!fetchGpsRequest(requestId, beaconId)) {
    delay(50);
    return;
  }

  if (requestId.length() == 0 || beaconId.length() == 0) {
    return;
  }

  if (requestId == lastHandledRequestId) {
    return;
  }

  Serial.printf("New GPS request: %s (beacon %s)\n", requestId.c_str(), beaconId.c_str());
  if (sendGpsResponse(requestId, beaconId)) {
    lastHandledRequestId = requestId;
  }
}
