// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#ifndef QRPCBENCH_COMMON_H
#define QRPCBENCH_COMMON_H

#include <QtCore/qcommandlineoption.h>
#include <QtCore/qcommandlineparser.h>
#include <QtCore/qelapsedtimer.h>
#include <QtCore/qstringlist.h>
#include <QtCore/qsysinfo.h>

#include <chrono>
#include <concepts>
#include <cstdlib>
#include <format>
#include <iomanip>
#include <iostream>
#include <ranges>
#include <string_view>

inline std::string getTransportAddress(QAnyStringView transport)
{
    if (transport == "http") {
        return "localhost:65002";
    } else if (transport == "https") {
        return "localhost:65003";
    }
#ifndef Q_OS_WINDOWS
    else if (transport == "unix") {
        return "unix-abstract:bench";
    }
#endif
    else {
        std::cerr << "Invalid transport specified: " << transport.toString().toStdString()
                  << std::endl;
        std::exit(EXIT_FAILURE);
    }
}

// Valid for the next 100 years.
static constexpr std::string_view SslCert = R"(
-----BEGIN CERTIFICATE-----
MIID+jCCAuKgAwIBAgIUVJNsgxX2GVzFdU8yC0xwETtnHWwwDQYJKoZIhvcNAQEL
BQAwgZMxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJl
cmxpbjEcMBoGA1UECgwTVGhlIFF0IENvbXBhbnkgR21iSDEMMAoGA1UECwwDUiZE
MRIwEAYDVQQDDAlsb2NhbGhvc3QxIjAgBgkqhkiG9w0BCQEWE2Rlbm5pcy5vYmVy
c3RAcXQuaW8wIBcNMjUwMTE2MTU1ODIzWhgPMjEyNDEyMjMxNTU4MjNaMIGTMQsw
CQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xHDAa
BgNVBAoME1RoZSBRdCBDb21wYW55IEdtYkgxDDAKBgNVBAsMA1ImRDESMBAGA1UE
AwwJbG9jYWxob3N0MSIwIAYJKoZIhvcNAQkBFhNkZW5uaXMub2JlcnN0QHF0Lmlv
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAszG9ps195LVF9kUDbQXj
1/5JAemlRvLMAJcr28virbqcCqphnode/vYGYiy2pbnPMJKxN9MsH5NMZH1/sn1A
PHGQ3sD6vcp24BZkgitDXLQab3QCF00K3Kka7GO6wQ9eloobBTtAWZDU/Px2LkLx
bmlIsr9Lbic2ZbKVWL3oRUECIAppmjEn1La+QSsV5lI2vROy6/b84OrBcYRDHifp
q70ZgIGO7AcucIeRAJGpfJJHxx8cuguDf0bgXJjsnIfXiAXCQkwtTWCrvES9akP4
dIJUz8onKAe7TRS1jaNGnXpikezhRa++GJNJhFAY3m6/0+qVHa0wByk5lxRoWV40
xwIDAQABo0IwQDAdBgNVHQ4EFgQUd2BWbXCVfYO0kHX+Ea5DcIzUlWIwHwYDVR0j
BBgwFoAU8jBTKqrkQqH2H7Kgyt6jtJtlZgcwDQYJKoZIhvcNAQELBQADggEBAG2f
xwaTuwBQ7ldk/8u4vfu1H8cq/eMLnmrbUm+QaSMdK1LXlhPQPpFcGN+6no/0tUA/
LhhzVzK6SRUgrm+IoGwL8ojyxdO760LgUOHN/VpvxQDY6DmBLct2L1sIRrjMNLym
ZgkXSmmeoswNgeonOwN8hIJz9Kx6xil1mPASGHvUaBFBdWDFxB/RuEYyv7X1wPeX
G2WqJZ+hoJESgtJFoybcbvqRGYq+ftXY8phacC1IuoaBinEBFxPrOkC2pzSHrvx2
Awm0fIbfSml23kefPh19Kquhf4NPdyUJo+RHuNn86tadANOEVVQ5slzaSuVu86Ym
u2A5Ea7yqvthhAzHrbg=
-----END CERTIFICATE-----
)";

static constexpr std::string_view SslKey = R"(
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCzMb2mzX3ktUX2
RQNtBePX/kkB6aVG8swAlyvby+KtupwKqmGeh17+9gZiLLaluc8wkrE30ywfk0xk
fX+yfUA8cZDewPq9ynbgFmSCK0NctBpvdAIXTQrcqRrsY7rBD16WihsFO0BZkNT8
/HYuQvFuaUiyv0tuJzZlspVYvehFQQIgCmmaMSfUtr5BKxXmUja9E7Lr9vzg6sFx
hEMeJ+mrvRmAgY7sBy5wh5EAkal8kkfHHxy6C4N/RuBcmOych9eIBcJCTC1NYKu8
RL1qQ/h0glTPyicoB7tNFLWNo0ademKR7OFFr74Yk0mEUBjebr/T6pUdrTAHKTmX
FGhZXjTHAgMBAAECggEAN5khTNXJT+LmmCiFjZgcP3IIWO2TeFXw8eX1l7bE2D5k
F/MRYsyBrv3KsT9KVFU4ccux7K46rHlZZHyD2G+ANMDPwC2EHsro41JPUQv3VJYU
9au60lv3GMvnLJ0s3qXUJUUoaREfQCrtyqjSSjw/CJDmG3+6+ax09kzYhbY2kPW7
RfIWZFTq9n5x3e7Zd2wPDD7PUiqZy3TIHFOwxtpSD3TapVismIFiRtLA6nV84gKf
zKCOLNqzU0usgfHiLBWGXxKcFVjXiv6Nss0o+TFbKhVPgh9r22tEkSFbLp19x2Ii
jF1DU1M5oR1ew9uF22IAQDl13xgwQ/qAHqtQssAR0QKBgQDoelgfUKeFndypiDct
zMtd9AxXAmDl4HSA01eUH9m6yXw5PS3mtK14eInJtywq+PwIvCeJz8IkaioG+JVi
x7xrACEcxjud6Yq8MkPpi8omFSlGBzL0VnZfJpEXUdwlYMiyy5viYLiFPBm8Z94x
Putiob227E1WEooaPMo77Hd4rQKBgQDFUz3bHU5R0tI4bBiM5WlBRsB1gfk/Sc10
WQi9D9d5uFfdFn2bXBzeBtWR8PYB9RO+SMc+i81Vva1IAk/7XbMLYXtoH2rrC6Fz
8OarQARhTGG5gfnAV48YD2AJGH4uCt1snVoJ/bxDz15vNSR0ywzfuURVgvSLXkPe
51PGXz6NwwKBgQDZZf+eWSgvVW6SwyUGmWrcU2puu3Stw3ZvOjO9+wL7H4whYsrX
4cIO1HnVvot5LBlUec9nmnds4jKnDjN0il/yl85fQClkBI+OalsDvYuujT9pkzXd
NDXBySkJa6247ocAXFNMITKstYVDoMYxuysXszTcKKIxiWjIHGzqGLmoiQKBgQC8
kQO3dJX3k2PZD1OWsVSYUKhyorYxSLHR0ZOMOKtNYmB0op197dSYSCenw4ET9cPc
P2hH2QlsOkpxWeRc7fm/knR/2CYwX3j2duu4EwEcigWJZS/qIsJX17mKd6F9Flzr
AqOckKFsm6o+06X3BmNTGJS4suBGntp1FNL16ua4SQKBgC0Y+822m1CjxLMtU6rr
5OH7MSHC7szyo+yiuch3APOOg1jxqjx5Ru1dohLvzU2vLmbFIWezxKmLwRYFjnoH
xhA6h48p0zvZRJlFjOyQCRAT4w3Hm938g9o81F+QNFWI44USsX/htdaPOxqR0SkP
UKzk7CPqzQYR2xHUi+VcBI93
-----END PRIVATE KEY-----
)";

static constexpr std::string_view SslRootKey = R"(
-----BEGIN CERTIFICATE-----
MIIECzCCAvOgAwIBAgIUBraeFSR5B//R/M22CKLtFNwf6XswDQYJKoZIhvcNAQEL
BQAwgZMxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJl
cmxpbjEcMBoGA1UECgwTVGhlIFF0IENvbXBhbnkgR21iSDEMMAoGA1UECwwDUiZE
MRIwEAYDVQQDDAlsb2NhbGhvc3QxIjAgBgkqhkiG9w0BCQEWE2Rlbm5pcy5vYmVy
c3RAcXQuaW8wIBcNMjUwMTE2MTU1ODIzWhgPMjEyNDEyMjMxNTU4MjNaMIGTMQsw
CQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xHDAa
BgNVBAoME1RoZSBRdCBDb21wYW55IEdtYkgxDDAKBgNVBAsMA1ImRDESMBAGA1UE
AwwJbG9jYWxob3N0MSIwIAYJKoZIhvcNAQkBFhNkZW5uaXMub2JlcnN0QHF0Lmlv
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq2n9SNDLpd59+wV2AgOy
KYLMCcZE719w67cnYDAdFyBsVS5/Us2AiQtPYbESNDRX8SxOLM0b9FfTrFH3EU70
EIuClNqHq9OhtPoRoPo7bDULlEqOQeCFSMOrY2OW1m5ngvSqrY7LyVViH3QHk02g
afnkuCdTbAkNNfNyHWCkyVrad7RMk28t2dptSaH4SFripiaO2f59l8//lZ/6bdze
m2HhP5wcx88X02JdskmqcVuOOSlE03UoWGKG2O5TuRY0M48piypSuLz5Ajz72Erg
g3yFh98DKojW60EepM6Tgcq5l3UNlY4rzXBzCV2X+EO/VzvVKSc9u2k12cJmHZxh
lwIDAQABo1MwUTAdBgNVHQ4EFgQU8jBTKqrkQqH2H7Kgyt6jtJtlZgcwHwYDVR0j
BBgwFoAU8jBTKqrkQqH2H7Kgyt6jtJtlZgcwDwYDVR0TAQH/BAUwAwEB/zANBgkq
hkiG9w0BAQsFAAOCAQEARZWMphTnpwhUKS5/Z52MZOBTeqQgZ6vJ8SfRi1LIoDw/
tVrJQm1yLEvwZUY3NKGSOy6Rseuz68L6VOj9RHVc9xD5e8Si3gh/3Yyhjdv2g+1V
fJNaMEqvVA4sGnkXk21kbnloYaVpojyyBU5nBEivvFoSyYhlT3V+6gWqfFI6+F/o
FW9KC07WXcLcM41sQN7DsOx5q+6Qfc3VYl4z+k6X8rDEe3s1zFXZu2nPgYHD/New
9/h7mAQbil06MwuPOYbousc1uY95EVRLN4XrKHDkm3TxM8wHDipoanwyPnOqcpTy
Esh/Nj3INbL1yljh4Fi447mtfgg+ERh+BgAv61ZwsA==
-----END CERTIFICATE-----
)";

struct BenchmarkData
{
    explicit BenchmarkData(uint64_t expected)
    {
        requestLatenciesNanos.reserve(expected);
        responseLatenciesNanos.reserve(expected);
    }

    int64_t callCount = {};
    std::vector<uint64_t> requestLatenciesNanos;
    std::vector<uint64_t> responseLatenciesNanos;
    uint64_t receivedBytes = {};
    uint64_t sendBytes = {};
    int64_t elapsedNanos = {};
};

#if defined QTGRPCCLIENT
#  include "google/protobuf/timestamp.qpb.h"
#else
#  include <google/protobuf/timestamp.pb.h>
#endif

static google::protobuf::Timestamp getTimestamp()
{
    google::protobuf::Timestamp ts;
    auto now = std::chrono::system_clock::now();
    auto seconds = std::chrono::time_point_cast<std::chrono::seconds>(now);
    auto nanos = static_cast<
        int32_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(now - seconds).count());
#if defined QTGRPCCLIENT
    ts.setSeconds(seconds.time_since_epoch().count());
    ts.setNanos(nanos);
#else
    ts.set_seconds(seconds.time_since_epoch().count());
    ts.set_nanos(nanos);
#endif
    return ts;
}

[[maybe_unused]] static int64_t calculateLatencyNanos(const google::protobuf::Timestamp &start,
                                                      const google::protobuf::Timestamp &end)
{
    int64_t startNanos = (start.seconds() * 1'000'000'000LL) + start.nanos();
    int64_t endNanos = (end.seconds() * 1'000'000'000LL) + end.nanos();
    return endNanos - startNanos;
}

[[maybe_unused]] static int64_t calculateLatencyNanosNow(const google::protobuf::Timestamp &start)
{
    int64_t startNanos = (start.seconds() * 1'000'000'000LL) + start.nanos();
    int64_t endNanos = std::chrono::time_point_cast<
                           std::chrono::nanoseconds>(std::chrono::system_clock::now())
                           .time_since_epoch()
                           .count();
    return endNanos - startNanos;
}

namespace Client {

template <typename T>
concept ClientConcept = requires(T t) {
    { T(std::string(), uint64_t()) };
    { t.unaryCall() } -> std::same_as<void>;
    { t.serverStreaming() } -> std::same_as<void>;
    { t.clientStreaming() } -> std::same_as<void>;
    { t.bidiStreaming() } -> std::same_as<void>;
};

template <ClientConcept C>
inline void benchmarkMain(std::string_view name, int argc, char *argv[])
{
    QStringList args;
    for (int i = 0; i < argc; ++i)
        args.push_back(argv[i]);

    QCommandLineParser parser;
    parser.setApplicationDescription(name.data());
    parser.addHelpOption();

    QCommandLineOption transport({ "t", "transport" }, "Use Transport", "http|https", "http");
#ifndef Q_OS_WINDOWS
    transport.setValueName("http|https|unix");
#endif

    QCommandLineOption calls({ "c", "calls" }, "Amount of calls made.", "size", "1000");
    QCommandLineOption payload({ "p", "payload" }, "Payload size in bytes", "size", "0");
    QCommandLineOption uniqueRpc({ "u", "unique" }, "Make each RPC on a fresh client");

    QCommandLineOption enableUnary("U", "Enable UnaryCalls");
    QCommandLineOption enableSStream("S", "Enable ServerStream");
    QCommandLineOption enableCStream("C", "Enable ClientStream");
    QCommandLineOption enableBStream("B", "Enable BiDiStream");

    parser.addOptions({
        transport,

        calls,
        payload,
        uniqueRpc,

        enableUnary,
        enableSStream,
        enableCStream,
        enableBStream,
    });

    parser.process(args);

    bool defaultRun = !parser.isSet(enableUnary) && !parser.isSet(enableSStream)
        && !parser.isSet(enableCStream) && !parser.isSet(enableBStream);
    uint64_t amountCalls = parser.value(calls).toULong();
    qsizetype payloadSize = parser.value(payload).toLong();
    const auto transportValue = parser.value(transport).toStdString();

    std::cout << std::format("#### Start of {} benchmark ####\n", name);
    std::cout << std::format("  cpu-arch: {}\n", QSysInfo::currentCpuArchitecture().toStdString());
    std::cout << std::format("  kernel: {}, {}\n", QSysInfo::kernelType().toStdString(),
                             QSysInfo::kernelVersion().toStdString());
    std::cout << std::format("  host URI: {}, {}\n\n", transportValue,
                             getTransportAddress(transportValue));

    if (parser.isSet(payload))
        std::cout << std::format("  Option: payload per message {} bytes\n", payloadSize);
    if (parser.isSet(uniqueRpc))
        std::cout << std::format("  Option: unique client per RPC {}\n", parser.isSet(uniqueRpc));

    if (parser.isSet(uniqueRpc)) {
        {
            C client(transportValue, amountCalls, payloadSize);
            if (defaultRun || parser.isSet(enableUnary))
                client.unaryCall();
        }
        {
            C client(transportValue, amountCalls, payloadSize);
            if (defaultRun || parser.isSet(enableSStream))
                client.serverStreaming();
        }
        {
            C client(transportValue, amountCalls, payloadSize);
            if (defaultRun || parser.isSet(enableCStream))
                client.clientStreaming();
        }
        {
            C client(transportValue, amountCalls, payloadSize);
            if (defaultRun || parser.isSet(enableBStream))
                client.bidiStreaming();
        }
    } else {
        C client(transportValue, amountCalls, payloadSize);
        if (defaultRun || parser.isSet(enableUnary))
            client.unaryCall();
        if (defaultRun || parser.isSet(enableSStream))
            client.serverStreaming();
        if (defaultRun || parser.isSet(enableCStream))
            client.clientStreaming();
        if (defaultRun || parser.isSet(enableBStream))
            client.bidiStreaming();
    }

    std::cout << std::format("\n#### End of {} benchmark ####\n", name);
}

inline std::string formatTime(uint64_t ns)
{
    std::ostringstream oss;
    if (ns < 1000) {
        oss << ns << " ns";
    } else if (ns < 1000000) { // less than 1e6 ns -> microseconds
        oss << std::fixed << std::setprecision(2) << (ns / 1000.0) << " us";
    } else if (ns < 1000000000) { // less than 1e9 ns -> milliseconds
        oss << std::fixed << std::setprecision(2) << (ns / 1000000.0) << " ms";
    } else { // one second or more -> seconds
        oss << std::fixed << std::setprecision(2) << (ns / 1000000000.0) << " s";
    }
    return oss.str();
}

inline std::string formatBytes(double bytes, uint8_t p)
{
    std::ostringstream oss;
    constexpr uint32_t D = 1000;
    if (bytes < D) {
        oss << std::fixed << std::setprecision(p) << bytes << " B";
    } else if (bytes < D * D) {
        oss << std::fixed << std::setprecision(p) << (bytes / D) << " KB";
    } else if (bytes < D * D * D) {
        oss << std::fixed << std::setprecision(p) << (bytes / (D * D)) << " MB";
    } else {
        oss << std::fixed << std::setprecision(p) << (bytes / (D * D * D)) << " GB";
    }
    return oss.str();
}

inline void printLatencyStats(const std::vector<uint64_t> &latencies, const std::string &label)
{
    if (latencies.empty())
        return;

    std::vector<uint64_t> sorted = latencies;
    std::sort(sorted.begin(), sorted.end());
    size_t n = sorted.size();

    double sum = std::accumulate(sorted.begin(), sorted.end(), 0.0);
    double mean = sum / n;

    // Compute the p-th percentile of latency
    auto percentile = [n, &sorted](double p) -> uint64_t {
        size_t idx = (n == 0) ? 0 : (static_cast<size_t>(std::ceil(p * n)) - 1);
        if (idx >= n)
            idx = n - 1;
        return sorted[idx];
    };

    uint64_t p80 = percentile(0.80);
    uint64_t p95 = percentile(0.95);
    uint64_t minVal = sorted.front();
    uint64_t maxVal = sorted.back();

    double variance = std::accumulate(sorted.begin(), sorted.end(), 0.0,
                                      [mean](double acc, uint64_t val) {
                                          double diff = static_cast<double>(val) - mean;
                                          return acc + diff * diff;
                                      })
        / n;
    double stddev = std::sqrt(variance);

    std::vector<std::string> l = {
        formatTime(minVal), formatTime(static_cast<uint64_t>(mean)),
        formatTime(maxVal), formatTime(p80),
        formatTime(p95),    formatTime(static_cast<uint64_t>(stddev)),
    };
    constexpr int keyWidth = 6;
    auto valWidth = std::ranges::max(l | std::views::transform(&std::string::size)) + 1;
    std::cout << label << " Latencies:\n";
    std::cout << std::format("  {:<{}}: {:<{}}| {:<{}}: {:<{}}| {:<{}}: {:<{}}\n", "Min", keyWidth,
                             l[0], valWidth, "Mean", keyWidth, l[1], valWidth, "Max", keyWidth,
                             l[2], valWidth);
    std::cout << std::format("  {:<{}}: {:<{}}| {:<{}}: {:<{}}| {:<{}}: {:<{}}\n", "P80", keyWidth,
                             l[3], valWidth, "P95", keyWidth, l[4], valWidth, "StdDev", keyWidth,
                             l[5], valWidth);
}

void printBenchmarkResult(const std::string &title, const BenchmarkData &data)
{
    assert(data.callCount > 0);
    std::string titleString = "========== Benchmark Results: " + title + " ==========";
    std::cout << std::format("\n{}\n", titleString);

    std::cout << "Calls: " << data.callCount << "\n";

    auto avgCallTime = data.elapsedNanos / data.callCount;
    std::cout << "Total Time: " << formatTime(data.elapsedNanos);
    std::cout << " | Avg per call: " << formatTime(avgCallTime);
    std::cout << "\n";

    printLatencyStats(data.requestLatenciesNanos, "Request  (Client → Server)");
    printLatencyStats(data.responseLatenciesNanos, "Response (Server → Client)");

    if (data.sendBytes > 0 || data.receivedBytes > 0) {
        std::cout << "Total Sent: " << formatBytes(double(data.sendBytes), 0)
                  << " | Total Recv: " << formatBytes(double(data.receivedBytes), 0) << '\n';
    }

    double totalTimeSec = double(data.elapsedNanos) / 1e9;
    double totalBytes = static_cast<double>(data.sendBytes + data.receivedBytes);
    double throughput = (totalTimeSec > 0) ? (totalBytes / totalTimeSec) : 0.0;
    if (throughput > 0.0)
        std::cout << "Throughput: " << formatBytes(throughput, 2) << "/s\n";
    double qps = totalTimeSec > 0.0 ? (data.callCount / totalTimeSec) : 0.0;
    std::cout << std::format("QPS: {:.0f}{}\n", qps > 10000.0 ? qps / 1'000 : qps,
                             qps > 10'000 ? " k" : "");

    std::cout << std::string(titleString.size(), '=') << '\n';
}

inline void printRpcResult(std::string benched, int64_t elapsedNs, uint64_t amountCalls,
                           uint64_t recvBytes = 0, uint64_t sendBytes = 0)
{
    using namespace std::chrono;

    std::cout << std::format("Finished benchmark: {}\n", benched);
    const auto ns = nanoseconds(elapsedNs);
    const auto us = duration_cast<microseconds>(ns);
    std::cout << std::format("  Completed calls: {}\n", amountCalls);
    std::cout << std::format("  Total time: {}, {}\n", us, ns);
    const auto avgNs = nanoseconds(ns / amountCalls);
    const auto avgUs = duration_cast<microseconds>(avgNs);
    std::cout << std::format("  Average time: {}, {}\n", avgUs, avgNs);
    if (recvBytes > 0 && sendBytes > 0) {
        std::cout << std::format("  Send bytes: {}, Recv bytes: {}\n", sendBytes, recvBytes);
        const auto sec = duration_cast<duration<double>>(ns);
        double totalTimeSec = sec.count();
        double totalBytes = static_cast<double>(recvBytes + sendBytes);
        double throughputKBPerSec = (totalBytes / 1024) / totalTimeSec;
        std::cout << std::format("  Throughput (KB/sec): {:.2f}\n", throughputKBPerSec);
    }
}

} // namespace Client

#endif // QRPCBENCH_COMMON_H
