/*******************************************************************************
* 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 "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
		};

		// 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;
			uint64_t recv_frame_count = 0; // diagnostic: STREAM frames received
			// Track received byte ranges for gap detection
			std::set<std::pair<uint64_t,uint64_t>> recv_ranges;
		};

		// Constructors/Destructor
		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; }


		// 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; }


		// 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:
		// 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.
		enum class EncryptionLevel { Initial, Handshake, Application };

		// 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
		void processIncomingPacket(const uint8_t* data, size_t len);
		void processLongHeaderPacket(const uint8_t* data, size_t len);
		void processShortHeaderPacket(const uint8_t* data, size_t len);
		void processInitialPacket(const uint8_t* data, size_t len);
		void processHandshakePacket(const uint8_t* data, size_t len);
		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);
		void processAckFrame(const uint8_t* data, size_t len, size_t& offset);
		// 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);

		// 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);

		// Packet building
		std::vector<uint8_t> buildInitialPacket(const std::vector<uint8_t>& payload);
		std::vector<uint8_t> buildHandshakePacket(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();

		// `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);
		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);

		// 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;

		// 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();
		void sendHttp3ControlStreams();
		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
		bool _h3_ctrl_sent = false;      // Track if HTTP/3 control streams have 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;

		// 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;
		uint32_t _app_unacked_count = 0;  // Delayed ACK counter
		bool     _app_ack_pending = false; // Deferred ACK flag

		// 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

		// TLS handshake traffic secrets (for computing Finished)
		std::vector<uint8_t> _c_hs_traffic_secret;
		std::vector<uint8_t> _s_hs_traffic_secret;

		// TLS handshake state
		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;

		// 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
		std::map<uint64_t, Stream> _streams;
		std::set<uint64_t> _closed_streams;  // IDs of fully-closed streams (prevent resurrection)
		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();

		// 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;

		// 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;

		// 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, and epoll.cpp for the EPOLLONESHOT removal this
		// enables). 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.
		mutable std::shared_mutex _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

		// 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()

		// 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);

		std::atomic<bool> _dispatch_promoted{false};
		std::mutex _dispatch_queue_mutex;
		std::deque<std::vector<PendingDispatch>> _dispatch_queue;
		std::atomic<bool> _dispatch_running{false};

		// 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

		// 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
		// RTT estimation (RFC 9002 §5)
		double _srtt = 0.0;           // smoothed RTT (seconds)
		double _rttvar = 0.0;         // RTT variance (seconds)
		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};

		// 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);

		// 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()
		struct BatchEntry {
			std::vector<uint8_t> data;
		};
		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
