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

#include <vector>
#include <fstream>
#include <iostream>
#include <cctype>
#include <ctime>
#include <string>
#include <thread>
#include <chrono>

#include <netplus/socket.h>
#include <netplus/exception.h>

#include <cmdplus/cmdplus.h>

#include "http.h"
#include "httpd.h"
#include "exception.h"
#include "config.h"
#include "hpack.h"
#include "qpack.h"

#define MAXDEFAULTCONN 1024
#define MBEDTLS_PSA_CRYPTO_CONFIG true

static constexpr uint8_t H2_FRAME_DATA = 0x00;
static constexpr uint8_t H2_FRAME_HEADERS = 0x01;
static constexpr uint8_t H2_FRAME_PRIORITY = 0x02;
static constexpr uint8_t H2_FRAME_RST_STREAM = 0x03;
static constexpr uint8_t H2_FRAME_SETTINGS = 0x04;
static constexpr uint8_t H2_FRAME_PING = 0x06;
static constexpr uint8_t H2_FRAME_GOAWAY = 0x07;
static constexpr uint8_t H2_FRAME_WINDOW_UPDATE = 0x08;
static constexpr uint8_t H2_FRAME_CONTINUATION = 0x09;

static constexpr uint8_t H2_FLAG_END_STREAM = 0x01;
static constexpr uint8_t H2_FLAG_ACK = 0x01;
static constexpr uint8_t H2_FLAG_END_HEADERS = 0x04;
static constexpr uint8_t H2_FLAG_PRIORITY = 0x20;

static constexpr size_t H2_FRAME_HEADER_LEN = 9;
static constexpr size_t H2_MAX_FRAME_SIZE = 16384;

// RFC 7540 §6.9.1: a flow-control window must never exceed 2^31-1.
static constexpr int64_t H2_MAX_WINDOW_SIZE = 0x7fffffff;

// How long a streaming response may sit blocked purely on a closed peer
// flow-control window (no WINDOW_UPDATE arriving) before we give up on
// it. Guards against a peer that silently stops crediting the window —
// without this, the stream (and its per-stream tempreq) stays parked in
// activeStreams forever and the connection looks hung from the client's
// side until it tears down the TCP connection itself.
static constexpr std::chrono::seconds H2_WINDOW_STALL_TIMEOUT{30};

static const char H2_CLIENT_PREFACE[] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
static constexpr size_t H2_CLIENT_PREFACE_LEN = 24;

static std::string h2BuildFrame(uint8_t type, uint8_t flags, uint32_t stream_id,
                                const std::string &payload) {
    std::string f(9 + payload.size(), '\0');
    uint32_t len = static_cast<uint32_t>(payload.size());
    f[0] = static_cast<char>((len >> 16) & 0xff);
    f[1] = static_cast<char>((len >> 8) & 0xff);
    f[2] = static_cast<char>(len & 0xff);
    f[3] = static_cast<char>(type);
    f[4] = static_cast<char>(flags);
    f[5] = static_cast<char>((stream_id >> 24) & 0x7f);
    f[6] = static_cast<char>((stream_id >> 16) & 0xff);
    f[7] = static_cast<char>((stream_id >> 8) & 0xff);
    f[8] = static_cast<char>(stream_id & 0xff);
    if (!payload.empty()) {
        std::memcpy(&f[9], payload.data(), payload.size());
    }
    return f;
}

// RFC 7540: SETTINGS_INITIAL_WINDOW_SIZE applies per-stream.
// Use 16 MB so large POST bodies don't stall.
static constexpr uint32_t H2_INITIAL_WINDOW_SIZE = 16u * 1024 * 1024;

static std::string h2BuildSettings() {
    // SETTINGS_MAX_CONCURRENT_STREAMS (0x03) = 100
    // SETTINGS_INITIAL_WINDOW_SIZE    (0x04) = H2_INITIAL_WINDOW_SIZE
    // SETTINGS_MAX_FRAME_SIZE         (0x05) = H2_MAX_FRAME_SIZE
    std::string p(18, '\0');
    p[0]  = 0x00; p[1]  = 0x03;
    p[2]  = 0x00; p[3]  = 0x00; p[4]  = 0x00; p[5]  = 0x64;
    p[6]  = 0x00; p[7]  = 0x04;
    p[8]  = static_cast<char>((H2_INITIAL_WINDOW_SIZE >> 24) & 0xff);
    p[9]  = static_cast<char>((H2_INITIAL_WINDOW_SIZE >> 16) & 0xff);
    p[10] = static_cast<char>((H2_INITIAL_WINDOW_SIZE >>  8) & 0xff);
    p[11] = static_cast<char>((H2_INITIAL_WINDOW_SIZE      ) & 0xff);
    p[12] = 0x00; p[13] = 0x05;
    p[14] = static_cast<char>((H2_MAX_FRAME_SIZE >> 24) & 0xff);
    p[15] = static_cast<char>((H2_MAX_FRAME_SIZE >> 16) & 0xff);
    p[16] = static_cast<char>((H2_MAX_FRAME_SIZE >>  8) & 0xff);
    p[17] = static_cast<char>((H2_MAX_FRAME_SIZE      ) & 0xff);
    return h2BuildFrame(H2_FRAME_SETTINGS, 0, 0, p);
}

static std::string h2BuildSettingsAck() {
    return h2BuildFrame(H2_FRAME_SETTINGS, H2_FLAG_ACK, 0, "");
}

// Build a WINDOW_UPDATE frame (RFC 7540 §6.9).
// stream_id==0 updates the connection window.
static std::string h2BuildWindowUpdate(uint32_t stream_id, uint32_t increment) {
    std::string p(4, '\0');
    p[0] = static_cast<char>((increment >> 24) & 0x7f);
    p[1] = static_cast<char>((increment >> 16) & 0xff);
    p[2] = static_cast<char>((increment >>  8) & 0xff);
    p[3] = static_cast<char>((increment      ) & 0xff);
    return h2BuildFrame(H2_FRAME_WINDOW_UPDATE, 0, stream_id, p);
}

// Connection-level WINDOW_UPDATE to raise the default 65535 window
// to match the per-stream INITIAL_WINDOW_SIZE.
static std::string h2ConnectionWindowBoost() {
    uint32_t boost = H2_INITIAL_WINDOW_SIZE - 65535u;
    if (boost == 0) return "";
    return h2BuildWindowUpdate(0, boost);
}

// Append a single frame directly to 'out' without creating a temp string.
// Avoids allocating + copying a temporary std::string per frame.
static void h2AppendFrameDirect(std::string &out, uint8_t type, uint8_t flags,
                                uint32_t stream_id,
                                const char *payload, size_t payload_size) {
    size_t old_size = out.size();
    out.resize(old_size + 9 + payload_size);
    char *f = &out[old_size];
    f[0] = static_cast<char>((payload_size >> 16) & 0xff);
    f[1] = static_cast<char>((payload_size >> 8) & 0xff);
    f[2] = static_cast<char>(payload_size & 0xff);
    f[3] = static_cast<char>(type);
    f[4] = static_cast<char>(flags);
    f[5] = static_cast<char>((stream_id >> 24) & 0x7f);
    f[6] = static_cast<char>((stream_id >> 16) & 0xff);
    f[7] = static_cast<char>((stream_id >> 8) & 0xff);
    f[8] = static_cast<char>(stream_id & 0xff);
    if (payload_size > 0) {
        std::memcpy(f + 9, payload, payload_size);
    }
}

// Append DATA frames chunked to max_frame_size.
// Uses direct memcpy to avoid temporary string copies per chunk.
static void h2AppendDataFrames(std::string &out, uint32_t stream_id,
                               const std::string &body, bool end_stream,
                               size_t max_frame_size = H2_MAX_FRAME_SIZE) {
    // Pre-reserve space for all frames: 9-byte header per chunk + body data
    size_t num_chunks = (body.size() + max_frame_size - 1) / max_frame_size;
    out.reserve(out.size() + body.size() + num_chunks * H2_FRAME_HEADER_LEN);
    size_t offset = 0;
    while (offset < body.size()) {
        size_t chunk = std::min(body.size() - offset, max_frame_size);
        bool last_chunk = (offset + chunk >= body.size());
        uint8_t flags = (last_chunk && end_stream) ? H2_FLAG_END_STREAM : 0;
        h2AppendFrameDirect(out, H2_FRAME_DATA, flags, stream_id,
                            body.data() + offset, chunk);
        offset += chunk;
    }
}

static std::string h2BuildResponse(uint32_t stream_id,
                                   uint16_t status_code,
                                   const std::string &body,
                                   const std::string &content_type,
                                   const std::vector<libhttppp::hpack::HeaderField> &extra = {},
                                   bool end_stream = true) {
    std::string out;
    std::string hpack = libhttppp::hpack::Encoder::encodeResponseHeaders(
        status_code, content_type, body.size(), extra);
    uint8_t hdr_flags = H2_FLAG_END_HEADERS;
    if (body.empty() && end_stream) hdr_flags |= H2_FLAG_END_STREAM;
    out += h2BuildFrame(H2_FRAME_HEADERS, hdr_flags, stream_id, hpack);
    if (!body.empty()) {
        h2AppendDataFrames(out, stream_id, body, end_stream);
    }
    return out;
}

static size_t h3EncodeVarInt(uint64_t value, uint8_t *out) {
    if (value < 64) {
        out[0] = static_cast<uint8_t>(value);
        return 1;
    }
    if (value < 16384) {
        out[0] = static_cast<uint8_t>(0x40 | (value >> 8));
        out[1] = static_cast<uint8_t>(value & 0xff);
        return 2;
    }
    if (value < 1073741824) {
        out[0] = static_cast<uint8_t>(0x80 | (value >> 24));
        out[1] = static_cast<uint8_t>((value >> 16) & 0xff);
        out[2] = static_cast<uint8_t>((value >> 8) & 0xff);
        out[3] = static_cast<uint8_t>(value & 0xff);
        return 4;
    }
    out[0] = static_cast<uint8_t>(0xc0 | (value >> 56));
    out[1] = static_cast<uint8_t>((value >> 48) & 0xff);
    out[2] = static_cast<uint8_t>((value >> 40) & 0xff);
    out[3] = static_cast<uint8_t>((value >> 32) & 0xff);
    out[4] = static_cast<uint8_t>((value >> 24) & 0xff);
    out[5] = static_cast<uint8_t>((value >> 16) & 0xff);
    out[6] = static_cast<uint8_t>((value >> 8) & 0xff);
    out[7] = static_cast<uint8_t>(value & 0xff);
    return 8;
}

static uint64_t h3DecodeVarInt(const uint8_t *data, size_t len, size_t &bytes_read) {
    if (len == 0) {
        bytes_read = 0;
        return 0;
    }
    uint8_t prefix = data[0] >> 6;
    uint64_t value = data[0] & 0x3f;
    if (prefix == 0) {
        bytes_read = 1;
        return value;
    }
    if (prefix == 1) {
        if (len < 2) { bytes_read = 0; return 0; }
        bytes_read = 2;
        return (value << 8) | data[1];
    }
    if (prefix == 2) {
        if (len < 4) { bytes_read = 0; return 0; }
        bytes_read = 4;
        value = (value << 8) | data[1];
        value = (value << 8) | data[2];
        value = (value << 8) | data[3];
        return value;
    }
    if (len < 8) { bytes_read = 0; return 0; }
    bytes_read = 8;
    for (int i = 1; i < 8; ++i) {
        value = (value << 8) | data[i];
    }
    return value;
}

static std::vector<uint8_t> h3BuildResponse(uint16_t status_code,
                                            const std::string &body,
                                            const std::string &content_type,
                                            const std::vector<libhttppp::qpack::HeaderField> &extra = {}) {
    std::vector<uint8_t> out;
    std::vector<uint8_t> headers = libhttppp::qpack::Encoder::encodeResponseHeaders(
        status_code, content_type, body.size(), extra);

    out.push_back(0x01);
    uint8_t len_buf[8];
    size_t len_bytes = h3EncodeVarInt(headers.size(), len_buf);
    out.insert(out.end(), len_buf, len_buf + len_bytes);
    out.insert(out.end(), headers.begin(), headers.end());

    if (!body.empty()) {
        out.push_back(0x00);
        len_bytes = h3EncodeVarInt(body.size(), len_buf);
        out.insert(out.end(), len_buf, len_buf + len_bytes);
        out.insert(out.end(), body.begin(), body.end());
    }

    return out;
}

libhttppp::HttpEvent::HttpEvent(std::vector<netplus::socket*> serversocket, int timeout, size_t h2OffloadThreads)
    : netplus::event(serversocket, timeout) {
    if (h2OffloadThreads > 0)
        _h2DispatchPool = std::make_unique<netplus::ThreadPool>(h2OffloadThreads);
    // Set the HTTP/3 stream callback on QUIC server sockets so that
    // child connections created by accept() inherit it.  The event loop
    // never creates a con / calls ConnectEvent for QUIC children,
    // so this is the only opportunity to wire up Http3StreamEvent.
    for (auto* sock : serversocket) {
        if (auto* quicsock = dynamic_cast<netplus::quic*>(sock)) {
            quicsock->setStreamCallback([this](netplus::socket *s,
                                               uint64_t stream_id,
                                               const std::vector<uint8_t> &data,
                                               bool fin) {
                this->Http3StreamEvent(s, stream_id, data, fin);
            });

            // Extract the QUIC port so HTTP/2 responses can advertise Alt-Svc
            int h3port = -1;
            sockaddr* sa = reinterpret_cast<sockaddr*>(&quicsock->_Addr);
            if (sa->sa_family == AF_INET)
                h3port = ntohs(reinterpret_cast<sockaddr_in*>(sa)->sin_port);
            else if (sa->sa_family == AF_INET6)
                h3port = ntohs(reinterpret_cast<sockaddr_in6*>(sa)->sin6_port);
            if (h3port > 0)
                _altSvcH3 = "h3=\":" + std::to_string(h3port) + "\"; ma=86400";
        }

        // Set ALPN selection and framing callbacks on SSL server sockets.
        // These are propagated to child sockets via ssl::accept().
        if (auto* sslsock = dynamic_cast<netplus::ssl*>(sock)) {
            // Attach the TLS session cache to the server socket.
            // Child sockets created by accept() inherit the cache pointer
            // so that abbreviated TLS 1.2 handshakes can be used.
            sslsock->getTls().session_cache = &_tlsSessionCache;

            // Prefer h2, fall back to http/1.1
            sslsock->setAlpnSelectCallback([](const std::vector<std::string>& protos) -> std::string {
                for (auto& p : protos) {
                    if (p == "h2") return "h2";
                }
                for (auto& p : protos) {
                    if (p == "http/1.1") return "http/1.1";
                }
                return protos.empty() ? "" : protos[0];
            });

            // Framing callback: intentionally empty.
            // Server SETTINGS + connection WINDOW_UPDATE boost are sent
            // exactly once from Http2RequestEvent (guarded by the
            // serverPrefaceSent flag) to avoid repeated boosts that
            // overflow the peer's connection send window past 2^31-1.
            sslsock->setFramingCallback([](const std::string& /*alpn*/,
                                           uint32_t /*stream_id*/,
                                           const std::string& /*body*/,
                                           const std::string& /*content_type*/) -> std::string {
                return "";
            });
        }
    }
}

struct nop
{
    template <typename T>
    void operator() (T const &) const noexcept { }
};

void libhttppp::HttpEvent::CreateConnection(std::shared_ptr<netplus::con> &res){
    res=std::make_shared<HttpRequest>(this);
}

void libhttppp::HttpEvent::RequestEvent(HttpRequest &/*curreq*/,const int /*tid*/,ULONG_PTR /*args*/){
}

void libhttppp::HttpEvent::ResponseEvent(HttpRequest &/*curreq*/,const int /*tid*/,ULONG_PTR /*args*/){

}

void libhttppp::HttpEvent::ConnectEvent(HttpRequest &/*curreq*/,const int /*tid*/,ULONG_PTR /*args*/){
}

void libhttppp::HttpEvent::DisconnectEvent(HttpRequest &/*curreq*/,const int /*tid*/,ULONG_PTR /*args*/){
}

bool libhttppp::HttpEvent::_dispatchH2Stream(HttpRequest &cureq,
                                             std::string &out,
                                             uint32_t sid,
                                             const std::vector<hpack::HeaderField> &decoded,
                                             const std::string &reqBody,
                                             const int tid, ULONG_PTR args) {
    // Create a per-stream temporary HttpRequest (HTTP/2 multiplexes
    // multiple streams on one connection — each needs its own state)
    auto tempreq_ptr = std::make_unique<HttpRequest>();
    HttpRequest &tempreq = *tempreq_ptr;
    tempreq.parseH2(decoded, sid);

    // Log dispatch for debugging
    std::string dbg_path;
    for (auto &hf : decoded) {
        if (hf.name == ":path") { dbg_path = hf.value; break; }
    }
    bool had_active = cureq.h2state().activeStreams.count(sid) > 0;
    std::cerr << "[H2-DISPATCH] sid=" << sid
              << " path=" << dbg_path
              << " activeAlready=" << had_active
              << std::endl;

    // Seed this stream's flow-control window now, synchronously, while we
    // hold the connection's event_mutex (see epoll.cpp's per-event lock
    // around dispatch). Offloaded requests hand the actual response off to
    // a background pool and don't call _finishH2Dispatch — which is what
    // normally sets this up — until the render completes; a fast peer can
    // send a WINDOW_UPDATE for this stream before that happens. Without an
    // entry here yet, that WINDOW_UPDATE finds nothing in peerStreamWindows
    // to credit (see the H2_FRAME_WINDOW_UPDATE case below) and is silently
    // discarded — permanently, since a well-behaved peer never repeats a
    // credit grant it believes already landed. The stream is then capped at
    // whatever the default window covers and stalls forever once that's
    // exhausted, with no further WINDOW_UPDATE ever coming to unstick it.
    cureq.h2state().peerStreamWindows.try_emplace(
        sid, cureq.h2state().peerInitialStreamWindow);

    // Store POST body in RecvData so HttpForm::parse() can access it
    if (!reqBody.empty()) {
        tempreq.RecvData.append(reqBody.data(), reqBody.size());
    }

    // Ensure content-length is set from actual body size (H2 framing
    // provides the length, but application code may rely on the header)
    if (!reqBody.empty() && !tempreq.getHeaderData("content-length")) {
        tempreq.setHeaderData("content-length")->push_back(std::to_string(reqBody.size()));
    }

    if (_h2DispatchPool && shouldOffloadH2Dispatch(tempreq, sid)) {
        int connFd = cureq.slots[0].csock ? cureq.slots[0].csock->fd() : -1;
        std::shared_ptr<netplus::con> connOwner =
            connFd >= 0 ? netplus::lookupConnection(connFd) : nullptr;
        if (connOwner) {
            // std::function (what ThreadPool::submit takes) requires its
            // target to be copy-constructible even though it's only ever
            // moved/invoked here — a bare move-captured unique_ptr fails
            // that static check. Box it in a shared_ptr (copy-constructible
            // regardless of the pointee) purely to satisfy that; ownership
            // stays effectively unique since nothing else ever sees this
            // particular shared_ptr.
            auto trBox = std::make_shared<std::unique_ptr<HttpRequest>>(std::move(tempreq_ptr));
            _h2DispatchPool->submit(
                [this, connOwner, trBox, sid, tid, args]() mutable {
                    // The ONLY blocking call in this task — touches
                    // *trBox (private to this task) only, never
                    // cureq/h2state().
                    RequestEvent(**trBox, tid, args);

                    HttpRequest &cureq2 = static_cast<HttpRequest&>(*connOwner);
                    std::lock_guard<std::recursive_mutex> lk(cureq2.event_mutex);
                    if (!cureq2.slots[0].csock)
                        return; // connection torn down while we were blocked

                    std::string out2;
                    _finishH2Dispatch(cureq2, out2, sid, std::move(*trBox), tid, args);
                    if (!out2.empty())
                        cureq2.SendData.append(out2.data(), out2.size());
                    _flushSendDataNow(cureq2);
                    if (cureq2.SendData.pos < cureq2.SendData.size())
                        netplus::requestWritablePoll(cureq2);
                });
            return true;
        }
        // Connection lookup failed (shouldn't happen while we're still
        // holding event_mutex for this call) — fall through to the
        // synchronous path rather than silently dropping the request.
    }

    // Dispatch to user's virtual RequestEvent
    RequestEvent(tempreq, tid, args);
    _finishH2Dispatch(cureq, out, sid, std::move(tempreq_ptr), tid, args);
    return false;
}

void libhttppp::HttpEvent::_finishH2Dispatch(HttpRequest &cureq,
                                             std::string &out,
                                             uint32_t sid,
                                             std::unique_ptr<HttpRequest> tempreq_ptr,
                                             const int tid, ULONG_PTR args) {
    HttpRequest &tempreq = *tempreq_ptr;

    // Read structured response info from :res-* pseudo-headers
    auto *resValid = tempreq.getHeaderData(":res-valid");
    if (resValid && resValid->getfirstValue()) {
        // Extract status code from :res-status (e.g. "200 OK" -> 200)
        uint16_t status_code = 200;
        auto *resStatus = tempreq.getHeaderData(":res-status");
        if (resStatus && resStatus->getfirstValue()) {
            try { status_code = static_cast<uint16_t>(std::stoi(resStatus->getfirstValue()->getvalue())); }
            catch (...) { status_code = 200; }
        }

        // Content type
        std::string content_type;
        auto *resCT = tempreq.getHeaderData(":res-content-type");
        if (resCT && resCT->getfirstValue())
            content_type = resCT->getfirstValue()->getvalue();

        // Content length
        size_t content_length = 0;
        auto *resCL = tempreq.getHeaderData(":res-content-length");
        if (resCL && resCL->getfirstValue())
            content_length = resCL->getfirstValue()->getSizetValue();

        // Capture body data from tempreq's SendData
        std::string body;
        if (!tempreq.SendData.empty()) {
            body.assign(tempreq.SendData.data(), tempreq.SendData.size());
            tempreq.SendData.clear();
        }

        bool is_streaming = (content_length > 0 &&
                             body.size() < content_length);

        if (is_streaming) {
            // Non-blocking streaming: send HEADERS + initial DATA within
            // the flow-control window, store state, and return.
            // Http2RequestEvent resumes sending on WINDOW_UPDATE.

            tempreq.h2state().expectedContentLength = content_length;
            tempreq.SendData.pos = 0;

            // Per-stream peer window was already seeded in
            // _dispatchH2Stream. Don't overwrite it here — a WINDOW_UPDATE
            // for this stream may have already arrived (and been credited
            // to that entry) while an offloaded render was in flight; an
            // unconditional reassignment here would silently discard it.
            cureq.h2state().peerStreamWindows.try_emplace(
                sid, cureq.h2state().peerInitialStreamWindow);

            // Collect extra :res-* headers
            std::vector<libhttppp::hpack::HeaderField> extra;
            if (!_altSvcH3.empty())
                extra.push_back({"alt-svc", _altSvcH3});
            for (auto *hd = tempreq.getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
                const std::string &key = hd->getkey();
                if (key.substr(0, 5) != ":res-") continue;
                if (key == ":res-valid" || key == ":res-status" ||
                    key == ":res-content-type" || key == ":res-content-length") continue;
                std::string realKey = key.substr(5);
                std::string joined;
                for (auto *v = hd->getfirstValue(); v; v = v->nextvalue()) {
                    if (!joined.empty()) joined += "; ";
                    joined += v->getvalue();
                }
                if (!joined.empty())
                    extra.push_back({realKey, joined});
            }

            // HEADERS frame
            std::string hpack_out = libhttppp::hpack::Encoder::encodeResponseHeaders(
                status_code, content_type, content_length, extra);
            out += h2BuildFrame(H2_FRAME_HEADERS, H2_FLAG_END_HEADERS, sid, hpack_out);

            // Create streaming state
            auto ss = std::make_shared<HttpRequest::H2StreamingResponse>();
            ss->streamId = sid;
            ss->contentLength = content_length;
            ss->totalSent = 0;
            ss->tempreq = std::move(tempreq_ptr);
            ss->tid = tid;
            ss->args = args;

            // Send initial body data within current window
            if (!body.empty()) {
                ss->pendingData = std::move(body);
                ss->pendingOffset = 0;
            }

            // Send as much DATA as the window allows right now
            int32_t &sw = cureq.h2state().peerStreamWindows[sid];
            int32_t &cw = cureq.h2state().peerConnWindow;
            while (ss->pendingOffset < ss->pendingData.size()) {
                int32_t allowed = std::min(sw, cw);
                if (allowed <= 0) break;
                size_t avail = ss->pendingData.size() - ss->pendingOffset;
                size_t remaining = content_length > ss->totalSent
                                 ? content_length - ss->totalSent : 0;
                if (remaining == 0) break;
                size_t peer_mfs = cureq.h2state().peerMaxFrameSize;
                size_t chunk_sz = std::min({avail,
                                            static_cast<size_t>(allowed),
                                            peer_mfs,
                                            remaining});
                bool last = (ss->totalSent + chunk_sz >= content_length);
                uint8_t flags = last ? H2_FLAG_END_STREAM : 0;
                out += h2BuildFrame(H2_FRAME_DATA, flags, sid,
                    std::string(ss->pendingData.data() + ss->pendingOffset, chunk_sz));
                sw -= static_cast<int32_t>(chunk_sz);
                cw -= static_cast<int32_t>(chunk_sz);
                ss->pendingOffset += chunk_sz;
                ss->totalSent += chunk_sz;
            }
            // Discard consumed pending data
            if (ss->pendingOffset > 0) {
                ss->pendingData.erase(0, ss->pendingOffset);
                ss->pendingOffset = 0;
            }

            if (ss->totalSent >= content_length) {
                cureq.h2state().peerStreamWindows.erase(sid);
            } else {
                // Guard: don't overwrite an active streaming response
                // for the same stream ID — that would destroy the old
                // tempreq while ResponseEvent may still reference it.
                if (cureq.h2state().activeStreams.count(sid) > 0) {
                    std::cerr << "[H2-DISPATCH] WARNING: sid=" << sid
                              << " already has an active stream, skipping"
                              << std::endl;
                    // Send RST_STREAM to signal the duplicate was rejected
                    uint8_t rst[4] = {0, 0, 0, 0x08}; // CANCEL
                    out += h2BuildFrame(H2_FRAME_RST_STREAM, 0, sid,
                        std::string(reinterpret_cast<char*>(rst), 4));
                    return;
                }
                // Store for resumption on WINDOW_UPDATE
                cureq.h2state().activeStreams[sid] = ss;
                // Immediately kick off the first data fetch — don't wait
                // for an external WINDOW_UPDATE.  Without this, streaming
                // responses with empty initial body never send any DATA,
                // causing a deadlock (Chrome won't WINDOW_UPDATE until it
                // receives data; we won't send data until WINDOW_UPDATE).
                _resumeH2Streams(cureq, out, tid, args);
            }

            return;  // Already handled — skip the normal encoding below
        }

        // Collect extra :res-* headers (skip handled ones)
        std::vector<libhttppp::hpack::HeaderField> extra;

        // Advertise HTTP/3 via Alt-Svc so browsers discover QUIC
        if (!_altSvcH3.empty()) {
            extra.push_back({"alt-svc", _altSvcH3});
        }

        for (auto *hd = tempreq.getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
            const std::string &key = hd->getkey();
            if (key.substr(0, 5) != ":res-") continue;
            if (key == ":res-valid" || key == ":res-status" ||
                key == ":res-content-type" || key == ":res-content-length") continue;
            std::string realKey = key.substr(5);
            // Join all Values with "; " to reconstruct the full
            // header value (e.g. set-cookie attributes).
            std::string joined;
            for (auto *v = hd->getfirstValue(); v; v = v->nextvalue()) {
                if (!joined.empty()) joined += "; ";
                joined += v->getvalue();
            }
            if (!joined.empty())
                extra.push_back({realKey, joined});
        }

        // Build HPACK-encoded HEADERS frame
        std::string hpack_out = libhttppp::hpack::Encoder::encodeResponseHeaders(
            status_code, content_type, body.size(), extra);

        // Per-stream peer window was already seeded in _dispatchH2Stream —
        // see the comment there. Don't overwrite it here; it may already
        // carry credit from a WINDOW_UPDATE that arrived while an offloaded
        // render was in flight.
        cureq.h2state().peerStreamWindows.try_emplace(
            sid, cureq.h2state().peerInitialStreamWindow);

        // HEADERS frame
        uint8_t hdr_flags = H2_FLAG_END_HEADERS;
        if (body.empty()) hdr_flags |= H2_FLAG_END_STREAM;
        out += h2BuildFrame(H2_FRAME_HEADERS, hdr_flags, sid, hpack_out);

        if (!body.empty()) {
            // Send as much DATA as flow control allows
            int32_t &sw = cureq.h2state().peerStreamWindows[sid];
            int32_t &cw = cureq.h2state().peerConnWindow;
            size_t boff = 0;
            while (boff < body.size()) {
                int32_t allowed = std::min(sw, cw);
                if (allowed <= 0) break;
                size_t peer_mfs = cureq.h2state().peerMaxFrameSize;
                size_t chunk_sz = std::min({body.size() - boff,
                                            static_cast<size_t>(allowed),
                                            peer_mfs});
                bool last = (boff + chunk_sz >= body.size());
                uint8_t flags = last ? H2_FLAG_END_STREAM : 0;
                out += h2BuildFrame(H2_FRAME_DATA, flags, sid,
                    std::string(body.data() + boff, chunk_sz));
                sw -= static_cast<int32_t>(chunk_sz);
                cw -= static_cast<int32_t>(chunk_sz);
                boff += chunk_sz;
            }
            if (boff < body.size()) {
                // Window exhausted — park remainder for WINDOW_UPDATE
                auto ss = std::make_shared<HttpRequest::H2StreamingResponse>();
                ss->streamId = sid;
                ss->contentLength = body.size();
                ss->totalSent = boff;
                ss->pendingData.assign(body.data() + boff, body.size() - boff);
                ss->pendingOffset = 0;
                ss->finished = true;  // no more ResponseEvent calls needed
                ss->tid = tid;
                ss->args = args;
                cureq.h2state().activeStreams[sid] = ss;
            } else {
                cureq.h2state().peerStreamWindows.erase(sid);
            }
        }
    } else {
        // Application never produced a response (forgot to call
        // HttpResponse::send / equivalent, or an internal handler path
        // returned without answering). Previously this left the stream
        // open with nothing ever sent -- no HEADERS, no DATA, no
        // RST_STREAM -- so the client just hangs forever. Cancel the
        // stream instead so the client sees a definitive failure.
        uint8_t rst[4] = {0, 0, 0, 0x02}; // INTERNAL_ERROR
        out += h2BuildFrame(H2_FRAME_RST_STREAM, 0, sid,
            std::string(reinterpret_cast<char*>(rst), 4));
        // This stream's window (seeded in _dispatchH2Stream) never got
        // used since no response body was ever sent — drop it rather than
        // leaking one stale entry per failed dispatch on a long-lived
        // connection.
        cureq.h2state().peerStreamWindows.erase(sid);
    }
}

bool libhttppp::HttpEvent::_flushSendDataNow(HttpRequest &cureq) {
    auto &slot = cureq.slots[0];

    if (!slot.csock) {
        return false;
    }

    bool blocked = false;

    try {
        while (cureq.SendData.pos < cureq.SendData.size()) {
            const size_t remaining = cureq.SendData.size() - cureq.SendData.pos;
            const size_t sendlen = std::min<size_t>(remaining, 16 * 1024);

            netplus::buffer buf(cureq.SendData.data() + cureq.SendData.pos,
                                sendlen);
            const size_t sent = slot.csock->sendData(buf, 0);

            if (sent == 0) {
                blocked = true;
                break;
            }

            cureq.SendData.pos += sent;

            // sendData() may only enqueue encrypted TLS records. Try to
            // push them immediately, but retain the write interest when the
            // transport cannot drain the complete output buffer right now.
            if (slot.csock->hasPendingWrite()) {
                slot.csock->flush_out();
                if (slot.csock->hasPendingWrite()) {
                    blocked = true;
                    break;
                }
            }
        }
    } catch (const std::exception &e) {
        std::cerr << "[HTTP-WRITE] deferred: " << e.what()
                  << " pos=" << cureq.SendData.pos
                  << " size=" << cureq.SendData.size()
                  << std::endl;
        blocked = true;
    } catch (...) {
        std::cerr << "[HTTP-WRITE] deferred: unknown exception"
                  << " pos=" << cureq.SendData.pos
                  << " size=" << cureq.SendData.size()
                  << std::endl;
        blocked = true;
    }

    const bool appDataDone = cureq.SendData.pos >= cureq.SendData.size();
    const bool transportDone = !slot.csock->hasPendingWrite();

    if (appDataDone) {
        // sendData() has taken ownership of all plaintext bytes. It is safe
        // to release that buffer even when encrypted transport bytes are
        // still queued inside the socket implementation.
        cureq.SendData.clear();
        cureq.SendData.pos = 0;
    }

    if (appDataDone && transportDone && !blocked) {
        slot.ResponsePending.store(false, std::memory_order_release);
        return true;
    }

    // A short write, EAGAIN, or queued TLS output must explicitly re-arm
    // EPOLLOUT/EVFILT_WRITE. Merely leaving bytes in SendData is not enough
    // with an edge-triggered or one-shot event loop.
    slot.ResponsePending.store(true, std::memory_order_release);
    netplus::requestWritablePoll(cureq);
    return false;
}

void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq,
                                             std::string &out,
                                             const int /*tid*/, ULONG_PTR /*args*/) {
    auto &streams = cureq.h2state().activeStreams;
    if (streams.empty()) return;

    // Iterate over a copy of keys since we may erase during iteration
    std::vector<uint32_t> sids;
    sids.reserve(streams.size());
    for (auto &kv : streams) sids.push_back(kv.first);

    for (uint32_t sid : sids) {
        auto it = streams.find(sid);
        if (it == streams.end()) continue;
        auto &ss = it->second;

        int32_t &sw = cureq.h2state().peerStreamWindows[sid];
        int32_t &cw = cureq.h2state().peerConnWindow;
        size_t peer_mfs = cureq.h2state().peerMaxFrameSize;

        // First: drain any buffered pending data
        while (!ss->pendingData.empty() && ss->pendingOffset < ss->pendingData.size()) {
            int32_t allowed = std::min(sw, cw);
            if (allowed <= 0) {
                if (!ss->windowBlocked) {
                    ss->windowBlocked = true;
                    ss->blockedSince = std::chrono::steady_clock::now();
                }
                break;
            }
            size_t remaining = ss->contentLength > ss->totalSent
                             ? ss->contentLength - ss->totalSent : 0;
            if (remaining == 0) break;
            size_t avail = ss->pendingData.size() - ss->pendingOffset;
            size_t chunk_sz = std::min({avail,
                                        static_cast<size_t>(allowed),
                                        peer_mfs,
                                        remaining});
            bool last = (ss->totalSent + chunk_sz >= ss->contentLength);
            uint8_t flags = last ? H2_FLAG_END_STREAM : 0;
            out += h2BuildFrame(H2_FRAME_DATA, flags, sid,
                std::string(ss->pendingData.data() + ss->pendingOffset, chunk_sz));
            sw -= static_cast<int32_t>(chunk_sz);
            cw -= static_cast<int32_t>(chunk_sz);
            ss->pendingOffset += chunk_sz;
            ss->totalSent += chunk_sz;
            ss->windowBlocked = false;
        }
        if (ss->pendingOffset > 0) {
            ss->pendingData.erase(0, ss->pendingOffset);
            ss->pendingOffset = 0;
        }

        if (ss->totalSent >= ss->contentLength) {
            cureq.h2state().peerStreamWindows.erase(sid);
            streams.erase(it);
            continue;
        }

        // If body is fully buffered (non-streaming), just wait for more window
        if (ss->finished) continue;

        // Fetch more data from upstream via ResponseEvent if window available
        while (ss->totalSent < ss->contentLength) {
            int32_t allowed = std::min(sw, cw);
            if (allowed <= 0) {
                if (!ss->windowBlocked) {
                    ss->windowBlocked = true;
                    ss->blockedSince = std::chrono::steady_clock::now();
                }
                break;
            }

            // NOTE: Do NOT reset SendData.pos here — application code
            // (e.g. file serving) uses pos as a source-file read cursor
            // that must survive across ResponseEvent calls.
            if (!ss->tempreq) {
                std::cerr << "[H2-RESUME] BUG: tempreq is null for sid=" << sid << std::endl;
                streams.erase(it);
                break;
            }
            std::cerr << "[H2-RESUME] calling ResponseEvent sid=" << sid
                      << " totalSent=" << ss->totalSent
                      << " contentLen=" << ss->contentLength
                      << " emptyCount=" << ss->emptyCount
                      << " ext=" << ss->tempreq->extension.get()
                      << std::endl;
            try {
                ResponseEvent(*ss->tempreq, ss->tid, ss->args);
            } catch (std::exception &) {
                // Application signalled a genuine error (e.g. upstream
                // failed mid-stream) by throwing instead of returning.
                // Abort just this stream rather than letting the exception
                // propagate up and tear down the whole (possibly
                // multiplexed) connection.
                uint8_t rst[4] = {0, 0, 0, 0x02}; // INTERNAL_ERROR
                out += h2BuildFrame(H2_FRAME_RST_STREAM, 0, sid,
                    std::string(reinterpret_cast<char*>(rst), 4));
                cureq.h2state().peerStreamWindows.erase(sid);
                streams.erase(it);
                break;
            }
            size_t sa = ss->tempreq->SendData.size();

            if (sa > 0) {
                ss->emptyCount = 0;
                ss->backoffMs = 1;
                size_t doff = 0;
                while (doff < sa) {
                    allowed = std::min(sw, cw);
                    if (allowed <= 0) {
                        // Buffer remainder for next WINDOW_UPDATE
                        if (!ss->windowBlocked) {
                            ss->windowBlocked = true;
                            ss->blockedSince = std::chrono::steady_clock::now();
                        }
                        size_t buf_remaining = ss->contentLength > ss->totalSent
                                             ? ss->contentLength - ss->totalSent : 0;
                        size_t buf_len = std::min(sa - doff, buf_remaining);
                        if (buf_len > 0)
                            ss->pendingData.append(
                                ss->tempreq->SendData.data() + doff, buf_len);
                        ss->pendingOffset = 0;
                        doff = sa;
                        break;
                    }
                    size_t remaining = ss->contentLength > ss->totalSent
                                     ? ss->contentLength - ss->totalSent : 0;
                    if (remaining == 0) break;
                    size_t chunk_sz = std::min({sa - doff,
                                                static_cast<size_t>(allowed),
                                                peer_mfs,
                                                remaining});
                    bool last = (ss->totalSent + chunk_sz >= ss->contentLength);
                    uint8_t flags = last ? H2_FLAG_END_STREAM : 0;
                    out += h2BuildFrame(H2_FRAME_DATA, flags, sid,
                        std::string(ss->tempreq->SendData.data() + doff, chunk_sz));
                    sw -= static_cast<int32_t>(chunk_sz);
                    cw -= static_cast<int32_t>(chunk_sz);
                    doff += chunk_sz;
                    ss->totalSent += chunk_sz;
                    ss->windowBlocked = false;
                }
                ss->tempreq->SendData.clear();
            } else {
                // No data yet — upstream still loading.
                // NOTE: ss->tempreq is a synthetic per-stream request, not a
                // real connection tracked by epoll — nothing external ever
                // sets its ResponsePending. The app-level ResponseEvent we
                // just called (e.g. bolli's streaming handler) sets it on
                // *every* empty read as a "rearm me" signal to a real
                // connection; reading it back here as if it meant "new data
                // arrived" was self-referential and reset emptyCount every
                // single time, so the stall counter below could never
                // advance and a permanently-stalled upstream spun forever.
                ++ss->emptyCount;
                if (ss->emptyCount > 500) {
                    // Genuine stall with the response short of the
                    // advertised Content-Length. Sending END_STREAM here
                    // would make the client accept a truncated body as
                    // complete (e.g. a corrupted image). Abort the stream
                    // instead so the client sees a failed transfer.
                    uint8_t rst[4] = {0, 0, 0, 0x02}; // INTERNAL_ERROR
                    out += h2BuildFrame(H2_FRAME_RST_STREAM, 0, sid,
                        std::string(reinterpret_cast<char*>(rst), 4));
                    cureq.h2state().peerStreamWindows.erase(sid);
                    streams.erase(it);
                } else {
                    // Signal and explicitly re-arm the real H2 connection.
                    // The synthetic per-stream request is not registered in
                    // epoll, therefore setting only ResponsePending is not
                    // sufficient to wake the connection again.
                    cureq.slots[0].ResponsePending.store(
                        true,
                        std::memory_order_release
                    );
                    netplus::requestWritablePoll(cureq);
                }
                break;  // Don't busy-loop; wait for next event
            }

            if (ss->totalSent >= ss->contentLength) {
                cureq.h2state().peerStreamWindows.erase(sid);
                streams.erase(it);
                break;
            }
        }
    }
}

// Abort streaming responses that have been sitting blocked purely on a
// closed peer flow-control window for too long. Called on every
// Http2RequestEvent invocation (i.e. whenever any frame arrives on the
// connection) so that even a connection where one stream is permanently
// stuck still gets cleaned up as soon as something else happens on it —
// there is no separate timer driving the H2 layer.
void libhttppp::HttpEvent::_reapStalledH2Streams(HttpRequest &cureq,
                                                  std::string &out) {
    auto &streams = cureq.h2state().activeStreams;
    if (streams.empty()) return;

    auto now = std::chrono::steady_clock::now();
    std::vector<uint32_t> sids;
    sids.reserve(streams.size());
    for (auto &kv : streams) sids.push_back(kv.first);

    for (uint32_t sid : sids) {
        auto it = streams.find(sid);
        if (it == streams.end()) continue;
        auto &ss = it->second;
        if (!ss->windowBlocked) continue;
        if (now - ss->blockedSince < H2_WINDOW_STALL_TIMEOUT) continue;

        // Peer never credited the window back — give up on this stream
        // instead of leaving it (and its tempreq) parked forever.
        uint8_t rst[4] = {0, 0, 0, 0x03}; // FLOW_CONTROL_ERROR
        out += h2BuildFrame(H2_FRAME_RST_STREAM, 0, sid,
            std::string(reinterpret_cast<char*>(rst), 4));
        cureq.h2state().peerStreamWindows.erase(sid);
        streams.erase(it);
    }
}

bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon,
                                             const int tid,
                                             ULONG_PTR args,
                                             const std::string &alpn,
                                             const netplus::ssl::FramingCallback &frame_cb) {
    (void)alpn;
    (void)frame_cb;

    HttpRequest &cureq = dynamic_cast<HttpRequest&>(curcon);
    cureq._httpProtocol = 1;  // Mark connection as HTTP/2

    std::string out;
    out.reserve(4096);  // Pre-allocate to avoid repeated reallocations
    size_t off = 0;
    int reprocess_count = 0;
    // Set by _dispatchH2Stream whenever it offloads a stream to the
    // background pool instead of finishing it synchronously. An offloaded
    // dispatch leaves `out` empty and `off` already reset to 0 (the frame
    // was still consumed from RecvData before the call) — without this
    // flag, the reprocess condition below would see "no progress" and stop
    // looking at any further buffered frames, permanently stranding every
    // other multiplexed stream's HEADERS frame in RecvData with nothing
    // left to ever re-trigger processing it.
    bool anyOffloaded = false;

    // Consume HTTP/2 client connection preface if present
    if (cureq.RecvData.size() >= H2_CLIENT_PREFACE_LEN &&
        std::memcmp(cureq.RecvData.data(), H2_CLIENT_PREFACE, H2_CLIENT_PREFACE_LEN) == 0) {
        off += H2_CLIENT_PREFACE_LEN;
    }

    // Send server SETTINGS + connection window boost exactly once per connection.
    // The framing callback may have already sent them during TLS handshake;
    // use a flag so we never re-send (repeated WINDOW_UPDATE boosts would
    // overflow the peer's connection send window past 2^31-1).
    if (!cureq.h2state().serverPrefaceSent) {
        cureq.h2state().serverPrefaceSent = true;
        out += h2BuildSettings();
        out += h2ConnectionWindowBoost();
    }

    // Reap any streams that have been window-blocked past the stall
    // timeout before doing anything else — a new request coming in on an
    // otherwise-stuck connection is exactly the point where an old
    // zombie stream would never get cleaned up any other way.
    _reapStalledH2Streams(cureq, out);

    // Process all complete frames in RecvData
reprocess:
    while (off + H2_FRAME_HEADER_LEN <= cureq.RecvData.size()) {
        const char *data = cureq.RecvData.data();
        uint32_t flen = (static_cast<uint8_t>(data[off]) << 16) |
                        (static_cast<uint8_t>(data[off + 1]) << 8) |
                        static_cast<uint8_t>(data[off + 2]);
        uint8_t ftype = static_cast<uint8_t>(data[off + 3]);
        uint8_t fflags = static_cast<uint8_t>(data[off + 4]);
        uint32_t sid = ((static_cast<uint8_t>(data[off + 5]) & 0x7f) << 24) |
                       (static_cast<uint8_t>(data[off + 6]) << 16) |
                       (static_cast<uint8_t>(data[off + 7]) << 8) |
                       static_cast<uint8_t>(data[off + 8]);

        if (off + H2_FRAME_HEADER_LEN + flen > cureq.RecvData.size()) {
            break; // Incomplete frame, wait for more data
        }

        off += H2_FRAME_HEADER_LEN;

        switch (ftype) {
        case H2_FRAME_SETTINGS:
            if (!(fflags & H2_FLAG_ACK)) {
                // Parse peer SETTINGS to extract flow-control parameters
                for (size_t soff = 0; soff + 6 <= flen; soff += 6) {
                    uint16_t sid_s = (static_cast<uint8_t>(data[off + soff]) << 8) |
                                     static_cast<uint8_t>(data[off + soff + 1]);
                    uint32_t sval  = (static_cast<uint8_t>(data[off + soff + 2]) << 24) |
                                     (static_cast<uint8_t>(data[off + soff + 3]) << 16) |
                                     (static_cast<uint8_t>(data[off + soff + 4]) << 8) |
                                     static_cast<uint8_t>(data[off + soff + 5]);
                    if (sid_s == 0x04 && sval <= H2_MAX_WINDOW_SIZE) { // SETTINGS_INITIAL_WINDOW_SIZE
                        // RFC 7540 §6.5.2: values above 2^31-1 are a
                        // FLOW_CONTROL_ERROR — reject rather than let the
                        // int32_t narrowing below turn it negative.
                        int32_t delta = static_cast<int32_t>(sval) -
                                        cureq.h2state().peerInitialStreamWindow;
                        cureq.h2state().peerInitialStreamWindow = static_cast<int32_t>(sval);
                        // Adjust all existing per-stream windows (RFC 7540 §6.9.2),
                        // clamped so an existing window can't be pushed past
                        // the legal maximum either.
                        for (auto &kv : cureq.h2state().peerStreamWindows) {
                            int64_t adjusted = static_cast<int64_t>(kv.second) + delta;
                            kv.second = static_cast<int32_t>(std::min<int64_t>(adjusted, H2_MAX_WINDOW_SIZE));
                        }
                    } else if (sid_s == 0x05) { // SETTINGS_MAX_FRAME_SIZE
                        // RFC 7540 §6.5.2: value must be between 16384 and 16777215
                        if (sval >= 16384 && sval <= 16777215)
                            cureq.h2state().peerMaxFrameSize = sval;
                    }
                }
                out += h2BuildSettingsAck();
            }
            break;

        case H2_FRAME_HEADERS: {
            // RFC 7540 §6.2: If PRIORITY flag is set, the first 5 bytes
            // are priority data (stream dependency + weight), not HPACK.
            const uint8_t *hpack_data = reinterpret_cast<const uint8_t*>(data + off);
            size_t hpack_len = flen;
            if ((fflags & H2_FLAG_PRIORITY) && hpack_len >= 5) {
                hpack_data += 5;
                hpack_len -= 5;
            }

            if (fflags & H2_FLAG_END_HEADERS) {
                // Complete header block in this single frame
                auto decoded = cureq.h2state().hpackDecoder.decode(hpack_data, hpack_len);
                if (fflags & H2_FLAG_END_STREAM) {
                    // No body expected (GET, HEAD, etc.) — dispatch immediately
                    // Erase all processed frames so pumpPeerFrames sees clean RecvData
                    off += flen;
                    cureq.RecvData.erase(cureq.RecvData.begin(),
                                         cureq.RecvData.begin() + off);
                    off = 0;
                    anyOffloaded |= _dispatchH2Stream(cureq, out, sid, decoded, "", tid, args);
                    goto done;
                } else {
                    // Body expected via DATA frames (POST, PUT, etc.)
                    auto &pending = cureq.h2state().pendingIncoming[sid];
                    pending.headers = std::move(decoded);
                    pending.headersComplete = true;
                    pending.streaming = onH2StreamHeaders(cureq, sid, pending.headers);
                }
            } else {
                // END_HEADERS not set: HPACK block continues in CONTINUATION
                // frames. Accumulate raw bytes and defer decoding.
                auto &pending = cureq.h2state().pendingIncoming[sid];
                pending.rawHpack.assign(hpack_data, hpack_data + hpack_len);
                pending.headersComplete = false;
                pending.endStreamOnHeaders = (fflags & H2_FLAG_END_STREAM) != 0;
            }
            break;
        }

        case H2_FRAME_CONTINUATION: {
            // RFC 7540 §6.10: CONTINUATION carries more HPACK data for
            // a HEADERS block whose END_HEADERS was not yet set.
            auto it = cureq.h2state().pendingIncoming.find(sid);
            if (it != cureq.h2state().pendingIncoming.end() && !it->second.headersComplete) {
                const uint8_t *cont_data = reinterpret_cast<const uint8_t*>(data + off);
                it->second.rawHpack.insert(it->second.rawHpack.end(),
                                           cont_data, cont_data + flen);
                if (fflags & H2_FLAG_END_HEADERS) {
                    // Full HPACK block received — decode now
                    it->second.headers = cureq.h2state().hpackDecoder.decode(
                        it->second.rawHpack.data(), it->second.rawHpack.size());
                    it->second.rawHpack.clear();
                    it->second.rawHpack.shrink_to_fit();
                    it->second.headersComplete = true;

                    if (it->second.endStreamOnHeaders) {
                        // No body expected — dispatch immediately
                        off += flen;
                        cureq.RecvData.erase(cureq.RecvData.begin(),
                                             cureq.RecvData.begin() + off);
                        off = 0;
                        anyOffloaded |= _dispatchH2Stream(cureq, out, sid,
                                          it->second.headers, "", tid, args);
                        cureq.h2state().pendingIncoming.erase(it);
                        goto done;
                    }
                    it->second.streaming = onH2StreamHeaders(cureq, sid, it->second.headers);
                }
            }
            break;
        }

        case H2_FRAME_DATA: {
            auto it = cureq.h2state().pendingIncoming.find(sid);
            if (it != cureq.h2state().pendingIncoming.end()) {
                bool endStream = (fflags & H2_FLAG_END_STREAM) != 0;

                // Replenish flow-control windows so the client
                // can keep sending (both stream and connection).
                if (flen > 0) {
                    out += h2BuildWindowUpdate(sid, flen);
                    out += h2BuildWindowUpdate(0,   flen);
                }

                if (it->second.streaming) {
                    // Streaming mode: feed data to callback instead of accumulating
                    onH2DataChunk(cureq, sid, data + off, flen, endStream, out, tid, args);
                    if (endStream) {
                        off += flen;
                        cureq.RecvData.erase(cureq.RecvData.begin(),
                                             cureq.RecvData.begin() + off);
                        off = 0;
                        cureq.h2state().pendingIncoming.erase(it);
                        goto done;
                    }
                } else {
                    it->second.body.append(data + off, flen);

                    if (endStream) {
                        if (it->second.headersComplete) {
                            off += flen;
                            cureq.RecvData.erase(cureq.RecvData.begin(),
                                                 cureq.RecvData.begin() + off);
                            off = 0;
                            anyOffloaded |= _dispatchH2Stream(cureq, out, sid,
                                              it->second.headers,
                                              it->second.body,
                                              tid, args);
                            cureq.h2state().pendingIncoming.erase(it);
                            goto done;
                        }
                    }
                }
            }
            break;
        }

        case H2_FRAME_PING:
            // RFC 7540: respond only to non-ACK PING frames, payload must be 8 bytes.
            if (!(fflags & H2_FLAG_ACK) && flen == 8) {
                out += h2BuildFrame(H2_FRAME_PING, H2_FLAG_ACK, 0,
                                    std::string(data + off, flen));
            }
            break;

        case H2_FRAME_GOAWAY:
            goto done;

        case H2_FRAME_RST_STREAM: {
            // Clean up pending state for the reset stream to prevent
            // memory leaks and stalled stream bookkeeping.
            auto it = cureq.h2state().pendingIncoming.find(sid);
            if (it != cureq.h2state().pendingIncoming.end()) {
                cureq.h2state().pendingIncoming.erase(it);
            }
            // Also remove any pending outbound responses for this stream
            auto &pr = cureq.h2state().pendingResponses;
            for (auto pit = pr.begin(); pit != pr.end(); ) {
                if (pit->streamId == sid) {
                    pit = pr.erase(pit);
                } else {
                    ++pit;
                }
            }
            // Clean up active streaming state so we don't send DATA
            // on a closed stream (which would be a protocol error)
            cureq.h2state().activeStreams.erase(sid);
            cureq.h2state().peerStreamWindows.erase(sid);
            break;
        }

        case H2_FRAME_WINDOW_UPDATE: {
            if (flen >= 4) {
                uint32_t increment = ((static_cast<uint8_t>(data[off]) & 0x7f) << 24) |
                                     (static_cast<uint8_t>(data[off + 1]) << 16) |
                                     (static_cast<uint8_t>(data[off + 2]) << 8) |
                                     static_cast<uint8_t>(data[off + 3]);
                if (sid == 0) {
                    // Clamp instead of letting the int32_t wrap negative
                    // (RFC 7540 §6.9.1). An overflowed window never
                    // recovers on its own — every future send sees
                    // min(sw, cw) <= 0 and the connection stalls
                    // permanently until the client tears down the TCP
                    // connection and reconnects.
                    int64_t newWindow = static_cast<int64_t>(cureq.h2state().peerConnWindow) + increment;
                    cureq.h2state().peerConnWindow =
                        static_cast<int32_t>(std::min<int64_t>(newWindow, H2_MAX_WINDOW_SIZE));
                } else {
                    // Only update streams we're still tracking — ignore
                    // WINDOW_UPDATEs for already-completed streams to
                    // avoid creating phantom peerStreamWindows entries.
                    auto swit = cureq.h2state().peerStreamWindows.find(sid);
                    if (swit != cureq.h2state().peerStreamWindows.end()) {
                        int64_t newWindow = static_cast<int64_t>(swit->second) + increment;
                        swit->second = static_cast<int32_t>(std::min<int64_t>(newWindow, H2_MAX_WINDOW_SIZE));
                    }
                }
                // Resume any parked streaming responses
                _resumeH2Streams(cureq, out, tid, args);
            }
            break;
        }
        case H2_FRAME_PRIORITY:
            break;

        default:
            // Unknown frame types: skip per RFC 7540 Section 4.1
            break;
        }

        off += flen;
    }

done:
    if (!out.empty()) {
        cureq.SendData.append(out.data(), out.size());
    }

    // Flush SendData immediately so H2 responses for completed streams
    // reach the client without waiting for the reprocess loop to finish
    // all remaining dispatches.  Without this, a slow RequestEvent
    // (e.g. on-demand preview rendering) serialises all streams and the
    // client may time out before later streams are served.
    _flushSendDataNow(cureq);

    if (off > 0) {
        cureq.RecvData.erase(cureq.RecvData.begin(), cureq.RecvData.begin() + off);
    }

    // If frames remain in RecvData (e.g. HEADERS that arrived during
    // streaming), process them now instead of waiting for the next
    // EPOLLIN which may never fire.
    // Only reprocess if we actually made progress (consumed frames,
    // generated output, or offloaded a stream). Otherwise we'd spin
    // forever on an incomplete frame that needs more network data to
    // arrive. anyOffloaded matters here: an offloaded dispatch consumes
    // its frame from RecvData just like a synchronous one, but leaves
    // `out` empty and `off` already reset — without checking it, any
    // further multiplexed streams' HEADERS frames buffered in this same
    // read would never get looked at (nothing else will ever re-trigger
    // processing them, since the client is done sending and is only
    // waiting on responses).
    // Guard: limit reprocessing iterations to prevent infinite spin when
    // frames continuously generate output (e.g. ACKs) without consuming data.
    if ((!out.empty() || off > 0 || anyOffloaded) && cureq.RecvData.size() >= H2_FRAME_HEADER_LEN) {
        if (reprocess_count < 16) {
            ++reprocess_count;
            out.clear();
            out.reserve(4096);
            off = 0;
            goto reprocess;
        }

        // Fairness cap hit while frames for further streams are still
        // buffered (e.g. a client bursting >17 simple GET/HEAD requests
        // over one multiplexed connection in a single read). SendData was
        // already flushed synchronously above, so epoll's own writable
        // check sees nothing queued and won't call ResponseEvent on its
        // own -- and the client won't send more bytes, it's waiting on
        // responses already dispatched. Without an explicit rearm here,
        // the remaining request(s) are stranded in RecvData forever.
        // ResponseEvent's H2 branch unconditionally reprocesses leftover
        // frames, so requesting a writable poll gets them handled with a
        // fresh reprocess budget.
        cureq.slots[0].ResponsePending.store(true, std::memory_order_release);
        netplus::requestWritablePoll(cureq);
    }

    return true;
}



void libhttppp::HttpEvent::Http3StreamEvent(netplus::socket *sock,
                                            uint64_t stream_id,
                                            const std::vector<uint8_t> &data,
                                            bool fin) {
    if (data.empty() && !fin) {
        return;
    }

    // Accumulate stream data
    {
        std::lock_guard<std::mutex> lock(_h3BufferMutex);
        auto &state = _h3StreamStates[stream_id];
        if (!data.empty()) {
            state.data.insert(state.data.end(), data.begin(), data.end());
        }
    }

    // Try to parse H3 frames incrementally (outside lock for callbacks).
    // QUIC guarantees ordered delivery per stream, so no concurrent calls
    // for the same stream_id.
    H3StreamState *sp = nullptr;
    {
        std::lock_guard<std::mutex> lock(_h3BufferMutex);
        auto it = _h3StreamStates.find(stream_id);
        if (it != _h3StreamStates.end()) sp = &it->second;
    }
    if (!sp) return;
    auto &state = *sp;

    while (state.parseOffset < state.data.size()) {
        size_t remaining = state.data.size() - state.parseOffset;
        const uint8_t *p = state.data.data() + state.parseOffset;

        size_t type_bytes = 0;
        uint64_t frame_type = h3DecodeVarInt(p, remaining, type_bytes);
        if (type_bytes == 0) break;
        if (type_bytes >= remaining) break;

        size_t len_bytes = 0;
        uint64_t frame_len = h3DecodeVarInt(p + type_bytes, remaining - type_bytes, len_bytes);
        if (len_bytes == 0) break;

        size_t header_size = type_bytes + len_bytes;
        if (state.parseOffset + header_size + frame_len > state.data.size()) break;

        const uint8_t *frame_data = p + header_size;

        if (frame_type == 0x01 && !state.headersParsed) {
            // HEADERS frame
            auto decoded = qpack::Decoder::decode(frame_data, frame_len);
            state.headersParsed = true;
            state.streaming = onH3StreamHeaders(sock, stream_id, decoded);
        } else if (frame_type == 0x00 && state.streaming) {
            // DATA frame in streaming mode — feed directly
            onH3DataChunk(sock, stream_id,
                         reinterpret_cast<const char*>(frame_data), frame_len, false);
        }

        state.parseOffset += header_size + frame_len;
    }

    if (!fin) return;

    if (state.streaming) {
        // Final callback with fin=true
        onH3DataChunk(sock, stream_id, nullptr, 0, true);
        std::lock_guard<std::mutex> lock(_h3BufferMutex);
        _h3StreamStates.erase(stream_id);
        return;
    }

    // Non-streaming path: extract complete data and process as before
    std::vector<uint8_t> completeData;
    {
        std::lock_guard<std::mutex> lock(_h3BufferMutex);
        auto it = _h3StreamStates.find(stream_id);
        if (it != _h3StreamStates.end()) {
            completeData = std::move(it->second.data);
            _h3StreamStates.erase(it);
        }
    }

    if (completeData.empty()) {
        return;
    }

    auto *q = dynamic_cast<netplus::quic*>(sock);
    if (!q) {
        return;
    }

    // Two-pass approach: first collect HEADERS and DATA payloads,
    // then dispatch.  This ensures POST body data is available
    // in RecvData when RequestEvent is called.
    std::vector<uint8_t> headersPayload;
    std::string bodyData;

    size_t offset = 0;
    int frame_idx = 0;
    while (offset < completeData.size()) {
        size_t bytes = 0;
        uint64_t frame_type = h3DecodeVarInt(completeData.data() + offset, completeData.size() - offset, bytes);
        if (bytes == 0) {
            break;
        }
        offset += bytes;
        if (offset >= completeData.size()) {
            break;
        }

        uint64_t frame_len = h3DecodeVarInt(completeData.data() + offset, completeData.size() - offset, bytes);
        if (bytes == 0) {
            break;
        }
        offset += bytes;
        if (offset + frame_len > completeData.size()) {
            break;
        }

        if (frame_type == 0x01) { // HEADERS frame
            headersPayload.assign(completeData.data() + offset, completeData.data() + offset + frame_len);
        } else if (frame_type == 0x00) { // DATA frame
            bodyData.append(reinterpret_cast<const char*>(completeData.data() + offset), frame_len);
        }

        offset += frame_len;
        frame_idx++;
    }

    if (headersPayload.empty()) {
        return;
    }

    // Decode QPACK headers
    auto decoded = libhttppp::qpack::Decoder::decode(
        headersPayload.data(), headersPayload.size());

    // Create a heap-allocated HttpRequest (like H2 path) so it can
    // be moved into a background thread for streaming responses.
    auto tempreq_ptr = std::make_unique<HttpRequest>();
    HttpRequest &tempreq = *tempreq_ptr;
    tempreq.parseH3(decoded);

    // Store POST body in RecvData so HttpForm::parse() can access it
    if (!bodyData.empty()) {
        tempreq.RecvData.append(bodyData.data(), bodyData.size());
    }

    // Ensure content-length is set from actual body size (H3 framing
    // provides the length, but application code may rely on the header)
    if (!bodyData.empty() && !tempreq.getHeaderData("content-length")) {
        tempreq.setHeaderData("content-length")->push_back(std::to_string(bodyData.size()));
    }

    // Assign a thread-safe tid via round-robin so H3 requests
    // don't all collide on tid=0 resources (PG connections, etc.)
    int h3tid = _h3NextTid.fetch_add(1, std::memory_order_relaxed) % std::max(threads, 1);

    // Call user's RequestEvent
    RequestEvent(tempreq, h3tid, 0);

    // Read structured response info from :res-* pseudo-headers
    auto *resValid = tempreq.getHeaderData(":res-valid");
    if (resValid && resValid->getfirstValue()) {
        // Extract status code
        uint16_t status_code = 200;
        auto *resStatus = tempreq.getHeaderData(":res-status");
        if (resStatus && resStatus->getfirstValue()) {
            try { status_code = static_cast<uint16_t>(std::stoi(resStatus->getfirstValue()->getvalue())); }
            catch (...) { status_code = 200; }
        }

        // Content type
        std::string content_type;
        auto *resCT = tempreq.getHeaderData(":res-content-type");
        if (resCT && resCT->getfirstValue())
            content_type = resCT->getfirstValue()->getvalue();

        // Content length
        size_t content_length = 0;
        auto *resCL = tempreq.getHeaderData(":res-content-length");
        if (resCL && resCL->getfirstValue())
            content_length = resCL->getfirstValue()->getSizetValue();

        // Capture body data from SendData
        std::string body;
        if (content_length > 0)
            body.reserve(content_length);
        if (!tempreq.SendData.empty()) {
            body.assign(tempreq.SendData.data(), tempreq.SendData.size());
            tempreq.SendData.clear();
        }

        bool is_streaming = (content_length > 0 &&
                             body.size() < content_length);

        // If streaming, send HEADERS immediately then push DATA
        // frames incrementally so the client receives data while
        // we are still reading from upstream (same as H2 path).
        if (is_streaming) {
            // Collect extra :res-* headers before we start sending
            std::vector<libhttppp::qpack::HeaderField> hdr_extra;
            for (auto *hd = tempreq.getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
                const std::string &ekey = hd->getkey();
                if (ekey.substr(0, 5) != ":res-") continue;
                if (ekey == ":res-valid" || ekey == ":res-status" ||
                    ekey == ":res-content-type" || ekey == ":res-content-length") continue;
                std::string realKey = ekey.substr(5);
                std::string joined;
                for (auto *v = hd->getfirstValue(); v; v = v->nextvalue()) {
                    if (!joined.empty()) joined += "; ";
                    joined += v->getvalue();
                }
                if (!joined.empty())
                    hdr_extra.push_back({realKey, joined});
            }

            // Build and send H3 HEADERS frame immediately
            auto hdr_block = libhttppp::qpack::Encoder::encodeResponseHeaders(
                status_code,
                content_type.empty() ? "text/html" : content_type,
                content_length, hdr_extra);
            // H3 frame: type=0x01 (HEADERS), then varint length, then payload
            std::vector<uint8_t> h3hdr;
            h3hdr.push_back(0x01); // HEADERS frame type
            uint8_t lenbuf[8];
            size_t lb = h3EncodeVarInt(hdr_block.size(), lenbuf);
            h3hdr.insert(h3hdr.end(), lenbuf, lenbuf + lb);
            h3hdr.insert(h3hdr.end(), hdr_block.begin(), hdr_block.end());

            // Send initial body as DATA frame if any
            if (!body.empty()) {
                size_t body_send = std::min(body.size(), content_length);
                h3hdr.push_back(0x00); // DATA frame type
                lb = h3EncodeVarInt(body_send, lenbuf);
                h3hdr.insert(h3hdr.end(), lenbuf, lenbuf + lb);
                h3hdr.insert(h3hdr.end(), body.begin(), body.begin() + body_send);
            }
            bool initial_complete = (!body.empty() && std::min(body.size(), content_length) >= content_length);
            q->sendStreamData(stream_id, h3hdr, initial_complete);

            size_t total_sent = std::min(body.size(), content_length);
            body.clear();
            body.shrink_to_fit();

            if (initial_complete) {
                // Entire body was sent with initial DATA frame
                tempreq.deldata(":res-valid");
                tempreq.deldata(":res-status");
                tempreq.deldata(":res-content-type");
                tempreq.deldata(":res-content-length");
                return;
            }

            // Event-driven streaming: move tempreq to a background
            // thread (like H2's _resumeH2Streams) so we don't block
            // the QUIC callback.  The thread calls ResponseEvent in
            // a loop, sending DATA frames as upstream produces data,
            // with proper sleeps between empty iterations.
            tempreq.SendData.pos = 0;
            std::thread([this, q, stream_id, h3tid,
                         tr = std::move(tempreq_ptr),
                         content_length, total_sent]() mutable {
                size_t sent = total_sent;
                size_t empty_streak = 0;
                static constexpr size_t max_empty = 500;
                uint8_t lbuf[8];

                while (sent < content_length) {
                    // Pump incoming on every iteration to keep flow
                    // control responsive for large transfers.
                    q->pumpIncoming();

                    try {
                        ResponseEvent(*tr, h3tid, 0);
                    } catch (...) {
                        // This runs on a detached background thread with no
                        // other exception handler above it — letting an
                        // application error (e.g. upstream failed
                        // mid-stream) escape here would call
                        // std::terminate() and crash the whole process.
                        // Stop the loop; the trailing "sent < content_length"
                        // check below will reset the stream instead of
                        // sending a false END_STREAM.
                        break;
                    }
                    size_t sa = tr->SendData.size();

                    if (sa > 0) {
                        size_t remaining = content_length - sent;
                        if (sa > remaining) sa = remaining;

                        std::vector<uint8_t> data_frame;
                        data_frame.reserve(1 + 8 + sa);
                        data_frame.push_back(0x00); // DATA frame type
                        size_t lb = h3EncodeVarInt(sa, lbuf);
                        data_frame.insert(data_frame.end(), lbuf, lbuf + lb);
                        data_frame.insert(data_frame.end(),
                                          tr->SendData.data(),
                                          tr->SendData.data() + sa);
                        tr->SendData.clear();
                        sent += sa;
                        bool last = (sent >= content_length);
                        q->sendStreamData(stream_id, data_frame, last);
                        empty_streak = 0;
                    } else {
                        q->pumpIncoming();
                        if (tr->slots[0].ResponsePending.exchange(false)) {
                            // Upstream signalled more data is coming
                            // — retry immediately without sleeping.
                            empty_streak = 0;
                            continue;
                        } else {
                            ++empty_streak;
                        }
                        if (empty_streak >= max_empty) {
                            break;
                        }
                        // Brief sleep to let upstream produce data.
                        // Use a short 1ms sleep to avoid busy-waiting
                        // while upstream fetches the next chunk.
                        std::this_thread::sleep_for(
                            std::chrono::milliseconds(1));
                    }
                }

                if (sent < content_length) {
                    // Genuine stall with the response short of the
                    // advertised Content-Length. Sending FIN here would make
                    // the client accept a truncated body as complete (e.g. a
                    // corrupted image). Abort the stream instead so the
                    // client sees a failed transfer, mirroring the H2 stall
                    // handling above.
                    q->resetStream(stream_id, 0x0102); // H3_INTERNAL_ERROR
                }
            }).detach();

            return;
        }

        // Collect extra :res-* headers (e.g. set-cookie)
        std::vector<libhttppp::qpack::HeaderField> extra;
        for (auto *hd = tempreq.getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
            const std::string &ekey = hd->getkey();
            if (ekey.substr(0, 5) != ":res-") continue;
            if (ekey == ":res-valid" || ekey == ":res-status" ||
                ekey == ":res-content-type" || ekey == ":res-content-length") continue;
            std::string realKey = ekey.substr(5);
            std::string joined;
            for (auto *v = hd->getfirstValue(); v; v = v->nextvalue()) {
                if (!joined.empty()) joined += "; ";
                joined += v->getvalue();
            }
            if (!joined.empty())
                extra.push_back({realKey, joined});
        }

        std::vector<uint8_t> response = h3BuildResponse(
            status_code, body,
            content_type.empty() ? "text/html" : content_type, extra);
        q->sendStreamData(stream_id, response, true);

        // Clean up :res-* pseudo-headers
        tempreq.deldata(":res-valid");
        tempreq.deldata(":res-status");
        tempreq.deldata(":res-content-type");
        tempreq.deldata(":res-content-length");
    } else {
        // The application never produced a response for this H3 request
        // (mirrors the H2 case in _finishH2Dispatch). Without this, the
        // stream is left open with nothing ever sent, and the client
        // hangs forever waiting for a reply that will never arrive.
        q->resetStream(stream_id, 0x0102); // H3_INTERNAL_ERROR
    }
}

// Default streaming callback implementations — return false / no-op
bool libhttppp::HttpEvent::onH2StreamHeaders(HttpRequest &, uint32_t,
                                             const std::vector<hpack::HeaderField> &) {
    return false;
}

bool libhttppp::HttpEvent::onH3StreamHeaders(netplus::socket *, uint64_t,
                                             const std::vector<qpack::HeaderField> &) {
    return false;
}

void libhttppp::HttpEvent::onH2DataChunk(HttpRequest &, uint32_t,
                                         const char *, size_t, bool,
                                         std::string &, const int, ULONG_PTR) {
}

void libhttppp::HttpEvent::onH3DataChunk(netplus::socket *, uint64_t,
                                         const char *, size_t, bool) {
}

// Helper: send a complete H2 response (HEADERS + DATA) on a stream
void libhttppp::HttpEvent::sendH2StreamResponse(std::string &h2out, uint32_t streamId,
                                                 uint16_t status,
                                                 const std::string &contentType,
                                                 const std::string &body) {
    h2out += h2BuildResponse(streamId, status, body, contentType);
}

// Helper: send a complete H3 response (HEADERS + DATA) on a stream
void libhttppp::HttpEvent::sendH3StreamResponse(netplus::socket *sock, uint64_t streamId,
                                                 uint16_t status,
                                                 const std::string &contentType,
                                                 const std::string &body) {
    auto *q = dynamic_cast<netplus::quic*>(sock);
    if (!q) return;
    auto hdr_block = qpack::Encoder::encodeResponseHeaders(
        status, contentType.empty() ? "application/json" : contentType,
        body.size());
    std::vector<uint8_t> response;
    uint8_t lenbuf[8];
    // HEADERS frame
    response.push_back(0x01);
    size_t lb = h3EncodeVarInt(hdr_block.size(), lenbuf);
    response.insert(response.end(), lenbuf, lenbuf + lb);
    response.insert(response.end(), hdr_block.begin(), hdr_block.end());
    // DATA frame
    if (!body.empty()) {
        response.push_back(0x00);
        lb = h3EncodeVarInt(body.size(), lenbuf);
        response.insert(response.end(), lenbuf, lenbuf + lb);
        response.insert(response.end(), body.begin(), body.end());
    }
    q->sendStreamData(streamId, response, true);
}

void libhttppp::HttpEvent::RequestEvent(netplus::con &curcon,const int tid,ULONG_PTR args){
    HttpRequest &cureq=dynamic_cast<HttpRequest&>(curcon);
    try{
        if (auto *quicsock = dynamic_cast<netplus::quic*>(curcon.slots[0].csock.get())) {
            (void)quicsock;
            return;
        }

        if (auto *sslsock = dynamic_cast<netplus::ssl*>(curcon.slots[0].csock.get())) {
            const std::string &alpn = sslsock->getSelectedAlpn();
            if (alpn == "h2") {
                Http2RequestEvent(curcon, tid, args, alpn, sslsock->getFramingCallback());
                return;
            }
        }

        // h2c: detect HTTP/2 client connection preface on cleartext connections
        // (e.g. behind reverse proxies like Envoy that speak h2c upstream)
        if (cureq.RecvData.size() >= H2_CLIENT_PREFACE_LEN &&
            std::memcmp(cureq.RecvData.data(), H2_CLIENT_PREFACE, H2_CLIENT_PREFACE_LEN) == 0) {
            static const netplus::ssl::FramingCallback nullCb;
            Http2RequestEvent(curcon, tid, args, "h2c", nullCb);
            return;
        }

        // h2c continuation: connection already identified as HTTP/2 (cleartext)
        if (cureq._httpProtocol == 1) {
            static const netplus::ssl::FramingCallback nullCb;
            Http2RequestEvent(curcon, tid, args, "h2c", nullCb);
            return;
        }

REQUESTHANDLING:
        // Guard: only loop for HTTP/1.x; HTTP/2 and HTTP/3 parse() always
        // returns 1, which would cause an infinite loop here.
        if (cureq._httpProtocol != 0) {
            return;
        }
        switch(cureq.getRequestType()){
            case PARSEREQUEST:
                if(cureq.parse()!=0)
                    goto REQUESTHANDLING;
                break;
            case GETREQUEST:
                RequestEvent(cureq,tid,args);
                cureq._RequestType=PARSEREQUEST;
                break;
            case DELETEREQUEST:
                RequestEvent(cureq,tid,args);
                cureq._RequestType=PARSEREQUEST;
                break;
            case PUTREQUEST: {
                if(cureq.isChunkedRequest()){
                    int dr = cureq.decodeChunkedBody();
                    if(dr == 0) break;          // body incomplete, wait for more
                    if(dr < 0){
                        cureq.clear();
                        libhttppp::HTTPException re;
                        re[libhttppp::HTTPException::Error] << "malformed chunked request body!";
                        throw re;
                    }
                    size_t clen = cureq.getContentLength();
                    RequestEvent(cureq,tid,args);
                    cureq.RecvData.erase(cureq.RecvData.begin(),cureq.RecvData.begin()+clen);
                    cureq._RequestType=PARSEREQUEST;
                    break;
                }
                size_t clen = cureq.getContentLength();
                if(clen == 0){
                    RequestEvent(cureq,tid,args);
                    cureq._RequestType=PARSEREQUEST;
                } else if(cureq.RecvData.size()>=clen){
                    RequestEvent(cureq,tid,args);
                    cureq.RecvData.erase(cureq.RecvData.begin(),cureq.RecvData.begin()+clen);
                    cureq._RequestType=PARSEREQUEST;
                }
                break;
            }
            case POSTREQUEST: {
                if(cureq.isChunkedRequest()){
                    int dr = cureq.decodeChunkedBody();
                    if(dr == 0) break;          // body incomplete, wait for more
                    if(dr < 0){
                        cureq.clear();
                        libhttppp::HTTPException re;
                        re[libhttppp::HTTPException::Error] << "malformed chunked request body!";
                        throw re;
                    }
                    size_t clen = cureq.getContentLength();
                    RequestEvent(cureq,tid,args);
                    cureq.RecvData.erase(cureq.RecvData.begin(),cureq.RecvData.begin()+clen);
                    cureq._RequestType=PARSEREQUEST;
                    break;
                }
                size_t clen = cureq.getContentLength();
                if(clen == 0){
                    RequestEvent(cureq,tid,args);
                    cureq._RequestType=PARSEREQUEST;
                } else if(cureq.RecvData.size()>=clen){
                    RequestEvent(cureq,tid,args);
                    cureq.RecvData.erase(cureq.RecvData.begin(),cureq.RecvData.begin()+clen);
                    cureq._RequestType=PARSEREQUEST;
                }
                break;
            }
            default:{
                cureq.clear();
                libhttppp::HTTPException re;
                re[libhttppp::HTTPException::Error] << "unknown requesttype !";
                throw re;
            }
        }
    }catch(HTTPException &e){
        netplus::NetException re;
        re[netplus::NetException::Error] << "httpd error: " << e.what();
        throw re;
    }
}

void libhttppp::HttpEvent::ResponseEvent(netplus::con &curcon,const int tid,ULONG_PTR args){
    HttpRequest &cureq=static_cast<HttpRequest&>(curcon);
    try{
        // HTTP/2: connection is persistent and multiplexed.
        // Incrementally send pending H2 stream DATA frames.
        if (cureq._httpProtocol == 1) {
            if (cureq._h2 && !cureq._h2->pendingResponses.empty()) {
                auto &pending = cureq._h2->pendingResponses.front();
                // Batch up to 8 DATA chunks per event loop iteration
                // for significantly higher throughput on large responses.
                std::string batch;
                size_t peer_mfs = cureq.h2state().peerMaxFrameSize;
                batch.reserve(std::min(pending.body.size() - pending.offset,
                                       8 * peer_mfs) + 8 * H2_FRAME_HEADER_LEN);
                for (int i = 0; i < 8 && pending.offset < pending.body.size(); ++i) {
                    size_t remaining = pending.body.size() - pending.offset;
                    size_t chunk = std::min(remaining, peer_mfs);
                    bool last = (pending.offset + chunk >= pending.body.size());
                    uint8_t flags = last ? H2_FLAG_END_STREAM : 0;
                    h2AppendFrameDirect(batch, H2_FRAME_DATA, flags,
                                        pending.streamId,
                                        pending.body.data() + pending.offset, chunk);
                    pending.offset += chunk;
                    if (last) break;
                }
                if (!batch.empty()) {
                    cureq.SendData.append(batch.data(), batch.size());
                }
                if (pending.offset >= pending.body.size()) {
                    cureq._h2->pendingResponses.pop_front();
                }
            }
            // Resume active streaming responses (video, large files).
            // Without this, streams stall when the upstream is
            // temporarily slow and the flow-control window is not
            // yet exhausted — there is no WINDOW_UPDATE to trigger
            // _resumeH2Streams from Http2RequestEvent.
            if (cureq._h2 && !cureq._h2->activeStreams.empty()) {
                std::string out;
                out.reserve(4096);
                _resumeH2Streams(cureq, out, tid, args);
                if (!out.empty()) {
                    cureq.SendData.append(out.data(), out.size());
                }
            }
            // Process any remaining H2 frames that couldn't be handled
            // in the previous Http2RequestEvent call (reprocess_count limit).
            // Without this, HEADERS frames for additional streams stall
            // until the next EPOLLIN (which may never fire if the client
            // is waiting for responses).
            if (cureq.RecvData.size() >= H2_FRAME_HEADER_LEN) {
                static const netplus::ssl::FramingCallback nullCb;
                if (auto *sslsock = dynamic_cast<netplus::ssl*>(curcon.slots[0].csock.get())) {
                    Http2RequestEvent(curcon, tid, args,
                                      sslsock->getSelectedAlpn(),
                                      sslsock->getFramingCallback());
                } else {
                    Http2RequestEvent(curcon, tid, args, "h2c", nullCb);
                }
            }

            // DATA generated above must be handed to the socket now. If the
            // socket is temporarily blocked, _flushSendDataNow() re-arms the
            // writable event so the response cannot remain parked forever.
            const bool fullyFlushed = _flushSendDataNow(cureq);

            /*
             * _flushSendDataNow() clears ResponsePending when the current
             * transport/application buffers are empty. That is correct for
             * completed responses, but not for an active streaming response:
             * the application may need another ResponseEvent call to produce
             * the next media chunk even though SendData is currently empty.
             *
             * Without this re-arm, an H2 video stream can stop after the first
             * chunk whenever no WINDOW_UPDATE or further inbound frame arrives.
             */
            const bool hasActiveStreams =
                cureq._h2 && !cureq._h2->activeStreams.empty();

            const bool hasPendingResponses =
                cureq._h2 && !cureq._h2->pendingResponses.empty();

            const bool hasQueuedApplicationData =
                cureq.SendData.pos < cureq.SendData.size();

            const bool hasQueuedTransportData =
                cureq.slots[0].csock &&
                cureq.slots[0].csock->hasPendingWrite();

            if (hasActiveStreams ||
                hasPendingResponses ||
                hasQueuedApplicationData ||
                hasQueuedTransportData ||
                !fullyFlushed) {
                cureq.slots[0].ResponsePending.store(
                    true,
                    std::memory_order_release
                );
                netplus::requestWritablePoll(cureq);
            }

            return;
        }

        // Skip when there is no active request or when still waiting
        // for POST body data to arrive (clearing state prematurely would
        // destroy parsed headers — including Content-Length — causing the
        // partially received body to be re-parsed as HTTP headers, which
        // leads to an endless loop for large/multi-file uploads).
        if (cureq.getRequestType() == POSTREQUEST || cureq.getRequestType() == PUTREQUEST) {
            return;
        }
        if (cureq.getRequestType() == PARSEREQUEST && cureq.getRequestURL().empty()) {
            return;
        }

        ResponseEvent(cureq,tid,args);
        if(cureq.SendData.empty()
           && !cureq.slots[0].ResponsePending.load(std::memory_order_acquire)){
            cureq.SendData.pos=0;
            cureq.clear();
        }
    }catch(HTTPException &e){
        netplus::NetException ne;
        ne[netplus::NetException::Error] << e.what();
        throw ne;
    }
}

void libhttppp::HttpEvent::ConnectEvent(netplus::con &curcon,const int tid,ULONG_PTR args){
    if (auto *quicsock = dynamic_cast<netplus::quic*>(curcon.slots[0].csock.get())) {
        quicsock->setStreamCallback([this](netplus::socket *sock,
                                           uint64_t stream_id,
                                           const std::vector<uint8_t> &data,
                                           bool fin) {
            this->Http3StreamEvent(sock, stream_id, data, fin);
        });
    }

    ConnectEvent(static_cast<HttpRequest&>(curcon), tid, args);
}

void libhttppp::HttpEvent::DisconnectEvent(netplus::con &curcon,const int tid,ULONG_PTR args){
    DisconnectEvent(static_cast<HttpRequest&>(curcon),tid,args);
}

libhttppp::HttpD::HttpD(int argc, char** argv) {
    _fileServer = false;
    /*Register Parameters*/
    cmdplus::CmdController &HTTPDCmdController = cmdplus::CmdController::getInstance();
    HTTPDCmdController.registerCmd("help", 'h', false,"", "Helpmenu");
    HTTPDCmdController.registerCmd("httpaddr",'a', true,"","Address to listen");
    HTTPDCmdController.registerCmd("httpport", 'p', false, 8080, "Port to listen");
    HTTPDCmdController.registerCmd("maxconnections", 'm',false, MAXDEFAULTCONN, "Max connections that can connect");
    HTTPDCmdController.registerCmd("httpsder", 'c',false,"", "SSL DER File");
    HTTPDCmdController.registerCmd("httpskeyder", 'k', false, "", "SSL RSA private key (DER)");
    /*Parse Parameters*/
    HTTPDCmdController.parseCmd(argc,argv);

    if (HTTPDCmdController.getCmdbyKey("help") && HTTPDCmdController.getCmdbyKey("help")->getFound()) {
        HTTPDCmdController.printHelp();
        throw _httpexception[HTTPException::Note] << "Help Menu printed";
    }

    if (!HTTPDCmdController.checkRequired()) {
        HTTPDCmdController.printHelp();
        _httpexception[HTTPException::Critical] << "cmd parser not enough arguments given";
        throw _httpexception;
    }
      
    /*get port from console paramter*/
    int port = 0;
    if(HTTPDCmdController.getCmdbyKey("httpport")){
        port = HTTPDCmdController.getCmdbyKey("httpport")->getValueInt();
    }
    
    /*get httpaddress from console paramter*/
    std::string httpaddr;
    if (HTTPDCmdController.getCmdbyKey("httpaddr"))
        httpaddr = HTTPDCmdController.getCmdbyKey("httpaddr")->getValue();
    
    /*get max connections from console paramter*/
    int maxconnections = 0;
    if (HTTPDCmdController.getCmdbyKey("maxconnections"))
        maxconnections = HTTPDCmdController.getCmdbyKey("maxconnections")->getValueInt();
    
    /*get httpaddress from console paramter*/
    std::string derfile;
    if (HTTPDCmdController.getCmdbyKey("httpsder"))
        derfile = HTTPDCmdController.getCmdbyKey("httpsder")->getValue();

    std::string keyder;
    if (HTTPDCmdController.getCmdbyKey("httpskeyder"))
        keyder = HTTPDCmdController.getCmdbyKey("httpskeyder")->getValue();

    try {
            if (!derfile.empty()) {
                if (keyder.empty()) {
                    _httpexception[HTTPException::Critical] << "httpsder set but httpskeyder missing";
                    throw _httpexception;
                }

                netplus::ssl::CertificateBundle bundle;
                if (!bundle.loadFromFile(derfile, keyder)) {
                    _httpexception[HTTPException::Critical] << "failed to load cert/key: " << derfile;
                    throw _httpexception;
                }
                if (!bundle.cert.checkValidity()) {
                    time_t now = time(nullptr);
                    _httpexception[HTTPException::Critical] << "invalid certfile: " << derfile
                        << " (CN=" << bundle.cert.getSubjectCN()
                        << ", system_time=" << now << ")";
                    throw _httpexception;
                }

                _certBundle[httpaddr] = bundle;
                auto tss = std::make_unique<netplus::ssl>(_certBundle, httpaddr, port, maxconnections, -1);
                auto qss = std::make_unique<netplus::quic>(_certBundle, httpaddr, port, maxconnections, -1);
                _ServerSockets.push_back(std::move(tss));
                _ServerSockets.push_back(std::move(qss));
        }else{

            #ifndef Windows
            if (port > 0)
                _ServerSockets.push_back(std::make_unique<netplus::tcp>(httpaddr, port, maxconnections,-1));
            else
                _ServerSockets.push_back(std::make_unique<netplus::tcp>(httpaddr, maxconnections,-1));
            #else
                _ServerSockets.push_back(std::make_unique<netplus::tcp>(httpaddr, port, maxconnections, -1));
            #endif 
        }


    }catch (netplus::NetException &e) {
        HTTPException ee;
        ee[HTTPException::Critical] << e.what();
        throw ee;
    }
}

libhttppp::HttpD::HttpD(const std::string& httpaddr, int port,int maxconnections,const  std::string &sslcertpath,const  std::string &sslkeypath, const std::string &sslpassword) {
    try {
		_fileServer = false;
        if (!sslcertpath.empty()) {
                std::cerr << "[HttpD] Loading cert bundle..." << std::endl;
                netplus::ssl::CertificateBundle bundle;
                if (!bundle.loadFromFile(sslcertpath, sslkeypath, sslpassword)) {
                    _httpexception[HTTPException::Critical] << "failed to load cert/key: " << sslcertpath;
                    throw _httpexception;
                }
                std::cerr << "[HttpD] Cert loaded, checking validity..." << std::endl;
                if (!bundle.cert.checkValidity()) {
                    time_t now = time(nullptr);
                    _httpexception[HTTPException::Critical] << "invalid certfile: " << sslcertpath
                        << " (CN=" << bundle.cert.getSubjectCN()
                        << ", system_time=" << now << ")";
                    throw _httpexception;
                }

                std::cerr << "[HttpD] Copying to certBundle map..." << std::endl;
                _certBundle[httpaddr] = bundle;
                std::cerr << "[HttpD] Creating SSL socket..." << std::endl;
                auto tss = std::make_unique<netplus::ssl>(_certBundle, httpaddr, port, maxconnections, -1);
                std::cerr << "[HttpD] Creating QUIC socket..." << std::endl;
                auto qss = std::make_unique<netplus::quic>(_certBundle, httpaddr, port, maxconnections, -1);

                _ServerSockets.push_back(std::move(tss));
                _ServerSockets.push_back(std::move(qss));
        }else{
            #ifndef Windows
            if (port != -1)
                _ServerSockets.push_back(std::make_unique<netplus::tcp>(httpaddr, port, maxconnections,-1));
            else
                _ServerSockets.push_back(std::make_unique<netplus::tcp>(httpaddr, maxconnections,-1));
            #else
                _ServerSockets.push_back(std::make_unique<netplus::tcp>(httpaddr, port, maxconnections,-1));
            #endif
        }
    }catch (netplus::NetException &e) {
        HTTPException ee;
        ee[HTTPException::Critical] << e.what();
        throw ee;
    }
}

std::vector<netplus::socket*> libhttppp::HttpD::getServerSockets(){
    std::vector<netplus::socket*> sockets;
    for (auto &sock : _ServerSockets) {
        sockets.push_back(sock.get());
    }
    return sockets;
}

bool libhttppp::HttpD::reloadCertificates(const std::string &certpath, const std::string &keypath, const std::string &password) {
    netplus::ssl::CertificateBundle bundle;
    if (!bundle.loadFromFile(certpath, keypath, password)) {
        return false;
    }
    if (!bundle.cert.checkValidity()) {
        return false;
    }

    // Update internal bundle map
    for (auto &[addr, _] : _certBundle) {
        _certBundle[addr] = bundle;
    }

    // Push new certs to all ssl/quic server sockets
    for (auto &sock : _ServerSockets) {
        if (auto *s = dynamic_cast<netplus::ssl*>(sock.get())) {
            s->setCertificates(_certBundle);
        } else if (auto *q = dynamic_cast<netplus::quic*>(sock.get())) {
            q->setCertificates(_certBundle);
        }
    }
    return true;
}

libhttppp::HttpD::~HttpD(){
}
