#include "api.h"
#include "config.h"
#include "certs.h"
#include <WiFi.h>
#include <NetworkClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "mbedtls/md.h"
#include "esp_random.h"

namespace {
  String devId, devSecret;
  int64_t clockOffset = 0; // server time minus local NTP time, learned from every response

  String hmacHex(const String& key, const String& msg) {
    uint8_t out[32];
    const mbedtls_md_info_t* info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
    mbedtls_md_hmac(info, (const uint8_t*)key.c_str(), key.length(), (const uint8_t*)msg.c_str(), msg.length(), out);
    char hex[65];
    for (int i = 0; i < 32; i++) sprintf(hex + i * 2, "%02x", out[i]);
    hex[64] = 0;
    return String(hex);
  }
  String nonceHex() {
    char hex[33];
    for (int i = 0; i < 4; i++) sprintf(hex + i * 8, "%08lx", (unsigned long)esp_random());
    hex[32] = 0;
    return String(hex);
  }
  int64_t nowTs() { return (int64_t)time(nullptr) + clockOffset; }

  void learnClock(JsonDocument& doc) {
    if (doc["server_time"].is<int64_t>()) clockOffset = doc["server_time"].as<int64_t>() - (int64_t)time(nullptr);
  }

  // POST JSON; signs when an identity is set. Returns HTTP status or a negative transport error.
  int post(const char* path, const String& body, JsonDocument& out, bool sign) {
    NetworkClientSecure client;
    client.setCACert(ROOT_CA_PEM);
    client.setTimeout(HTTP_TIMEOUT_MS / 1000);
    HTTPClient http;
    http.setTimeout(HTTP_TIMEOUT_MS);
    http.setConnectTimeout(8000);
    if (!http.begin(client, API_HOST, API_PORT, path, true)) return -1;
    http.addHeader("content-type", "application/json");
    http.addHeader("user-agent", "sendit-fw/" FW_VERSION);
    if (sign && devId.length()) {
      String ts = String((long long)nowTs());
      String nonce = nonceHex();
      http.addHeader("x-sendit-device", devId);
      http.addHeader("x-sendit-ts", ts);
      http.addHeader("x-sendit-nonce", nonce);
      http.addHeader("x-sendit-sig", hmacHex(devSecret, ts + "." + nonce + "." + body));
    }
    int status = http.POST(body);
    if (status > 0) {
      String payload = http.getString();
      DeserializationError e = deserializeJson(out, payload);
      if (!e) learnClock(out);
    }
    http.end();
    return status;
  }
}

namespace api {
  void setIdentity(const String& id, const String& secret) { devId = id; devSecret = secret; }
  int64_t serverTime() { return nowTs(); }

  bool pair(const String& code, String& deviceId, String& secret, String& err) {
    JsonDocument req;
    req["code"] = code;
    req["hw"] = WiFi.macAddress();
    req["fw"] = FW_VERSION;
    String body; serializeJson(req, body);
    JsonDocument res;
    int status = post("/api/device/pair", body, res, false);
    if (status != 200) { err = status < 0 ? "Could not reach the server" : String(res["error"] | "Pairing failed"); return false; }
    deviceId = res["device_id"].as<String>();
    secret = res["secret"].as<String>();
    return deviceId.length() > 0 && secret.length() == 64;
  }

  PressResult press(uint32_t seq) {
    JsonDocument req;
    req["event"] = "press";
    req["seq"] = seq;
    req["rssi"] = WiFi.RSSI();
    req["fw"] = FW_VERSION;
    String body; serializeJson(req, body);
    JsonDocument res;
    int status = post("/api/device/press", body, res, true);
    PressResult r{"error", "", 0, false};
    if (status < 0) { r.transport = true; r.reason = "transport"; return r; }
    r.code = res["code"] | "error";
    r.reason = res["reason"] | "";
    r.retryIn = res["retry_in"] | 0;
    return r;
  }

  bool ping(String& otaUrl, String& otaVersion) {
    JsonDocument req;
    req["event"] = "ping";
    req["rssi"] = WiFi.RSSI();
    req["fw"] = FW_VERSION;
    String body; serializeJson(req, body);
    JsonDocument res;
    int status = post("/api/device/ping", body, res, true);
    if (status != 200) return false;
    if (res["ota"]["url"].is<const char*>()) { otaUrl = res["ota"]["url"].as<String>(); otaVersion = res["ota"]["version"].as<String>(); }
    return true;
  }
}
