// skulllabs_auth.hpp — single-header C++ client for Skull Labs Auth.
// Requires: libcurl (transport) + nlohmann/json.hpp (JSON parsing).
// Build flags: -lcurl   ( + on Linux: -lpthread )
// C++17.
//
// Usage:
//   #define SKULLLABS_AUTH_IMPLEMENTATION    // exactly one .cpp must define this before including
//   #include "skulllabs_auth.hpp"
//
//   skulllabs::AuthClient auth("slk_your_project_key");
//   auto session = auth.login("alice", "hunter2");
//   if (session.ok) std::cout << "hi, " << session.user.username << "\n";
//
// HARDWARE FINGERPRINT
//   Built from multiple stable identifiers. Sources, by platform:
//     Windows:  HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid
//               + GetSystemFirmwareTable RSMB ("RSMB") -> SMBIOS System UUID + Board Serial
//               + first non-loopback MAC
//     Linux:    /etc/machine-id
//               + /sys/class/dmi/id/product_uuid (when readable)
//               + /sys/class/dmi/id/board_serial (when readable)
//               + first non-loopback MAC
//     macOS:    `ioreg -rd1 -c IOPlatformExpertDevice` -> IOPlatformUUID
//               + first non-loopback MAC
//   All sources are joined with '|' and SHA-256 hashed. Junk / OEM placeholder
//   values are filtered. Cached for the process lifetime.

#ifndef SKULLLABS_AUTH_HPP
#define SKULLLABS_AUTH_HPP

#include <string>
#include <optional>
#include <nlohmann/json.hpp>

namespace skulllabs {

struct SessionUser {
    std::string id;
    std::string username;
    std::string email;
    std::optional<int64_t> expires_at;  // unix ms
};

struct LicenseInfo {
    std::string id;
    std::optional<int64_t> expires_at;
    int uses = 0;
    int max_uses = 0;
};

struct LoginResult {
    bool ok = false;
    std::string error;
    SessionUser user;
    LicenseInfo license;
    std::string project_id;
    std::string project_name;
    std::string session_id;
    int64_t issued_at = 0;
};

class AuthClient {
public:
    explicit AuthClient(std::string project_key,
                        std::string base_url = "https://api.skulllabs.in")
        : project_key_(std::move(project_key)),
          base_url_(std::move(base_url)) {}

    LoginResult login(const std::string& username, const std::string& password);
    LoginResult login_with_license(const std::string& license_key);
    bool register_user(const std::string& username,
                       const std::string& password,
                       std::string* err = nullptr,
                       const std::string& email = {});

    /// Stable hardware fingerprint (SHA-256 hex). Cached.
    static std::string hardware_id();

private:
    LoginResult post_login(const nlohmann::json& body);
    LoginResult post_license_verify(const nlohmann::json& body);

    std::string project_key_;
    std::string base_url_;
};

} // namespace skulllabs

#ifdef SKULLLABS_AUTH_IMPLEMENTATION

#include <curl/curl.h>
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <vector>

#if defined(_WIN32)
  #include <windows.h>
  #include <iphlpapi.h>
  #pragma comment(lib, "iphlpapi.lib")
  #pragma comment(lib, "advapi32.lib")
#elif defined(__APPLE__) || defined(__linux__)
  #include <unistd.h>
  #include <ifaddrs.h>
  #include <net/if.h>
  #if defined(__linux__)
    #include <netpacket/packet.h>
  #endif
  #if defined(__APPLE__)
    #include <net/if_dl.h>
  #endif
#endif

namespace skulllabs {
namespace detail {

static size_t curl_write(void* ptr, size_t size, size_t nmemb, void* userdata) {
    auto* s = static_cast<std::string*>(userdata);
    s->append(static_cast<char*>(ptr), size * nmemb);
    return size * nmemb;
}

static std::string http_post(const std::string& url, const std::string& body, long* status_out) {
    std::string response;
    CURL* curl = curl_easy_init();
    if (!curl) return {};
    struct curl_slist* headers = nullptr;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
    curl_easy_setopt(curl, CURLOPT_POST, 1L);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        curl_slist_free_all(headers);
        curl_easy_cleanup(curl);
        if (status_out) *status_out = 0;
        return std::string("{\"ok\":false,\"error\":\"network: ") + curl_easy_strerror(res) + "\"}";
    }
    long status = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
    if (status_out) *status_out = status;
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    return response;
}

static std::string sha256_hex(const std::string& data) {
    static const uint32_t K[64] = {
        0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
        0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
        0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
        0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
        0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
        0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
        0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
        0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
    };
    uint32_t H[8] = {0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19};
    std::vector<uint8_t> msg(data.begin(), data.end());
    uint64_t bitlen = msg.size() * 8;
    msg.push_back(0x80);
    while (msg.size() % 64 != 56) msg.push_back(0);
    for (int i = 7; i >= 0; --i) msg.push_back((bitlen >> (i * 8)) & 0xff);
    auto rotr = [](uint32_t x, uint32_t n){ return (x >> n) | (x << (32 - n)); };
    for (size_t chunk = 0; chunk < msg.size(); chunk += 64) {
        uint32_t W[64];
        for (int i = 0; i < 16; ++i)
            W[i] = (msg[chunk+i*4]<<24)|(msg[chunk+i*4+1]<<16)|(msg[chunk+i*4+2]<<8)|(msg[chunk+i*4+3]);
        for (int i = 16; i < 64; ++i) {
            uint32_t s0 = rotr(W[i-15],7)^rotr(W[i-15],18)^(W[i-15]>>3);
            uint32_t s1 = rotr(W[i-2],17)^rotr(W[i-2],19)^(W[i-2]>>10);
            W[i] = W[i-16] + s0 + W[i-7] + s1;
        }
        uint32_t a=H[0],b=H[1],c=H[2],d=H[3],e=H[4],f=H[5],g=H[6],h=H[7];
        for (int i = 0; i < 64; ++i) {
            uint32_t S1=rotr(e,6)^rotr(e,11)^rotr(e,25);
            uint32_t ch=(e&f)^((~e)&g);
            uint32_t t1=h+S1+ch+K[i]+W[i];
            uint32_t S0=rotr(a,2)^rotr(a,13)^rotr(a,22);
            uint32_t mj=(a&b)^(a&c)^(b&c);
            uint32_t t2=S0+mj;
            h=g;g=f;f=e;e=d+t1; d=c;c=b;b=a;a=t1+t2;
        }
        H[0]+=a;H[1]+=b;H[2]+=c;H[3]+=d;H[4]+=e;H[5]+=f;H[6]+=g;H[7]+=h;
    }
    std::ostringstream os;
    for (int i = 0; i < 8; ++i) os << std::hex << std::setw(8) << std::setfill('0') << H[i];
    return os.str();
}

static std::string trim(const std::string& s) {
    size_t a = 0, b = s.size();
    while (a < b && std::isspace((unsigned char)s[a])) ++a;
    while (b > a && std::isspace((unsigned char)s[b-1])) --b;
    return s.substr(a, b - a);
}

static bool is_junk(const std::string& v) {
    std::string l = v;
    std::transform(l.begin(), l.end(), l.begin(), [](unsigned char c){ return std::tolower(c); });
    return l.empty()
        || l == "to be filled by o.e.m."
        || l == "default string"
        || l == "system serial number"
        || l == "system manufacturer"
        || l == "0"
        || l == "00000000-0000-0000-0000-000000000000"
        || l == "none" || l == "n/a" || l == "null"
        || l == "not specified" || l == "not applicable";
}

static void append_part(std::string& seed, const char* key, const std::string& value) {
    auto v = trim(value);
    if (is_junk(v)) return;
    if (!seed.empty()) seed += "|";
    seed += key;
    seed += "=";
    seed += v;
}

static std::string read_file(const char* path) {
    std::ifstream f(path);
    if (!f) return {};
    std::ostringstream os; os << f.rdbuf();
    return os.str();
}

#if defined(_WIN32)
static std::string reg_read_machine_guid() {
    HKEY h; LSTATUS r = RegOpenKeyExA(HKEY_LOCAL_MACHINE,
        "SOFTWARE\\Microsoft\\Cryptography", 0,
        KEY_READ | KEY_WOW64_64KEY, &h);
    if (r != ERROR_SUCCESS) return {};
    char buf[128]; DWORD sz = sizeof(buf); DWORD type = 0;
    r = RegQueryValueExA(h, "MachineGuid", nullptr, &type, (LPBYTE)buf, &sz);
    RegCloseKey(h);
    if (r != ERROR_SUCCESS || type != REG_SZ) return {};
    return std::string(buf, sz > 0 ? sz - 1 : 0);
}

// Minimal SMBIOS parser: extract System UUID (Type 1) + Board Serial (Type 2).
struct SmbiosResult { std::string system_uuid; std::string board_serial; };
static SmbiosResult read_smbios() {
    SmbiosResult out;
    UINT size = GetSystemFirmwareTable('RSMB', 0, nullptr, 0);
    if (size == 0) return out;
    std::vector<uint8_t> buf(size);
    if (GetSystemFirmwareTable('RSMB', 0, buf.data(), size) != size) return out;
    if (buf.size() < 8) return out;
    const uint8_t* data = buf.data() + 8;
    size_t remaining = buf.size() - 8;
    while (remaining >= 4) {
        uint8_t type = data[0];
        uint8_t hlen = data[1];
        if (hlen < 4 || hlen > remaining) break;
        if (type == 1 && hlen >= 0x19) {
            // System Information: UUID is 16 bytes at offset 0x08
            const uint8_t* u = data + 0x08;
            char buf2[64];
            // Endianness: first 3 fields are little-endian per SMBIOS 2.6+
            std::snprintf(buf2, sizeof(buf2),
                "%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X",
                u[3],u[2],u[1],u[0], u[5],u[4], u[7],u[6],
                u[8],u[9], u[10],u[11],u[12],u[13],u[14],u[15]);
            out.system_uuid = buf2;
        }
        if (type == 2 && hlen >= 0x08) {
            // Baseboard: Serial Number is a string-ref at offset 0x07
            uint8_t str_idx = data[0x07];
            if (str_idx > 0) {
                const char* p = (const char*)(data + hlen);
                for (uint8_t i = 1; i < str_idx; ++i) {
                    p += strlen(p) + 1;
                    if ((size_t)(p - (const char*)data) >= remaining) { p = nullptr; break; }
                }
                if (p && *p) out.board_serial = p;
            }
        }
        // Skip header + strings (terminated by double null).
        const uint8_t* strings = data + hlen;
        size_t adv = hlen;
        while ((size_t)(strings - data) + 1 < remaining) {
            if (strings[0] == 0 && strings[1] == 0) { strings += 2; break; }
            ++strings;
        }
        adv = (size_t)(strings - data);
        if (adv == 0 || adv > remaining) break;
        data += adv;
        remaining -= adv;
        if (type == 127) break; // End-of-Table
    }
    return out;
}
#endif

static std::string first_mac() {
#if defined(_WIN32)
    ULONG buf_len = 0;
    GetAdaptersInfo(nullptr, &buf_len);
    std::vector<uint8_t> buf(buf_len);
    if (GetAdaptersInfo(reinterpret_cast<IP_ADAPTER_INFO*>(buf.data()), &buf_len) != NO_ERROR) return {};
    auto* a = reinterpret_cast<IP_ADAPTER_INFO*>(buf.data());
    while (a) {
        if (a->AddressLength == 6 && a->Type != MIB_IF_TYPE_LOOPBACK) {
            char mac[32];
            std::snprintf(mac, sizeof(mac), "%02X-%02X-%02X-%02X-%02X-%02X",
                a->Address[0],a->Address[1],a->Address[2],
                a->Address[3],a->Address[4],a->Address[5]);
            if (std::string(mac) != "00-00-00-00-00-00") return mac;
        }
        a = a->Next;
    }
    return {};
#elif defined(__linux__) || defined(__APPLE__)
    ifaddrs* ifa = nullptr;
    if (getifaddrs(&ifa) != 0) return {};
    std::string out;
    for (auto* p = ifa; p; p = p->ifa_next) {
        if (!p->ifa_addr) continue;
        if (p->ifa_flags & IFF_LOOPBACK) continue;
    #if defined(__linux__)
        if (p->ifa_addr->sa_family != AF_PACKET) continue;
        auto* s = reinterpret_cast<sockaddr_ll*>(p->ifa_addr);
        if (s->sll_halen != 6) continue;
        char mac[32];
        std::snprintf(mac, sizeof(mac), "%02X-%02X-%02X-%02X-%02X-%02X",
            s->sll_addr[0],s->sll_addr[1],s->sll_addr[2],
            s->sll_addr[3],s->sll_addr[4],s->sll_addr[5]);
        if (std::string(mac) == "00-00-00-00-00-00") continue;
        out = mac; break;
    #elif defined(__APPLE__)
        if (p->ifa_addr->sa_family != AF_LINK) continue;
        auto* s = reinterpret_cast<sockaddr_dl*>(p->ifa_addr);
        if (s->sdl_alen != 6) continue;
        auto* d = reinterpret_cast<unsigned char*>(LLADDR(s));
        char mac[32];
        std::snprintf(mac, sizeof(mac), "%02X-%02X-%02X-%02X-%02X-%02X", d[0],d[1],d[2],d[3],d[4],d[5]);
        if (std::string(mac) == "00-00-00-00-00-00") continue;
        out = mac; break;
    #endif
    }
    freeifaddrs(ifa);
    return out;
#else
    return {};
#endif
}

#if defined(__APPLE__)
static std::string macos_platform_uuid() {
    FILE* p = popen("ioreg -rd1 -c IOPlatformExpertDevice 2>/dev/null", "r");
    if (!p) return {};
    std::string out; char buf[512];
    while (fgets(buf, sizeof(buf), p)) out += buf;
    pclose(p);
    auto pos = out.find("IOPlatformUUID");
    if (pos == std::string::npos) return {};
    auto q1 = out.find('"', pos + 14);
    if (q1 == std::string::npos) return {};
    q1 = out.find('"', q1 + 1);
    if (q1 == std::string::npos) return {};
    auto q2 = out.find('"', q1 + 1);
    if (q2 == std::string::npos) return {};
    return out.substr(q1 + 1, q2 - q1 - 1);
}
#endif

} // namespace detail

std::string AuthClient::hardware_id() {
    static std::string cached;
    if (!cached.empty()) return cached;
    std::string seed;

#if defined(_WIN32)
    detail::append_part(seed, "mg", detail::reg_read_machine_guid());
    auto sm = detail::read_smbios();
    detail::append_part(seed, "uuid", sm.system_uuid);
    detail::append_part(seed, "board", sm.board_serial);
    char host[256] = {0}; DWORD sz = sizeof(host); GetComputerNameA(host, &sz);
    detail::append_part(seed, "host", host);
#elif defined(__linux__)
    detail::append_part(seed, "mid",   detail::trim(detail::read_file("/etc/machine-id")));
    detail::append_part(seed, "uuid",  detail::trim(detail::read_file("/sys/class/dmi/id/product_uuid")));
    detail::append_part(seed, "board", detail::trim(detail::read_file("/sys/class/dmi/id/board_serial")));
    char host[256] = {0}; gethostname(host, sizeof(host));
    detail::append_part(seed, "host", host);
#elif defined(__APPLE__)
    detail::append_part(seed, "uuid", detail::macos_platform_uuid());
    char host[256] = {0}; gethostname(host, sizeof(host));
    detail::append_part(seed, "host", host);
#endif

    detail::append_part(seed, "mac", detail::first_mac());

    if (seed.empty()) seed = "fallback";
    cached = detail::sha256_hex(seed);
    return cached;
}

LoginResult AuthClient::login(const std::string& username, const std::string& password) {
    return post_login({
        {"apiKey", project_key_},
        {"username", username},
        {"password", password},
        {"hwid", hardware_id()}
    });
}

LoginResult AuthClient::login_with_license(const std::string& license_key) {
    return post_license_verify({
        {"apiKey", project_key_},
        {"key", license_key},
        {"hwid", hardware_id()}
    });
}

bool AuthClient::register_user(const std::string& username, const std::string& password,
                               std::string* err, const std::string& email) {
    nlohmann::json body{
        {"apiKey", project_key_},
        {"username", username},
        {"password", password}
    };
    if (!email.empty()) body["email"] = email;
    long status = 0;
    auto raw = detail::http_post(base_url_ + "/v1/sdk/register", body.dump(), &status);
    try {
        auto j = nlohmann::json::parse(raw);
        if (j.value("ok", false)) return true;
        if (err) *err = j.value("error", "register failed");
    } catch (...) {
        if (err) *err = "invalid response";
    }
    return false;
}

namespace detail {
static LoginResult parse_login_result(const std::string& raw) {
    LoginResult out;
    try {
        auto j = nlohmann::json::parse(raw);
        out.ok = j.value("ok", false);
        out.error = j.value("error", "");
        if (j.contains("user") && j["user"].is_object()) {
            auto& u = j["user"];
            out.user.id = u.value("id", "");
            out.user.username = u.value("username", "");
            out.user.email = u.value("email", "");
            if (u.contains("expiresAt") && !u["expiresAt"].is_null())
                out.user.expires_at = u["expiresAt"].get<int64_t>();
        }
        if (j.contains("license") && j["license"].is_object()) {
            auto& l = j["license"];
            out.license.id = l.value("id", "");
            out.license.uses = l.value("uses", 0);
            out.license.max_uses = l.value("maxUses", 0);
            if (l.contains("expiresAt") && !l["expiresAt"].is_null())
                out.license.expires_at = l["expiresAt"].get<int64_t>();
        }
        if (j.contains("project") && j["project"].is_object()) {
            out.project_id = j["project"].value("id", "");
            out.project_name = j["project"].value("name", "");
        }
        out.session_id = j.value("sessionId", "");
        out.issued_at = j.value("issuedAt", (int64_t)0);
    } catch (...) {
        out.ok = false;
        out.error = "invalid response";
    }
    return out;
}
} // namespace detail

LoginResult AuthClient::post_login(const nlohmann::json& body) {
    long status = 0;
    auto raw = detail::http_post(base_url_ + "/v1/sdk/login", body.dump(), &status);
    return detail::parse_login_result(raw);
}

LoginResult AuthClient::post_license_verify(const nlohmann::json& body) {
    long status = 0;
    auto raw = detail::http_post(base_url_ + "/v1/license/verify", body.dump(), &status);
    return detail::parse_login_result(raw);
}

} // namespace skulllabs

#endif // SKULLLABS_AUTH_IMPLEMENTATION
#endif // SKULLLABS_AUTH_HPP
