#pragma once

#include "ProtobufMessages.hpp"
#include "SessionId.hpp"
#include "monitoring/MetricsReporter.hpp"
#include "network/PlayerSessionRegistry.hpp"

#include "message_protocol/MessageEndpoint.hpp"

#include <spdlog/spdlog.h>

#include <cstdint>
#include <deque>
#include <functional>
#include <memory>
#include <span>

namespace tw::net {

/**
 * Accepts player connections and routes their messages to the handlers the
 * zone server registers, translating peers into sessions on the way.
 */
class NetworkReceiver {
    PlayerSessionRegistry* m_session_registry;

    std::unique_ptr<msg::MessageEndpoint> m_endpoint;
    ProtobufMessages                      m_messages;

    std::deque<SessionId> m_new_sessions;

    NetworkMetricsReporter* m_metrics_reporter;

    uint64_t m_reported_bytes_received = 0;
    uint64_t m_reported_bytes_sent     = 0;

    void report_traffic();

public:
    NetworkReceiver(
        PlayerSessionRegistry* session_registry,
        int32_t udp_port,
        NetworkMetricsReporter* metrics_reporter
    );

    /**
     * Calls `handler` for every T that arrives, along with the session that
     * sent it.
     */
    template<typename T>
    void set_handler(std::function<void(SessionId, const T&)> handler) {
        m_messages.set_handler<T>(
            [this, handler = std::move(handler)](msg::PeerId peer, const T& message) {
                const SessionId session_id = m_session_registry->session_for_peer(peer);
                if(session_id == 0) {
                    spdlog::warn("Dropped a message from peer {}, which has no session", peer);
                    return;
                }

                handler(session_id, message);
            });
    }

    bool peek_new_session() const {
        return !m_new_sessions.empty();
    }

    SessionId pop_new_session() {
        auto session_id = m_new_sessions.front();
        m_new_sessions.pop_front();
        return session_id;
    }

    void update();

    template<typename T>
    size_t send_mesg(SessionId session_id, const T& mesg) {
        auto* session = m_session_registry->session(session_id);
        if(session == nullptr) {
            spdlog::warn("Cannot send to session {}, it is not registered", session_id);
            return 0;
        }

        auto send_r = m_messages.send(session->connection, mesg, true);
        if(!send_r) {
            spdlog::error("Failed to send to session {}: {}", session_id, send_r.error().message());
            return 0;
        }

        return msg::MessageHeader::SIZE + mesg.ByteSizeLong();
    }

    /**
     * Sends a message the caller has already framed, so that a writer holding
     * its own buffer does not have to be copied through an encoder.
     */
    size_t send_framed(SessionId session_id, std::span<const std::byte> message);
};

}
