// Authsy SDK for C++ (header-only). Requires libcurl + OpenSSL + nlohmann/json.
//
//   #include "authsy.hpp"
//   authsy::Client c({ "https://YOUR-HOST/api/v1", "ak_live_xxx", "signing-secret" });
//   c.activate(licenseKey, machineId);
//   bool ok = c.validate(licenseKey, machineId);
//
// Responses are HMAC-SHA256 signed over canonical JSON; validate() verifies the
// signature, echoes a per-call nonce and rejects stale responses.
//
// Build: g++ main.cpp -lcurl -lcrypto   (vcpkg/conan for nlohmann-json)

#ifndef AUTHSY_HPP
#define AUTHSY_HPP

#include <string>
#include <map>
#include <chrono>
#include <random>
#include <stdexcept>
#include <cstdio>
#include <curl/curl.h>
#include <openssl/hmac.h>
#include <nlohmann/json.hpp>

namespace authsy {

using json = nlohmann::json;

struct Options {
    std::string baseUrl;
    std::string apiKey;
    std::string signingSecret;        // empty => responses are not verified
    long maxSkewMs = 5 * 60 * 1000;
};

class Error : public std::runtime_error {
public:
    std::string code;
    Error(const std::string& m, const std::string& c = "") : std::runtime_error(m), code(c) {}
};

class Client {
public:
    explicit Client(Options o) : opt_(std::move(o)) {
        if (opt_.baseUrl.empty()) throw Error("baseUrl is required");
        if (opt_.apiKey.empty()) throw Error("apiKey is required");
        while (!opt_.baseUrl.empty() && opt_.baseUrl.back() == '/') opt_.baseUrl.pop_back();
    }

    // Canonical JSON: object keys sorted lexicographically at every level.
    static std::string canonical(const json& v) {
        if (v.is_object()) {
            std::map<std::string, json> sorted(v.begin(), v.end());
            std::string out = "{"; bool first = true;
            for (auto& kv : sorted) {
                if (!first) out += ","; first = false;
                out += json(kv.first).dump() + ":" + canonical(kv.second);
            }
            return out + "}";
        }
        if (v.is_array()) {
            std::string out = "["; bool first = true;
            for (auto& e : v) { if (!first) out += ","; first = false; out += canonical(e); }
            return out + "]";
        }
        return v.dump();
    }

    bool verifySignature(json resp) const {
        if (opt_.signingSecret.empty()) throw Error("signingSecret required to verify responses");
        if (!resp.contains("signature") || !resp["signature"].is_string()) return false;
        std::string sig = resp["signature"].get<std::string>();
        resp.erase("signature");
        std::string expected = hmacHex(canonical(resp));
        if (expected.size() != sig.size()) return false;
        return CRYPTO_memcmp(expected.data(), sig.data(), sig.size()) == 0;
    }

    json activate(const std::string& licenseKey, const std::string& machineId) {
        std::string nonce = makeNonce();
        json body = { {"license_key", licenseKey}, {"machine_id", machineId}, {"nonce", nonce} };
        return checked(post("/client/activate", body), nonce);
    }

    bool validate(const std::string& licenseKey, const std::string& machineId) {
        std::string nonce = makeNonce();
        json body = { {"license_key", licenseKey}, {"machine_id", machineId}, {"nonce", nonce} };
        json resp = checked(post("/client/validate", body), nonce);
        lastResponse = resp;
        return resp.value("valid", false);
    }

    json lastResponse;

private:
    Options opt_;

    static std::string hmacHexKeyed(const std::string& key, const std::string& msg) {
        unsigned char mac[EVP_MAX_MD_SIZE]; unsigned int len = 0;
        HMAC(EVP_sha256(), key.data(), (int)key.size(),
             (const unsigned char*)msg.data(), msg.size(), mac, &len);
        static const char* hex = "0123456789abcdef";
        std::string out; out.reserve(len * 2);
        for (unsigned i = 0; i < len; i++) { out += hex[mac[i] >> 4]; out += hex[mac[i] & 0xf]; }
        return out;
    }
    std::string hmacHex(const std::string& msg) const { return hmacHexKeyed(opt_.signingSecret, msg); }

    static std::string makeNonce() {
        static std::mt19937_64 rng{ std::random_device{}() };
        char buf[33];
        std::snprintf(buf, sizeof(buf), "%016llx%016llx",
                      (unsigned long long)rng(), (unsigned long long)rng());
        return std::string(buf);
    }

    static size_t writeCb(void* p, size_t s, size_t n, void* out) {
        ((std::string*)out)->append((char*)p, s * n); return s * n;
    }

    json post(const std::string& path, const json& body) {
        std::string payload = body.dump(), resp;
        long status = 0;
        CURL* c = curl_easy_init();
        if (!c) throw Error("curl init failed");
        curl_slist* h = nullptr;
        h = curl_slist_append(h, "Content-Type: application/json");
        h = curl_slist_append(h, ("X-Api-Key: " + opt_.apiKey).c_str());
        curl_easy_setopt(c, CURLOPT_URL, (opt_.baseUrl + path).c_str());
        curl_easy_setopt(c, CURLOPT_HTTPHEADER, h);
        curl_easy_setopt(c, CURLOPT_POSTFIELDS, payload.c_str());
        curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, writeCb);
        curl_easy_setopt(c, CURLOPT_WRITEDATA, &resp);
        CURLcode rc = curl_easy_perform(c);
        curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &status);
        curl_slist_free_all(h);
        curl_easy_cleanup(c);
        if (rc != CURLE_OK) throw Error(std::string("request failed: ") + curl_easy_strerror(rc));
        if (status == 403) throw Error("invalid api key", "invalid_api_key");
        if (status == 429) throw Error("over quota", "over_quota");
        return json::parse(resp, nullptr, false);
    }

    json checked(json resp, const std::string& nonce) {
        if (!opt_.signingSecret.empty()) {
            if (!verifySignature(resp)) throw Error("response signature invalid (tampered?)", "bad_signature");
            if (resp.value("nonce", std::string()) != nonce) throw Error("nonce mismatch (replay?)", "nonce_mismatch");
            if (resp.contains("ts")) {
                long long now = std::chrono::duration_cast<std::chrono::milliseconds>(
                    std::chrono::system_clock::now().time_since_epoch()).count();
                if (std::llabs(now - resp["ts"].get<long long>()) > opt_.maxSkewMs)
                    throw Error("response is stale", "stale");
            }
        }
        return resp;
    }
};

} // namespace authsy
#endif // AUTHSY_HPP
