// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Pure (no-I/O) core of the xmrig updater: release-JSON parsing, platform/asset matching, checksum
// parsing, and SHA-256 hashing. Split from xmrig_updater.cpp (the libcurl/miniz worker) so it can
// be unit-tested in the test binary, which links libsodium + nlohmann/json but not curl/miniz.

#include "xmrig_updater.h"

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

#include <algorithm>
#include <cctype>
#include <cstring>
#include <sstream>

using json = nlohmann::json;

namespace dragonx {
namespace util {

namespace {

std::string toLower(std::string s)
{
    std::transform(s.begin(), s.end(), s.begin(),
                   [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
    return s;
}

bool isHex64(const std::string& s)
{
    if (s.size() != 64) return false;
    for (unsigned char c : s)
        if (!std::isxdigit(c)) return false;
    return true;
}

} // namespace

namespace {
// Fill an XmrigRelease from one Gitea release JSON object (ok=true iff it has a tag).
XmrigRelease parseOneXmrigRelease(const json& j)
{
    XmrigRelease r;
    if (!j.is_object()) return r;
    if (j.contains("tag_name") && j["tag_name"].is_string())
        r.tag = j["tag_name"].get<std::string>();
    if (j.contains("name") && j["name"].is_string())
        r.name = j["name"].get<std::string>();
    if (j.contains("body") && j["body"].is_string())
        r.body = j["body"].get<std::string>();
    if (j.contains("prerelease") && j["prerelease"].is_boolean())
        r.prerelease = j["prerelease"].get<bool>();
    if (j.contains("published_at") && j["published_at"].is_string())
        r.publishedAt = j["published_at"].get<std::string>();
    if (j.contains("assets") && j["assets"].is_array()) {
        for (const auto& a : j["assets"]) {
            if (!a.is_object()) continue;
            XmrigReleaseAsset asset;
            if (a.contains("name") && a["name"].is_string())
                asset.name = a["name"].get<std::string>();
            if (a.contains("browser_download_url") && a["browser_download_url"].is_string())
                asset.downloadUrl = a["browser_download_url"].get<std::string>();
            if (a.contains("size") && a["size"].is_number_integer())
                asset.size = a["size"].get<long long>();
            if (!asset.name.empty() && !asset.downloadUrl.empty())
                r.assets.push_back(std::move(asset));
        }
    }
    if (!r.tag.empty()) r.ok = true;
    return r;
}
} // namespace

XmrigRelease parseXmrigRelease(const std::string& jsonStr)
{
    XmrigRelease r;
    try {
        r = parseOneXmrigRelease(json::parse(jsonStr));
        if (!r.ok) r.error = "release JSON has no tag_name";
    } catch (const std::exception& e) {
        r.error = std::string("failed to parse release JSON: ") + e.what();
    }
    return r;
}

std::vector<XmrigRelease> parseXmrigReleaseList(const std::string& jsonStr)
{
    std::vector<XmrigRelease> out;
    try {
        const json j = json::parse(jsonStr);
        if (!j.is_array()) return out;
        for (const auto& e : j) {
            if (e.contains("draft") && e["draft"].is_boolean() && e["draft"].get<bool>())
                continue;   // skip drafts (not meant for end users)
            XmrigRelease r = parseOneXmrigRelease(e);
            if (r.ok) out.push_back(std::move(r));
        }
    } catch (const std::exception&) {
        // malformed list -> empty (caller treats as "could not load releases")
    }
    return out;
}

std::string currentXmrigPlatformToken()
{
#if defined(_WIN32)
    return "win-x64";
#elif defined(__APPLE__)
  #if defined(__aarch64__) || defined(__arm64__)
    return "macos-arm64";   // no arm64 build published yet -> resolves to Unavailable
  #else
    return "macos-x86_64";  // matches the release asset naming (note: not "macos-x64")
  #endif
#elif defined(__linux__)
  #if defined(__aarch64__)
    return "linux-arm64";
  #else
    return "linux-x64";
  #endif
#else
    return "";
#endif
}

int selectXmrigAsset(const XmrigRelease& release, const std::string& platformToken)
{
    if (platformToken.empty()) return -1;
    const std::string needle = "-" + toLower(platformToken) + ".zip";
    for (std::size_t i = 0; i < release.assets.size(); ++i) {
        const std::string n = toLower(release.assets[i].name);
        if (n.size() >= needle.size() &&
            n.compare(n.size() - needle.size(), needle.size(), needle) == 0)
            return static_cast<int>(i);
    }
    return -1;
}

std::map<std::string, std::string> parseXmrigChecksums(const std::string& body)
{
    std::map<std::string, std::string> out;
    std::istringstream in(body);
    std::string line;
    while (std::getline(in, line)) {
        std::istringstream ls(line);
        std::string hash, name;
        if (!(ls >> hash)) continue;
        if (!isHex64(hash)) continue;
        if (!(ls >> name)) continue;
        // First whitespace-delimited token after the hash is the key (zip name, or "xmrig"/
        // "xmrig.exe" for the inner-binary block; any trailing "(linux-x64)" annotation is ignored).
        out[name] = toLower(hash);
    }
    return out;
}

std::string sha256Hex(const void* data, std::size_t len)
{
    unsigned char hash[crypto_hash_sha256_BYTES];
    if (crypto_hash_sha256(hash, static_cast<const unsigned char*>(data), len) != 0)
        return {};
    static const char* kHex = "0123456789abcdef";
    std::string out;
    out.reserve(crypto_hash_sha256_BYTES * 2);
    for (unsigned char c : hash) {
        out.push_back(kHex[c >> 4]);
        out.push_back(kHex[c & 0x0F]);
    }
    return out;
}

std::vector<std::string> xmrigExtractBasenames(const std::string& platformToken)
{
    if (platformToken.rfind("win", 0) == 0)
        return {"xmrig.exe", "WinRing0x64.sys"};
    return {"xmrig"};
}

int selectXmrigSignatureAsset(const XmrigRelease& release, const std::string& archiveName)
{
    if (archiveName.empty()) return -1;
    for (std::size_t i = 0; i < release.assets.size(); ++i) {
        const std::string& n = release.assets[i].name;
        if (n == archiveName + ".sig" || n == archiveName + ".minisig")
            return static_cast<int>(i);
    }
    return -1;
}

namespace {
// Decode base64 (whitespace tolerated) into exactly `expected` bytes; false on any mismatch.
bool base64ToFixed(const std::string& b64, unsigned char* out, std::size_t expected)
{
    std::size_t binLen = 0;
    const char* end = nullptr;
    if (sodium_base642bin(out, expected, b64.data(), b64.size(), " \t\r\n",
                          &binLen, &end, sodium_base64_VARIANT_ORIGINAL) != 0)
        return false;
    return binLen == expected;
}
} // namespace

bool verifyXmrigSignature(const std::string& data,
                          const std::string& signatureFileContent,
                          const std::string& pubKeyBase64)
{
    if (sodium_init() < 0) return false;

    unsigned char pubKey[crypto_sign_PUBLICKEYBYTES];
    if (!base64ToFixed(pubKeyBase64, pubKey, sizeof(pubKey))) return false;

    // Accept either base64 (what the .sig assets contain) or a raw 64-byte signature. Try base64
    // first on whitespace-trimmed text; only if that fails fall back to RAW bytes, using the EXACT
    // untrimmed content — so a raw signature byte that happens to equal a whitespace value (~1.6% of
    // random signatures end in one) is never stripped/corrupted. A raw 64-byte input can't be
    // mistaken for base64: 64 base64 chars decode to 48 bytes, never 64, so base64ToFixed rejects it.
    std::string trimmed = signatureFileContent;
    while (!trimmed.empty() &&
           (trimmed.back() == '\n' || trimmed.back() == '\r' ||
            trimmed.back() == ' '  || trimmed.back() == '\t'))
        trimmed.pop_back();

    unsigned char sig[crypto_sign_BYTES];
    if (!base64ToFixed(trimmed, sig, sizeof(sig))) {
        if (signatureFileContent.size() == crypto_sign_BYTES)
            std::memcpy(sig, signatureFileContent.data(), crypto_sign_BYTES);
        else
            return false;
    }

    return crypto_sign_verify_detached(
               sig, reinterpret_cast<const unsigned char*>(data.data()), data.size(), pubKey) == 0;
}

} // namespace util
} // namespace dragonx
