/*******************************************************************************
* Copyright (c) 2014, Jan Koester jan.koester@gmx.net
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
*      notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
*      notice, this list of conditions and the following disclaimer in the
*      documentation and/or other materials provided with the distribution.
* Neither the name of the <organization> nor the
*      names of its contributors may be used to endorse or promote products
*      derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

#pragma once

#include <array>
#include <iostream>
#include <atomic>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <memory>
#include <deque>
#include <map>
#include <unordered_map>
#include <set>
#include <cstring>
#include <algorithm>
#include <functional>

#include "config.h"
#include "rwlock.h"
#include "crypto/aes.h"
#include "crypto/x509.h"
#include "crypto/sha.h"
#include "crypto/tls.h"

#ifndef Windows
	typedef int SOCKET;
	typedef unsigned long ULONG_PTR;
	#include <sys/socket.h>
	#include <sys/types.h>
	#include <netdb.h>
	#include <arpa/inet.h>
	#include <netinet/in.h>
	#include <sys/un.h>
	#include <unistd.h>
	#include <fcntl.h>
	#include <errno.h>
#else
	#define WIN32_LEAN_AND_MEAN
	#ifndef NOMINMAX
	#define NOMINMAX
	#endif
	#include <WinSock2.h>
	#include <ws2tcpip.h>
	#include <mswsock.h>
	#include <BaseTsd.h>
	typedef SSIZE_T ssize_t;
	#ifndef MSG_DONTWAIT
	#define MSG_DONTWAIT 0
	#endif
#endif

namespace netplus {

	// Forward-declared (defined in eventapi.h, which itself includes this
	// header) so `quic` can hold one via unique_ptr without a circular
	// #include — see quic::_cc_socketwait's comment.
	class socketwait;

	enum sockettype { TCP = 0, UDP = 1, SSL = 2, QUIC = 3 };

#ifdef Windows
	enum IO_OPERATION { OP_READ = 0, OP_WRITE = 1 , OP_ACCEPT = 2 };

	struct AcceptContext {
		WSAOVERLAPPED ov{};
		SOCKET acceptSock = INVALID_SOCKET;

		static constexpr size_t ADDR_BUF_SZ =
			2 * (sizeof(SOCKADDR_STORAGE) + 16);

		alignas(void*) char addrBuf[ADDR_BUF_SZ]{};
	};
#endif

	// ---------------------------------------------------
	// buffer bleibt wie gehabt (Windows + Linux getrennt)
	// ---------------------------------------------------
#ifdef Windows
	class buffer {
	public:
		buffer(size_t s = BLOCKSIZE) {
			size = s;
			data.buf = new char[size];
			data.len = (ULONG)size;
			std::memset(&overlapped, 0, sizeof(WSAOVERLAPPED));
			operation = OP_READ;
		}

		buffer(const char *pointer,size_t s) {
			size = s;
			data.buf = new char[size];
			data.len = (ULONG)size;
			std::copy(pointer, pointer + size, data.buf);
			std::memset(&overlapped, 0, sizeof(WSAOVERLAPPED));
			operation = OP_READ;
		}

		~buffer() { delete[] data.buf; }

		bool   ptr=false;
		size_t size;

		WSAOVERLAPPED overlapped;
		WSABUF        data;
		IO_OPERATION  operation;
		void* owner = nullptr;

		buffer(const buffer&) = delete;
		buffer& operator=(const buffer&) = delete;
	};
#else
	class buffer {
	public:
		buffer(size_t bsize) {
			data.buf = new char[bsize]();
			size = bsize;
		}

		buffer(const char* bptr, size_t bsize) {
			data.buf = nullptr;
			data.ptr = bptr;
			size = bsize;
			ptr = true;
		}

		~buffer() {
			if (!ptr)
				delete[] data.buf;
		}

		size_t size;
		bool ptr = false;
		union {
			char* buf;
			const char* ptr;
		} data;
	};
#endif

	// ===================================================
	// ✅ SOCKET BASE CLASS (refactored)
	// ===================================================
	class socket {
	public:
		socket();
		socket(socket& socket) = delete;
		socket& operator=(const socket& socket) = delete;

		virtual ~socket();

		void setFlag(int flag, int value);
		void setTimeout(int msec);

		virtual void accept(std::unique_ptr<socket>& csock,bool nonblock) = 0;
		virtual void handshake_after_accept(){};
		virtual void  handshake_after_connect(){};

#ifdef Windows
		virtual void accept(LPFN_ACCEPTEX lpfnAcceptEx, std::unique_ptr<socket>& csock) = 0;
		virtual void prime_read() = 0;
		std::unique_ptr<buffer> _ReadBuffer;
		std::unique_ptr<buffer> _SendBuffer;
#endif

		virtual void bind() = 0;
		virtual void listen() = 0;
		virtual void setNonBlock() = 0;
		virtual void setBlock()=0;

		virtual int getMaxconnections() = 0;

		virtual size_t sendData(buffer& data, int flags = 0) = 0;
		virtual size_t recvData(buffer& data, int flags = 0) = 0;

		// Send raw transport bytes (bypasses TLS encryption for handshake traffic)
		// Default implementation calls sendData; SSL overrides to use tcp::sendData
		// This makes TLS layer work with both TCP and UDP transports
		virtual size_t sendRaw(buffer& data, int flags = 0) { return sendData(data, flags); }

		// Read raw transport data and buffer it (for SSL/TLS pre-decryption)
		// Returns bytes read. For plain TCP, this is a no-op (data is read
		// directly in recvData). For SSL, reads raw TCP into internal buffer
		// for later decryption.
		virtual size_t prime_recv(buffer& data, int flags = 0) { (void)data; (void)flags; return 0; }

		virtual bool getHandshakeDone() { return true; }
		virtual bool isClient() const { return false; }
		virtual bool hasBufferedData() const { return false; }
		virtual int  getSocketType() const { return _Type; }
		virtual bool hasPendingWrite() const { return _pendingIocpWrite.load(); }
		virtual void setPendingWrite(bool pending) { _pendingIocpWrite.store(pending); }

		// Push received data directly into buffer (for IOCP)
		virtual void pushReceivedData(const uint8_t* data, size_t len) { (void)data; (void)len; }

		virtual void connect(const std::string& addr, int port, bool nonblock = false) = 0;

		// Non-blocking connect bounded by timeout_ms, instead of the OS
		// default (which can be minutes) or blocking forever -- for callers
		// that need a configurable ceiling on how long an unreachable host
		// (dead service, wrong address, dropped SYNs) can stall them.
		// Throws NetException on failure or timeout; returns once connected.
		virtual void connectTimeout(const std::string& addr, int port, int timeout_ms) = 0;

		virtual void getAddress(std::string& addr) = 0;

		SOCKET fd();

		virtual socket& operator=(SOCKET sock) = 0;
		virtual void close();
		virtual void flush_out() = 0;

		// Stream callback (for QUIC stream data handling)
		using StreamCallback = std::function<void(socket*, uint64_t, const std::vector<uint8_t>&, bool)>;
		virtual void setStreamCallback(StreamCallback cb) { (void)cb; }

		// ===================================================
		// ✅ NEW ADDRESS STORAGE (Linux + Windows also usable)
		// ===================================================
		sockaddr_storage _Addr{};
		socklen_t        _AddrLen = sizeof(sockaddr_storage);

		// Old fields (keep for Windows compatibility or legacy code)
		ULONG_PTR        _SocketInfo = 0;
		size_t           _SocketInfoLen = 0;

		SOCKET           _Socket;
		int              _Type;
		ULONG_PTR        _Extension;

		bool _Bound    = false;
		bool _Listening = false;

		// Mark this socket as associated with an IOCP handle.
		// Only IOCP-bound sockets should use overlapped WSASend/WSARecv.
		void setIocpAssociated(bool v) { _iocpAssociated = v; }
		bool isIocpAssociated() const  { return _iocpAssociated; }

	protected:
		std::atomic<bool> _pendingIocpWrite{false};
		bool _iocpAssociated = false;
		// ✅ legacy helper still available
		void copyAddrInfo(ULONG_PTR* dest, ULONG_PTR src, size_t srclen);

		// ✅ NEW helper: store from addrinfo into sockaddr_storage
		void setAddrFromAI(const addrinfo* ai) {
			if (!ai || !ai->ai_addr || ai->ai_addrlen <= 0) return;
			std::memset(&_Addr, 0, sizeof(_Addr));
			_AddrLen = (socklen_t)std::min<size_t>(sizeof(_Addr), (size_t)ai->ai_addrlen);
			std::memcpy(&_Addr, ai->ai_addr, _AddrLen);
		}
	};

	// ===================================================
	// TCP / UDP / SSL remain same declarations
	// ===================================================
	class tcp : public socket {
	public:
		tcp();
		tcp(SOCKET sock);

		tcp(const netplus::tcp& ctcp) = delete;
		tcp& operator=(const tcp& socket) = delete;

		tcp(const std::string& uxsocket, int maxconnections, int sockopts);
		tcp(const std::string& addr, int port, int maxconnections, int sockopts);
		~tcp();
		void flush_out() override;

#ifdef Windows
		void accept(LPFN_ACCEPTEX lpfnAcceptEx, std::unique_ptr<socket>& csock);
		void prime_read();
#endif

		void accept(std::unique_ptr<socket>& csock,bool nonblock) override;
		void bind() override;
		void listen() override;
		void setNonBlock() override;
		void setBlock() override;
		netplus::tcp& operator=(SOCKET sock) override;

		int getMaxconnections() override;

		size_t sendData(buffer& data, int flags = 0) override;
		size_t recvData(buffer& data, int flags = 0) override;

		void connect(const std::string& addr, int port, bool nonblock = false) override;
		void connectTimeout(const std::string& addr, int port, int timeout_ms) override;

		void getAddress(std::string& addr) override;

	private:
		int         _Maxconnections;
		std::string _UxPath;
	};

	class udp : public socket {
	public:
		udp();
		udp(SOCKET sock);
		udp(const udp& cudp) = delete;

		udp(const std::string& uxsocket, int maxconnections,int sockopts);
		udp(const std::string& addr, int port, int maxconnections,int sockopts);
		~udp();
		void flush_out() override;

		void accept(std::unique_ptr<socket>& csock,bool nonblock) override;

#ifdef Windows
		void accept(LPFN_ACCEPTEX lpfnAcceptEx, std::unique_ptr<socket>& csock);
		void prime_read();
#endif

		void bind() override;
		void listen() override;
		void setNonBlock() override;
		void setBlock() override;

		udp& operator=(SOCKET socket) override;
		int getMaxconnections() override;

		size_t sendData(buffer& data, int flags = 0) override;
		size_t recvData(buffer& data, int flags = 0) override;

		// Send to stored _Addr destination (for unconnected sockets)
		size_t sendTo(buffer& data, int flags = 0);

		void connect(const std::string& addr, int port, bool nonblock = false) override;
		void connectTimeout(const std::string& addr, int port, int timeout_ms) override;
		void getAddress(std::string& addr) override;

		// ---- UDP GSO/GRO + batched I/O (Linux kernel offload) ----
		// Detect and enable GSO (UDP_SEGMENT) and GRO (UDP_GRO) if supported
		void probeGSOGRO(uint16_t segment_size = 1472);

		// Send multiple datagrams in one syscall.
		// Uses GSO if enabled and all datagrams are the same size,
		// otherwise falls back to sendmmsg/sendto loop.
		// |datagrams|: vector of (pointer, length) pairs.
		// |dest|: optional destination address (nullptr → use connected peer).
		// Returns number of datagrams successfully submitted.
		ssize_t sendBatch(const std::vector<std::pair<const uint8_t*, size_t>>& datagrams,
		                  sockaddr_storage* dest = nullptr, socklen_t dest_len = 0);

		// Receive multiple datagrams in one syscall (recvmmsg + GRO split).
		// |out|: filled with individual datagrams.
		// |max_count|: maximum datagrams to read (capped to 64).
		// Returns number of datagrams placed into |out|.
		size_t recvBatch(std::vector<std::vector<uint8_t>>& out, int max_count = 64);

		// Like recvBatch but also captures the peer address for each datagram.
		// |addrs|: filled with the peer sockaddr_storage for each datagram in |out|.
		size_t recvBatchAddr(std::vector<std::vector<uint8_t>>& out,
		                     std::vector<sockaddr_storage>& addrs,
		                     int max_count = 64);

		bool gsoEnabled() const { return _gso_enabled; }
		bool groEnabled() const { return _gro_enabled; }

	private:
		int         _Maxconnections;
		std::string _UxPath;

	protected:
		bool     _gso_enabled = false;
		bool     _gro_enabled = false;
		uint16_t _gso_segment_size = 1472;
	};

	class ssl : public tcp {
	public:
		// Use tls::CertificateBundle for compatibility
		using CertificateBundle = netplus::tls::CertificateBundle;

		using tcp::operator=;

		ssl(const std::map<std::string, CertificateBundle>& certs);
		ssl(const std::map<std::string, CertificateBundle>& certs, int sock);

		virtual ~ssl() = default;

		ssl(const std::map<std::string, CertificateBundle>& certs, const std::string& addr, int port, int maxconnections, int sockopts);

		void setCertificates(const std::map<std::string, CertificateBundle>& certs);

		void accept(std::unique_ptr<socket>& csock, bool nonblock) override;
		void handshake_after_accept() override { _tls.handshake_after_accept(); }
		void handshake_after_connect() override { _tls.handshake_after_connect(); }
		void connect(const std::string& addr, int port, bool nonblock = false) override;
		void close() override;

		size_t sendData(buffer& data, int flags = 0) override;
		size_t recvData(buffer& data, int flags = 0) override;
		
		// Send raw bytes without TLS encryption (for TLS handshake traffic)
		size_t sendRaw(buffer& data, int flags = 0) override { return tcp::sendData(data, flags); }

		// Read raw TCP data and buffer it for TLS processing
		size_t prime_recv(buffer& data, int flags = 0) override {
			size_t rcv = tcp::recvData(data, flags);
			if (rcv > 0) {
				_tls.pushReceivedData((const uint8_t*)data.data.buf, rcv);
			}
			return rcv;
		}

#ifdef Windows
		void accept(LPFN_ACCEPTEX lpfnAcceptEx, std::unique_ptr<socket>& csock) override;
		void prime_read();
#endif

		bool hasPendingWrite() const override {
			return _tls.hasPendingWrite() || socket::_pendingIocpWrite.load();
		}

		bool getHandshakeDone() override { return _tls.getHandshakeDone(); }
		bool isClient() const override { return _tls.is_client; }

		virtual void queueRaw(const std::vector<uint8_t>& v) { _tls.queueRaw(v); }
		virtual void queueRaw(std::vector<uint8_t>&& v) { _tls.queueRaw(std::move(v)); }
		virtual void queueRaw(const uint8_t* p, size_t n) { _tls.queueRaw(p, n); }

		void flush_out() override { _tls.flush_out(); }

		std::vector<uint8_t> readTlsRecordAsync() { return _tls.readTlsRecordAsync(); }

		void resetTLS() { _tls.resetTLS(); }

		// SNI accessor - get the requested hostname from client
		const std::string& getRequestedHostname() const { return _tls.getRequestedHostname(); }

		// Check if there's buffered data waiting to be processed
		bool hasBufferedData() const override { return _tls.hasBufferedData(); }

		// Push received data directly into the buffer (called by IOCP)
		void pushReceivedData(const uint8_t* data, size_t len) override {
			_tls.pushReceivedData(data, len);
		}

		// ALPN selection callback: given client-offered protocols, return the one to use.
		using AlpnSelectCallback = std::function<std::string(const std::vector<std::string>&)>;

		// Framing callback: given the negotiated ALPN, HTTP/2 stream ID (0 for HTTP/1.1),
		// response body, and content-type, produce the fully framed response.
		using FramingCallback = std::function<std::string(const std::string& alpn,
		                                                  uint32_t stream_id,
		                                                  const std::string& body,
		                                                  const std::string& content_type)>;

		void setAlpnSelectCallback(AlpnSelectCallback cb) {
			_tls.alpn_select_cb = std::move(cb);
		}

		void setFramingCallback(FramingCallback cb) {
			_framing_cb = std::move(cb);
		}

		const FramingCallback& getFramingCallback() const { return _framing_cb; }

		// Get the ALPN protocol negotiated during TLS handshake
		const std::string& getSelectedAlpn() const { return _tls.selected_alpn; }

		// Access to TLS state for advanced operations
		tls& getTls() { return _tls; }
		const tls& getTls() const { return _tls; }

	private:
		ssl(std::shared_ptr<const std::map<std::string, CertificateBundle>> shared_certs);
		tls _tls;
		FramingCallback _framing_cb;

#ifdef Windows
		AcceptContext _acpt;
		std::array<char, 2 * (sizeof(SOCKADDR_STORAGE) + 16)> _acceptBuf{};
		buffer* _iocpBuf = nullptr;
#endif

		friend class poll;
		friend class event;
		friend class EventWorker;
		friend class EventWorkerArgs;
		friend class TestSSL;
	};

	// ===================================================
	// QUIC Protocol Implementation (RFC 9000)
	// UDP-based transport with TLS 1.3 encryption
	// ===================================================
	class quic : public udp {
	public:
		// QUIC version constants
		static constexpr uint32_t QUIC_VERSION_1 = 0x00000001;
		static constexpr uint32_t QUIC_VERSION_2 = 0x6b3343cf;

		// Packet types (long header)
		enum class PacketType : uint8_t {
			Initial    = 0x00,
			ZeroRTT    = 0x01,
			Handshake  = 0x02,
			Retry      = 0x03
		};

		// Frame types
		enum class FrameType : uint64_t {
			PADDING             = 0x00,
			PING                = 0x01,
			ACK                 = 0x02,
			ACK_ECN             = 0x03,
			RESET_STREAM        = 0x04,
			STOP_SENDING        = 0x05,
			CRYPTO              = 0x06,
			NEW_TOKEN           = 0x07,
			STREAM              = 0x08, // 0x08-0x0f
			MAX_DATA            = 0x10,
			MAX_STREAM_DATA     = 0x11,
			MAX_STREAMS_BIDI    = 0x12,
			MAX_STREAMS_UNI     = 0x13,
			DATA_BLOCKED        = 0x14,
			STREAM_DATA_BLOCKED = 0x15,
			STREAMS_BLOCKED_BIDI= 0x16,
			STREAMS_BLOCKED_UNI = 0x17,
			NEW_CONNECTION_ID   = 0x18,
			RETIRE_CONNECTION_ID= 0x19,
			PATH_CHALLENGE      = 0x1a,
			PATH_RESPONSE       = 0x1b,
			CONNECTION_CLOSE    = 0x1c,
			CONNECTION_CLOSE_APP= 0x1d,
			HANDSHAKE_DONE      = 0x1e
		};

		// Connection state
		enum class ConnectionState {
			Idle,
			Handshaking,
			Connected,
			Draining,
			Closed
		};

		// Sent packet metadata for retransmission / loss detection
		struct SentPacket {
			uint64_t pn;             // packet number
			std::chrono::steady_clock::time_point sent_time;
			uint64_t stream_id;
			uint64_t stream_offset;
			size_t   data_length;    // stream payload length (for retransmit)
			size_t   sent_size;      // wire-level packet size (for CC accounting)
			bool     fin;            // carried FIN flag
			bool     ack_eliciting;  // true for STREAM frames
			std::vector<uint8_t> stream_data; // copy of payload for retransmit
			// True if this packet was originally sent as 0-RTT (long-header,
			// _early_keys) rather than 1-RTT (_app_keys) — see
			// checkLossAndRetransmit()'s branch on this for why a retransmit
			// must not just always re-encrypt under _app_keys.send.
			bool     sent_as_early_data = false;
		};

		// Stream state
		struct Stream {
			uint64_t stream_id;
			std::vector<uint8_t> recv_buffer;
			std::vector<uint8_t> send_buffer;
			uint64_t recv_offset = 0;
			uint64_t recv_contiguous = 0;  // bytes received contiguously from offset 0
			uint64_t recv_fin_offset = UINT64_MAX; // final stream size (set when FIN received)
			uint64_t send_offset = 0;
			uint64_t max_data_local = 16777216;   // 16MB
			uint64_t max_data_remote = 16777216;
			uint64_t max_data_local_committed = 16777216; // last advertised
			bool recv_fin = false;
			bool recv_dispatched = false;
			uint64_t recv_dispatched_offset = 0; // bytes already handed to the stream callback (incremental dispatch only)
			bool recv_complete = false; // all data up to FIN received
			bool send_fin = false;
			bool reset = false;
			// Absolute stream offset corresponding to recv_buffer[0]. Stays
			// 0 outside incremental dispatch (recv_buffer then holds the
			// stream from offset 0, as before); incremental dispatch
			// advances it to recv_dispatched_offset each time it trims the
			// already-dispatched prefix out of recv_buffer (see
			// processStreamFrame), so the buffer only ever holds
			// undispatched bytes instead of growing to the stream's full
			// cumulative size.
			uint64_t recv_base_offset = 0;
			uint64_t recv_frame_count = 0; // also used to detect a peer-initiated stream's first frame (processStreamFrame)
			// Track received byte ranges for gap detection
			std::set<std::pair<uint64_t,uint64_t>> recv_ranges;
		};

		// Constructors/Destructor
		//
		// Scaling a listener beyond one core (`sockopts` incl. SO_REUSEPORT):
		// a single quic listener's incoming datagrams are serviced by only
		// one thread at a time, by design — see epoll.cpp's comment at the
		// QUIC listener's EPOLLONESHOT registration for why (a measured 10x
		// regression from letting multiple threads drain the same socket
		// concurrently). To use more than one core for ingress, construct N
		// independent listener instances bound to the same address/port
		// with SO_REUSEPORT in `sockopts` (the kernel load-balances
		// datagrams across them by 4-tuple hash), and drive each with its
		// own netplus::event instance whose `threads` field is set to 1 —
		// not N instances sharing one `event`/epoll fd, which would just
		// reproduce the single-socket contention this avoids. Pin each
		// driving thread to a distinct core (pthread_setaffinity_np, as
		// event/epoll.cpp's worker threads already do) so shard N's
		// connections don't bounce between cores. Note RFC 9000 §9
		// Connection Migration is not shard-aware: a migrating connection's
		// datagrams can land on a different shard than the one holding its
		// registry entry.
		quic();
		quic(SOCKET sock);
		quic(const std::string& addr, int port, int maxconnections, int sockopts);
		quic(const std::map<std::string, ssl::CertificateBundle>& certs,
		     const std::string& addr, int port, int maxconnections, int sockopts);
		~quic();

		// Bring base class operator= into scope to avoid hiding warning
		using udp::operator=;

		// Socket interface overrides
		void flush_out() override;
		void accept(std::unique_ptr<socket>& csock, bool nonblock) override;
#ifdef Windows
		void accept(LPFN_ACCEPTEX lpfnAcceptEx, std::unique_ptr<socket>& csock) override;
		void prime_read() override;
#endif
		void connect(const std::string& addr, int port, bool nonblock = false) override;
		size_t sendData(buffer& data, int flags = 0) override;
		size_t recvData(buffer& data, int flags = 0) override;
		void close() override;
		
		// Handshake handling (for event loop integration)
		void handshake_after_accept() override;
		void pushReceivedData(const uint8_t* data, size_t len) override;
		bool hasBufferedData() const override { return !_recv_buffer.empty(); }

		// Read raw UDP data and buffer it for QUIC processing
		size_t prime_recv(buffer& data, int flags = 0) override {
			size_t rcv = udp::recvData(data, flags);
			if (rcv > 0) {
				pushReceivedData((const uint8_t*)data.data.buf, rcv);
			}
			return rcv;
		}

		// QUIC-specific methods
		void setVersion(uint32_t version) { _version = version; }
		uint32_t getVersion() const { return _version; }

		ConnectionState getConnectionState() const { return _conn_state.load(); }

		// Path MTU Discovery (DPLPMTUD, RFC 8899 / RFC 9000 §14.3-14.4).
		// Probing starts automatically once the handshake completes — no
		// separate enable call needed. This raises the ceiling the
		// post-handshake prober searches toward; the default (1472)
		// matches the fixed pre-discovery baseline exactly, so leaving it
		// untouched is a no-op. The peer's advertised max_udp_payload_size
		// transport parameter always applies as an additional hard ceiling
		// on top of this value. A listening socket's setting is copied to
		// every connection it accepts (see quic::accept()).
		void setPmtuCeiling(uint64_t ceiling) { _pmtud_ceiling = ceiling; }

		// Currently confirmed usable UDP payload size for this
		// connection's sends — 1472 until (and unless) PMTU discovery has
		// probed something larger.
		uint64_t getMaxUdpPayload() const { return _max_udp_payload; }

		// Stream management
		uint64_t openStream(bool bidirectional = true);
		void closeStream(uint64_t stream_id);
		size_t sendStreamData(uint64_t stream_id, const uint8_t* data, size_t len, bool fin = false);
		size_t recvStreamData(uint64_t stream_id, uint8_t* data, size_t max_len);
		bool hasStreamData(uint64_t stream_id) const;
		bool isStreamComplete(uint64_t stream_id) const;
		bool isStreamReset(uint64_t stream_id) const;

		// Abort a stream abnormally (RFC 9000 §19.4 RESET_STREAM). Use this
		// instead of sendStreamData(..., fin=true) when the body falls short
		// of the advertised Content-Length, so the peer sees a failed
		// transfer instead of silently accepting a truncated body as complete.
		void resetStream(uint64_t stream_id, uint64_t error_code = 0x0102);

		// Tell the peer to stop sending on a stream whose response we no
		// longer want (RFC 9000 §19.5 STOP_SENDING) — e.g. abandoning a
		// redundant/slow fetch once enough other responses arrived. Without
		// this, the peer has no signal that we stopped listening, so it may
		// not finish its send side for a long time (or ever), and neither
		// side ever retires the stream or reclaims its stream-count credit.
		void stopSending(uint64_t stream_id, uint64_t error_code = 0x0102);

		// Read raw UDP datagrams and process QUIC packets without consuming
		// stream data.  Use this in HTTP/3 client loops to pump the network
		// while reading specific streams via recvStreamData().
		void pumpNetwork(int flags = 0);

		// Connection ID management
		const std::vector<uint8_t>& getLocalConnectionId() const { return _local_cid; }
		const std::vector<uint8_t>& getRemoteConnectionId() const { return _remote_cid; }

		// Handshake status
		bool getHandshakeDone() override { return _handshake_complete; }

		// Certificate configuration (server mode)
		void setCertificates(const std::map<std::string, ssl::CertificateBundle>& certs);
		
		// SNI (Server Name Indication) support
		const std::string& getRequestedHostname() const { return _requested_hostname; }
		
		// Cipher suite information
		uint16_t getSelectedCipher() const { return _selected_cipher; }

		// RFC 9000 §8.1.3: a client-side address-validation token issued by
		// the server after a successful handshake (via NEW_TOKEN). Save it
		// with getStoredToken() and hand it to setToken() on a later, separate
		// connection attempt (before calling connect()) to skip that
		// attempt's Retry round trip. Empty if no NEW_TOKEN has arrived yet
		// (e.g. mid-handshake, or this is a server-role object).
		const std::vector<uint8_t>& getStoredToken() const { return _received_new_token; }
		// Must be called before connect(). Accepts a token obtained from
		// either getStoredToken() on a prior connection, or from processRetryPacket()
		// internally (an application never needs to call this for the
		// Retry case — only for reusing a NEW_TOKEN across connections).
		void setToken(const std::vector<uint8_t>& token) { _pending_token = token; }

		// RFC 8446 §4.6.1 session ticket, issued by the server after a
		// successful handshake (via NewSessionTicket), carrying a
		// resumption PSK. Save it with getSessionTicket() and hand it to
		// setSessionTicket() on a later, separate connection attempt
		// (before calling connect()) to request 0-RTT (early data) on that
		// attempt. An opaque blob to the application, like getStoredToken()/
		// setToken() above — combine both on the same reconnect to skip
		// Retry too. Empty if no NewSessionTicket has arrived yet.
		const std::vector<uint8_t>& getSessionTicket() const { return _received_session_ticket; }
		// Must be called before connect(). 0-RTT is only actually attempted
		// when connect() is used in nonblock mode — see sendStreamData()'s
		// early-data gate — since the default blocking connect() only
		// returns once the full handshake (and thus any early-data window)
		// has already completed.
		void setSessionTicket(const std::vector<uint8_t>& ticket) { _pending_session_ticket = ticket; }


		// Stream data callback (for HTTP/3 handling in application layer)
		// Override base class method - internally wraps to quic-specific callback
		void setStreamCallback(StreamCallback cb) override {
			_stream_callback = [cb](quic* q, uint64_t id, const std::vector<uint8_t>& data, bool fin) {
				cb(static_cast<socket*>(q), id, data, fin);
			};
		}

		// Opt-in, default off: dispatch bidi/uni stream data to the
		// callback as it arrives (one call per contiguous chunk, fin=true
		// only on the last one) instead of buffering the whole message and
		// firing once at FIN. Lets a server start responding before the
		// peer finishes sending, instead of the two being fully serialized.
		// Off by default because it changes an existing contract: with it
		// enabled, a single logical request can reach the callback as
		// several calls with partial data rather than exactly one call
		// with the complete buffer — callers written against the old
		// one-shot-per-stream assumption need to switch to accumulating
		// across calls (or opt out and keep today's behavior).
		void setIncrementalStreamDispatch(bool enable) { _incremental_stream_dispatch = enable; }

		// Opt-in, default off: seed every accepted child connection's
		// _dispatch_promoted as already true, instead of waiting for its
		// first dispatch to be observed slower than kSlowDispatchThreshold.
		// Without this, a connection's very first stream callback always
		// runs inline on the thread demuxing every other connection's
		// packets too (see scheduleDispatches()) — fine for callbacks that
		// never block, but a server whose message handlers do blocking I/O
		// (e.g. writing a large block to disk) can stall unrelated
		// connections' traffic for that first call's whole duration. Set
		// this when the handler's cost can't be bounded below the
		// threshold ahead of time, so no connection ever pays that cost
		// inline even once.
		void setStartDispatchPromoted(bool enable) { _start_dispatch_promoted = enable; }

		// Congestion control strategy for this connection. Default
		// (NewReno) is the existing, unchanged RFC-9002-conformant AIMD
		// behavior. BbrLite is an experimental, opt-in bandwidth-delay-
		// product based alternative (state declared further below,
		// implementation in onPacketAckedCC()/onPacketLostCC()/
		// cwndAllowsSend()) — deliberately NOT a full BBR2 implementation:
		// it has no send pacing (packets are still gated purely by cwnd,
		// same as NewReno — a real BBR pacer smooths sends across an RTT
		// instead, which this mode does not do), no gain-cycling PROBE_BW
		// phase, and no periodic PROBE_RTT re-measurement (min RTT is a
		// simple running minimum for the connection's lifetime, so it can
		// only ever go down, never adapt back up if the path's true min
		// RTT increases). It's a real, working starting point for
		// experimenting with BDP-based cwnd sizing on high-BDP/lossy
		// paths, not a drop-in production BBR2 replacement — that needs
		// dedicated pacing infrastructure and validation against real
		// variable-bandwidth networks that this project's test suite
		// (loopback only) cannot exercise. Call before the connection
		// starts sending data.
		enum class CongestionControlAlgorithm { NewReno, BbrLite };
		void setCongestionControlAlgorithm(CongestionControlAlgorithm algo) { _cc_algorithm = algo; }

		// Send data on a stream (vector overload for application use)
		void sendStreamData(uint64_t stream_id, const std::vector<uint8_t>& data, bool fin);

		// Drain incoming UDP datagrams and process them (ACKs,
		// flow-control updates) so long-running stream callbacks
		// don't starve the QUIC connection.
		void pumpIncoming();

	private:
		// Body of pumpIncoming(), assuming quic_mtx() on `this` is already
		// held by the caller (recursive_mutex, so re-entering it isn't
		// incorrect — just two extra uncontended lock/unlock cycles per
		// call, for the self-targeted case only, that add up when this
		// runs repeatedly from sendStreamData()'s congestion-window-wait
		// spin loop). Every other connection's own mutex (`t->quic_mtx()`
		// for `t != this` in the grouped-datagram loop) is still acquired
		// normally — those are genuinely different locks, not redundant.
		void pumpIncomingLocked();
		// Shared tail of pumpIncoming()/pumpIncomingLocked(): flush pending
		// flow-control frames and check for retransmits, both operating on
		// `this`'s own already-locked state.
		void flushPendingFlowControlAndCheckLoss();
		// Current encryption level for frame processing. Declared here
		// (rather than down with the other crypto state) because it's a
		// parameter type for protectPacket()/unprotectPacket() below, and a
		// nested type must be declared before it's named in a member
		// function's parameter list.
		// EarlyData (0-RTT) is asymmetric, unlike the other three levels:
		// only one direction is ever populated (client fills
		// _early_keys.send, server fills _early_keys.recv) — see
		// deriveEarlyDataKeys().
		enum class EncryptionLevel { Initial, Handshake, Application, EarlyData };

		// Key material + resolved ciphers for one direction (send or recv)
		// at one encryption level. Filled once, in deriveInitialKeys()/
		// deriveHandshakeKeys()/deriveApplicationKeys(), using _is_server —
		// every other call site asks for `.send` (packets we originate) or
		// `.recv` (packets we decrypt from the peer) and never branches on
		// _is_server again. Reused for the Initial level too even though
		// Initial is always AES-128 (RFC 9001 §5.2): key/hp then only fill
		// the first 16 bytes and leave the rest zeroed — the same zero-pad
		// convention Handshake/Application already use for the
		// AES-128-negotiated case, not a new one.
		struct LevelKeyMaterial {
			uint8_t key[32] = {0};
			uint8_t iv[12]  = {0};
			uint8_t hp[32]  = {0};
			std::unique_ptr<aes> aes_gcm;  // payload AEAD cipher, this direction
			std::unique_ptr<aes> aes_hp;   // header-protection ECB cipher, this direction

			void zero() {
				std::memset(key, 0, sizeof(key));
				std::memset(iv,  0, sizeof(iv));
				std::memset(hp,  0, sizeof(hp));
				aes_gcm.reset();
				aes_hp.reset();
			}
		};
		struct LevelKeys {
			LevelKeyMaterial send;
			LevelKeyMaterial recv;
			void zero() { send.zero(); recv.zero(); }
		};

		// Packet processing
		//
		// `pn_offset_hint` (default SIZE_MAX = unknown): when the caller has
		// already parsed this long-header packet's DCID/SCID/token/length
		// fields once (processIncomingPacket does, to find packet
		// boundaries in a possibly-coalesced datagram), it passes the
		// resulting packet-number field offset here so processInitialPacket/
		// processHandshakePacket/unprotectPacket don't each walk the same
		// fields again from scratch. Falls back to the old self-contained
		// parse when omitted or out of range — used for the "incomplete/
		// malformed header" edge cases where processIncomingPacket itself
		// couldn't determine it.
		void processIncomingPacket(const uint8_t* data, size_t len);
		void processLongHeaderPacket(const uint8_t* data, size_t len, size_t pn_offset_hint = SIZE_MAX);
		void processShortHeaderPacket(const uint8_t* data, size_t len);
		void processInitialPacket(const uint8_t* data, size_t len, size_t pn_offset_hint = SIZE_MAX);
		void processHandshakePacket(const uint8_t* data, size_t len, size_t pn_offset_hint = SIZE_MAX);
		// Server-only: decrypt and process an incoming 0-RTT (early data)
		// packet using _early_keys.recv. Shares the Application
		// packet-number space with 1-RTT (RFC 9000 §12.3) — tracked via
		// _app_pn_recv/_app_pn_recv_ranges, not a space of its own. Only
		// ever reached when _early_keys.recv is populated (see the dispatch
		// guard in processLongHeaderPacket()).
		void processZeroRTTPacket(const uint8_t* data, size_t len, size_t pn_offset_hint = SIZE_MAX);
		void processApplicationPacket(const uint8_t* data, size_t len);
		// Client-side: handle an incoming Retry packet (RFC 9000 §8.1.2) —
		// verify its RFC 9001 §5.8 Integrity Tag, capture the token + new
		// SCID, and resend the (already-built) Initial with the token.
		void processRetryPacket(const uint8_t* data, size_t len);

		// Frame processing
		// ack_eliciting is set to whether this frame counts as ack-eliciting
		// per RFC 9000 §13.2 (everything except ACK/ACK_ECN/CONNECTION_CLOSE/
		// PADDING) — set once, right after the frame type is decoded, so
		// every exit path (including error/abort paths) reports it
		// consistently for the frame type that was actually present. Left
		// untouched if the call returns before a frame type could even be
		// read (e.g. offset already >= len). Callers use this to decide
		// whether the packet as a whole should trigger an ACK; see
		// _app_ack_pending's call sites for why that distinction matters.
		void processFrame(const uint8_t* data, size_t len, size_t& offset, bool& ack_eliciting);
		void processCryptoFrame(const uint8_t* data, size_t len, size_t& offset);
		void processStreamFrame(const uint8_t* data, size_t len, size_t& offset, uint8_t frame_type);
		void processAckFrame(const uint8_t* data, size_t len, size_t& offset, bool is_ecn);
		// Sends CONNECTION_CLOSE(error_code, reason), drains the connection,
		// and sets offset = len so the caller's frame loop terminates —
		// the response to any malformed/truncated frame data parsing hits
		// (RFC 9000 §12.4: treat it as a connection error, not something to
		// silently skip past).
		void abortFrameParsing(size_t len, size_t& offset, uint64_t error_code, const char* reason);

		// If a stream has terminated in both directions (FIN/RESET on both
		// send and recv sides), erase it and — for a peer-initiated stream —
		// grant the peer fresh stream-count credit via MAX_STREAMS. Called
		// only from closeStream() and resetStream() — i.e. once the
		// application itself is done with the stream — never from passive
		// incoming-frame processing, since the application may still need
		// to read buffered data or check isStreamReset()/hasStreamData()
		// after a STREAM/RESET_STREAM frame updates send_fin/recv_fin.
		void retireStreamIfDone(uint64_t stream_id);
		// Iterator-based variant: used by closeStream()/resetStream(), which
		// already hold a valid iterator from their own _streams lookup, so
		// one logical "close a stream" call doesn't re-find the same
		// stream_id 2-3 times over (F48). `it` must be a valid, dereferenceable
		// iterator (checked by the caller) — this overload skips the
		// existence check the stream_id-only version does.
		void retireStreamIfDone(uint64_t stream_id, std::unordered_map<uint64_t, Stream>::iterator it);

		// Wire-only STOP_SENDING send, no existence check — shared by the
		// public stopSending() (which checks first) and closeStream() (which
		// already knows the stream exists from its own lookup).
		void sendStopSendingFrame(uint64_t stream_id, uint64_t error_code);

		// Loss detection and retransmission
		void checkLossAndRetransmit();
		void recordSentPacket(uint64_t pn, uint64_t stream_id, uint64_t stream_offset,
		                      const uint8_t* data, size_t len, bool fin, size_t wire_size = 0,
		                      bool sent_as_early_data = false);

		// Client-only 0-RTT send path, used by sendStreamData() in place of
		// its normal (heavily optimized, batched, congestion-window-gated)
		// logic whenever the handshake hasn't completed yet but early data
		// keys are ready. Deliberately simple/unbatched/uncongestion-gated:
		// 0-RTT only ever runs once, for a modest amount of data sent
		// optimistically before any handshake response, so keeping the
		// well-tested 1-RTT hot path untouched matters more here than raw
		// throughput. Packets are recorded via recordSentPacket(...,
		// sent_as_early_data=true) so checkLossAndRetransmit() retransmits
		// correctly (as 0-RTT while still possible, else falling back to
		// 1-RTT) if the server never acknowledges them — including the
		// case where the server declines 0-RTT entirely.
		size_t sendStreamDataEarly(uint64_t stream_id, const uint8_t* data, size_t len, bool fin);

		// Packet building
		std::vector<uint8_t> buildInitialPacket(const std::vector<uint8_t>& payload);
		std::vector<uint8_t> buildHandshakePacket(const std::vector<uint8_t>& payload);
		// Client-only: builds a 0-RTT (early data) long-header packet —
		// same layout as buildHandshakePacket() (no token field) but with
		// packet-type bits 0x01 and protected under _early_keys.send. Packet
		// number is drawn from the shared _app_pn_send counter (RFC 9000
		// §12.3 — 0-RTT and 1-RTT share the Application packet-number space).
		std::vector<uint8_t> buildZeroRTTPacket(const std::vector<uint8_t>& payload);
		std::vector<uint8_t> buildShortHeaderPacket(const std::vector<uint8_t>& payload);
		// Server-side: build and send a Retry packet (RFC 9000 §17.2.5) in
		// response to an Initial that arrived without a valid
		// address-validation token. `client_dcid`/`client_scid` are that
		// Initial's own DCID/SCID fields. No child connection exists yet —
		// sent directly on the listening socket to `peer_addr`.
		void sendRetryPacket(const std::vector<uint8_t>& client_dcid,
		                     const std::vector<uint8_t>& client_scid,
		                     sockaddr_storage peer_addr, socklen_t peer_len);

		// Server-side: build and send a Version Negotiation packet (RFC 9000
		// §6, §17.2.1) in response to a long-header packet whose version this
		// server doesn't speak. `client_dcid`/`client_scid` are that packet's
		// own DCID/SCID fields (echoed back, swapped — see RFC 9000 §17.2.1).
		// No child connection exists yet — sent directly on the listening
		// socket to `peer_addr`.
		void sendVersionNegotiation(const std::vector<uint8_t>& client_dcid,
		                            const std::vector<uint8_t>& client_scid,
		                            sockaddr_storage peer_addr, socklen_t peer_len);

		// Frame building
		std::vector<uint8_t> buildCryptoFrame(const std::vector<uint8_t>& data, uint64_t offset);
		std::vector<uint8_t> buildStreamFrame(uint64_t stream_id, const std::vector<uint8_t>& data,
		                                      uint64_t offset, bool fin);
		std::vector<uint8_t> buildAckFrame(uint64_t largest_ack, uint64_t ack_delay,
		                                    const std::set<std::pair<uint64_t,uint64_t>>& recv_ranges);
		std::vector<uint8_t> buildConnectionCloseFrame(uint64_t error_code, const std::string& reason);

		// PN range tracking helper
		static void trackRecvPN(uint64_t pn, std::set<std::pair<uint64_t,uint64_t>>& ranges);

		// Crypto operations (TLS 1.3 based)
		void deriveInitialKeys(const std::vector<uint8_t>& dcid, bool is_server);
		void deriveHandshakeKeys(const std::vector<uint8_t>& shared_secret);
		void deriveApplicationKeys();
		// RFC 8446 §7.1 / RFC 9001 §4.6.1 early (0-RTT) traffic secret,
		// derived from a resumption PSK rather than the (EC)DHE shared
		// secret. Unlike the three functions above, only one direction is
		// ever populated per call (`is_send`: true fills _early_keys.send —
		// client, offering the ticket; false fills _early_keys.recv —
		// server, having just accepted it) — see EncryptionLevel's comment.
		// `psk` is the ticket's resumption PSK (_early_data_psk); the
		// transcript used is whatever `_tls_transcript` holds at the call
		// site, which callers must ensure is exactly the ClientHello bytes
		// (RFC 8446: Derive-Secret(early_secret, "c e traffic", ClientHello)).
		void deriveEarlyDataKeys(const std::vector<uint8_t>& psk, bool is_send);

		// `mat` is the already-role-resolved key material for this call —
		// the send-direction struct (e.g. _app_keys.send) to protect/apply
		// HP, or the recv-direction struct (e.g. _app_keys.recv) to
		// unprotect — so these functions never branch on _is_server or
		// re-derive "which cached cipher" themselves; the caller's choice
		// of which LevelKeyMaterial to pass in *is* that decision. `level`
		// is needed only for the defensive fallback that builds an ephemeral
		// cipher when mat.aes_gcm/aes_hp is unexpectedly null (Initial is
		// always AES-128; Handshake/Application depend on _selected_cipher).
		std::vector<uint8_t> protectPacket(const std::vector<uint8_t>& header,
		                                   const std::vector<uint8_t>& payload,
		                                   uint64_t packet_number,
		                                   EncryptionLevel level, LevelKeyMaterial& mat);
		// Same encryption as protectPacket(), but writes into `out` (capacity
		// reused across calls) instead of returning a freshly allocated
		// vector — the primitive sendShortHeaderControlPacket() uses to stay
		// allocation-free.
		void protectPacketInto(const std::vector<uint8_t>& header,
		                        const std::vector<uint8_t>& payload,
		                        uint64_t packet_number,
		                        EncryptionLevel level, LevelKeyMaterial& mat,
		                        std::vector<uint8_t>& out);
		// `pn_offset_hint`: see processLongHeaderPacket()'s comment — reuses
		// an already-parsed long-header packet-number offset instead of
		// re-walking DCID/SCID/token/length fields when the caller has one.
		bool unprotectPacket(const uint8_t* data, size_t len,
		                     std::vector<uint8_t>& header,
		                     std::vector<uint8_t>& payload,
		                     uint64_t& packet_number,
		                     EncryptionLevel level, LevelKeyMaterial& mat,
		                     size_t pn_offset_hint = SIZE_MAX);

		// Header protection (send direction only — decrypt-side header
		// protection removal is handled inline inside unprotectPacket()).
		void applyHeaderProtection(std::vector<uint8_t>& packet, EncryptionLevel level, LevelKeyMaterial& mat);

		// Variable-length integer encoding/decoding (RFC 9000 Section 16).
		static size_t encodeVarInt(uint64_t value, uint8_t* out);
		// decodeVarInt is bounds-checked against `remaining` (the number of
		// bytes actually available at `data`): if the prefix byte's encoded
		// length would read past `remaining`, it never touches `data` and
		// reports failure via bytes_read == 0 — unambiguous, since a real
		// varint always consumes at least 1 byte. Callers MUST check for
		// this and abort parsing the current frame/field on failure; as a
		// second line of defense, every frame/field parsing loop in this
		// file also carries an explicit progress guard (offset must
		// advance every iteration or parsing aborts), so a call site that
		// forgets to check still can't spin forever or read past the end.
		static uint64_t decodeVarInt(const uint8_t* data, size_t remaining, size_t& bytes_read);
		static size_t varIntSize(uint64_t value);

		// Connection ID generation
		void generateConnectionId(std::vector<uint8_t>& cid, size_t len = 8);
		
		// Send packet to peer (uses sendto for child connections)
		ssize_t sendPacket(const uint8_t* data, size_t len);

		// Batched packet sending: accumulate packets, then flush via udp class
		void    batchPacket(const uint8_t* data, size_t len);
		ssize_t flushBatch();
		static constexpr size_t BATCH_MAX = 64;

		// Batched processing of 1-RTT (short-header) packets: when one
		// recvmmsg cycle delivers several packets for this connection,
		// decrypt and process them together so their packet numbers
		// accumulate into a single ACK range instead of each packet
		// firing its own standalone ACK.
		void processApplicationPacketsBatch(
			const std::vector<std::pair<const uint8_t*, size_t>>& packets);

		// Retry/NEW_TOKEN address-validation tokens (RFC 9000 §8.1). Both
		// token kinds share one AEAD-sealed wire format and this one
		// generate/validate pair — see generateToken()'s definition for the
		// exact layout. `original_dcid` is only meaningful when `is_retry`:
		// it's the client's *original* (pre-Retry) DCID, needed so Initial
		// key derivation and the original_destination_connection_id
		// transport parameter stay correct across the Retry round trip
		// (RFC 9001 §5.2 derives Initial keys from the client's *first*
		// Initial packet's DCID, which is not the same as the DCID field of
		// the retried Initial that carries this token).
		struct ParsedToken {
			bool valid = false;
			bool is_retry = false;
			std::vector<uint8_t> original_dcid;
		};
		std::vector<uint8_t> generateToken(bool is_retry, const std::vector<uint8_t>& original_dcid,
		                                    const sockaddr_storage& peer_addr, socklen_t peer_len) const;
		ParsedToken validateToken(const std::vector<uint8_t>& token,
		                          const sockaddr_storage& peer_addr, socklen_t peer_len) const;

		// Session tickets (RFC 8446 §4.6.1): a self-encrypted, AEAD-sealed
		// opaque blob carrying a resumption PSK, mirroring generateToken()/
		// validateToken()'s wire format exactly (12-byte nonce + AES-128-GCM
		// ciphertext + 16-byte tag) but sealed under _ticket_secret instead
		// of _token_secret, since this payload (a real key) is more
		// sensitive than an address-validation token. `psk`/`cipher_suite`
		// are the already-derived per-ticket PSK (see sendNewSessionTicket())
		// and the cipher suite it was issued under.
		struct ParsedTicket {
			bool valid = false;
			std::vector<uint8_t> ticket_id;
			uint16_t cipher_suite = 0;
			std::vector<uint8_t> psk;
			uint32_t max_early_data_size = 0;
		};
		std::vector<uint8_t> generateSessionTicket(const std::vector<uint8_t>& ticket_id,
		                                            uint16_t cipher_suite,
		                                            const std::vector<uint8_t>& psk,
		                                            uint32_t lifetime_secs,
		                                            uint32_t max_early_data_size) const;
		ParsedTicket validateSessionTicket(const std::vector<uint8_t>& ticket) const;

		// RFC 9001 §5.8 Retry Integrity Tag: fixed, RFC-specified key/nonce
		// (public, not secret — authenticates packet *format*, not origin;
		// real protection comes from the token above plus the transport
		// parameter cross-check). Computed identically on both ends
		// (server building a Retry, client verifying one), so this needs no
		// connection state beyond what's passed in.
		static std::array<uint8_t, 16> computeRetryIntegrityTag(
			const std::vector<uint8_t>& original_dcid,
			const uint8_t* header_and_token, size_t header_and_token_len);

		// TLS 1.3 handshake integration
		void processClientHello(const std::vector<uint8_t>& data);
		void processServerHello(const std::vector<uint8_t>& data);
		void processEncryptedExtensions(const std::vector<uint8_t>& data);
		void parseTransportParameters(const uint8_t* data, size_t len);
		void sendClientHello();
		void sendServerHello();
		void sendServerHandshakeFlight();
		void generateECDHEKeyPair();
		void computeSharedSecret(const std::vector<uint8_t>& peer_public_key);
		std::vector<uint8_t> buildClientHello();
		std::vector<uint8_t> buildTransportParameters();
		void processTLSMessages();
		void processFinished(const std::vector<uint8_t>& msg);
		void sendClientFinished();
		void sendHandshakeDone();
		// Server-only, RFC 9000 §8.1.3: issued once per connection right
		// after HANDSHAKE_DONE, letting this client skip Retry on a future
		// reconnect by presenting the token via setToken().
		void sendNewTokenFrame();
		// Server-only, RFC 8446 §4.6.1: issued once per connection right
		// after HANDSHAKE_DONE (alongside sendNewTokenFrame()), carrying a
		// resumption PSK the client can present on a later connection to
		// request 0-RTT. Sent as a CRYPTO frame at the Application level.
		void sendNewSessionTicket();
		// Client-only: handles the post-handshake NewSessionTicket message
		// (handshake type 0x04) arriving over the Application-level CRYPTO
		// stream — independently rederives the same resumption PSK the
		// server used and stores the ticket for getSessionTicket().
		void processNewSessionTicket(const std::vector<uint8_t>& msg);
		// HTTP/3-specific (SETTINGS + QPACK encoder/decoder streams); the
		// base implementation is a no-op since that framing is HTTP-layer
		// knowledge, not QUIC transport. libhttppp overrides this with the
		// real implementation — see libhttppp::Http3QuicSocket.
		virtual void sendControlStreams();
		// ALPN extension (0x0010) for the EncryptedExtensions message. Which
		// protocol string to advertise and how is HTTP-layer policy, not
		// QUIC transport, so the base implementation is a no-op (no ALPN
		// extension sent). libhttppp overrides this with the real
		// implementation — see libhttppp::Http3QuicSocket.
		virtual std::vector<uint8_t> buildAlpnExtension(const std::string& alpn);
		void completeHandshake();

		// Packet number handling
		uint64_t decodePacketNumber(uint32_t truncated_pn, size_t pn_len, uint64_t largest_pn);

		// State
		std::atomic<ConnectionState> _conn_state{ConnectionState::Idle};
		bool _is_server = false;
		bool _handshake_complete = false;
		bool _serverhello_sent = false;  // Track if ServerHello has been sent
		uint32_t _version = QUIC_VERSION_1;

		// SNI (Server Name Indication)
		std::string _requested_hostname;
		std::string _selected_alpn;  // ALPN protocol selected (e.g., "h3")
		
		// Selected cipher suite (0x1301 = TLS_AES_128_GCM_SHA256, 0x1302 = TLS_AES_256_GCM_SHA384)
		uint16_t _selected_cipher = 0x1301;  // Default to AES-128-GCM
		// Signature algorithm support parsed from ClientHello
		bool _sigalg_rsa_pss_sha256 = true;  // Safe default if extension missing
		bool _sigalg_rsa_pss_sha384 = false;
		
		// Connection IDs
		std::vector<uint8_t> _local_cid;
		std::vector<uint8_t> _remote_cid;
		std::vector<uint8_t> _original_dcid;
		// Retry-exchange CID, meaning depends on role: client-side, the
		// SCID it read off the actual Retry packet (kept to cross-check
		// against the server's later-echoed retry_source_connection_id
		// transport parameter, RFC 9000 §7.3); server-side (child
		// connection), the DCID field of the retried Initial that created
		// it — which is exactly the SCID this server put on its own Retry
		// packet, since the client always echoes whatever CID it was last
		// told to use as destination. Used to build that same transport
		// parameter server-side. Empty when no Retry occurred.
		std::vector<uint8_t> _retry_scid;

		// Client-only Retry/NEW_TOKEN state.
		std::vector<uint8_t> _pending_token;      // echoed in the next Initial's token field
		std::vector<uint8_t> _received_new_token; // last NEW_TOKEN frame payload, for getStoredToken()
		bool _retry_received = false;             // RFC 9000 §17.2.5.2: discard a second Retry
		// Padded CRYPTO-frame bytes from the very first Initial, retained
		// so a post-Retry resend can reuse them verbatim instead of
		// rebuilding the ClientHello — rebuilding would re-append to
		// _tls_transcript and regenerate the client random/ECDHE keypair,
		// corrupting the transcript hash the eventual Finished message is
		// computed over.
		std::vector<uint8_t> _initial_crypto_frame;

		// Client-only 0-RTT/session-ticket state (RFC 8446 4.6.1,
		// RFC 9001 4.6.1). _pending_session_ticket is offered in the next
		// ClientHello's pre_shared_key extension (set via
		// setSessionTicket(), mirroring _pending_token/setToken());
		// _received_session_ticket is the last NewSessionTicket this
		// connection processed, for getSessionTicket(). Opaque to the
		// application, same as the NEW_TOKEN pattern, but NOT simply the
		// server's wire ticket bytes internally: unlike the server (whose
		// ticket is self-encrypted, so any server instance can recover the
		// PSK from the ticket alone), a client presenting this on a later,
		// separate quic object has no shared state with the connection that
		// received it, so the PSK has to travel with the blob — the actual
		// format (built in processNewSessionTicket(), consumed in
		// buildClientHello()) is 2-byte cipher_suite + 2-byte psk length +
		// psk + the server's own ticket bytes.
		std::vector<uint8_t> _pending_session_ticket;
		std::vector<uint8_t> _received_session_ticket;
		// Resumption PSK recovered from _pending_session_ticket (client) or
		// from the validated ticket offered by the peer (server) — set
		// right before deriveEarlyDataKeys() is called. Cleared once no
		// longer needed (it is sensitive key material).
		std::vector<uint8_t> _early_data_psk;
		// Cipher suite the offered/accepted ticket was issued under —
		// determines which hash (SHA-256/SHA-384) the PSK binder and early
		// traffic secret use, independent of _selected_cipher (RFC 8446
		// 4.2.11: a PSK's hash must match the negotiated cipher's hash,
		// checked before accepting it — see processClientHello()).
		uint16_t _early_data_cipher_suite = 0x1301;
		bool _early_data_offered = false;   // client sent pre_shared_key + early_data
		bool _early_data_accepted = false;  // peer confirmed early_data (ServerHello PSK / EncryptedExtensions)
		bool _early_data_rejected = false;  // handshake completed without confirmation — stop using 0-RTT, retransmit unacked bytes at 1-RTT

		// Anti-amplification limit (RFC 9000 §8.1): until the peer's
		// address is validated, this endpoint must never send more than
		// 3x the bytes it has received from that address — otherwise it
		// could be used to amplify traffic toward a spoofed victim. Under
		// this library's always-Retry server policy, _address_validated is
		// set true at child-connection creation (a child is only ever
		// created once a valid token has already proven address
		// ownership), so the gate is open from birth for every connection
		// today — that's correct, not dead code: it's what makes a future
		// "make Retry conditional" policy safe without further changes,
		// and it still guards Initial/Handshake-space traffic in general
		// (e.g. also flips true on successfully processing a Handshake
		// packet, RFC 9000 §8.1's other validation trigger).
		bool _address_validated = false;
		uint64_t _bytes_sent_unvalidated = 0;
		uint64_t _bytes_received_unvalidated = 0;

		// Packet numbers (per packet number space)
		uint64_t _initial_pn_send = 0;
		uint64_t _initial_pn_recv = 0;
		uint64_t _handshake_pn_send = 0;
		uint64_t _handshake_pn_recv = 0;
		uint64_t _app_pn_send = 0;
		uint64_t _app_pn_recv = 0;
		// ACK cadence is batch-driven, not a per-packet counter/timer: one
		// ACK is sent after each recvmmsg batch is fully processed (see
		// _app_ack_pending below and pumpIncoming()'s comment on the
		// measured ~30x reduction in ACK-only sendto() calls this gives
		// vs. acking every packet). RFC 9000 §13.2.1's "at least every
		// second ack-eliciting packet" guidance is satisfied structurally
		// by that batch boundary rather than by counting.
		bool     _app_ack_pending = false; // Deferred ACK flag

		// Latest ECN counts reported by the peer via an ACK_ECN frame (RFC
		// 9000 §19.3.2). Not yet fed into a congestion-signal decision (see
		// onPacketAckedCC()/onPacketLostCC()) — captured now so the frame is
		// parsed correctly (offset must advance past all three fields) and
		// so the values are available for a future ECN-reactive CC without
		// another wire-format change.
		uint64_t _peer_ect0_count = 0;
		uint64_t _peer_ect1_count = 0;
		uint64_t _peer_ecn_ce_count = 0;

		// Received packet-number ranges (inclusive) for proper ACK encoding
		// Each pair is [lo, hi] representing contiguous received PNs.
		std::set<std::pair<uint64_t,uint64_t>> _initial_pn_recv_ranges;
		std::set<std::pair<uint64_t,uint64_t>> _handshake_pn_recv_ranges;
		std::set<std::pair<uint64_t,uint64_t>> _app_pn_recv_ranges;

		// Crypto key material (send/recv per level) — see LevelKeyMaterial's
		// comment near the top of the private section.
		LevelKeys _initial_keys;  // Initial: derived from connection ID, always AES-128
		LevelKeys _hs_keys;       // Handshake: size depends on negotiated cipher
		LevelKeys _app_keys;      // Application: size depends on negotiated cipher
		// 0-RTT (RFC 9001 §4.6.1): asymmetric — client only ever fills
		// .send, server only ever fills .recv (see EncryptionLevel's
		// comment). Populated by deriveEarlyDataKeys() when a session
		// ticket's PSK is offered (client) or accepted (server).
		LevelKeys _early_keys;

		// TLS handshake traffic secrets (for computing Finished)
		std::vector<uint8_t> _c_hs_traffic_secret;
		std::vector<uint8_t> _s_hs_traffic_secret;
		// Master secret from the main (EC)DHE key schedule (RFC 8446 §7.1) —
		// needed after the handshake completes to derive
		// resumption_master_secret for session tickets. Set at the end of
		// deriveApplicationKeys(); was previously just a local temporary.
		std::vector<uint8_t> _master_secret;
		// Derive-Secret(_master_secret, "res master", transcript through the
		// client's Finished) — set (server-side only) in processFinished(),
		// consumed by sendNewSessionTicket() to derive each ticket's PSK.
		std::vector<uint8_t> _resumption_master_secret;

		// TLS handshake state.
		//
		// _tls_transcript grows across the handshake (ClientHello,
		// ServerHello, ... Finished) and is re-hashed from byte 0 on each of
		// the ~8-10 sha256_hash()/sha384_hash() calls over it, rather than
		// fed into an incremental/streaming hash state that only processes
		// the newly-appended bytes each time. This is O(n^2) in transcript
		// hash calls rather than O(n), but n is a handshake's worth of
		// messages (typically low single-digit KB), the whole thing happens
		// once per connection (never in the per-packet hot path), and
		// sha256_hash()/sha384_hash() (crypto/sha.h) only expose a one-shot
		// API — adding and validating a new incremental variant carries real
		// correctness risk (this feeds Finished/CertificateVerify; a subtly
		// wrong incremental state would silently break every handshake) for
		// a gain that only shows up with unusually large certificate chains.
		std::vector<uint8_t> _tls_transcript;
		std::vector<uint8_t> _client_random;
		std::vector<uint8_t> _server_random;
		std::vector<uint8_t> _client_session_id;  // Session ID from ClientHello (for echo)
		std::vector<uint8_t> _ecdhe_private;
		std::vector<uint8_t> _ecdhe_public;
		std::vector<uint8_t> _ecdhe_shared;

		// Crypto buffers (per encryption level)
		std::vector<uint8_t> _crypto_recv_initial;
		std::vector<uint8_t> _crypto_recv_handshake;
		std::vector<uint8_t> _crypto_recv_app;
		std::vector<uint8_t> _crypto_send_initial;
		std::vector<uint8_t> _crypto_send_handshake;
		// Server-only: post-handshake CRYPTO stream (NewSessionTicket),
		// sent at the Application level — see sendNewSessionTicket().
		std::vector<uint8_t> _crypto_send_app;

		// CRYPTO stream reassembly - stores received ranges to handle out-of-order delivery
		struct CryptoRange {
			uint64_t offset;
			uint64_t length;
			std::vector<uint8_t> data;
		};
		std::vector<CryptoRange> _crypto_ranges_initial;
		std::vector<CryptoRange> _crypto_ranges_handshake;
		std::vector<CryptoRange> _crypto_ranges_app;
		uint64_t _crypto_offset_initial = 0;
		uint64_t _crypto_offset_handshake = 0;
		uint64_t _crypto_offset_app = 0;

		// Current encryption level for frame processing (EncryptionLevel
		// itself is declared near the top of the private section, above).
		EncryptionLevel _current_enc_level = EncryptionLevel::Initial;

		// Streams. unordered_map, not map: stream_id has no ordering
		// requirement anywhere this is used (both iteration sites in
		// quic.cpp visit "every stream"/"any stream with data" without
		// relying on sort order — grep confirms it), so there is no reason
		// to pay std::map's O(log n) tree lookups on the hot per-STREAM-
		// frame lookup path when std::hash<uint64_t> gives O(1) average
		// lookup for free — mirrors the same std::map -> unordered_map
		// reasoning already applied to _child_connections.
		std::unordered_map<uint64_t, Stream> _streams;

		// Bounded tracking for "has this stream_id been fully closed"
		// (prevents resurrection via a stale/retransmitted frame after
		// both sides have FIN'd — see retireStreamIfDone()/closeStream()).
		// A plain std::set<uint64_t> of every closed stream_id ever would
		// grow without bound over a connection's lifetime (permanent
		// memory plus ever-costlier lookups on every incoming STREAM
		// frame). QUIC stream IDs are allocated gaplessly within each of
		// the 4 (initiator, direction) classes (stream_id % 4), so
		// closures are almost always contiguous within a class too — track
		// a contiguous-closed-count per class plus a small set of any
		// out-of-order closures still waiting to be absorbed into that
		// count, instead of remembering every ID forever.
		struct ClosedStreamTracker {
			uint64_t contiguous = 0; // stream indices [0, contiguous) in this class are closed
			std::set<uint64_t> gaps; // out-of-order closed indices >= contiguous
		};
		std::array<ClosedStreamTracker, 4> _closed_stream_trackers;

		void markStreamClosed(uint64_t stream_id) {
			auto& t = _closed_stream_trackers[stream_id % 4];
			uint64_t idx = stream_id / 4;
			if (idx < t.contiguous) return; // already recorded
			if (idx == t.contiguous) {
				++t.contiguous;
				while (true) {
					auto it = t.gaps.find(t.contiguous);
					if (it == t.gaps.end()) break;
					t.gaps.erase(it);
					++t.contiguous;
				}
			} else {
				t.gaps.insert(idx);
			}
		}

		bool isStreamClosed(uint64_t stream_id) const {
			const auto& t = _closed_stream_trackers[stream_id % 4];
			uint64_t idx = stream_id / 4;
			return idx < t.contiguous || t.gaps.count(idx) > 0;
		}

		uint64_t _next_stream_id_bidi = 0;
		uint64_t _next_stream_id_uni = 2;

		// Flow control (connection level)
		uint64_t _max_data_local = 67108864;    // 64MB default
		uint64_t _max_data_remote = 67108864;
		uint64_t _max_data_local_committed = 67108864; // last advertised
		uint64_t _data_sent = 0;
		uint64_t _data_recv = 0;

		// Stream count limits (MAX_STREAMS)
		uint64_t _max_streams_bidi_local = 10000000;   // we allow peer to open
		uint64_t _max_streams_bidi_remote = 10000000;  // peer allows us to open
		uint64_t _max_streams_uni_local = 10000000;
		uint64_t _max_streams_uni_remote = 10000000;
		uint64_t _streams_bidi_opened = 0;        // streams we opened
		uint64_t _streams_uni_opened = 0;
		uint64_t _peer_streams_bidi_opened = 0;   // streams peer opened
		uint64_t _peer_streams_uni_opened = 0;

		// Initial per-stream limits from peer transport parameters
		uint64_t _peer_initial_max_stream_data_bidi_local = 16777216;
		uint64_t _peer_initial_max_stream_data_bidi_remote = 16777216;
		uint64_t _peer_initial_max_stream_data_uni = 16777216;

		/// Send a MAX_STREAM_DATA frame for the given stream
		void sendMaxStreamData(uint64_t stream_id, uint64_t max_data);
		/// Send a MAX_DATA frame for the connection
		void sendMaxData(uint64_t max_data);
		/// Send a DATA_BLOCKED frame
		void sendDataBlocked(uint64_t limit);
		/// Send a STREAM_DATA_BLOCKED frame
		void sendStreamDataBlocked(uint64_t stream_id, uint64_t limit);
		/// Send an ACK frame for 1-RTT packets
		void sendAppAck();

		// Builds a short-header 1-RTT packet around `frame`, applies AEAD
		// protection + header protection, and sends it — all using
		// persistent scratch buffers (_ctrl_header_scratch/_ctrl_pkt_scratch)
		// instead of buildShortHeaderPacket()'s return-by-value plus a
		// second protectPacket() allocation. Shared by every control-only
		// sender (ACK, MAX_DATA, MAX_STREAM_DATA, DATA_BLOCKED,
		// STREAM_DATA_BLOCKED) — mirrors the allocation-free pattern
		// sendStreamData(uint8_t*, ...) already uses for bulk data.
		//
		// `batch` (F20): when true, queues the packet via batchPacket()
		// instead of sending it immediately via sendPacket(). Only pass
		// true from call sites that are guaranteed to call flushBatch()
		// themselves before returning (currently: sendMaxStreamData() and
		// sendMaxData(), always invoked from one of the repeated "flush
		// pending flow control" blocks that already end in a flushBatch()
		// call — see their call sites). DATA_BLOCKED/STREAM_DATA_BLOCKED/
		// STOP_SENDING stay unbatched (the default): they fire from
		// scattered, not-uniformly-flush-bounded call sites, and batching
		// them without that guarantee risks a frame sitting queued
		// indefinitely — exactly the class of "peer never gets the
		// update, connection stalls" bug this project has repeatedly hit
		// elsewhere (see memory: dispatch staircase, mediadb1/authdb
		// unresponsive incidents).
		ssize_t sendShortHeaderControlPacket(const std::vector<uint8_t>& frame, bool batch = false);
		std::vector<uint8_t> _ctrl_header_scratch;
		std::vector<uint8_t> _ctrl_pkt_scratch;

		// Transport parameters
		uint64_t _max_idle_timeout = 30000;    // 30s
		uint64_t _max_udp_payload = 1472;      // Default: Ethernet-safe (1500 - IP/UDP headers).
		                                        // Also doubles as PMTUD's "current confirmed
		                                        // send size" — see pmtudTick().
		uint64_t _active_connection_id_limit = 2;

		// Path MTU Discovery ceilings (see setPmtuCeiling()). _pmtud_ceiling
		// is our own local cap (what we advertise as max_udp_payload_size
		// and search up to); _pmtud_peer_ceiling is the peer's advertised
		// value, applied as a hard cap in parseTransportParameters().
		uint64_t _pmtud_ceiling = 1472;
		uint64_t _pmtud_peer_ceiling = 1472;

		// ack_delay_exponent (RFC 9000 §18.2, transport parameter 0x0a).
		// _ack_delay_exponent is what we advertise (and what we use to
		// encode our own outgoing ACK Delay fields); _peer_ack_delay_exponent
		// is the peer's advertised value, applied when *decoding* an ACK
		// Delay field the peer sends us (RFC 9000 §13.2.5 — the field is
		// always interpreted using the sender's own advertised exponent,
		// never ours). Both default to the RFC-specified default of 3,
		// applying even if the peer's transport parameters never mention it.
		static constexpr uint64_t _ack_delay_exponent = 3;
		uint64_t _peer_ack_delay_exponent = 3;

		// Wall-clock time the earliest not-yet-acknowledged ack-eliciting
		// 1-RTT packet arrived (reset once sendAppAck() actually sends).
		// Used to encode a real ACK Delay instead of a hardcoded 0 — see
		// sendAppAck() and its call sites in processApplicationPacket(),
		// processApplicationPacketsBatch().
		std::chrono::steady_clock::time_point _app_ack_elicit_since{};

		// Receive/send queues
		std::vector<uint8_t> _recv_buffer;
		std::deque<std::vector<uint8_t>> _send_queue;


		// Certificate bundle for server mode
		std::map<std::string, ssl::CertificateBundle> _cert_map;
		ssl::CertificateBundle* _selected_cert = nullptr;

		// Secret backing Retry/NEW_TOKEN address-validation tokens (RFC
		// 9000 §8.1) — 32 random bytes, generated once per listener at
		// construction. Lives only on the listener; child connections
		// reach it via _parent (same `_parent ? _parent : this` idiom
		// pumpIncoming() already uses), since a child needs it to issue a
		// NEW_TOKEN after its own handshake completes, and the listener
		// needs it in accept() to build/validate tokens before any child
		// exists yet. No rotation: a process restart invalidates
		// outstanding tokens, which just costs affected clients one extra
		// Retry round trip — acceptable, and simpler than the alternative
		// of persisting/rotating a secret across restarts.
		std::vector<uint8_t> _token_secret;

		// Secret backing session tickets (RFC 8446 4.6.1), same
		// generated-once-per-listener/reached-via-_parent lifetime as
		// _token_secret above, but a distinct secret — a ticket AEAD-seals
		// the actual resumption PSK, a more sensitive payload than an
		// address-validation token, so it gets its own key rather than
		// reusing _token_secret under a different label.
		std::vector<uint8_t> _ticket_secret;

		// Anti-replay for session-ticket PSKs (RFC 8446 8.1): a ticket's
		// resumption PSK is single-use for early data. Keyed by ticket_id
		// (the same 16 random bytes doubling as the wire ticket_nonce),
		// value is the ticket's expiry — swept lazily on lookup, same
		// pattern as _recent_retries below. Guarded by _registry_mutex.
		std::unordered_map<std::string, std::chrono::steady_clock::time_point> _used_ticket_ids;

		// Mutex for this connection's own state (recursive: musl returns
		// EDEADLK on double-lock by same thread, which happens when
		// processFrame callbacks re-enter locking functions like
		// sendStreamData). Each quic object — listener or child — has and
		// uses its own; unlike the old design, a child's quic_mtx() no
		// longer delegates to the parent's, so different connections on
		// the same listener can be processed by different threads at the
		// same time (see _registry_mutex below for what still needs a
		// shared lock). NOTE: the QUIC listener fd itself is still
		// EPOLLONESHOT'd in epoll.cpp (and EV_ONESHOT'd in kqueue.cpp) — an
		// EPOLLEXCLUSIVE/no-ONESHOT variant was tried on the theory that
		// this per-connection mutex made it safe, measured as a 10x
		// throughput regression (splitting one large uncontended recvmmsg
		// batch into several smaller ones that then contend on a single
		// connection's mutex), and reverted; see epoll.cpp's comment at the
		// listener registration site for the full measurement. This
		// per-connection locking is groundwork for a future real fix (e.g.
		// sharding connections to dedicated threads/listener sockets, see
		// SO_REUSEPORT usage), not something that already removed
		// EPOLLONESHOT. Declared before _child_connections deliberately:
		// members are destroyed in reverse declaration order, and
		// ~quic() (run for every child when _child_connections' last
		// shared_ptr reference to it drops) locks quic_mtx() on itself —
		// harmless now that it's not shared with the parent, but keeping
		// the ordering avoids relying on that distinction.
		mutable std::recursive_mutex _quic_mutex;

		// Guards _child_connections only (registry membership), never a
		// connection's own state — that's quic_mtx()'s job. shared_mutex
		// because lookups (every packet routed to an existing connection)
		// vastly outnumber inserts/erases (new/closed connections): many
		// threads can look up different — or the same — connections
		// concurrently via a shared_lock, and only take the exclusive
		// unique_lock for the rare registry mutation. Declared before
		// _child_connections for the same destruction-order reason as
		// _quic_mutex above.
		//
		// Writer-preferring, not plain std::shared_mutex: a busy
		// connection's sendStreamData() congestion-wait loop calls
		// pumpIncomingLocked() up to ~2000x per stalled chunk, each taking
		// a shared_lock here — with glibc's default reader-preferring
		// policy that can starve out accept()'s exclusive lock (needed to
		// register a brand-new connection) for the entire transfer.
		// Reproduced live in production (mediadb cluster DEGRADED
		// flapping / "Failed to connect to node" errors).
		mutable netplus::WriterPreferringSharedMutex _registry_mutex;

		// Connection tracking for server mode (maps DCID to child quic*).
		// Keyed by the raw CID bytes as a std::string (see cidKey() in
		// quic.cpp) rather than std::vector<uint8_t>: same correctness
		// (CIDs are 0-20 variable-length bytes, so a fixed-width integer
		// key isn't safe), but an unordered_map avoids the O(log n)
		// byte-wise tree comparisons of the old std::map, and a CID that
		// fits in std::string's small-string buffer needs no heap
		// allocation for the lookup, unlike a std::vector key.
		// Holds shared_ptr rather than a raw pointer + separate owning
		// vector: each connection is registered under two keys (original
		// DCID and our local CID) that share ownership of the same
		// object, and a caller that finds an entry can copy the
		// shared_ptr out (a "pin") before releasing _registry_mutex,
		// keeping the connection alive across the handoff to its own
		// quic_mtx() even if another thread concurrently erases both
		// registry entries for it.
		std::unordered_map<std::string, std::shared_ptr<quic>> _child_connections;
		quic* _parent = nullptr;  // For child connections, pointer to parent listening socket

		// De-dupes Retry sends for a client's first flight: a client's
		// Initial CRYPTO data can span more than one UDP datagram (large
		// ClientHello — e.g. a post-quantum hybrid key share — needs more
		// than one 1200-byte packet), and each such datagram shares the
		// same DCID/token-less state and independently reaches this no-
		// token branch before any registry entry exists to catch it as
		// "already handled". Without this, each datagram mints its own
		// Retry with its own random SCID/token for what the client
		// considers one attempt; the client accepts exactly one of them
		// per RFC 9000 §17.2.5.2, but receiving two distinct, differently-
		// keyed retries for a single connection attempt has been observed
		// to leave real-world stacks (ngtcp2) unable to complete the
		// retried handshake at all — decrypting its first Initial keyed
		// off the accepted Retry then fails every time. Keyed by the
		// client's DCID (see cidKey()), guarded by _registry_mutex like
		// _child_connections. Entries older than kRetryDedupeWindow are
		// swept out lazily on lookup so this can't grow unbounded.
		std::unordered_map<std::string, std::chrono::steady_clock::time_point> _recent_retries;
		static constexpr std::chrono::seconds kRetryDedupeWindow{5};

		// Stream data callback for application layer (quic-specific internal type)
		using QuicStreamCallback = std::function<void(quic*, uint64_t, const std::vector<uint8_t>&, bool)>;
		QuicStreamCallback _stream_callback;
		bool _incremental_stream_dispatch = false; // see setIncrementalStreamDispatch()
		bool _start_dispatch_promoted = false; // see setStartDispatchPromoted()

		// Deferred stream dispatches — filled inside _quic_mutex,
		// invoked AFTER releasing it so callbacks can't block the
		// QUIC packet processing loop (prevents PING timeouts).
		struct PendingDispatch {
			uint64_t stream_id;
			std::vector<uint8_t> data;
			bool fin;
		};
		std::vector<PendingDispatch> _pending_dispatches;

		// Dispatches _stream_callback batches — inline on the calling
		// thread by default (same cost as calling the callback directly:
		// this is the common case, e.g. an echo server's fast reply, and
		// must not pay any queuing/thread-hop overhead for it), but
		// permanently promotes a connection to running its callbacks on a
		// shared thread pool instead, once one of its dispatches is
		// observed taking longer than kSlowDispatchThreshold. A callback
		// that takes a while (e.g. sending a large reply, which needs
		// several congestion-window-limited round trips) would otherwise
		// tie up whichever epoll worker thread is servicing accept() for
		// its whole duration, and enough concurrent slow callbacks can
		// exhaust the pool and starve unrelated connections'
		// handshakes/ACKs — but that's only worth paying the async-path
		// cost for once a connection has actually shown that behavior.
		// Never auto-demotes back to inline: a connection that needed it
		// once (e.g. mid-transfer) is likely to need it again for the
		// rest of that transfer, and flapping between paths would add
		// its own overhead.
		//
		// Per-connection FIFO ordering on the async path (a stream's
		// chunks must still arrive in order, with fin=true only on the
		// last one) is preserved by only ever having one runner active
		// per connection at a time: scheduleDispatches() enqueues a batch
		// and, if no runner is currently active for this connection,
		// submits one; that runner drains _dispatch_queue until empty
		// before clearing _dispatch_running, so a batch enqueued while a
		// runner is already active is picked up by that same runner
		// rather than racing a second one on another thread.
		//
		// Takes the shared_ptr as a parameter (rather than using
		// enable_shared_from_this) because callers already hold one — this
		// is only ever reached for child connections, always constructed
		// via make_shared (see quic::accept()); the listener and client
		// connections never call it.
		static constexpr std::chrono::microseconds kSlowDispatchThreshold{2000};
		static void scheduleDispatches(std::shared_ptr<quic> self,
		                                std::vector<PendingDispatch> dispatches);
		void runDispatchQueue(std::shared_ptr<quic> self);

		// F50: _dispatch_promoted is polled by whichever thread just
		// finished processing a packet (deciding inline-vs-pool dispatch)
		// while _dispatch_queue_mutex/_dispatch_queue/_dispatch_running are
		// concurrently touched by the pool worker thread draining the
		// queue — two independent synchronization points, each hit from a
		// different core, that used to sit close enough to share (or
		// border) a cache line with each other and with unrelated hot
		// connection state declared around them. Every write to one would
		// bounce the line and stall a concurrent read/write of the other
		// even though the two are logically unrelated. Padding each group
		// out to its own 64-byte boundary avoids that false-sharing
		// traffic. 64 covers every mainstream x86_64/ARM64 L1 line size;
		// std::hardware_destructive_interference_size was deliberately
		// not used here to avoid its libstdc++ ABI-tuning warning.
		alignas(64) std::atomic<bool> _dispatch_promoted{false};
		char _dispatch_promoted_pad[64 - sizeof(std::atomic<bool>)];

		alignas(64) std::mutex _dispatch_queue_mutex;
		std::deque<std::vector<PendingDispatch>> _dispatch_queue;
		std::atomic<bool> _dispatch_running{false};
		char _dispatch_queue_pad[64];

		// Handshake timeout tracking
		// Detects when client sends incomplete ClientHello with large gaps (likely packet loss)
		std::chrono::steady_clock::time_point _handshake_start_time;
		bool _incomplete_ch_detected = false;
		bool _handshake_timeout_triggered = false;
		static const int HANDSHAKE_TIMEOUT_MS = 5000;  // 5 second timeout for handshake

		// PING keepalive: last activity timestamp & interval
		std::chrono::steady_clock::time_point _last_activity;
		static constexpr int KEEPALIVE_INTERVAL_MS = 15000;  // Send PING every 15s of inactivity

		// Handshake retransmission: store last handshake flight for retransmit
		std::vector<std::vector<uint8_t>> _hs_flight_packets;  // Stored for retransmit
		std::chrono::steady_clock::time_point _hs_last_send;
		int _hs_retransmit_count = 0;
		static constexpr int HS_RETRANSMIT_MAX = 5;
		static constexpr int HS_RETRANSMIT_INTERVAL_MS = 200;  // Initial retransmit timer

		// F33 (Performance-Report): bewusst NICHT auf einen Ring-Buffer
		// (nach Packet-Number indiziert, O(1) amortisiert) umgestellt, wie
		// vom Report als Alternative zu diesem std::map (O(log n) je
		// Lookup/Insert/Erase) vorgeschlagen. Innerhalb dieses Umsetzungs-
		// zyklus wurden die beiden konkret gemessenen Kostenstellen bereits
		// gezielt behoben — F01 (recordSentPacket() nutzt jetzt einen
		// Buffer-Freelist statt pro Paket neu zu allokieren) und F36
		// (checkLossAndRetransmit() bricht früh ab, sobald keine
		// verlorenen Pakete mehr folgen können, statt immer die gesamte
		// Map zu durchlaufen). Ein vollständiger Wechsel der Datenstruktur
		// (mit den Konsequenzen für Lücken durch Paketverlust, PN-Space-
		// Rollover-Handling und alle ~15 Erase-Stellen, siehe
		// _sent_data_pool-Kommentar) ist ein Aufwand, der eine dedizierte
		// Umsetzung mit eigener Testabdeckung verdient, statt ihn beiläufig
		// an diesem bereits sehr umfangreichen Umsetzungszyklus
		// anzuhängen — die verbleibende Verbesserung gegenüber den beiden
		// bereits behobenen Kostenstellen ist zudem in der Praxis klein
		// (std::map-Operationen sind hier durch das Betriebssystem/die
		// AEAD-Kosten pro Paket ohnehin dominiert, nicht durch die
		// Baumtiefe bei realistischen In-Flight-Fenstergrößen).
		// Application packet retransmission (loss detection + recovery)
		std::map<uint64_t, SentPacket> _sent_packets;  // pn → metadata
		uint64_t _largest_acked_pn = UINT64_MAX;       // largest packet number ACKed

		// Free list of retired SentPacket::stream_data buffers, capacity
		// preserved. recordSentPacket() pops one instead of default-
		// constructing a fresh vector for every sent packet; every erase
		// site of _sent_packets (ACK processing, PTO, loss detection) pushes
		// its entry's buffer back here (capacity intact) instead of letting
		// it deallocate. Capped so a connection that briefly has very large
		// packets in flight doesn't pin that capacity in the pool forever.
		std::vector<std::vector<uint8_t>> _sent_data_pool;
		static constexpr size_t SENT_DATA_POOL_CAP = 256;

		// Returns `buf`'s capacity to the pool if there's room, else lets it
		// deallocate normally.
		void releaseSentDataBuffer(std::vector<uint8_t>&& buf) {
			if (_sent_data_pool.size() < SENT_DATA_POOL_CAP) {
				buf.clear();
				_sent_data_pool.push_back(std::move(buf));
			}
		}
		// RTT estimation (RFC 9002 §5)
		double _srtt = 0.0;           // smoothed RTT (seconds)
		double _rttvar = 0.0;         // RTT variance (seconds)
		double _latest_rtt = 0.0;     // most recent RTT sample (seconds) — RFC 9002 §5.3
		double _rto = 0.5;            // retransmission timeout (seconds), initial 500ms
		bool   _rtt_initialized = false;
		// Loss detection
		static constexpr uint64_t PACKET_THRESHOLD = 3;     // RFC 9002 kPacketThreshold
		static constexpr double   TIME_THRESHOLD   = 9.0/8; // RFC 9002 kTimeThreshold

		// Shared by both buildAckFrame() (encoder) and processAckFrame()
		// (decoder) so they can never disagree about how many ACK ranges
		// are acceptable. _app_pn_recv_ranges can grow one entry per lost
		// packet for the lifetime of the connection — this implementation
		// always retransmits lost data under a brand new packet number
		// (see checkLossAndRetransmit()), so the original, now-superseded
		// PN is a gap that will never be filled; it's a permanent range,
		// not a transient one healed by an eventual retransmission at the
		// same PN. A long-lived connection with any sustained loss can
		// therefore accumulate far more ranges than fit in one packet.
		// buildAckFrame() truncates to the MOST RECENT ranges (RFC 9000
		// §13.2.3 explicitly allows this: older ranges matter less since
		// the peer has likely already given up on/retransmitted that data
		// under other packet numbers by the time they'd be reported), and
		// processAckFrame() rejects anything beyond this as malformed —
		// keeping the two sides consistent by construction. Sized so
		// MAX_ACK_RANGES_PER_FRAME ranges (worst case 16 bytes each: two
		// 8-byte varints) plus the fixed fields comfortably fit within one
		// packet at the smallest allowed datagram size (1200 bytes).
		static constexpr uint64_t MAX_ACK_RANGES_PER_FRAME = 64;
		int _pto_count = 0;           // PTO back-off counter (RFC 9002 §6.2)

		// Throttle for pumpNetwork()'s opportunistic checkLossAndRetransmit()
		// call only (see pumpNetwork) — NOT used by the ACK-frame or
		// pumpIncoming() call sites, which must stay untouched since they
		// run precisely because something new arrived. Per-instance so
		// servicing multiple connections on one thread can't make one
		// connection's polling suppress another's.
		std::chrono::steady_clock::time_point _last_pump_loss_scan{};
		static constexpr std::chrono::microseconds _pump_loss_scan_interval{50};

		// Throttle for accept()'s "purge stale (Closed) connections" scan
		// (listener-only state — a child never calls accept()). That scan
		// is O(_child_connections.size()) under the exclusive
		// _registry_mutex lock; at 100k+ concurrent connections, running it
		// unconditionally on every accept() call (i.e. every drain of the
		// listener socket, potentially every event-loop tick) both burns
		// CPU and holds pumpIncoming()'s shared_lock lookups off the
		// registry longer than needed. A closed connection sitting in the
		// map for up to one interval longer than before is harmless — nothing
		// reads _conn_state==Closed entries as "alive" in the meantime.
		std::chrono::steady_clock::time_point _last_registry_purge{};
		static constexpr std::chrono::milliseconds _registry_purge_interval{100};

		// Flow control batching: avoid sending duplicate MAX_DATA/MAX_STREAM_DATA
		bool _pending_max_data = false;
		uint64_t _pending_max_data_value = 0;
		std::map<uint64_t, uint64_t> _pending_max_stream_data;

		// ---- Congestion Control (NewReno, RFC 9002) ----
		// RFC 9002 §7.2: kInitialWindow = min(10*max_datagram_size,
		// max(2*max_datagram_size, 14720)). The previous 10MB value
		// bypassed congestion control entirely for any transfer under
		// ~10MB, letting the sender burst far more data than the kernel's
		// UDP receive buffer (already maxed out at this host's
		// net.core.rmem_max of 4MB) can hold — measured 10,502 real
		// kernel-level RcvbufErrors (genuine packet loss, confirmed via
		// /proc/net/snmp) in one benchmark run, which in turn drove
		// spurious PACKET_THRESHOLD retransmissions.
		uint64_t _cwnd = std::min<uint64_t>(10 * _max_udp_payload,
		                                     std::max<uint64_t>(2 * _max_udp_payload, 14720));
		uint64_t _ssthresh = UINT64_MAX;           // slow start threshold
		uint64_t _bytes_in_flight = 0;             // unacknowledged bytes
		static constexpr uint64_t MIN_CWND = 2 * 1472; // minimum cwnd (2 * MTU)
		bool _in_recovery = false;                 // in congestion recovery
		uint64_t _recovery_start_pn = 0;           // PN that triggered recovery
		bool cwndAllowsSend(size_t packet_size) const;
		void onPacketSentCC(size_t packet_size);
		void onPacketAckedCC(size_t packet_size, uint64_t pn);
		void onPacketLostCC(size_t packet_size, uint64_t pn);

		// ---- BBR-lite: opt-in BDP-based cwnd sizing ----
		// See setCongestionControlAlgorithm()'s comment for what this is
		// and, importantly, is not. State only updated/consulted when
		// _cc_algorithm == BbrLite; computing it is cheap enough that
		// there's no need to gate the bookkeeping itself behind the flag,
		// only the cwnd-setting decisions in onPacketAckedCC()/
		// onPacketLostCC() that act on it.
		CongestionControlAlgorithm _cc_algorithm = CongestionControlAlgorithm::NewReno;
		static constexpr size_t BBR_BW_WINDOW = 10; // samples (~RTTs) the max-filter looks back over
		double _bbr_bw_samples[BBR_BW_WINDOW] = {0};
		size_t _bbr_bw_sample_idx = 0;
		std::chrono::steady_clock::time_point _bbr_last_delivery_time{};
		double _bbr_min_rtt = -1.0; // simple running minimum — see class comment on why this never re-adapts upward
		static constexpr double BBR_CWND_GAIN = 2.0;    // headroom over the raw estimated bandwidth-delay product
		static constexpr uint64_t BBR_MIN_CWND = 2 * 1472;
		// Recomputes cwnd from the current max-filtered bandwidth estimate
		// and min RTT; a no-op until both have at least one real sample.
		// `acked_bytes` is this ACK's contribution to the delivery-rate
		// sample.
		void bbrLiteUpdateCwnd(size_t acked_bytes);

		// ---- HyStart++ (RFC 9438) — slow-start overshoot protection ----
		// Plain NewReno slow start only ever exits via an actual loss,
		// letting cwnd overshoot the path's real capacity on high-BDP/high-
		// latency paths before the first loss event corrects it. HyStart++
		// watches for a sustained per-round RTT increase (a sign of queue
		// buildup ahead of loss) and reacts by capping growth (Conservative
		// Slow Start) instead of continuing to double cwnd every RTT.
		// MIN_RTT_THRESH guards against false triggers from ordinary
		// scheduling jitter on very-low-RTT paths (loopback, LAN) — the
		// same class of problem the loss-detection 5ms floor elsewhere in
		// this file was added for.
		static constexpr double HYSTART_MIN_RTT_THRESH = 0.004;  // 4ms
		static constexpr double HYSTART_MAX_RTT_THRESH = 0.016;  // 16ms
		static constexpr int HYSTART_N_RTT_SAMPLE = 8;
		static constexpr int HYSTART_CSS_ROUNDS = 5;
		static constexpr uint64_t HYSTART_CSS_GROWTH_DIVISOR = 4;

		bool _hystart_css_active = false;
		int _hystart_css_rounds_remaining = 0;
		uint64_t _hystart_window_end = 0;         // PN marking current round's end
		double _hystart_curr_round_min_rtt = -1.0;
		double _hystart_last_round_min_rtt = -1.0;
		int _hystart_rtt_sample_count = 0;

		// Called from processAckFrame() for every RTT sample while in slow
		// start or CSS (a no-op once in ordinary congestion avoidance).
		void hystartOnRttSample(double rtt_sample, uint64_t acked_pn);
		// Resets all HyStart++ round-tracking state — called whenever a
		// loss/recovery event redefines cwnd growth from scratch, so stale
		// round data from before the loss can't influence the decision
		// after it.
		void hystartReset();

		// Reused across the congestion-window-blocked wait retries in
		// sendStreamData() instead of constructing a fresh socketwait (and
		// its underlying epoll fd) on every ~1ms poll attempt. Profiling a
		// single cwnd-limited 5MB transfer showed tens of thousands of
		// epoll_create1/epoll_ctl/close cycles from that loop alone —
		// dwarfing the actual data-path cost and explaining most of the
		// gap to TCP throughput on loopback. Lazily constructed (most
		// connections finish without ever blocking on cwnd) so a server
		// handling many short-lived connections doesn't pay for an epoll
		// fd per connection that never needs one.
		std::unique_ptr<socketwait> _cc_socketwait;

		// ---- Path MTU Discovery (DPLPMTUD, RFC 8899 / RFC 9000 §14.3-14.4) ----
		// A probe is a PATH_CHALLENGE frame padded with PADDING to an exact
		// target wire size, sent as an ordinary 1-RTT packet. Success is
		// judged solely by receiving a PATH_RESPONSE echoing the challenge
		// data back — proof a datagram of that exact size both left this
		// host and was decrypted intact by the peer, which ACK/loss
		// tracking alone can't distinguish from "would've been fine at a
		// smaller size too". Deliberately kept out of
		// _sent_packets/onPacketLostCC: RFC 9000 §14.4 requires that
		// losing a PMTU probe NOT be treated as a congestion signal, since
		// a path MTU black hole has nothing to do with congestion.
		uint64_t _pmtud_lo = 0;          // largest confirmed-working size this search (0 = not yet seeded)
		uint64_t _pmtud_ruled_out = UINT64_MAX; // smallest size confirmed NOT to work (exclusive upper bound)
		uint64_t _pmtud_probe_size = 0;  // size of the probe currently in flight (0 = none outstanding)
		std::array<uint8_t, 8> _pmtud_probe_data{};
		std::chrono::steady_clock::time_point _pmtud_probe_sent;
		int _pmtud_attempts = 0;         // retries already made at the current probe size
		static constexpr int PMTUD_MAX_ATTEMPTS = 3;
		static constexpr uint64_t PMTUD_CONVERGE = 8; // stop searching once the [lo, hi) gap closes to this many bytes
		void pmtudTick();                // called once per pumpNetwork() iteration
		void pmtudSendProbe(uint64_t size);
		void pmtudOnProbeAcked();         // matching PATH_RESPONSE arrived

		// ACK tracking: need to send ACK for initial/handshake levels
		bool _need_initial_ack = false;
		bool _need_handshake_ack = false;

		// Persistent receive buffer for pumpNetwork() to avoid 65KB alloc/free per call
		buffer _pump_buf{65535};

		// Persistent header/payload scratch for unprotectPacket() — reused
		// across processInitialPacket()/processHandshakePacket()/
		// processApplicationPacket()/processApplicationPacketsBatch() calls
		// instead of each declaring its own fresh std::vector, avoiding two
		// heap allocations (resize() on each) per received packet. Safe to
		// share across all four: every entry point into packet processing
		// locks this connection's own quic_mtx() first (a recursive_mutex,
		// so no two threads and no nested call can be inside at once), and
		// each call fully consumes the buffers (parses payload, dispatches
		// frames) before returning — nothing outlives one call to hold a
		// reference into them across the next.
		std::vector<uint8_t> _recv_header_scratch;
		std::vector<uint8_t> _recv_payload_scratch;

		// Batch send buffer for flushBatch(). Packet bytes live in the flat
		// `_send_arena` (grown rarely, compacted on partial send) rather
		// than one heap-allocated vector per queued packet — BatchEntry is
		// just an {offset, len} view into it.
		struct BatchEntry {
			size_t offset;
			size_t len;
		};
		std::vector<uint8_t> _send_arena;
		size_t _send_arena_used = 0;
		std::vector<BatchEntry> _send_batch;

		// Each connection's own lock — see _quic_mutex's comment for why
		// this no longer delegates to the parent.
		std::recursive_mutex& quic_mtx() const {
			return _quic_mutex;
		}

		// Friend classes for event handling
		friend class poll;
		friend class event;
		friend class EventWorker;
		friend class EventWorkerArgs;
	};

	bool isIPAddr(const std::string &host);

} // namespace netplus
