// AuthGuard client SDK for C++17 on Windows.
// Dependencies (install with vcpkg):  vcpkg install libsodium nlohmann-json
// Link with: winhttp.lib, libsodium.lib
#pragma once

#include <windows.h>
#include <winhttp.h>
#include <sodium.h>
#include <nlohmann/json.hpp>

#include <chrono>
#include <cstdlib>
#include <stdexcept>
#include <string>
#include <vector>

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

namespace authguard {

using json = nlohmann::json;

class Error : public std::runtime_error {
public:
    json response;
    explicit Error(const std::string& msg, json resp = json::object()) : std::runtime_error(msg), response(std::move(resp)) {}
    bool update_required() const { return response.value("update_required", false); }
    std::string download_url() const { return response.contains("download_url") && response["download_url"].is_string() ? response["download_url"].get<std::string>() : ""; }
};

inline std::string to_hex(const unsigned char* data, size_t len) {
    std::string out(len * 2 + 1, '\0');
    sodium_bin2hex(out.data(), out.size(), data, len);
    out.resize(len * 2);
    return out;
}

inline std::vector<unsigned char> from_hex(const std::string& hex) {
    std::vector<unsigned char> out(hex.size() / 2);
    size_t len = 0;
    if (sodium_hex2bin(out.data(), out.size(), hex.c_str(), hex.size(), nullptr, &len, nullptr) != 0) throw Error("Invalid server signature");
    out.resize(len);
    return out;
}

inline std::string get_hwid() {
    char guid[64] = {};
    DWORD size = sizeof(guid);
    std::string raw = "unknown";
    if (RegGetValueA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography", "MachineGuid", RRF_RT_REG_SZ | RRF_SUBKEY_WOW6464KEY, nullptr, guid, &size) == ERROR_SUCCESS)
        raw = guid;
    unsigned char hash[crypto_hash_sha256_BYTES];
    crypto_hash_sha256(hash, reinterpret_cast<const unsigned char*>(raw.data()), raw.size());
    return to_hex(hash, sizeof(hash));
}

class AuthGuard {
public:
    json info = json::object();
    std::string hwid;

    // url like "https://authguard.dk/api/client"
    AuthGuard(std::string app_id, std::string version, const std::string& public_key_hex, std::string url = "https://authguard.dk/api/client")
        : app_id_(std::move(app_id)), version_(std::move(version)), url_(std::move(url)) {
        if (sodium_init() < 0) throw Error("libsodium failed to initialize");
        public_key_ = from_hex(public_key_hex);
        if (public_key_.size() != crypto_sign_PUBLICKEYBYTES) throw Error("Public key has the wrong length");
        hwid = get_hwid();
    }

    void init() {
        json r = request("init", {{"version", version_}, {"hwid", hwid}});
        session_id_ = r["sessionid"].get<std::string>();
    }
    void license(const std::string& key) { info = request("license", {{"key", key}, {"hwid", hwid}})["info"]; }
    void login(const std::string& user, const std::string& pass) { info = request("login", {{"username", user}, {"password", pass}, {"hwid", hwid}})["info"]; }
    void register_user(const std::string& user, const std::string& pass, const std::string& key) {
        info = request("register", {{"username", user}, {"password", pass}, {"key", key}, {"hwid", hwid}})["info"];
    }
    std::string var(const std::string& name) { return request("var", {{"name", name}})["value"].get<std::string>(); }
    bool check() {
        try { request("check", json::object()); return true; } catch (const Error&) { return false; }
    }

private:
    std::string app_id_, version_, url_, session_id_;
    std::vector<unsigned char> public_key_;

    json request(const std::string& type, json fields) {
        unsigned char nonce_bytes[16];
        randombytes_buf(nonce_bytes, sizeof(nonce_bytes));
        std::string nonce = to_hex(nonce_bytes, sizeof(nonce_bytes));

        fields["type"] = type;
        fields["app_id"] = app_id_;
        fields["nonce"] = nonce;
        if (!session_id_.empty() && type != "init") fields["sessionid"] = session_id_;

        json outer;
        try { outer = json::parse(post(fields.dump())); } catch (const json::exception&) { throw Error("Could not reach the license server"); }
        if (!outer.contains("payload") || !outer.contains("signature")) throw Error("Could not reach the license server");

        std::string payload = outer["payload"].get<std::string>();
        auto sig = from_hex(outer["signature"].get<std::string>());
        if (sig.size() != crypto_sign_BYTES ||
            crypto_sign_verify_detached(sig.data(), reinterpret_cast<const unsigned char*>(payload.data()), payload.size(), public_key_.data()) != 0)
            throw Error("Invalid server signature");

        json data = json::parse(payload);
        long long now = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
        if (data.value("nonce", "") != nonce || std::llabs(now - data.value("timestamp", 0LL)) > 300) throw Error("Invalid server response");
        if (!data.value("success", false)) throw Error(data.value("message", "Request failed"), data);
        return data;
    }

    std::string post(const std::string& body) {
        std::wstring wurl(url_.begin(), url_.end());
        URL_COMPONENTS uc{};
        uc.dwStructSize = sizeof(uc);
        wchar_t host[256], path[1024];
        uc.lpszHostName = host; uc.dwHostNameLength = 256;
        uc.lpszUrlPath = path; uc.dwUrlPathLength = 1024;
        if (!WinHttpCrackUrl(wurl.c_str(), 0, 0, &uc)) throw Error("Invalid server URL");

        // The user agent is explicit because some hosts' firewalls block requests without one.
        HINTERNET session = WinHttpOpen(L"AuthGuard-SDK/1.0", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
        HINTERNET conn = session ? WinHttpConnect(session, host, uc.nPort, 0) : nullptr;
        DWORD flags = uc.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0;
        HINTERNET req = conn ? WinHttpOpenRequest(conn, L"POST", path, nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, flags) : nullptr;

        std::string result;
        bool ok = req &&
            WinHttpSendRequest(req, L"Content-Type: application/json\r\n", (DWORD)-1L, (LPVOID)body.data(), (DWORD)body.size(), (DWORD)body.size(), 0) &&
            WinHttpReceiveResponse(req, nullptr);
        if (ok) {
            DWORD avail = 0;
            while (WinHttpQueryDataAvailable(req, &avail) && avail > 0) {
                std::string chunk(avail, '\0');
                DWORD read = 0;
                if (!WinHttpReadData(req, chunk.data(), avail, &read)) { ok = false; break; }
                result.append(chunk.data(), read);
            }
        }
        if (req) WinHttpCloseHandle(req);
        if (conn) WinHttpCloseHandle(conn);
        if (session) WinHttpCloseHandle(session);
        if (!ok) throw Error("Could not reach the license server");
        return result;
    }
};

}  // namespace authguard
