// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// DaemonUpdater background worker (libcurl download + miniz extract). The pure, no-I/O helpers it
// calls (release parsing, asset/platform matching, the checksum-table parser, version core) live in
// daemon_updater_core.cpp; the generic SHA-256 / ed25519 verification is reused from the miner
// updater (xmrig_updater_core.cpp) via util::sha256Hex / util::verifyXmrigSignature.

#include "daemon_updater.h"
#include "xmrig_updater.h"   // util::sha256Hex, util::verifyXmrigSignature (shared crypto)

#include "http_download.h"
#include "logger.h"

#include <curl/curl.h>
#include <miniz.h>

#include <algorithm>
#include <cctype>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iterator>

namespace fs = std::filesystem;

namespace dragonx {
namespace util {

namespace {

// Bound the archive/member sizes before they can fill memory/disk — a defense even ahead of the
// checksum check (which only runs once the file is fully downloaded). The full-node archive bundles
// the daemon binaries plus Sapling params, so the caps are well above the miner updater's.
constexpr curl_off_t kMaxArchiveBytes = 256LL * 1024 * 1024;  // 256 MiB
constexpr std::size_t kMaxMemberBytes = 128u * 1024 * 1024;   // 128 MiB per extracted file

std::string baseName(const std::string& path)
{
    const auto slash = path.find_last_of("/\\");
    return slash == std::string::npos ? path : path.substr(slash + 1);
}

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;
}

} // namespace

DaemonUpdater::~DaemonUpdater()
{
    cancel_requested_ = true;
    if (worker_.joinable()) worker_.join();
}

void DaemonUpdater::setProgress(State state, const std::string& text, double done, double total)
{
    std::lock_guard<std::mutex> lk(mutex_);
    progress_.state = state;
    progress_.status_text = text;
    if (done > 0) progress_.downloaded_bytes = done;
    if (total > 0) progress_.total_bytes = total;
    progress_.percent = (progress_.total_bytes > 0)
        ? static_cast<float>(100.0 * progress_.downloaded_bytes / progress_.total_bytes)
        : progress_.percent;
    if (state == State::Failed) progress_.error = text;
}

DaemonUpdater::Progress DaemonUpdater::getProgress() const
{
    std::lock_guard<std::mutex> lk(mutex_);
    return progress_;
}

bool DaemonUpdater::onDownloadProgress(double downloadedBytes, double totalBytes)
{
    {
        std::lock_guard<std::mutex> lk(mutex_);
        progress_.downloaded_bytes = downloadedBytes;
        if (totalBytes > 0) progress_.total_bytes = totalBytes;
        progress_.percent = (progress_.total_bytes > 0)
            ? static_cast<float>(100.0 * progress_.downloaded_bytes / progress_.total_bytes)
            : progress_.percent;
    }
    return !cancel_requested_.load();   // false -> curl aborts the transfer
}

bool DaemonUpdater::isDone() const
{
    std::lock_guard<std::mutex> lk(mutex_);
    return progress_.state == State::Done || progress_.state == State::Failed ||
           progress_.state == State::Unavailable;
}

void DaemonUpdater::cancel()
{
    cancel_requested_ = true;
}

std::string DaemonUpdater::httpGet(const std::string& url)
{
    return httpGetString(url, "[daemon-updater]");
}

bool DaemonUpdater::downloadToFile(const std::string& url, const std::string& destPath)
{
    return httpDownloadToFile(url, destPath, "[daemon-updater]", kMaxArchiveBytes,
        [this](double downloaded, double total) { return onDownloadProgress(downloaded, total); });
}

void DaemonUpdater::startCheck(const std::string& installedVersion)
{
    if (worker_running_.exchange(true)) return;
    cancel_requested_ = false;
    if (worker_.joinable()) worker_.join();
    {
        std::lock_guard<std::mutex> lk(mutex_);
        progress_ = Progress{};
        progress_.installed_tag = installedVersion;
        progress_.state = State::Checking;
        progress_.status_text = "Checking for the latest node…";
    }
    worker_ = std::thread([this, installedVersion] { runCheck(installedVersion); worker_running_ = false; });
}

void DaemonUpdater::startInstall(const std::string& targetDir)
{
    if (worker_running_.exchange(true)) return;
    cancel_requested_ = false;
    if (worker_.joinable()) worker_.join();
    {
        std::lock_guard<std::mutex> lk(mutex_);
        progress_.state = State::Downloading;
        progress_.error.clear();
        progress_.status_text = "Preparing…";
        progress_.downloaded_bytes = 0;   // clear any prior op's progress so the bar starts at 0%
        progress_.total_bytes = 0;
        progress_.percent = 0.0f;
    }
    worker_ = std::thread([this, targetDir] { runInstall(targetDir); worker_running_ = false; });
}

void DaemonUpdater::startListReleases()
{
    if (worker_running_.exchange(true)) return;
    cancel_requested_ = false;
    if (worker_.joinable()) worker_.join();
    {
        std::lock_guard<std::mutex> lk(mutex_);
        progress_.state = State::Listing;
        progress_.error.clear();
        progress_.status_text = "Loading releases…";
        progress_.downloaded_bytes = 0;   // clear any prior op's progress
        progress_.total_bytes = 0;
        progress_.percent = 0.0f;
    }
    worker_ = std::thread([this] { runListReleases(); worker_running_ = false; });
}

void DaemonUpdater::startInstallRelease(const std::string& targetDir, DaemonRelease release)
{
    if (worker_running_.exchange(true)) return;
    cancel_requested_ = false;
    if (worker_.joinable()) worker_.join();
    {
        std::lock_guard<std::mutex> lk(mutex_);
        progress_.state = State::Downloading;
        progress_.error.clear();
        progress_.status_text = "Preparing…";
        progress_.downloaded_bytes = 0;   // clear any prior op's progress so the bar starts at 0%
        progress_.total_bytes = 0;
        progress_.percent = 0.0f;
    }
    worker_ = std::thread([this, targetDir, release = std::move(release)] {
        installResolved(targetDir, release);
        worker_running_ = false;
    });
}

std::vector<DaemonRelease> DaemonUpdater::getReleases() const
{
    std::lock_guard<std::mutex> lk(mutex_);
    return releases_;
}

void DaemonUpdater::runListReleases()
{
    const std::string body = httpGet(kReleasesUrl);
    if (body.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; }
    std::vector<DaemonRelease> list = parseDaemonReleaseList(body);
    const bool empty = list.empty();
    {
        std::lock_guard<std::mutex> lk(mutex_);
        releases_ = std::move(list);
    }
    if (empty) { setProgress(State::Failed, "No releases found."); return; }
    setProgress(State::ReleaseList, "Select a version to install.");
}

void DaemonUpdater::runCheck(std::string installedVersion)
{
    const std::string body = httpGet(kApiUrl);
    if (body.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; }
    const DaemonRelease rel = parseDaemonRelease(body);
    if (!rel.ok) { setProgress(State::Failed, rel.error.empty() ? "Invalid release data." : rel.error); return; }

    const std::string token = currentDaemonPlatformToken();
    const int idx = selectDaemonAsset(rel, token);
    {
        std::lock_guard<std::mutex> lk(mutex_);
        progress_.latest_tag = rel.tag;
        progress_.installed_tag = installedVersion;
    }
    if (idx < 0) {
        setProgress(State::Unavailable, "No node build is available for this platform (" +
                                       (token.empty() ? "unknown" : token) + ").");
        return;
    }
    // Compare by vN.N.N core so an installed "v1.0.2-<commit>" matches a release "v1.0.2".
    const std::string instCore = daemonVersionCore(installedVersion);
    const bool updateAvailable = instCore.empty() || instCore != daemonVersionCore(rel.tag);
    {
        std::lock_guard<std::mutex> lk(mutex_);
        progress_.update_available = updateAvailable;
    }
    if (updateAvailable)
        setProgress(State::UpdateAvailable, "A new node version is available (" + rel.tag + ").");
    else
        setProgress(State::UpToDate, "The node is up to date (" + rel.tag + ").");
}

void DaemonUpdater::runInstall(std::string targetDir)
{
    setProgress(State::Checking, "Checking for the latest node…");
    const std::string apiBody = httpGet(kApiUrl);
    if (apiBody.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; }
    const DaemonRelease rel = parseDaemonRelease(apiBody);
    if (!rel.ok) { setProgress(State::Failed, rel.error.empty() ? "Invalid release data." : rel.error); return; }
    installResolved(targetDir, rel);
}

void DaemonUpdater::installResolved(const std::string& targetDir, const DaemonRelease& rel)
{
    const std::string token = currentDaemonPlatformToken();
    const int idx = selectDaemonAsset(rel, token);
    if (idx < 0) {
        setProgress(State::Unavailable, "No node build is available for this platform (" +
                                       (token.empty() ? "unknown" : token) + ").");
        return;
    }
    const DaemonReleaseAsset& asset = rel.assets[idx];
    const auto checksums = parseDaemonChecksums(rel.body);
    {
        std::lock_guard<std::mutex> lk(mutex_);
        progress_.latest_tag = rel.tag;
    }
    if (cancel_requested_) { setProgress(State::Failed, "Cancelled."); return; }

    std::error_code ec;
    fs::create_directories(targetDir, ec);

    // 1. Download the archive.
    const std::string zipPath = (fs::path(targetDir) / ".dragonx-daemon-download.zip").string();
    setProgress(State::Downloading, "Downloading " + asset.name + "…", 0,
                static_cast<double>(asset.size));
    if (!downloadToFile(asset.downloadUrl, zipPath)) {
        setProgress(State::Failed, "Download failed.");
        return;
    }
    if (cancel_requested_) { fs::remove(zipPath, ec); setProgress(State::Failed, "Cancelled."); return; }

    // 2. Verify the downloaded archive. Read it once, then (a) compare its SHA-256 to the published
    //    checksum and (b) verify a detached ed25519 signature over the archive bytes against the
    //    pinned key, so a checksum rewritten in a tampered release body is not sufficient to install.
    setProgress(State::Verifying, "Verifying download…");
    std::string bytes;   // kept in scope through extraction so we extract the VERIFIED buffer (I-01)
    {
        std::ifstream f(zipPath, std::ios::binary);
        if (!f) {
            fs::remove(zipPath, ec);
            setProgress(State::Failed, "Could not read the downloaded archive.");
            return;
        }
        bytes.assign(std::istreambuf_iterator<char>(f), std::istreambuf_iterator<char>());
        if (f.bad()) {
            fs::remove(zipPath, ec);
            setProgress(State::Failed, "Could not read the downloaded archive.");
            return;
        }

        // 2a. SHA-256 (integrity / transit corruption).
        const std::string actual = sha256Hex(bytes.data(), bytes.size());
        const auto it = checksums.find(toLower(asset.name));  // keys are lowercased
        if (it == checksums.end()) {
            fs::remove(zipPath, ec);
            setProgress(State::Failed, "No published checksum for " + asset.name + " — refusing to install.");
            return;
        }
        if (actual != it->second) {
            fs::remove(zipPath, ec);
            setProgress(State::Failed, "Archive checksum mismatch — refusing to install (possible tampering).");
            return;
        }

        // 2b. Detached ed25519 signature (authenticity) against the pinned key.
        const std::string pubKey = kDaemonSignaturePublicKeyBase64;
        if (!pubKey.empty()) {
            const int sigIdx = selectDaemonSignatureAsset(rel, asset.name);
            if (sigIdx < 0) {
                if (kDaemonRequireSignature) {
                    fs::remove(zipPath, ec);
                    setProgress(State::Failed, "No signature published for this release — refusing to install.");
                    return;
                }
                DEBUG_LOGF("[daemon-updater] no signature asset for %s; proceeding on checksum only\n",
                           asset.name.c_str());
            } else {
                setProgress(State::Verifying, "Verifying signature…");
                const std::string sigContent = httpGet(rel.assets[sigIdx].downloadUrl);
                if (sigContent.empty() || !verifyXmrigSignature(bytes, sigContent, pubKey)) {
                    fs::remove(zipPath, ec);
                    setProgress(State::Failed, "Signature verification failed — refusing to install.");
                    return;
                }
            }
        } else if (kDaemonRequireSignature) {
            fs::remove(zipPath, ec);
            setProgress(State::Failed, "No signing key is pinned in this build — refusing to install.");
            return;
        }
    }

    // 3. Extract the daemon binaries (flatten the versioned subdir). No per-member hash check is
    //    needed: the whole archive was already verified above (SHA-256 + ed25519 signature), so
    //    every member is authentic by transitivity. The archive also carries params/asmap, which
    //    this updater deliberately leaves to the wallet's own resource extraction.
    setProgress(State::Extracting, "Installing node…");
    const std::vector<std::string> wanted = daemonExtractBasenames(token);
    const std::string daemonName = wanted.front();  // "dragonxd" / "dragonxd.exe"

    mz_zip_archive zip{};
    // Extract from the ALREADY-VERIFIED in-memory buffer, not by reopening zipPath — otherwise a fast
    // local attacker could swap the file on disk between the hash/signature check and extraction. (I-01)
    if (!mz_zip_reader_init_mem(&zip, bytes.data(), bytes.size(), 0)) {
        fs::remove(zipPath, ec);
        setProgress(State::Failed, "Could not open the downloaded archive.");
        return;
    }
    bool daemonInstalled = false;
    bool failed = false;
    const int numFiles = static_cast<int>(mz_zip_reader_get_num_files(&zip));
    for (int i = 0; i < numFiles && !failed; ++i) {
        if (cancel_requested_) { failed = true; break; }   // honor cancel mid-extraction so the join returns promptly (L-07)
        mz_zip_archive_file_stat st;
        if (!mz_zip_reader_file_stat(&zip, i, &st)) continue;
        if (mz_zip_reader_is_file_a_directory(&zip, i)) continue;
        const std::string base = baseName(st.m_filename);
        if (std::find(wanted.begin(), wanted.end(), base) == wanted.end()) continue;  // skip params/asmap/etc.

        // Reject an implausibly large member before decompressing it into memory.
        if (st.m_uncomp_size > kMaxMemberBytes) { failed = true; break; }

        size_t outSize = 0;
        void* mem = mz_zip_reader_extract_to_heap(&zip, i, &outSize, 0);
        if (!mem) { failed = true; break; }

        const std::string finalPath = (fs::path(targetDir) / base).string();
        const std::string tmpPath = finalPath + ".tmp";
        // Tidy any leftover from a previous update (the old binary moved aside, freed after restart).
        fs::remove(finalPath + ".old", ec);
        {
            std::ofstream of(tmpPath, std::ios::binary | std::ios::trunc);
            if (!of) { mz_free(mem); failed = true; break; }
            of.write(static_cast<const char*>(mem), static_cast<std::streamsize>(outSize));
        }
        mz_free(mem);

        // Atomic install that also works while the daemon is running: POSIX rename() replaces the
        // path even if the old binary is in use (the running process keeps the unlinked inode). On
        // Windows a running .exe cannot be renamed-over, so move it aside to ".old" first, then put
        // the new file in place; the ".old" is cleaned up on a later update once the daemon restarts.
        fs::rename(tmpPath, finalPath, ec);
        if (ec) {
            std::error_code mec;
            fs::rename(finalPath, finalPath + ".old", mec);
            ec.clear();
            fs::rename(tmpPath, finalPath, ec);
        }
        if (ec) { fs::remove(tmpPath, ec); failed = true; break; }

#ifndef _WIN32
        // Every wanted member is an executable in the daemon set — make them all runnable.
        fs::permissions(finalPath,
                        fs::perms::owner_all | fs::perms::group_read | fs::perms::group_exec |
                        fs::perms::others_read | fs::perms::others_exec,
                        fs::perm_options::replace, ec);
#endif
        if (base == daemonName) daemonInstalled = true;
    }
    mz_zip_reader_end(&zip);
    fs::remove(zipPath, ec);

    if (failed) { setProgress(State::Failed, "Could not verify/install the node binaries."); return; }
    if (!daemonInstalled) { setProgress(State::Failed, "Daemon binary not found in the archive."); return; }

    setProgress(State::Done, "Node installed (" + rel.tag + ").");
}

} // namespace util
} // namespace dragonx
