// sellkeys Auth – C++17 SDK, header-only.
// Needs: libcurl, libsodium, nlohmann/json  (vcpkg: curl libsodium nlohmann-json · apt: libcurl4-openssl-dev libsodium-dev nlohmann-json3-dev)
//
//   #include "sellkeys_auth.hpp"
//   sellkeys::Auth auth("APP_ID", "PUBLIC_KEY", "1.0");
//   try {
//     auth.init();
//     auto lic = auth.license("XXXXX-XXXXX-XXXXX-XXXXX");
//     std::cout << "Welcome! Level " << lic["level"] << "\n";
//   } catch (const sellkeys::Error& e) {
//     std::cout << "Login failed: " << e.code << " " << e.what() << "\n";
//   }
//
// Every answer is signed with your app's Ed25519 key; the SDK rejects wrong signatures and replayed nonces, so a fake
// server cannot unlock your program. (It does not protect against patching your program.)
#pragma once
#include <curl/curl.h>
#include <sodium.h>
#include <nlohmann/json.hpp>

#include <cctype>
#include <cstdio>
#include <fstream>
#include <mutex>
#include <stdexcept>
#include <string>
#include <vector>
#ifdef _WIN32
#include <windows.h>
#endif

namespace sellkeys {

using json = nlohmann::json;
inline const char* DEFAULT_API_URL = "https://auth.sellkeys.cc/v1";

struct Error : std::runtime_error {
  std::string code;
  json data;  // full server answer, e.g. data["update_url"] for code "update_required"
  Error(std::string c, const std::string& msg, json d = json::object()) : std::runtime_error(msg), code(std::move(c)), data(std::move(d)) {}
};

namespace detail {
inline void global_init() {
  static std::once_flag once;
  std::call_once(once, [] {
    if (sodium_init() < 0) throw Error("init_failed", "libsodium could not be initialised.");
    curl_global_init(CURL_GLOBAL_DEFAULT);
  });
}

inline std::string hex(const unsigned char* p, size_t n) {
  static const char* d = "0123456789abcdef";
  std::string s(n * 2, '0');
  for (size_t i = 0; i < n; i++) { s[2 * i] = d[p[i] >> 4]; s[2 * i + 1] = d[p[i] & 15]; }
  return s;
}

inline std::string sha256_hex(const std::string& data) {
  unsigned char h[crypto_hash_sha256_BYTES];
  crypto_hash_sha256(h, reinterpret_cast<const unsigned char*>(data.data()), data.size());
  return hex(h, sizeof h);
}

inline std::vector<unsigned char> b64decode(const std::string& in) {
  std::vector<unsigned char> out(in.size());
  size_t len = 0;
  if (sodium_base642bin(out.data(), out.size(), in.c_str(), in.size(), " \r\n", &len, nullptr, sodium_base64_VARIANT_ORIGINAL) != 0) return {};
  out.resize(len);
  return out;
}

inline size_t on_body(char* p, size_t s, size_t n, void* u) { static_cast<std::string*>(u)->append(p, s * n); return s * n; }

inline size_t on_header(char* p, size_t s, size_t n, void* u) {
  std::string line(p, s * n);
  const std::string name = "x-sellkeys-signature:";
  if (line.size() > name.size()) {
    std::string head = line.substr(0, name.size());
    for (auto& c : head) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
    if (head == name) {
      std::string v = line.substr(name.size());
      v.erase(0, v.find_first_not_of(" \t"));
      v.erase(v.find_last_not_of(" \t\r\n") + 1);
      *static_cast<std::string*>(u) = v;
    }
  }
  return s * n;
}

// POST (form) or GET; returns status, body and the signature header
inline long http(const std::string& url, const std::string* form, long timeout_s, std::string& body, std::string& sig) {
  CURL* c = curl_easy_init();
  if (!c) throw Error("network_error", "curl_easy_init failed.");
  curl_easy_setopt(c, CURLOPT_URL, url.c_str());
  curl_easy_setopt(c, CURLOPT_TIMEOUT, timeout_s);
  curl_easy_setopt(c, CURLOPT_USERAGENT, "sellkeys-auth-cpp/1");
  curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, on_body);
  curl_easy_setopt(c, CURLOPT_WRITEDATA, &body);
  curl_easy_setopt(c, CURLOPT_HEADERFUNCTION, on_header);
  curl_easy_setopt(c, CURLOPT_HEADERDATA, &sig);
  if (form) { curl_easy_setopt(c, CURLOPT_POSTFIELDS, form->c_str()); curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, static_cast<long>(form->size())); }
  CURLcode rc = curl_easy_perform(c);
  long status = 0;
  curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &status);
  curl_easy_cleanup(c);
  if (rc != CURLE_OK) throw Error("network_error", std::string("Could not reach the license server: ") + curl_easy_strerror(rc));
  return status;
}

inline std::string urlencode(const std::string& s) {
  std::string o;
  static const char* d = "0123456789ABCDEF";
  for (unsigned char ch : s) {
    if (std::isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '~') o += static_cast<char>(ch);
    else { o += '%'; o += d[ch >> 4]; o += d[ch & 15]; }
  }
  return o;
}
}  // namespace detail

// Stable per-machine id (hashed, the raw value never leaves the machine).
inline std::string default_hwid() {
  detail::global_init();
  std::string raw;
#ifdef _WIN32
  HKEY k;
  if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography", 0, KEY_READ | KEY_WOW64_64KEY, &k) == ERROR_SUCCESS) {
    char buf[128]; DWORD len = sizeof buf, type = 0;
    if (RegQueryValueExA(k, "MachineGuid", nullptr, &type, reinterpret_cast<LPBYTE>(buf), &len) == ERROR_SUCCESS && type == REG_SZ) raw.assign(buf, strnlen(buf, len));
    RegCloseKey(k);
  }
#elif defined(__APPLE__)
  if (FILE* p = popen("ioreg -rd1 -c IOPlatformExpertDevice | awk -F'\"' '/IOPlatformUUID/{print $4}'", "r")) {
    char buf[128]; if (fgets(buf, sizeof buf, p)) raw = buf; pclose(p);
    while (!raw.empty() && (raw.back() == '\n' || raw.back() == '\r')) raw.pop_back();
  }
#else
  for (const char* path : {"/etc/machine-id", "/var/lib/dbus/machine-id"}) {
    std::ifstream f(path);
    if (f && std::getline(f, raw) && !raw.empty()) break;
  }
#endif
  if (raw.empty()) raw = "unknown-machine";  // shortcut: no fallback id on exotic systems, pass your own hwid then
  return detail::sha256_hex(raw);
}

class Auth {
 public:
  std::string session;
  json app, license_info, user;

  Auth(std::string app_id, const std::string& public_key, std::string version, std::string api_url = DEFAULT_API_URL, std::string hwid = "", long timeout_s = 15)
      : app_id_(std::move(app_id)), version_(std::move(version)), api_url_(std::move(api_url)), timeout_(timeout_s) {
    detail::global_init();
    pub_ = detail::b64decode(public_key);
    if (pub_.size() != crypto_sign_PUBLICKEYBYTES) throw Error("bad_public_key", "The public key must be 32 bytes, base64.");
    hwid_ = hwid.empty() ? default_hwid() : std::move(hwid);
  }

  // Starts a session. Throws code "update_required" (see e.data["update_url"]) if your version is too old.
  json init(const std::string& file_hash = "") {
    json b = call("init", {{"version", version_}, {"hash", file_hash}});
    session = b["session"].get<std::string>();
    return app = b["app"];
  }
  json license(const std::string& key) { ensure(); return license_info = call("license", {{"session", session}, {"key", key}, {"hwid", hwid_}})["license"]; }
  // Call periodically: throws if the license/account was banned, expired or the session ended.
  json check() {
    json b = call("check", {{"session", session}});
    if (b.contains("user")) return user = b["user"];
    return license_info = b["license"];
  }

  // ---- accounts (username + password; the key is redeemed to the account) ----
  json register_user(const std::string& username, const std::string& password, const std::string& key) {
    ensure(); return user = call("register", {{"session", session}, {"username", username}, {"password", password}, {"key", key}, {"hwid", hwid_}})["user"];
  }
  json login(const std::string& username, const std::string& password) {
    ensure(); return user = call("login", {{"session", session}, {"username", username}, {"password", password}, {"hwid", hwid_}})["user"];
  }
  json upgrade(const std::string& username, const std::string& key) { ensure(); return call("upgrade", {{"session", session}, {"username", username}, {"key", key}})["user"]; }

  // ---- content ----
  std::string var(const std::string& name) { return call("var", {{"session", session}, {"name", name}})["value"].get<std::string>(); }
  std::string get_user_var(const std::string& name) { return call("getvar", {{"session", session}, {"name", name}})["value"].get<std::string>(); }
  void set_user_var(const std::string& name, const std::string& value) { call("setvar", {{"session", session}, {"name", name}, {"value", value}}); }

  // Downloads an app file and checks it against the signed SHA-256. Returns the bytes.
  std::string download(const std::string& file_id) {
    json info = call("file", {{"session", session}, {"file", file_id}});
    std::string body, sig;
    long st = detail::http(info["url"].get<std::string>(), nullptr, timeout_ > 120 ? timeout_ : 120, body, sig);
    if (st != 200) throw Error("network_error", "Download failed: HTTP " + std::to_string(st));
    if (detail::sha256_hex(body) != info["sha256"].get<std::string>()) throw Error("bad_file_hash", "The downloaded file does not match its signed checksum.");
    return body;
  }

  // Calls a webhook set in the dashboard through sellkeys (its URL and keys stay on the server). Returns {status, response}.
  json webhook(const std::string& name, const std::string& params = "", const std::string& body = "", const std::string& content_type = "") {
    json b = call("webhook", {{"session", session}, {"webid", name}, {"params", params}, {"body", body}, {"content_type", content_type}});
    return {{"status", b["status"]}, {"response", b["response"]}};
  }

  void log(const std::string& message) { call("log", {{"session", session}, {"message", message.substr(0, 500)}}); }
  void logout() { if (!session.empty()) { call("logout", {{"session", session}}); session.clear(); } }

 private:
  std::string app_id_, version_, api_url_, hwid_;
  long timeout_;
  std::vector<unsigned char> pub_;

  void ensure() { if (session.empty()) init(); }

  json call(const std::string& type, std::vector<std::pair<std::string, std::string>> fields) {
    unsigned char rnd[16];
    randombytes_buf(rnd, sizeof rnd);
    const std::string nonce = detail::hex(rnd, sizeof rnd);
    std::string form = "type=" + detail::urlencode(type) + "&app=" + detail::urlencode(app_id_) + "&nonce=" + nonce;
    for (auto& [k, v] : fields) if (!v.empty()) form += "&" + k + "=" + detail::urlencode(v);
    std::string body, sig;
    long status = detail::http(api_url_, &form, timeout_, body, sig);
    if (sig.empty()) {  // only errors before the app is known (rate limit, unknown app) are unsigned
      json e = json::parse(body, nullptr, false);
      throw Error(e.is_object() ? e.value("code", "http_" + std::to_string(status)) : "http_" + std::to_string(status),
                  e.is_object() ? e.value("message", "Unexpected server response.") : "Unexpected server response.");
    }
    auto s = detail::b64decode(sig);
    if (s.size() != crypto_sign_BYTES ||
        crypto_sign_verify_detached(s.data(), reinterpret_cast<const unsigned char*>(body.data()), body.size(), pub_.data()) != 0)
      throw Error("bad_signature", "The server response is not signed by sellkeys.");
    json b = json::parse(body);
    if (b.value("nonce", "") != nonce) throw Error("bad_nonce", "The server response does not belong to this request.");
    if (!b.value("success", false)) throw Error(b.value("code", "error"), b.value("message", "Request failed."), b);
    return b;
  }
};

}  // namespace sellkeys
