// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// rpc_client.cpp — JSON-RPC client over HTTPS using libcurl.
// All calls are blocking; run on RPCWorker threads, never on main thread.

#include "rpc_client.h"
#include "connection.h"
#include "../config/version.h"
#include "../util/base64.h"

#include <curl/curl.h>
#include <sodium.h>
#include <atomic>
#include <cstdio>
#include <cstring>
#include <utility>
#include "../util/logger.h"

namespace dragonx {
namespace rpc {

namespace {

// Recursively zero every string value in a JSON tree in place — used to wipe a discarded parse tree
// that held a secret (B7). Operates on the underlying std::string buffers via get_ref.
// Templated so it works on both nlohmann::json and nlohmann::ordered_json (callRaw uses the latter).
template <typename J>
void scrubJsonSecrets(J& j) {
    if (j.is_string()) {
        auto& s = j.template get_ref<std::string&>();
        if (!s.empty()) sodium_memzero(&s[0], s.size());
    } else if (j.is_object() || j.is_array()) {
        for (auto& el : j) scrubJsonSecrets(el);
    }
}

std::mutex g_trace_mutex;
RPCClient::TraceCallback g_trace_callback;
std::atomic_bool g_trace_enabled{false};
thread_local std::string g_trace_source;

void emitRpcTrace(const std::string& method)
{
    if (!g_trace_enabled.load(std::memory_order_relaxed)) return;

    RPCClient::TraceCallback callback;
    {
        std::lock_guard<std::mutex> lock(g_trace_mutex);
        callback = g_trace_callback;
    }
    if (!callback) return;

    std::string source = g_trace_source.empty() ? std::string("App") : g_trace_source;
    callback(source, method);
}

} // namespace

RPCClient::TraceScope::TraceScope(std::string source)
    : previous_(RPCClient::currentTraceSource())
{
    RPCClient::setTraceSource(std::move(source));
}

RPCClient::TraceScope::~TraceScope()
{
    RPCClient::setTraceSource(std::move(previous_));
}

void RPCClient::setTraceCallback(TraceCallback callback)
{
    std::lock_guard<std::mutex> lock(g_trace_mutex);
    g_trace_callback = std::move(callback);
}

void RPCClient::setTraceEnabled(bool enabled)
{
    g_trace_enabled.store(enabled, std::memory_order_relaxed);
}

bool RPCClient::isTraceEnabled()
{
    return g_trace_enabled.load(std::memory_order_relaxed);
}

std::string RPCClient::currentTraceSource()
{
    return g_trace_source;
}

void RPCClient::setTraceSource(std::string source)
{
    g_trace_source = std::move(source);
}

// Callback for libcurl to write response data
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) {
    size_t totalSize = size * nmemb;
    // Bound accumulation so a hostile/compromised daemon cannot OOM the client with an unbounded
    // response body. 256 MiB is far above any legitimate JSON-RPC response yet prevents exhaustion.
    static constexpr size_t kMaxRpcResponseBytes = 256u * 1024 * 1024;
    if (userp->size() + totalSize > kMaxRpcResponseBytes) return 0;  // short count aborts the transfer
    userp->append((char*)contents, totalSize);
    return totalSize;
}

// curl progress callback: a non-zero return aborts the in-flight transfer. This lets a
// requestAbort() from another thread (disconnect/shutdown) unblock curl_easy_perform so the
// UI thread's worker join() returns promptly instead of waiting out the request timeout.
static int xferInfoCallback(void* clientp, curl_off_t, curl_off_t, curl_off_t, curl_off_t) {
    const auto* self = static_cast<const RPCClient*>(clientp);
    return (self != nullptr && self->abortRequested()) ? 1 : 0;
}

// Private implementation using libcurl
class RPCClient::Impl {
public:
    CURL* curl = nullptr;
    struct curl_slist* headers = nullptr;
    std::string url;
    
    ~Impl() {
        if (headers) {
            curl_slist_free_all(headers);
        }
        if (curl) {
            curl_easy_cleanup(curl);
        }
    }
};

// Initialize curl globally (once)
static bool initCurl() {
    static bool initialized = false;
    if (!initialized) {
        curl_global_init(CURL_GLOBAL_DEFAULT);
        initialized = true;
    }
    return true;
}
static bool curl_init = initCurl();

RPCClient::RPCClient() : impl_(std::make_unique<Impl>())
{
}

RPCClient::~RPCClient() {
    // Scrub the persistent Basic-auth secret on destruction (disconnect() may not have run). impl_ is
    // still destroyed normally afterward (curl cleanup unchanged). (L-04)
    if (!auth_.empty()) sodium_memzero(auth_.data(), auth_.size());
}

bool RPCClient::connect(const std::string& host, const std::string& port,
                        const std::string& user, const std::string& password)
{
    return connect(host, port, user, password, false);
}

bool RPCClient::connect(const std::string& host, const std::string& port,
                        const std::string& user, const std::string& password,
                        bool useTls)
{
    std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
    host_ = host;
    port_ = port;
    last_connect_info_ = json();

    // Create Basic auth header with proper base64 encoding, then wipe the plaintext
    // "user:password" temporary (std::string does not zero its buffer on destruction).
    std::string credentials = user + ":" + password;
    if (!auth_.empty()) sodium_memzero(auth_.data(), auth_.size());   // wipe any prior secret before overwrite (L-04)
    auth_ = util::base64_encode(credentials);
    if (!credentials.empty()) sodium_memzero(credentials.data(), credentials.size());

    impl_->url = std::string(useTls ? "https://" : "http://") + host + ":" + port + "/";
    VERBOSE_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str());

    // Clean up previous curl handle/headers to avoid leaks on retries
    if (impl_->headers) {
        curl_slist_free_all(impl_->headers);
        impl_->headers = nullptr;
    }
    if (impl_->curl) {
        curl_easy_cleanup(impl_->curl);
        impl_->curl = nullptr;
    }

    // Initialize curl handle
    impl_->curl = curl_easy_init();
    if (!impl_->curl) {
        DEBUG_LOGF("Failed to initialize curl\n");
        return false;
    }

    // Set up headers - daemon expects text/plain, not application/json
    impl_->headers = curl_slist_append(nullptr, "Content-Type: text/plain");
    std::string auth_header = "Authorization: Basic " + auth_;
    impl_->headers = curl_slist_append(impl_->headers, auth_header.c_str());
    if (!auth_header.empty()) sodium_memzero(auth_header.data(), auth_header.size());   // curl copied it (L-04)

    // Configure curl
    curl_easy_setopt(impl_->curl, CURLOPT_URL, impl_->url.c_str());
    curl_easy_setopt(impl_->curl, CURLOPT_HTTPHEADER, impl_->headers);
    curl_easy_setopt(impl_->curl, CURLOPT_WRITEFUNCTION, WriteCallback);
    // Progress callback so requestAbort() can unblock an in-flight curl_easy_perform.
    clearAbort();  // a fresh connection must not start in the aborted state
    curl_easy_setopt(impl_->curl, CURLOPT_NOPROGRESS, 0L);
    curl_easy_setopt(impl_->curl, CURLOPT_XFERINFOFUNCTION, xferInfoCallback);
    curl_easy_setopt(impl_->curl, CURLOPT_XFERINFODATA, this);
    curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, 30L);
    // Localhost fails fast if nothing is listening; a remote/TLS daemon needs a larger
    // budget for the TCP + TLS handshake over real network latency (1s would spuriously fail).
    const long connectTimeout = Connection::isLocalHost(host) ? 2L : 10L;
    curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, connectTimeout);
    // Enforce TLS certificate + hostname verification explicitly rather than relying on libcurl's
    // build defaults. Harmless on the localhost http:// case; essential for a remote https daemon.
    curl_easy_setopt(impl_->curl, CURLOPT_SSL_VERIFYPEER, 1L);
    curl_easy_setopt(impl_->curl, CURLOPT_SSL_VERIFYHOST, 2L);

    // Test connection with getinfo. Use a SHORT timeout for the probe on localhost: a healthy
    // local daemon answers in milliseconds and a warming one returns -28 just as fast, so a long
    // hang means a wedged/loading occupant — no point blocking the full 30s before we retry and
    // update the UI. (call(timeoutSec) restores the persistent 30s afterwards, so normal RPC calls
    // that legitimately take longer are unaffected.) Remote/TLS daemons keep the full budget.
    const long probeTimeout = Connection::isLocalHost(host) ? 8L : 30L;
    try {
        json result = call("getinfo", json::array(), probeTimeout);
        if (result.contains("version")) {
            connected_ = true;
            warming_up_ = false;
            warmup_status_.clear();
            last_connect_error_.clear();
            last_connect_info_ = result;
            DEBUG_LOGF("Connected to dragonxd v%d\n", result["version"].get<int>());
            return true;
        }
    } catch (const std::exception& e) {
        last_connect_error_ = e.what();
        // Daemon warmup messages (Loading block index, Verifying blocks, etc.)
        // are normal startup progress — the daemon is reachable and auth works,
        // it just hasn't finished initializing yet.  Mark as connected+warmup
        // so the wallet can show the UI instead of a blocking overlay.
        std::string msg = e.what();
        // Warmup is JSON-RPC error code -28 (RPC_IN_WARMUP) — the robust signal. Fall back
        // to message substrings for any path that didn't carry the numeric code.
        int code = 0;
        if (const auto* re = dynamic_cast<const RpcError*>(&e)) code = re->code;
        bool isWarmup = (code == -28) ||
                        (msg.find("Loading") != std::string::npos ||
                         msg.find("Verifying") != std::string::npos ||
                         msg.find("Activating") != std::string::npos ||
                         msg.find("Rewinding") != std::string::npos ||
                         msg.find("Rescanning") != std::string::npos ||
                         msg.find("Pruning") != std::string::npos);
        if (isWarmup) {
            connected_ = true;
            warming_up_ = true;
            warmup_status_ = msg;
            DEBUG_LOGF("Daemon warming up: %s\n", msg.c_str());
            return true;
        } else {
            DEBUG_LOGF("Connection failed: %s\n", msg.c_str());
        }
    }

    connected_ = false;
    warming_up_ = false;
    warmup_status_.clear();
    last_connect_info_ = json();
    return false;
}

json RPCClient::getLastConnectInfo() const
{
    std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
    return last_connect_info_;
}

void RPCClient::requestAbort()
{
    // Deliberately NOT taking curl_mutex_ — the whole point is to interrupt a call() that is
    // currently holding it inside curl_easy_perform. The atomic is read by xferInfoCallback.
    abort_.store(true, std::memory_order_relaxed);
}

void RPCClient::clearAbort()
{
    abort_.store(false, std::memory_order_relaxed);
}

void RPCClient::disconnect()
{
    std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
    connected_ = false;
    warming_up_ = false;
    warmup_status_.clear();
    last_connect_info_ = json();
    if (impl_->curl) {
        curl_easy_cleanup(impl_->curl);
        impl_->curl = nullptr;
    }
    if (impl_->headers) {
        curl_slist_free_all(impl_->headers);
        impl_->headers = nullptr;
    }
    if (!auth_.empty()) { sodium_memzero(auth_.data(), auth_.size()); auth_.clear(); }   // scrub Basic-auth secret (L-04)
}

json RPCClient::makePayload(const std::string& method, const json& params)
{
    return {
        {"jsonrpc", "1.0"},
        {"id", "ObsidianDragon"},
        {"method", method},
        {"params", params}
    };
}

std::string RPCClient::performCall(const std::string& method, const json& params, long& httpCodeOut)
{
    emitRpcTrace(method);

    json payload = makePayload(method, params);
    std::string body = payload.dump();
    std::string response_data;

    // Set POST data
    curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDS, body.c_str());
    curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDSIZE, (long)body.size());
    curl_easy_setopt(impl_->curl, CURLOPT_WRITEDATA, &response_data);

    // Perform request
    CURLcode res = curl_easy_perform(impl_->curl);

    if (res != CURLE_OK) {
        throw std::runtime_error("RPC request failed: " + std::string(curl_easy_strerror(res)));
    }

    // Check HTTP response code
    httpCodeOut = 0;
    curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &httpCodeOut);

    return response_data;
}

json RPCClient::parseRpcResult(long httpCode, const std::string& body, bool scrubSource)
{
    // Bitcoin/Hush RPC returns HTTP 500 for application-level errors
    // (insufficient funds, bad params, etc.) with a valid JSON body.
    // Parse the body first to extract the real error message.
    if (httpCode != 200) {
        int errCode = 0;
        try {
            json response = json::parse(body);
            if (response.contains("error") && response["error"].is_object()) {
                if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
                    errCode = response["error"]["code"].get<int>();
                if (response["error"].contains("message") && response["error"]["message"].is_string())
                    throw RpcError(errCode, response["error"]["message"].get<std::string>());
                // message missing/non-string — keep the detail instead of a bare HTTP code
                throw RpcError(errCode, "RPC error: " + response["error"].dump());
            }
        } catch (const json::exception&) {
            // Body wasn't valid JSON — fall through to generic HTTP error
        }
        throw RpcError(errCode, "RPC error: HTTP " + std::to_string(httpCode));
    }

    json response = json::parse(body);

    if (response.contains("error") && !response["error"].is_null()) {
        int errCode = 0;
        std::string err_msg;
        if (response["error"].is_object()) {
            if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
                errCode = response["error"]["code"].get<int>();
            if (response["error"].contains("message") && response["error"]["message"].is_string())
                err_msg = response["error"]["message"].get<std::string>();
        }
        if (err_msg.empty()) err_msg = response["error"].dump();
        throw RpcError(errCode, "RPC error: " + err_msg);
    }

    json result = response["result"];   // a COPY of the subobject (operator[] yields an lvalue ref)
    if (scrubSource) scrubJsonSecrets(response);  // zero the discarded tree's secret before it frees (B7)
    return result;
}

json RPCClient::call(const std::string& method, const json& params)
{
    std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
    if (!impl_->curl) {
        throw std::runtime_error("Not connected");
    }

    long http_code = 0;
    std::string response_data = performCall(method, params, http_code);
    return parseRpcResult(http_code, response_data);
}

json RPCClient::callSecret(const std::string& method, const json& params)
{
    std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
    if (!impl_->curl) {
        throw std::runtime_error("Not connected");
    }

    // Zero the raw response body whether parsing succeeds or throws — it holds the secret in the
    // clear (the curl write buffer performCall returns), and would otherwise be freed un-wiped (B7).
    long http_code = 0;
    std::string response_data = performCall(method, params, http_code);
    try {
        json result = parseRpcResult(http_code, response_data, /*scrubSource=*/true);
        if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
        return result;
    } catch (...) {
        if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
        throw;
    }
}

std::string RPCClient::callSecretString(const std::string& method, const json& params)
{
    std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
    if (!impl_->curl) {
        throw std::runtime_error("Not connected");
    }

    long http_code = 0;
    std::string response_data = performCall(method, params, http_code);
    try {
        json result = parseRpcResult(http_code, response_data, /*scrubSource=*/true);
        std::string out;
        if (result.is_string()) {
            // Copy the secret out, then zero the json node's OWN heap buffer — parseRpcResult's
            // json::parse allocates this independently of response_data, so scrubbing the raw body
            // alone would leave it behind (B7). `out` is now the only live copy; the caller wipes it.
            auto& s = result.get_ref<std::string&>();
            out = s;
            if (!s.empty()) sodium_memzero(&s[0], s.size());
        }
        if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
        if (!result.is_string()) throw std::runtime_error("RPC result is not a string");
        return out;
    } catch (...) {
        if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
        throw;
    }
}

json RPCClient::call(const std::string& method, const json& params, long timeoutSec)
{
    std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
    if (!impl_->curl) {
        throw std::runtime_error("Not connected");
    }

    // Temporarily override timeout
    long prevTimeout = 30L;
    curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, timeoutSec);

    try {
        long http_code = 0;
        std::string response_data = performCall(method, params, http_code);
        // Restore original timeout
        curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
        return parseRpcResult(http_code, response_data);
    } catch (...) {
        // Ensure timeout is always restored
        curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
        throw;
    }
}

std::string RPCClient::callRaw(const std::string& method, const json& params)
{
    std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
    if (!impl_->curl) {
        throw std::runtime_error("Not connected");
    }

    emitRpcTrace(method);

    json payload = makePayload(method, params);
    std::string body = payload.dump();
    std::string response_data;

    curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDS, body.c_str());
    curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDSIZE, (long)body.size());
    curl_easy_setopt(impl_->curl, CURLOPT_WRITEDATA, &response_data);

    CURLcode res = curl_easy_perform(impl_->curl);
    if (res != CURLE_OK) {
        throw std::runtime_error("RPC request failed: " + std::string(curl_easy_strerror(res)));
    }

    long http_code = 0;
    curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &http_code);

    if (http_code != 200) {
        try {
            json response = json::parse(response_data);
            if (response.contains("error") && !response["error"].is_null()) {
                std::string err_msg = response["error"]["message"].get<std::string>();
                throw std::runtime_error(err_msg);
            }
        } catch (const json::exception&) {}
        throw std::runtime_error("RPC error: HTTP " + std::to_string(http_code));
    }

    // Parse with ordered_json to preserve the daemon's original key order
    nlohmann::ordered_json oj = nlohmann::ordered_json::parse(response_data);
    if (oj.contains("error") && !oj["error"].is_null()) {
        // A daemon error object normally has a string "message", but don't assume it — a malformed
        // error (missing/non-string message) must yield a clean RPC error, not a json type-exception.
        const auto& err = oj["error"];
        std::string err_msg;
        if (err.is_object() && err.contains("message") && err["message"].is_string())
            err_msg = err["message"].get<std::string>();
        else
            err_msg = err.dump();
        throw std::runtime_error("RPC error: " + err_msg);
    }

    auto& result = oj["result"];
    std::string out;
    if (result.is_null()) {
        out = "null";
    } else if (result.is_string()) {
        // Return the raw string (not JSON-encoded) — caller wraps as needed
        out = result.get<std::string>();
    } else {
        out = result.dump(4);
    }
    // B7: this raw path serves arbitrary console commands including dumpprivkey / z_exportkey,
    // whose response carries plaintext key material. Zero the raw buffer and the parsed tree so
    // the secret does not linger in freed heap (matching callSecret). The single returned copy is
    // the caller's to manage.
    scrubJsonSecrets(oj);
    if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
    return out;
}

void RPCClient::doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err)
{
    try {
        json result = call(method, params);
        if (cb) cb(result);
    } catch (const std::exception& e) {
        if (err) {
            err(e.what());
        } else {
            DEBUG_LOGF("RPC error (%s): %s\n", method.c_str(), e.what());
        }
    }
}

// High-level API implementations

void RPCClient::getInfo(Callback cb, ErrorCallback err)
{
    doRPC("getinfo", {}, cb, err);
}

void RPCClient::getBlockchainInfo(Callback cb, ErrorCallback err)
{
    doRPC("getblockchaininfo", {}, cb, err);
}

void RPCClient::getMiningInfo(Callback cb, ErrorCallback err)
{
    doRPC("getmininginfo", {}, cb, err);
}

void RPCClient::getBalance(Callback cb, ErrorCallback err)
{
    doRPC("getbalance", {}, cb, err);
}

void RPCClient::z_getTotalBalance(Callback cb, ErrorCallback err)
{
    doRPC("z_gettotalbalance", {}, cb, err);
}

void RPCClient::listUnspent(Callback cb, ErrorCallback err)
{
    doRPC("listunspent", {0}, cb, err);
}

void RPCClient::z_listUnspent(Callback cb, ErrorCallback err)
{
    doRPC("z_listunspent", {0}, cb, err);
}

void RPCClient::getAddressesByAccount(Callback cb, ErrorCallback err)
{
    doRPC("getaddressesbyaccount", {""}, cb, err);
}

void RPCClient::z_listAddresses(Callback cb, ErrorCallback err)
{
    doRPC("z_listaddresses", {}, cb, err);
}

void RPCClient::getNewAddress(Callback cb, ErrorCallback err)
{
    doRPC("getnewaddress", {}, cb, err);
}

void RPCClient::z_getNewAddress(Callback cb, ErrorCallback err)
{
    doRPC("z_getnewaddress", {}, cb, err);
}

void RPCClient::listTransactions(int count, Callback cb, ErrorCallback err)
{
    doRPC("listtransactions", {"", count}, cb, err);
}

void RPCClient::z_viewTransaction(const std::string& txid, Callback cb, ErrorCallback err)
{
    doRPC("z_viewtransaction", {txid}, cb, err);
}

void RPCClient::getRawTransaction(const std::string& txid, Callback cb, ErrorCallback err)
{
    doRPC("getrawtransaction", {txid, 1}, cb, err);
}

void RPCClient::sendToAddress(const std::string& address, double amount, Callback cb, ErrorCallback err)
{
    doRPC("sendtoaddress", {address, amount}, cb, err);
}

void RPCClient::z_sendMany(const std::string& from, const json& recipients, Callback cb, ErrorCallback err)
{
    doRPC("z_sendmany", {from, recipients}, cb, err);
}

void RPCClient::setGenerate(bool generate, int threads, Callback cb, ErrorCallback err)
{
    doRPC("setgenerate", {generate, threads}, cb, err);
}

void RPCClient::getNetworkHashPS(Callback cb, ErrorCallback err)
{
    doRPC("getnetworkhashps", {}, cb, err);
}

void RPCClient::getLocalHashrate(Callback cb, ErrorCallback err)
{
    // RPC name is "getlocalsolps" (inherited from HUSH/Zcash daemon API)
    // but DragonX uses RandomX, so the value is H/s not Sol/s
    doRPC("getlocalsolps", {}, cb, err);
}

void RPCClient::getPeerInfo(Callback cb, ErrorCallback err)
{
    doRPC("getpeerinfo", {}, cb, err);
}

void RPCClient::listBanned(Callback cb, ErrorCallback err)
{
    doRPC("listbanned", {}, cb, err);
}

void RPCClient::setBan(const std::string& ip, const std::string& command, Callback cb, ErrorCallback err, int bantime)
{
    // setban "ip" "add|remove" [bantime] [absolute]
    doRPC("setban", {ip, command, bantime}, cb, err);
}

void RPCClient::clearBanned(Callback cb, ErrorCallback err)
{
    doRPC("clearbanned", {}, cb, err);
}

void RPCClient::dumpPrivKey(const std::string& address, Callback cb, ErrorCallback err)
{
    doRPC("dumpprivkey", {address}, cb, err);
}

void RPCClient::z_exportKey(const std::string& address, Callback cb, ErrorCallback err)
{
    doRPC("z_exportkey", {address}, cb, err);
}

void RPCClient::z_exportViewingKey(const std::string& address, Callback cb, ErrorCallback err)
{
    doRPC("z_exportviewingkey", {address}, cb, err);
}

void RPCClient::importPrivKey(const std::string& key, bool rescan, Callback cb, ErrorCallback err)
{
    doRPC("importprivkey", {key, "", rescan}, cb, err);
}

void RPCClient::z_importKey(const std::string& key, bool rescan, Callback cb, ErrorCallback err)
{
    doRPC("z_importkey", {key, rescan ? "yes" : "no"}, cb, err);
}

void RPCClient::validateAddress(const std::string& address, Callback cb, ErrorCallback err)
{
    doRPC("validateaddress", {address}, cb, err);
}

void RPCClient::getBlock(const std::string& hash_or_height, Callback cb, ErrorCallback err)
{
    doRPC("getblock", {hash_or_height}, cb, err);
}

void RPCClient::stop(Callback cb, ErrorCallback err)
{
    doRPC("stop", {}, cb, err);
}

void RPCClient::rescanBlockchain(int startHeight, Callback cb, ErrorCallback err)
{
    // hush/komodo daemons expose this as "rescan <height>", not bitcoin's "rescanblockchain".
    doRPC("rescan", {startHeight}, cb, err);
}

void RPCClient::z_validateAddress(const std::string& address, Callback cb, ErrorCallback err)
{
    doRPC("z_validateaddress", {address}, cb, err);
}

void RPCClient::getBlockHash(int height, Callback cb, ErrorCallback err)
{
    doRPC("getblockhash", {height}, cb, err);
}

void RPCClient::getTransaction(const std::string& txid, Callback cb, ErrorCallback err)
{
    doRPC("gettransaction", {txid}, cb, err);
}

void RPCClient::getWalletInfo(Callback cb, ErrorCallback err)
{
    doRPC("getwalletinfo", {}, cb, err);
}

void RPCClient::encryptWallet(const std::string& passphrase, Callback cb, ErrorCallback err)
{
    doRPC("encryptwallet", {passphrase}, cb, err);
}

void RPCClient::walletPassphrase(const std::string& passphrase, int timeout, Callback cb, ErrorCallback err)
{
    doRPC("walletpassphrase", {passphrase, timeout}, cb, err);
}

void RPCClient::walletLock(Callback cb, ErrorCallback err)
{
    doRPC("walletlock", {}, cb, err);
}

void RPCClient::walletPassphraseChange(const std::string& oldPass, const std::string& newPass,
                                       Callback cb, ErrorCallback err)
{
    doRPC("walletpassphrasechange", {oldPass, newPass}, cb, err);
}

void RPCClient::z_exportWallet(const std::string& filename, Callback cb, ErrorCallback err)
{
    doRPC("z_exportwallet", {filename}, cb, err);
}

void RPCClient::z_importWallet(const std::string& filename, Callback cb, ErrorCallback err)
{
    doRPC("z_importwallet", {filename}, cb, err);
}

void RPCClient::z_shieldCoinbase(const std::string& fromAddr, const std::string& toAddr,
                                  double fee, int limit, Callback cb, ErrorCallback err)
{
    doRPC("z_shieldcoinbase", {fromAddr, toAddr, fee, limit}, cb, err);
}

void RPCClient::z_mergeToAddress(const std::vector<std::string>& fromAddrs, const std::string& toAddr,
                                  double fee, int limit, Callback cb, ErrorCallback err)
{
    json addrs = json::array();
    for (const auto& addr : fromAddrs) {
        addrs.push_back(addr);
    }
    doRPC("z_mergetoaddress", {addrs, toAddr, fee, 0, limit}, cb, err);
}

void RPCClient::z_getOperationStatus(const std::vector<std::string>& opids, Callback cb, ErrorCallback err)
{
    json ids = json::array();
    for (const auto& id : opids) {
        ids.push_back(id);
    }
    doRPC("z_getoperationstatus", {ids}, cb, err);
}

void RPCClient::z_getOperationResult(const std::vector<std::string>& opids, Callback cb, ErrorCallback err)
{
    json ids = json::array();
    for (const auto& id : opids) {
        ids.push_back(id);
    }
    doRPC("z_getoperationresult", {ids}, cb, err);
}

void RPCClient::z_listReceivedByAddress(const std::string& address, int minconf, Callback cb, ErrorCallback err)
{
    doRPC("z_listreceivedbyaddress", {address, minconf}, cb, err);
}

// Adapts a UnifiedCallback into the (Callback, ErrorCallback) pair doRPC expects: the
// success lambda forwards (result, ""), the error lambda forwards ({}, error). The cb
// null-check is preserved on each invocation.
std::pair<Callback, ErrorCallback> RPCClient::splitUnified(UnifiedCallback cb)
{
    return {
        [cb](const json& result) {
            if (cb) cb(result, "");
        },
        [cb](const std::string& error) {
            if (cb) cb(json{}, error);
        }
    };
}

// Unified callback versions
void RPCClient::getInfo(UnifiedCallback cb)
{
    auto handlers = splitUnified(std::move(cb));
    doRPC("getinfo", {}, handlers.first, handlers.second);
}

void RPCClient::rescanBlockchain(int startHeight, UnifiedCallback cb)
{
    // hush/komodo daemons expose this as "rescan <height>", not bitcoin's "rescanblockchain".
    auto handlers = splitUnified(std::move(cb));
    doRPC("rescan", {startHeight}, handlers.first, handlers.second);
}

void RPCClient::z_shieldCoinbase(const std::string& fromAddr, const std::string& toAddr,
                                  double fee, int limit, UnifiedCallback cb)
{
    auto handlers = splitUnified(std::move(cb));
    doRPC("z_shieldcoinbase", {fromAddr, toAddr, fee, limit}, handlers.first, handlers.second);
}

void RPCClient::z_mergeToAddress(const std::vector<std::string>& fromAddrs, const std::string& toAddr,
                                  double fee, int limit, UnifiedCallback cb)
{
    json addrs = json::array();
    for (const auto& addr : fromAddrs) {
        addrs.push_back(addr);
    }
    auto handlers = splitUnified(std::move(cb));
    doRPC("z_mergetoaddress", {addrs, toAddr, fee, 0, limit}, handlers.first, handlers.second);
}

void RPCClient::z_getOperationStatus(const std::vector<std::string>& opids, UnifiedCallback cb)
{
    json ids = json::array();
    for (const auto& id : opids) {
        ids.push_back(id);
    }
    auto handlers = splitUnified(std::move(cb));
    doRPC("z_getoperationstatus", {ids}, handlers.first, handlers.second);
}

void RPCClient::getBlock(int height, UnifiedCallback cb)
{
    // First get block hash, then get block
    getBlockHash(height,
        [this, cb](const json& hashResult) {
            std::string hash = hashResult.get<std::string>();
            auto handlers = splitUnified(cb);
            getBlock(hash, handlers.first, handlers.second);
        },
        [cb](const std::string& error) {
            if (cb) cb(json{}, error);
        }
    );
}

} // namespace rpc
} // namespace dragonx
