// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Pure (no-I/O) implementation of the pool registry + hashrate helpers. Links
// nlohmann_json but NOT libcurl, so it can be exercised directly from the test
// binary (mirrors util/xmrig_updater_core.cpp).

#include "pool_registry.h"

#include <nlohmann/json.hpp>

#include <algorithm>
#include <cctype>
#include <cstddef>

namespace dragonx {
namespace util {

using json = nlohmann::json;

const std::vector<KnownPool>& knownPools()
{
    // NOTE: only PPLNS (shared) pools belong here. The .cc SOLO pool
    // (dragonx-solo, ports 5555/6666) is intentionally omitted — its hashrate
    // is not comparable for network balancing.
    static const std::vector<KnownPool> pools = {
        KnownPool{
            "dragonx-is", "pool.dragonx.is", "pool.dragonx.is:3433", "rx/hush",
            "https://pool.dragonx.is/api/stats", PoolStatsSchema::DragonXIs,
            /*miningcorePoolId=*/"", /*feePercent=*/1.0, /*official=*/true,
        },
    };
    return pools;
}

namespace {

std::string trimmed(const std::string& s)
{
    const auto b = s.find_first_not_of(" \t\r\n");
    if (b == std::string::npos) return {};
    const auto e = s.find_last_not_of(" \t\r\n");
    return s.substr(b, e - b + 1);
}

// Extract "host[:port]" from a URL that may carry a scheme, userinfo, path or query.
// Our pools are plain host:port endpoints (no IPv6 literals), so a last-colon split
// for the port is sufficient.
std::string hostPortOf(const std::string& url)
{
    std::string s = trimmed(url);
    if (const auto scheme = s.find("://"); scheme != std::string::npos)
        s = s.substr(scheme + 3);
    if (const auto at = s.find('@'); at != std::string::npos)
        s = s.substr(at + 1);
    if (const auto cut = s.find_first_of("/?#"); cut != std::string::npos)
        s = s.substr(0, cut);
    return s;
}

void splitHostPort(const std::string& hostport, std::string& host, std::string& port)
{
    if (const auto colon = hostport.rfind(':'); colon == std::string::npos) {
        host = hostport;
        port.clear();
    } else {
        host = hostport.substr(0, colon);
        port = hostport.substr(colon + 1);
    }
    std::transform(host.begin(), host.end(), host.begin(),
                   [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
}

// Same endpoint when hosts match and (either side omits a port, or the ports match).
bool sameEndpoint(const std::string& a, const std::string& b)
{
    std::string ha, pa, hb, pb;
    splitHostPort(hostPortOf(a), ha, pa);
    splitHostPort(hostPortOf(b), hb, pb);
    if (ha.empty() || ha != hb) return false;
    if (pa.empty() || pb.empty()) return true;
    return pa == pb;
}

// Build a synthetic, selectable pool row for a user-supplied URL (a saved favorite
// or the current custom pool). We don't know its stats API, so it carries no
// statsUrl / live hashrate and an unknown (<0) fee — the UI falls back to "—".
KnownPool makeUserPool(const std::string& url)
{
    KnownPool p;
    const std::string hp = hostPortOf(url);
    std::string host, port;
    splitHostPort(hp, host, port);
    p.id       = "user:" + trimmed(url);  // stable + unique (used as the ImGui id)
    p.label    = host.empty() ? hp : host;
    p.stratum  = trimmed(url);            // what the miner connects to / a row-click restores
    p.algo     = "";                      // unknown; xmrig resolves via resolvePoolAlgo's fallback
    p.statsUrl = "";                      // no known stats endpoint -> no live hashrate/fee
    p.schema   = PoolStatsSchema::DragonXIs;
    p.miningcorePoolId = "";
    p.feePercent = -1.0;                  // unknown fee
    p.official = false;
    return p;
}

} // namespace

const KnownPool* findPoolByUrl(const std::vector<KnownPool>& pools, const std::string& url)
{
    for (const auto& p : pools)
        if (sameEndpoint(p.stratum, url)) return &p;
    return nullptr;
}

const KnownPool* findKnownPoolByUrl(const std::string& url)
{
    return findPoolByUrl(knownPools(), url);
}

std::vector<KnownPool> effectivePools(const std::string& currentPoolUrl,
                                      const std::vector<std::string>& savedPoolUrls)
{
    std::vector<KnownPool> pools = knownPools();

    // Skip anything whose endpoint already appears (official or an earlier user row).
    auto listed = [&](const std::string& url) {
        return findPoolByUrl(pools, url) != nullptr;
    };

    for (const auto& url : savedPoolUrls) {
        if (trimmed(url).empty() || listed(url)) continue;
        pools.push_back(makeUserPool(url));
    }

    // The pool currently being mined, if not already shown, so the active pool is
    // always visible even before it's bookmarked.
    if (!trimmed(currentPoolUrl).empty() && !listed(currentPoolUrl))
        pools.push_back(makeUserPool(currentPoolUrl));

    return pools;
}

std::string resolvePoolAlgo(const std::string& url, const std::string& fallback)
{
    if (const KnownPool* p = findKnownPoolByUrl(url)) return p->algo;
    return fallback;
}

double parsePoolHashrate(PoolStatsSchema schema, const std::string& jsonStr,
                         const std::string& miningcorePoolId, bool& ok)
{
    ok = false;
    try {
        const json j = json::parse(jsonStr);

        if (schema == PoolStatsSchema::DragonXIs) {
            // { "pools": { "dragonx": { "hashrate": <num>, ... }, ... } }
            if (j.contains("pools") && j["pools"].is_object()) {
                const auto& pools = j["pools"];
                auto readHr = [&](const json& pool, double& out) -> bool {
                    if (pool.is_object() && pool.contains("hashrate") &&
                        pool["hashrate"].is_number()) {
                        out = pool["hashrate"].get<double>();
                        return true;
                    }
                    return false;
                };
                double hr = 0.0;
                if (pools.contains("dragonx") && readHr(pools["dragonx"], hr)) {
                    ok = true;
                    return hr;
                }
                for (auto it = pools.begin(); it != pools.end(); ++it) {
                    if (readHr(it.value(), hr)) {
                        ok = true;
                        return hr;
                    }
                }
            }
        } else {  // Miningcore
            // { "pools": [ { "id": "...", "poolStats": { "poolHashrate": <num> } }, ... ] }
            if (j.contains("pools") && j["pools"].is_array()) {
                const json* chosen = nullptr;
                for (const auto& pool : j["pools"]) {
                    if (!pool.is_object()) continue;
                    if (!miningcorePoolId.empty()) {
                        if (pool.value("id", std::string{}) == miningcorePoolId) {
                            chosen = &pool;
                            break;
                        }
                    } else if (!chosen) {
                        chosen = &pool;  // first pool when no id requested
                    }
                }
                if (chosen && chosen->contains("poolStats") &&
                    (*chosen)["poolStats"].is_object() &&
                    (*chosen)["poolStats"].contains("poolHashrate") &&
                    (*chosen)["poolStats"]["poolHashrate"].is_number()) {
                    ok = true;
                    return (*chosen)["poolStats"]["poolHashrate"].get<double>();
                }
            }
        }
    } catch (...) {
        // fall through — ok stays false
    }
    return 0.0;
}

double parsePoolFee(PoolStatsSchema schema, const std::string& jsonStr,
                    const std::string& miningcorePoolId, bool& ok)
{
    ok = false;
    try {
        const json j = json::parse(jsonStr);

        if (schema == PoolStatsSchema::DragonXIs) {
            // { "pools": { "dragonx": { "poolFee": <num>, ... }, ... } }
            if (j.contains("pools") && j["pools"].is_object()) {
                const auto& pools = j["pools"];
                auto readFee = [&](const json& pool, double& out) -> bool {
                    if (pool.is_object() && pool.contains("poolFee") &&
                        pool["poolFee"].is_number()) {
                        out = pool["poolFee"].get<double>();
                        return true;
                    }
                    return false;
                };
                double fee = 0.0;
                if (pools.contains("dragonx") && readFee(pools["dragonx"], fee)) {
                    ok = true;
                    return fee;
                }
                for (auto it = pools.begin(); it != pools.end(); ++it) {
                    if (readFee(it.value(), fee)) {
                        ok = true;
                        return fee;
                    }
                }
            }
        } else {  // Miningcore: pools[id].poolFeePercent
            if (j.contains("pools") && j["pools"].is_array()) {
                const json* chosen = nullptr;
                for (const auto& pool : j["pools"]) {
                    if (!pool.is_object()) continue;
                    if (!miningcorePoolId.empty()) {
                        if (pool.value("id", std::string{}) == miningcorePoolId) {
                            chosen = &pool;
                            break;
                        }
                    } else if (!chosen) {
                        chosen = &pool;  // first pool when no id requested
                    }
                }
                if (chosen && chosen->contains("poolFeePercent") &&
                    (*chosen)["poolFeePercent"].is_number()) {
                    ok = true;
                    return (*chosen)["poolFeePercent"].get<double>();
                }
            }
        }
    } catch (...) {
        // fall through — ok stays false
    }
    return 0.0;
}

std::string chooseWeightedPool(const std::vector<PoolHashrate>& pools,
                               const std::string& currentId,
                               std::mt19937& rng)
{
    std::vector<const PoolHashrate*> usable;
    usable.reserve(pools.size());
    for (const auto& p : pools)
        if (p.ok) usable.push_back(&p);

    if (usable.empty()) return currentId;
    if (usable.size() == 1) return usable.front()->id;

    // weight = 1/(hr + eps): smaller pools get more weight; eps keeps a zero-hashrate
    // pool finite (and most-favored). The incumbent gets a stickiness multiplier so a
    // periodic re-balance only rarely restarts the miner.
    constexpr double kEps = 1.0;  // H/s
    std::vector<double> weights;
    weights.reserve(usable.size());
    double total = 0.0;
    for (const auto* p : usable) {
        double w = 1.0 / (std::max(0.0, p->hashrateHs) + kEps);
        if (!currentId.empty() && p->id == currentId) w *= kIncumbentStayBias;
        weights.push_back(w);
        total += w;
    }
    if (total <= 0.0) return usable.front()->id;

    std::uniform_real_distribution<double> dist(0.0, total);
    const double roll = dist(rng);
    double cum = 0.0;
    for (std::size_t i = 0; i < usable.size(); ++i) {
        cum += weights[i];
        if (roll <= cum) return usable[i]->id;
    }
    return usable.back()->id;
}

} // namespace util
} // namespace dragonx
