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

#include "dht.h"
#include "rng.h"
#include "search.h"
#include "storage.h"
#include "request.h"

#include <msgpack.hpp>

#include <algorithm>
#include <random>
#include <sstream>
#include <fstream>

#include <inttypes.h>

namespace dht {

using namespace std::placeholders;

constexpr std::chrono::minutes Dht::MAX_STORAGE_MAINTENANCE_EXPIRE_TIME;
constexpr std::chrono::minutes Dht::SEARCH_EXPIRE_TIME;
constexpr std::chrono::seconds Dht::BOOTSTRAP_PERIOD;
constexpr duration Dht::LISTEN_EXPIRE_TIME;
constexpr duration Dht::LISTEN_EXPIRE_TIME_PUBLIC;
constexpr duration Dht::REANNOUNCE_MARGIN;
static constexpr size_t MAX_REQUESTS_PER_SEC {8 * 1024};
static constexpr duration BOOTSTRAP_PERIOD_MAX {std::chrono::hours(24)};

NodeStatus
Dht::updateStatus(sa_family_t af)
{
    auto& d = dht(af);
    auto old = d.status;
    d.status = d.getStatus(scheduler.time());
    if (d.status == NodeStatus::Disconnected && bootstrap_pending && bootstrapJob && !bootstrap_nodes.empty())
        d.status = NodeStatus::Connecting;
    if (d.status != old) {
        auto& other = dht(af == AF_INET ? AF_INET6 : AF_INET);
        if (other.status == NodeStatus::Disconnected && d.status == NodeStatus::Disconnected) {
            onDisconnected();
        } else if (other.status == NodeStatus::Connected || d.status == NodeStatus::Connected) {
            onConnected();
        }
    }
    return d.status;
}

NodeStatus
Dht::Kad::getStatus(time_point now) const
{
    unsigned dubious = 0;
    for (const auto& b : buckets) {
        for (auto& n : b.nodes) {
            if (n->isGood(now)) {
                return NodeStatus::Connected;
            } else if (not n->isExpired())
                dubious++;
        }
    }
    auto& ping = pending_pings;
    if (dubious or ping)
        return NodeStatus::Connecting;
    return NodeStatus::Disconnected;
}

void
Dht::shutdown(ShutdownCallback cb, bool stop)
{
    if (not persistPath.empty())
        saveState(persistPath);

    if (stop) {
        for (auto dht : {&dht4, &dht6}) {
            for (auto& sr : dht->searches) {
                for (const auto& r : sr.second->callbacks)
                    r.second.done_cb(false, {});
                sr.second->callbacks.clear();
                for (const auto& a : sr.second->announce) {
                    for (auto& cb : a.callbacks)
                        cb(false, {});
                }
                sr.second->announce.clear();
                sr.second->listeners.clear();
            }
        }
        network_engine.clear();
    }

    if (not maintain_storage) {
        if (cb)
            cb();
        return;
    }

    // Last store maintenance
    scheduler.syncTime();
    auto remaining = std::make_shared<int>(0);
    auto str_donecb = [=](bool, const std::vector<Sp<Node>>&) {
        --*remaining;
        if (logger_)
            logger_->warn("Shutting down node: {} ops remaining", *remaining);
        if (!*remaining && cb) {
            cb();
        }
    };

    for (auto& str : store)
        *remaining += maintainStorage(str, true, str_donecb);

    if (logger_)
        logger_->warn("Shutting down node: after storage, {} ops", *remaining);

    if (!*remaining) {
        if (cb)
            cb();
    }
}

bool
Dht::isRunning(sa_family_t af) const
{
    return network_engine.isRunning(af);
}

/* Every bucket contains an unordered list of nodes. */
const Sp<Node>
Dht::findNode(const InfoHash& id, sa_family_t af) const
{
    if (const Bucket* b = findBucket(id, af))
        for (const auto& n : b->nodes)
            if (n->id == id)
                return n;
    return {};
}

/* Every bucket caches the address of a likely node.  Ping it. */
void
Dht::sendCachedPing(Bucket& b)
{
    if (b.cached)
        if (logger_)
            logger_->debug("[node {}] Sending ping to cached node", b.cached->toString());
    b.sendCachedPing(network_engine);
}

std::vector<SockAddr>
Dht::getPublicAddress(sa_family_t family)
{
    std::vector<SockAddr> ret;
    if (family == AF_UNSPEC) {
        auto& d4 = dht(AF_INET).reported_addr;
        auto& d6 = dht(AF_INET6).reported_addr;
        ret.reserve(d4.size() + d6.size());
        for (const auto& a : d4)
            ret.emplace_back(a.second);
        for (const auto& a : d6)
            ret.emplace_back(a.second);
        return ret;
    }
    auto& reported_addr = dht(family).reported_addr;
    ret.reserve(!family ? reported_addr.size() : reported_addr.size() / 2);
    for (const auto& addr : reported_addr)
        if (!family || family == addr.second.getFamily())
            ret.emplace_back(addr.second);
    return ret;
}

std::pair<size_t, size_t>
Dht::getLocalStoreSize() const
{
    return {local_store_quota->size(), local_store_quota->valueCount()};
}

bool
Dht::trySearchInsert(const Sp<Node>& node)
{
    const auto& now = scheduler.time();
    if (not node)
        return false;

    auto& srs = searches(node->getFamily());
    auto closest = srs.lower_bound(node->id);
    bool inserted {false};

    // insert forward
    for (auto it = closest; it != srs.end(); it++) {
        auto& s = *it->second;
        if (s.insertNode(node, now)) {
            inserted = true;
            scheduler.edit(s.nextSearchStep, now);
        } else if (not s.expired and not s.done)
            break;
    }
    // insert backward
    for (auto it = closest; it != srs.begin();) {
        --it;
        auto& s = *it->second;
        if (s.insertNode(node, now)) {
            inserted = true;
            scheduler.edit(s.nextSearchStep, now);
        } else if (not s.expired and not s.done)
            break;
    }
    return inserted;
}

void
Dht::reportedAddr(const SockAddr& addr)
{
    auto& reported_addr = dht(addr.getFamily()).reported_addr;
    auto firstBefore = reported_addr.empty() ? nullptr : reported_addr.begin()->second.get();
    auto it = std::find_if(reported_addr.begin(), reported_addr.end(), [&](const ReportedAddr& a) {
        return a.second == addr;
    });
    if (it == reported_addr.end()) {
        if (reported_addr.size() < 32)
            reported_addr.emplace_back(1, addr);
    } else
        it->first++;
    std::sort(reported_addr.begin(), reported_addr.end(), [](const ReportedAddr& a, const ReportedAddr& b) {
        return a.first > b.first;
    });
    if (publicAddressChangedCb_) {
        auto firstAfter = reported_addr.begin()->second.get();
        if (firstBefore != firstAfter) {
            auto& otherDht = dht(addr.getFamily() == AF_INET ? AF_INET6 : AF_INET).reported_addr;
            std::vector<SockAddr> v;
            v.reserve(otherDht.empty() ? 1 : 2);
            v.emplace_back(reported_addr.begin()->second);
            if (not otherDht.empty())
                v.emplace_back(otherDht.begin()->second);
            publicAddressChangedCb_(std::move(v));
        }
    }
}

/* We just learnt about a node, not necessarily a new one.  Confirm is 1 if
   the node sent a message, 2 if it sent us a reply. */
void
Dht::onNewNode(const Sp<Node>& node, int confirm)
{
    const auto& now = scheduler.time();
    auto& b = buckets(node->getFamily());
    auto wasEmpty = confirm < 2 && b.grow_time < now - std::chrono::minutes(5);
    if (b.onNewNode(node, confirm, now, myid, network_engine) or confirm) {
        trySearchInsert(node);
        if (wasEmpty) {
            scheduler.edit(nextNodesConfirmation, now + std::chrono::seconds(1));
        }
    }
}

/* Called periodically to purge known-bad nodes.  Note that we're very
   conservative here: broken nodes in the table don't do much harm, we'll
   recover as soon as we find better ones. */
void
Dht::expireBuckets(RoutingTable& list)
{
    for (auto& b : list) {
        bool changed = false;
        b.nodes.remove_if([&changed](const Sp<Node>& n) {
            if (n->isExpired()) {
                changed = true;
                return true;
            }
            return false;
        });
        if (changed)
            sendCachedPing(b);
    }
}

void
Dht::expireSearches()
{
    auto t = scheduler.time() - SEARCH_EXPIRE_TIME;
    auto expired = [&](std::pair<const InfoHash, Sp<Search>>& srp) {
        auto& sr = *srp.second;
        auto b = sr.callbacks.empty() && sr.announce.empty() && sr.listeners.empty() && sr.step_time < t;
        if (b) {
            if (logger_)
                logger_->debug("[search {}] Removing search", srp.first.to_view());
            sr.clear();
            return b;
        } else {
            return false;
        }
    };
    erase_if(dht4.searches, expired);
    erase_if(dht6.searches, expired);
}

void
Dht::searchNodeGetDone(const net::Request& req, net::RequestAnswer&& answer, std::weak_ptr<Search> ws, Sp<Query> query)
{
    const auto& now = scheduler.time();
    if (auto sr = ws.lock()) {
        sr->insertNode(req.node, now, answer.ntoken);
        if (auto srn = sr->getNode(req.node)) {
            /* all other get requests which are satisfied by this answer
               should not be sent anymore */
            for (auto& g : sr->callbacks) {
                auto& q = g.second.query;
                if (q->isSatisfiedBy(*query) and q != query) {
                    auto dummy_req = std::make_shared<net::Request>();
                    dummy_req->cancel();
                    srn->getStatus[q] = std::move(dummy_req);
                }
            }
            auto syncTime = srn->getSyncTime(scheduler.time());
            if (srn->syncJob)
                scheduler.edit(srn->syncJob, syncTime);
            else
                srn->syncJob = scheduler.add(syncTime, std::bind(&Dht::searchStep, this, ws));
        }
        onGetValuesDone(req.node, answer, sr, query);
    }
}

void
Dht::searchNodeGetExpired(const net::Request& status, bool over, std::weak_ptr<Search> ws, Sp<Query> query)
{
    if (auto sr = ws.lock()) {
        if (auto srn = sr->getNode(status.node)) {
            srn->candidate = not over;
            if (over)
                srn->getStatus.erase(query);
        }
        scheduler.edit(sr->nextSearchStep, scheduler.time());
    }
}

void
Dht::paginate(std::weak_ptr<Search> ws, Sp<Query> query, SearchNode* n)
{
    auto sr = ws.lock();
    if (not sr)
        return;
    auto select_q = std::make_shared<Query>(Select {}.field(Value::Field::Id), query ? query->where : Where {});
    auto onSelectDone = [this, ws, query](const net::Request& status, net::RequestAnswer&& answer) mutable {
        // Retrieve search
        auto sr = ws.lock();
        if (not sr)
            return;
        const auto& id = sr->id;
        // Retrieve search node
        auto sn = sr->getNode(status.node);
        if (not sn)
            return;
        // backward compatibility
        if (answer.fields.empty()) {
            searchNodeGetDone(status, std::move(answer), ws, query);
            return;
        }
        for (const auto& fvi : answer.fields) {
            try {
                auto vid = fvi->index.at(Value::Field::Id).getInt();
                if (vid == Value::INVALID_ID)
                    continue;
                auto query_for_vid = std::make_shared<Query>(Select {}, Where {}.id(vid));
                sn->pagination_queries[query].push_back(query_for_vid);
                if (logger_)
                    logger_->debug("[search {}] [node {}] Sending {}",
                                   id.toString(),
                                   sn->node->toString(),
                                   query_for_vid->toString());
                sn->getStatus[query_for_vid] = network_engine.sendGetValues(
                    status.node,
                    id,
                    *query_for_vid,
                    -1,
                    std::bind(&Dht::searchNodeGetDone, this, _1, _2, ws, query),
                    std::bind(&Dht::searchNodeGetExpired, this, _1, _2, ws, query_for_vid));
            } catch (const std::out_of_range&) {
                if (logger_)
                    logger_->error("[search {}] [node {}] Received non-id field in response to "
                                   "'SELECT id' request…",
                                   id.toString(),
                                   sn->node->toString());
            }
        }
    };
    /* add pagination query key for tracking ongoing requests. */
    n->pagination_queries[query].push_back(select_q);

    if (logger_)
        logger_->debug("[search {}] [node {}] Sending {}", sr->id.toString(), n->node->toString(), select_q->toString());
    n->getStatus[select_q] = network_engine.sendGetValues(
        n->node, sr->id, *select_q, -1, onSelectDone, std::bind(&Dht::searchNodeGetExpired, this, _1, _2, ws, select_q));
}

Dht::SearchNode*
Dht::searchSendGetValues(Sp<Search> sr, SearchNode* pn, bool update)
{
    if (sr->done or sr->currentlySolicitedNodeCount() >= MAX_REQUESTED_SEARCH_NODES)
        return nullptr;

    const auto& now = scheduler.time();

    std::weak_ptr<Search> ws = sr;
    auto cb = sr->callbacks.begin();
    static const auto ANY_QUERY = std::make_shared<Query>(Select {}, Where {}, true);
    do { /* for all requests to send */
        SearchNode* n = nullptr;
        auto& query = sr->callbacks.empty() ? ANY_QUERY : cb->second.query;
        const time_point up = (not sr->callbacks.empty() and update) ? sr->getLastGetTime(*query) : time_point::min();

        if (pn and pn->canGet(now, up, query)) {
            n = pn;
        } else {
            for (auto& sn : sr->nodes) {
                if (sn->canGet(now, up, query)) {
                    n = sn.get();
                    break;
                }
            }
        }

        if (sr->callbacks.empty()) { /* 'find_node' request */
            if (not n)
                return nullptr;

            /* if (logger_)
                   logger_->d(sr->id, n->node->id, "[search %s] [node %s] Sending 'find_node'",
                        sr->id.toString().c_str(), n->node->toString().c_str());*/
            n->getStatus[query] = network_engine
                                      .sendFindNode(n->node,
                                                    sr->id,
                                                    -1,
                                                    std::bind(&Dht::searchNodeGetDone, this, _1, _2, ws, query),
                                                    std::bind(&Dht::searchNodeGetExpired, this, _1, _2, ws, query));

        } else { /* 'get' request */
            if (not n)
                continue;

            if (query and not query->select.empty()) {
                /* The request contains a select. No need to paginate… */
                /* if (logger_)
                       logger_->d(sr->id, n->node->id, "[search %s] [node %s] Sending 'get'",
                            sr->id.toString().c_str(), n->node->toString().c_str());*/
                n->getStatus[query]
                    = network_engine.sendGetValues(n->node,
                                                   sr->id,
                                                   *query,
                                                   -1,
                                                   std::bind(&Dht::searchNodeGetDone, this, _1, _2, ws, query),
                                                   std::bind(&Dht::searchNodeGetExpired, this, _1, _2, ws, query));
            } else
                paginate(ws, query, n);
        }

        /* We only attempt to send one request. Return. */
        return n;

    } while (++cb != sr->callbacks.end());

    /* no request were sent */
    return nullptr;
}

void
Dht::searchSendAnnounceValue(const Sp<Search>& sr, unsigned syncLevel)
{
    if (sr->announce.empty())
        return;
    std::weak_ptr<Search> ws = sr;

    auto onDone = [this, ws](const net::Request& req,
                             net::RequestAnswer&& answer) { /* when put done */
                                                            if (auto sr = ws.lock()) {
                                                                onAnnounceDone(req.node, answer, sr);
                                                                scheduler.edit(sr->nextSearchStep, scheduler.time());
                                                            }
    };

    auto onExpired = [this, ws](const net::Request&, bool over) { /* when put expired */
                                                                  if (over)
                                                                      if (auto sr = ws.lock())
                                                                          scheduler.edit(sr->nextSearchStep,
                                                                                         scheduler.time());
    };

    auto onSelectDone = [this, ws, onDone, onExpired](const net::Request& req, net::RequestAnswer&& answer) mutable {
        /* on probing done */
        auto sr = ws.lock();
        if (not sr)
            return;
        const auto& now = scheduler.time();
        sr->insertNode(req.node, now, answer.ntoken);
        auto sn = sr->getNode(req.node);
        if (not sn)
            return;

        if (not sn->isSynced(now)) {
            /* Search is now unsynced. Let's call searchStep to sync again. */
            scheduler.edit(sr->nextSearchStep, now);
            return;
        }
        for (auto& a : sr->announce) {
            if (sn->getAnnounceTime(a.value->id) > now)
                continue;
            bool hasValue {false};
            uint16_t seq_no = 0;
            try {
                const auto& f = std::find_if(answer.fields.cbegin(),
                                             answer.fields.cend(),
                                             [&a](const Sp<FieldValueIndex>& i) {
                                                 return i->index.at(Value::Field::Id).getInt() == a.value->id;
                                             });
                if (f != answer.fields.cend() and *f) {
                    hasValue = true;
                    seq_no = static_cast<uint16_t>((*f)->index.at(Value::Field::SeqNum).getInt());
                }
            } catch (std::out_of_range&) {
            }

            auto next_refresh_time = now + getType(a.value->type).expiration;
            auto& acked = sn->acked[a.value->id];
            scheduler.cancel(acked.refresh);
            /* only put the value if the node doesn't already have it */
            if (not hasValue or seq_no < a.value->seq) {
                if (logger_)
                    logger_->debug("[search {}] [node {}] Sending 'put' (vid: {:016x})",
                                   sr->id.toString(),
                                   sn->node->toString(),
                                   a.value->id);
                auto created = a.permanent ? time_point::max() : a.created;
                acked = {network_engine
                             .sendAnnounceValue(sn->node, sr->id, a.value, created, sn->token, onDone, onExpired),
                         next_refresh_time};
            } else if (hasValue and a.permanent) {
                if (logger_)
                    logger_->warn("[search {}] [node {}] Sending 'refresh' (vid: {:016x})",
                                  sr->id.toString(),
                                  sn->node->toString(),
                                  a.value->id);
                acked = {network_engine.sendRefreshValue(
                             sn->node,
                             sr->id,
                             a.value->id,
                             sn->token,
                             onDone,
                             [this,
                              ws,
                              node = sn->node,
                              v = a.value,
                              onDone,
                              onExpired,
                              created = a.permanent ? time_point::max() : a.created,
                              next_refresh_time](const net::Request& /*req*/, net::DhtProtocolException&& e) {
                                 if (e.getCode() == net::DhtProtocolException::NOT_FOUND) {
                                     if (logger_)
                                         logger_->error("[node {}] Returned error 404: storage not found",
                                                        node->toString());
                                     if (auto sr = ws.lock()) {
                                         if (auto sn = sr->getNode(node)) {
                                             sn->acked[v->id] = {network_engine.sendAnnounceValue(sn->node,
                                                                                                  sr->id,
                                                                                                  v,
                                                                                                  created,
                                                                                                  sn->token,
                                                                                                  onDone,
                                                                                                  onExpired),
                                                                 next_refresh_time};
                                             scheduler.edit(sr->nextSearchStep, scheduler.time());
                                             return true;
                                         }
                                     }
                                 }
                                 return false;
                             },
                             onExpired),
                         next_refresh_time};
            } else {
                if (logger_)
                    logger_->warn("[search {}] [node {}] Already has value (vid: {:016x}). Aborting.",
                                  sr->id.toString(),
                                  sn->node->toString(),
                                  a.value->id);
                auto ack_req = std::make_shared<net::Request>(net::Request::State::COMPLETED);
                ack_req->reply_time = now;
                acked = {std::move(ack_req), next_refresh_time};

                /* step to clear announces */
                scheduler.edit(sr->nextSearchStep, now);
            }
            if (a.permanent) {
                acked.refresh = scheduler.add(next_refresh_time - REANNOUNCE_MARGIN,
                                              std::bind(&Dht::searchStep, this, ws));
            }
        }
    };

    static const auto PROBE_QUERY = std::make_shared<Query>(
        Select {}.field(Value::Field::Id).field(Value::Field::SeqNum));

    const auto& now = scheduler.time();
    unsigned i = 0;
    for (auto& np : sr->nodes) {
        auto& n = *np;
        if (not n.isSynced(now))
            continue;

        auto gs = n.probe_query ? n.getStatus.find(n.probe_query) : n.getStatus.end();
        if (gs != n.getStatus.end() and gs->second and gs->second->pending()) {
            continue;
        }

        bool sendQuery = false;
        for (auto& a : sr->announce) {
            if (n.getAnnounceTime(a.value->id) <= now) {
                if (a.permanent) {
                    sendQuery = true;
                } else {
                    if (logger_)
                        logger_->warn("[search {}] [node {}] Sending 'put' (vid: {:016x})",
                                      sr->id.toString(),
                                      n.node->toString(),
                                      a.value->id);
                    n.acked[a.value->id] = {network_engine.sendAnnounceValue(
                                                n.node, sr->id, a.value, a.created, n.token, onDone, onExpired),
                                            now + getType(a.value->type).expiration};
                }
            }
        }

        if (sendQuery) {
            n.probe_query = PROBE_QUERY;
            if (logger_)
                logger_->debug("[search {}] [node {}] Sending {}",
                               sr->id.toString(),
                               n.node->toString(),
                               n.probe_query->toString());
            auto req = network_engine.sendGetValues(n.node,
                                                    sr->id,
                                                    *PROBE_QUERY,
                                                    -1,
                                                    onSelectDone,
                                                    std::bind(&Dht::searchNodeGetExpired, this, _1, _2, ws, PROBE_QUERY));
            n.getStatus[PROBE_QUERY] = std::move(req);
        }
        if (not n.candidate and ++i == syncLevel)
            break;
    }
}

void
Dht::searchSynchedNodeListen(const Sp<Search>& sr, SearchNode& n)
{
    const auto& listenExp = getListenExpiration();
    std::weak_ptr<Search> ws = sr;
    for (const auto& l : sr->listeners) {
        const auto& query = l.second.query;

        auto openListenSocket = [this, ws, query, node = n.node] {
            return node->openSocket([this, ws, query](const Sp<Node>& node, net::RequestAnswer&& answer) mutable {
                /* on new values */
                if (auto sr = ws.lock()) {
                    scheduler.edit(sr->nextSearchStep, scheduler.time());
                    if (auto sn = sr->getNode(node)) {
                        if (not answer.ntoken.empty())
                            sn->refreshToken(answer.ntoken);
                        sn->onValues(query, std::move(answer), types, scheduler);
                    }
                }
            });
        };

        auto r = n.listenStatus.find(query);
        if (n.getListenTime(r, listenExp) > scheduler.time())
            continue;
        // if (logger_)
        //     logger_->d(sr->id, n.node->id, "[search %s] [node %s] Sending 'listen'",
        //        sr->id.toString().c_str(), n.node->toString().c_str());

        if (r == n.listenStatus.end()) {
            r = n.listenStatus
                    .emplace(std::piecewise_construct,
                             std::forward_as_tuple(query),
                             std::forward_as_tuple(l.second.get_cb, l.second.sync_cb, openListenSocket()))
                    .first;
            r->second.cacheExpirationJob = scheduler.add(time_point::max(), [this, ws, query, node = n.node] {
                if (auto sr = ws.lock()) {
                    if (auto sn = sr->getNode(node)) {
                        sn->expireValues(query, scheduler);
                    }
                }
            });
        } else if (not n.node->getSocket(r->second.socketId)) {
            r->second.socketId = openListenSocket();
        }
        auto new_req = network_engine.sendListen(
            n.node,
            sr->id,
            *query,
            n.token,
            r->second.socketId,
            [this, ws, query](const net::Request& req, net::RequestAnswer&& answer) mutable {
                /* on done */
                if (auto sr = ws.lock()) {
                    scheduler.edit(sr->nextSearchStep, scheduler.time());
                    if (auto sn = sr->getNode(req.node)) {
                        auto job = scheduler.add(sn->getListenTime(query, getListenExpiration()),
                                                 std::bind(&Dht::searchStep, this, ws));
                        sn->onListenSynced(query, true, std::move(job));
                    }
                    onListenDone(req.node, answer, sr);
                }
            },
            [this, ws, query](const net::Request& req, bool over) mutable {
                /* on request expired */
                if (auto sr = ws.lock()) {
                    scheduler.edit(sr->nextSearchStep, scheduler.time());
                    if (over)
                        if (auto sn = sr->getNode(req.node))
                            sn->listenStatus.erase(query);
                }
            });
        // Here the request may have failed and the CachedListenStatus removed
        r = n.listenStatus.find(query);
        if (r != n.listenStatus.end()) {
            r->second.req = new_req;
        }
    }
}

/* When a search is in progress, we periodically call search_step to send
   further requests. */
void
Dht::searchStep(std::weak_ptr<Search> ws)
{
    auto sr = ws.lock();
    if (not sr or sr->expired or sr->done)
        return;

    const auto& now = scheduler.time();
    auto level = sr->syncLevel(now);
    constexpr auto MARGIN = 1;
    bool preSynced = level > MARGIN;
    auto syncLevel = preSynced ? level - MARGIN : 0;
    /*if (auto req_count = sr->currentlySolicitedNodeCount())
        if (logger_)
            logger_->d(sr->id, "[search %s IPv%c] Step (%d requests)",
                sr->id.toString().c_str(), sr->af == AF_INET ? '4' : '6', req_count);*/
    sr->step_time = now;

    if (sr->refill_time + Node::NODE_EXPIRE_TIME < now and sr->nodes.size() - sr->getNumberOfBadNodes() < SEARCH_NODES)
        refill(*sr);

    /* Check if the first TARGET_NODES (8) live nodes have replied. */
    if (sr->isSynced(now)) {
        if (not(sr->callbacks.empty() and sr->announce.empty())) {
            // search is synced but some (newer) get operations are not complete
            // Call callbacks when done
            std::vector<Get> completed_gets;
            for (auto b = sr->callbacks.begin(); b != sr->callbacks.end();) {
                if (sr->isDone(b->second)) {
                    sr->setDone(b->second);
                    completed_gets.emplace_back(std::move(b->second));
                    b = sr->callbacks.erase(b);
                } else
                    ++b;
            }
            // clear corresponding queries
            for (const auto& get : completed_gets)
                for (auto& sn : sr->nodes) {
                    sn->getStatus.erase(get.query);
                    sn->pagination_queries.erase(get.query);
                }

            /* clearing callbacks for announced values */
            sr->checkAnnounced();

            if (sr->callbacks.empty() && sr->announce.empty() && sr->listeners.empty())
                sr->setDone();
        }

        // true if this node is part of the target nodes cluter.
        /*bool in = sr->id.xorCmp(myid, sr->nodes.back().node->id) < 0;

        logger__DBG("[search %s IPv%c] synced%s",
                sr->id.toString().c_str(), sr->af == AF_INET ? '4' : '6', in ? ", in" : "");*/

        if (not sr->listeners.empty()) {
            unsigned i = 0;
            for (auto& n : sr->nodes) {
                if (not n->isSynced(now))
                    continue;
                searchSynchedNodeListen(sr, *n);
                if (not n->candidate and ++i == LISTEN_NODES)
                    break;
            }
        }

        // Announce requests
        searchSendAnnounceValue(sr);

        if (sr->callbacks.empty() && sr->announce.empty() && sr->listeners.empty())
            sr->setDone();
    } else if (preSynced) {
        if (not sr->listeners.empty()) {
            auto nl = std::min(syncLevel, LISTEN_NODES);
            unsigned i = 0;
            for (auto& n : sr->nodes) {
                if (not n->isSynced(now))
                    break;
                searchSynchedNodeListen(sr, *n);
                if (++i == nl)
                    break;
            }
        }

        // Announce requests
        searchSendAnnounceValue(sr, syncLevel);
    }

    while (sr->currentlySolicitedNodeCount() < MAX_REQUESTED_SEARCH_NODES and searchSendGetValues(sr))
        ;

    if (sr->getNumberOfConsecutiveBadNodes() >= std::min<size_t>(sr->nodes.size(), SEARCH_MAX_BAD_NODES)) {
        if (logger_)
            logger_->warn("[search {} IPv{}] Expired", sr->id.to_view(), sr->af == AF_INET ? '4' : '6');
        sr->expire();
        if (not public_stable)
            connectivityChanged(sr->af);
    }

    /* dumpSearch(*sr, std::cout); */
}

unsigned
Dht::refill(Dht::Search& sr)
{
    const auto& now = scheduler.time();
    sr.refill_time = now;
    /* we search for up to SEARCH_NODES good nodes. */
    auto cached_nodes = network_engine.getCachedNodes(sr.id, sr.af, SEARCH_NODES);

    if (cached_nodes.empty()) {
        if (logger_)
            logger_->error("[search {} IPv{}] No nodes from cache while refilling search",
                           sr.id.toString(),
                           (sr.af == AF_INET) ? '4' : '6');
        return 0;
    }

    unsigned inserted = 0;
    for (auto& i : cached_nodes) {
        /* try to insert the nodes. Search::insertNode will know how many to insert. */
        if (sr.insertNode(i, now))
            ++inserted;
    }
    if (logger_)
        logger_->debug("[search {} IPv{}] Refilled search with {} nodes from node cache",
                       sr.id.toString(),
                       (sr.af == AF_INET) ? '4' : '6',
                       inserted);
    return inserted;
}

/* Start a search. */
Sp<Dht::Search>
Dht::search(const InfoHash& id,
            sa_family_t af,
            GetCallback gcb,
            QueryCallback qcb,
            DoneCallback dcb,
            Value::Filter f,
            const Sp<Query>& q)
{
    if (!isRunning(af)) {
        if (logger_)
            logger_->error("[search {} IPv{}] Unsupported protocol", id.to_view(), (af == AF_INET) ? '4' : '6');
        if (dcb)
            dcb(false, {});
        return {};
    }

    auto& srs = searches(af);
    const auto& srp = srs.find(id);
    Sp<Search> sr {};

    if (srp != srs.end()) {
        sr = srp->second;
        sr->done = false;
        sr->expired = false;
    } else {
        if (srs.size() < max_searches) {
            sr = std::make_shared<Search>();
            srs.emplace(id, sr);
        } else {
            for (auto it = srs.begin(); it != srs.end(); ++it) {
                auto& s = *it->second;
                if ((s.done or s.expired) and s.announce.empty() and s.listeners.empty()) {
                    sr = it->second;
                    if (sr->nextSearchStep)
                        scheduler.cancel(sr->nextSearchStep);
                    if (sr->opExpirationJob)
                        scheduler.cancel(sr->opExpirationJob);
                    sr->clear();
                    srs.erase(it);
                    break;
                }
            }
            if (not sr) {
                if (logger_)
                    logger_->error("[search {} IPv{}] Maximum number of searches reached!",
                                   id.toString(),
                                   (af == AF_INET) ? '4' : '6');
                if (dcb)
                    dcb(false, {});
                return {};
            }
            srs.emplace(id, sr);
        }
        sr->af = af;
        sr->tid = search_id++;
        sr->step_time = time_point::min();
        sr->id = id;
        sr->done = false;
        sr->expired = false;
        sr->nodes.clear();
        sr->nodes.reserve(SEARCH_NODES + 1);
        sr->nextSearchStep = scheduler.add(time_point::max(),
                                           std::bind(&Dht::searchStep, this, std::weak_ptr<Search>(sr)));
        if (logger_)
            logger_->warn("[search {} IPv{}] New search", id.to_view(), (af == AF_INET) ? '4' : '6');
        if (search_id == 0)
            search_id++;
    }

    sr->get(f, q, qcb, gcb, dcb, scheduler);
    refill(*sr);

    return sr;
}

void
Dht::announce(
    const InfoHash& id, sa_family_t af, Sp<Value> value, DoneCallback callback, time_point created, bool permanent)
{
    auto& srs = searches(af);
    auto srp = srs.find(id);
    if (auto sr = srp == srs.end() ? search(id, af) : srp->second) {
        sr->put(value, callback, created, permanent);
        scheduler.edit(sr->nextSearchStep, scheduler.time());
    } else if (callback) {
        callback(false, {});
    }
}

size_t
Dht::listenTo(const InfoHash& id, sa_family_t af, ValueCallback cb, Value::Filter f, const Sp<Query>& q)
{
    if (!isRunning(af))
        return 0;
    // logger__ERR("[search %s IPv%c] Search_time is now in %lfs", sr->id.toString().c_str(), (sr->af == AF_INET) ? '4'
    // : '6', print_duration(tm-clock::now()));

    // logger__WARN("listenTo %s", id.toString().c_str());
    auto& srs = searches(af);
    auto srp = srs.find(id);
    Sp<Search> sr = (srp == srs.end()) ? search(id, af) : srp->second;
    if (!sr)
        throw DhtException("Unable to create search");
    if (logger_)
        logger_->warn("[search {} IPv{}] Listen", id.to_view(), (af == AF_INET) ? '4' : '6');
    return sr->listen(cb, std::move(f), q, scheduler);
}

size_t
Dht::listen(const InfoHash& id, ValueCallback cb, Value::Filter f, Where where)
{
    if (not id) {
        if (logger_)
            logger_->warn("Listen called with invalid key");
        return 0;
    }
    scheduler.syncTime();

    auto token = ++listener_token;
    auto gcb = OpValueCache::cacheCallback(std::move(cb), [this, id, token] { cancelListen(id, token); });

    auto query = std::make_shared<Query>(Select {}, std::move(where));
    auto filter = Value::Filter::chain(std::move(f), query->where.getFilter());
    auto st = store.find(id);
    if (st == store.end() && store.size() < max_store_keys)
        st = store.emplace(id, scheduler.time() + MAX_STORAGE_MAINTENANCE_EXPIRE_TIME).first;

    size_t tokenlocal = 0;
    if (st != store.end()) {
        tokenlocal = st->second.listen(gcb, filter, query);
        if (tokenlocal == 0)
            return 0;
    }

    auto token4 = Dht::listenTo(id, AF_INET, gcb, filter, query);
    auto token6 = token4 == 0 ? 0 : Dht::listenTo(id, AF_INET6, gcb, filter, query);
    if (token6 == 0 && st != store.end()) {
        st->second.cancelListen(tokenlocal);
        return 0;
    }

    listeners.emplace(token, std::make_tuple(tokenlocal, token4, token6));
    return token;
}

bool
Dht::cancelListen(const InfoHash& id, size_t token)
{
    scheduler.syncTime();

    auto it = listeners.find(token);
    if (it == listeners.end()) {
        if (logger_)
            logger_->warn("Listen token not found: {}", token);
        return false;
    }
    if (logger_)
        logger_->debug("cancelListen {} with token {}", id.to_view(), token);
    if (auto tokenlocal = std::get<0>(it->second)) {
        auto st = store.find(id);
        if (st != store.end())
            st->second.cancelListen(tokenlocal);
    }
    auto searches_cancel_listen = [this, &id](std::map<InfoHash, Sp<Search>>& srs, size_t token) {
        if (token) {
            auto srp = srs.find(id);
            if (srp != srs.end())
                srp->second->cancelListen(token, scheduler);
        }
    };
    searches_cancel_listen(dht4.searches, std::get<1>(it->second));
    searches_cancel_listen(dht6.searches, std::get<2>(it->second));
    listeners.erase(it);
    return true;
}

struct OpStatus
{
    struct Status
    {
        bool done {false};
        bool ok {false};
        Status(bool done = false, bool ok = false)
            : done(done)
            , ok(ok)
        {}
    };
    Status status;
    Status status4;
    Status status6;
};

template<typename T>
struct GetStatus : public OpStatus
{
    T values;
    std::vector<Sp<Node>> nodes;
};

void
Dht::put(const InfoHash& id, Sp<Value> val, DoneCallback callback, time_point created, bool permanent)
{
    if (not id or not val) {
        if (logger_)
            logger_->warn("Put called with invalid key or value");
        if (callback)
            callback(false, {});
        return;
    }
    if (val->id == Value::INVALID_ID)
        val->id = std::uniform_int_distribution<Value::Id> {1}(rd);
    scheduler.syncTime();
    const auto& now = scheduler.time();
    created = std::min(now, created);
    storageStore(id, val, created, {}, permanent);

    if (logger_)
        logger_->debug("put: adding {} → {}", id.to_view(), val->toString());

    auto op = std::make_shared<OpStatus>();
    auto donecb = [callback](const std::vector<Sp<Node>>& nodes, OpStatus& op) {
        // Callback as soon as the value is announced on one of the available networks
        if (callback and not op.status.done and (op.status4.done && op.status6.done)) {
            callback(op.status4.ok or op.status6.ok, nodes);
            op.status.done = true;
        }
    };
    announce(
        id,
        AF_INET,
        val,
        [=](bool ok4, const std::vector<Sp<Node>>& nodes) {
            if (logger_)
                logger_->debug("Announce done IPv4 {}", ok4);
            auto& o = *op;
            o.status4 = {true, ok4};
            donecb(nodes, o);
        },
        created,
        permanent);
    announce(
        id,
        AF_INET6,
        val,
        [=](bool ok6, const std::vector<Sp<Node>>& nodes) {
            if (logger_)
                logger_->debug("Announce done IPv6 {}", ok6);
            auto& o = *op;
            o.status6 = {true, ok6};
            donecb(nodes, o);
        },
        created,
        permanent);
}

template<typename T>
void
doneCallbackWrapper(DoneCallback dcb, const std::vector<Sp<Node>>& nodes, GetStatus<T>& op)
{
    if (op.status.done)
        return;
    op.nodes.insert(op.nodes.end(), nodes.begin(), nodes.end());
    if (op.status.ok or (op.status4.done and op.status6.done)) {
        bool ok = op.status.ok or op.status4.ok or op.status6.ok;
        op.status.done = true;
        if (dcb)
            dcb(ok, op.nodes);
    }
}

template<typename T, typename St, typename Cb, typename Av, typename Cv>
bool
callbackWrapper(
    Cb get_cb, DoneCallback done_cb, const std::vector<Sp<T>>& values, Av add_values, Cv cache_values, GetStatus<St>& op)
{
    if (op.status.done)
        return false;
    auto newvals = add_values(values);
    if (not newvals.empty()) {
        op.status.ok = !get_cb(newvals);
        cache_values(newvals);
    }
    doneCallbackWrapper(done_cb, {}, op);
    return !op.status.ok;
}

void
Dht::get(const InfoHash& id, GetCallback getcb, DoneCallback donecb, Value::Filter&& filter, Where&& where)
{
    if (not id) {
        if (logger_)
            logger_->warn("Get called with invalid key");
        if (donecb)
            donecb(false, {});
        return;
    }
    scheduler.syncTime();

    auto op = std::make_shared<GetStatus<std::map<Value::Id, Sp<Value>>>>();
    auto gcb = [getcb, donecb, op](const std::vector<Sp<Value>>& vals) {
        auto& o = *op;
        return callbackWrapper(
            getcb,
            donecb,
            vals,
            [&o](const std::vector<Sp<Value>>& values) {
                std::vector<Sp<Value>> newvals {};
                for (const auto& v : values) {
                    auto it = o.values.find(v->id);
                    if (it == o.values.cend() or (it->second != v && !(*it->second == *v))) {
                        newvals.push_back(v);
                    }
                }
                return newvals;
            },
            [&o](const std::vector<Sp<Value>>& newvals) {
                for (const auto& v : newvals)
                    o.values[v->id] = v;
            },
            o);
    };

    auto q = std::make_shared<Query>(Select {}, std::move(where));
    auto f = filter.chain(q->where.getFilter());

    /* Try to answer this search locally. */
    gcb(getLocal(id, f));

    Dht::search(
        id,
        AF_INET,
        gcb,
        {},
        [=](bool ok, const std::vector<Sp<Node>>& nodes) {
            // logger__WARN("DHT done IPv4");
            op->status4 = {true, ok};
            doneCallbackWrapper(donecb, nodes, *op);
        },
        f,
        q);
    Dht::search(
        id,
        AF_INET6,
        gcb,
        {},
        [=](bool ok, const std::vector<Sp<Node>>& nodes) {
            // logger__WARN("DHT done IPv6");
            op->status6 = {true, ok};
            doneCallbackWrapper(donecb, nodes, *op);
        },
        f,
        q);
}

void
Dht::query(const InfoHash& id, QueryCallback cb, DoneCallback done_cb, Query&& q)
{
    if (not id) {
        if (logger_)
            logger_->warn("Query called with invalid key");
        if (done_cb)
            done_cb(false, {});
        return;
    }
    scheduler.syncTime();
    auto op = std::make_shared<GetStatus<std::vector<Sp<FieldValueIndex>>>>();
    auto f = q.where.getFilter();
    auto qcb = [cb, done_cb, op](const std::vector<Sp<FieldValueIndex>>& fields) {
        auto& o = *op;
        return callbackWrapper(
            cb,
            done_cb,
            fields,
            [&](const std::vector<Sp<FieldValueIndex>>& fields) {
                std::vector<Sp<FieldValueIndex>> newvals {};
                for (const auto& f : fields) {
                    auto it = std::find_if(o.values.cbegin(), o.values.cend(), [&](const Sp<FieldValueIndex>& sf) {
                        return sf == f or f->containedIn(*sf);
                    });
                    if (it == o.values.cend()) {
                        auto lesser = std::find_if(o.values.begin(),
                                                   o.values.end(),
                                                   [&](const Sp<FieldValueIndex>& sf) { return sf->containedIn(*f); });
                        if (lesser != o.values.end())
                            o.values.erase(lesser);
                        newvals.push_back(f);
                    }
                }
                return newvals;
            },
            [&](const std::vector<Sp<FieldValueIndex>>& fields) {
                o.values.insert(o.values.end(), fields.begin(), fields.end());
            },
            o);
    };

    /* Try to answer this search locally. */
    auto values = getLocal(id, f);
    std::vector<Sp<FieldValueIndex>> local_fields(values.size());
    std::transform(values.begin(), values.end(), local_fields.begin(), [&q](const Sp<Value>& v) {
        return std::make_shared<FieldValueIndex>(*v, q.select);
    });
    qcb(local_fields);

    auto sq = std::make_shared<Query>(std::move(q));
    Dht::search(
        id,
        AF_INET,
        {},
        qcb,
        [=](bool ok, const std::vector<Sp<Node>>& nodes) {
            // logger__WARN("DHT done IPv4");
            op->status4 = {true, ok};
            doneCallbackWrapper(done_cb, nodes, *op);
        },
        f,
        sq);
    Dht::search(
        id,
        AF_INET6,
        {},
        qcb,
        [=](bool ok, const std::vector<Sp<Node>>& nodes) {
            // logger__WARN("DHT done IPv6");
            op->status6 = {true, ok};
            doneCallbackWrapper(done_cb, nodes, *op);
        },
        f,
        sq);
}

std::vector<Sp<Value>>
Dht::getLocal(const InfoHash& id, const Value::Filter& f) const
{
    auto s = store.find(id);
    if (s == store.end())
        return {};
    return s->second.get(f);
}

Sp<Value>
Dht::getLocalById(const InfoHash& id, Value::Id vid) const
{
    auto s = store.find(id);
    if (s != store.end())
        return s->second.getById(vid);
    return {};
}

std::vector<Sp<Value>>
Dht::getPut(const InfoHash& id) const
{
    std::vector<Sp<Value>> ret;
    auto find_values = [&](const std::map<InfoHash, Sp<Search>>& srs) {
        auto srp = srs.find(id);
        if (srp == srs.end())
            return;
        auto vals = srp->second->getPut();
        ret.insert(ret.end(), vals.begin(), vals.end());
    };
    find_values(dht4.searches);
    find_values(dht6.searches);
    return ret;
}

Sp<Value>
Dht::getPut(const InfoHash& id, const Value::Id& vid) const
{
    auto find_value = [&](const std::map<InfoHash, Sp<Search>>& srs) {
        auto srp = srs.find(id);
        return (srp != srs.end()) ? srp->second->getPut(vid) : Sp<Value> {};
    };
    if (auto v4 = find_value(dht4.searches))
        return v4;
    if (auto v6 = find_value(dht6.searches))
        return v6;
    return {};
}

bool
Dht::cancelPut(const InfoHash& id, const Value::Id& vid)
{
    bool canceled {false};
    auto sr_cancel_put = [&](std::map<InfoHash, Sp<Search>>& srs) {
        auto srp = srs.find(id);
        return (srp != srs.end()) ? srp->second->cancelPut(vid) : false;
    };
    canceled |= sr_cancel_put(dht4.searches);
    canceled |= sr_cancel_put(dht6.searches);
    if (canceled) {
        auto st = store.find(id);
        if (st != store.end()) {
            if (auto value = st->second.remove(id, vid))
                storageRemoved(id, st->second, {value}, value->size());
        }
    }
    return canceled;
}

// Storage

void
Dht::storageChanged(const InfoHash& id, Storage& st, const Sp<Value>& v, bool newValue)
{
    if (newValue) {
        if (not st.local_listeners.empty()) {
            if (logger_)
                logger_->debug("[store {}] {} local listeners", id.to_view(), st.local_listeners.size());
            std::vector<std::pair<ValueCallback, std::vector<Sp<Value>>>> cbs;
            cbs.reserve(st.local_listeners.size());
            for (const auto& l : st.local_listeners) {
                std::vector<Sp<Value>> vals;
                if (not l.second.filter or l.second.filter(*v))
                    vals.push_back(v);
                if (not vals.empty()) {
                    if (logger_)
                        logger_->debug("[store {}] Sending update local listener with token {}", id.to_view(), l.first);
                    cbs.emplace_back(l.second.get_cb, std::move(vals));
                }
            }
            // listeners are copied: they may be deleted by the callback
            for (auto& cb : cbs)
                cb.first(cb.second, false);
        }
    }

    if (not st.listeners.empty()) {
        if (logger_)
            logger_->debug("[store {}] {} remote listeners", id.to_view(), st.listeners.size());
        for (const auto& node_listeners : st.listeners) {
            for (const auto& l : node_listeners.second) {
                auto f = l.second.query.where.getFilter();
                if (f and not f(*v))
                    continue;
                if (logger_)
                    logger_->debug("[store {}] [node {}] Sending update",
                                   id.to_view(),
                                   node_listeners.first->toString());
                std::vector<Sp<Value>> vals;
                vals.push_back(v);
                Blob ntoken = makeToken(node_listeners.first->getAddr(), false);
                network_engine.tellListener(node_listeners.first,
                                            l.first,
                                            id,
                                            0,
                                            ntoken,
                                            {},
                                            {},
                                            std::move(vals),
                                            l.second.query,
                                            l.second.version);
            }
        }
    }
}

bool
Dht::storageStore(const InfoHash& id,
                  const Sp<Value>& value,
                  time_point created,
                  const SockAddr& sa,
                  bool permanent,
                  time_point expiration)
{
    const auto& now = scheduler.time();
    created = std::min(created, now);
    if (expiration == time_point::min())
        expiration = permanent ? time_point::max() : created + getType(value->type).expiration;
    else if (not permanent)
        expiration = std::min(expiration, created + getType(value->type).expiration);
    if (expiration < now)
        return false;

    auto st = store.find(id);
    if (st == store.end()) {
        if (store.size() >= max_store_keys)
            return false;
        auto st_i = store.emplace(id, now);
        st = st_i.first;
        if (maintain_storage and st_i.second)
            scheduler.add(st->second.maintenance_time, std::bind(&Dht::dataPersistence, this, id));
    }

    StorageBucket* store_bucket {nullptr};
    bool is_local = !sa;
    if (sa)
        store_bucket = &store_quota.try_emplace(sa, sa).first->second;
    else
        store_bucket = local_store_quota.get();

    // Reject new local values if local storage limit is exceeded
    if (is_local && max_local_store_size != STORAGE_LIMIT_UNLIMITED
        && local_store_quota->size() + value->size() > max_local_store_size) {
        if (logger_)
            logger_->warn("[store {}] Rejecting local put: local storage limit exceeded ({}/{})",
                          id.to_view(),
                          local_store_quota->size(),
                          max_local_store_size);
        return false;
    }

    auto store = st->second.store(id, value, created, expiration, store_bucket);
    if (auto vs = store.first) {
        total_store_size += store.second.size_diff;
        total_values += store.second.values_diff;
        scheduler.cancel(vs->expiration_job);
        if (not permanent) {
            vs->expiration_job = scheduler.add(expiration, std::bind(&Dht::expireStorage, this, id));
        }
        if (total_store_size - local_store_quota->size() > max_store_size) {
            auto value = vs->data;
            auto value_diff = store.second.values_diff;
            auto value_edit = store.second.edited_values;
            expireStore();
            storageChanged(id, st->second, value, value_diff > 0 || value_edit > 0);
        } else {
            storageChanged(id, st->second, vs->data, store.second.values_diff > 0 || store.second.edited_values > 0);
        }
    }

    return std::get<0>(store);
}

void
Dht::storageAddListener(const InfoHash& id, const Sp<Node>& node, size_t socket_id, Query&& query, int version)
{
    const auto& now = scheduler.time();
    auto st = store.find(id);
    if (st == store.end()) {
        if (store.size() >= max_store_keys)
            return;
        st = store.emplace(id, now).first;
    }
    auto& node_listeners = st->second.listeners[node];
    auto l = node_listeners.find(socket_id);
    if (l == node_listeners.end()) {
        auto vals = st->second.get(query.where.getFilter());
        if (not vals.empty()) {
            network_engine.tellListener(node,
                                        socket_id,
                                        id,
                                        WANT4 | WANT6,
                                        makeToken(node->getAddr(), false),
                                        dht4.buckets.findClosestNodes(id, now, TARGET_NODES),
                                        dht6.buckets.findClosestNodes(id, now, TARGET_NODES),
                                        std::move(vals),
                                        query,
                                        version);
        }
        node_listeners.emplace(socket_id, Listener {now, std::forward<Query>(query), version});
    } else
        l->second.refresh(now, std::forward<Query>(query));
}

void
Dht::expireStore(decltype(store)::iterator i)
{
    const auto& id = i->first;
    auto& st = i->second;
    auto stats = st.expire(id, scheduler.time());
    if (not stats.expired_values.empty()) {
        storageRemoved(id, st, stats.expired_values, -stats.size_diff);
    }
}

void
Dht::expireStorage(InfoHash h)
{
    auto i = store.find(h);
    if (i != store.end())
        expireStore(i);
}

void
Dht::storageRemoved(const InfoHash& id, Storage& st, const std::vector<Sp<Value>>& values, size_t totalSize)
{
    if (logger_)
        logger_->debug("[store {}] Discarded {} values ({} bytes)", id.to_view(), values.size(), totalSize);

    total_store_size -= totalSize;
    total_values -= values.size();

    if (not st.listeners.empty()) {
        if (logger_)
            logger_->debug("[store {}] {} remote listeners", id.to_view(), st.listeners.size());

        std::vector<Value::Id> ids;
        ids.reserve(values.size());
        for (const auto& v : values)
            ids.emplace_back(v->id);

        for (const auto& node_listeners : st.listeners) {
            for (const auto& l : node_listeners.second) {
                if (logger_)
                    logger_->warn("[store {}] [node {}] Sending expired",
                                  id.to_view(),
                                  node_listeners.first->toString());
                Blob ntoken = makeToken(node_listeners.first->getAddr(), false);
                network_engine.tellListenerExpired(node_listeners.first, l.first, id, ntoken, ids, l.second.version);
            }
        }
    }
    for (const auto& local_listeners : st.local_listeners) {
        local_listeners.second.get_cb(values, true);
    }
}

void
Dht::expireStore()
{
    // removing expired values
    for (auto i = store.begin(); i != store.end();) {
        expireStore(i);

        if (i->second.empty() && i->second.listeners.empty() && i->second.local_listeners.empty()) {
            if (logger_)
                logger_->debug("[store {}] Discarding empty storage", i->first.to_view());
            i = store.erase(i);
        } else
            ++i;
    }

    // remove more values if remote storage limit is exceeded
    while (total_store_size - local_store_quota->size() > max_store_size) {
        // find IP using the most storage
        if (store_quota.empty()) {
            if (logger_)
                logger_->warn("No space left: local data consumes all the quota!");
            break;
        }
        auto largest = store_quota.begin();
        for (auto it = std::next(largest); it != store_quota.end(); ++it) {
            if (it->second.size() > largest->second.size())
                largest = it;
        }
        if (largest != store_quota.end() && !largest->second.empty()) {
            while (true) {
                auto exp_value = largest->second.getOldest();
                auto storage = store.find(exp_value.first);
                if (storage != store.end()) {
                    if (logger_)
                        logger_->warn("Storage quota full: discarding value from {} at {} {:016x}",
                                      largest->first.toString(),
                                      exp_value.first.to_view(),
                                      exp_value.second);

                    if (auto value = storage->second.remove(exp_value.first, exp_value.second)) {
                        storageRemoved(storage->first, storage->second, {value}, value->size());
                        break;
                    }
                }
            }
        } else {
            if (logger_)
                logger_->warn("No space left: remote data consumes all the quota!");
            break;
        }
    }

    // remove unused quota entires
    for (auto i = store_quota.begin(); i != store_quota.end();) {
        if (i->second.empty())
            i = store_quota.erase(i);
        else
            ++i;
    }
}

void
Dht::connectivityChanged(sa_family_t af)
{
    const auto& now = scheduler.time();
    scheduler.edit(nextNodesConfirmation, now);
    auto& dht = this->dht(af);
    dht.buckets.connectivityChanged(now);
    dht.reported_addr.clear();
    network_engine.connectivityChanged(af);
    startBootstrap(); // will only happen if disconnected
}

void
Dht::rotateSecrets()
{
    oldsecret = secret;
    secret = std::uniform_int_distribution<uint64_t> {}(rd);
    uniform_duration_distribution<> time_dist(std::chrono::minutes(15), std::chrono::minutes(45));
    auto rotate_secrets_time = scheduler.time() + time_dist(rd);
    scheduler.add(rotate_secrets_time, std::bind(&Dht::rotateSecrets, this));
}

Blob
Dht::makeToken(const SockAddr& addr, bool old) const
{
    const void* ip;
    size_t iplen;
    in_port_t port;

    auto family = addr.getFamily();
    if (family == AF_INET) {
        const auto& sin = addr.getIPv4();
        ip = &sin.sin_addr;
        iplen = 4;
        port = sin.sin_port;
    } else if (family == AF_INET6) {
        const auto& sin6 = addr.getIPv6();
        ip = &sin6.sin6_addr;
        iplen = 16;
        port = sin6.sin6_port;
    } else {
        return {};
    }

    const auto& c1 = old ? oldsecret : secret;
    Blob data;
    data.reserve(sizeof(secret) + sizeof(in_port_t) + iplen);
    data.insert(data.end(), (uint8_t*) &c1, ((uint8_t*) &c1) + sizeof(c1));
    data.insert(data.end(), (uint8_t*) ip, (uint8_t*) ip + iplen);
    data.insert(data.end(), (uint8_t*) &port, ((uint8_t*) &port) + sizeof(in_port_t));
    return crypto::hash(data, TOKEN_SIZE);
}

bool
Dht::tokenMatch(const Blob& token, const SockAddr& addr) const
{
    if (not addr or token.size() != TOKEN_SIZE)
        return false;
    if (token == makeToken(addr, false))
        return true;
    if (token == makeToken(addr, true))
        return true;
    return false;
}

NodeStats
Dht::getNodesStats(sa_family_t af) const
{
    NodeStats stats = dht(af).getNodesStats(scheduler.time(), myid);
    stats.node_cache_size = network_engine.getNodeCacheSize(af);
    return stats;
}

NodeStats
Dht::Kad::getNodesStats(time_point now, const InfoHash& myid) const
{
    NodeStats stats {};
    for (const auto& b : buckets) {
        for (auto& n : b.nodes) {
            if (n->isGood(now)) {
                stats.good_nodes++;
                if (n->isIncoming())
                    stats.incoming_nodes++;
            } else if (not n->isExpired())
                stats.dubious_nodes++;
        }
        if (b.cached)
            stats.cached_nodes++;
    }
    stats.table_depth = buckets.depth(buckets.findBucket(myid));
    stats.searches = searches.size();
    return stats;
}

void
Dht::dumpBucket(const Bucket& b, std::ostream& out) const
{
    const auto& now = scheduler.time();
    using namespace std::chrono;
    out << b.first << " count: " << b.nodes.size() << " updated: " << print_time_relative(now, b.time);
    if (b.cached)
        out << " (cached)";
    out << std::endl;
    for (auto& n : b.nodes) {
        out << "    Node " << n->toString();
        const auto& t = n->getTime();
        const auto& r = n->getReplyTime();
        if (t != r)
            out << " updated: " << print_time_relative(now, t) << ", replied: " << print_time_relative(now, r);
        else
            out << " updated: " << print_time_relative(now, t);
        if (n->isExpired())
            out << " [expired]";
        else if (n->isGood(now))
            out << " [good]";
        out << std::endl;
    }
}

void
Dht::dumpSearch(const Search& sr, std::ostream& out) const
{
    const auto& now = scheduler.time();
    const auto& listen_expire = getListenExpiration();
    using namespace std::chrono;
    fmt::print(out,
               "Search IPv{} {} gets: {} last step: {}{}{}{}{}\n",
               (sr.af == AF_INET6 ? '6' : '4'),
               sr.id,
               sr.callbacks.size(),
               print_time_relative(now, sr.step_time),
               (sr.done ? " [done]"sv : ""sv),
               (sr.expired ? " [expired]"sv : ""sv),
               (sr.isSynced(now) ? " [synced]"sv : " [not synced]"sv),
               (sr.isListening(now, listen_expire) ? " [listening]"sv : ""sv));

    // printing the queries
    if (sr.callbacks.size() + sr.listeners.size() > 0)
        fmt::print(out, "Queries:\n");
    for (const auto& cb : sr.callbacks) {
        out << *cb.second.query << std::endl;
    }
    for (const auto& l : sr.listeners) {
        out << *l.second.query << std::endl;
    }

    for (const auto& a : sr.announce) {
        bool announced = sr.isAnnounced(a.value->id);
        out << "Announcement: " << *a.value << (announced ? " [announced]" : "") << std::endl;
    }

    fmt::print(out, " Common bits    InfoHash                       Conn. Get   Ops  IP\n");
    auto last_get = sr.getLastGetTime();
    for (const auto& np : sr.nodes) {
        auto& n = *np;
        auto listen = (not sr.listeners.empty()
                           ? (n.listenStatus.empty() ? "    "
                                                     : fmt::format("[{}] ",
                                                                   (n.isListening(now, listen_expire)
                                                                        ? 'l'
                                                                        : (n.pending(n.listenStatus) ? 'f' : ' '))))
                           : "");
        std::string announce;
        if (not sr.announce.empty()) {
            if (n.acked.empty()) {
                announce = std::string(sr.announce.size() + 3, ' ');
            } else {
                announce = "[";
                for (const auto& a : sr.announce) {
                    auto ack = n.acked.find(a.value->id);
                    if (ack == n.acked.end() or not ack->second.req) {
                        announce += ' ';
                    } else {
                        announce += ack->second.req->getStateChar();
                    }
                }
                announce += "] ";
            }
        }

        fmt::print(out,
                   "{:3} {} {} [{}{}] {}{}{}\n",
                   InfoHash::commonBits(sr.id, n.node->id),
                   n.node->id,
                   (findNode(n.node->id, sr.af) ? '*' : ' '),
                   (n.pending(n.getStatus) ? (n.candidate ? 'c' : 'f') : ' '),
                   (n.isSynced(now) ? (n.last_get_reply > last_get ? 'u' : 's') : '-'),
                   listen,
                   announce,
                   n.node->getAddrStr());
    }
}

void
Dht::dumpTables() const
{
    std::ostringstream out;
    out << "My id " << myid << std::endl;

    out << "Buckets IPv4 :" << std::endl;
    for (const auto& b : dht4.buckets)
        dumpBucket(b, out);
    out << "Buckets IPv6 :" << std::endl;
    for (const auto& b : dht6.buckets)
        dumpBucket(b, out);

    auto dump_searches = [&](std::map<InfoHash, Sp<Search>> srs) {
        for (auto& srp : srs)
            dumpSearch(*srp.second, out);
    };
    dump_searches(dht4.searches);
    dump_searches(dht6.searches);
    out << std::endl;

    out << getStorageLog() << std::endl;

    if (logger_)
        logger_->debug("{}", out.str());
}

std::string
Dht::getStorageLog() const
{
    std::ostringstream out;
    for (const auto& s : store) {
        const auto& st = s.second;
        out << "Storage " << s.first << " " << st.listeners.size() << " list., " << st.valueCount() << " values, "
            << printByteCount(st.totalSize()) << "\n";
    }
    out << std::endl << std::endl;
    std::multimap<size_t, const SockAddr*> q_map;
    for (const auto& ip : store_quota)
        if (ip.second.size())
            q_map.emplace(ip.second.size(), &ip.first);
    for (auto ip = q_map.rbegin(); ip != q_map.rend(); ++ip)
        out << "IP " << ip->second->toString() << " uses " << printByteCount(ip->first) << std::endl;
    out << std::endl;
    out << "Total " << store.size() << " storages, " << total_values << " values (";
    if (total_store_size < 1024)
        out << total_store_size << " bytes)";
    else {
        auto remote_store_size = total_store_size - local_store_quota->size();
        out << (remote_store_size / 1024) << " / " << (max_store_size / 1024) << " KB remote, "
            << (local_store_quota->size() / 1024) << " / ";
        if (max_local_store_size == STORAGE_LIMIT_UNLIMITED)
            out << "unlimited";
        else
            out << (max_local_store_size / 1024) << " KB";
        out << " local)";
    }
    out << std::endl;
    return out.str();
}

std::string
Dht::getStorageLog(const InfoHash& h) const
{
    auto s = store.find(h);
    if (s == store.end())
        return fmt::format("Storage {} empty\n", h);
    return printStorageLog(*s);
}

std::string
Dht::printStorageLog(const decltype(store)::value_type& s) const
{
    std::ostringstream out;
    using namespace std::chrono;
    const auto& st = s.second;
    const auto& now = scheduler.time();
    out << "Storage " << s.first << " " << st.listeners.size() << " list., " << st.valueCount() << " values, "
        << printByteCount(st.totalSize()) << std::endl;
    for (const auto& v : st.getValues()) {
        out << "   Value " << v.data->id << " size: " << printByteCount(v.data->size())
            << " created: " << print_time_relative(now, v.created)
            << " expires: " << print_time_relative(now, v.expiration) << std::endl;
    }
    if (not st.local_listeners.empty())
        out << "   " << st.local_listeners.size() << " local listeners" << std::endl;
    for (const auto& node_listeners : st.listeners) {
        const auto& node = node_listeners.first;
        out << "   " << "Listener " << node->toString() << " : " << node_listeners.second.size() << " entries"
            << std::endl;
    }
    return out.str();
}

std::string
Dht::getRoutingTablesLog(sa_family_t af) const
{
    std::ostringstream out;
    for (const auto& b : buckets(af))
        dumpBucket(b, out);
    return out.str();
}

std::string
Dht::getSearchesLog(sa_family_t af) const
{
    std::ostringstream out;
    auto num_searches = dht4.searches.size() + dht6.searches.size();
    if (num_searches > 8) {
        if (not af or af == AF_INET)
            for (const auto& sr : dht4.searches)
                out << "[search " << sr.first << " IPv4]" << std::endl;
        if (not af or af == AF_INET6)
            for (const auto& sr : dht6.searches)
                out << "[search " << sr.first << " IPv6]" << std::endl;
    } else {
        out << "s:synched, u:updated, a:announced, c:candidate, f:cur req, x:expired, *:known" << std::endl;
        if (not af or af == AF_INET)
            for (const auto& sr : dht4.searches)
                dumpSearch(*sr.second, out);
        if (not af or af == AF_INET6)
            for (const auto& sr : dht6.searches)
                dumpSearch(*sr.second, out);
    }
    out << "Total: " << num_searches << " searches (" << dht4.searches.size() << " IPv4, " << dht6.searches.size()
        << " IPv6)." << std::endl;
    return out.str();
}

std::string
Dht::getSearchLog(const InfoHash& id, sa_family_t af) const
{
    std::ostringstream out;
    if (af == AF_UNSPEC) {
        out << getSearchLog(id, AF_INET) << getSearchLog(id, AF_INET6);
    } else {
        auto& srs = searches(af);
        auto sr = srs.find(id);
        if (sr != srs.end())
            dumpSearch(*sr->second, out);
    }
    return out.str();
}

Dht::~Dht()
{
    for (auto& s : dht4.searches)
        s.second->clear();
    for (auto& s : dht6.searches)
        s.second->clear();
}

net::NetworkConfig
fromDhtConfig(const Config& config)
{
    net::NetworkConfig netConf;
    netConf.network = config.network;
    netConf.max_req_per_sec = config.max_req_per_sec ? config.max_req_per_sec : MAX_REQUESTS_PER_SEC;
    netConf.max_peer_req_per_sec = config.max_peer_req_per_sec ? config.max_peer_req_per_sec
                                                               : netConf.max_req_per_sec / 8;
    netConf.is_client = config.client_mode;
    return netConf;
}

Dht::Dht(std::unique_ptr<net::DatagramSocket>&& sock,
         const Config& config,
         const Sp<Logger>& l,
         std::unique_ptr<std::mt19937_64>&& rda)
    : DhtInterface(l)
    , rd(rda ? std::move(*rda) : crypto::getSeededRandomEngine<std::mt19937_64>())
    , myid(config.node_id ? config.node_id : InfoHash::getRandom(rd))
    , store()
    , store_quota()
    , local_store_quota(std::make_unique<StorageBucket>())
    , max_store_keys(config.max_store_keys ? (int) config.max_store_keys : MAX_HASHES)
    , max_store_size(config.max_store_size ? (size_t) config.max_store_size : STORAGE_LIMIT_DEFAULT)
    , max_local_store_size(config.max_local_store_size ? (size_t) config.max_local_store_size : STORAGE_LIMIT_UNLIMITED)
    , max_searches(config.max_searches ? (int) config.max_searches : MAX_SEARCHES)
    , network_engine(myid,
                     fromDhtConfig(config),
                     std::move(sock),
                     logger_,
                     rd,
                     scheduler,
                     std::bind(&Dht::onError, this, _1, _2),
                     std::bind(&Dht::onNewNode, this, _1, _2),
                     std::bind(&Dht::onReportedAddr, this, _1, _2),
                     std::bind(&Dht::onPing, this, _1),
                     std::bind(&Dht::onFindNode, this, _1, _2, _3),
                     std::bind(&Dht::onGetValues, this, _1, _2, _3, _4),
                     std::bind(&Dht::onListen, this, _1, _2, _3, _4, _5, _6),
                     std::bind(&Dht::onAnnounce, this, _1, _2, _3, _4, _5),
                     std::bind(&Dht::onRefresh, this, _1, _2, _3, _4))
    , persistPath(config.persist_path)
    , is_bootstrap(config.is_bootstrap)
    , maintain_storage(config.maintain_storage)
    , public_stable(config.public_stable)
{
    scheduler.syncTime();
    auto s = network_engine.getSocket();
    if (not s or (not s->hasIPv4() and not s->hasIPv6()))
        throw DhtException("Opened socket required");
    if (s->hasIPv4()) {
        dht4.buckets = {Bucket {AF_INET}};
        dht4.buckets.is_client = config.is_bootstrap;
    }
    if (s->hasIPv6()) {
        dht6.buckets = {Bucket {AF_INET6}};
        dht6.buckets.is_client = config.is_bootstrap;
    }

    search_id = std::uniform_int_distribution<decltype(search_id)> {}(rd);

    uniform_duration_distribution<> time_dis {std::chrono::seconds(3), std::chrono::seconds(5)};
    nextNodesConfirmation = scheduler.add(scheduler.time() + time_dis(rd), std::bind(&Dht::confirmNodes, this));

    // Fill old secret
    secret = std::uniform_int_distribution<uint64_t> {}(rd);
    rotateSecrets();

    if (not persistPath.empty())
        loadState(persistPath);

    expire();

    if (logger_)
        logger_->debug("DHT node initialised with ID {:s}", myid);
}

bool
Dht::neighbourhoodMaintenance(RoutingTable& list)
{
    // logger__DBG("neighbourhoodMaintenance");
    auto b = list.findBucket(myid);
    if (b == list.end())
        return false;

    InfoHash id = myid;
#ifdef _WIN32
    std::uniform_int_distribution<int> rand_byte {0, std::numeric_limits<uint8_t>::max()};
#else
    std::uniform_int_distribution<uint8_t> rand_byte;
#endif
    id[HASH_LEN - 1] = rand_byte(rd);

    std::bernoulli_distribution rand_trial(1. / 8.);
    auto q = b;
    if (std::next(q) != list.end() && (q->nodes.empty() || rand_trial(rd)))
        q = std::next(q);
    if (b != list.begin() && (q->nodes.empty() || rand_trial(rd))) {
        auto r = std::prev(b);
        if (!r->nodes.empty())
            q = r;
    }

    auto n = q->randomNode(rd);
    if (n) {
        if (logger_)
            logger_->debug("[node {}] Sending [find {}] for neighborhood maintenance", n->toString(), id.to_view());
        /* Since our node-id is the same in both DHTs, it's probably
           profitable to query both families. */
        network_engine.sendFindNode(n, id, network_engine.want());
    }

    return true;
}

bool
Dht::bucketMaintenance(RoutingTable& list)
{
    std::bernoulli_distribution rand_trial(1. / 8.);
    std::bernoulli_distribution rand_trial_38(1. / 38.);

    bool sent {false};
    for (auto b = list.begin(); b != list.end(); ++b) {
        if (b->time < scheduler.time() - std::chrono::minutes(10) || b->nodes.empty()) {
            /* This bucket hasn't seen any positive confirmation for a long
               time. Pick a random id in this bucket's range, and send a request
               to a random node. */
            InfoHash id = list.randomId(b, rd);
            auto q = b;
            /* If the bucket is empty, we try to fill it from a neighbour.
               We also sometimes do it gratuitiously to recover from
               buckets full of broken nodes. */
            if (std::next(b) != list.end() && (q->nodes.empty() || rand_trial(rd)))
                q = std::next(b);
            if (b != list.begin() && (q->nodes.empty() || rand_trial(rd))) {
                auto r = std::prev(b);
                if (!r->nodes.empty())
                    q = r;
            }

            auto n = q->randomNode(rd);
            if (n and not n->isPendingMessage()) {
                want_t want = -1;

                if (network_engine.want() != want) {
                    auto otherbucket = findBucket(id, q->af == AF_INET ? AF_INET6 : AF_INET);
                    if (otherbucket && otherbucket->nodes.size() < TARGET_NODES)
                        /* The corresponding bucket in the other family
                           is emptyish -- querying both is useful. */
                        want = WANT4 | WANT6;
                    else if (rand_trial_38(rd))
                        /* Most of the time, this just adds overhead.
                           However, it might help stitch back one of
                           the DHTs after a network collapse, so query
                           both, but only very occasionally. */
                        want = WANT4 | WANT6;
                }

                if (logger_)
                    logger_->debug("[node {}] Sending find {} for bucket maintenance", n->toString(), id.to_view());
                // auto start = scheduler.time();
                network_engine.sendFindNode(n, id, want, nullptr, [this, n](const net::Request&, bool over) {
                    if (over) {
                        const auto& end = scheduler.time();
                        // using namespace std::chrono;
                        // if (logger_)
                        //     logger_->d(n->id, "[node %s] Bucket maintenance op expired after %s",
                        //     n->toString().c_str(), print_duration(end-start).c_str());
                        scheduler.edit(nextNodesConfirmation, end + Node::MAX_RESPONSE_TIME);
                    }
                });
                sent = true;
            }
        }
    }
    return sent;
}

void
Dht::dataPersistence(InfoHash id)
{
    const auto& now = scheduler.time();
    auto str = store.find(id);
    if (str != store.end() and now > str->second.maintenance_time) {
        if (logger_)
            logger_->debug("[storage {}] Maintenance ({} values, {} bytes)",
                           id.to_view(),
                           str->second.valueCount(),
                           str->second.totalSize());
        maintainStorage(*str);
        str->second.maintenance_time = now + MAX_STORAGE_MAINTENANCE_EXPIRE_TIME;
        scheduler.add(str->second.maintenance_time, std::bind(&Dht::dataPersistence, this, id));
    }
}

size_t
Dht::maintainStorage(decltype(store)::value_type& storage, bool force, const DoneCallback& donecb)
{
    const auto& now = scheduler.time();
    size_t announce_per_af = 0;

    auto maintain = [&](sa_family_t af) {
        bool want = true;
        auto nodes = buckets(af).findClosestNodes(storage.first, now);
        if (!nodes.empty()) {
            if (force || storage.first.xorCmp(nodes.back()->id, myid) < 0) {
                for (auto& value : storage.second.getValues()) {
                    const auto& vt = getType(value.data->type);
                    if (force || value.created + vt.expiration > now + MAX_STORAGE_MAINTENANCE_EXPIRE_TIME) {
                        // gotta put that value there
                        announce(storage.first, af, value.data, donecb, value.created);
                        ++announce_per_af;
                    }
                }
                want = false;
            }
        }
        return want;
    };
    bool want4 = maintain(AF_INET), want6 = maintain(AF_INET6);

    if (not want4 and not want6) {
        if (logger_)
            logger_->debug("Discarding storage values {}", storage.first.to_view());
        auto diff = storage.second.clear(storage.first);
        total_store_size += diff.size_diff;
        total_values += diff.values_diff;
    }

    return announce_per_af;
}

time_point
Dht::periodic(const uint8_t* buf, size_t buflen, SockAddr from, const time_point& now)
{
    scheduler.syncTime(now);
    if (buflen) {
        try {
            network_engine.processMessage(buf, buflen, std::move(from));
        } catch (const std::exception& e) {
            if (logger_)
                logger_->warn("Unable to process message: {}", e.what());
        }
    }
    return scheduler.run();
}

void
Dht::expire()
{
    uniform_duration_distribution<> time_dis(std::chrono::minutes(2), std::chrono::minutes(6));
    auto expire_stuff_time = scheduler.time() + duration(time_dis(rd));

    expireBuckets(dht4.buckets);
    expireBuckets(dht6.buckets);
    expireStore();
    expireSearches();
    scheduler.add(expire_stuff_time, std::bind(&Dht::expire, this));
}

void
Dht::onConnected()
{
    stopBootstrap();

    auto restartSearches = [this](std::map<InfoHash, Sp<Search>>& searches) {
        const auto& now = scheduler.time();
        for (auto& srp : searches) {
            auto& sr = *srp.second;
            if (sr.done)
                continue;
            scheduler.edit(sr.nextSearchStep, now);
        }
    };
    restartSearches(dht4.searches);
    restartSearches(dht6.searches);
    auto callbacks = std::move(onConnectCallbacks_);
    while (not callbacks.empty()) {
        callbacks.front()();
        callbacks.pop();
    }
}

void
Dht::onDisconnected()
{
    if (not bootstrapJob)
        bootstrap();
}

void
Dht::bootstrap()
{
    if (dht4.status != NodeStatus::Disconnected || dht6.status != NodeStatus::Disconnected) {
        // The job that led here has already run, and the scheduler does not
        // clear the handle to a job it has executed. onDisconnected() reads a
        // set handle as "a bootstrap is already scheduled" and does nothing,
        // so leaving it set here means the node never tries to rejoin the
        // network once it has lost it. Clearing it is what says that no
        // bootstrap is pending any more.
        bootstrapJob.reset();
        return;
    }
    if (logger_)
        logger_->debug("[{}] Bootstraping", myid.to_view());
    bootstrap_pending = false;
    for (const auto& boootstrap : bootstrap_nodes) {
        try {
            auto ips = network_engine.getSocket()->resolve(boootstrap.first, boootstrap.second);
            for (auto& ip : ips) {
                if (ip.getPort() == 0)
                    ip.setPort(net::DHT_DEFAULT_PORT);
                pingNode(ip);
            }
        } catch (const std::exception& e) {
            if (logger_)
                logger_->error("[{}] Unable to resolve {}:{}: {}",
                               myid.to_view(),
                               boootstrap.first,
                               boootstrap.second,
                               e.what());
        }
    }
    scheduler.cancel(bootstrapJob);
    bootstrapJob = scheduler.add(scheduler.time() + bootstrap_period, std::bind(&Dht::bootstrap, this));
    bootstrap_period = std::min(bootstrap_period * 2, BOOTSTRAP_PERIOD_MAX);
}

void
Dht::startBootstrap()
{
    stopBootstrap();
    bootstrap_pending = !bootstrap_nodes.empty();
    bootstrapJob = scheduler.add(scheduler.time(), std::bind(&Dht::bootstrap, this));
}

void
Dht::stopBootstrap()
{
    scheduler.cancel(bootstrapJob);
    bootstrap_pending = false;
    bootstrap_period = BOOTSTRAP_PERIOD;
}

void
Dht::confirmNodes()
{
    using namespace std::chrono;
    bool soon = false;
    const auto& now = scheduler.time();

    if (dht4.searches.empty() and dht4.status == NodeStatus::Connected) {
        if (logger_)
            logger_->debug("[confirm nodes] Initial IPv4 'get' for my id ({})", myid.to_view());
        search(myid, AF_INET);
    }
    if (dht6.searches.empty() and dht6.status == NodeStatus::Connected) {
        if (logger_)
            logger_->debug("[confirm nodes] Initial IPv6 'get' for my id ({})", myid.to_view());
        search(myid, AF_INET6);
    }

    soon |= bucketMaintenance(dht4.buckets);
    soon |= bucketMaintenance(dht6.buckets);

    if (!soon) {
        if (dht4.buckets.grow_time >= now - seconds(150))
            soon |= neighbourhoodMaintenance(dht4.buckets);
        if (dht6.buckets.grow_time >= now - seconds(150))
            soon |= neighbourhoodMaintenance(dht6.buckets);
    }

    /* In order to maintain all buckets' age within 600 seconds, worst
       case is roughly 27 seconds, assuming the table is 22 bits deep.
       We want to keep a margin for neighborhood maintenance, so keep
       this within 25 seconds. */
    auto time_dis = soon ? uniform_duration_distribution<> {seconds(5), seconds(25)}
                         : uniform_duration_distribution<> {seconds(60), seconds(180)};
    auto confirm_nodes_time = now + time_dis(rd);

    scheduler.edit(nextNodesConfirmation, confirm_nodes_time);
}

std::vector<ValuesExport>
Dht::exportValues() const
{
    const auto& now = scheduler.time();
    const auto system_now = system_clock::now();

    std::vector<ValuesExport> e {};
    e.reserve(store.size());
    for (const auto& h : store) {
        ValuesExport ve;
        ve.first = h.first;

        msgpack::sbuffer buffer;
        msgpack::packer<msgpack::sbuffer> pk(&buffer);
        const auto& vals = h.second.getValues();
        pk.pack_array(vals.size());
        for (const auto& v : vals) {
            pk.pack_array(4);
            pk.pack(to_system_time(v.created, now, system_now));
            v.data->msgpack_pack(pk);
            if (const auto& store_addr = v.store_bucket ? v.store_bucket->getAddr() : SockAddr {}) {
                pk.pack_bin(store_addr.getLength());
                pk.pack_bin_body(reinterpret_cast<const char*>(store_addr.get()), store_addr.getLength());
            } else {
                pk.pack_bin(0);
            }
            pk.pack(to_system_time(v.expiration, now, system_now));
        }
        ve.second = {buffer.data(), buffer.data() + buffer.size()};
        e.push_back(std::move(ve));
    }
    return e;
}

void
Dht::importValues(const std::vector<ValuesExport>& import)
{
    const auto& now = scheduler.time();
    const auto system_now = system_clock::now();

    unsigned imported = 0, ignored = 0;
    size_t imported_size = 0;
    for (const auto& value : import) {
        if (value.second.empty()) {
            ignored++;
            continue;
        }

        try {
            msgpack::unpacked msg;
            msgpack::unpack(msg, (const char*) value.second.data(), value.second.size());
            auto valarr = msg.get();
            if (valarr.type != msgpack::type::ARRAY)
                throw msgpack::type_error();
            for (unsigned i = 0; i < valarr.via.array.size; i++) {
                auto& valel = valarr.via.array.ptr[i];
                if (valel.type != msgpack::type::ARRAY or valel.via.array.size < 2)
                    throw msgpack::type_error();
                time_point val_time;
                time_point expiration = time_point::min();
                Value tmp_val;
                SockAddr store_addr;
                try {
                    const bool uses_system_time = valel.via.array.ptr[0].type == msgpack::type::EXT;
                    if (uses_system_time) {
                        val_time = from_system_time(valel.via.array.ptr[0].as<system_clock::time_point>(),
                                                    now,
                                                    system_now);
                    } else {
                        val_time = time_point {
                            time_point::duration {valel.via.array.ptr[0].as<time_point::duration::rep>()}};
                    }
                    tmp_val.msgpack_unpack(valel.via.array.ptr[1]);
                    if (valel.via.array.size >= 3) {
                        auto addr_blob = valel.via.array.ptr[2].as<Blob>();
                        if (not addr_blob.empty()) {
                            store_addr = SockAddr(reinterpret_cast<const sockaddr*>(addr_blob.data()),
                                                  static_cast<socklen_t>(addr_blob.size()));
                        }
                    }
                    if (valel.via.array.size >= 4) {
                        if (uses_system_time) {
                            expiration = from_system_time(valel.via.array.ptr[3].as<system_clock::time_point>(),
                                                          now,
                                                          system_now);
                        } else {
                            expiration = time_point {
                                time_point::duration {valel.via.array.ptr[3].as<time_point::duration::rep>()}};
                        }
                    }
                } catch (const std::exception&) {
                    if (logger_)
                        logger_->error("Error reading value at {}", value.first.to_view());
                    ignored++;
                    continue;
                }
                val_time = std::min(val_time, now);
                auto val_size = tmp_val.size();
                if (storageStore(value.first,
                                 std::make_shared<Value>(std::move(tmp_val)),
                                 val_time,
                                 store_addr,
                                 false,
                                 expiration)) {
                    imported++;
                    imported_size += val_size;
                } else {
                    ignored++;
                }
            }
        } catch (const std::exception&) {
            ignored++;
            if (logger_)
                logger_->error("Error reading values at {}", value.first.to_view());
            continue;
        }
    }
    if (logger_)
        logger_->debug("Imported {} values, {}, ignored {}", imported, dht::printByteCount(imported_size), ignored);
}

std::vector<NodeExport>
Dht::exportNodes() const
{
    const auto& now = scheduler.time();
    std::vector<NodeExport> nodes;
    const auto b4 = dht4.buckets.findBucket(myid);
    if (b4 != dht4.buckets.end()) {
        for (auto& n : b4->nodes)
            if (n->isGood(now))
                nodes.emplace_back(n->exportNode());
    }
    const auto b6 = dht6.buckets.findBucket(myid);
    if (b6 != dht6.buckets.end()) {
        for (auto& n : b6->nodes)
            if (n->isGood(now))
                nodes.emplace_back(n->exportNode());
    }
    for (auto b = dht4.buckets.begin(); b != dht4.buckets.end(); ++b) {
        if (b == b4)
            continue;
        for (auto& n : b->nodes)
            if (n->isGood(now))
                nodes.emplace_back(n->exportNode());
    }
    for (auto b = dht6.buckets.begin(); b != dht6.buckets.end(); ++b) {
        if (b == b6)
            continue;
        for (auto& n : b->nodes)
            if (n->isGood(now))
                nodes.emplace_back(n->exportNode());
    }
    return nodes;
}

void
Dht::insertNode(const InfoHash& id, const SockAddr& addr)
{
    if (addr.getFamily() != AF_INET && addr.getFamily() != AF_INET6)
        return;
    scheduler.syncTime();
    network_engine.insertNode(id, addr);
}

void
Dht::pingNode(SockAddr sa, DoneCallbackSimple&& cb)
{
    scheduler.syncTime();
    if (logger_)
        logger_->debug("Sending ping to {}", sa);
    auto& count = dht(sa.getFamily()).pending_pings;
    count++;
    network_engine.sendPing(
        std::move(sa),
        [&count, cb](const net::Request&, net::RequestAnswer&&) {
            count--;
            if (cb)
                cb(true);
        },
        [&count, cb](const net::Request&, bool last) {
            if (last) {
                count--;
                if (cb)
                    cb(false);
            }
        });
}

void
Dht::onError(Sp<net::Request> req, net::DhtProtocolException e)
{
    const auto& node = req->node;
    if (e.getCode() == net::DhtProtocolException::UNAUTHORIZED) {
        if (logger_)
            logger_->error("[node {}] Token flush", node->toString());
        node->authError();
        for (auto& srp : searches(node->getFamily())) {
            auto& sr = srp.second;
            for (auto& n : sr->nodes) {
                if (n->node != node)
                    continue;
                n->token.clear();
                n->last_get_reply = time_point::min();
                for (auto& listen : n->listenStatus) {
                    if (listen.second.req and listen.second.req->pending())
                        n->node->cancelRequest(listen.second.req);
                    listen.second.req = {};
                    n->node->closeSocket(listen.second.socketId);
                    if (listen.second.refresh) {
                        listen.second.refresh->cancel();
                        listen.second.refresh = {};
                    }
                    listen.second.cache.onSynced(false);
                }
                searchSendGetValues(sr);
                scheduler.edit(sr->nextSearchStep, scheduler.time());
                break;
            }
        }
    } else if (e.getCode() == net::DhtProtocolException::NOT_FOUND) {
        if (logger_)
            logger_->error("[node {}] Returned error 404: storage not found", node->toString());
        node->cancelRequest(req);
    }
}

void
Dht::onReportedAddr(const InfoHash& /*id*/, const SockAddr& addr)
{
    if (addr)
        reportedAddr(addr);
}

net::RequestAnswer
Dht::onPing(Sp<Node>)
{
    return {};
}

net::RequestAnswer
Dht::onFindNode(Sp<Node> node, const InfoHash& target, want_t want)
{
    const auto& now = scheduler.time();
    net::RequestAnswer answer;
    answer.ntoken = makeToken(node->getAddr(), false);
    if (want & WANT4)
        answer.nodes4 = dht4.buckets.findClosestNodes(target, now, TARGET_NODES);
    if (want & WANT6)
        answer.nodes6 = dht6.buckets.findClosestNodes(target, now, TARGET_NODES);
    return answer;
}

net::RequestAnswer
Dht::onGetValues(Sp<Node> node, const InfoHash& hash, want_t, const Query& query)
{
    if (not hash) {
        if (logger_)
            logger_->warn("[node {}] Eek! Got get_values with no info_hash", node->toString());
        throw net::DhtProtocolException {net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION,
                                         net::DhtProtocolException::GET_NO_INFOHASH};
    }
    const auto& now = scheduler.time();
    net::RequestAnswer answer {};
    auto st = store.find(hash);
    answer.ntoken = makeToken(node->getAddr(), false);
    answer.nodes4 = dht4.buckets.findClosestNodes(hash, now, TARGET_NODES);
    answer.nodes6 = dht6.buckets.findClosestNodes(hash, now, TARGET_NODES);
    if (st != store.end() && not st->second.empty()) {
        answer.values = st->second.get(query.where.getFilter());
        if (logger_)
            logger_->debug("[node {}] Sending {} values", node->toString(), answer.values.size());
    }
    return answer;
}

void
Dht::onGetValuesDone(const Sp<Node>& node, net::RequestAnswer& a, Sp<Search>& sr, const Sp<Query>& orig_query)
{
    if (not sr) {
        if (logger_)
            logger_->warn("[search unknown] Got reply to 'get'. Ignoring.");
        return;
    }

    /* if (logger_)
           logger_->d(sr->id, "[search %s] [node %s] Got reply to 'get' with %u nodes",
            sr->id.toString().c_str(), node->toString().c_str(), a.nodes4.size()+a.nodes6.size());*/

    if (not a.ntoken.empty()) {
        if (not a.values.empty() or not a.fields.empty()) {
            if (logger_)
                logger_->debug("[search {}] [node {}] Found {} values",
                               sr->id.to_view(),
                               node->toString(),
                               a.values.size());
            for (auto& getp : sr->callbacks) { /* call all callbacks for this search */
                auto& get = getp.second;
                if (not(get.get_cb or get.query_cb)
                    or (orig_query and get.query and not get.query->isSatisfiedBy(*orig_query)))
                    continue;

                if (get.query_cb) { /* in case of a request with query */
                    if (not a.fields.empty()) {
                        get.query_cb(a.fields);
                    } else if (not a.values.empty()) {
                        std::vector<Sp<FieldValueIndex>> fields;
                        fields.reserve(a.values.size());
                        for (const auto& v : a.values)
                            fields.emplace_back(
                                std::make_shared<FieldValueIndex>(*v, orig_query ? orig_query->select : Select {}));
                        get.query_cb(fields);
                    }
                } else if (get.get_cb) { /* in case of a vanilla get request */
                    std::vector<Sp<Value>> tmp;
                    for (const auto& v : a.values)
                        if (not get.filter or get.filter(*v))
                            tmp.emplace_back(v);
                    if (not tmp.empty())
                        get.get_cb(tmp);
                }
            }

            /* callbacks for local search listeners */
            /*std::vector<std::pair<ValueCallback, std::vector<Sp<Value>>>> tmp_lists;
            for (auto& l : sr->listeners) {
                if (!l.second.get_cb or (orig_query and l.second.query and not
            l.second.query->isSatisfiedBy(*orig_query))) continue; std::vector<Sp<Value>> tmp; for (const auto& v :
            a.values) if (not l.second.filter or l.second.filter(*v)) tmp.emplace_back(v); if (not tmp.empty())
                    tmp_lists.emplace_back(l.second.get_cb, std::move(tmp));
            }
            for (auto& l : tmp_lists)
                l.first(l.second, false);*/
        } else if (not a.expired_values.empty()) {
            if (logger_)
                logger_->warn("[search {}] [node {}] {} expired values",
                              sr->id.toString(),
                              node->toString(),
                              a.expired_values.size());
        }
    } else {
        if (logger_)
            logger_->warn("[node {}] No token provided. Ignoring response content.", node->toString());
        network_engine.blacklistNode(node);
    }

    if (not sr->done) {
        searchSendGetValues(sr);

        // Force to recompute the next step time
        scheduler.edit(sr->nextSearchStep, scheduler.time());
    }
}

net::RequestAnswer
Dht::onListen(Sp<Node> node, const InfoHash& hash, const Blob& token, size_t socket_id, const Query& query, int version)
{
    if (not hash) {
        if (logger_)
            logger_->warn("[node {}] Listen with no info_hash", node->toString());
        throw net::DhtProtocolException {net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION,
                                         net::DhtProtocolException::LISTEN_NO_INFOHASH};
    }
    if (not tokenMatch(token, node->getAddr())) {
        if (logger_)
            logger_->warn("[node {}] Incorrect token {} for 'listen'", node->toString(), hash.toString());
        throw net::DhtProtocolException {net::DhtProtocolException::UNAUTHORIZED,
                                         net::DhtProtocolException::LISTEN_WRONG_TOKEN};
    }
    Query q = query;
    storageAddListener(hash, node, socket_id, std::move(q), version);
    net::RequestAnswer answer {};
    answer.ntoken = makeToken(node->getAddr(), false);
    return answer;
}

void
Dht::onListenDone(const Sp<Node>& node, net::RequestAnswer& answer, Sp<Search>& sr)
{
    // if (logger_)
    //     logger_->d(sr->id, node->id, "[search %s] [node %s] Got listen confirmation",
    //            sr->id.toString().c_str(), node->toString().c_str(), answer.values.size());

    if (not sr->done) {
        const auto& now = scheduler.time();
        if (not answer.ntoken.empty())
            if (auto sn = sr->getNode(node))
                sn->refreshToken(answer.ntoken);
        searchSendGetValues(sr);
        scheduler.edit(sr->nextSearchStep, now);
    }
}

net::RequestAnswer
Dht::onAnnounce(Sp<Node> n,
                const InfoHash& hash,
                const Blob& token,
                const std::vector<Sp<Value>>& values,
                const time_point& creation_date)
{
    auto& node = *n;
    if (not hash) {
        if (logger_)
            logger_->warn("Put with no info_hash");
        throw net::DhtProtocolException {net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION,
                                         net::DhtProtocolException::PUT_NO_INFOHASH};
    }
    if (!tokenMatch(token, node.getAddr())) {
        if (logger_)
            logger_->warn("[node {}] Incorrect token {} for 'put'", node.toString(), hash.toString());
        throw net::DhtProtocolException {net::DhtProtocolException::UNAUTHORIZED,
                                         net::DhtProtocolException::PUT_WRONG_TOKEN};
    }
    {
        // We store a value only if we think we're part of the
        // SEARCH_NODES nodes around the target id.
        auto closest_nodes = buckets(node.getFamily()).findClosestNodes(hash, scheduler.time(), SEARCH_NODES);
        if (closest_nodes.size() >= TARGET_NODES and hash.xorCmp(closest_nodes.back()->id, myid) < 0) {
            if (logger_)
                logger_->warn("[node {}] Announce too far from the target. Dropping value.", node.toString());
            return {};
        }
    }

    auto created = std::min(creation_date, scheduler.time());
    for (const auto& v : values) {
        if (v->id == Value::INVALID_ID) {
            if (logger_)
                logger_->warn("[value {}] Incorrect value id", hash.to_view());
            throw net::DhtProtocolException {net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION,
                                             net::DhtProtocolException::PUT_INVALID_ID};
        }
        auto lv = getLocalById(hash, v->id);
        Sp<Value> vc = v;
        if (lv) {
            if (*lv == *vc) {
                storageRefresh(hash, v->id);
                if (logger_)
                    logger_->debug("[store {}] [node {}] Refreshed value {:016x}",
                                   hash.toString(),
                                   node.toString(),
                                   v->id);
            } else {
                const auto& type = getType(lv->type);
                if (type.editPolicy(hash, lv, vc, node.id, node.getAddr())) {
                    if (logger_)
                        logger_->debug("[store {}] Editing {}", hash.toString(), vc->toString());
                    storageStore(hash, vc, created, node.getAddr());
                } else {
                    if (logger_)
                        logger_->debug("[store {}] Rejecting edition of {} because of storage policy",
                                       hash.toString(),
                                       vc->toString());
                }
            }
        } else {
            // Allow the value to be edited by the storage policy
            const auto& type = getType(vc->type);
            if (type.storePolicy(hash, vc, node.id, node.getAddr())) {
                // if (logger_)
                //     logger_->d(hash, node.id, "[store %s] Storing %s", hash.toString().c_str(),
                //     std::to_string(vc->id).c_str());
                storageStore(hash, vc, created, node.getAddr());
            } else {
                if (logger_)
                    logger_->debug("[store {}] Rejecting storage of {}", hash.toString(), vc->toString());
            }
        }
    }
    return {};
}

net::RequestAnswer
Dht::onRefresh(Sp<Node> node, const InfoHash& hash, const Blob& token, const Value::Id& vid)
{
    using namespace net;

    if (not tokenMatch(token, node->getAddr())) {
        if (logger_)
            logger_->warn("[node {}] Incorrect token {} for 'put'", node->toString(), hash.toString());
        throw DhtProtocolException {DhtProtocolException::UNAUTHORIZED, DhtProtocolException::PUT_WRONG_TOKEN};
    }
    if (storageRefresh(hash, vid)) {
        if (logger_)
            logger_->debug("[store {}] [node {}] Refreshed value {:016x}", hash.toString(), node->toString(), vid);
    } else {
        if (logger_)
            logger_->debug("[store {}] [node {}] Got refresh for unknown value", hash.toString(), node->toString());
        throw DhtProtocolException {DhtProtocolException::NOT_FOUND, DhtProtocolException::STORAGE_NOT_FOUND};
    }
    return {};
}

bool
Dht::storageRefresh(const InfoHash& id, Value::Id vid)
{
    const auto& now = scheduler.time();
    auto s = store.find(id);
    if (s != store.end()) {
        // Values like for a permanent put can be refreshed. So, inform remote listeners that the value
        // need to be refreshed
        auto& st = s->second;
        if (not st.listeners.empty()) {
            if (logger_)
                logger_->debug("[store {}] {} remote listeners", id.to_view(), st.listeners.size());
            std::vector<Value::Id> ids = {vid};
            for (const auto& node_listeners : st.listeners) {
                for (const auto& l : node_listeners.second) {
                    if (logger_)
                        logger_->warn("[store {}] [node {}] Sending refresh",
                                      id.toString(),
                                      node_listeners.first->toString());
                    Blob ntoken = makeToken(node_listeners.first->getAddr(), false);
                    network_engine
                        .tellListenerRefreshed(node_listeners.first, l.first, id, ntoken, ids, l.second.version);
                }
            }
        }

        auto expiration = s->second.refresh(id, now, vid, types);
        if (expiration.first) {
            scheduler.cancel(expiration.first->expiration_job);
            if (expiration.second != time_point::max()) {
                expiration.first->expiration_job = scheduler.add(expiration.second,
                                                                 std::bind(&Dht::expireStorage, this, id));
            }
        }
        return true;
    }
    return false;
}

void
Dht::onAnnounceDone(const Sp<Node>& node, net::RequestAnswer& answer, Sp<Search>& sr)
{
    if (logger_)
        logger_->debug("[search {}] [node {}] Got reply to put!", sr->id.toString(), node->toString());
    searchSendGetValues(sr);
    sr->checkAnnounced(answer.vid);
}

struct DhtState
{
    unsigned v {1};
    InfoHash id;
    std::vector<NodeExport> nodes;
    std::vector<ValuesExport> values;

    MSGPACK_DEFINE_MAP(v, id, nodes, values)
};

void
Dht::saveState(const std::string& path) const
{
    DhtState state;
    state.id = myid;
    state.nodes = exportNodes();
    state.values = exportValues();
    std::ofstream file(path);
    msgpack::pack(file, state);
}

void
Dht::loadState(const std::string& path)
{
    if (logger_)
        logger_->debug("Importing state from {}", path);
    try {
        // Import nodes from binary file
        msgpack::unpacker pac;
        {
            // Read whole file
            std::ifstream file(path, std::ios::binary | std::ios::ate);
            if (!file.is_open()) {
                return;
            }
            auto size = file.tellg();
            file.seekg(0, std::ios::beg);
            pac.reserve_buffer(size);
            file.read(pac.buffer(), size);
            pac.buffer_consumed(size);
        }
        // Import nodes
        msgpack::object_handle oh;
        if (pac.next(oh)) {
            auto state = oh.get().as<DhtState>();
            if (logger_)
                logger_->debug("Importing {} nodes", state.nodes.size());
            if (state.id)
                myid = state.id;
            std::vector<Sp<Node>> tmpNodes;
            tmpNodes.reserve(state.nodes.size());
            for (const auto& node : state.nodes)
                tmpNodes.emplace_back(network_engine.insertNode(node.id, node.addr));
            importValues(state.values);
        }
    } catch (const std::exception& e) {
        if (logger_)
            logger_->warn("Error importing state from {}: {}", path, e.what());
    }
}

} // namespace dht
