// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "dht_proxy_server.h"

#include "default_types.h"
#include "dhtrunner.h"
#include "base64.h"

#include <msgpack.hpp>
#include <json/json.h>

#include <chrono>
#include <functional>
#include <limits>
#include <iostream>
#include <fstream>

using namespace std::placeholders;
using namespace std::chrono_literals;
using namespace std::literals;

namespace dht {
constexpr char RESP_MSG_JSON_INCORRECT[] = "{\"err:\":\"Incorrect JSON\"}";
constexpr char RESP_MSG_SERVICE_UNAVAILABLE[] = "{\"err\":\"Incorrect DhtRunner\"}";
constexpr char RESP_MSG_INTERNAL_SERVER_ERRROR[] = "{\"err\":\"Internal server error\"}";
constexpr char RESP_MSG_MISSING_PARAMS[] = "{\"err\":\"Missing parameters\"}";
constexpr char RESP_MSG_PUT_FAILED[] = "{\"err\":\"Put failed\"}";
#ifdef OPENDHT_PROXY_SERVER_IDENTITY
constexpr char RESP_MSG_DESTINATION_NOT_FOUND[] = "{\"err\":\"No destination found\"}";
#endif
#ifdef OPENDHT_PUSH_NOTIFICATIONS
constexpr char RESP_MSG_NO_TOKEN[] = "{\"err\":\"No token\"}";
#endif

constexpr const std::chrono::minutes PRINT_STATS_PERIOD {2};

using ResponseByParts = restinio::chunked_output_t;
using ResponseByPartsBuilder = restinio::response_builder_t<ResponseByParts>;

namespace http {
/**
 * Session value associated with a connection_id_t key.
 */
struct ListenerSession
{
    ListenerSession() = default;
    dht::InfoHash hash;
    std::future<size_t> token;
    std::shared_ptr<restinio::response_builder_t<restinio::chunked_output_t>> response;
};
} // namespace http

class opendht_logger_t
{
public:
    opendht_logger_t(std::shared_ptr<Logger> logger = {})
        : m_logger(std::move(logger))
    {}

    template<typename Builder>
    void trace(Builder&& /* msg_builder */)
    {
        /* if (m_logger) m_logger->d("[proxy:server] %s", msg_builder().c_str()); */
    }

    template<typename Builder>
    void info(Builder&& msg_builder)
    {
        if (m_logger)
            m_logger->debug("[proxy:server] {}", msg_builder());
    }

    template<typename Builder>
    void warn(Builder&& msg_builder)
    {
        if (m_logger)
            m_logger->warn("[proxy:server] {}", msg_builder());
    }

    template<typename Builder>
    void error(Builder&& msg_builder)
    {
        if (m_logger)
            m_logger->error("[proxy:server] {}", msg_builder());
    }

private:
    std::shared_ptr<Logger> m_logger;
};

std::string
DhtProxyServer::ServerStats::toString() const
{
    auto ret = fmt::format("Listens: {}, Puts: {}, PushListeners: {}\n"
                           "Push requests in the last {}: [Android: {}], [iOS: {}], [Unified: {}]\n"
                           "Requests: {} per second.",
                           listenCount,
                           putCount,
                           pushListenersCount,
                           print_duration(lastUpdated - serverStartTime),
                           androidPush.toString(),
                           iosPush.toString(),
                           unifiedPush.toString(),
                           requestRate);
    if (nodeInfo) {
        auto& ipv4 = nodeInfo->ipv4;
        if (ipv4.table_depth > 1)
            ret += fmt::format("IPv4 Network estimation: {}\n", ipv4.getNetworkSizeEstimation());
        auto& ipv6 = nodeInfo->ipv6;
        if (ipv6.table_depth > 1)
            ret += fmt::format("IPv6 Network estimation: {}\n", ipv6.getNetworkSizeEstimation());
    }
    return ret;
}

Json::Value
DhtProxyServer::ServerStats::toJson() const
{
    Json::Value result;
    result["listenCount"] = static_cast<Json::UInt64>(listenCount);
    result["putCount"] = static_cast<Json::UInt64>(putCount);
    result["totalPermanentPuts"] = static_cast<Json::UInt64>(totalPermanentPuts);
    result["pushListenersCount"] = static_cast<Json::UInt64>(pushListenersCount);
    result["serverStartTime"] = static_cast<Json::LargestInt>(to_time_t(serverStartTime));
    result["lastUpdated"] = static_cast<Json::LargestInt>(to_time_t(lastUpdated));
    result["androidPush"] = androidPush.toJson();
    result["iosPush"] = iosPush.toJson();
    result["unifiedPush"] = unifiedPush.toJson();
    result["requestRate"] = requestRate;
    if (nodeInfo)
        result["nodeInfo"] = nodeInfo->toJson();
    return result;
}

restinio::request_handling_status_t
DhtProxyServer::serverError(restinio::request_t& request)
{
    auto response = initHttpResponse(request.create_response(restinio::status_internal_server_error()));
    response.set_body(RESP_MSG_INTERNAL_SERVER_ERRROR);
    return response.done();
}

// connection listener

class DhtProxyServer::ConnectionListener
{
public:
    ConnectionListener() {};
    explicit ConnectionListener(std::function<void(restinio::connection_id_t)> onClosed)
        : onClosed_(std::move(onClosed)) {};
    ~ConnectionListener() {};

    /**
     * Connection state change used to handle Listeners disconnects.
     * RESTinio >= 0.5.1 https://github.com/Stiffstream/restinio/issues/28
     */
    void state_changed(const restinio::connection_state::notice_t& notice) noexcept;

private:
    std::function<void(restinio::connection_id_t)> onClosed_;
};

void
DhtProxyServer::ConnectionListener::state_changed(const restinio::connection_state::notice_t& notice) noexcept
{
    if (std::holds_alternative<restinio::connection_state::closed_t>(notice.cause())) {
        onClosed_(notice.connection_id());
    }
}

void
DhtProxyServer::onConnectionClosed(restinio::connection_id_t id)
{
    std::lock_guard lock(lockListener_);
    auto it = listeners_.find(id);
    if (it != listeners_.end()) {
        dht_->cancelListen(it->second.hash, std::move(it->second.token));
        listeners_.erase(it);
        if (logger_)
            logger_->debug("[proxy:server] [connection:{}] listener cancelled, {} still connected",
                           id,
                           listeners_.size());
    }
}

struct DhtProxyServer::RestRouterTraitsTls : public restinio::default_tls_traits_t
{
    using timer_manager_t = restinio::asio_timer_manager_t;
    using logger_t = opendht_logger_t;
    using request_handler_t = RestRouter;
    using connection_state_listener_t = ConnectionListener;
};
struct DhtProxyServer::RestRouterTraits : public restinio::default_traits_t
{
    using timer_manager_t = restinio::asio_timer_manager_t;
    using logger_t = opendht_logger_t;
    using request_handler_t = RestRouter;
    using connection_state_listener_t = ConnectionListener;
};

void
DhtProxyServer::PermanentPut::msgpack_unpack(const msgpack::object& o)
{
    if (auto cid = findMapValue(o, "cid"sv)) {
        clientId = cid->as<std::string>();
    }
    if (auto exp = findMapValue(o, "exp"sv)) {
        expiration = from_time_t(exp->as<time_t>());
    }
    if (auto token = findMapValue(o, "token"sv)) {
        pushToken = token->as<std::string>();
    }
    if (auto sid = findMapValue(o, "sid"sv)) {
        if (not sessionCtx)
            sessionCtx = std::make_shared<PushSessionContext>(sid->as<std::string>());
        else
            sessionCtx->sessionId = sid->as<std::string>();
    }
    if (auto t = findMapValue(o, "t"sv)) {
        type = t->as<PushType>();
    }
    if (auto val = findMapValue(o, "value"sv)) {
        value = std::make_shared<dht::Value>(*val);
    }
    if (auto top = findMapValue(o, "top"sv)) {
        topic = top->as<std::string>();
    }
}

#ifdef OPENDHT_PUSH_NOTIFICATIONS
void
DhtProxyServer::Listener::msgpack_unpack(const msgpack::object& o)
{
    if (auto cid = findMapValue(o, "cid"sv)) {
        clientId = cid->as<std::string>();
    }
    if (auto exp = findMapValue(o, "exp"sv)) {
        expiration = from_time_t(exp->as<time_t>());
    }
    if (auto sid = findMapValue(o, "sid"sv)) {
        if (not sessionCtx)
            sessionCtx = std::make_shared<PushSessionContext>(sid->as<std::string>());
        else
            sessionCtx->sessionId = sid->as<std::string>();
    }
    if (auto t = findMapValue(o, "t"sv)) {
        type = t->as<PushType>();
    }
    if (auto top = findMapValue(o, "top"sv)) {
        topic = top->as<std::string>();
    }
}
#endif

DhtProxyServer::DhtProxyServer(const std::shared_ptr<DhtRunner>& dht,
                               const ProxyServerConfig& config,
                               const std::shared_ptr<dht::Logger>& logger)
    : ioContext_(std::make_shared<asio::io_context>())
    , dht_(dht)
    , persistPath_(config.persistStatePath)
    , logger_(logger)
    , printStatsTimer_(std::make_unique<asio::steady_timer>(*ioContext_, 3s))
    , serverStartTime_(clock::now())
    , connListener_(std::make_shared<ConnectionListener>(
          std::bind(&DhtProxyServer::onConnectionClosed, this, std::placeholders::_1)))
    , pushServer_(config.pushServer)
    , bundleId_(config.bundleId)
{
    if (not dht_)
        throw std::invalid_argument("A DHT instance must be provided");

    if (logger_)
        logger_->debug("[proxy:server] [init] running on {:d}", config.port);
    if (not pushServer_.empty()) {
#ifdef OPENDHT_PUSH_NOTIFICATIONS
        if (logger_)
            logger_->debug("[proxy:server] [init] using push server {}", pushServer_);
#else
        if (logger_)
            logger_->error("[proxy:server] [init] opendht built without push notification support");
#endif
    }

    jsonBuilder_["commentStyle"] = "None";
    jsonBuilder_["indentation"] = "";

    if (!pushServer_.empty()) {
        // no host delim, assume port only
        if (pushServer_.find(":") == std::string::npos)
            pushServer_ = "localhost:" + pushServer_;
        // define http request destination for push notifications
        pushHostPort_ = splitPort(pushServer_);
        if (logger_)
            logger_->debug("Using push server for notifications: {}:{}", pushHostPort_.first, pushHostPort_.second);
    }
    if (config.identity.first and config.identity.second) {
        asio::error_code ec;
        // define tls context
        asio::ssl::context tls_context {asio::ssl::context::tlsv13_server};
        tls_context.set_options(asio::ssl::context::default_workarounds | asio::ssl::context::no_sslv2
                                    | asio::ssl::context::single_dh_use,
                                ec);
        if (ec)
            throw std::runtime_error("Error setting tls context options: " + ec.message());
        // add more security options
#ifdef SSL_OP_NO_RENEGOTIATION
        SSL_CTX_set_options(tls_context.native_handle(), SSL_OP_NO_RENEGOTIATION); // CVE-2009-3555
#endif
        // node private key
        auto key = config.identity.first->serialize();
        tls_context.use_private_key(asio::const_buffer {key.data(), key.size()},
                                    asio::ssl::context::file_format::pem,
                                    ec);
        if (ec)
            throw std::runtime_error("Error setting node's private key: " + ec.message());
        // certificate chain
        auto certchain = config.identity.second->toString(true /*chain*/);
        tls_context.use_certificate_chain(asio::const_buffer {certchain.data(), certchain.size()}, ec);
        if (ec)
            throw std::runtime_error("Error setting certificate chain: " + ec.message());
        if (logger_)
            logger_->debug("[proxy:server] using certificate chain for ssl:\n{}", certchain);
        // build http server
        auto settings = restinio::run_on_this_thread_settings_t<RestRouterTraitsTls>();
        addServerSettings(settings);
        if (not config.address.empty())
            settings.address(config.address);
        settings.port(config.port);
        settings.tls_context(std::move(tls_context));
        httpsServer_ = std::make_unique<restinio::http_server_t<RestRouterTraitsTls>>(
            ioContext_, std::forward<restinio::run_on_this_thread_settings_t<RestRouterTraitsTls>>(std::move(settings)));
        // run https server
        serverThread_ = std::thread([this] {
            httpsServer_->open_async([] { /*ok*/ }, [](std::exception_ptr ex) { std::rethrow_exception(ex); });
            httpsServer_->io_context().run();
        });
    } else {
        auto settings = restinio::run_on_this_thread_settings_t<RestRouterTraits>();
        addServerSettings(settings);
        if (not config.address.empty())
            settings.address(config.address);
        settings.port(config.port);
        httpServer_ = std::make_unique<restinio::http_server_t<RestRouterTraits>>(
            ioContext_, std::forward<restinio::run_on_this_thread_settings_t<RestRouterTraits>>(std::move(settings)));
        // run http server
        serverThread_ = std::thread([this]() {
            httpServer_->open_async([] { /*ok*/ }, [](std::exception_ptr ex) { std::rethrow_exception(ex); });
            httpServer_->io_context().run();
        });
    }
    dht->forwardAllMessages(true);
    updateStats();
    printStatsTimer_->async_wait(std::bind(&DhtProxyServer::handlePrintStats, this, std::placeholders::_1));

    if (not persistPath_.empty()) {
        try {
            std::ifstream stateFile(persistPath_, std::ios::binary | std::ios::ate);
            if (stateFile) {
                std::streamsize size = stateFile.tellg();
                stateFile.seekg(0, std::ios::beg);
                if (logger_)
                    logger_->debug("[proxy:server] Loading proxy state from {} ({} bytes)", persistPath_, size);
                loadState(stateFile, size);
            }
        } catch (const std::exception& e) {
            if (logger_)
                logger_->error("[proxy:server] Error loading state from file: {}", e.what());
        }
    }
}

template<typename Os>
void
DhtProxyServer::saveState(Os& stream)
{
    msgpack::packer<Os> pk(&stream);
    pk.pack_map(2);
    {
        std::lock_guard lock(lockSearchPuts_);
        pk.pack("puts");
        pk.pack(puts_);
    }
#ifdef OPENDHT_PUSH_NOTIFICATIONS
    {
        std::lock_guard lock(lockListener_);
        pk.pack("pushListeners");
        pk.pack(pushListeners_);
    }
#endif
}

template<typename Is>
void
DhtProxyServer::loadState(Is& is, size_t size)
{
    msgpack::unpacker pac;
    pac.reserve_buffer(size);
    if (is.read(pac.buffer(), size)) {
        pac.buffer_consumed(size);

        msgpack::object_handle oh;
        while (pac.next(oh)) {
            if (oh.get().type != msgpack::type::MAP)
                continue;
            if (auto puts = findMapValue(oh.get(), "puts"sv)) {
                std::lock_guard lock(lockSearchPuts_);
                puts_ = puts->as<decltype(puts_)>();
                if (logger_)
                    logger_->debug("[proxy:server] Loading {} persistent puts", puts_.size());
                for (auto& put : puts_) {
                    for (auto& pput : put.second.puts) {
                        pput.second.expireTimer = std::make_unique<asio::steady_timer>(io_context(),
                                                                                       pput.second.expiration);
                        pput.second.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPermamentPut,
                                                                      this,
                                                                      std::placeholders::_1,
                                                                      put.first,
                                                                      pput.first));
#ifdef OPENDHT_PUSH_NOTIFICATIONS
                        if (not pput.second.pushToken.empty()) {
                            auto jsonProvider = [infoHash = put.first.toString(),
                                                 clientId = pput.second.clientId,
                                                 vid = pput.first,
                                                 sessionCtx = pput.second.sessionCtx]() {
                                Json::Value json;
                                json["timeout"] = infoHash;
                                json["to"] = clientId;
                                json["vid"] = std::to_string(vid);
                                if (sessionCtx) {
                                    std::lock_guard l(sessionCtx->lock);
                                    json["s"] = sessionCtx->sessionId;
                                }
                                return json;
                            };
                            pput.second.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(),
                                                                                                 pput.second.expiration
                                                                                                     - proxy::OP_MARGIN);
                            pput.second.expireNotifyTimer->async_wait(
                                std::bind(&DhtProxyServer::handleNotifyPushListenExpire,
                                          this,
                                          std::placeholders::_1,
                                          pput.second.pushToken,
                                          std::move(jsonProvider),
                                          pput.second.type,
                                          pput.second.topic));
                        }
#endif
                        dht_->put(put.first, pput.second.value, DoneCallbackSimple {}, time_point::max(), true);
                    }
                }
            } else {
                if (logger_)
                    logger_->debug("No persistent puts in state");
            }
#ifdef OPENDHT_PUSH_NOTIFICATIONS
            if (auto pushListeners = findMapValue(oh.get(), "pushListeners"sv)) {
                std::lock_guard lock(lockListener_);
                pushListeners_ = pushListeners->as<decltype(pushListeners_)>();
                if (logger_)
                    logger_->debug("[proxy:server] Loading {} push listeners", pushListeners_.size());
                for (auto& pushListener : pushListeners_) {
                    for (auto& listeners : pushListener.second.listeners) {
                        for (auto& listener : listeners.second) {
                            // start listening
                            listener.internalToken
                                = dht_->listen(listeners.first,
                                               [this,
                                                infoHash = listeners.first,
                                                pushToken = pushListener.first,
                                                type = listener.type,
                                                clientId = listener.clientId,
                                                sessionCtx = listener.sessionCtx,
                                                topic = listener.topic](const std::vector<Sp<Value>>& values,
                                                                        bool expired) {
                                                   return this->handlePushListen(infoHash,
                                                                                 pushToken,
                                                                                 type,
                                                                                 clientId,
                                                                                 sessionCtx,
                                                                                 topic,
                                                                                 values,
                                                                                 expired);
                                               });
                            // expire notify
                            listener.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(),
                                                                                              listener.expiration
                                                                                                  - proxy::OP_MARGIN);
                            auto jsonProvider = [infoHash = listeners.first.toString(),
                                                 clientId = listener.clientId,
                                                 sessionCtx = listener.sessionCtx]() {
                                Json::Value json;
                                json["timeout"] = infoHash;
                                json["to"] = clientId;
                                std::lock_guard l(sessionCtx->lock);
                                json["s"] = sessionCtx->sessionId;
                                return json;
                            };
                            listener.expireNotifyTimer->async_wait(
                                std::bind(&DhtProxyServer::handleNotifyPushListenExpire,
                                          this,
                                          std::placeholders::_1,
                                          pushListener.first,
                                          std::move(jsonProvider),
                                          listener.type,
                                          listener.topic));
                            // cancel push listen
                            listener.expireTimer = std::make_unique<asio::steady_timer>(io_context(),
                                                                                        listener.expiration);
                            listener.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPushListen,
                                                                       this,
                                                                       std::placeholders::_1,
                                                                       pushListener.first,
                                                                       listeners.first,
                                                                       listener.clientId));
                        }
                    }
                }
            } else {
                if (logger_)
                    logger_->debug("[proxy:server] No push listeners in state");
            }
#endif
        }
        if (logger_)
            logger_->debug("[proxy:server] loading ended");
    }
}

asio::io_context&
DhtProxyServer::io_context() const
{
    return *ioContext_;
}

DhtProxyServer::~DhtProxyServer()
{
    if (not persistPath_.empty()) {
        if (logger_)
            logger_->debug("[proxy:server] Saving proxy state to {}", persistPath_);
        std::ofstream stateFile(persistPath_, std::ios::binary);
        saveState(stateFile);
    }
    if (dht_) {
        std::lock_guard lock(lockListener_);
        for (auto& l : listeners_) {
            dht_->cancelListen(l.second.hash, std::move(l.second.token));
            if (l.second.response)
                l.second.response->done();
        }
#ifdef OPENDHT_PUSH_NOTIFICATIONS
        for (auto& lm : pushListeners_) {
            for (auto& ls : lm.second.listeners)
                for (auto& l : ls.second) {
                    if (l.expireNotifyTimer)
                        l.expireNotifyTimer->cancel();
                    if (l.expireTimer)
                        l.expireTimer->cancel();
                    dht_->cancelListen(ls.first, std::move(l.internalToken));
                }
        }
        pushListeners_.clear();
#endif
    }
    if (logger_)
        logger_->debug("[proxy:server] closing http server");
    ioContext_->stop();
    if (serverThread_.joinable())
        serverThread_.join();
    if (logger_)
        logger_->debug("[proxy:server] http server closed");
}

template<typename ServerSettings>
void
DhtProxyServer::addServerSettings(ServerSettings& settings, const unsigned int max_pipelined_requests)
{
    using namespace std::chrono;
    /**
     * If max_pipelined_requests is greater than 1 then RESTinio will continue
     * to read from the socket after parsing the first request.
     * In that case, RESTinio can detect the disconnection
     * and calls state listener as expected.
     * https://github.com/Stiffstream/restinio/issues/28
     */
    settings.max_pipelined_requests(max_pipelined_requests);
    // one less to detect the listener disconnect
    settings.concurrent_accepts_count(max_pipelined_requests - 1);
    settings.separate_accept_and_create_connect(true);
    settings.logger(logger_);
    settings.protocol(restinio::asio_ns::ip::tcp::v6());
    settings.request_handler(createRestRouter());
    // time limits                                              // ~ 0.8 month
    std::chrono::milliseconds timeout_request(std::numeric_limits<int>::max());
    settings.read_next_http_message_timelimit(timeout_request);
    settings.write_http_response_timelimit(60s);
    settings.handle_request_timeout(timeout_request);
    // socket options
    settings.socket_options_setter([](auto& options) {
        options.set_option(asio::ip::tcp::no_delay {true});
        options.set_option(asio::socket_base::keep_alive {true});
    });
    settings.connection_state_listener(connListener_);
}

std::shared_ptr<DhtProxyServer::ServerStats>
DhtProxyServer::updateStats(std::shared_ptr<NodeInfo> info) const
{
    auto now = clock::now();
    auto last = lastStatsReset_.exchange(now);
    auto count = requestNum_.exchange(0);
    auto dt = std::chrono::duration<double>(now - last);
    auto sstats = std::make_shared<ServerStats>();
    auto& stats = *sstats;
    stats.requestRate = count / dt.count();
#ifdef OPENDHT_PUSH_NOTIFICATIONS
    stats.pushListenersCount = pushListeners_.size();
    {
        std::lock_guard lk(pushStatsMutex_);
        stats.androidPush = androidPush_;
        stats.iosPush = iosPush_;
        stats.unifiedPush = unifiedPush_;
    }
#endif
    stats.serverStartTime = serverStartTime_;
    stats.lastUpdated = now;
    stats.totalPermanentPuts = 0;
    std::for_each(puts_.begin(), puts_.end(), [&stats](const auto& put) {
        stats.totalPermanentPuts += put.second.puts.size();
    });
    stats.putCount = puts_.size();
    stats.listenCount = listeners_.size();
    stats.nodeInfo = std::move(info);
    return sstats;
}

void
DhtProxyServer::updateStats()
{
    dht_->getNodeInfo([this](std::shared_ptr<NodeInfo> newInfo) {
        stats_ = updateStats(newInfo);
        nodeInfo_ = newInfo;
        if (logger_) {
            auto str = Json::writeString(jsonBuilder_, newInfo->toJson());
            logger_->debug("[proxy:server] [stats] {}", str);
        }
    });
}

void
DhtProxyServer::handlePrintStats(const asio::error_code& ec)
{
    if (ec == asio::error::operation_aborted)
        return;
    updateStats();
    printStatsTimer_->expires_at(printStatsTimer_->expiry() + PRINT_STATS_PERIOD);
    printStatsTimer_->async_wait(std::bind(&DhtProxyServer::handlePrintStats, this, std::placeholders::_1));
}

template<typename HttpResponse>
HttpResponse
DhtProxyServer::initHttpResponse(HttpResponse response)
{
    response.append_header("Server", "RESTinio");
    response.append_header(restinio::http_field::content_type, "application/json");
    response.append_header(restinio::http_field::access_control_allow_origin, "*");
    return response;
}

std::unique_ptr<RestRouter>
DhtProxyServer::createRestRouter()
{
    using namespace std::placeholders;
    auto router = std::make_unique<RestRouter>();

    // **************************** LEGACY ROUTES ****************************
    // node.info
    router->http_get("/", std::bind(&DhtProxyServer::getNodeInfo, this, _1, _2));
    // key.options
    router->add_handler(restinio::http_method_options(), "/:hash", std::bind(&DhtProxyServer::options, this, _1, _2));
    // key.get
    router->http_get("/:hash", std::bind(&DhtProxyServer::get, this, _1, _2));
    // key.post
    router->http_post("/:hash", std::bind(&DhtProxyServer::put, this, _1, _2));
#ifdef OPENDHT_PUSH_NOTIFICATIONS
    // key.subscribe
    router->add_handler(restinio::http_method_subscribe(),
                        "/:hash",
                        std::bind(&DhtProxyServer::subscribe, this, _1, _2));
    // key.unsubscribe
    router->add_handler(restinio::http_method_unsubscribe(),
                        "/:hash",
                        std::bind(&DhtProxyServer::unsubscribe, this, _1, _2));
#endif // OPENDHT_PUSH_NOTIFICATIONS
#ifdef OPENDHT_PROXY_SERVER_IDENTITY
#endif // OPENDHT_PROXY_SERVER_IDENTITY

    // **************************** NEW ROUTES ****************************
    // node.info
    router->http_get("/node/info", std::bind(&DhtProxyServer::getNodeInfo, this, _1, _2));
    // node.stats
    router->http_get("/node/stats", std::bind(&DhtProxyServer::getStats, this, _1, _2));
    // key.options
    router->http_get("/key/:hash/options", std::bind(&DhtProxyServer::options, this, _1, _2));
    // key.get
    router->http_get("/key/:hash", std::bind(&DhtProxyServer::get, this, _1, _2));
    // key.post
    router->http_post("/key/:hash", std::bind(&DhtProxyServer::put, this, _1, _2));
    router->add_handler(restinio::http_method_options(),
                        "/key/:hash/listen",
                        std::bind(&DhtProxyServer::options, this, _1, _2));
    // key.listen
    router->http_get("/key/:hash/listen", std::bind(&DhtProxyServer::listen, this, _1, _2));
#ifdef OPENDHT_PUSH_NOTIFICATIONS
    // node.pingPush
    router->http_post("/node/pingPush", std::bind(&DhtProxyServer::pingPush, this, _1, _2));
    // key.subscribe
    router->add_handler(restinio::http_method_subscribe(),
                        "/key/:hash",
                        std::bind(&DhtProxyServer::subscribe, this, _1, _2));
    // key.unsubscribe
    router->add_handler(restinio::http_method_unsubscribe(),
                        "/key/:hash",
                        std::bind(&DhtProxyServer::unsubscribe, this, _1, _2));
#endif // OPENDHT_PUSH_NOTIFICATIONS
#ifdef OPENDHT_PROXY_SERVER_IDENTITY
    // key.sign
    router->http_post("/key/:hash/sign", std::bind(&DhtProxyServer::putSigned, this, _1, _2));
    // key.encrypt
    router->http_post("/key/:hash/encrypt", std::bind(&DhtProxyServer::putEncrypted, this, _1, _2));
#endif // OPENDHT_PROXY_SERVER_IDENTITY

    return router;
}

RequestStatus
DhtProxyServer::getNodeInfo(restinio::request_handle_t request, restinio::router::route_params_t /*params*/) const
{
    try {
        if (auto nodeInfo = nodeInfo_) {
            auto result = nodeInfo->toJson();
            // [ipv6:ipv4]:port or ipv4:port
            result["public_ip"] = request->remote_endpoint().address().to_string();
            auto response = initHttpResponse(request->create_response());
            response.append_body(Json::writeString(jsonBuilder_, result) + "\n");
            return response.done();
        }
        auto response = initHttpResponse(request->create_response(restinio::status_service_unavailable()));
        response.set_body(RESP_MSG_SERVICE_UNAVAILABLE);
        return response.done();
    } catch (...) {
        return serverError(*request);
    }
}

RequestStatus
DhtProxyServer::getStats(restinio::request_handle_t request, restinio::router::route_params_t /*params*/)
{
    requestNum_++;
    try {
        if (auto stats = stats_) {
            auto response = initHttpResponse(request->create_response());
            response.append_body(Json::writeString(jsonBuilder_, stats->toJson()) + "\n");
            return response.done();
        } else {
            auto response = initHttpResponse(request->create_response(restinio::status_service_unavailable()));
            response.set_body(RESP_MSG_SERVICE_UNAVAILABLE);
            return response.done();
        }
    } catch (...) {
        return serverError(*request);
    }
}

RequestStatus
DhtProxyServer::get(restinio::request_handle_t request, restinio::router::route_params_t params)
{
    requestNum_++;
    try {
        InfoHash infoHash(params["hash"]);
        if (!infoHash)
            infoHash = InfoHash::get(params["hash"]);
        auto response = std::make_shared<ResponseByPartsBuilder>(
            initHttpResponse(request->create_response<ResponseByParts>()));
        response->flush();
        dht_->get(
            infoHash,
            [this, response](const std::vector<Sp<Value>>& values) {
                std::ostringstream output;
                for (const auto& value : values) {
                    output << Json::writeString(jsonBuilder_, value->toJson()) << "\n";
                }
                response->append_chunk(output.str());
                response->flush();
                return true;
            },
            [response](bool /*ok*/) { response->done(); });
        return restinio::request_handling_status_t::accepted;
    } catch (const std::exception& e) {
        return serverError(*request);
    }
}

RequestStatus
DhtProxyServer::listen(restinio::request_handle_t request, restinio::router::route_params_t params)
{
    requestNum_++;

    try {
        InfoHash infoHash(params["hash"]);
        if (!infoHash)
            infoHash = InfoHash::get(params["hash"]);
        auto response = std::make_shared<ResponseByPartsBuilder>(
            initHttpResponse(request->create_response<ResponseByParts>()));
        response->flush();
        std::lock_guard lock(lockListener_);
        // save the listener to handle a disconnect
        auto& session = listeners_[request->connection_id()];
        session.hash = infoHash;
        session.response = response;
        session.token = dht_->listen(infoHash, [this, response](const std::vector<Sp<Value>>& values, bool expired) {
            for (const auto& value : values) {
                auto jsonVal = value->toJson();
                if (expired)
                    jsonVal["expired"] = true;
                response->append_chunk(Json::writeString(jsonBuilder_, jsonVal) + "\n");
            }
            response->flush();
            return true;
        });
        return restinio::request_handling_status_t::accepted;
    } catch (const std::exception& e) {
        return serverError(*request);
    }
}

#ifdef OPENDHT_PUSH_NOTIFICATIONS

PushType
DhtProxyServer::getTypeFromString(const std::string& type)
{
    if (type == "android")
        return PushType::Android;
    else if (type == "ios")
        return PushType::iOS;
    else if (type == "unifiedpush")
        return PushType::UnifiedPush;
    return PushType::None;
}

std::string
DhtProxyServer::getDefaultTopic(PushType)
{
    return bundleId_;
}

RequestStatus
DhtProxyServer::pingPush(restinio::request_handle_t request, restinio::router::route_params_t /*params*/)
{
    requestNum_++;
    try {
        std::string err;
        Json::Value r;
        auto* char_data = reinterpret_cast<const char*>(request->body().data());
        auto reader = std::unique_ptr<Json::CharReader>(jsonReaderBuilder_.newCharReader());
        if (!reader->parse(char_data, char_data + request->body().size(), &r, &err)) {
            auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
            response.set_body(RESP_MSG_JSON_INCORRECT);
            return response.done();
        }
        const Json::Value& root(r); // parse using const Json so [] never creates element
        auto pushToken = root["key"].asString();
        if (pushToken.empty()) {
            auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
            response.set_body(RESP_MSG_NO_TOKEN);
            return response.done();
        }
        auto type = getTypeFromString(root["platform"].asString());
        auto topic = root["topic"].asString();
        if (topic.empty()) {
            topic = getDefaultTopic(type);
        }
        Json::Value json;
        json["to"] = root["client_id"];
        json["pong"] = true;
        sendPushNotification(pushToken, std::move(json), type, true, topic);
    } catch (...) {
        return serverError(*request);
    }
    return restinio::request_handling_status_t::accepted;
}

RequestStatus
DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::route_params_t params)
{
    requestNum_++;
    try {
        InfoHash infoHash(params["hash"]);
        if (!infoHash)
            infoHash = InfoHash::get(params["hash"]);

        std::string err;
        Json::Value r;
        auto* char_data = reinterpret_cast<const char*>(request->body().data());
        auto reader = std::unique_ptr<Json::CharReader>(jsonReaderBuilder_.newCharReader());
        if (!reader->parse(char_data, char_data + request->body().size(), &r, &err)) {
            auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
            response.set_body(RESP_MSG_JSON_INCORRECT);
            return response.done();
        }
        const Json::Value& root(r); // parse using const Json so [] never creates element
        auto pushToken = root["key"].asString();
        if (pushToken.empty()) {
            auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
            response.set_body(RESP_MSG_NO_TOKEN);
            return response.done();
        }
        auto type = getTypeFromString(root["platform"].asString());
        auto topic = root["topic"].asString();
        if (topic.empty()) {
            topic = getDefaultTopic(type);
        }
        auto clientId = root["client_id"].asString();
        auto sessionId = root["session_id"].asString();

        if (logger_)
            logger_->debug("[proxy:server] [subscribe {}] [client {}] [session {}]", infoHash, clientId, sessionId);

        // Insert new or return existing push listeners of a token
        std::lock_guard lock(lockListener_);
        auto& pushListener = pushListeners_[pushToken];
        auto& pushListeners = pushListener.listeners[infoHash];

        auto listIt = std::find_if(pushListeners.begin(), pushListeners.end(), [&](const Listener& l) {
            return l.clientId == clientId;
        });
        bool newListener = listIt == pushListeners.end();
        if (newListener) {
            pushListeners.emplace_back(Listener {});
            listIt = std::prev(pushListeners.end());
            listIt->clientId = clientId;
            listIt->sessionCtx = std::make_shared<PushSessionContext>(sessionId);
        } else {
            std::lock_guard l(listIt->sessionCtx->lock);
            listIt->sessionCtx->sessionId = sessionId;
        }
        auto& listener = *listIt;

        // Expiration
        auto timeout = std::chrono::steady_clock::now() + proxy::OP_TIMEOUT;
        listener.expiration = timeout;
        listener.type = type;
        listener.topic = topic;
        if (listener.expireNotifyTimer)
            listener.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN);
        else
            listener.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(), timeout - proxy::OP_MARGIN);
        auto jsonProvider = [h = infoHash.toString(), clientId, sessionCtx = listener.sessionCtx]() {
            Json::Value json;
            json["timeout"] = h;
            json["to"] = clientId;
            std::lock_guard l(sessionCtx->lock);
            json["s"] = sessionCtx->sessionId;
            return json;
        };
        listener.expireNotifyTimer->async_wait(std::bind(&DhtProxyServer::handleNotifyPushListenExpire,
                                                         this,
                                                         std::placeholders::_1,
                                                         pushToken,
                                                         std::move(jsonProvider),
                                                         listener.type,
                                                         listener.topic));
        if (!listener.expireTimer)
            listener.expireTimer = std::make_unique<asio::steady_timer>(io_context(), timeout);
        else
            listener.expireTimer->expires_at(timeout);
        listener.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPushListen,
                                                   this,
                                                   std::placeholders::_1,
                                                   pushToken,
                                                   infoHash,
                                                   clientId));

        // Send response
        if (not newListener) {
            if (logger_)
                logger_->debug("[proxy:server] [subscribe {}] found [client {}]", infoHash, listener.clientId);
            // Send response header
            auto response = std::make_shared<ResponseByPartsBuilder>(
                initHttpResponse(request->create_response<ResponseByParts>()));
            response->flush();
            if (!root["refresh"].asBool()) {
                // No Refresh
                dht_->get(
                    infoHash,
                    [this, response](const Sp<Value>& value) {
                        auto output = Json::writeString(jsonBuilder_, value->toJson()) + "\n";
                        response->append_chunk(output);
                        response->flush();
                        return true;
                    },
                    [response](bool) { response->done(); });
            } else {
                // Refresh
                response->append_chunk("{}\n");
                return response->done();
            }
        } else {
            // =========== No existing listener for an infoHash ============
            // Add listen on dht
            if (logger_)
                logger_->debug("[proxy:server] [subscribe {}] new", infoHash);
            listener.internalToken = dht_->listen(infoHash,
                                                  [this,
                                                   infoHash,
                                                   pushToken,
                                                   type,
                                                   clientId,
                                                   sessionCtx = listener.sessionCtx,
                                                   topic](const std::vector<Sp<Value>>& values, bool expired) {
                                                      return this->handlePushListen(infoHash,
                                                                                    pushToken,
                                                                                    type,
                                                                                    clientId,
                                                                                    sessionCtx,
                                                                                    topic,
                                                                                    values,
                                                                                    expired);
                                                  });
            // Send response header
            auto response = initHttpResponse(request->create_response());
            response.set_body("{}\n");
            return response.done();
        }
    } catch (...) {
        return serverError(*request);
    }
    return restinio::request_handling_status_t::accepted;
}

RequestStatus
DhtProxyServer::unsubscribe(restinio::request_handle_t request, restinio::router::route_params_t params)
{
    requestNum_++;

    InfoHash infoHash(params["hash"]);
    if (!infoHash)
        infoHash = InfoHash::get(params["hash"]);

    if (logger_)
        logger_->debug("[proxy:server] [unsubscribe {}]", infoHash);

    try {
        std::string err;
        Json::Value root;
        auto* char_data = reinterpret_cast<const char*>(request->body().data());
        auto reader = std::unique_ptr<Json::CharReader>(jsonReaderBuilder_.newCharReader());

        if (!reader->parse(char_data, char_data + request->body().size(), &root, &err)) {
            auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
            response.set_body(RESP_MSG_JSON_INCORRECT);
            return response.done();
        }
        auto pushToken = root["key"].asString();
        if (pushToken.empty())
            return restinio::request_handling_status_t::rejected;
        auto clientId = root["client_id"].asString();

        handleCancelPushListen(asio::error_code() /*success*/, pushToken, infoHash, clientId);
        auto response = initHttpResponse(request->create_response());
        return response.done();
    } catch (...) {
        return serverError(*request);
    }
}

void
DhtProxyServer::handleNotifyPushListenExpire(const asio::error_code& ec,
                                             const std::string pushToken,
                                             std::function<Json::Value()> jsonProvider,
                                             PushType type,
                                             const std::string& topic)
{
    if (ec == asio::error::operation_aborted)
        return;
    else if (ec) {
        if (logger_)
            logger_->error("[proxy:server] [subscribe] error sending put refresh: {}", ec.message());
    }
    if (logger_)
        logger_->debug("[proxy:server] [subscribe] sending refresh to {} token", pushToken);
    sendPushNotification(pushToken, jsonProvider(), type, false, topic);
}

void
DhtProxyServer::handleCancelPushListen(const asio::error_code& ec,
                                       const std::string pushToken,
                                       const InfoHash key,
                                       const std::string clientId)
{
    if (ec == asio::error::operation_aborted)
        return;
    else if (ec) {
        if (logger_)
            logger_->error("[proxy:server] [listen:push {}] error cancel: {}", key, ec.message());
    }
    if (logger_)
        logger_->debug("[proxy:server] [listen:push {}] cancelled for {}", key, clientId);
    std::lock_guard lock(lockListener_);

    auto pushListener = pushListeners_.find(pushToken);
    if (pushListener == pushListeners_.end())
        return;
    auto listeners = pushListener->second.listeners.find(key);
    if (listeners == pushListener->second.listeners.end())
        return;

    for (auto listener = listeners->second.begin(); listener != listeners->second.end();) {
        if (listener->clientId == clientId) {
            if (dht_)
                dht_->cancelListen(key, std::move(listener->internalToken));
            listener = listeners->second.erase(listener);
        } else {
            ++listener;
        }
    }
    if (listeners->second.empty())
        pushListener->second.listeners.erase(listeners);
    if (pushListener->second.listeners.empty())
        pushListeners_.erase(pushListener);
}

bool
DhtProxyServer::handlePushListen(const InfoHash& infoHash,
                                 const std::string& pushToken,
                                 PushType type,
                                 const std::string& clientId,
                                 const std::shared_ptr<DhtProxyServer::PushSessionContext>& sessionCtx,
                                 const std::string& topic,
                                 const std::vector<std::shared_ptr<Value>>& values,
                                 bool expired)
{
    Json::Value json;
    json["key"] = infoHash.toString();
    json["to"] = clientId;
    using namespace std::chrono;
    json["t"] = Json::Value::Int64(duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count());

    {
        std::lock_guard l(sessionCtx->lock);
        json["s"] = sessionCtx->sessionId;
    }

    // Build a comma-separated list of ids from the values. This is intended to be used when
    // streaming from the iOS notification extension in order to filter out unwanted values.
    std::string ids;
    std::string pushTypes;
    for (const auto& value : values) {
        if (!ids.empty()) {
            ids += ",";
            pushTypes += ",";
        }
        pushTypes += value->pushType;
        ids += std::to_string(value->id);
    }
    json["ids"] = ids;
    json["pt"] = pushTypes;

    // If message is expired copy the value ID to the `exp` field.
    // This is presumably used by the Android notification system.
    if (expired && values.size() < 2) {
        json["exp"] = json["ids"];
    }

    auto minPriority = 1000u;
    for (const auto& v : values)
        minPriority = std::min(minPriority, v->priority);

    if (logger_)
        logger_->debug("[proxy:server] [listen {}] [client {}] [session {}] [expired {}] [priority {}] [values {}]",
                       infoHash,
                       clientId,
                       sessionCtx->sessionId,
                       expired,
                       minPriority,
                       values.size());

    sendPushNotification(pushToken, std::move(json), type, !expired and minPriority == 0, topic);

    return true;
}

void
DhtProxyServer::sendPushNotification(
    const std::string& token, Json::Value&& json, PushType type, bool highPriority, const std::string& topic)
{
    if (pushServer_.empty() && type != PushType::UnifiedPush)
        return;

    unsigned reqid = 0;
    try {
        std::shared_ptr<http::Request> request;
        if (type == PushType::UnifiedPush) {
            request = std::make_shared<http::Request>(io_context(), token, logger_);
        } else {
            request = std::make_shared<http::Request>(io_context(),
                                                      pushHostPort_.first,
                                                      pushHostPort_.second,
                                                      pushHostPort_.first.find("https") == 0,
                                                      logger_);
        }
        reqid = request->id();
        request->set_method(restinio::http_method_post());
        request->set_header_field(restinio::http_field_t::user_agent, "RESTinio client");
        request->set_header_field(restinio::http_field_t::accept, "*/*");

        if (type == PushType::UnifiedPush) {
            std::string_view topicView = topic;
            Blob pubKey; ///< P-256 Public key
            Blob auth;   ///< Auth secret
            auto delim = topicView.find('|');
            if (delim != std::string_view::npos) {
                pubKey = base64_default_or_url_decode(topicView.substr(0, delim));
                auth = base64_default_or_url_decode(topicView.substr(delim + 1));
            }

            request->set_header_field(restinio::http_field_t::ttl, "86400");
            request->set_header_field(restinio::http_field_t::urgency, highPriority ? "high" : "normal");
            request->set_header_field(restinio::http_field_t::content_type, "application/json");

            std::string payloadStr = Json::writeString(jsonBuilder_, std::move(json));
            if (!pubKey.empty() && !auth.empty()) {
                request->set_header_field(restinio::http_field_t::content_encoding, "aes128gcm");
                try {
                    Blob encrypted = crypto::webPushEncrypt(pubKey,
                                                            auth,
                                                            reinterpret_cast<const uint8_t*>(payloadStr.data()),
                                                            payloadStr.size());
                    request->set_body(std::string(encrypted.begin(), encrypted.end()));
                } catch (const std::exception& e) {
                    if (logger_)
                        logger_->error("[proxy:server] [notification] encryption failed: {}", e.what());
                    return;
                }
            } else {
                request->set_body(payloadStr);
            }
        } else {
            request->set_target("/api/push");
            request->set_header_field(restinio::http_field_t::host, pushServer_);
            request->set_header_field(restinio::http_field_t::content_type, "application/json");

            // NOTE: see https://github.com/appleboy/gorush
            auto isResubscribe = json.isMember("timeout");
            Json::Value notification(Json::objectValue);
            Json::Value tokens(Json::arrayValue);
            tokens[0] = token;
            notification["tokens"] = std::move(tokens);
            notification["platform"] = type == PushType::Android ? 2 : 1;
            notification["data"] = std::move(json);
            auto priority = highPriority ? "high" : "normal";
            if (type == PushType::Android) {
                Json::Value androidConfig(Json::objectValue);
                androidConfig["priority"] = priority;
                androidConfig["ttl"] = "86400s"; // time to live = 24 hours
                notification["android"] = std::move(androidConfig);
            } else {
                notification["priority"] = priority;
                const auto expiration = std::chrono::system_clock::now() + std::chrono::hours(24);
                uint32_t exp = std::chrono::duration_cast<std::chrono::seconds>(expiration.time_since_epoch()).count();
                notification["expiration"] = exp;
                if (!topic.empty())
                    notification["topic"] = topic;
                if (highPriority || isResubscribe) {
                    Json::Value alert(Json::objectValue);
                    alert["title"] = "hello";
                    notification["push_type"] = "alert";
                    notification["alert"] = alert;
                    notification["mutable_content"] = true;
                    notification["priority"] = "high";
                } else {
                    notification["push_type"] = "background";
                    notification["content_available"] = true;
                }
            }

            Json::Value notifications(Json::arrayValue);
            notifications[0] = std::move(notification);

            Json::Value content;
            content["notifications"] = std::move(notifications);
            request->set_body(Json::writeString(jsonBuilder_, content));
        }

        request->add_on_state_change_callback([this, reqid](http::Request::State state, const http::Response& response) {
            if (state == http::Request::State::DONE) {
                if (logger_ and response.status_code != 200)
                    logger_->error("[proxy:server] [notification] push failed: {}", response.status_code);
                std::lock_guard l(requestLock_);
                requests_.erase(reqid);
            }
        });
        {
            std::lock_guard l(requestLock_);
            requests_[reqid] = request;
        }
        request->send();
        // For monitoring purposes
        std::lock_guard lk(pushStatsMutex_);
        switch (type) {
        case PushType::Android:
            androidPush_.increment(highPriority);
            break;
        case PushType::iOS:
            iosPush_.increment(highPriority);
            break;
        case PushType::UnifiedPush:
            unifiedPush_.increment(highPriority);
            break;
        default:
            break;
        }
    } catch (const std::exception& e) {
        if (logger_)
            logger_->error("[proxy:server] [notification] error send push: {}", e.what());
        if (reqid) {
            std::lock_guard l(requestLock_);
            requests_.erase(reqid);
        }
    }
}

#endif // OPENDHT_PUSH_NOTIFICATIONS

void
DhtProxyServer::handleCancelPermamentPut(const asio::error_code& ec, const InfoHash& key, Value::Id vid)
{
    if (ec == asio::error::operation_aborted)
        return;
    else if (ec) {
        if (logger_)
            logger_->error("[proxy:server] [put:permament] error sending put refresh: {}", ec.message());
    }
    if (logger_)
        logger_->debug("[proxy:server] [put {}] cancel permament put {}", key, vid);
    std::lock_guard lock(lockSearchPuts_);
    auto sPuts = puts_.find(key);
    if (sPuts == puts_.end())
        return;
    auto& sPutsMap = sPuts->second.puts;
    auto put = sPutsMap.find(vid);
    if (put == sPutsMap.end())
        return;
    if (dht_)
        dht_->cancelPut(key, vid);
    if (put->second.expireTimer)
        put->second.expireTimer->cancel();
    if (put->second.expireNotifyTimer)
        put->second.expireNotifyTimer->cancel();
    sPutsMap.erase(put);
    if (sPutsMap.empty())
        puts_.erase(sPuts);
}

RequestStatus
DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_params_t params)
{
    requestNum_++;
    InfoHash infoHash(params["hash"]);
    if (!infoHash)
        infoHash = InfoHash::get(params["hash"]);

    if (request->body().empty()) {
        auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
        response.set_body(RESP_MSG_MISSING_PARAMS);
        return response.done();
    }

    try {
        std::string err;
        Json::Value root;
        auto* char_data = reinterpret_cast<const char*>(request->body().data());
        auto reader = std::unique_ptr<Json::CharReader>(jsonReaderBuilder_.newCharReader());

        if (reader->parse(char_data, char_data + request->body().size(), &root, &err)) {
            auto value = std::make_shared<Value>(root);
            bool permanent = root.isMember("permanent");
            if (logger_)
                logger_->debug("[proxy:server] [put {}] {} {}",
                               infoHash,
                               value->toString(),
                               (permanent ? "permanent" : ""));
            if (permanent) {
                std::string pushToken, clientId, sessionId, platform, topic;
                auto& pVal = root["permanent"];
                if (pVal.isObject()) {
                    pushToken = pVal["key"].asString();
                    clientId = pVal["client_id"].asString();
                    platform = pVal["platform"].asString();
                    sessionId = pVal["session_id"].asString();
                    topic = pVal["topic"].asString();
                }
                std::lock_guard lock(lockSearchPuts_);
                auto timeout = std::chrono::steady_clock::now() + proxy::OP_TIMEOUT;
                auto& sPuts = puts_[infoHash];
                if (value->id == Value::INVALID_ID) {
                    for (auto& pp : sPuts.puts) {
                        if (pp.second.pushToken == pushToken and pp.second.clientId == clientId
                            and pp.second.value->contentEquals(*value)) {
                            pp.second.expireTimer->expires_at(timeout);
                            pp.second.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPermamentPut,
                                                                        this,
                                                                        std::placeholders::_1,
                                                                        infoHash,
                                                                        pp.second.value->id));
                            if (not sessionId.empty()) {
                                if (not pp.second.sessionCtx)
                                    pp.second.sessionCtx = std::make_shared<PushSessionContext>(sessionId);
                                else {
                                    std::lock_guard l(pp.second.sessionCtx->lock);
                                    pp.second.sessionCtx->sessionId = sessionId;
                                }
                            }
                            auto response = initHttpResponse(request->create_response());
                            response.append_body(Json::writeString(jsonBuilder_, pp.second.value->toJson()) + "\n");
                            return response.done();
                        }
                    }
                    value->id = std::uniform_int_distribution<Value::Id> {1}(rd);
                }

                auto vid = value->id;
                auto& pput = sPuts.puts[vid];
                pput.value = value;
                pput.expiration = timeout;
                if (not pput.expireTimer) {
                    // cancel permanent put
                    pput.expireTimer = std::make_unique<asio::steady_timer>(io_context(), timeout);
#ifdef OPENDHT_PUSH_NOTIFICATIONS
                    if (not pushToken.empty()) {
                        pput.pushToken = pushToken;
                        pput.clientId = clientId;
                        pput.type = getTypeFromString(platform);
                        if (topic.empty())
                            topic = getDefaultTopic(pput.type);
                        pput.topic = topic;
                        pput.sessionCtx = std::make_shared<PushSessionContext>(sessionId);
                    }
#endif
                } else {
                    if (not sessionId.empty()) {
                        if (not pput.sessionCtx)
                            pput.sessionCtx = std::make_shared<PushSessionContext>(sessionId);
                        else {
                            std::lock_guard l(pput.sessionCtx->lock);
                            pput.sessionCtx->sessionId = sessionId;
                        }
                    }
                    pput.expireTimer->expires_at(timeout);
                }
                pput.expireTimer->async_wait(
                    std::bind(&DhtProxyServer::handleCancelPermamentPut, this, std::placeholders::_1, infoHash, vid));

#ifdef OPENDHT_PUSH_NOTIFICATIONS
                // notify put permanent expiration
                if (pput.sessionCtx) {
                    auto jsonProvider = [infoHash, clientId, vid, sessionCtx = pput.sessionCtx]() {
                        Json::Value json;
                        json["timeout"] = infoHash.toString();
                        json["to"] = clientId;
                        json["vid"] = std::to_string(vid);
                        std::lock_guard l(sessionCtx->lock);
                        json["s"] = sessionCtx->sessionId;
                        return json;
                    };
                    if (!pput.expireNotifyTimer)
                        pput.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(),
                                                                                      timeout - proxy::OP_MARGIN);
                    else
                        pput.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN);
                    pput.expireNotifyTimer->async_wait(std::bind(&DhtProxyServer::handleNotifyPushListenExpire,
                                                                 this,
                                                                 std::placeholders::_1,
                                                                 pushToken,
                                                                 std::move(jsonProvider),
                                                                 pput.type,
                                                                 pput.topic));
                }
#endif
            }

            dht_->put(
                infoHash,
                value,
                [this, request, value](bool ok) {
                    if (ok) {
                        auto response = initHttpResponse(request->create_response());
                        response.append_body(Json::writeString(jsonBuilder_, value->toJson()) + "\n");
                        response.done();
                    } else {
                        auto response = initHttpResponse(request->create_response(restinio::status_bad_gateway()));
                        response.set_body(RESP_MSG_PUT_FAILED);
                        response.done();
                    }
                },
                time_point::max(),
                permanent);
            return restinio::request_handling_status_t::accepted;
        } else {
            auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
            response.set_body(RESP_MSG_JSON_INCORRECT);
            return response.done();
        }
    } catch (const std::exception& e) {
        if (logger_)
            logger_->error("[proxy:server] error in put: {}", e.what());
        return serverError(*request);
    }
}

#ifdef OPENDHT_PROXY_SERVER_IDENTITY

RequestStatus
DhtProxyServer::putSigned(restinio::request_handle_t request, restinio::router::route_params_t params) const
{
    requestNum_++;
    InfoHash infoHash(params["hash"]);
    if (!infoHash)
        infoHash = InfoHash::get(params["hash"]);

    if (request->body().empty()) {
        auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
        response.set_body(RESP_MSG_MISSING_PARAMS);
        return response.done();
    }

    try {
        std::string err;
        Json::Value root;
        auto* char_data = reinterpret_cast<const char*>(request->body().data());
        auto reader = std::unique_ptr<Json::CharReader>(jsonReaderBuilder_.newCharReader());

        if (reader->parse(char_data, char_data + request->body().size(), &root, &err)) {
            auto value = std::make_shared<Value>(root);

            dht_->putSigned(infoHash, value, [this, request, value](bool ok) {
                if (ok) {
                    auto output = Json::writeString(jsonBuilder_, value->toJson()) + "\n";
                    auto response = initHttpResponse(request->create_response());
                    response.append_body(output);
                    response.done();
                } else {
                    auto response = initHttpResponse(request->create_response(restinio::status_bad_gateway()));
                    response.set_body(RESP_MSG_PUT_FAILED);
                    response.done();
                }
            });
            return restinio::request_handling_status_t::accepted;
        } else {
            auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
            response.set_body(RESP_MSG_JSON_INCORRECT);
            return response.done();
        }
    } catch (const std::exception& e) {
        if (logger_)
            logger_->error("[proxy:server] error in putSigned: {}", e.what());
        return serverError(*request);
    }
}

RequestStatus
DhtProxyServer::putEncrypted(restinio::request_handle_t request, restinio::router::route_params_t params)
{
    requestNum_++;
    InfoHash infoHash(params["hash"]);
    if (!infoHash)
        infoHash = InfoHash::get(params["hash"]);

    if (request->body().empty()) {
        auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
        response.set_body(RESP_MSG_MISSING_PARAMS);
        return response.done();
    }

    try {
        std::string err;
        Json::Value root;
        auto* char_data = reinterpret_cast<const char*>(request->body().data());
        auto reader = std::unique_ptr<Json::CharReader>(jsonReaderBuilder_.newCharReader());

        if (reader->parse(char_data, char_data + request->body().size(), &root, &err)) {
            InfoHash to(root["to"].asString());
            if (!to) {
                auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
                response.set_body(RESP_MSG_DESTINATION_NOT_FOUND);
                return response.done();
            }
            auto value = std::make_shared<Value>(root);
            dht_->putEncrypted(infoHash, to, value, [this, request, value](bool ok) {
                if (ok) {
                    auto response = initHttpResponse(request->create_response());
                    response.append_body(Json::writeString(jsonBuilder_, value->toJson()) + "\n");
                    response.done();
                } else {
                    auto response = initHttpResponse(request->create_response(restinio::status_bad_gateway()));
                    response.set_body(RESP_MSG_PUT_FAILED);
                    response.done();
                }
            });
            return restinio::request_handling_status_t::accepted;
        } else {
            auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
            response.set_body(RESP_MSG_JSON_INCORRECT);
            return response.done();
        }
    } catch (const std::exception& e) {
        if (logger_)
            logger_->error("[proxy:server] error in put: {}", e.what());
        return serverError(*request);
    }
}

#endif // OPENDHT_PROXY_SERVER_IDENTITY

RequestStatus
DhtProxyServer::options(restinio::request_handle_t request, restinio::router::route_params_t /*params*/)
{
    requestNum_++;
#ifdef OPENDHT_PROXY_SERVER_IDENTITY
    const auto methods = "OPTIONS, GET, POST, LISTEN, SIGN, ENCRYPT";
#else
    const auto methods = "OPTIONS, GET, POST, LISTEN";
#endif
    auto response = initHttpResponse(request->create_response());
    response.append_header(restinio::http_field::access_control_allow_methods, methods);
    response.append_header(restinio::http_field::access_control_allow_headers, "content-type");
    response.append_header(restinio::http_field::access_control_max_age, "86400");
    return response.done();
}

RequestStatus
DhtProxyServer::getFiltered(restinio::request_handle_t request, restinio::router::route_params_t params)
{
    requestNum_++;
    auto query = params["value"];
    InfoHash infoHash(params["hash"]);
    if (!infoHash)
        infoHash = InfoHash::get(params["hash"]);

    try {
        auto response = std::make_shared<ResponseByPartsBuilder>(
            initHttpResponse(request->create_response<ResponseByParts>()));
        response->flush();
        dht_->get(
            infoHash,
            [this, response](const Sp<Value>& value) {
                response->append_chunk(Json::writeString(jsonBuilder_, value->toJson()) + "\n");
                response->flush();
                return true;
            },
            [response](bool /*ok*/) { response->done(); },
            {},
            query);
        return restinio::request_handling_status_t::accepted;
    } catch (const std::exception& e) {
        return serverError(*request);
    }
}

} // namespace dht
