// saku_auth.hpp - Saku Method license client (single header, Windows, C++17)
//
// No dependencies: uses WinHTTP and BCrypt, both shipped with Windows.
// Visual Studio links them automatically. MinGW: add -lwinhttp -lbcrypt -ladvapi32.
//
// Usage:
//   saku::Client auth("https://your-server", "your-app-id", "your-secret");
//
//   saku::Result r = auth.activate(license_key);
//   if (!r.success) { show(r.message); return 1; }
//
//   auth.start_heartbeat([](const std::string& reason) {
//       // Runs on the heartbeat thread when the license stops being valid.
//       show(reason);
//       ExitProcess(0);
//   });
//
// Every server response is checked with HMAC-SHA256 (your secret) and must echo
// a random nonce sent with the request, so responses can't be forged or replayed.

#pragma once

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

#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <cstdlib>
#include <functional>
#include <mutex>
#include <string>
#include <thread>
#include <utility>
#include <vector>

#ifdef _MSC_VER
#pragma comment(lib, "winhttp.lib")
#pragma comment(lib, "bcrypt.lib")
#pragma comment(lib, "advapi32.lib")
#endif

namespace saku {

struct Result {
    bool success = false;
    std::string message;
    std::string plan;
    long long expires = 0;  // Unix timestamp
};

namespace detail {

inline std::wstring widen(const std::string& s) {
    if (s.empty()) return std::wstring();
    int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), nullptr, 0);
    std::wstring w(n, L'\0');
    MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), &w[0], n);
    return w;
}

inline std::string to_hex(const unsigned char* data, size_t len) {
    static const char digits[] = "0123456789abcdef";
    std::string out(len * 2, '0');
    for (size_t i = 0; i < len; ++i) {
        out[2 * i] = digits[data[i] >> 4];
        out[2 * i + 1] = digits[data[i] & 0x0f];
    }
    return out;
}

// SHA-256 of data, or HMAC-SHA256 when a key is given. Empty string on failure.
inline std::string sha256_hex(const std::string& data, const std::string* key = nullptr) {
    BCRYPT_ALG_HANDLE alg = nullptr;
    BCRYPT_HASH_HANDLE hash = nullptr;
    unsigned char digest[32] = {};
    std::string out;
    ULONG flags = key ? BCRYPT_ALG_HANDLE_HMAC_FLAG : 0;
    if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(&alg, BCRYPT_SHA256_ALGORITHM, nullptr, flags)))
        return out;
    if (BCRYPT_SUCCESS(BCryptCreateHash(alg, &hash, nullptr, 0,
                                        key ? (PUCHAR)key->data() : nullptr,
                                        key ? (ULONG)key->size() : 0, 0)) &&
        BCRYPT_SUCCESS(BCryptHashData(hash, (PUCHAR)data.data(), (ULONG)data.size(), 0)) &&
        BCRYPT_SUCCESS(BCryptFinishHash(hash, digest, sizeof(digest), 0))) {
        out = to_hex(digest, sizeof(digest));
    }
    if (hash) BCryptDestroyHash(hash);
    BCryptCloseAlgorithmProvider(alg, 0);
    return out;
}

inline std::string random_hex(size_t bytes) {
    std::vector<unsigned char> buf(bytes);
    if (!BCRYPT_SUCCESS(BCryptGenRandom(nullptr, buf.data(), (ULONG)buf.size(), BCRYPT_USE_SYSTEM_PREFERRED_RNG)))
        return std::string();
    return to_hex(buf.data(), buf.size());
}

inline bool constant_time_equals(const std::string& a, const std::string& b) {
    if (a.size() != b.size()) return false;
    unsigned char diff = 0;
    for (size_t i = 0; i < a.size(); ++i) diff |= (unsigned char)(a[i] ^ b[i]);
    return diff == 0;
}

inline std::string json_escape(const std::string& s) {
    std::string out;
    for (unsigned 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:
                if (c < 0x20) {
                    char buf[8];
                    snprintf(buf, sizeof(buf), "\\u%04x", c);
                    out += buf;
                } else {
                    out += (char)c;
                }
        }
    }
    return out;
}

// ---- Minimal JSON reader: top-level object, values kept as raw JSON text ----

struct Field {
    std::string key;
    std::string raw;
};

inline void skip_ws(const std::string& s, size_t& i) {
    while (i < s.size() && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r')) ++i;
}

inline bool scan_string(const std::string& s, size_t& i) {
    if (i >= s.size() || s[i] != '"') return false;
    for (++i; i < s.size(); ++i) {
        if (s[i] == '\\') ++i;
        else if (s[i] == '"') { ++i; return true; }
    }
    return false;
}

inline bool scan_value(const std::string& s, size_t& i) {
    if (i >= s.size()) return false;
    if (s[i] == '"') return scan_string(s, i);
    if (s[i] == '{' || s[i] == '[') {
        int depth = 0;
        while (i < s.size()) {
            char c = s[i];
            if (c == '"') {
                if (!scan_string(s, i)) return false;
                continue;
            }
            if (c == '{' || c == '[') ++depth;
            else if ((c == '}' || c == ']') && --depth == 0) { ++i; return true; }
            ++i;
        }
        return false;
    }
    size_t start = i;
    while (i < s.size() && s[i] != ',' && s[i] != '}' && s[i] != ' ' && s[i] != '\t' && s[i] != '\n' && s[i] != '\r') ++i;
    return i > start;
}

inline bool parse_object(const std::string& s, std::vector<Field>& out) {
    size_t i = 0;
    skip_ws(s, i);
    if (i >= s.size() || s[i] != '{') return false;
    ++i;
    skip_ws(s, i);
    if (i < s.size() && s[i] == '}') return true;
    while (i < s.size()) {
        skip_ws(s, i);
        size_t key_start = i;
        if (!scan_string(s, i)) return false;
        std::string key = s.substr(key_start + 1, i - key_start - 2);
        skip_ws(s, i);
        if (i >= s.size() || s[i] != ':') return false;
        ++i;
        skip_ws(s, i);
        size_t value_start = i;
        if (!scan_value(s, i)) return false;
        out.push_back({key, s.substr(value_start, i - value_start)});
        skip_ws(s, i);
        if (i < s.size() && s[i] == ',') { ++i; continue; }
        return i < s.size() && s[i] == '}';
    }
    return false;
}

inline void append_utf8(std::string& out, unsigned long cp) {
    if (cp < 0x80) {
        out += (char)cp;
    } else if (cp < 0x800) {
        out += (char)(0xC0 | (cp >> 6));
        out += (char)(0x80 | (cp & 0x3F));
    } else if (cp < 0x10000) {
        out += (char)(0xE0 | (cp >> 12));
        out += (char)(0x80 | ((cp >> 6) & 0x3F));
        out += (char)(0x80 | (cp & 0x3F));
    } else {
        out += (char)(0xF0 | (cp >> 18));
        out += (char)(0x80 | ((cp >> 12) & 0x3F));
        out += (char)(0x80 | ((cp >> 6) & 0x3F));
        out += (char)(0x80 | (cp & 0x3F));
    }
}

// Raw JSON string token ("...") -> decoded UTF-8 text.
inline std::string decode_string(const std::string& raw) {
    std::string out;
    if (raw.size() < 2 || raw.front() != '"' || raw.back() != '"') return out;
    const size_t end = raw.size() - 1;
    for (size_t i = 1; i < end; ++i) {
        char c = raw[i];
        if (c != '\\') { out += c; continue; }
        if (i + 1 >= end) break;
        char e = raw[++i];
        switch (e) {
            case 'n': out += '\n'; break;
            case 't': out += '\t'; break;
            case 'r': out += '\r'; break;
            case 'b': out += '\b'; break;
            case 'f': out += '\f'; break;
            case 'u': {
                if (i + 4 >= end) return out;
                unsigned long cp = std::strtoul(raw.substr(i + 1, 4).c_str(), nullptr, 16);
                i += 4;
                if (cp >= 0xD800 && cp <= 0xDBFF && i + 6 < end && raw[i + 1] == '\\' && raw[i + 2] == 'u') {
                    unsigned long low = std::strtoul(raw.substr(i + 3, 4).c_str(), nullptr, 16);
                    if (low >= 0xDC00 && low <= 0xDFFF) {
                        cp = 0x10000 + ((cp - 0xD800) << 10) + (low - 0xDC00);
                        i += 6;
                    }
                }
                append_utf8(out, cp);
                break;
            }
            default: out += e;  // \" \\ \/
        }
    }
    return out;
}

inline const std::string* find_raw(const std::vector<Field>& fields, const char* key) {
    for (const auto& f : fields)
        if (f.key == key) return &f.raw;
    return nullptr;
}

// ---- HTTP ----

class InternetHandle {
public:
    explicit InternetHandle(HINTERNET h) : h_(h) {}
    ~InternetHandle() { if (h_) WinHttpCloseHandle(h_); }
    InternetHandle(const InternetHandle&) = delete;
    InternetHandle& operator=(const InternetHandle&) = delete;
    HINTERNET get() const { return h_; }
    explicit operator bool() const { return h_ != nullptr; }

private:
    HINTERNET h_;
};

// False on network failure. HTTP error statuses still return their body.
inline bool http_post_json(const std::wstring& url, const std::string& body, std::string& response) {
    URL_COMPONENTS parts = {};
    parts.dwStructSize = sizeof(parts);
    parts.dwSchemeLength = (DWORD)-1;
    parts.dwHostNameLength = (DWORD)-1;
    parts.dwUrlPathLength = (DWORD)-1;
    parts.dwExtraInfoLength = (DWORD)-1;
    if (!WinHttpCrackUrl(url.c_str(), 0, 0, &parts)) return false;
    std::wstring host(parts.lpszHostName, parts.dwHostNameLength);
    std::wstring path(parts.lpszUrlPath, parts.dwUrlPathLength);
    if (parts.dwExtraInfoLength) path.append(parts.lpszExtraInfo, parts.dwExtraInfoLength);
    if (path.empty()) path = L"/";

    InternetHandle session(WinHttpOpen(L"SakuAuth/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                                       WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0));
    if (!session) return false;
    WinHttpSetTimeouts(session.get(), 10000, 10000, 10000, 15000);
    InternetHandle connection(WinHttpConnect(session.get(), host.c_str(), parts.nPort, 0));
    if (!connection) return false;
    DWORD flags = parts.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0;
    InternetHandle request(WinHttpOpenRequest(connection.get(), L"POST", path.c_str(), nullptr,
                                              WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, flags));
    if (!request) return false;

    static const wchar_t headers[] = L"Content-Type: application/json\r\n";
    if (!WinHttpSendRequest(request.get(), headers, (DWORD)-1L, (LPVOID)body.data(),
                            (DWORD)body.size(), (DWORD)body.size(), 0) ||
        !WinHttpReceiveResponse(request.get(), nullptr))
        return false;

    response.clear();
    for (;;) {
        DWORD available = 0;
        if (!WinHttpQueryDataAvailable(request.get(), &available)) return false;
        if (available == 0) break;
        size_t offset = response.size();
        response.resize(offset + available);
        DWORD read = 0;
        if (!WinHttpReadData(request.get(), &response[offset], available, &read)) return false;
        response.resize(offset + read);
    }
    return true;
}

// ---- Hardware ID ----

inline std::string machine_guid() {
    HKEY key;
    if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography", 0,
                      KEY_READ | KEY_WOW64_64KEY, &key) != ERROR_SUCCESS)
        return std::string();
    char buf[128] = {};
    DWORD size = sizeof(buf) - 1;
    DWORD type = 0;
    LONG rc = RegQueryValueExA(key, "MachineGuid", nullptr, &type, (LPBYTE)buf, &size);
    RegCloseKey(key);
    return rc == ERROR_SUCCESS && type == REG_SZ ? std::string(buf) : std::string();
}

inline std::string system_volume_serial() {
    char windows_dir[MAX_PATH] = {};
    if (GetWindowsDirectoryA(windows_dir, MAX_PATH) < 3) return std::string();
    char root[4] = {windows_dir[0], ':', '\\', '\0'};
    DWORD serial = 0;
    if (!GetVolumeInformationA(root, nullptr, 0, &serial, nullptr, nullptr, nullptr, 0)) return std::string();
    char buf[16];
    snprintf(buf, sizeof(buf), "%08lX", (unsigned long)serial);
    return buf;
}

inline std::string computer_name() {
    char buf[MAX_COMPUTERNAME_LENGTH + 1] = {};
    DWORD size = sizeof(buf);
    return GetComputerNameA(buf, &size) ? std::string(buf, size) : std::string();
}

}  // namespace detail

// Stable per-machine identifier (SHA-256 of the Windows machine GUID and system volume serial).
inline std::string get_hwid() {
    std::string guid = detail::machine_guid();
    if (guid.empty()) guid = detail::computer_name();
    return detail::sha256_hex("saku|" + guid + "|" + detail::system_volume_serial());
}

class Client {
public:
    Client(std::string server_url, std::string app_id, std::string secret)
        : server_(std::move(server_url)), app_id_(std::move(app_id)), secret_(std::move(secret)) {
        while (!server_.empty() && server_.back() == '/') server_.pop_back();
    }

    ~Client() { stop_heartbeat(); }

    Client(const Client&) = delete;
    Client& operator=(const Client&) = delete;

    // Checks the key and binds it to this machine on first use.
    Result activate(const std::string& license_key) {
        Result result;
        Response res = post("activate", "\"key\":\"" + detail::json_escape(license_key) +
                                            "\",\"hwid\":\"" + detail::json_escape(get_hwid()) + "\"");
        if (!res.ok) {
            result.message = res.error;
            return result;
        }
        result.success = get_bool(res, "success");
        result.message = get_string(res, "message");
        result.plan = get_string(res, "plan");
        result.expires = get_int(res, "expires");
        if (result.success) {
            std::lock_guard<std::mutex> lock(state_mutex_);
            session_ = get_string(res, "session");
            plan_ = result.plan;
            expires_ = result.expires;
        }
        return result;
    }

    // Keeps the session alive in a background thread. on_invalid is called once (on that
    // thread) when the key is banned/expired/used elsewhere or the server stays unreachable.
    // The server drops sessions after 120 s without heartbeat, so interval is capped at 90 s.
    void start_heartbeat(std::function<void(const std::string& reason)> on_invalid,
                         std::chrono::seconds interval = std::chrono::seconds(60)) {
        stop_heartbeat();
        if (interval > std::chrono::seconds(90)) interval = std::chrono::seconds(90);
        running_ = true;
        worker_ = std::thread([this, on_invalid = std::move(on_invalid), interval] {
            int network_failures = 0;
            while (wait_or_stop(network_failures ? kRetryDelay : interval)) {
                std::string reason;
                Status status = check_session(reason);
                if (status == Status::Ok) {
                    network_failures = 0;
                    continue;
                }
                if (status == Status::Transient && ++network_failures < kMaxNetworkFailures) continue;
                running_ = false;
                if (on_invalid) on_invalid(reason);
                return;
            }
        });
    }

    void stop_heartbeat() {
        {
            std::lock_guard<std::mutex> lock(wait_mutex_);
            running_ = false;
        }
        stop_cv_.notify_all();
        if (worker_.joinable()) {
            if (worker_.get_id() == std::this_thread::get_id()) worker_.detach();
            else worker_.join();
        }
    }

    bool is_active() const {
        std::lock_guard<std::mutex> lock(state_mutex_);
        return !session_.empty();
    }

    std::string plan() const {
        std::lock_guard<std::mutex> lock(state_mutex_);
        return plan_;
    }

    long long expires() const {
        std::lock_guard<std::mutex> lock(state_mutex_);
        return expires_;
    }

private:
    enum class Status { Ok, Invalid, Transient };

    struct Response {
        bool ok = false;
        bool transient = false;  // network error or non-JSON reply (e.g. server restarting)
        std::string error;
        std::vector<detail::Field> fields;
    };

    static constexpr int kMaxNetworkFailures = 4;
    static constexpr std::chrono::seconds kRetryDelay{15};

    Response post(const char* endpoint, const std::string& json_members) {
        Response res;
        const std::string nonce = detail::random_hex(16);
        const std::string body = "{" + json_members + ",\"nonce\":\"" + nonce + "\"}";
        std::string raw;
        if (nonce.empty() ||
            !detail::http_post_json(detail::widen(server_ + "/api/v1/" + app_id_ + "/" + endpoint), body, raw)) {
            res.transient = true;
            res.error = "Impossible de joindre le serveur de licences.";
            return res;
        }
        if (!detail::parse_object(raw, res.fields)) {
            res.transient = true;
            res.error = "Reponse inattendue du serveur de licences.";
            return res;
        }
        if (!signature_valid(res.fields, nonce)) {
            res.error = "Signature du serveur invalide.";
            return res;
        }
        res.ok = true;
        return res;
    }

    // Rebuilds the JSON the server signed (sorted keys, no spaces, without _sig/_ts)
    // and checks _sig = HMAC-SHA256(secret, _ts + json). The nonce must match this request.
    bool signature_valid(const std::vector<detail::Field>& fields, const std::string& nonce) const {
        const std::string* sig = detail::find_raw(fields, "_sig");
        const std::string* ts = detail::find_raw(fields, "_ts");
        const std::string* echoed = detail::find_raw(fields, "nonce");
        if (!sig || !ts || !echoed || detail::decode_string(*echoed) != nonce) return false;

        std::vector<const detail::Field*> signed_fields;
        for (const auto& f : fields)
            if (f.key != "_sig" && f.key != "_ts") signed_fields.push_back(&f);
        std::sort(signed_fields.begin(), signed_fields.end(),
                  [](const detail::Field* a, const detail::Field* b) { return a->key < b->key; });
        std::string canonical = "{";
        for (size_t i = 0; i < signed_fields.size(); ++i) {
            if (i) canonical += ',';
            canonical += "\"" + signed_fields[i]->key + "\":" + signed_fields[i]->raw;
        }
        canonical += '}';

        const std::string expected = detail::sha256_hex(detail::decode_string(*ts) + canonical, &secret_);
        return !expected.empty() && detail::constant_time_equals(expected, detail::decode_string(*sig));
    }

    Status check_session(std::string& reason) {
        std::string session;
        {
            std::lock_guard<std::mutex> lock(state_mutex_);
            session = session_;
        }
        if (session.empty()) {
            reason = "Aucune session active.";
            return Status::Invalid;
        }
        Response res = post("heartbeat", "\"session\":\"" + detail::json_escape(session) + "\"");
        if (!res.ok) {
            reason = res.error;
            return res.transient ? Status::Transient : Status::Invalid;
        }
        std::lock_guard<std::mutex> lock(state_mutex_);
        if (!get_bool(res, "success")) {
            reason = get_string(res, "message");
            session_.clear();
            return Status::Invalid;
        }
        plan_ = get_string(res, "plan");
        expires_ = get_int(res, "expires");
        return Status::Ok;
    }

    // Sleeps for duration; returns false as soon as stop_heartbeat() is called.
    bool wait_or_stop(std::chrono::seconds duration) {
        std::unique_lock<std::mutex> lock(wait_mutex_);
        return !stop_cv_.wait_for(lock, duration, [this] { return !running_.load(); });
    }

    static std::string get_string(const Response& res, const char* key) {
        const std::string* raw = detail::find_raw(res.fields, key);
        return raw ? detail::decode_string(*raw) : std::string();
    }

    static long long get_int(const Response& res, const char* key) {
        const std::string* raw = detail::find_raw(res.fields, key);
        return raw ? std::atoll(raw->c_str()) : 0;
    }

    static bool get_bool(const Response& res, const char* key) {
        const std::string* raw = detail::find_raw(res.fields, key);
        return raw && *raw == "true";
    }

    std::string server_;
    std::string app_id_;
    std::string secret_;

    mutable std::mutex state_mutex_;
    std::string session_;
    std::string plan_;
    long long expires_ = 0;

    std::atomic<bool> running_{false};
    std::mutex wait_mutex_;
    std::condition_variable stop_cv_;
    std::thread worker_;
};

}  // namespace saku
