// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Pure (no-I/O) core of the daemon updater: release-JSON parsing, platform/asset matching, the
// markdown checksum-table parser, version normalization, and signature-asset selection. Split from
// daemon_updater.cpp (the libcurl/miniz worker) so it can be unit-tested without curl/miniz. The
// generic SHA-256 / ed25519 primitives are reused from the miner updater (xmrig_updater_core.cpp).

#include "daemon_updater.h"

#include <nlohmann/json.hpp>

#include <algorithm>
#include <cctype>
#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 a DaemonRelease from one Gitea release JSON object (ok=true iff it has a tag).
DaemonRelease parseOneDaemonRelease(const json& j)
{
    DaemonRelease 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;
            DaemonReleaseAsset 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

DaemonRelease parseDaemonRelease(const std::string& jsonStr)
{
    DaemonRelease r;
    try {
        r = parseOneDaemonRelease(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<DaemonRelease> parseDaemonReleaseList(const std::string& jsonStr)
{
    std::vector<DaemonRelease> 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)
            DaemonRelease r = parseOneDaemonRelease(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 currentDaemonPlatformToken()
{
#if defined(_WIN32)
    return "win64";
#elif defined(__APPLE__)
    return "macos";          // single macOS archive (no arm/x86 split in the release naming)
#elif defined(__linux__)
  #if defined(__aarch64__)
    return "linux-arm64";    // no arm64 build published yet -> resolves to Unavailable
  #else
    return "linux-amd64";
  #endif
#else
    return "";
#endif
}

int selectDaemonAsset(const DaemonRelease& 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> parseDaemonChecksums(const std::string& body)
{
    // The release body publishes checksums as a markdown table:
    //   | File | SHA-256 |
    //   |------|---------|
    //   | dragonx-1.0.2-linux-amd64.zip | `85f1dd…16` |
    // Per line: blank out the table/code/emphasis delimiters ('|', '`', and markdown '*'/'_' so a
    // bolded **archive.zip** still tokenizes), then find the 64-hex token (the hash) and a token
    // ending in ".zip" (the archive name). Header/separator/prose rows lack one or the other and are
    // skipped, so this is robust to surrounding text and column order.
    std::map<std::string, std::string> out;
    std::istringstream in(body);
    std::string line;
    while (std::getline(in, line)) {
        for (char& c : line)
            if (c == '|' || c == '`' || c == '*' || c == '_') c = ' ';
        std::istringstream ls(line);
        std::string tok, hash, name;
        while (ls >> tok) {
            if (hash.empty() && isHex64(tok)) { hash = toLower(tok); continue; }
            if (name.empty()) {
                const std::string low = toLower(tok);
                // Key by the lowercased name so the lookup (also lowercased) is case-insensitive,
                // in case the markdown table and the JSON asset names differ in case.
                if (low.size() >= 4 && low.compare(low.size() - 4, 4, ".zip") == 0) name = low;
            }
        }
        if (!hash.empty() && !name.empty()) out[name] = hash;
    }
    return out;
}

std::vector<std::string> daemonExtractBasenames(const std::string& platformToken)
{
    if (platformToken.rfind("win", 0) == 0)
        return {"dragonxd.exe", "dragonx-cli.exe", "dragonx-tx.exe"};
    return {"dragonxd", "dragonx-cli", "dragonx-tx"};
}

int selectDaemonSignatureAsset(const DaemonRelease& 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;
}

std::string daemonVersionCore(const std::string& version)
{
    // Extract a leading "v?MAJOR.MINOR.PATCH" run, ignoring any "-<commit>" / build suffix. Hand-
    // rolled (no <regex>) to stay light and dependency-free.
    const std::size_t start = version.find_first_of("0123456789");
    if (start == std::string::npos) return version;
    std::size_t i = start;
    int dots = 0;
    for (; i < version.size(); ++i) {
        const char c = version[i];
        if (c >= '0' && c <= '9') continue;
        if (c == '.' && dots < 2) { ++dots; continue; }
        break;
    }
    if (dots < 2) return version;   // not a full N.N.N — leave as-is
    const bool hasV = start > 0 && (version[start - 1] == 'v' || version[start - 1] == 'V');
    return (hasV ? "v" : "") + version.substr(start, i - start);
}

} // namespace util
} // namespace dragonx
