/* XMRig
 * Copyright (c) 2018-2021 SChernykh   <https://github.com/SChernykh>
 * Copyright (c) 2016-2021 XMRig       <https://github.com/xmrig>, <support@xmrig.com>
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include "base/net/stratum/benchmark/BenchClient.h"
#include "3rdparty/fmt/core.h"
#include "3rdparty/rapidjson/document.h"
#include "backend/common/benchmark/BenchState.h"
#include "backend/common/interfaces/IBackend.h"
#include "backend/cpu/Cpu.h"
#include "base/io/json/Json.h"
#include "base/io/log/Log.h"
#include "base/io/log/Tags.h"
#include "base/kernel/interfaces/IClientListener.h"
#include "base/net/dns/Dns.h"
#include "base/net/dns/DnsRecords.h"
#include "base/net/http/Fetch.h"
#include "base/net/http/HttpData.h"
#include "base/net/http/HttpListener.h"
#include "base/net/stratum/benchmark/BenchConfig.h"
#include "base/tools/Cvt.h"
#include "version.h"

#ifdef XMRIG_FEATURE_DMI
#   include "hw/dmi/DmiReader.h"
#endif


xmrig::BenchClient::BenchClient(const std::shared_ptr<BenchConfig> &benchmark, IClientListener* listener) :
    m_listener(listener),
    m_benchmark(benchmark),
    m_hash(benchmark->hash())
{
    std::vector<char> blob(112 * 2 + 1, '0');
    blob.back() = '\0';

#   ifdef XMRIG_ALGO_GHOSTRIDER
    if (m_benchmark->algorithm() == Algorithm::GHOSTRIDER_RTM) {
        const uint32_t q = (benchmark->rotation() / 20) & 1;
        const uint32_t r = benchmark->rotation() % 20;

        static constexpr uint32_t indices[20][3] = {
             { 0, 1, 2 },
             { 0, 1, 3 },
             { 0, 1, 4 },
             { 0, 1, 5 },
             { 0, 2, 3 },
             { 0, 2, 4 },
             { 0, 2, 5 },
             { 0, 3, 4 },
             { 0, 3, 5 },
             { 0, 4, 5 },
             { 1, 2, 3 },
             { 1, 2, 4 },
             { 1, 2, 5 },
             { 1, 3, 4 },
             { 1, 3, 5 },
             { 1, 4, 5 },
             { 2, 3, 4 },
             { 2, 3, 5 },
             { 2, 4, 5 },
             { 3, 4, 5 },
        };

        blob[ 8] = '0' + indices[r][q ? 2 : 1];
        blob[ 9] = '0' + indices[r][0];
        blob[11] = '0' + indices[r][q ? 1 : 2];
    }
#   endif

    m_job.setAlgorithm(m_benchmark->algorithm());
    m_job.setBlob(blob.data());
    m_job.setDiff(std::numeric_limits<uint64_t>::max());
    m_job.setHeight(1);
    m_job.setId("00000000");

    blob[Job::kMaxSeedSize * 2] = '\0';
    m_job.setSeedHash(blob.data());

    BenchState::init(this, m_benchmark->size());

#   ifdef XMRIG_FEATURE_HTTP
    if (m_benchmark->isSubmit() && (m_benchmark->algorithm().family() == Algorithm::RANDOM_X)) {
        m_mode  = ONLINE_BENCH;
        m_token = m_benchmark->token();

        return;
    }

    if (!m_benchmark->id().isEmpty()) {
        m_job.setId(m_benchmark->id());
        m_token = m_benchmark->token();
        m_mode  = ONLINE_VERIFY;

        return;
    }
#   endif

    if (m_hash && setSeed(m_benchmark->seed())) {
        m_mode = STATIC_VERIFY;

        return;
    }

    m_job.setBenchSize(m_benchmark->size());

}


xmrig::BenchClient::~BenchClient()
{
    BenchState::destroy();
}


const char *xmrig::BenchClient::tag() const
{
    return Tags::bench();
}


void xmrig::BenchClient::connect()
{
#   ifdef XMRIG_FEATURE_HTTP
    if (m_mode == ONLINE_BENCH || m_mode == ONLINE_VERIFY) {
        return resolve();
    }
#   endif

    start();
}


void xmrig::BenchClient::setPool(const Pool &pool)
{
    m_pool = pool;
}


void xmrig::BenchClient::onBenchDone(uint64_t result, uint64_t diff, uint64_t ts)
{
    m_result    = result;
    m_diff      = diff;
    m_doneTime  = ts;

#   ifdef XMRIG_FEATURE_HTTP
    if (!m_token.isEmpty()) {
        send(DONE_BENCH);
    }
#   endif

    const uint64_t ref = referenceHash();
    const char *color  = ref ? ((result == ref) ? GREEN_BOLD_S : RED_BOLD_S) : BLACK_BOLD_S;

    const double dt = static_cast<int64_t>(ts - m_readyTime) / 1000.0;
    LOG_NOTICE("%s " WHITE_BOLD("benchmark finished in ") CYAN_BOLD("%.3f seconds (%.1f h/s)") WHITE_BOLD_S " hash sum = " CLEAR "%s%016" PRIX64 CLEAR, tag(), dt, BenchState::size() / dt, color, result);

    if (m_token.isEmpty()) {
        printExit();
    }
}


void xmrig::BenchClient::onBenchReady(uint64_t ts, uint32_t threads, const IBackend *backend)
{
    m_readyTime = ts;
    m_threads   = threads;
    m_backend   = backend;

#   ifdef XMRIG_FEATURE_HTTP
    if (m_mode == ONLINE_BENCH) {
        send(CREATE_BENCH);
    }
#   endif
}


void xmrig::BenchClient::onHttpData(const HttpData &data)
{
#   ifdef XMRIG_FEATURE_HTTP
    rapidjson::Document doc;

    try {
        doc = data.json();
    } catch (const std::exception &ex) {
        return setError(ex.what());
    }

    if (data.status != 200) {
        return setError(data.statusName());
    }

    switch (m_request) {
    case GET_BENCH:
        return onGetReply(doc);

    case CREATE_BENCH:
        return onCreateReply(doc);

    case DONE_BENCH:
        return onDoneReply(doc);

    default:
        break;
    }
#   endif
}


void xmrig::BenchClient::onResolved(const DnsRecords &records, int status, const char *error)
{
#   ifdef XMRIG_FEATURE_HTTP
    assert(!m_httpListener);

    m_dns.reset();

    if (status < 0) {
        return setError(error, "DNS error");
    }

    m_ip            = records.get().ip();
    m_httpListener  = std::make_shared<HttpListener>(this, tag());

    if (m_mode == ONLINE_BENCH) {
        start();
    }
    else {
        send(GET_BENCH);
    }
#   endif
}


bool xmrig::BenchClient::setSeed(const char *seed)
{
    if (!seed) {
        return false;
    }

    size_t size = strlen(seed);
    if (size % 2 != 0) {
        return false;
    }

    size /= 2;
    if (size < 4 || size >= m_job.size()) {
        return false;
    }

    if (!Cvt::fromHex(m_job.blob(), m_job.size(), seed, size * 2)) {
        return false;
    }

    m_job.setBenchSize(BenchState::size());

    LOG_NOTICE("%s " WHITE_BOLD("seed ") BLACK_BOLD("%s"), tag(), seed);

    return true;
}


uint64_t xmrig::BenchClient::referenceHash() const
{
    if (m_hash || m_mode == ONLINE_BENCH) {
        return m_hash;
    }

    return BenchState::referenceHash(m_job.algorithm(), BenchState::size(), m_threads);
}


void xmrig::BenchClient::printExit() const
{
    LOG_INFO("%s " WHITE_BOLD("press ") MAGENTA_BOLD("Ctrl+C") WHITE_BOLD(" to exit"), tag());
}


void xmrig::BenchClient::start()
{
    const uint32_t size = BenchState::size();

    LOG_NOTICE("%s " MAGENTA_BOLD("start benchmark ") "hashes " CYAN_BOLD("%u%s") " algo " WHITE_BOLD("%s"),
               tag(),
               size < 1000000 ? size / 1000 : size / 1000000,
               size < 1000000 ? "K" : "M",
               m_job.algorithm().name());

    m_listener->onLoginSuccess(this);
    m_listener->onJobReceived(this, m_job, rapidjson::Value());
}



#ifdef XMRIG_FEATURE_HTTP
void xmrig::BenchClient::onCreateReply(const rapidjson::Value &value)
{
    m_startTime = Chrono::steadyMSecs();
    m_token     = Json::getString(value, BenchConfig::kToken);

    m_job.setId(Json::getString(value, BenchConfig::kId));
    setSeed(Json::getString(value, BenchConfig::kSeed));

    m_listener->onJobReceived(this, m_job, rapidjson::Value());

    send(START_BENCH);
}


void xmrig::BenchClient::onDoneReply(const rapidjson::Value &)
{
    LOG_NOTICE("%s " WHITE_BOLD("benchmark submitted ") CYAN_BOLD("https://xmrig.com/benchmark/%s"), tag(), m_job.id().data());
    printExit();
}


void xmrig::BenchClient::onGetReply(const rapidjson::Value &value)
{
    const char *hash = Json::getString(value, BenchConfig::kHash);
    if (hash) {
        m_hash = strtoull(hash, nullptr, 16);
    }

    BenchState::setSize(Json::getUint(value, BenchConfig::kSize));

    m_job.setAlgorithm(Json::getString(value, BenchConfig::kAlgo));
    setSeed(Json::getString(value, BenchConfig::kSeed));

    start();
}


void xmrig::BenchClient::resolve()
{
    m_dns = Dns::resolve(BenchConfig::kApiHost, this);
}


void xmrig::BenchClient::send(Request request)
{
    using namespace rapidjson;

    Document doc(kObjectType);
    auto &allocator = doc.GetAllocator();
    m_request       = request;

    switch (m_request) {
    case GET_BENCH:
        {
            FetchRequest req(HTTP_GET, m_ip, BenchConfig::kApiPort, fmt::format("/1/benchmark/{}", m_job.id()).c_str(), BenchConfig::kApiTLS, true);
            fetch(tag(), std::move(req), m_httpListener);
        }
        break;

    case CREATE_BENCH:
        {
            doc.AddMember(StringRef(BenchConfig::kSize),    m_benchmark->size(), allocator);
            doc.AddMember(StringRef(BenchConfig::kAlgo),    m_benchmark->algorithm().toJSON(), allocator);
            doc.AddMember(StringRef(BenchConfig::kUser),    m_benchmark->user().toJSON(), allocator);
            doc.AddMember("version",                        APP_VERSION, allocator);
            doc.AddMember("threads",                        m_threads, allocator);
            doc.AddMember("steady_ready_ts",                m_readyTime, allocator);
            doc.AddMember("cpu",                            Cpu::toJSON(doc), allocator);

#           ifdef XMRIG_FEATURE_DMI
            if (m_benchmark->isDMI()) {
                DmiReader reader;
                if (reader.read()) {
                    doc.AddMember("dmi", reader.toJSON(doc), allocator);
                }
            }
#           endif

            FetchRequest req(HTTP_POST, m_ip, BenchConfig::kApiPort, "/1/benchmark", doc, BenchConfig::kApiTLS, true);

            if (!m_token.isEmpty()) {
                req.headers.insert({ "Authorization", fmt::format("Bearer {}", m_token)});
            }

            fetch(tag(), std::move(req), m_httpListener);
        }
        break;

    case START_BENCH:
        doc.AddMember("steady_start_ts",    m_startTime, allocator);
        update(doc);
        break;

    case DONE_BENCH:
        doc.AddMember("steady_done_ts",     m_doneTime, allocator);
        doc.AddMember("hash",               Value(fmt::format("{:016X}", m_result).c_str(), allocator), allocator);
        doc.AddMember("diff",               m_diff, allocator);
        doc.AddMember("backend",            m_backend->toJSON(doc), allocator);
        update(doc);
        break;

    case NO_REQUEST:
        break;
    }
}


void xmrig::BenchClient::setError(const char *message, const char *label)
{
    LOG_ERR("%s " RED("%s: ") RED_BOLD("\"%s\""), tag(), label ? label : "benchmark failed", message);
    printExit();

    BenchState::destroy();
}


void xmrig::BenchClient::update(const rapidjson::Value &body)
{
    assert(!m_token.isEmpty());

    FetchRequest req(HTTP_PATCH, m_ip, BenchConfig::kApiPort, fmt::format("/1/benchmark/{}", m_job.id()).c_str(), body, BenchConfig::kApiTLS, true);
    req.headers.insert({ "Authorization", fmt::format("Bearer {}", m_token)});

    fetch(tag(), std::move(req), m_httpListener);
}
#endif
