// LicenseX C++ SDK — implementation.
// See licensex.hpp for the public API and licensex.cpp for build flags:
//   g++ -std=c++17 your_loader.cpp licensex.cpp -lwinhttp -lbcrypt -o loader.exe
#include "licensex.hpp"

#include <windows.h>
#include <winhttp.h>
#include <bcrypt.h>
#include <intrin.h>

#include <algorithm>
#include <map>
#include <sstream>
#include <variant>
#include <vector>

#pragma comment(lib, "winhttp.lib")
#pragma comment(lib, "bcrypt.lib")

namespace licensex {
namespace {

// ---------------------------------------------------------------------
// Minimal JSON value + parser/writer.
//
// Only implements what this SDK needs to talk to /api/v1/*: objects,
// strings, numbers, bools, null. No comments, no trailing commas — this
// is not a general-purpose JSON library, just enough to round-trip the
// LicenseX response shape.
// ---------------------------------------------------------------------

struct JsonValue {
    enum class Type { Null, Bool, Number, String, Object } type = Type::Null;
    bool b = false;
    double num = 0;
    std::string str;
    std::map<std::string, JsonValue> obj;

    bool isNull() const { return type == Type::Null; }
    bool asBool() const { return b; }
    double asNumber() const { return num; }
    const std::string& asString() const { return str; }

    const JsonValue* find(const std::string& key) const {
        auto it = obj.find(key);
        return it == obj.end() ? nullptr : &it->second;
    }
    std::string getString(const std::string& key, const std::string& def = "") const {
        auto* v = find(key);
        return (v && v->type == Type::String) ? v->str : def;
    }
    double getNumber(const std::string& key, double def = 0) const {
        auto* v = find(key);
        return (v && v->type == Type::Number) ? v->num : def;
    }
    bool getBool(const std::string& key, bool def = false) const {
        auto* v = find(key);
        return (v && v->type == Type::Bool) ? v->b : def;
    }
};

class JsonParser {
public:
    explicit JsonParser(const std::string& s) : s_(s), i_(0) {}

    bool parse(JsonValue& out) {
        skipWs();
        return parseValue(out) && (skipWs(), true);
    }

private:
    const std::string& s_;
    size_t i_;

    void skipWs() { while (i_ < s_.size() && isspace((unsigned char)s_[i_])) ++i_; }
    bool eof() const { return i_ >= s_.size(); }
    char peek() const { return s_[i_]; }

    bool parseValue(JsonValue& out) {
        skipWs();
        if (eof()) return false;
        switch (peek()) {
            case '{': return parseObject(out);
            case '"': return parseString(out);
            case 't': case 'f': return parseBool(out);
            case 'n': return parseNull(out);
            default: return parseNumber(out);
        }
    }

    bool parseObject(JsonValue& out) {
        out.type = JsonValue::Type::Object;
        ++i_;  // '{'
        skipWs();
        if (!eof() && peek() == '}') { ++i_; return true; }
        while (true) {
            skipWs();
            JsonValue keyVal;
            if (!parseString(keyVal)) return false;
            skipWs();
            if (eof() || s_[i_] != ':') return false;
            ++i_;
            JsonValue val;
            if (!parseValue(val)) return false;
            out.obj[keyVal.str] = std::move(val);
            skipWs();
            if (eof()) return false;
            if (s_[i_] == ',') { ++i_; continue; }
            if (s_[i_] == '}') { ++i_; return true; }
            return false;
        }
    }

    bool parseString(JsonValue& out) {
        if (eof() || peek() != '"') return false;
        ++i_;
        std::string result;
        while (!eof() && s_[i_] != '"') {
            char c = s_[i_++];
            if (c == '\\' && !eof()) {
                char esc = s_[i_++];
                switch (esc) {
                    case 'n': result += '\n'; break;
                    case 't': result += '\t'; break;
                    case 'r': result += '\r'; break;
                    case '"': result += '"'; break;
                    case '\\': result += '\\'; break;
                    case '/': result += '/'; break;
                    case 'u': i_ += 4; result += '?'; break;  // not needed for this API's payloads
                    default: result += esc;
                }
            } else {
                result += c;
            }
        }
        if (eof()) return false;
        ++i_;  // closing quote
        out.type = JsonValue::Type::String;
        out.str = std::move(result);
        return true;
    }

    bool parseNumber(JsonValue& out) {
        size_t start = i_;
        if (!eof() && (peek() == '-' || peek() == '+')) ++i_;
        while (!eof() && (isdigit((unsigned char)peek()) || peek() == '.' || peek() == 'e' || peek() == 'E' || peek() == '-' || peek() == '+')) ++i_;
        if (i_ == start) return false;
        out.type = JsonValue::Type::Number;
        out.num = std::stod(s_.substr(start, i_ - start));
        return true;
    }

    bool parseBool(JsonValue& out) {
        if (s_.compare(i_, 4, "true") == 0) { i_ += 4; out.type = JsonValue::Type::Bool; out.b = true; return true; }
        if (s_.compare(i_, 5, "false") == 0) { i_ += 5; out.type = JsonValue::Type::Bool; out.b = false; return true; }
        return false;
    }

    bool parseNull(JsonValue& out) {
        if (s_.compare(i_, 4, "null") == 0) { i_ += 4; out.type = JsonValue::Type::Null; return true; }
        return false;
    }
};

std::string jsonEscape(const std::string& s) {
    std::string out;
    out.reserve(s.size());
    for (char c : s) {
        switch (c) {
            case '"': out += "\\\""; break;
            case '\\': out += "\\\\"; break;
            case '\n': out += "\\n"; break;
            case '\r': out += "\\r"; break;
            case '\t': out += "\\t"; break;
            default: out += c;
        }
    }
    return out;
}

// Builds the request body for the four /api/v1/* endpoints. Deliberately
// not a general JSON writer — request bodies here are always a flat
// object of string fields, which matches every endpoint's zod schema.
std::string buildRequestJson(const std::vector<std::pair<std::string, std::string>>& fields) {
    std::ostringstream out;
    out << "{";
    for (size_t i = 0; i < fields.size(); ++i) {
        if (i) out << ",";
        out << "\"" << fields[i].first << "\":\"" << jsonEscape(fields[i].second) << "\"";
    }
    out << "}";
    return out.str();
}

// ---------------------------------------------------------------------
// Signature verification.
//
// Mirrors the server's stableStringify + HMAC-SHA256 exactly (see the
// LicenseX repo's src/lib/sign.ts): recursively sort every object's keys
// (not just top-level), JSON-encode with no extra whitespace, then HMAC
// the resulting string with api_secret. If this doesn't byte-for-byte
// match stableStringify's output the signature will never verify, even
// for a perfectly legitimate response.
// ---------------------------------------------------------------------

std::string stableStringify(const JsonValue& v) {
    switch (v.type) {
        case JsonValue::Type::Null: return "null";
        case JsonValue::Type::Bool: return v.b ? "true" : "false";
        case JsonValue::Type::Number: {
            // Server values here are always integers (timestamp, counts),
            // so format without a trailing ".0" to match JS's JSON.stringify.
            if (v.num == (long long)v.num) {
                return std::to_string((long long)v.num);
            }
            std::ostringstream os;
            os << v.num;
            return os.str();
        }
        case JsonValue::Type::String:
            return "\"" + jsonEscape(v.str) + "\"";
        case JsonValue::Type::Object: {
            std::vector<std::string> keys;
            for (auto& [k, _] : v.obj) keys.push_back(k);
            std::sort(keys.begin(), keys.end());
            std::string out = "{";
            for (size_t i = 0; i < keys.size(); ++i) {
                if (i) out += ",";
                out += "\"" + jsonEscape(keys[i]) + "\":" + stableStringify(v.obj.at(keys[i]));
            }
            out += "}";
            return out;
        }
    }
    return "null";
}

std::string toHex(const std::vector<unsigned char>& bytes) {
    static const char* hex = "0123456789abcdef";
    std::string out;
    out.reserve(bytes.size() * 2);
    for (unsigned char b : bytes) {
        out += hex[b >> 4];
        out += hex[b & 0xF];
    }
    return out;
}

std::vector<unsigned char> hmacSha256(const std::string& key, const std::string& data) {
    std::vector<unsigned char> result(32);
    BCRYPT_ALG_HANDLE hAlg = nullptr;
    BCRYPT_HASH_HANDLE hHash = nullptr;
    BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA256_ALGORITHM, nullptr, BCRYPT_ALG_HANDLE_HMAC_FLAG);
    BCryptCreateHash(hAlg, &hHash, nullptr, 0,
                      (PUCHAR)key.data(), (ULONG)key.size(), 0);
    BCryptHashData(hHash, (PUCHAR)data.data(), (ULONG)data.size(), 0);
    BCryptFinishHash(hHash, result.data(), (ULONG)result.size(), 0);
    BCryptDestroyHash(hHash);
    BCryptCloseAlgorithmProvider(hAlg, 0);
    return result;
}

// Verifies `signature` against every other top-level field in `resp`
// (i.e. everything the server signed, which is the whole body except
// the signature field itself — see buildSignedResponse in sign.ts).
bool verifySignature(const JsonValue& resp, const std::string& apiSecret) {
    auto* sigVal = resp.find("signature");
    if (!sigVal || sigVal->type != JsonValue::Type::String) return false;

    JsonValue unsigned_ = resp;
    unsigned_.obj.erase("signature");

    std::string data = stableStringify(unsigned_);
    std::string expected = toHex(hmacSha256(apiSecret, data));
    return expected == sigVal->str;
}

// ---------------------------------------------------------------------
// HTTP (WinHTTP), split into URL parts once per Client.
// ---------------------------------------------------------------------

struct HttpResponse {
    bool succeeded = false;
    bool timedOut = false;
    int status = 0;
    std::string body;
};

HttpResponse httpPostJson(const std::wstring& host, INTERNET_PORT port, bool https,
                           const std::wstring& path, const std::string& jsonBody) {
    HttpResponse result;

    HINTERNET hSession = WinHttpOpen(L"LicenseX-CppSDK/1.0",
                                      WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
                                      WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
    if (!hSession) return result;

    // 10s connect/send/receive timeouts — a stuck license check shouldn't
    // hang the caller's loader indefinitely.
    WinHttpSetTimeouts(hSession, 10000, 10000, 10000, 10000);

    HINTERNET hConnect = WinHttpConnect(hSession, host.c_str(), port, 0);
    if (!hConnect) { WinHttpCloseHandle(hSession); return result; }

    DWORD flags = https ? WINHTTP_FLAG_SECURE : 0;
    HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"POST", path.c_str(),
                                             nullptr, WINHTTP_NO_REFERER,
                                             WINHTTP_DEFAULT_ACCEPT_TYPES, flags);
    if (!hRequest) { WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return result; }

    std::wstring headers = L"Content-Type: application/json";
    BOOL sent = WinHttpSendRequest(hRequest, headers.c_str(), (DWORD)headers.size(),
                                    (LPVOID)jsonBody.data(), (DWORD)jsonBody.size(),
                                    (DWORD)jsonBody.size(), 0);
    if (sent && WinHttpReceiveResponse(hRequest, nullptr)) {
        DWORD statusCode = 0, size = sizeof(statusCode);
        WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_FLAG_NUMBER | WINHTTP_QUERY_STATUS_CODE,
                             WINHTTP_HEADER_NAME_BY_INDEX, &statusCode, &size, WINHTTP_NO_HEADER_INDEX);
        result.status = (int)statusCode;

        std::string body;
        DWORD avail = 0;
        do {
            avail = 0;
            WinHttpQueryDataAvailable(hRequest, &avail);
            if (avail == 0) break;
            std::vector<char> buf(avail);
            DWORD read = 0;
            if (!WinHttpReadData(hRequest, buf.data(), avail, &read)) break;
            body.append(buf.data(), read);
        } while (avail > 0);

        result.body = std::move(body);
        result.succeeded = true;
    } else {
        result.timedOut = (GetLastError() == ERROR_WINHTTP_TIMEOUT);
    }

    WinHttpCloseHandle(hRequest);
    WinHttpCloseHandle(hConnect);
    WinHttpCloseHandle(hSession);
    return result;
}

// Splits "https://host[:port]" into its parts. LicenseX deployments are
// always plain host + optional port — no path prefix, no query string.
void parseBaseUrl(const std::string& baseUrl, bool& https, std::wstring& host, INTERNET_PORT& port) {
    std::string rest = baseUrl;
    https = rest.rfind("https://", 0) == 0;
    size_t schemeLen = https ? 8 : (rest.rfind("http://", 0) == 0 ? 7 : 0);
    rest = rest.substr(schemeLen);
    if (!rest.empty() && rest.back() == '/') rest.pop_back();

    size_t colon = rest.find(':');
    std::string hostPart = colon == std::string::npos ? rest : rest.substr(0, colon);
    port = colon == std::string::npos
               ? (https ? INTERNET_DEFAULT_HTTPS_PORT : INTERNET_DEFAULT_HTTP_PORT)
               : (INTERNET_PORT)std::stoi(rest.substr(colon + 1));

    host = std::wstring(hostPart.begin(), hostPart.end());
}

// SHA-256 of arbitrary bytes, used to fold the three HWID components
// into one fixed-length string without leaking the raw component values
// in whatever gets logged/displayed.
std::string sha256Hex(const std::string& input) {
    std::vector<unsigned char> digest(32);
    BCRYPT_ALG_HANDLE hAlg = nullptr;
    BCRYPT_HASH_HANDLE hHash = nullptr;
    BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA256_ALGORITHM, nullptr, 0);
    BCryptCreateHash(hAlg, &hHash, nullptr, 0, nullptr, 0, 0);
    BCryptHashData(hHash, (PUCHAR)input.data(), (ULONG)input.size(), 0);
    BCryptFinishHash(hHash, digest.data(), (ULONG)digest.size(), 0);
    BCryptDestroyHash(hHash);
    BCryptCloseAlgorithmProvider(hAlg, 0);
    return toHex(digest);
}

std::string getCpuVendorModel() {
    int cpuInfo[4] = {0};
    char vendor[13] = {0};
    __cpuid(cpuInfo, 0);
    memcpy(vendor + 0, &cpuInfo[1], 4);
    memcpy(vendor + 4, &cpuInfo[3], 4);
    memcpy(vendor + 8, &cpuInfo[2], 4);

    char brand[49] = {0};
    __cpuid(cpuInfo, 0x80000000);
    unsigned int maxExt = (unsigned int)cpuInfo[0];
    if (maxExt >= 0x80000004) {
        __cpuid((int*)&brand[0], 0x80000002);
        __cpuid((int*)&brand[16], 0x80000003);
        __cpuid((int*)&brand[32], 0x80000004);
    }
    return std::string(vendor) + "|" + std::string(brand);
}

std::string getMachineGuid() {
    HKEY hKey;
    char buf[64] = {0};
    DWORD size = sizeof(buf);
    if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography", 0,
                       KEY_READ | KEY_WOW64_64KEY, &hKey) == ERROR_SUCCESS) {
        RegQueryValueExA(hKey, "MachineGuid", nullptr, nullptr, (LPBYTE)buf, &size);
        RegCloseKey(hKey);
    }
    return std::string(buf);
}

std::string getVolumeSerial() {
    DWORD serial = 0;
    if (!GetVolumeInformationA("C:\\", nullptr, 0, &serial, nullptr, nullptr, nullptr, 0)) {
        return "0";
    }
    std::ostringstream os;
    os << std::hex << serial;
    return os.str();
}

}  // namespace

std::string GetHardwareId() {
    std::string combined = getCpuVendorModel() + "|" + getMachineGuid() + "|" + getVolumeSerial();
    return sha256Hex(combined);
}

Client::Client(std::string baseUrl, std::string appId, std::string apiSecret)
    : baseUrl_(std::move(baseUrl)), appId_(std::move(appId)), apiSecret_(std::move(apiSecret)) {}

namespace {

// Shared plumbing for every endpoint: POST JSON, parse the response,
// verify the signature (unless the caller says not to — only used for
// the InvalidCredentials case, which the server never signs), and map
// transport/parse failures onto the right ErrorCode.
struct RawCall {
    bool transportOk = false;
    bool timedOut = false;
    int status = 0;
    JsonValue json;
    bool parsedOk = false;
};

RawCall callEndpoint(const std::string& baseUrl, const std::string& path, const std::string& bodyJson) {
    RawCall out;
    bool https;
    std::wstring host;
    INTERNET_PORT port;
    parseBaseUrl(baseUrl, https, host, port);

    std::wstring wpath(path.begin(), path.end());
    HttpResponse resp = httpPostJson(host, port, https, wpath, bodyJson);
    out.transportOk = resp.succeeded;
    out.timedOut = resp.timedOut;
    out.status = resp.status;
    if (resp.succeeded) {
        JsonParser parser(resp.body);
        out.parsedOk = parser.parse(out.json) && out.json.type == JsonValue::Type::Object;
    }
    return out;
}

}  // namespace

AuthResult Client::Authenticate(const std::string& licenseKey, const std::string& hwid) {
    AuthResult result;
    std::string body = buildRequestJson({
        {"app_id", appId_}, {"api_secret", apiSecret_},
        {"license_key", licenseKey}, {"hwid", hwid},
    });
    RawCall call = callEndpoint(baseUrl_, "/api/v1/authenticate", body);

    if (!call.transportOk) {
        result.error = call.timedOut ? ErrorCode::Timeout : ErrorCode::NetworkError;
        result.errorMessage = call.timedOut ? "Request timed out" : "Could not reach license server";
        return result;
    }
    if (!call.parsedOk) {
        result.error = ErrorCode::ParseError;
        result.errorMessage = "Server response was not valid JSON";
        return result;
    }

    // 401 = invalid app_id/api_secret. Sent unsigned by design (see
    // licensex.hpp) — do NOT attempt signature verification on this path.
    if (call.status == 401) {
        result.error = ErrorCode::InvalidCredentials;
        result.errorMessage = call.json.getString("error", "Invalid application credentials");
        return result;
    }
    if (call.status == 429) {
        result.error = ErrorCode::RateLimited;
        result.errorMessage = call.json.getString("error", "Too many requests");
        return result;
    }

    if (!verifySignature(call.json, apiSecret_)) {
        result.error = ErrorCode::SignatureInvalid;
        result.errorMessage = "Response signature did not verify";
        return result;
    }

    bool success = call.json.getBool("success", false);
    if (!success) {
        result.error = ErrorCode::ServerRejected;
        result.errorMessage = call.json.getString("error", "License rejected");
        return result;
    }

    auto* licenseVal = call.json.find("license");
    result.ok = true;
    if (licenseVal) {
        result.license.status = licenseVal->getString("status");
        std::string exp = licenseVal->getString("expires_at");
        result.license.expiresAt = exp.empty() ? std::nullopt : std::optional<std::string>(exp);
    }
    return result;
}

ResetResult Client::ResetHwid(const std::string& licenseKey) {
    ResetResult result;
    std::string body = buildRequestJson({
        {"app_id", appId_}, {"api_secret", apiSecret_}, {"license_key", licenseKey},
    });
    RawCall call = callEndpoint(baseUrl_, "/api/v1/reset-hwid", body);

    if (!call.transportOk) {
        result.error = call.timedOut ? ErrorCode::Timeout : ErrorCode::NetworkError;
        result.errorMessage = call.timedOut ? "Request timed out" : "Could not reach license server";
        return result;
    }
    if (!call.parsedOk) {
        result.error = ErrorCode::ParseError;
        result.errorMessage = "Server response was not valid JSON";
        return result;
    }
    if (call.status == 401) {
        result.error = ErrorCode::InvalidCredentials;
        result.errorMessage = call.json.getString("error", "Invalid application credentials");
        return result;
    }
    if (call.status == 429) {
        result.error = ErrorCode::RateLimited;
        result.errorMessage = call.json.getString("error", "Too many requests");
        return result;
    }
    if (!verifySignature(call.json, apiSecret_)) {
        result.error = ErrorCode::SignatureInvalid;
        result.errorMessage = "Response signature did not verify";
        return result;
    }

    bool success = call.json.getBool("success", false);
    result.resetsUsed = (int)call.json.getNumber("resets_used", 0);
    result.resetsAllowed = (int)call.json.getNumber("resets_allowed", 0);
    if (!success) {
        result.error = (result.resetsAllowed > 0 && result.resetsUsed >= result.resetsAllowed)
                            ? ErrorCode::ResetLimitReached
                            : ErrorCode::ServerRejected;
        result.errorMessage = call.json.getString("error", "Reset rejected");
        return result;
    }

    result.ok = true;
    return result;
}

VariableResult Client::GetVariable(const std::string& name) {
    VariableResult result;
    std::string body = buildRequestJson({
        {"app_id", appId_}, {"api_secret", apiSecret_}, {"name", name},
    });
    RawCall call = callEndpoint(baseUrl_, "/api/v1/variable", body);

    if (!call.transportOk) {
        result.error = call.timedOut ? ErrorCode::Timeout : ErrorCode::NetworkError;
        result.errorMessage = call.timedOut ? "Request timed out" : "Could not reach license server";
        return result;
    }
    if (!call.parsedOk) {
        result.error = ErrorCode::ParseError;
        result.errorMessage = "Server response was not valid JSON";
        return result;
    }
    if (call.status == 401) {
        result.error = ErrorCode::InvalidCredentials;
        result.errorMessage = call.json.getString("error", "Invalid application credentials");
        return result;
    }
    if (call.status == 429) {
        result.error = ErrorCode::RateLimited;
        result.errorMessage = call.json.getString("error", "Too many requests");
        return result;
    }
    if (!verifySignature(call.json, apiSecret_)) {
        result.error = ErrorCode::SignatureInvalid;
        result.errorMessage = "Response signature did not verify";
        return result;
    }

    bool success = call.json.getBool("success", false);
    if (!success) {
        result.error = (call.status == 404) ? ErrorCode::NotFound : ErrorCode::ServerRejected;
        result.errorMessage = call.json.getString("error", "Variable not found");
        return result;
    }

    result.ok = true;
    result.value = call.json.getString("value");
    return result;
}

LogResult Client::Log(const std::string& message, const std::string& licenseKey) {
    LogResult result;
    std::vector<std::pair<std::string, std::string>> fields = {
        {"app_id", appId_}, {"api_secret", apiSecret_}, {"message", message},
    };
    if (!licenseKey.empty()) fields.push_back({"license_key", licenseKey});
    std::string body = buildRequestJson(fields);
    RawCall call = callEndpoint(baseUrl_, "/api/v1/log", body);

    if (!call.transportOk) {
        result.error = call.timedOut ? ErrorCode::Timeout : ErrorCode::NetworkError;
        result.errorMessage = call.timedOut ? "Request timed out" : "Could not reach license server";
        return result;
    }
    if (!call.parsedOk) {
        result.error = ErrorCode::ParseError;
        result.errorMessage = "Server response was not valid JSON";
        return result;
    }
    if (call.status == 401) {
        result.error = ErrorCode::InvalidCredentials;
        result.errorMessage = call.json.getString("error", "Invalid application credentials");
        return result;
    }
    if (call.status == 429) {
        result.error = ErrorCode::RateLimited;
        result.errorMessage = call.json.getString("error", "Too many requests");
        return result;
    }
    if (!verifySignature(call.json, apiSecret_)) {
        result.error = ErrorCode::SignatureInvalid;
        result.errorMessage = "Response signature did not verify";
        return result;
    }

    result.ok = call.json.getBool("success", false);
    if (!result.ok) result.errorMessage = call.json.getString("error", "Log rejected");
    return result;
}

}  // namespace licensex
