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

#include <functional>
#include <mutex>

#include "callbacks.h"
#include "def.h"
#include "dht_interface.h"
#include "proxy.h"
#include "http.h"

#include <restinio/all.hpp>
#include <json/json.h>

#include <chrono>
#include <vector>
#include <functional>

namespace Json {
class Value;
}

namespace http {
class Resolver;
class Request;
} // namespace http

namespace dht {

class OPENDHT_PUBLIC DhtProxyClient final : public DhtInterface
{
public:
    DhtProxyClient();

    explicit DhtProxyClient(std::shared_ptr<crypto::Certificate> serverCA,
                            crypto::Identity clientIdentity,
                            std::function<void()> loopSignal,
                            const std::string& serverHost,
                            const std::string& userAgent = "",
                            const std::string& pushClientId = "",
                            const std::string& pushToken = "",
                            const std::string& pushTopic = "",
                            const std::string& pushPlatform = "",
                            std::shared_ptr<Logger> logger = {});

    void setHeaderFields(http::Request& request);

    virtual void setPushNotificationToken(const std::string& token) override;

    virtual void setPushNotificationTopic(const std::string& topic) override
    {
#ifdef OPENDHT_PUSH_NOTIFICATIONS
        notificationTopic_ = topic;
#else
        (void) topic;
#endif
    }

    virtual void setPushNotificationPlatform(const std::string& platform) override
    {
#ifdef OPENDHT_PUSH_NOTIFICATIONS
        platform_ = platform;
#else
        (void) platform;
#endif
    }

    virtual ~DhtProxyClient();

    /**
     * Get the ID of the node.
     */
    inline const InfoHash& getNodeId() const override { return myid; }
    void setOnPublicAddressChanged(PublicAddressChangedCb cb) override { publicAddressChangedCb_ = std::move(cb); }

    /**
     * Get the current status of the node for the given family.
     */
    NodeStatus getStatus(sa_family_t af) const override;
    NodeStatus getStatus() const override { return std::max(getStatus(AF_INET), getStatus(AF_INET6)); }

    /**
     * Performs final operations before quitting.
     */
    void shutdown(ShutdownCallback cb, bool) override;

    /**
     * Returns true if the node is running (have access to an open socket).
     *
     *  af: address family. If non-zero, will return true if the node
     *      is running for the provided family.
     */
    bool isRunning(sa_family_t af = 0) const override;

    /**
     * Get a value by asking the proxy and call the provided get callback when
     * values are found at key.
     * The operation will start as soon as the node is connected to the network.
     * @param cb a function called when new values are found on the network.
     *           It should return false to stop the operation.
     * @param donecb a function called when the operation is complete.
                     cb and donecb won't be called again afterward.
     * @param f a filter function used to prefilter values.
     */
    virtual void get(
        const InfoHash& key, GetCallback cb, DoneCallback donecb = {}, Value::Filter&& f = {}, Where&& w = {}) override;
    virtual void get(const InfoHash& key,
                     GetCallback cb,
                     DoneCallbackSimple donecb = {},
                     Value::Filter&& f = {},
                     Where&& w = {}) override
    {
        get(key, cb, bindDoneCb(std::move(donecb)), std::forward<Value::Filter>(f), std::forward<Where>(w));
    }
    virtual void get(const InfoHash& key,
                     GetCallbackSimple cb,
                     DoneCallback donecb = {},
                     Value::Filter&& f = {},
                     Where&& w = {}) override
    {
        get(key, bindGetCb(cb), std::move(donecb), std::forward<Value::Filter>(f), std::forward<Where>(w));
    }
    virtual void get(const InfoHash& key,
                     GetCallbackSimple cb,
                     DoneCallbackSimple donecb,
                     Value::Filter&& f = {},
                     Where&& w = {}) override
    {
        get(key, bindGetCb(cb), bindDoneCb(std::move(donecb)), std::forward<Value::Filter>(f), std::forward<Where>(w));
    }

    /**
     * Announce a value on all available protocols (IPv4, IPv6).
     *
     * The operation will start as soon as the node is connected to the network.
     * The done callback will be called once, when the first announce succeeds, or fails.
     * NOTE: For now, created parameter is ignored.
     */
    void put(const InfoHash& key,
             Sp<Value>,
             DoneCallback cb = nullptr,
             time_point created = time_point::max(),
             bool permanent = false) override;
    void put(const InfoHash& key,
             const Sp<Value>& v,
             DoneCallbackSimple cb,
             time_point created = time_point::max(),
             bool permanent = false) override
    {
        put(key, v, bindDoneCb(std::move(cb)), created, permanent);
    }

    void put(const InfoHash& key,
             Value&& v,
             DoneCallback cb = nullptr,
             time_point created = time_point::max(),
             bool permanent = false) override
    {
        put(key, std::make_shared<Value>(std::move(v)), std::move(cb), created, permanent);
    }
    void put(const InfoHash& key,
             Value&& v,
             DoneCallbackSimple cb,
             time_point created = time_point::max(),
             bool permanent = false) override
    {
        put(key, std::forward<Value>(v), bindDoneCb(std::move(cb)), created, permanent);
    }

    /**
     * @param  af the socket family
     * @return node stats from the proxy
     */
    NodeStats getNodesStats(sa_family_t af) const override;

    /**
     * @param  family the socket family
     * @return public address
     */
    std::vector<SockAddr> getPublicAddress(sa_family_t family = 0) override;

    /**
     * Listen on the network for any changes involving a specified hash.
     * The node will register to receive updates from relevent nodes when
     * new values are added or removed.
     *
     * @return a token to cancel the listener later.
     */
    virtual size_t listen(const InfoHash&, ValueCallback, Value::Filter = {}, Where = {}) override;

    virtual size_t listen(const InfoHash& key, GetCallback cb, Value::Filter f = {}, Where w = {}) override
    {
        return listen(
            key,
            [cb = std::move(cb)](const std::vector<Sp<Value>>& vals, bool expired) {
                if (not expired)
                    return cb(vals);
                return true;
            },
            std::forward<Value::Filter>(f),
            std::forward<Where>(w));
    }
    virtual size_t listen(const InfoHash& key, GetCallbackSimple cb, Value::Filter f = {}, Where w = {}) override
    {
        return listen(key, bindGetCb(std::move(cb)), std::forward<Value::Filter>(f), std::forward<Where>(w));
    }
    /*
     * This function relies on the cache implementation.
     * It means that there are no true cancel here, it keeps the caching in higher priority.
     */
    virtual bool cancelListen(const InfoHash& key, size_t token) override;

    /**
     * Call linked callback with a push notification
     * @param notification to process
     */
    PushNotificationResult pushNotificationReceived(const std::map<std::string, std::string>& notification) override;

    time_point periodic(const uint8_t*, size_t, SockAddr, const time_point& now) override;
    time_point periodic(
        const uint8_t* buf, size_t buflen, const sockaddr* from, socklen_t fromlen, const time_point& now) override
    {
        return periodic(buf, buflen, SockAddr(from, fromlen), now);
    }

    /**
     * Similar to Dht::get, but sends a Query to filter data remotely.
     * @param key the key for which to query data for.
     * @param cb a function called when new values are found on the network.
     *           It should return false to stop the operation.
     * @param done_cb a function called when the operation is complete.
               cb and done_cb won't be called again afterward.
     * @param q a query used to filter values on the remotes before they send a
     *          response.
     */
    virtual void query(const InfoHash& /*key*/,
                       QueryCallback /*cb*/,
                       DoneCallback /*done_cb*/ = {},
                       Query&& /*q*/ = {}) override
    {}
    virtual void query(const InfoHash& key, QueryCallback cb, DoneCallbackSimple done_cb = {}, Query&& q = {}) override
    {
        query(key, cb, bindDoneCb(std::move(done_cb)), std::forward<Query>(q));
    }

    /**
     * Get data currently being put at the given hash.
     */
    std::vector<Sp<Value>> getPut(const InfoHash&) const override;

    /**
     * Get data currently being put at the given hash with the given id.
     */
    Sp<Value> getPut(const InfoHash&, const Value::Id&) const override;

    /**
     * Stop any put/announce operation at the given location,
     * for the value with the given id.
     */
    bool cancelPut(const InfoHash&, const Value::Id&) override;

    void pingNode(SockAddr, DoneCallbackSimple&& /*cb*/ = {}) override {}

    virtual void registerType(const ValueType& type) override { types.registerType(type); }
    const ValueType& getType(ValueType::Id type_id) const override { return types.getType(type_id); }

    std::vector<Sp<Value>> getLocal(const InfoHash& k, const Value::Filter& filter) const override;
    Sp<Value> getLocalById(const InfoHash& k, Value::Id id) const override;

    /**
     * NOTE: The following methods will not be implemented because the
     * DhtProxyClient doesn't have any storage nor synchronization process
     */
    void insertNode(const InfoHash&, const SockAddr&) override {}
    void insertNode(const NodeExport&) override {}
    std::pair<size_t, size_t> getStoreSize() const override { return {}; }
    std::pair<size_t, size_t> getLocalStoreSize() const override { return {}; }
    std::vector<NodeExport> exportNodes() const override { return {}; }
    std::vector<ValuesExport> exportValues() const override { return {}; }
    void importValues(const std::vector<ValuesExport>&) override {}
    std::string getStorageLog() const override { return {}; }
    std::string getStorageLog(const InfoHash&) const override { return {}; }
    std::string getRoutingTablesLog(sa_family_t) const override { return {}; }
    std::string getSearchesLog(sa_family_t) const override { return {}; }
    std::string getSearchLog(const InfoHash&, sa_family_t) const override { return {}; }
    void dumpTables() const override {}
    std::vector<unsigned> getNodeMessageStats(bool) override { return {}; }
    void setStorageLimit(size_t) override {}
    virtual size_t getStorageLimit() const override { return 0; }
    void setLocalStorageLimit(size_t) override {}
    virtual size_t getLocalStorageLimit() const override { return STORAGE_LIMIT_UNLIMITED; }
    void connectivityChanged(sa_family_t) override { getProxyInfos(); }
    void connectivityChanged() override
    {
        getProxyInfos();
        loopSignal_();
    }

    void listenKeepIdle(uint32_t seconds) { listenKeepIdle_ = seconds; }
    inline uint32_t listenKeepIdle() { return listenKeepIdle_; }

private:
    /**
     * Start the connection with a server.
     */
    void startProxy();
    void stop();

    /**
     * Get informations from the proxy node
     * @return the JSON returned by the proxy
     */
    struct InfoState;
    void getProxyInfos();
    void queryProxyInfo(const std::shared_ptr<InfoState>& infoState,
                        const std::shared_ptr<http::Resolver>& resolver,
                        sa_family_t family);
    void onProxyInfos(const Json::Value& val, const sa_family_t family);
    SockAddr parsePublicAddress(const Json::Value& val);

    void opFailed();

    void handleExpireListener(const asio::error_code& ec, const InfoHash& key);

    struct Listener;
    struct OperationState;
    enum class ListenMethod {
        LISTEN,
        SUBSCRIBE,
        RESUBSCRIBE,
    };
    using CacheValueCallback
        = std::function<bool(const std::vector<std::shared_ptr<Value>>& values, bool expired, system_clock::time_point)>;

    /**
     * Send Listen with httpClient_
     */
    void sendListen(const restinio::http_request_header_t& header,
                    const CacheValueCallback& cb,
                    const Sp<OperationState>& opstate,
                    Listener& listener,
                    ListenMethod method = ListenMethod::LISTEN);
    void handleResubscribe(const asio::error_code& ec,
                           const InfoHash& key,
                           const size_t token,
                           std::shared_ptr<OperationState> opstate);

    void doPut(const InfoHash&, Sp<Value>, DoneCallbackSimple, time_point created, bool permanent);
    void handleRefreshPut(const asio::error_code& ec, InfoHash key, Value::Id id);

    /**
     * Initialize statusIpvX_
     */
    void getConnectivityStatus();
    /**
     * cancel all Listeners
     */
    void cancelAllListeners();

    std::atomic_bool isDestroying_ {false};

    std::string proxyUrl_;
    dht::crypto::Identity clientIdentity_;
    std::shared_ptr<dht::crypto::Certificate> serverCertificate_;
    std::string userAgent_ {"OpenDHT"};
    std::string pushClientId_;
    std::string pushSessionId_;

    mutable std::mutex lockCurrentProxyInfos_;
    NodeStatus statusIpv4_ {NodeStatus::Disconnected};
    NodeStatus statusIpv6_ {NodeStatus::Disconnected};
    NodeStats stats4_ {};
    NodeStats stats6_ {};
    SockAddr localAddrv4_;
    SockAddr localAddrv6_;
    SockAddr publicAddressV4_;
    SockAddr publicAddressV6_;
    std::atomic_bool launchConnectedCbs_ {false};
    PublicAddressChangedCb publicAddressChangedCb_ {};

    InfoHash myid {};

    // registred types
    TypeStore types;

    /*
     * ASIO I/O Context for sockets in httpClient_
     * Note: Each context is used in one thread only
     */
    asio::io_context httpContext_;
    mutable std::mutex resolverLock_;
    std::shared_ptr<http::Resolver> resolver_;

    mutable std::mutex requestLock_;
    std::map<unsigned, std::shared_ptr<http::Request>> requests_;
    /*
     * Thread for executing the http io_context.run() blocking call
     */
    std::thread httpClientThread_;

    /**
     * Store listen requests.
     */
    struct ProxySearch;

    mutable std::mutex searchLock_;
    size_t listenerToken_ {0};
    std::map<InfoHash, ProxySearch> searches_;

    /**
     * Keepalive idle time for listen
     */
    uint32_t listenKeepIdle_ {120};

    /**
     * Callbacks should be executed in the main thread.
     */
    std::mutex lockCallbacks_;
    std::vector<std::function<void()>> callbacks_;

    Sp<InfoState> infoState_;

    /**
     * Retrieve if we can connect to the proxy (update statusIpvX_)
     */
    void handleProxyConfirm(const asio::error_code& ec);
    std::unique_ptr<asio::steady_timer> nextProxyConfirmationTimer_;
    std::unique_ptr<asio::steady_timer> listenerRestartTimer_;

    /**
     * Relaunch LISTEN requests if the client disconnect/reconnect.
     */
    void restartListeners(const asio::error_code& ec);

    /**
     * Refresh a listen via a token
     * @param token
     */
    void resubscribe(const InfoHash& key, const size_t token, Listener& listener);

    /**
     * If we want to use push notifications by default.
     * NOTE: empty by default to avoid to use services like FCM or APN.
     */
    std::string deviceKey_ {};

    /**
     * Notification topic for ios notifications.
     */
    std::string notificationTopic_ {};

    /**
     * Platform for push notifications (supported android, ios, unifiedpush)
     */
    std::string platform_
#ifdef __ANDROID__
        {"android"};
#else
#ifdef __APPLE__
        {"ios"};
#else
        {};
#endif
#endif

    const std::function<void()> loopSignal_;

#ifdef OPENDHT_PUSH_NOTIFICATIONS
    std::string fillBody(bool resubscribe);
    void getPushRequest(Json::Value&) const;
#endif // OPENDHT_PUSH_NOTIFICATIONS

    Json::StreamWriterBuilder jsonBuilder_;
    std::unique_ptr<Json::CharReader> jsonReader_;

    std::shared_ptr<http::Request> buildRequest(const std::string& target = {});
};

} // namespace dht
