/*******************************************************************************
* 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 <algorithm>
#include <array>
#include <sstream>
#include <charconv>
#include <cstdio>
#include <cstdlib>
#include <ctype.h>
#include <cstring>
#include <iterator>
#include <map>
#include <set>
#include <mutex>
#include <iomanip>
#include <random>

#include "config.h"

#include "http.h"
#include "hpack.h"
#include "qpack.h"

#include "exception.h"

#include <netplus/crypto/base64.h>

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

libhttppp::HttpUrl::HttpUrl(){
  _protocol=HTTP;
  _origProtocol=HTTP;
  _port=80;
  _path="/";
}

libhttppp::HttpUrl::HttpUrl(const std::string& url, bool http3) {
    HTTPException exp;

    // Get the C-style pointer for string operations
    const char* c_url = url.c_str();
    const char* p = c_url;

    // Detect the scheme as it actually appears in the URL string first --
    // _origProtocol keeps this even when http3 below forces _protocol to
    // HTTP3, so fallback logic downstream can still tell https from http.
    if (url.rfind("https://", 0) == 0) { // Using rfind for string prefix check
        _origProtocol = HTTPS;
        _port = 443;
        p += 8; // Advance pointer past "https://"
    }
    else if (url.rfind("http://", 0) == 0) {
        _origProtocol = HTTP;
        _port = 80;
        p += 7; // Advance pointer past "http://"
    }
    // Note: The "https" and "http" (without ://) checks are less robust but kept for compatibility
    else if (url.rfind("https", 0) == 0) {
        _origProtocol = HTTPS;
        _port = 443;
    }
    else if (url.rfind("http", 0) == 0) {
        _origProtocol = HTTP;
        _port = 80;
    }
    else {
        exp[HTTPException::Error] << "Httpurl: protocol not supported!";
        throw exp;
    }

    if (http3) {
        _protocol = HttpProtocol::HTTP3;
        _port = 443;
    } else {
        _protocol = _origProtocol;
    }
    _path = "/";

    const char* host_start = p;

    // Use the length of the original string to prevent reading past the end
    size_t url_len = url.length();
    size_t offset = p - c_url;

    // Iterate through the remaining part of the string
    while (offset < url_len && *p && *p != ':' && *p != '/') {
        ++p;
        ++offset;
    }

    // Assign the host part
    _host.assign(host_start, p - host_start);

    if (offset < url_len && *p == ':') {
        ++p;
        ++offset;
        const char* digits = p;

        // Iterate through the port digits
        while (offset < url_len && *p && std::isdigit((unsigned char)*p)) {
            ++p;
            ++offset;
        }

        if (p == digits || (p - digits) > 5) {
            exp[HTTPException::Error] << "Httpurl: Port range are Wrong !";
            throw exp;
        }
        _port = std::atoi(std::string(digits, p - digits).c_str());
    }

    // Check for the path part
    if (offset < url_len && *p == '/') {
        // Use string methods or assign from pointer
        // Here we use std::string::substr for clarity and safety with the remaining part of the URL
        _path = url.substr(p - c_url);
    }
}

libhttppp::HttpUrl::HttpUrl(const HttpUrl& src){
  _protocol=src._protocol;
  _origProtocol=src._origProtocol;
  _host=src._host;
  _port=src._port;
  _path=src._path;
}

libhttppp::HttpUrl::~HttpUrl(){
}

void libhttppp::HttpUrl::clear(){
  _protocol=HTTP;
  _origProtocol=HTTP;
  _host.clear();
  _port=80;
  _path="/";
}

bool libhttppp::HttpUrl::operator==(const HttpUrl& other) const {
        return (_protocol == other._protocol &&
                _host == other._host &&
                _port == other._port &&
                _path == other._path);
}

bool libhttppp::HttpUrl::operator<(const HttpUrl& other) const {
    if (print().length() != other.print().length()) {
        return print().length() < other.print().length();
    }

    if (_protocol != other._protocol) {
        return _protocol < other._protocol;
    }

    if (_host != other._host) {
        return _host < other._host;
    }

    if (_port != other._port) {
        return _port < other._port;
    }
    return _path < other._path;
}

std::string libhttppp::HttpUrl::print() const{
  std::string out = (_protocol == HTTP) ? "http://" : "https://";
  out += _host;
  out += ':';
  out += std::to_string(_port);
  out += _path;
  return out;
}


int libhttppp::HttpUrl::getProtocol() const{
  return _protocol;
}

int libhttppp::HttpUrl::getOriginalProtocol() const{
  return _origProtocol;
}

const std::string & libhttppp::HttpUrl::getHost() const{
  return _host;
}

int libhttppp::HttpUrl::getPort() const{
  return _port;
}

const std::string & libhttppp::HttpUrl::getPath() const{
  return _path;
}

// --- TLS Session Cache (process-wide singleton) ---
netplus::TlsSessionCache& libhttppp::HttpClient::tlsSessionCache() {
    static netplus::TlsSessionCache cache;
    return cache;
}

libhttppp::HttpClient::HttpClient(const HttpUrl& desturl, int vers, int timeoutSec)
: _url(desturl), _vers(vers){
    _recvTimeoutSec = timeoutSec;
    _sendTimeoutSec = timeoutSec;
    try {
      if(vers == 3) {
        if (!tryHttp3First()) {
          // HTTP/3 failed: try HTTP/2 first, then HTTP/1.1. Must check the
          // URL's original scheme here, not getProtocol() -- that's pinned
          // to HTTP3 for the lifetime of this object once requested above.
          if (_url.getOriginalProtocol() == HttpUrl::HTTPS) {
            bool h2_ok = false;
            try {
              // Internal probe mode: advertise ALPN h2 only.
              _vers = 4;
              resetConnection();
              h2_ok = _isH2;
            } catch (...) {
              h2_ok = false;
            }
            if (h2_ok) {
              _vers = 2;
            } else {
              // Peer doesn't support HTTP/2 via ALPN either (e.g. HTTP/1.1-only
              // server) -- fall back to plain HTTP/1.1 instead of giving up.
              _vers = 0;
              resetConnection();
            }
          } else {
            // Cleartext cannot negotiate ALPN; fall back directly.
            _vers = 0;
            resetConnection();
          }
        }
      } else {
        resetConnection();
      }
    } catch (netplus::NetException &e) {
        HTTPException ex;
        ex[HTTPException::Error] << "HttpClient ctor connect failed: " << e.what();
        throw ex; // !!! IMPORTANT: throw HTTPException, nicht NetException
    }
}

// Process-wide cache of host:port where QUIC/HTTP3 failed
static std::mutex s_h3FailMutex;
static std::set<std::string> s_h3FailedHosts;

bool libhttppp::HttpClient::tryHttp3First(){
  std::string hostKey = _url.getHost() + ":" + std::to_string(_url.getPort());
  {
    std::lock_guard<std::mutex> lk(s_h3FailMutex);
    if (s_h3FailedHosts.count(hostKey))
      return false;
  }
  try {
    auto q = std::make_unique<netplus::quic>();
    q->connect(_url.getHost(), _url.getPort(), false);
    q->setNonBlock();

    // Verify the QUIC handshake actually completes within a short timeout.
    // UDP connect() always succeeds, so we must probe the handshake.
    auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
    while (!q->getHandshakeDone()) {
      if (std::chrono::steady_clock::now() >= deadline) {
        // Handshake did not complete — server likely doesn't support QUIC
        std::lock_guard<std::mutex> lk(s_h3FailMutex);
        s_h3FailedHosts.insert(hostKey);
        return false;
      }
      try {
        q->pumpNetwork(MSG_DONTWAIT);
      } catch (netplus::NetException &e) {
        if (e.getErrorType() != netplus::NetException::Note) {
          std::lock_guard<std::mutex> lk(s_h3FailMutex);
          s_h3FailedHosts.insert(hostKey);
          return false;
        }
      }
      _sw.waitRead(*q, 50);
    }

    _cltsock = std::move(q);
    _isH2 = false;
    _isH3 = true;
    return true;
  } catch (netplus::NetException&) {
    _cltsock.reset();
    _isH2 = false;
    _isH3 = false;
    std::lock_guard<std::mutex> lk(s_h3FailMutex);
    s_h3FailedHosts.insert(hostKey);
    return false;
  }
}

// Shared by resetConnection()/reconnect() -- both need to (re)establish
// _cltsock from scratch (fresh QUIC/TLS/TCP connect) and were, before this
// refactor, ~80 lines of byte-for-byte duplicated logic that had already
// started risking silent divergence between the two call paths.
void libhttppp::HttpClient::resetConnection(){
  // Reset HTTP/2 connection state on any new connection
  _h2PrefaceSent = false;
  _h2NextStreamId = 1;
  _h2Decoder.reset();
  // getProtocol() alone isn't enough: it's pinned to HTTP3 for the object's
  // whole lifetime once requested, even after the ctor downgrades _vers on
  // fallback. Only take the QUIC path when HTTP/3 is still what we want.
  if (_url.getProtocol() == HttpUrl::HTTP3 && _vers == 3) {
    auto q = std::make_unique<netplus::quic>();
    q->connect(_url.getHost(), _url.getPort(), false);
    q->setNonBlock();
    // Wait for QUIC handshake to complete before returning
    auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
    while (!q->getHandshakeDone()) {
      if (std::chrono::steady_clock::now() >= deadline) {
        HTTPException ee;
        ee[HTTPException::Error] << "HTTP/3 QUIC handshake timeout";
        throw ee;
      }
      try {
        q->pumpNetwork(MSG_DONTWAIT);
      } catch (netplus::NetException &e) {
        if (e.getErrorType() != netplus::NetException::Note) throw;
      }
      _sw.waitRead(*q, 50);
    }
    _cltsock = std::move(q);
    _isH2 = false;
    _isH3 = true;
  } else if (_url.getOriginalProtocol() == HttpUrl::HTTPS) {
    std::map<std::string, netplus::ssl::CertificateBundle> certs;
    auto sslsock = std::make_unique<netplus::ssl>(certs,-1);

    // Advertise ALPN based on version preference
    if (_vers >= 4) {
        sslsock->getTls().client_alpn_protocols =
          std::string("\x02h2", 3);
    } else if (_vers >= 1) {
        sslsock->getTls().client_alpn_protocols =
          std::string("\x02h2\x08http/1.1", 12);
    } else {
        sslsock->getTls().client_alpn_protocols =
          std::string("\x08http/1.1", 9);
    }

    // --- TLS session resumption: attach cache and load saved session ---
    netplus::tls& t = sslsock->getTls();
    t.session_cache = &tlsSessionCache();

    // Build host:port key for the cache lookup
    std::string cacheKey = _url.getHost() + ":" + std::to_string(_url.getPort());
    netplus::TlsSessionCacheEntry saved;
    if (tlsSessionCache().lookup_by_host(cacheKey, saved)) {
        t.attempt_resumption = true;
        t.resumption_session_id    = saved.session_id;
        t.resumption_master_secret = saved.master_secret;
        t.resumption_cipher        = saved.cipher_suite;
    }

    _cltsock = std::move(sslsock);
    _cltsock->connect(_url.getHost(), _url.getPort(), false);
    _cltsock->setNonBlock();

    // Check negotiated ALPN after TLS handshake
    const std::string &alpn = dynamic_cast<netplus::ssl*>(_cltsock.get())->getSelectedAlpn();
    _isH2 = (alpn == "h2");
    _isH3 = false;

    // --- Save session for future resumption ---
    {
        netplus::tls& tc = dynamic_cast<netplus::ssl*>(_cltsock.get())->getTls();
        if (tc.handshakeDone && !tc.is_tls13
            && !tc.client_session_id.empty() && !tc.masterSecret.empty()) {
            tlsSessionCache().store_by_host(cacheKey,
                tc.client_session_id, tc.masterSecret, tc.chosenSuite);
        }
    }
  } else {
    _cltsock = std::make_unique<netplus::tcp>(-1);
    _connectTcp(static_cast<netplus::tcp&>(*_cltsock));
    _isH2 = false;
    _isH3 = false;
  }
}

// Bounded by _recvTimeoutSec instead of the OS default connect timeout
// (which can be minutes) -- a fully unreachable host (dead container,
// wrong IP, dropped SYNs) would otherwise hang the caller far longer than
// any configured setTimeout() would suggest. The actual non-blocking
// connect/waitWrite/SO_ERROR dance lives in netplus::tcp::connectTimeout()
// so any direct netplus::tcp consumer can reuse it too.
void libhttppp::HttpClient::_connectTcp(netplus::tcp &sock) {
    try {
        sock.connectTimeout(_url.getHost(), _url.getPort(), _recvTimeoutSec * 1000);
    } catch (netplus::NetException &e) {
        HTTPException he;
        he[HTTPException::Error] << e.what();
        throw he;
    }
}


void libhttppp::HttpClient::reconnect(){
  // resetConnection() already does exactly this (fresh QUIC/TLS/TCP connect
  // for the current _url/_vers); the two were previously ~80 lines of
  // duplicated logic that had already begun to accumulate small differences.
  resetConnection();
}

void libhttppp::HttpClient::setTimeout(int timeout_sec) {
    _recvTimeoutSec = timeout_sec;
    _sendTimeoutSec = timeout_sec;
}

static inline std::string tolower_copy(std::string s) {
    std::transform(s.begin(), s.end(), s.begin(),
                   [](unsigned char c){ return (char)std::tolower(c); });
    return s;
}

// Check if existing connection is still alive; only reconnect if necessary.
// Avoids expensive TCP/TLS handshakes on every request.
void libhttppp::HttpClient::_ensureConnected() {
    if (_cltsock && _cltsock->fd() >= 0) {
        if (_sw.isDisconnected(*_cltsock)) {
            resetConnection();
        }
        return;
    }
    resetConnection();
}

// Efficient recv: uses socketwait::waitRead() instead of busy-waiting with sleep
// Returns 0 on connection closure. Throws on hard errors.
// Hard timeout to prevent infinite hangs on silently-dead connections.
size_t libhttppp::HttpClient::_recvBlocking(netplus::buffer &b, int timeout_sec) {
    // tcp::recvData()/ssl::recvData() request up to b.size bytes and then
    // OVERWRITE b.size with however many actually arrived (see
    // libnetplus/src/posix/tcp.cpp). b is always _recvScratch, reused across
    // calls instead of freshly constructed -- without resetting b.size back
    // to its real capacity here, a single small read (common on the very
    // first TCP segment) would permanently shrink every subsequent request
    // on this object to that same small size, turning what used to be a few
    // 64KB reads into many tiny ones.
    b.size = CHUNKSIZE;
    if (timeout_sec <= 0)
        timeout_sec = _recvTimeoutSec;
    auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_sec);
    for (;;) {
        try {
            return _cltsock->recvData(b, 0);
        } catch (netplus::NetException &e) {
            if (e.getErrorType() == netplus::NetException::Note) {
                if (std::chrono::steady_clock::now() >= deadline) {
                    HTTPException ee;
                    ee[HTTPException::Error] << "recv timeout: connection may be dead";
                    throw ee;
                }
                _sw.waitRead(*_cltsock, 500);
                continue;
            }
            throw;
        }
    }
}

// Non-blocking recv: returns 0 immediately if no data available (EAGAIN)
size_t libhttppp::HttpClient::_recvNonBlocking(netplus::buffer &b) {
    if (!_cltsock) {
        return 0;
    }
    b.size = CHUNKSIZE; // see _recvBlocking() -- recvData() shrinks b.size to bytes read
    try {
        return _cltsock->recvData(b, 0);
    } catch (netplus::NetException &e) {
        if (e.getErrorType() == netplus::NetException::Note) {
            return 0;   // EAGAIN — no data yet
        }
        throw;
    }
}

// Efficient send: uses socketwait::waitWrite() instead of busy-waiting
void libhttppp::HttpClient::_sendAll(const char *data, size_t len) {
    if (!_cltsock || len == 0) return;
    int send_sec = _sendTimeoutSec > 0 ? _sendTimeoutSec : 30;
    auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(send_sec);
    size_t off = 0;
    while (off < len) {
        netplus::buffer buf(data + off, len - off);
        try {
            size_t n = _cltsock->sendData(buf, 0);
            if (n == 0) {
                if (std::chrono::steady_clock::now() >= deadline) {
                    HTTPException ee;
                    ee[HTTPException::Error] << "send timeout: connection may be dead";
                    throw ee;
                }
                _sw.waitWrite(*_cltsock, 500);
                continue;
            }
            off += n;
        } catch (netplus::NetException &e) {
            if (e.getErrorType() == netplus::NetException::Note) {
                if (std::chrono::steady_clock::now() >= deadline) {
                    HTTPException ee;
                    ee[HTTPException::Error] << "send timeout: connection may be dead";
                    throw ee;
                }
                _sw.waitWrite(*_cltsock, 500);
                continue;
            }
            throw;
        }
    }
}

void libhttppp::HttpClient::_sendAll(const std::string &data) {
    _sendAll(data.c_str(), data.size());
}

// Shared HTTP/1.x response reader: reads headers, parses transfer mode, reads body.
// Eliminates ~1000 lines of duplicated code across Get/Post/Put/Delete.
std::vector<char> libhttppp::HttpClient::_h1ReadResponse(const std::string &label) {
    std::vector<char> ret;

    // 1) Read until we have full header (\r\n\r\n)
    std::vector<char> raw;
    raw.reserve(16384);

    size_t header_end = std::string::npos;
    size_t search_from = 0;   // avoid re-scanning from 0 on every recv (O(n²) → O(n))

    for (;;) {
        if (raw.size() >= 4) {
            size_t start = (search_from >= 3) ? search_from - 3 : 0;
            for (size_t i = start; i + 3 < raw.size(); ++i) {
                if (raw[i] == '\r' && raw[i+1] == '\n' &&
                    raw[i+2] == '\r' && raw[i+3] == '\n')
                {
                    header_end = i + 4;
                    break;
                }
            }
            search_from = raw.size();
        }
        if (header_end != std::string::npos)
            break;

        // Honor setTimeout() (_recvTimeoutSec) instead of a fixed 10s: a slow
        // backend (cold model load/swap) can take much longer than 10s to
        // send back even the first byte of the response.
        size_t n = _recvBlocking(_recvScratch);
        if (n == 0) {
            netplus::NetException ne;
            ne[netplus::NetException::Error] << "HTTP " << label << ": EOF while reading header";
            throw ne;
        }
        raw.insert(raw.end(), _recvScratch.data.buf, _recvScratch.data.buf + n);
    }

    // 2) Parse header
    libhttppp::HttpResponse res;
    size_t parsed_hsize = res.parse(raw.data(), raw.size());

    if (parsed_hsize == 0 || parsed_hsize > raw.size()) {
        libhttppp::HTTPException he;
        he[libhttppp::HTTPException::Error] << "HTTP " << label << ": response header parse failed";
        throw he;
    }

    // Store parsed response and extract fields for redirect handling
    _lastResponse = std::make_unique<HttpResponse>(res);
    _lastStatusCode = res.getStatusCode();
    _lastLocation.clear();
    _lastContentType.clear();
    try {
        libhttppp::HttpHeader::HeaderData *locHdr = res.getHeaderData("location");
        if (locHdr && locHdr->getfirstValue())
            _lastLocation = locHdr->getfirstValue()->getvalue();
    } catch (...) {}
    try {
        libhttppp::HttpHeader::HeaderData *ctHdr = res.getHeaderData("content-type");
        if (ctHdr && ctHdr->getfirstValue())
            _lastContentType = ctHdr->getfirstValue()->getvalue();
    } catch (...) {}

    size_t body_off = parsed_hsize;

    // 3) Determine transfer mode
    bool chunked = false;
    ptrdiff_t content_len = -1;

    try {
        for (libhttppp::HttpHeader::HeaderData::Values *v = res.getTransferEncoding();
             v; v = v->nextvalue())
        {
            std::string val = tolower_copy(v->getvalue());
            if (val == "chunked") {
                chunked = true;
                break;
            }
        }
    } catch (...) {}

    // getContentLength() returns 0 both when Content-Length is genuinely
    // absent (relying on EOF/connection-close to terminate the body) and
    // when the header is literally "Content-Length: 0" -- indistinguishable
    // from that call alone. Check presence directly so an EOF-terminated
    // response isn't misdetected as a zero-length body.
    if (!chunked && res.getHeaderData("content-length")) {
        try {
            content_len = static_cast<ptrdiff_t>(res.getContentLength());
            if (content_len < 0) content_len = -1;
        } catch (...) {
            content_len = -1;
        }
    }

    auto append_from_raw = [&](size_t& take_pos, size_t take_len) {
        if (take_len == 0) return;
        ret.insert(ret.end(), raw.begin() + (ptrdiff_t)take_pos,
                              raw.begin() + (ptrdiff_t)(take_pos + take_len));
        take_pos += take_len;
    };

    // 4A) BODY: Content-Length
    if (!chunked && content_len >= 0) {
        size_t remaining = (size_t)content_len;
        ret.reserve(remaining);
        size_t pos = body_off;

        size_t can = (std::min)(remaining, raw.size() - pos);
        append_from_raw(pos, can);
        remaining -= can;

        while (remaining > 0) {
            size_t n = _recvBlocking(_recvScratch);
            if (n == 0) {
                netplus::NetException ne;
                ne[netplus::NetException::Error] << "HTTP " << label << ": EOF before Content-Length complete";
                throw ne;
            }
            size_t take = (std::min)(remaining, n);
            ret.insert(ret.end(), _recvScratch.data.buf, _recvScratch.data.buf + take);
            remaining -= take;
        }
        return ret;
    }

    // 4B) BODY: chunked
    if (chunked) {
        size_t pos = body_off;

        auto ensure_bytes = [&](size_t need) {
            while (raw.size() - pos < need) {
                size_t n = _recvBlocking(_recvScratch);
                if (n == 0) {
                    netplus::NetException ne;
                    ne[netplus::NetException::Error] << "HTTP " << label << ": EOF while reading chunked body";
                    throw ne;
                }
                raw.insert(raw.end(), _recvScratch.data.buf, _recvScratch.data.buf + n);
            }
        };

        auto read_line = [&]() -> std::string {
            for (;;) {
                for (size_t i = pos; i + 1 < raw.size(); ++i) {
                    if (raw[i] == '\r' && raw[i+1] == '\n') {
                        std::string line(raw.begin() + (ptrdiff_t)pos,
                                         raw.begin() + (ptrdiff_t)i);
                        pos = i + 2;
                        return line;
                    }
                }
                size_t n = _recvBlocking(_recvScratch);
                if (n == 0) {
                    netplus::NetException ne;
                    ne[netplus::NetException::Error] << "HTTP " << label << ": EOF while reading chunk size line";
                    throw ne;
                }
                raw.insert(raw.end(), _recvScratch.data.buf, _recvScratch.data.buf + n);
            }
        };

        for (;;) {
            std::string szline = read_line();
            auto sem = szline.find(';');
            if (sem != std::string::npos) szline.resize(sem);

            size_t chunk_size = 0;
            try {
                chunk_size = (size_t)std::stoul(szline, nullptr, 16);
            } catch (...) {
                libhttppp::HTTPException he;
                he[libhttppp::HTTPException::Error] << "HTTP " << label << ": invalid chunk size line: " << szline;
                throw he;
            }

            if (chunk_size == 0) {
                // Consume trailers (limit to 128 to prevent unbounded
                // memory growth from a malicious server).
                for (int trailer_i = 0; trailer_i < 128; ++trailer_i) {
                    std::string tline = read_line();
                    if (tline.empty()) break;
                }
                break;
            }

            ensure_bytes(chunk_size + 2);

            ret.insert(ret.end(),
                       raw.begin() + (ptrdiff_t)pos,
                       raw.begin() + (ptrdiff_t)(pos + chunk_size));
            pos += chunk_size;

            if (raw[pos] != '\r' || raw[pos+1] != '\n') {
                libhttppp::HTTPException he;
                he[libhttppp::HTTPException::Error] << "HTTP " << label << ": bad chunk terminator";
                throw he;
            }
            pos += 2;
        }

        return ret;
    }

    // 4C) BODY: no length, not chunked -> read until EOF
    {
        size_t pos = body_off;
        if (raw.size() > pos) {
            ret.insert(ret.end(), raw.begin() + (ptrdiff_t)pos, raw.end());
        }
        for (;;) {
            size_t n = 0;
            try {
                n = _recvBlocking(_recvScratch);
            } catch (...) {
                throw;
            }
            if (n == 0) break;
            ret.insert(ret.end(), _recvScratch.data.buf, _recvScratch.data.buf + n);
        }
    }

    return ret;
}

// Forward declarations for H2/H3 helpers (defined later in this file)
static size_t h3EncodeVarInt(uint64_t value, uint8_t *out);
static uint64_t h3DecodeVarInt(const uint8_t *data, size_t len, size_t &bytes_read);
static void h3AppendFrame(std::vector<uint8_t> &out, uint64_t type, const std::vector<uint8_t> &payload);

static constexpr uint8_t H2C_FRAME_DATA         = 0x00;
static constexpr uint8_t H2C_FRAME_HEADERS      = 0x01;
static constexpr uint8_t H2C_FRAME_SETTINGS     = 0x04;
static constexpr uint8_t H2C_FRAME_PING         = 0x06;
static constexpr uint8_t H2C_FRAME_GOAWAY       = 0x07;
static constexpr uint8_t H2C_FRAME_WINDOW_UPDATE = 0x08;

static constexpr uint8_t H2C_FLAG_END_STREAM  = 0x01;
static constexpr uint8_t H2C_FLAG_ACK         = 0x01;
static constexpr uint8_t H2C_FLAG_END_HEADERS = 0x04;

static constexpr size_t  H2C_FRAME_HEADER_LEN = 9;
static constexpr size_t  H2C_MAX_FRAME_SIZE   = 16384;

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

static std::string h2cBuildFrame(uint8_t type, uint8_t flags, uint32_t stream_id,
                                 const std::string &payload);
static std::string h2cBuildSettings();
static std::string h2cBuildSettingsAck();
static std::string h2cBuildWindowUpdate(uint32_t stream_id, uint32_t increment);
static std::string h2cConnectionWindowBoost();

// ── Streaming API ──────────────────────────────────────────────────────

bool libhttppp::HttpClient::isStreaming() const {
    return _streamMode != STREAM_NONE;
}

bool libhttppp::HttpClient::waitReadable(int timeout_ms) {
    if (!_cltsock) return false;
    return _sw.waitRead(*_cltsock, timeout_ms);
}

libhttppp::HttpResponse libhttppp::HttpClient::GetStream(libhttppp::HttpRequest &nreq) {
    try {
        _ensureConnected();

        // Reset any previous streaming state
        _streamMode = STREAM_NONE;
        _streamBuf.clear();
        _streamBuf.reserve(CHUNKSIZE);
        _streamBufPos = 0;
        _streamRemaining = 0;
        _streamChunkDone = false;
        _streamChunkRemaining = 0;
        _streamH2Sid = 0;
        _streamH2EndStream = false;
        _streamH2Raw.clear();
        _streamH2Raw.reserve(CHUNKSIZE);
        _streamH3Sid = 0;
        _streamH3EndStream = false;
        _streamH3Raw.clear();
        _streamH3Raw.reserve(CHUNKSIZE);
        _streamH3Body.clear();
        _streamH3Body.reserve(CHUNKSIZE);
        _streamH3BodyPos = 0;
        _streamH3InDataFrame = false;
        _streamH3DataRemaining = 0;

        if (!nreq.getHeaderData("host"))
            nreq.setHeaderData("host")->push_back(_url.getHost() + ":" + std::to_string(_url.getPort()));
        nreq.setRequestType(GETREQUEST);

        if (nreq.getRequestURL().empty())
            nreq.setRequestURL(_url.getPath());

        // ── HTTP/3 (QUIC) streaming path ──
        if (auto *q = dynamic_cast<netplus::quic*>(_cltsock.get())) {
            std::string path = nreq.getRequestURL();
            if (path.empty()) path = _url.getPath();
            std::string scheme = (_url.getProtocol() == HttpUrl::HTTP) ? "http" : "https";
            std::string authority = _url.getHost() + ":" + std::to_string(_url.getPort());

            std::vector<qpack::HeaderField> extra;
            for (HttpHeader::HeaderData *hd = nreq.getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
                const std::string &key = hd->getkey();
                if (key.empty() || key[0] == ':') continue;
                if (key == "host" || key == "connection") continue;
                for (HttpHeader::HeaderData::Values *v = hd->getfirstValue(); v; v = v->nextvalue())
                    extra.push_back({key, v->getvalue(), false});
            }

            std::vector<uint8_t> headers = qpack::Encoder::encodeRequestHeaders(
                "GET", scheme, authority, path, extra, false, 0);

            std::vector<uint8_t> stream_payload;
            h3AppendFrame(stream_payload, 0x01, headers);

            uint64_t stream_id = q->openStream(true);
            q->sendStreamData(stream_id, stream_payload, true);

            // Read until we have the HEADERS frame
            libhttppp::HttpResponse res;
            bool got_headers = false;
            _streamH3Sid = stream_id;

            // pumpNetwork() returns normally (never throws) when a non-blocking
            // read simply found no datagrams, so looping a fixed N times used
            // to always burn N non-blocking recv syscalls on every idle wait.
            // Stop as soon as our stream actually has something to show for it.
            auto h3_pump = [&]() {
                for (int pump_i = 0; pump_i < 4; ++pump_i) {
                    try {
                        q->pumpNetwork(MSG_DONTWAIT);
                    } catch (netplus::NetException &e) {
                        if (e.getErrorType() != netplus::NetException::Note)
                            throw;
                        break;
                    }
                    if (q->hasStreamData(stream_id) || q->isStreamComplete(stream_id))
                        break;
                }
            };

            // Honor setTimeout() (_recvTimeoutSec) instead of a fixed 10s: a slow
            // backend (cold model load/swap) can take much longer than 10s to
            // send back even the response headers.
            auto h3_header_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(_recvTimeoutSec);
            while (!got_headers) {
                // Drain all currently available stream data
                {
                    uint8_t rbuf[65536];
                    bool got_any = false;
                    for (;;) {
                        size_t n = q->recvStreamData(stream_id, rbuf, sizeof(rbuf));
                        if (n == 0) break;
                        _streamH3Raw.insert(_streamH3Raw.end(), rbuf, rbuf + n);
                        got_any = true;
                    }
                    if (!got_any) {
                        h3_pump();
                        for (;;) {
                            size_t n = q->recvStreamData(stream_id, rbuf, sizeof(rbuf));
                            if (n == 0) break;
                            _streamH3Raw.insert(_streamH3Raw.end(), rbuf, rbuf + n);
                            got_any = true;
                        }
                        if (!got_any) {
                            if (q->isStreamComplete(stream_id) && !q->hasStreamData(stream_id))
                                break;
                            if (q->isStreamReset(stream_id)) {
                                HTTPException ee;
                                ee[HTTPException::Error] << "HTTP/3 stream aborted by peer";
                                throw ee;
                            }
                            if (std::chrono::steady_clock::now() >= h3_header_deadline) {
                                HTTPException ee;
                                ee[HTTPException::Error] << "HTTP/3 header timeout";
                                throw ee;
                            }
                            _sw.waitRead(*_cltsock, 50);
                            continue;
                        }
                    }
                }

                // Try to parse frames (incremental: handles partial DATA frames)
                size_t pos = 0;
                // First: consume partial DATA frame continuation
                if (_streamH3InDataFrame && !_streamH3Raw.empty()) {
                    size_t avail = _streamH3Raw.size();
                    size_t take = (std::min)(avail, static_cast<size_t>(_streamH3DataRemaining));
                    _streamH3Body.insert(_streamH3Body.end(),
                        _streamH3Raw.begin(), _streamH3Raw.begin() + static_cast<ptrdiff_t>(take));
                    pos = take;
                    _streamH3DataRemaining -= take;
                    if (_streamH3DataRemaining == 0)
                        _streamH3InDataFrame = false;
                }
                while (pos < _streamH3Raw.size() && !_streamH3InDataFrame) {
                    size_t frame_start = pos;
                    size_t bytes = 0;
                    uint64_t frame_type = h3DecodeVarInt(_streamH3Raw.data() + pos, _streamH3Raw.size() - pos, bytes);
                    if (bytes == 0) break;
                    pos += bytes;
                    if (pos >= _streamH3Raw.size()) { pos = frame_start; break; }

                    uint64_t frame_len = h3DecodeVarInt(_streamH3Raw.data() + pos, _streamH3Raw.size() - pos, bytes);
                    if (bytes == 0) { pos = frame_start; break; }
                    pos += bytes;

                    if (frame_type == 0x00) { // DATA — handle incrementally
                        size_t avail = _streamH3Raw.size() - pos;
                        size_t take = (std::min)(avail, static_cast<size_t>(frame_len));
                        _streamH3Body.insert(_streamH3Body.end(),
                            _streamH3Raw.begin() + static_cast<ptrdiff_t>(pos),
                            _streamH3Raw.begin() + static_cast<ptrdiff_t>(pos + take));
                        pos += take;
                        if (take < frame_len) {
                            _streamH3InDataFrame = true;
                            _streamH3DataRemaining = frame_len - take;
                        }
                    } else if (pos + frame_len > _streamH3Raw.size()) {
                        pos = frame_start; break; // need more data for non-DATA frame
                    } else if (frame_type == 0x01) { // HEADERS
                        auto decoded = qpack::Decoder::decode(_streamH3Raw.data() + pos, frame_len);
                        for (const auto &h : decoded) {
                            if (h.name == ":status") {
                                try {
                                    res.setState(std::string("HTTP/3 ") + h.value);
                                } catch (...) {}
                            } else if (h.name == "content-length") {
                                try {
                                    res.setContentLength(static_cast<size_t>(std::stoul(h.value)));
                                    _streamRemaining = static_cast<size_t>(std::stoul(h.value));
                                } catch (...) {}
                            } else if (h.name == "content-type") {
                                res.setContentType(h.value);
                            } else {
                                res.setHeaderData(h.name.c_str())->push_back(h.value);
                            }
                        }
                        got_headers = true;
                        pos += frame_len;
                    } else {
                        pos += frame_len; // skip unknown frame types
                    }
                }
                if (pos > 0)
                    _streamH3Raw.erase(_streamH3Raw.begin(), _streamH3Raw.begin() + static_cast<ptrdiff_t>(pos));
            }

            _streamMode = STREAM_H3;
            _streamH3EndStream = q->isStreamComplete(stream_id) && _streamH3Raw.empty() && !q->hasStreamData(stream_id);
            return res;
        }

        // ── HTTP/2 streaming path ──
        if (_isH2) {
            if (!_h2PrefaceSent) {
                std::string preface(H2C_CLIENT_PREFACE, H2C_CLIENT_PREFACE_LEN);
                preface += h2cBuildSettings();
                preface += h2cConnectionWindowBoost();
                _sendAll(preface);
                _h2PrefaceSent = true;
            }

            std::string path = nreq.getRequestURL();
            if (path.empty()) path = _url.getPath();
            std::string scheme = (_url.getProtocol() == HttpUrl::HTTPS) ? "https" : "http";
            std::string auth = _url.getHost() + ":" + std::to_string(_url.getPort());

            std::vector<hpack::HeaderField> extra;
            for (HttpHeader::HeaderData *hd = nreq.getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
                const std::string &key = hd->getkey();
                if (key.empty() || key[0] == ':') continue;
                if (key == "host") continue;
                for (HttpHeader::HeaderData::Values *v = hd->getfirstValue(); v; v = v->nextvalue())
                    extra.push_back({key, v->getvalue(), false});
            }

            std::string hpack_block = hpack::Encoder::encodeRequestHeaders(
                "GET", path, scheme, auth, extra);

            uint32_t stream_id = _h2NextStreamId;
            _h2NextStreamId += 2;
            uint8_t hdr_flags = H2C_FLAG_END_HEADERS | H2C_FLAG_END_STREAM;
            std::string headers_frame = h2cBuildFrame(H2C_FRAME_HEADERS, hdr_flags,
                                                       stream_id, hpack_block);
            _sendAll(headers_frame);

            if (!_h2Decoder) _h2Decoder = std::make_unique<hpack::Decoder>();

            // Read until we get response HEADERS for our stream
            libhttppp::HttpResponse res;
            bool got_headers = false;
            _streamH2Sid = stream_id;
            _streamH2EndStream = false;

            size_t h2_off = 0;

            auto h2_ensure_bytes = [&](size_t need) {
                while (_streamH2Raw.size() - h2_off < need) {
                    size_t n = _recvBlocking(_recvScratch, 15);
                    if (n == 0) {
                        HTTPException he;
                        he[HTTPException::Error] << "HTTP/2 GetStream: EOF while reading frames";
                        throw he;
                    }
                    _streamH2Raw.insert(_streamH2Raw.end(),
                        reinterpret_cast<uint8_t*>(_recvScratch.data.buf),
                        reinterpret_cast<uint8_t*>(_recvScratch.data.buf) + n);
                }
            };

            while (!got_headers) {
                h2_ensure_bytes(H2C_FRAME_HEADER_LEN);

                uint32_t frame_len = (static_cast<uint32_t>(_streamH2Raw[h2_off]) << 16)
                                   | (static_cast<uint32_t>(_streamH2Raw[h2_off+1]) << 8)
                                   | static_cast<uint32_t>(_streamH2Raw[h2_off+2]);
                uint8_t frame_type = _streamH2Raw[h2_off+3];
                uint8_t frame_flags = _streamH2Raw[h2_off+4];
                uint32_t frame_stream = ((static_cast<uint32_t>(_streamH2Raw[h2_off+5]) & 0x7f) << 24)
                                      | (static_cast<uint32_t>(_streamH2Raw[h2_off+6]) << 16)
                                      | (static_cast<uint32_t>(_streamH2Raw[h2_off+7]) << 8)
                                      | static_cast<uint32_t>(_streamH2Raw[h2_off+8]);

                h2_ensure_bytes(H2C_FRAME_HEADER_LEN + frame_len);

                const uint8_t *payload = _streamH2Raw.data() + h2_off + H2C_FRAME_HEADER_LEN;

                switch (frame_type) {
                    case H2C_FRAME_SETTINGS: {
                        if (!(frame_flags & H2C_FLAG_ACK))
                            _sendAll(h2cBuildSettingsAck());
                        break;
                    }
                    case H2C_FRAME_HEADERS: {
                        if (frame_stream == stream_id) {
                            auto headers = _h2Decoder->decode(payload, frame_len);
                            for (const auto &h : headers) {
                                if (h.name == ":status") {
                                    try {
                                        res.setState(std::string("HTTP/2 ") + h.value);
                                    } catch (...) {}
                                } else if (h.name == "content-length") {
                                    try {
                                        res.setContentLength(static_cast<size_t>(std::stoul(h.value)));
                                        _streamRemaining = static_cast<size_t>(std::stoul(h.value));
                                    } catch (...) {}
                                } else if (h.name == "content-type") {
                                    res.setContentType(h.value);
                                } else {
                                    res.setHeaderData(h.name.c_str())->push_back(h.value);
                                }
                            }
                            got_headers = true;
                            if (frame_flags & H2C_FLAG_END_STREAM)
                                _streamH2EndStream = true;
                        }
                        break;
                    }
                    case H2C_FRAME_DATA: {
                        if (frame_stream == stream_id) {
                            // Body data arrived with headers — save for readBodyChunk
                            _streamBuf.insert(_streamBuf.end(), payload, payload + frame_len);
                            if (frame_len > 0) {
                                std::string wu = h2cBuildWindowUpdate(0, frame_len)
                                               + h2cBuildWindowUpdate(stream_id, frame_len);
                                _sendAll(wu);
                            }
                            if (frame_flags & H2C_FLAG_END_STREAM)
                                _streamH2EndStream = true;
                        }
                        break;
                    }
                    case H2C_FRAME_PING: {
                        if (!(frame_flags & H2C_FLAG_ACK)) {
                            std::string ping_payload(reinterpret_cast<const char*>(payload), frame_len);
                            _sendAll(h2cBuildFrame(H2C_FRAME_PING, H2C_FLAG_ACK, 0, ping_payload));
                        }
                        break;
                    }
                    case H2C_FRAME_GOAWAY: {
                        // Reaching this means got_headers is still false (the
                        // while-loop condition) -- our stream's HEADERS never
                        // arrived. Previously this set got_headers=true anyway,
                        // returning an empty/default HttpResponse as if the
                        // request had succeeded. Only treat it as a hard
                        // failure when the peer explicitly never processed
                        // this stream; otherwise keep reading.
                        uint32_t last_stream_id = 0;
                        if (frame_len >= 4) {
                            last_stream_id = ((static_cast<uint32_t>(payload[0]) & 0x7f) << 24)
                                           | (static_cast<uint32_t>(payload[1]) << 16)
                                           | (static_cast<uint32_t>(payload[2]) << 8)
                                           | static_cast<uint32_t>(payload[3]);
                        }
                        if (stream_id > last_stream_id) {
                            HTTPException ee;
                            ee[HTTPException::Error]
                                << "HTTP/2 GetStream: GOAWAY before stream " << stream_id
                                << " was processed (last_stream_id=" << last_stream_id << ")";
                            throw ee;
                        }
                        break;
                    }
                    default:
                        break;
                }

                h2_off += H2C_FRAME_HEADER_LEN + frame_len;
            }

            // Single compact: remove all consumed bytes at once
            if (h2_off > 0) {
                _streamH2Raw.erase(_streamH2Raw.begin(),
                    _streamH2Raw.begin() + static_cast<ptrdiff_t>(h2_off));
            }

            _streamMode = STREAM_H2;
            _streamBufPos = 0;
            return res;
        }

        // ── HTTP/1.x streaming path ──
        if (nreq.getRequestVersion().empty())
            nreq.setRequestVersion(HTTPVERSION(1.1));

        // Send request header
        {
            std::string header;
            nreq.printHeader(header);
            if (!_cltsock || _cltsock->fd() < 0)
                resetConnection();
            _sendAll(header);
        }

        // Read until we have full response header (\r\n\r\n)
        std::vector<char> raw;
        raw.reserve(16384);
        size_t header_end = std::string::npos;
        size_t search_from = 0;

        // Single deadline for the whole header, not one per chunk — a peer
        // trickling a byte every few seconds must not be able to hold this
        // loop (and the calling thread) open indefinitely. Honors setTimeout()
        // (_recvTimeoutSec) instead of a fixed 10s: a slow backend (cold model
        // load/swap) can take much longer than 10s to send back even the
        // response headers.
        auto header_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(_recvTimeoutSec);

        for (;;) {
            if (raw.size() >= 4) {
                size_t start = (search_from >= 3) ? search_from - 3 : 0;
                for (size_t i = start; i + 3 < raw.size(); ++i) {
                    if (raw[i] == '\r' && raw[i+1] == '\n' &&
                        raw[i+2] == '\r' && raw[i+3] == '\n')
                    {
                        header_end = i + 4;
                        break;
                    }
                }
                search_from = raw.size();
            }
            if (header_end != std::string::npos)
                break;

            auto now = std::chrono::steady_clock::now();
            if (now >= header_deadline) {
                HTTPException he;
                he[HTTPException::Error] << "HTTP GetStream: timeout while reading header";
                throw he;
            }
            int remaining_sec = static_cast<int>(std::chrono::duration_cast<std::chrono::seconds>(
                header_deadline - now).count());
            if (remaining_sec <= 0)
                remaining_sec = 1;

            size_t n = _recvBlocking(_recvScratch, remaining_sec);
            if (n == 0) {
                netplus::NetException ne;
                ne[netplus::NetException::Error] << "HTTP GetStream: EOF while reading header";
                throw ne;
            }
            raw.insert(raw.end(), _recvScratch.data.buf, _recvScratch.data.buf + n);
        }

        // Parse header
        libhttppp::HttpResponse res;
        size_t parsed_hsize = res.parse(raw.data(), raw.size());

        if (parsed_hsize == 0 || parsed_hsize > raw.size()) {
            libhttppp::HTTPException he;
            he[libhttppp::HTTPException::Error] << "HTTP GetStream: response header parse failed";
            throw he;
        }

        // Save leftover body data that was read together with headers
        size_t body_off = parsed_hsize;
        if (raw.size() > body_off) {
            _streamBuf.assign(raw.begin() + (ptrdiff_t)body_off, raw.end());
        }
        _streamBufPos = 0;

        // Determine transfer mode
        bool chunked = false;
        ptrdiff_t content_len = -1;

        try {
            for (libhttppp::HttpHeader::HeaderData::Values *v = res.getTransferEncoding();
                 v; v = v->nextvalue())
            {
                std::string val = tolower_copy(v->getvalue());
                if (val == "chunked") {
                    chunked = true;
                    break;
                }
            }
        } catch (...) {}

        // See _h1ReadResponse() for why presence is checked explicitly
        // rather than trusting getContentLength()'s ambiguous 0 return.
        if (!chunked && res.getHeaderData("content-length")) {
            try {
                content_len = static_cast<ptrdiff_t>(res.getContentLength());
                if (content_len < 0) content_len = -1;
            } catch (...) {
                content_len = -1;
            }
        }

        if (chunked) {
            _streamMode = STREAM_CHUNKED;
            _streamChunkDone = false;
            _streamChunkRemaining = 0;
        } else if (content_len >= 0) {
            _streamMode = STREAM_CONTENT_LENGTH;
            _streamRemaining = (size_t)content_len;
            // Subtract what's already in the leftover buffer
            size_t have = _streamBuf.size() - _streamBufPos;
            if (have > _streamRemaining) {
                // More data than content-length (shouldn't happen, but be safe)
                _streamBuf.resize(_streamBufPos + _streamRemaining);
            }
        } else {
            _streamMode = STREAM_EOF;
        }

        return res;

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

libhttppp::HttpResponse libhttppp::HttpClient::PostStream(libhttppp::HttpRequest &nreq,
                                                            const std::vector<char> &postBody) {
    try {
        _ensureConnected();

        // Reset any previous streaming state (mirrors GetStream)
        _streamMode = STREAM_NONE;
        _streamBuf.clear();
        _streamBuf.reserve(CHUNKSIZE);
        _streamBufPos = 0;
        _streamRemaining = 0;
        _streamChunkDone = false;
        _streamChunkRemaining = 0;
        _streamH2Sid = 0;
        _streamH2EndStream = false;
        _streamH2Raw.clear();
        _streamH2Raw.reserve(CHUNKSIZE);
        _streamH3Sid = 0;
        _streamH3EndStream = false;
        _streamH3Raw.clear();
        _streamH3Raw.reserve(CHUNKSIZE);
        _streamH3Body.clear();
        _streamH3Body.reserve(CHUNKSIZE);
        _streamH3BodyPos = 0;
        _streamH3InDataFrame = false;
        _streamH3DataRemaining = 0;

        if (dynamic_cast<netplus::quic*>(_cltsock.get()) || _isH2) {
            HTTPException he;
            he[HTTPException::Error] << "PostStream: only HTTP/1.1 upstream connections are supported";
            throw he;
        }

        if (!nreq.getHeaderData("host"))
            nreq.setHeaderData("host")->push_back(_url.getHost() + ":" + std::to_string(_url.getPort()));
        nreq.setRequestType(POSTREQUEST);

        if (nreq.getRequestURL().empty())
            nreq.setRequestURL(_url.getPath());

        nreq.setHeaderData("content-length")->push_back(std::to_string(postBody.size()));

        if (nreq.getRequestVersion().empty())
            nreq.setRequestVersion(HTTPVERSION(1.1));

        // Send header + body together for best TCP segment utilization
        {
            std::string header;
            nreq.printHeader(header);
            if (!_cltsock || _cltsock->fd() < 0)
                resetConnection();

            std::vector<char> combined;
            combined.reserve(header.size() + postBody.size());
            combined.insert(combined.end(), header.begin(), header.end());
            combined.insert(combined.end(), postBody.begin(), postBody.end());
            _sendAll(combined.data(), combined.size());
        }

        // Read until we have full response header (\r\n\r\n) -- same structure
        // as GetStream's HTTP/1.x header-read, except the deadline honors
        // setTimeout() (_recvTimeoutSec) instead of a fixed 10s: a slow AI
        // backend (cold model load/swap) can take much longer than 10s to
        // send back even the response headers, well before any body/stream
        // data starts flowing.
        std::vector<char> raw;
        raw.reserve(16384);
        size_t header_end = std::string::npos;
        size_t search_from = 0;

        auto header_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(_recvTimeoutSec);

        for (;;) {
            if (raw.size() >= 4) {
                size_t start = (search_from >= 3) ? search_from - 3 : 0;
                for (size_t i = start; i + 3 < raw.size(); ++i) {
                    if (raw[i] == '\r' && raw[i+1] == '\n' &&
                        raw[i+2] == '\r' && raw[i+3] == '\n')
                    {
                        header_end = i + 4;
                        break;
                    }
                }
                search_from = raw.size();
            }
            if (header_end != std::string::npos)
                break;

            auto now = std::chrono::steady_clock::now();
            if (now >= header_deadline) {
                HTTPException he;
                he[HTTPException::Error] << "HTTP PostStream: timeout while reading header";
                throw he;
            }
            int remaining_sec = static_cast<int>(std::chrono::duration_cast<std::chrono::seconds>(
                header_deadline - now).count());
            if (remaining_sec <= 0)
                remaining_sec = 1;

            size_t n = _recvBlocking(_recvScratch, remaining_sec);
            if (n == 0) {
                netplus::NetException ne;
                ne[netplus::NetException::Error] << "HTTP PostStream: EOF while reading header";
                throw ne;
            }
            raw.insert(raw.end(), _recvScratch.data.buf, _recvScratch.data.buf + n);
        }

        libhttppp::HttpResponse res;
        size_t parsed_hsize = res.parse(raw.data(), raw.size());

        if (parsed_hsize == 0 || parsed_hsize > raw.size()) {
            libhttppp::HTTPException he;
            he[libhttppp::HTTPException::Error] << "HTTP PostStream: response header parse failed";
            throw he;
        }

        size_t body_off = parsed_hsize;
        if (raw.size() > body_off) {
            _streamBuf.assign(raw.begin() + (ptrdiff_t)body_off, raw.end());
        }
        _streamBufPos = 0;

        bool chunked = false;
        ptrdiff_t content_len = -1;

        try {
            for (libhttppp::HttpHeader::HeaderData::Values *v = res.getTransferEncoding();
                 v; v = v->nextvalue())
            {
                std::string val = tolower_copy(v->getvalue());
                if (val == "chunked") {
                    chunked = true;
                    break;
                }
            }
        } catch (...) {}

        // See _h1ReadResponse() for why presence is checked explicitly
        // rather than trusting getContentLength()'s ambiguous 0 return.
        if (!chunked && res.getHeaderData("content-length")) {
            try {
                content_len = static_cast<ptrdiff_t>(res.getContentLength());
                if (content_len < 0) content_len = -1;
            } catch (...) {
                content_len = -1;
            }
        }

        if (chunked) {
            _streamMode = STREAM_CHUNKED;
            _streamChunkDone = false;
            _streamChunkRemaining = 0;
        } else if (content_len >= 0) {
            _streamMode = STREAM_CONTENT_LENGTH;
            _streamRemaining = (size_t)content_len;
            size_t have = _streamBuf.size() - _streamBufPos;
            if (have > _streamRemaining) {
                _streamBuf.resize(_streamBufPos + _streamRemaining);
            }
        } else {
            _streamMode = STREAM_EOF;
        }

        return res;

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

// Tries to pull more bytes off the wire into _streamBuf. `blocking` selects
// _recvBlocking() (waits up to the recv timeout) vs _recvNonBlocking()
// (returns immediately if nothing is available). Returns true iff bytes
// were appended. Returns false otherwise; `eof` distinguishes why:
//   blocking=true,  false return => eof is always true (a clean EOF, e.g.
//                   TLS close_notify -- _recvBlocking() never returns 0
//                   without one, and any other failure throws)
//   blocking=false, false return => eof is always false (EAGAIN, i.e. no
//                   data is available *yet*); a real connection-ending
//                   condition in non-blocking mode throws instead, exactly
//                   like it would in blocking mode
// Does NOT itself catch exceptions: a chunked body ending mid-frame without
// its terminator is a real protocol error and should propagate, whereas an
// EOF-terminated body's caller (_streamReadEof) treats connection-ending
// exceptions as a normal, successful end of body -- this distinction
// already existed between the two modes before this method existed, and is
// preserved by leaving the choice to each caller instead of swallowing
// exceptions here.
bool libhttppp::HttpClient::_streamRecvMore(bool blocking, bool &eof) {
    eof = false;
    size_t n = blocking ? _recvBlocking(_recvScratch) : _recvNonBlocking(_recvScratch);
    if (n == 0) {
        if (blocking) eof = true;
        return false;
    }
    // Compact only when consumed portion exceeds half the buffer, to
    // amortize the O(n) memmove cost of dropping the consumed prefix.
    if (_streamBufPos > 0 && _streamBufPos >= _streamBuf.size() / 2) {
        _streamBuf.erase(_streamBuf.begin(), _streamBuf.begin() + (ptrdiff_t)_streamBufPos);
        _streamBufPos = 0;
    }
    _streamBuf.insert(_streamBuf.end(), _recvScratch.data.buf, _recvScratch.data.buf + n);
    return true;
}

// Single implementation of chunked-transfer-encoding body reading shared by
// readBodyChunk() (blocking=true) and readBodyChunkNonBlocking()
// (blocking=false) -- previously the non-blocking variant didn't implement
// this mode at all and silently fell back to the blocking one, meaning a
// caller using the non-blocking API to avoid stalling its thread would
// still stall for up to the recv timeout on a chunked response.
// Returns: bytes read (>0), 0 (blocking=false only: no data available yet),
// or (size_t)-1 (body complete).
size_t libhttppp::HttpClient::_streamReadChunked(char *buf, size_t bufsize, bool blocking) {
    if (_streamChunkDone) {
        _streamMode = STREAM_NONE;
        return static_cast<size_t>(-1);
    }

    auto drainBuf = [&](size_t maxBytes) -> size_t {
        size_t have = _streamBuf.size() - _streamBufPos;
        if (have == 0) return 0;
        size_t take = (std::min)(have, maxBytes);
        std::memcpy(buf, _streamBuf.data() + _streamBufPos, take);
        _streamBufPos += take;
        if (_streamBufPos == _streamBuf.size()) {
            _streamBuf.clear();
            _streamBufPos = 0;
        }
        return take;
    };

    enum class LineResult { Found, WouldBlock, Eof };
    auto readLine = [&](std::string &out) -> LineResult {
        for (;;) {
            size_t i = _streamBufPos;
            for (; i + 1 < _streamBuf.size(); ++i) {
                if (_streamBuf[i] == '\r' && _streamBuf[i + 1] == '\n') break;
            }
            if (i + 1 < _streamBuf.size()) {
                out.assign(_streamBuf.begin() + (ptrdiff_t)_streamBufPos,
                           _streamBuf.begin() + (ptrdiff_t)i);
                _streamBufPos = i + 2;
                return LineResult::Found;
            }
            bool eof = false;
            if (!_streamRecvMore(blocking, eof))
                return (!blocking) ? LineResult::WouldBlock : LineResult::Eof;
        }
    };

    enum class DataResult { Got, WouldBlock, Eof };
    auto readChunkData = [&](size_t &outGot) -> DataResult {
        size_t want = (std::min)(bufsize, _streamChunkRemaining);
        size_t got = drainBuf(want);
        while (got == 0) {
            bool eof = false;
            if (!_streamRecvMore(blocking, eof))
                return (!blocking) ? DataResult::WouldBlock : DataResult::Eof;
            got = drainBuf(want);
        }
        outGot = got;
        return DataResult::Got;
    };

    if (_streamChunkRemaining > 0) {
        size_t got = 0;
        switch (readChunkData(got)) {
            case DataResult::WouldBlock:
                return 0;
            case DataResult::Eof:
                // Clean EOF mid-chunk: matches the pre-existing (lenient)
                // behavior of treating it as an early-but-clean body end
                // rather than a hard error.
                _streamChunkDone = true;
                _streamMode = STREAM_NONE;
                return static_cast<size_t>(-1);
            case DataResult::Got:
                _streamChunkRemaining -= got;
                return got;
        }
    }

    for (;;) {
        std::string line;
        LineResult lr = readLine(line);
        if (lr == LineResult::WouldBlock) return 0;
        if (lr == LineResult::Eof) {
            _streamChunkDone = true;
            _streamMode = STREAM_NONE;
            return static_cast<size_t>(-1);
        }
        // An empty line here is the previous chunk's trailing CRLF
        // terminator (deferred from the last call if it hadn't arrived yet
        // when that chunk's data was fully delivered) -- skip it and read
        // the actual next line.
        if (line.empty()) continue;

        auto sem = line.find(';');
        if (sem != std::string::npos) line.resize(sem);

        size_t chunk_size = 0;
        try {
            chunk_size = static_cast<size_t>(std::stoul(line, nullptr, 16));
        } catch (...) {
            _streamChunkDone = true;
            _streamMode = STREAM_NONE;
            return static_cast<size_t>(-1);
        }

        if (chunk_size == 0) {
            // Final chunk -- consume trailers (bounded to prevent unbounded
            // memory growth from a malicious server).
            for (int trailer_i = 0; trailer_i < 128; ++trailer_i) {
                std::string tline;
                LineResult tr = readLine(tline);
                if (tr == LineResult::WouldBlock) return 0;
                if (tr == LineResult::Eof || tline.empty()) break;
            }
            _streamChunkDone = true;
            _streamMode = STREAM_NONE;
            return static_cast<size_t>(-1);
        }

        _streamChunkRemaining = chunk_size;
        size_t got = 0;
        switch (readChunkData(got)) {
            case DataResult::WouldBlock:
                return 0;
            case DataResult::Eof:
                _streamChunkDone = true;
                _streamMode = STREAM_NONE;
                return static_cast<size_t>(-1);
            case DataResult::Got:
                _streamChunkRemaining -= got;
                return got;
        }
    }
}

// Single implementation of EOF-terminated (no Content-Length, not chunked)
// body reading shared by readBodyChunk()/readBodyChunkNonBlocking(); see
// _streamReadChunked() for why this collapses what used to be a
// blocking-only implementation (silently reused via fallback for the
// non-blocking caller) into one implementation parameterized by `blocking`.
// Returns: bytes read (>0), 0 (blocking=false only: no data available yet),
// or (size_t)-1 (body complete).
size_t libhttppp::HttpClient::_streamReadEof(char *buf, size_t bufsize, bool blocking) {
    size_t have = _streamBuf.size() - _streamBufPos;
    if (have > 0) {
        size_t take = (std::min)(have, bufsize);
        std::memcpy(buf, _streamBuf.data() + _streamBufPos, take);
        _streamBufPos += take;
        if (_streamBufPos == _streamBuf.size()) {
            _streamBuf.clear();
            _streamBufPos = 0;
        }
        return take;
    }

    bool eof = false;
    bool progressed;
    try {
        progressed = _streamRecvMore(blocking, eof);
    } catch (...) {
        // An EOF-terminated body has no explicit terminator to wait for, so
        // any connection-ending condition (including a plain-TCP "peer
        // closed" exception, unlike the clean 0-return TLS gives) means the
        // body is done, not that something went wrong.
        _streamMode = STREAM_NONE;
        return static_cast<size_t>(-1);
    }
    if (!progressed) {
        if (eof) {
            _streamMode = STREAM_NONE;
            return static_cast<size_t>(-1);
        }
        return 0; // non-blocking: no data yet
    }

    have = _streamBuf.size() - _streamBufPos;
    size_t take = (std::min)(have, bufsize);
    std::memcpy(buf, _streamBuf.data() + _streamBufPos, take);
    _streamBufPos += take;
    if (_streamBufPos == _streamBuf.size()) {
        _streamBuf.clear();
        _streamBufPos = 0;
    }
    return take;
}

// Single implementation of HTTP/2 DATA-frame body reading shared by
// readBodyChunk() (blocking=true) and readBodyChunkNonBlocking()
// (blocking=false). The frame dispatch (SETTINGS/DATA/HEADERS/
// WINDOW_UPDATE/PING/GOAWAY) is written once in processFrame below; only
// how bytes are pulled off the wire differs by mode -- blocking waits
// (looping _recvBlocking) until there's body data to return or the stream
// ends, non-blocking makes exactly one _recvNonBlocking attempt and then
// drains whatever complete frames that leaves buffered, never waiting.
// Returns: bytes read (>0), 0 (blocking=false only: no data available yet),
// or (size_t)-1 (body complete).
size_t libhttppp::HttpClient::_streamReadH2(char *buf, size_t bufsize, bool blocking) {
    auto drainBuf = [&](size_t maxBytes) -> size_t {
        size_t have = _streamBuf.size() - _streamBufPos;
        if (have == 0) return 0;
        size_t take = (std::min)(have, maxBytes);
        std::memcpy(buf, _streamBuf.data() + _streamBufPos, take);
        _streamBufPos += take;
        if (_streamBufPos == _streamBuf.size()) {
            _streamBuf.clear();
            _streamBufPos = 0;
        }
        return take;
    };

    size_t got = drainBuf(bufsize);
    if (got > 0) return got;

    if (_streamH2EndStream) {
        _streamMode = STREAM_NONE;
        return static_cast<size_t>(-1);
    }

    size_t h2_off = 0;

    // Frame dispatch, shared verbatim between both modes below.
    auto processFrame = [&](uint32_t frame_len, uint8_t frame_type, uint8_t frame_flags,
                             uint32_t frame_stream, const uint8_t *payload) {
        switch (frame_type) {
            case H2C_FRAME_SETTINGS:
                if (!(frame_flags & H2C_FLAG_ACK)) _sendAll(h2cBuildSettingsAck());
                break;
            case H2C_FRAME_DATA:
                if (frame_stream == _streamH2Sid) {
                    if (_streamBufPos > 0) {
                        _streamBuf.erase(_streamBuf.begin(),
                            _streamBuf.begin() + static_cast<ptrdiff_t>(_streamBufPos));
                        _streamBufPos = 0;
                    }
                    _streamBuf.insert(_streamBuf.end(), payload, payload + frame_len);
                    if (frame_len > 0) {
                        // Batch both WINDOW_UPDATE frames into a single send
                        std::string wu = h2cBuildWindowUpdate(0, frame_len)
                                       + h2cBuildWindowUpdate(_streamH2Sid, frame_len);
                        _sendAll(wu);
                    }
                    if (frame_flags & H2C_FLAG_END_STREAM) _streamH2EndStream = true;
                }
                break;
            case H2C_FRAME_HEADERS:
                // Trailing headers
                if (frame_stream == _streamH2Sid && (frame_flags & H2C_FLAG_END_STREAM))
                    _streamH2EndStream = true;
                break;
            case H2C_FRAME_WINDOW_UPDATE:
                // Server-side window update: nothing to do for client reads.
                break;
            case H2C_FRAME_PING:
                if (!(frame_flags & H2C_FLAG_ACK)) {
                    std::string ping_payload(reinterpret_cast<const char*>(payload), frame_len);
                    _sendAll(h2cBuildFrame(H2C_FRAME_PING, H2C_FLAG_ACK, 0, ping_payload));
                }
                break;
            case H2C_FRAME_GOAWAY:
                _streamH2EndStream = true;
                break;
            default:
                break;
        }
    };

    if (blocking) {
        auto ensureBytes = [&](size_t need) -> bool {
            while (_streamH2Raw.size() - h2_off < need) {
                size_t n = _recvBlocking(_recvScratch);
                if (n == 0) { _streamH2EndStream = true; return false; }
                _streamH2Raw.insert(_streamH2Raw.end(),
                    reinterpret_cast<uint8_t*>(_recvScratch.data.buf),
                    reinterpret_cast<uint8_t*>(_recvScratch.data.buf) + n);
            }
            return true;
        };

        while (_streamBuf.empty() && !_streamH2EndStream) {
            if (!ensureBytes(H2C_FRAME_HEADER_LEN)) break;

            uint32_t frame_len = (static_cast<uint32_t>(_streamH2Raw[h2_off]) << 16)
                               | (static_cast<uint32_t>(_streamH2Raw[h2_off+1]) << 8)
                               | static_cast<uint32_t>(_streamH2Raw[h2_off+2]);
            uint8_t frame_type = _streamH2Raw[h2_off+3];
            uint8_t frame_flags = _streamH2Raw[h2_off+4];
            uint32_t frame_stream = ((static_cast<uint32_t>(_streamH2Raw[h2_off+5]) & 0x7f) << 24)
                                  | (static_cast<uint32_t>(_streamH2Raw[h2_off+6]) << 16)
                                  | (static_cast<uint32_t>(_streamH2Raw[h2_off+7]) << 8)
                                  | static_cast<uint32_t>(_streamH2Raw[h2_off+8]);

            if (!ensureBytes(H2C_FRAME_HEADER_LEN + frame_len)) break;

            const uint8_t *payload = _streamH2Raw.data() + h2_off + H2C_FRAME_HEADER_LEN;
            processFrame(frame_len, frame_type, frame_flags, frame_stream, payload);
            h2_off += H2C_FRAME_HEADER_LEN + frame_len;
        }
    } else {
        // Exactly one non-blocking attempt to top up _streamH2Raw, then
        // drain whatever complete frames that leaves buffered -- never wait.
        size_t n = _recvNonBlocking(_recvScratch);
        if (n > 0) {
            _streamH2Raw.insert(_streamH2Raw.end(),
                reinterpret_cast<uint8_t*>(_recvScratch.data.buf),
                reinterpret_cast<uint8_t*>(_recvScratch.data.buf) + n);
        }

        while (_streamH2Raw.size() - h2_off >= H2C_FRAME_HEADER_LEN) {
            uint32_t frame_len = (static_cast<uint32_t>(_streamH2Raw[h2_off]) << 16)
                               | (static_cast<uint32_t>(_streamH2Raw[h2_off+1]) << 8)
                               | static_cast<uint32_t>(_streamH2Raw[h2_off+2]);
            uint8_t frame_type = _streamH2Raw[h2_off+3];
            uint8_t frame_flags = _streamH2Raw[h2_off+4];
            uint32_t frame_stream = ((static_cast<uint32_t>(_streamH2Raw[h2_off+5]) & 0x7f) << 24)
                                  | (static_cast<uint32_t>(_streamH2Raw[h2_off+6]) << 16)
                                  | (static_cast<uint32_t>(_streamH2Raw[h2_off+7]) << 8)
                                  | static_cast<uint32_t>(_streamH2Raw[h2_off+8]);

            if (_streamH2Raw.size() - h2_off < H2C_FRAME_HEADER_LEN + frame_len)
                break; // incomplete frame, wait for more data

            const uint8_t *payload = _streamH2Raw.data() + h2_off + H2C_FRAME_HEADER_LEN;
            processFrame(frame_len, frame_type, frame_flags, frame_stream, payload);
            h2_off += H2C_FRAME_HEADER_LEN + frame_len;
        }
    }

    // Single compact: remove all consumed bytes at once
    if (h2_off > 0) {
        _streamH2Raw.erase(_streamH2Raw.begin(), _streamH2Raw.begin() + static_cast<ptrdiff_t>(h2_off));
    }

    got = drainBuf(bufsize);
    if (got > 0) return got;

    if (_streamH2EndStream) {
        _streamMode = STREAM_NONE;
        return static_cast<size_t>(-1);
    }
    return 0;
}

// Single implementation of HTTP/3 (QUIC) DATA-frame body reading shared by
// readBodyChunk() (blocking=true) and readBodyChunkNonBlocking()
// (blocking=false). The QUIC-varint frame parser (partial-DATA-frame
// continuation included) and the "is this stream actually finished" check
// are written once in parseFrames/streamFullyDone below; only how bytes are
// pulled off the wire differs by mode -- blocking loops (pump + wait) until
// there's body data to return, the stream ends, or it's aborted/times out;
// non-blocking makes one pump+drain attempt and returns immediately either
// way.
// Returns: bytes read (>0), 0 (blocking=false only: no data available yet),
// or (size_t)-1 (body complete).
size_t libhttppp::HttpClient::_streamReadH3(char *buf, size_t bufsize, bool blocking) {
    auto drainBody = [&]() -> size_t {
        if (_streamH3BodyPos >= _streamH3Body.size()) return 0;
        size_t have = _streamH3Body.size() - _streamH3BodyPos;
        size_t take = (std::min)(have, bufsize);
        std::memcpy(buf, _streamH3Body.data() + _streamH3BodyPos, take);
        _streamH3BodyPos += take;
        if (_streamH3BodyPos == _streamH3Body.size()) {
            _streamH3Body.clear();
            _streamH3BodyPos = 0;
        }
        return take;
    };

    size_t got = drainBody();
    if (got > 0) return got;

    if (_streamH3EndStream) {
        _streamMode = STREAM_NONE;
        return static_cast<size_t>(-1);
    }

    auto *q = dynamic_cast<netplus::quic*>(_cltsock.get());
    if (!q) {
        _streamMode = STREAM_NONE;
        return static_cast<size_t>(-1);
    }

    // pumpNetwork() returns normally (never throws) on an empty non-blocking
    // read, so a fixed 256-iteration loop used to always cost 256
    // non-blocking recv syscalls per call -- stop as soon as our stream
    // actually has something to show for it.
    auto h3_pump = [&]() {
        for (int pump_i = 0; pump_i < 4; ++pump_i) {
            try {
                q->pumpNetwork(MSG_DONTWAIT);
            } catch (netplus::NetException &e) {
                if (e.getErrorType() != netplus::NetException::Note) throw;
                break;
            }
            if (q->hasStreamData(_streamH3Sid) || q->isStreamComplete(_streamH3Sid)) break;
        }
    };

    // Parses whatever's currently buffered in _streamH3Raw into
    // _streamH3Body (handles partial DATA frames across calls).
    auto parseFrames = [&]() {
        size_t pos = 0;
        if (_streamH3InDataFrame && !_streamH3Raw.empty()) {
            size_t avail = _streamH3Raw.size();
            size_t take = (std::min)(avail, static_cast<size_t>(_streamH3DataRemaining));
            _streamH3Body.insert(_streamH3Body.end(),
                _streamH3Raw.begin(), _streamH3Raw.begin() + static_cast<ptrdiff_t>(take));
            pos = take;
            _streamH3DataRemaining -= take;
            if (_streamH3DataRemaining == 0) _streamH3InDataFrame = false;
        }
        while (pos < _streamH3Raw.size() && !_streamH3InDataFrame) {
            size_t frame_start = pos;
            size_t bytes = 0;
            uint64_t frame_type = h3DecodeVarInt(_streamH3Raw.data() + pos, _streamH3Raw.size() - pos, bytes);
            if (bytes == 0) break;
            pos += bytes;
            if (pos >= _streamH3Raw.size()) { pos = frame_start; break; }

            uint64_t frame_len = h3DecodeVarInt(_streamH3Raw.data() + pos, _streamH3Raw.size() - pos, bytes);
            if (bytes == 0) { pos = frame_start; break; }
            pos += bytes;

            if (frame_type == 0x00) { // DATA — handle incrementally
                size_t avail = _streamH3Raw.size() - pos;
                size_t take = (std::min)(avail, static_cast<size_t>(frame_len));
                _streamH3Body.insert(_streamH3Body.end(),
                    _streamH3Raw.begin() + static_cast<ptrdiff_t>(pos),
                    _streamH3Raw.begin() + static_cast<ptrdiff_t>(pos + take));
                pos += take;
                if (take < frame_len) {
                    _streamH3InDataFrame = true;
                    _streamH3DataRemaining = frame_len - take;
                }
            } else if (pos + frame_len > _streamH3Raw.size()) {
                pos = frame_start; break;
            } else {
                pos += frame_len;
            }
        }
        if (pos > 0)
            _streamH3Raw.erase(_streamH3Raw.begin(), _streamH3Raw.begin() + static_cast<ptrdiff_t>(pos));
    };

    auto streamFullyDone = [&]() -> bool {
        return q->isStreamComplete(_streamH3Sid) && _streamH3Raw.empty()
            && _streamH3BodyPos >= _streamH3Body.size()
            && !_streamH3InDataFrame && !q->hasStreamData(_streamH3Sid);
    };

    uint8_t rbuf[65536];

    if (blocking) {
        // Block until data arrives or stream completes (with timeout).
        // Unlike the non-blocking variant, this must not return 0 when data
        // is simply not available yet -- that would cause the caller to
        // think the stream is done.
        auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60);

        while (_streamH3BodyPos >= _streamH3Body.size()) {
            bool got_any = false;
            for (;;) {
                size_t n = q->recvStreamData(_streamH3Sid, rbuf, sizeof(rbuf));
                if (n == 0) break;
                _streamH3Raw.insert(_streamH3Raw.end(), rbuf, rbuf + n);
                got_any = true;
            }
            if (!got_any) {
                h3_pump();
                for (;;) {
                    size_t n = q->recvStreamData(_streamH3Sid, rbuf, sizeof(rbuf));
                    if (n == 0) break;
                    _streamH3Raw.insert(_streamH3Raw.end(), rbuf, rbuf + n);
                    got_any = true;
                }
            }
            if (!got_any) {
                if (streamFullyDone()) {
                    _streamH3EndStream = true;
                    _streamMode = STREAM_NONE;
                    return static_cast<size_t>(-1);
                }
                if (q->isStreamReset(_streamH3Sid)) {
                    // Upstream aborted mid-body (e.g. it stalled short of
                    // the advertised Content-Length). Surface this as an
                    // error rather than silently handing back a truncated
                    // body (a partially-decoded image, etc).
                    _streamMode = STREAM_NONE;
                    HTTPException ee;
                    ee[HTTPException::Error] << "HTTP/3 stream aborted by peer";
                    throw ee;
                }
                if (std::chrono::steady_clock::now() >= deadline) {
                    HTTPException ee;
                    ee[HTTPException::Error] << "HTTP/3 stream read timeout";
                    throw ee;
                }
                _sw.waitRead(*_cltsock, 100);
                continue;
            }

            parseFrames();

            if (streamFullyDone()) {
                _streamH3EndStream = true;
                _streamMode = STREAM_NONE;
                return static_cast<size_t>(-1);
            }
        }
    } else {
        h3_pump();

        bool got_any = false;
        for (;;) {
            size_t n = q->recvStreamData(_streamH3Sid, rbuf, sizeof(rbuf));
            if (n == 0) break;
            _streamH3Raw.insert(_streamH3Raw.end(), rbuf, rbuf + n);
            got_any = true;
        }
        if (!got_any && _streamH3Raw.empty() && !_streamH3InDataFrame) {
            if (streamFullyDone()) {
                _streamH3EndStream = true;
                _streamMode = STREAM_NONE;
                return static_cast<size_t>(-1);
            }
            if (q->isStreamReset(_streamH3Sid)) {
                // RESET_STREAM never sets recv_complete, so without this
                // check a caller looping on 0 would retry forever.
                _streamMode = STREAM_NONE;
                return static_cast<size_t>(-1);
            }
            return 0; // no data yet — caller should retry later
        }

        parseFrames();

        if (streamFullyDone()) _streamH3EndStream = true;
    }

    got = drainBody();
    if (got > 0) return got;

    if (_streamH3EndStream) {
        _streamMode = STREAM_NONE;
        return static_cast<size_t>(-1);
    }
    return 0;
}

size_t libhttppp::HttpClient::readBodyChunk(char *buf, size_t bufsize) {
    if (_streamMode == STREAM_NONE || bufsize == 0)
        return 0;

    // Helper: drain from internal leftover buffer
    auto drainBuf = [&](size_t maxBytes) -> size_t {
        size_t have = _streamBuf.size() - _streamBufPos;
        if (have == 0) return 0;
        size_t take = (std::min)(have, maxBytes);
        std::memcpy(buf, _streamBuf.data() + _streamBufPos, take);
        _streamBufPos += take;
        // Compact buffer when fully consumed
        if (_streamBufPos == _streamBuf.size()) {
            _streamBuf.clear();
            _streamBufPos = 0;
        }
        return take;
    };

    // ── Content-Length mode ──
    if (_streamMode == STREAM_CONTENT_LENGTH) {
        if (_streamRemaining == 0) {
            _streamMode = STREAM_NONE;
            return 0;
        }

        size_t want = (std::min)(bufsize, _streamRemaining);

        // First try leftover buffer
        size_t got = drainBuf(want);
        if (got > 0) {
            _streamRemaining -= got;
            if (_streamRemaining == 0)
                _streamMode = STREAM_NONE;
            return got;
        }

        // Read from socket
        size_t n = _recvBlocking(_recvScratch);
        if (n == 0) {
            _streamMode = STREAM_NONE;
            return 0;
        }
        size_t take = (std::min)({n, want, bufsize});
        std::memcpy(buf, _recvScratch.data.buf, take);
        _streamRemaining -= take;

        // Save excess data for next call
        if (take < n) {
            _streamBuf.assign(_recvScratch.data.buf + take, _recvScratch.data.buf + n);
            _streamBufPos = 0;
        }

        if (_streamRemaining == 0)
            _streamMode = STREAM_NONE;
        return take;
    }

    // ── Chunked / EOF modes ──
    // Shared with readBodyChunkNonBlocking() via one implementation
    // parameterized by `blocking` (see _streamReadChunked/_streamReadEof).
    // (size_t)-1 (their "body complete" signal) is translated to 0 here to
    // match this function's own contract.
    if (_streamMode == STREAM_CHUNKED) {
        size_t n = _streamReadChunked(buf, bufsize, /*blocking=*/true);
        return (n == static_cast<size_t>(-1)) ? 0 : n;
    }
    if (_streamMode == STREAM_EOF) {
        size_t n = _streamReadEof(buf, bufsize, /*blocking=*/true);
        return (n == static_cast<size_t>(-1)) ? 0 : n;
    }

    // ── HTTP/2 streaming mode ──
    // HTTP/2 mode -- shared with readBodyChunkNonBlocking() via one
    // implementation parameterized by `blocking` (see _streamReadH2()).
    if (_streamMode == STREAM_H2) {
        size_t n = _streamReadH2(buf, bufsize, /*blocking=*/true);
        return (n == static_cast<size_t>(-1)) ? 0 : n;
    }

    // ── HTTP/3 streaming mode ──
    // HTTP/3 mode -- shared with readBodyChunkNonBlocking() via one
    // implementation parameterized by `blocking` (see _streamReadH3()).
    if (_streamMode == STREAM_H3) {
        size_t n = _streamReadH3(buf, bufsize, /*blocking=*/true);
        return (n == static_cast<size_t>(-1)) ? 0 : n;
    }

    return 0;
}

size_t libhttppp::HttpClient::readBodyChunkNonBlocking(char *buf, size_t bufsize) {
    // Stream finished: signal completion with (size_t)-1
    if (_streamMode == STREAM_NONE || bufsize == 0)
        return static_cast<size_t>(-1);

    // If the underlying socket is gone, terminate the stream immediately
    if (!_cltsock) {
        _streamMode = STREAM_NONE;
        return static_cast<size_t>(-1);
    }

    // Helper: drain from internal leftover buffer
    auto drainBuf = [&](size_t maxBytes) -> size_t {
        size_t have = _streamBuf.size() - _streamBufPos;
        if (have == 0) return 0;
        size_t take = (std::min)(have, maxBytes);
        std::memcpy(buf, _streamBuf.data() + _streamBufPos, take);
        _streamBufPos += take;
        if (_streamBufPos == _streamBuf.size()) {
            _streamBuf.clear();
            _streamBufPos = 0;
        }
        return take;
    };

    // ── Content-Length mode ──
    if (_streamMode == STREAM_CONTENT_LENGTH) {
        if (_streamRemaining == 0) {
            _streamMode = STREAM_NONE;
            return static_cast<size_t>(-1);
        }

        size_t want = (std::min)(bufsize, _streamRemaining);

        // First try leftover buffer
        size_t got = drainBuf(want);
        if (got > 0) {
            _streamRemaining -= got;
            if (_streamRemaining == 0)
                _streamMode = STREAM_NONE;
            return got;
        }

        // Non-blocking read from socket
        size_t n = _recvNonBlocking(_recvScratch);
        if (n == 0)
            return 0;  // No data yet — caller should retry later

        size_t take = (std::min)({n, want, bufsize});
        std::memcpy(buf, _recvScratch.data.buf, take);
        _streamRemaining -= take;

        if (take < n) {
            _streamBuf.assign(_recvScratch.data.buf + take, _recvScratch.data.buf + n);
            _streamBufPos = 0;
        }

        if (_streamRemaining == 0)
            _streamMode = STREAM_NONE;
        return take;
    }

    // ── HTTP/3 non-blocking streaming ──
    // HTTP/3 mode -- shared with readBodyChunk() via one implementation
    // parameterized by `blocking` (see _streamReadH3()).
    if (_streamMode == STREAM_H3)
        return _streamReadH3(buf, bufsize, /*blocking=*/false);

    // ── HTTP/2 non-blocking streaming ──
    // HTTP/2 mode -- shared with readBodyChunk() via one implementation
    // parameterized by `blocking` (see _streamReadH2()).
    if (_streamMode == STREAM_H2)
        return _streamReadH2(buf, bufsize, /*blocking=*/false);

    // Chunked / EOF modes -- shared with readBodyChunk() via one
    // implementation parameterized by `blocking` (see
    // _streamReadChunked/_streamReadEof). Unlike readBodyChunk(), this
    // function's own contract already uses (size_t)-1 for "body complete",
    // so no translation is needed here.
    if (_streamMode == STREAM_CHUNKED)
        return _streamReadChunked(buf, bufsize, /*blocking=*/false);
    if (_streamMode == STREAM_EOF)
        return _streamReadEof(buf, bufsize, /*blocking=*/false);

    return static_cast<size_t>(-1);
}

static bool is_redirect_status(int code) {
  return code == 301 || code == 302 || code == 303 || code == 307 || code == 308;
}

static std::string normalize_path(const std::string &location, const std::string &base_path) {
  if (location.empty()) return base_path.empty() ? std::string("/") : base_path;
  if (location[0] == '/') return location;
  if (location[0] == '?') {
    std::string base = base_path.empty() ? std::string("/") : base_path;
    return base + location;
  }
  std::string base = base_path.empty() ? std::string("/") : base_path;
  size_t slash = base.find_last_of('/');
  if (slash == std::string::npos) return std::string("/") + location;
  return base.substr(0, slash + 1) + location;
}

  // --------------- HTTP/3 client frame helpers ---------------
  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 void h3AppendFrame(std::vector<uint8_t> &out, uint64_t type, const std::vector<uint8_t> &payload) {
    uint8_t buf[8];
    size_t tlen = h3EncodeVarInt(type, buf);
    out.insert(out.end(), buf, buf + tlen);
    size_t plen = h3EncodeVarInt(payload.size(), buf);
    out.insert(out.end(), buf, buf + plen);
    out.insert(out.end(), payload.begin(), payload.end());
  }

// --------------- HTTP/2 client frame helpers (definitions) ---------------

static std::string h2cBuildFrame(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 — use 16 MB so both large
// downloads and uploads don't stall on the default 65535-byte window.
static constexpr uint32_t H2C_INITIAL_WINDOW_SIZE = 16u * 1024 * 1024;

static std::string h2cBuildSettings() {
    // SETTINGS_INITIAL_WINDOW_SIZE (0x04) = H2C_INITIAL_WINDOW_SIZE
    std::string p(6, '\0');
    p[0] = 0x00; p[1] = 0x04;
    p[2] = static_cast<char>((H2C_INITIAL_WINDOW_SIZE >> 24) & 0xff);
    p[3] = static_cast<char>((H2C_INITIAL_WINDOW_SIZE >> 16) & 0xff);
    p[4] = static_cast<char>((H2C_INITIAL_WINDOW_SIZE >>  8) & 0xff);
    p[5] = static_cast<char>((H2C_INITIAL_WINDOW_SIZE      ) & 0xff);
    return h2cBuildFrame(H2C_FRAME_SETTINGS, 0, 0, p);
}

static std::string h2cBuildSettingsAck() {
    return h2cBuildFrame(H2C_FRAME_SETTINGS, H2C_FLAG_ACK, 0, "");
}

// Connection-level WINDOW_UPDATE to raise the default 65535 connection
// window to match the per-stream INITIAL_WINDOW_SIZE (RFC 7540 §6.9.2).
static std::string h2cConnectionWindowBoost() {
    uint32_t boost = H2C_INITIAL_WINDOW_SIZE - 65535u;
    if (boost == 0) return "";
    return h2cBuildWindowUpdate(0, boost);
}

static std::string h2cBuildWindowUpdate(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 h2cBuildFrame(H2C_FRAME_WINDOW_UPDATE, 0, stream_id, p);
}

// Generic HTTP/2 client request (GET or POST)
const std::vector<char> libhttppp::HttpClient::_h2Request(
    const std::string &method,
    HttpRequest &nreq,
    const std::vector<char> *postBody)
{
    std::vector<char> ret;

    try {
        // Helper: blocking recv using socketwait::waitRead()
        auto recv_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60);
        auto recv_blocking = [&](netplus::buffer& b) -> size_t {
            // b is _recvScratch; recvData() shrinks b.size to bytes read, so
            // reset before every attempt (see _recvBlocking() for why).
            b.size = CHUNKSIZE;
            for (;;) {
                try {
                    return _cltsock->recvData(b, 0);
                } catch (netplus::NetException& e) {
                    if (e.getErrorType() == netplus::NetException::Note) {
                        if (std::chrono::steady_clock::now() >= recv_deadline) {
                            HTTPException ee;
                            ee[HTTPException::Error] << "H2 recv timeout: connection may be dead";
                            throw ee;
                        }
                        _sw.waitRead(*_cltsock, 5000);
                        continue;
                    }
                    throw;
                }
            }
        };

        auto send_all = [&](const std::string &data) {
            auto send_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
            size_t off = 0;
            while (off < data.size()) {
                netplus::buffer buf(data.c_str() + off, data.size() - off);
                try {
                    size_t n = _cltsock->sendData(buf, 0);
                    if (n == 0) {
                        if (std::chrono::steady_clock::now() >= send_deadline) {
                            HTTPException ee;
                            ee[HTTPException::Error] << "H2 send timeout: connection may be dead";
                            throw ee;
                        }
                        _sw.waitWrite(*_cltsock, 500);
                        continue;
                    }
                    off += n;
                } catch (netplus::NetException& e) {
                    if (e.getErrorType() == netplus::NetException::Note) {
                        if (std::chrono::steady_clock::now() >= send_deadline) {
                            HTTPException ee;
                            ee[HTTPException::Error] << "H2 send timeout: connection may be dead";
                            throw ee;
                        }
                        _sw.waitWrite(*_cltsock, 500);
                        continue;
                    }
                    throw;
                }
            }
        };

        // 1) Send connection preface + SETTINGS (only once per connection)
        if (!_h2PrefaceSent) {
            std::string preface(H2C_CLIENT_PREFACE, H2C_CLIENT_PREFACE_LEN);
            preface += h2cBuildSettings();
            preface += h2cConnectionWindowBoost();
            send_all(preface);
            _h2PrefaceSent = true;
        }

        // 2) Build HEADERS frame for stream 1
        std::string path = nreq.getRequestURL();
        if (path.empty()) path = _url.getPath();

        std::string scheme = (_url.getProtocol() == HttpUrl::HTTPS) ? "https" : "http";

        std::string auth = _url.getHost() + ":" + std::to_string(_url.getPort());

        // Collect extra headers from the request
        bool has_content_length = false;
        std::vector<hpack::HeaderField> extra;
        for (HttpHeader::HeaderData *hd = nreq.getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
            const std::string &key = hd->getkey();
            if (key.empty() || key[0] == ':') continue;
            if (key == "host") continue; // :authority replaces host in H2
          if (key == "content-length") has_content_length = true;
            for (HttpHeader::HeaderData::Values *v = hd->getfirstValue(); v; v = v->nextvalue()) {
                extra.push_back({key, v->getvalue(), false});
            }
        }

        // Add content-length for POST body (servers may rely on it)
        if (postBody && !has_content_length) {
          extra.push_back({"content-length", std::to_string(postBody->size()), false});
        }

        std::string hpack_block = hpack::Encoder::encodeRequestHeaders(
            method, path, scheme, auth, extra);

        uint32_t stream_id = _h2NextStreamId;
        _h2NextStreamId += 2;  // Client streams use odd IDs: 1, 3, 5, ...
        bool end_stream = (postBody == nullptr || postBody->empty());
        uint8_t hdr_flags = H2C_FLAG_END_HEADERS;
        if (end_stream) hdr_flags |= H2C_FLAG_END_STREAM;

        std::string headers_frame = h2cBuildFrame(H2C_FRAME_HEADERS, hdr_flags,
                                                   stream_id, hpack_block);
        send_all(headers_frame);

        // 3) Send DATA frames for POST body with flow control (RFC 7540 §6.9)
        // raw is shared between POST-send and response-read phases
        std::vector<uint8_t> raw;
        raw.reserve(16384);

        if (postBody && !postBody->empty()) {
            // Track the peer's (server's) flow control window.
            // Initial value is 65535 until we receive the server's SETTINGS.
            int32_t peer_conn_window = 65535;
            int32_t peer_stream_window = 65535;

            // Helper: parse buffered frames for SETTINGS / WINDOW_UPDATE / PING
            auto process_fc_frames = [&]() {
                size_t foff = 0;
                while (foff + H2C_FRAME_HEADER_LEN <= raw.size()) {
                    uint32_t flen = (static_cast<uint32_t>(raw[foff]) << 16)
                                  | (static_cast<uint32_t>(raw[foff+1]) << 8)
                                  | static_cast<uint32_t>(raw[foff+2]);
                    uint8_t ftype = raw[foff+3];
                    uint8_t fflags = raw[foff+4];
                    uint32_t fsid = ((static_cast<uint32_t>(raw[foff+5]) & 0x7f) << 24)
                                  | (static_cast<uint32_t>(raw[foff+6]) << 16)
                                  | (static_cast<uint32_t>(raw[foff+7]) << 8)
                                  | static_cast<uint32_t>(raw[foff+8]);
                    if (foff + H2C_FRAME_HEADER_LEN + flen > raw.size()) break;
                    const uint8_t *fp = raw.data() + foff + H2C_FRAME_HEADER_LEN;

                    if (ftype == H2C_FRAME_SETTINGS && !(fflags & H2C_FLAG_ACK)) {
                        for (size_t si = 0; si + 6 <= flen; si += 6) {
                            uint16_t setting_id = (static_cast<uint16_t>(fp[si]) << 8)
                                                | fp[si+1];
                            uint32_t setting_val = (static_cast<uint32_t>(fp[si+2]) << 24)
                                                 | (static_cast<uint32_t>(fp[si+3]) << 16)
                                                 | (static_cast<uint32_t>(fp[si+4]) << 8)
                                                 | fp[si+5];
                            if (setting_id == 0x04) { // SETTINGS_INITIAL_WINDOW_SIZE
                                int32_t delta = static_cast<int32_t>(setting_val)
                                              - peer_stream_window;
                                peer_stream_window += delta;
                            }
                        }
                        send_all(h2cBuildSettingsAck());
                    } else if (ftype == H2C_FRAME_WINDOW_UPDATE && flen >= 4) {
                        uint32_t inc = ((static_cast<uint32_t>(fp[0]) & 0x7f) << 24)
                                     | (static_cast<uint32_t>(fp[1]) << 16)
                                     | (static_cast<uint32_t>(fp[2]) << 8)
                                     | fp[3];
                        if (inc > 0) {
                            if (fsid == 0)
                                peer_conn_window += static_cast<int32_t>(inc);
                            else if (fsid == stream_id)
                                peer_stream_window += static_cast<int32_t>(inc);
                        }
                    } else if (ftype == H2C_FRAME_PING && !(fflags & H2C_FLAG_ACK)
                               && flen == 8) {
                        std::string pp(reinterpret_cast<const char*>(fp), flen);
                        send_all(h2cBuildFrame(H2C_FRAME_PING, H2C_FLAG_ACK, 0, pp));
                    }
                    // Leave HEADERS / DATA in the buffer for the response phase
                    // but only consume control frames here.
                    foff += H2C_FRAME_HEADER_LEN + flen;
                }
                if (foff > 0)
                    raw.erase(raw.begin(), raw.begin() + static_cast<ptrdiff_t>(foff));
            };

            // Try a non-blocking read to pick up the server's SETTINGS
            // (which usually arrives right after TLS handshake).
            {
                _recvScratch.size = CHUNKSIZE; // see _recvBlocking() for why
                size_t n = 0;
                try { n = _cltsock->recvData(_recvScratch, 0); } catch (...) {}
                if (n > 0) {
                    raw.insert(raw.end(),
                        reinterpret_cast<uint8_t*>(_recvScratch.data.buf),
                        reinterpret_cast<uint8_t*>(_recvScratch.data.buf) + n);
                    process_fc_frames();
                }
            }

            size_t offset = 0;
            while (offset < postBody->size()) {
                int32_t allowed = std::min(peer_conn_window, peer_stream_window);

                // If window exhausted, block-read for WINDOW_UPDATE
                while (allowed <= 0) {
                    size_t n = recv_blocking(_recvScratch);
                    if (n == 0) {
                        HTTPException he;
                        he[HTTPException::Error] << "HTTP/2: EOF waiting for WINDOW_UPDATE";
                        throw he;
                    }
                    raw.insert(raw.end(),
                        reinterpret_cast<uint8_t*>(_recvScratch.data.buf),
                        reinterpret_cast<uint8_t*>(_recvScratch.data.buf) + n);
                    process_fc_frames();
                    allowed = std::min(peer_conn_window, peer_stream_window);
                }

                size_t chunk = std::min({postBody->size() - offset,
                                         static_cast<size_t>(allowed),
                                         H2C_MAX_FRAME_SIZE});
                bool last = (offset + chunk >= postBody->size());
                uint8_t flags = last ? H2C_FLAG_END_STREAM : 0;
                std::string payload(postBody->data() + offset, chunk);
                send_all(h2cBuildFrame(H2C_FRAME_DATA, flags, stream_id, payload));
                peer_conn_window -= static_cast<int32_t>(chunk);
                peer_stream_window -= static_cast<int32_t>(chunk);
                offset += chunk;
            }
        }

        // 4) Read response frames
        bool got_end_stream = false;
        bool got_headers = false;

        // Use connection-level decoder to maintain HPACK dynamic table state
        if (!_h2Decoder) _h2Decoder = std::make_unique<hpack::Decoder>();
        hpack::Decoder &decoder = *_h2Decoder;

        // Read enough data into raw buffer
        auto ensure_bytes = [&](size_t need) {
            while (raw.size() < need) {
                size_t n = recv_blocking(_recvScratch);
                if (n == 0) {
                    HTTPException he;
                    he[HTTPException::Error] << "HTTP/2: EOF while reading frames";
                    throw he;
                }
                raw.insert(raw.end(),
                           reinterpret_cast<uint8_t*>(_recvScratch.data.buf),
                           reinterpret_cast<uint8_t*>(_recvScratch.data.buf) + n);
            }
        };

        // Overall response deadline: prevents infinite loops when a
        // malicious server sends endless non-terminal frames (e.g. PINGs).
        auto h2_response_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(120);

        size_t raw_off = 0;   // track consumed position to avoid per-frame erase

        while (!got_end_stream) {
            if (std::chrono::steady_clock::now() >= h2_response_deadline) {
                HTTPException ee;
                ee[HTTPException::Error] << "HTTP/2 response timeout: no END_STREAM received";
                throw ee;
            }
            // Read frame header (9 bytes)
            ensure_bytes(raw_off + H2C_FRAME_HEADER_LEN);

            uint32_t frame_len = (static_cast<uint32_t>(raw[raw_off]) << 16)
                               | (static_cast<uint32_t>(raw[raw_off+1]) << 8)
                               | static_cast<uint32_t>(raw[raw_off+2]);
            uint8_t frame_type = raw[raw_off+3];
            uint8_t frame_flags = raw[raw_off+4];
            uint32_t frame_stream = ((static_cast<uint32_t>(raw[raw_off+5]) & 0x7f) << 24)
                                  | (static_cast<uint32_t>(raw[raw_off+6]) << 16)
                                  | (static_cast<uint32_t>(raw[raw_off+7]) << 8)
                                  | static_cast<uint32_t>(raw[raw_off+8]);

            // Read frame payload
            ensure_bytes(raw_off + H2C_FRAME_HEADER_LEN + frame_len);

            const uint8_t *payload = raw.data() + raw_off + H2C_FRAME_HEADER_LEN;

            switch (frame_type) {
                case H2C_FRAME_SETTINGS: {
                    if (!(frame_flags & H2C_FLAG_ACK)) {
                        // Server SETTINGS — acknowledge it
                        send_all(h2cBuildSettingsAck());
                    }
                    break;
                }

                case H2C_FRAME_HEADERS: {
                    if (frame_stream == stream_id) {
                        auto headers = decoder.decode(payload, frame_len);
                        got_headers = true;
                        _lastResponse = std::make_unique<HttpResponse>();
                        _lastResponse->parseH2(headers);
                        _lastStatusCode = _lastResponse->getStatusCode();
                        _lastContentType = _lastResponse->getContentType();
                        _lastLocation.clear();
                        try {
                            auto *loc = _lastResponse->getHeaderData("location");
                            if (loc && loc->getfirstValue())
                                _lastLocation = loc->getfirstValue()->getvalue();
                        } catch (...) {}
                        if (frame_flags & H2C_FLAG_END_STREAM) {
                            got_end_stream = true;
                        }
                    }
                    break;
                }

                case H2C_FRAME_DATA: {
                    if (frame_stream == stream_id) {
                        ret.insert(ret.end(), payload, payload + frame_len);
                        if (frame_flags & H2C_FLAG_END_STREAM) {
                            got_end_stream = true;
                        }
                        // Send WINDOW_UPDATE for connection and stream (batched)
                        if (frame_len > 0) {
                            std::string wu = h2cBuildWindowUpdate(0, frame_len)
                                           + h2cBuildWindowUpdate(stream_id, frame_len);
                            send_all(wu);
                        }
                    }
                    break;
                }

                case H2C_FRAME_WINDOW_UPDATE:
                case H2C_FRAME_PING: {
                    if (frame_type == H2C_FRAME_PING && !(frame_flags & H2C_FLAG_ACK)) {
                        // Respond to PING
                        std::string ping_payload(reinterpret_cast<const char*>(payload), frame_len);
                        send_all(h2cBuildFrame(H2C_FRAME_PING, H2C_FLAG_ACK, 0, ping_payload));
                    }
                    break;
                }

                case H2C_FRAME_GOAWAY: {
                    // RFC 7540 §6.8: 4-byte Last-Stream-ID (top bit reserved)
                    // followed by a 4-byte error code. Reaching this case means
                    // got_end_stream is still false (the while-loop condition),
                    // i.e. our stream's response is not yet complete. Previously
                    // this unconditionally set got_end_stream=true, silently
                    // returning an empty/partial response as if it had
                    // succeeded. If the peer explicitly never processed our
                    // stream, fail loudly instead; otherwise keep reading --
                    // it may still deliver the remaining frames before closing
                    // (bounded by the deadline/EOF checks around this loop).
                    uint32_t last_stream_id = 0;
                    if (frame_len >= 4) {
                        last_stream_id = ((static_cast<uint32_t>(payload[0]) & 0x7f) << 24)
                                       | (static_cast<uint32_t>(payload[1]) << 16)
                                       | (static_cast<uint32_t>(payload[2]) << 8)
                                       | static_cast<uint32_t>(payload[3]);
                    }
                    if (stream_id > last_stream_id) {
                        HTTPException ee;
                        ee[HTTPException::Error]
                            << "HTTP/2: GOAWAY before stream " << stream_id
                            << " was processed (last_stream_id=" << last_stream_id << ")";
                        throw ee;
                    }
                    break;
                }

                default:
                    // Ignore unknown frame types
                    break;
            }

            raw_off += H2C_FRAME_HEADER_LEN + frame_len;
        }

        return ret;

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

  // Generic HTTP/3 client request (GET/POST/PUT/DELETE)
  const std::vector<char> libhttppp::HttpClient::_h3Request(
    const std::string &method,
    HttpRequest &nreq,
    const std::vector<char> *postBody)
  {
    std::vector<char> ret;

    auto *q = dynamic_cast<netplus::quic*>(_cltsock.get());
    if (!q) {
      HTTPException ee;
      ee[HTTPException::Error] << "HTTP/3 requested on non-QUIC socket";
      throw ee;
    }

    const int kMaxRedirects = 1;
    int redirects = 0;

    for (;;) {
      std::string path = nreq.getRequestURL();
      if (path.empty()) path = _url.getPath();

      std::string scheme = (_url.getProtocol() == HttpUrl::HTTP) ? "http" : "https";

      std::string authority = _url.getHost() + ":" + std::to_string(_url.getPort());

      bool include_content_length = false;
      size_t content_length = 0;

      std::vector<qpack::HeaderField> extra;
      for (HttpHeader::HeaderData *hd = nreq.getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
        const std::string &key = hd->getkey();
        if (key.empty() || key[0] == ':') continue;
        if (key == "host" || key == "connection") continue;

        for (HttpHeader::HeaderData::Values *v = hd->getfirstValue(); v; v = v->nextvalue()) {
          if (key == "content-length") {
            include_content_length = true;
            content_length = static_cast<size_t>(std::stoul(v->getvalue()));
          } else {
            extra.push_back({key, v->getvalue(), false});
          }
        }
      }

      if (postBody) {
        include_content_length = true;
        content_length = postBody->size();
      }

      std::vector<uint8_t> headers = qpack::Encoder::encodeRequestHeaders(
        method, scheme, authority, path, extra, include_content_length, content_length);

      std::vector<uint8_t> stream_payload;
      h3AppendFrame(stream_payload, 0x01, headers);

      if (postBody && !postBody->empty()) {
        std::vector<uint8_t> body(postBody->begin(), postBody->end());
        h3AppendFrame(stream_payload, 0x00, body);
      }

      uint64_t stream_id = q->openStream(true);
      q->sendStreamData(stream_id, stream_payload, true);

      std::vector<uint8_t> raw;
      std::vector<char> body;
      bool got_headers = false;
      size_t expected_body = static_cast<size_t>(-1);
      int status_code = 0;
      std::string location;
      bool in_data_frame = false;
      uint64_t data_remaining = 0;
      size_t raw_off = 0; // consumed offset into raw (avoids erase-from-front)

      auto last_data = std::chrono::steady_clock::now();
      auto last_activity = last_data;
      const auto idle_timeout = std::chrono::seconds(15);
      const auto response_timeout = std::chrono::seconds(60);

      // Helper: drain all available stream data into raw
      auto drainStream = [&]() {
        uint8_t buf[65536];
        bool got_any = false;
        for (;;) {
          size_t n = q->recvStreamData(stream_id, buf, sizeof(buf));
          if (n == 0) break;
          raw.insert(raw.end(), buf, buf + n);
          got_any = true;
        }
        if (got_any) {
          auto now = std::chrono::steady_clock::now();
          last_data = now;
          last_activity = now;
        }
        return got_any;
      };

      // Helper: pump QUIC network layer (process incoming UDP datagrams).
      // pumpNetwork() returns normally (never throws) on an empty non-blocking
      // read, so a fixed 256-iteration loop used to always cost 256 non-blocking
      // recv syscalls per call regardless of whether anything arrived. Stop as
      // soon as our stream actually has something to show for it.
      auto pumpQuic = [&]() {
        bool pumped_any = false;
        for (int pump_i = 0; pump_i < 4; ++pump_i) {
          try {
            q->pumpNetwork(MSG_DONTWAIT);
            pumped_any = true;
          } catch (netplus::NetException &e) {
            if (e.getErrorType() != netplus::NetException::Note) {
              HTTPException ee;
              ee[HTTPException::Error] << e.what();
              throw ee;
            }
            break;
          }
          if (q->hasStreamData(stream_id) || q->isStreamComplete(stream_id))
            break;
        }
        return pumped_any;
      };

      for (;;) {
        // 1) Drain all currently buffered stream data
        if (!drainStream()) {
          // No data available — pump network and try again
          pumpQuic();
          drainStream();
        }

        // 2) Parse frames from raw buffer (using offset to avoid erase-from-front)
        size_t pos = raw_off;

        // Consume partial DATA frame continuation
        if (in_data_frame && pos < raw.size()) {
          size_t avail = raw.size() - pos;
          size_t take = (std::min)(avail, static_cast<size_t>(data_remaining));
          body.insert(body.end(), raw.begin() + static_cast<ptrdiff_t>(pos),
                raw.begin() + static_cast<ptrdiff_t>(pos + take));
          pos += take;
          data_remaining -= take;
          if (data_remaining == 0)
            in_data_frame = false;
        }
        while (pos < raw.size() && !in_data_frame) {
          size_t frame_start = pos;
          size_t bytes = 0;
          uint64_t frame_type = h3DecodeVarInt(raw.data() + pos, raw.size() - pos, bytes);
          if (bytes == 0) break;
          pos += bytes;
          if (pos >= raw.size()) { pos = frame_start; break; }

          uint64_t frame_len = h3DecodeVarInt(raw.data() + pos, raw.size() - pos, bytes);
          if (bytes == 0) { pos = frame_start; break; }
          pos += bytes;

          if (frame_type == 0x00) { // DATA — handle incrementally
            size_t avail = raw.size() - pos;
            size_t take = (std::min)(avail, static_cast<size_t>(frame_len));
            body.insert(body.end(), raw.begin() + static_cast<ptrdiff_t>(pos),
                  raw.begin() + static_cast<ptrdiff_t>(pos + take));
            pos += take;
            if (take < frame_len) {
              in_data_frame = true;
              data_remaining = frame_len - take;
            }
          } else if (pos + frame_len > raw.size()) {
            pos = frame_start; break;
          } else if (frame_type == 0x01) {
            auto decoded = qpack::Decoder::decode(raw.data() + pos, frame_len);
            _lastResponse = std::make_unique<HttpResponse>();
            _lastResponse->parseH3(decoded);
            _lastStatusCode = _lastResponse->getStatusCode();
            status_code = _lastStatusCode;
            _lastContentType = _lastResponse->getContentType();
            _lastLocation.clear();
            try {
                auto *loc = _lastResponse->getHeaderData("location");
                if (loc && loc->getfirstValue()) {
                    _lastLocation = loc->getfirstValue()->getvalue();
                    location = _lastLocation;
                }
            } catch (...) {}
            try {
                size_t cl = _lastResponse->getContentLength();
                if (cl > 0) {
                    expected_body = cl;
                    body.reserve(expected_body);
                }
            } catch (...) {}
            got_headers = true;
            pos += frame_len;
          } else {
            pos += frame_len;
          }
        }

        // 3) Compact: drop consumed bytes only when offset is large
        raw_off = pos;
        if (raw_off > 0 && (raw_off >= raw.size() || raw_off > 65536)) {
          raw.erase(raw.begin(), raw.begin() + static_cast<ptrdiff_t>(raw_off));
          raw_off = 0;
        }

        // 4) Check completion conditions
        if (got_headers && expected_body != static_cast<size_t>(-1) && body.size() >= expected_body) {
          ret = std::move(body);
          break;
        }

        if (got_headers && expected_body == static_cast<size_t>(-1)
            && q->isStreamComplete(stream_id)
            && !q->hasStreamData(stream_id)
            && raw_off >= raw.size()) {
          ret = std::move(body);
          break;
        }

        auto now = std::chrono::steady_clock::now();
        if (now - last_activity > idle_timeout) {
          HTTPException ee;
          ee[HTTPException::Error] << "HTTP/3 connection idle timeout";
          throw ee;
        }
        if (now - last_data > response_timeout) {
          HTTPException ee;
          ee[HTTPException::Error] << "HTTP/3 response timeout";
          throw ee;
        }

        // Wait for socket readability only when truly idle
        if (!q->hasStreamData(stream_id) && !q->isStreamComplete(stream_id)) {
          _sw.waitRead(*_cltsock, 50);
        }
      }

      if (is_redirect_status(status_code) && redirects < kMaxRedirects && !location.empty()) {
        ++redirects;
        if (location.rfind("http://", 0) == 0 || location.rfind("https://", 0) == 0) {
          _url = HttpUrl(location, false);
          nreq.setRequestURL(_url.getPath());
        } else {
          std::string base = nreq.getRequestURL().empty() ? _url.getPath() : nreq.getRequestURL();
          nreq.setRequestURL(normalize_path(location, base));
        }

        resetConnection();
        q = dynamic_cast<netplus::quic*>(_cltsock.get());
        if (!q) {
          return ret;
        }
        continue;
      }

      return ret;
    }
  }

const std::vector<char> libhttppp::HttpClient::_doH1Request(
    const std::string &method, int requestType, libhttppp::HttpRequest &nreq,
    const std::vector<char> *body, size_t maxTries, bool followRedirects)
{
    size_t tries = 0;
    int redirects = 0;
    bool convertedToGet = false;

    for (;;) {
        try {
            // Reuse existing connection if possible (avoid expensive TLS handshakes)
            _ensureConnected();

            if (!nreq.getHeaderData("host"))
                nreq.setHeaderData("host")->push_back(
                    _url.getHost() + ":" + std::to_string(_url.getPort()));

            // 303 redirect: switch to a bodyless GET (RFC 7231 §6.4.4).
            // 307/308 preserve the original method+body; GET stays GET.
            const bool sendingGetOverride = convertedToGet;
            nreq.setRequestType(sendingGetOverride ? GETREQUEST : requestType);

            if (nreq.getRequestURL().empty())
                nreq.setRequestURL(_url.getPath());

            const std::vector<char> *sendBody = sendingGetOverride ? nullptr : body;
            const std::string &curMethod = sendingGetOverride
                ? std::string("GET") : method;

            if (sendBody)
                nreq.setHeaderData("content-length")->push_back(std::to_string(sendBody->size()));

            std::vector<char> ret;

            if (_isH3) {
                ret = _h3Request(curMethod, nreq, sendBody);
            } else if (_isH2) {
                ret = _h2Request(curMethod, nreq, sendBody);
            } else {
                // HTTP/1.x path
                if (nreq.getRequestVersion().empty())
                    nreq.setRequestVersion(HTTPVERSION(1.1));

                std::string header;
                nreq.printHeader(header);
                if (!_cltsock || _cltsock->fd() < 0)
                    resetConnection();

                if (sendBody) {
                    // Send header + body together for best TCP segment utilization
                    std::vector<char> combined;
                    combined.reserve(header.size() + sendBody->size());
                    combined.insert(combined.end(), header.begin(), header.end());
                    combined.insert(combined.end(), sendBody->begin(), sendBody->end());
                    _sendAll(combined.data(), combined.size());
                } else {
                    _sendAll(header);
                }

                ret = _h1ReadResponse(curMethod);
            }

            // Handle redirects (301, 302, 303, 307, 308)
            if (followRedirects
                && _lastStatusCode >= 301 && _lastStatusCode <= 308
                && _lastStatusCode != 304 && _lastStatusCode != 305
                && !_lastLocation.empty() && redirects < _maxRedirects)
            {
                ++redirects;
                // Update request URL for relative or absolute redirect
                if (_lastLocation.find("://") != std::string::npos) {
                    _url = HttpUrl(_lastLocation);
                    resetConnection();
                }
                nreq.setRequestURL(_lastLocation.find("://") != std::string::npos
                    ? _url.getPath() : _lastLocation);
                if (_lastStatusCode == 303 && requestType != GETREQUEST)
                    convertedToGet = true;
                continue;
            }

            return ret;

        } catch (netplus::NetException &e) {
            ++tries;
            if (maxTries != 0 && tries >= maxTries) {
                libhttppp::HTTPException ee;
                ee[libhttppp::HTTPException::Error] << e.what();
                throw ee;
            }
            resetConnection();
        } catch (HTTPException &e) {
            ++tries;
            if (maxTries != 0 && tries >= maxTries)
                throw;
            resetConnection();
        }
    }
}

const std::vector<char> libhttppp::HttpClient::Get(libhttppp::HttpRequest &nreq, size_t maxTries)
{
    return _doH1Request("GET", GETREQUEST, nreq, nullptr, maxTries, /*followRedirects=*/true);
}

const std::vector<char> libhttppp::HttpClient::Post(libhttppp::HttpRequest &nreq,
                                                    const std::vector<char> &post, size_t maxTries)
{
    return _doH1Request("POST", POSTREQUEST, nreq, &post, maxTries, /*followRedirects=*/true);
}

const std::vector<char> libhttppp::HttpClient::Delete(libhttppp::HttpRequest &nreq, size_t maxTries)
{
    // Historically Delete() has not followed redirects; preserved as-is.
    return _doH1Request("DELETE", DELETEREQUEST, nreq, nullptr, maxTries, /*followRedirects=*/false);
}

const std::vector<char> libhttppp::HttpClient::Put(libhttppp::HttpRequest &nreq,
                                                   const std::vector<char> &put, size_t maxTries)
{
    return _doH1Request("PUT", PUTREQUEST, nreq, &put, maxTries, /*followRedirects=*/true);
}



libhttppp::HttpHeader::HttpHeader(){
  _firstHeaderData=nullptr;
  _lastHeaderData=nullptr;
}

void libhttppp::HttpHeader::HeaderData::clear(){
  _firstValue.reset();
}


libhttppp::HttpHeader::HeaderData* libhttppp::HttpHeader::getfirstHeaderData(){
  return _firstHeaderData.get();
}

libhttppp::HttpHeader::HeaderData * libhttppp::HttpHeader::HeaderData::nextHeaderData(){
  return _nextHeaderData.get();
}

libhttppp::HttpHeader::HeaderData *libhttppp::HttpHeader::getHeaderData(const std ::string &key) const{
  auto it = _index.find(key);
  return (it != _index.end()) ? it->second : nullptr;
}

libhttppp::HttpHeader::HeaderData * libhttppp::HttpHeader::setHeaderData(const std ::string &key){
  auto it = _index.find(key);
  if (it != _index.end()) {
    HeaderData *curdat = it->second;
    curdat->clear();
    curdat->_Key = key;
    return curdat;
  }

  std::unique_ptr<HeaderData> node = std::make_unique<HeaderData>(key);
  HeaderData *curdat = node.get();
  if (!_firstHeaderData) {
    _firstHeaderData = std::move(node);
    _lastHeaderData = _firstHeaderData.get();
  } else {
    _lastHeaderData->_nextHeaderData = std::move(node);
    _lastHeaderData = _lastHeaderData->_nextHeaderData.get();
  }
  _index[key] = curdat;
  return curdat;
}

void libhttppp::HttpHeader::deldata(const std::string& key){
  auto it = _index.find(key);
  if (it != _index.end())
    deldata(it->second);
}


void libhttppp::HttpHeader::deldata(libhttppp::HttpHeader::HeaderData* pos){
  if (!pos) return;
  std::unique_ptr<HeaderData>* link = &_firstHeaderData;
  while (link && link->get()) {
    if (link->get() == pos) {
      *link = std::move(pos->_nextHeaderData);
      break;
    }
    link = &link->get()->_nextHeaderData;
  }
  _lastHeaderData = _firstHeaderData.get();
  while (_lastHeaderData && _lastHeaderData->_nextHeaderData)
    _lastHeaderData = _lastHeaderData->_nextHeaderData.get();
  if (!_firstHeaderData) _lastHeaderData = nullptr;

  // Remove whichever index entry (if any) points at this node. Erase by
  // value rather than by pos->_Key: setHeaderData()'s found-existing path
  // can reassign _Key without touching the index, so they aren't guaranteed
  // to match in the (already-quirky, pre-existing) case of a caller reusing
  // the same key string with different casing across calls.
  for (auto idxIt = _index.begin(); idxIt != _index.end(); ) {
    if (idxIt->second == pos) idxIt = _index.erase(idxIt);
    else ++idxIt;
  }
}



size_t libhttppp::HttpHeader::getElements(){
  size_t elements = 0;
  for (HeaderData* cur = _firstHeaderData.get(); cur; cur = cur->_nextHeaderData.get()) {
    ++elements;
  }
  return elements;
}

size_t libhttppp::HttpHeader::getHeaderSize(){
  size_t hsize = 0;
  for (HeaderData* cur = _firstHeaderData.get(); cur; cur = cur->_nextHeaderData.get()) {
    hsize += cur->_Key.length();
    for (HeaderData::Values* v = cur->_firstValue.get(); v; v = v->nextvalue()) {
      hsize += v->getvalue().length();
    }
  }
  return hsize;
}

void libhttppp::HttpHeader::clear(){
  _firstHeaderData.reset();
  _lastHeaderData=nullptr;
  _index.clear();
}

void libhttppp::HttpHeader::HeaderData::erase(int /*pos*/){
}

void libhttppp::HttpHeader::HeaderData::push_back(const char* val){
  if(_firstValue==nullptr){
    _firstValue=std::make_unique<Values>(val);
    _lastValue=_firstValue.get();
  }else{
    _lastValue->_nextvalue=std::make_unique<Values>(val);
    _lastValue=_lastValue->_nextvalue.get();
  }
}

void libhttppp::HttpHeader::HeaderData::push_back(const Values& val){
  if(_firstValue==nullptr){
    _firstValue=std::make_unique<Values>(val);
    _lastValue=_firstValue.get();
  }else{
    _lastValue->_nextvalue=std::make_unique<Values>(val);
    _lastValue=_lastValue->_nextvalue.get();
  }
}


void libhttppp::HttpHeader::HeaderData::push_back(const std::string &val){
  if(_firstValue==nullptr){
    _firstValue=std::make_unique<Values>(val);
    _lastValue=_firstValue.get();
  }else{
    _lastValue->_nextvalue=std::make_unique<Values>(val);
    _lastValue=_lastValue->_nextvalue.get();
  }
}

void libhttppp::HttpHeader::HeaderData::push_back(int val){
  push_back(std::to_string(val));
}

void libhttppp::HttpHeader::HeaderData::push_back(size_t val){
  push_back(std::to_string(val));
}



bool libhttppp::HttpHeader::HeaderData::empty(){
  if(!_firstValue && _Key.empty())
    return true;
  return false;
}

const std ::string &libhttppp::HttpHeader::HeaderData::getkey(){
  return _Key;
}

libhttppp::HttpHeader::HeaderData::Values &libhttppp::HttpHeader::HeaderData::at(int pos){
  size_t cpos=0;
  for(Values *cval=_firstValue.get(); cval; cval=cval->nextvalue()){
    if(cpos==static_cast<size_t>(pos))
      return *cval;
    ++cpos;
  }
  HTTPException excep;
  excep[HTTPException::Error] << "no headerdata at:" << pos <<" out of memory";
  throw excep;
}

libhttppp::HttpHeader::HeaderData::Values & libhttppp::HttpHeader::HeaderData::operator[](int pos){
  return at(pos);
}

libhttppp::HttpHeader::HeaderData::HeaderData(const std ::string &key){
  HTTPException excep;
  if(key.empty()){
    excep[HTTPException::Error] << "no headerdata key set can't do this";
    throw excep;
  }
  for(size_t i=0; i<key.length(); ++i){
    _Key.push_back(tolower(key[i]));
  }
  _nextHeaderData=nullptr;
  _firstValue=nullptr;
  _lastValue=nullptr;
}

libhttppp::HttpHeader::HeaderData::Values * libhttppp::HttpHeader::HeaderData::getfirstValue(){
  return _firstValue.get();
}

libhttppp::HttpHeader::HeaderData::Values * libhttppp::HttpHeader::HeaderData::Values::nextvalue(){
  return _nextvalue.get();
}

libhttppp::HttpHeader::HeaderData::Values& libhttppp::HttpHeader::HeaderData::Values::operator<<(const std::string &value) {
  if (value.empty())
    return *this;
  _value.append(value);
  return *this;
}

libhttppp::HttpHeader::HeaderData::Values &libhttppp::HttpHeader::HeaderData::Values::operator<<(size_t value){
  _value.append(std::to_string(value));
  return *this;
}

libhttppp::HttpHeader::HeaderData::Values &libhttppp::HttpHeader::HeaderData::Values::operator<<(int value){
  _value.append(std::to_string(value));
  return *this;
}

const std::string& libhttppp::HttpHeader::HeaderData::Values::getvalue(){
  return _value;
}


int libhttppp::HttpHeader::HeaderData::Values::getIntvalue(){
  int size = 0;
  std::from_chars(_value.data(), _value.data() + _value.size(), size);
  return size;
}

size_t libhttppp::HttpHeader::HeaderData::Values::getSizetValue(){
  size_t size = 0;
  std::from_chars(_value.data(), _value.data() + _value.size(), size);
  return size;
}


libhttppp::HttpHeader::HeaderData::Values& libhttppp::HttpHeader::HeaderData::Values::operator=(const std::string &val) {
  _value=val;
  return *this;
}

libhttppp::HttpHeader::HeaderData::Values &libhttppp::HttpHeader::HeaderData::Values::operator=(size_t val){
  *this = std::to_string(val);
  return *this;
}

libhttppp::HttpHeader::HeaderData::Values &libhttppp::HttpHeader::HeaderData::Values::operator=(int val){
  *this = std::to_string(val);
  return *this;
}

libhttppp::HttpHeader::HeaderData::Values & libhttppp::HttpHeader::HeaderData::Values::operator=(const Values& val){
  if (this == &val) return *this;
  _value = val._value;
  if (val._nextvalue) {
    _nextvalue = std::make_unique<Values>(*val._nextvalue);
  } else {
    _nextvalue.reset();
  }
  return *this;
}


libhttppp::HttpHeader::HeaderData::Values::Values(const std::string& val) {
  _value=val;
}

libhttppp::HttpHeader::HeaderData::Values::Values(const Values& val)
: _value(val._value)
{
  if (val._nextvalue) {
    _nextvalue = std::make_unique<Values>(*val._nextvalue);
  }
}

libhttppp::HttpResponse::HttpResponse() : HttpHeader(){
  setState(HTTP200);
  _StatusCode = 200;
  _ContentType=nullptr;
  _ContentLength=setHeaderData("content-length");
  _ContentLength->push_back(0);
  _Connection=setHeaderData("connection");
  _Connection->push_back("keep-alive");
  _TransferEncoding=nullptr;
}

libhttppp::HttpResponse::HttpResponse(const HttpResponse &src) : HttpResponse(){
  _State = src._State;
  _Version = src._Version;
  _StatusCode = src._StatusCode;
  for(HttpHeader::HeaderData *curdat=src._firstHeaderData.get(); curdat; curdat=curdat->nextHeaderData()){
    HttpHeader::HeaderData  *newdat=setHeaderData(curdat->getkey());
    for(HttpHeader::HeaderData::Values *curval=curdat->getfirstValue(); curval; curval=curval->nextvalue()){
      newdat->push_back(curval->getvalue());
    }
  }
}

void libhttppp::HttpResponse::setState(const std ::string &httpstate){
  HTTPException httpexception;
  if(httpstate.empty()){
    httpexception[HTTPException::Error] << "http state not set don't do that !!!";
    throw httpexception;
  }
  size_t hlen=httpstate.length();
  if(hlen>254){
    httpexception[HTTPException::Error] << "http state with over 255 signs sorry your are drunk !";
    throw httpexception;
  }
  _State=httpstate;
}

void libhttppp::HttpResponse::setContentLength(size_t len){
  _ContentLength->at(0)=len;
}

void libhttppp::HttpResponse::setContentType(const std::string  &type){
  _ContentType=setHeaderData("content-type");
  _ContentType->push_back(type);

}

void libhttppp::HttpResponse::setConnection(const std::string  & type){
  _Connection=setHeaderData("connection");
  _Connection->push_back(type);
}

const std ::string &libhttppp::HttpResponse::getState() const{
  return _State;
}

int libhttppp::HttpResponse::getStatusCode() const{
  return _StatusCode;
}

size_t libhttppp::HttpResponse::getContentLength() const{
  if(!_ContentLength || _ContentLength->empty())
    return 0;

  return _ContentLength->at(0).getSizetValue();
}

const std ::string &libhttppp::HttpResponse::getContentType() const{
  static const std::string empty;
  if (!_ContentType) {
    return empty;
  }
  auto *first = _ContentType->getfirstValue();
  if (!first) {
    return empty;
  }

  const std::string &raw = first->getvalue();
  if (raw.empty()) {
    return raw;
  }

  size_t end = raw.find(';');
  if (end == std::string::npos) {
    end = raw.size();
  }
  size_t start = 0;
  while (start < end && std::isspace(static_cast<unsigned char>(raw[start]))) {
    ++start;
  }
  while (end > start && std::isspace(static_cast<unsigned char>(raw[end - 1]))) {
    --end;
  }

  _ContentTypeCache.assign(raw, start, end - start);
  std::transform(_ContentTypeCache.begin(), _ContentTypeCache.end(), _ContentTypeCache.begin(),
                 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
  return _ContentTypeCache;
}

size_t libhttppp::HttpResponse::printHeader(std::vector<char> &buffer){

  // Pre-estimate header size to avoid repeated reallocations
  size_t estimatedSize = _Version.size() + 1 + _State.size() + 2;
  for(HeaderData *curdat=getfirstHeaderData(); curdat; curdat=curdat->nextHeaderData()){
    estimatedSize += curdat->getkey().size() + 2; // "key: "
    for(HeaderData::Values *cval=curdat->getfirstValue(); cval; cval=cval->nextvalue()){
      estimatedSize += cval->getvalue().size() + 2; // "value; "
    }
    estimatedSize += 2; // "\r\n"
  }
  estimatedSize += 2; // trailing "\r\n"
  buffer.reserve(buffer.size() + estimatedSize);

  auto append=[&buffer](const std::string &data){
    buffer.insert(buffer.end(), data.begin(), data.end());
  };

  append(_Version);
  append(" ");
  append(_State);
  append("\r\n");

  for(HeaderData *curdat=getfirstHeaderData(); curdat; curdat=curdat->nextHeaderData()){
    append(curdat->getkey());
    append(": ");

    for(HeaderData::Values *cval=curdat->getfirstValue(); cval; cval=cval->nextvalue()){
      append(cval->getvalue());
      if(cval->nextvalue())
        append("; ");
    }
    append("\r\n");
  }

  append("\r\n");
  return buffer.size();
}

bool libhttppp::HttpResponse::_storeResponseInfo(netplus::con &curconnection, int datalen) {
  auto *req = dynamic_cast<libhttppp::HttpRequest*>(&curconnection);
  if (!req || (req->_httpProtocol != 1 && req->_httpProtocol != 2))
    return false;

  // Store response pseudo-headers and metadata into request's _firstHeaderData
  // using :res-status, :res-content-type, :res-content-length, :res-valid keys

  auto *hValid = req->setHeaderData(":res-valid");
  if (hValid) hValid->push_back("1");

  // Extract status code from state string (e.g. "200 OK" -> 200)
  auto *hStatus = req->setHeaderData(":res-status");
  if (hStatus) hStatus->push_back(_State);

  // Content type
  if (_ContentType) {
    auto *v = _ContentType->getfirstValue();
    if (v) {
      auto *hCT = req->setHeaderData(":res-content-type");
      if (hCT) hCT->push_back(v->getvalue());
    }
  }

  // Content length
  {
    size_t cl = (datalen >= 0) ? static_cast<size_t>(datalen) : getContentLength();
    auto *hCL = req->setHeaderData(":res-content-length");
    if (hCL) hCL->push_back(cl);
  }

  // Collect extra headers (skip ones handled separately or invalid for H2/H3)
  for (HeaderData *hd = getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
    const std::string &key = hd->getkey();
    if (key == "content-type" || key == "content-length" ||
        key == "connection" || key == "transfer-encoding") continue;
    std::string rkey = ":res-" + key;
    auto *rhd = req->setHeaderData(rkey);
    if (rhd) {
      for (auto *v = hd->getfirstValue(); v; v = v->nextvalue()) {
        rhd->push_back(v->getvalue());
      }
    }
  }

  return true;
}

void libhttppp::HttpResponse::send(netplus::con &curconnection,const std::string &data, int datalen){
  if(datalen==0)
    datalen=data.size();

  if(datalen>=0){
    setContentLength(datalen);
  }

  if (auto *req = dynamic_cast<libhttppp::HttpRequest*>(&curconnection)) {
    const std::string &reqver = req->getRequestVersion();
    if (!reqver.empty()) {
      _Version = reqver;
    }
  }

  // For HTTP/2 and HTTP/3: store structured response info directly,
  // write only body data to SendData (no H1 text round-trip)
  if (_storeResponseInfo(curconnection, datalen)) {
    if (datalen > 0)
      curconnection.SendData.append(data.data(), datalen);
    return;
  }

  std::vector<char> header;

  printHeader(header);

  curconnection.SendData.append(header.data(),header.size());

  if (datalen > 0)
    curconnection.SendData.append(data.data(),datalen);
}

void libhttppp::HttpResponse::send(netplus::con &curconnection,const unsigned  char *data,int datalen){
  if(datalen>=0){
    setContentLength(datalen);
  }

  if (auto *req = dynamic_cast<libhttppp::HttpRequest*>(&curconnection)) {
    const std::string &reqver = req->getRequestVersion();
    if (!reqver.empty()) {
      _Version = reqver;
    }
  }

  // For HTTP/2 and HTTP/3: store structured response info directly
  if (_storeResponseInfo(curconnection, datalen)) {
    if (datalen > 0 && data)
      curconnection.SendData.append(reinterpret_cast<const char*>(data), datalen);
    return;
  }

  std::vector<char> header;

  printHeader(header);

  curconnection.SendData.append(header.data(),header.size());

  if (datalen > 0 && data) {
    curconnection.SendData.append(reinterpret_cast<const char*>(data), datalen);
  }
}

void libhttppp::HttpResponse::send(netplus::con &curconnection,const std::vector<char> &data, int datalen){

  if(datalen==0)
    datalen=data.size();

  if(datalen>=0){
    setContentLength(datalen);
  }

  if (auto *req = dynamic_cast<libhttppp::HttpRequest*>(&curconnection)) {
    const std::string &reqver = req->getRequestVersion();
    if (!reqver.empty()) {
      _Version = reqver;
    }
  }

  // For HTTP/2 and HTTP/3: store structured response info directly
  if (_storeResponseInfo(curconnection, datalen)) {
    if (datalen > 0)
      curconnection.SendData.append(data.data(), datalen);
    return;
  }

  std::vector<char> header;

  printHeader(header);

  curconnection.SendData.append(header.data(),header.size());

  if (datalen > 0)
    curconnection.SendData.append(data.data(),datalen);
}

void libhttppp::HttpResponse::sendChunkedHeaders(netplus::con &curconnection) {
  // No Content-Length for a chunked body -- and _ContentLength would dangle
  // once its HeaderData node is unlinked, so drop the cached pointer too.
  deldata("content-length");
  _ContentLength = nullptr;
  setTransferEncoding("chunked");

  if (auto *req = dynamic_cast<libhttppp::HttpRequest*>(&curconnection)) {
    const std::string &reqver = req->getRequestVersion();
    if (!reqver.empty()) {
      _Version = reqver;
    }
  }

  std::vector<char> header;
  printHeader(header);
  curconnection.SendData.append(header.data(), header.size());
}

void libhttppp::HttpResponse::sendChunk(netplus::con &curconnection, const char *data, size_t len) {
  if (len == 0 || !data)
    return;

  char sizeLine[2 * sizeof(size_t) + 2 + 1];
  auto conv = std::to_chars(sizeLine, sizeLine + sizeof(sizeLine), len, 16);
  conv.ptr[0] = '\r';
  conv.ptr[1] = '\n';
  curconnection.SendData.append(sizeLine, static_cast<size_t>(conv.ptr - sizeLine) + 2);
  curconnection.SendData.append(data, len);
  curconnection.SendData.append("\r\n", 2);
}

void libhttppp::HttpResponse::endChunked(netplus::con &curconnection) {
  static const char terminator[] = "0\r\n\r\n";
  curconnection.SendData.append(terminator, 5);
}

size_t libhttppp::HttpResponse::parse(const char *data, size_t inlen) {
    HTTPException excep;

    if (inlen < 16) {
        excep[HTTPException::Error] << "HttpResponse header too small aborting!";
        throw excep;
    }

    // Discard the default constructor's placeholder headers (content-length:
    // 0, connection: keep-alive) before parsing wire data. Without this, a
    // genuinely EOF-terminated response (no Content-Length, no
    // Transfer-Encoding on the wire) would keep reporting
    // getContentLength()==0 from the leftover default instead of reflecting
    // that no such header was actually present -- misdetected as a
    // zero-length body instead of "read until connection close".
    clear();
    _ContentLength = nullptr;
    _Connection = nullptr;
    _ContentType = nullptr;
    _TransferEncoding = nullptr;

    // --- helpers: trim ---
    auto ltrim_inplace = [](std::string& s) {
        size_t i = 0;
        while (i < s.size() && std::isspace(static_cast<unsigned char>(s[i]))) ++i;
        if (i) s.erase(0, i);
    };
    auto rtrim_inplace = [](std::string& s) {
        size_t i = s.size();
        while (i && std::isspace(static_cast<unsigned char>(s[i - 1]))) --i;
        if (i != s.size()) s.erase(i);
    };
    auto trim_inplace = [&](std::string& s) {
        rtrim_inplace(s);
        ltrim_inplace(s);
    };

    // --- 1) find end of header: "\r\n\r\n" ---
    const char* header_end_marker = "\r\n\r\n";
    const char* end_ptr = data + inlen - 4;
    const char* header_end_pos = nullptr;

    for (const char* p = data; p <= end_ptr; ++p) {
        if (std::equal(p, p + 4, header_end_marker)) {
            header_end_pos = p;
            break;
        }
    }

    if (!header_end_pos) {
        excep[HTTPException::Error] << "HttpHeader end not found!";
        throw excep;
    }

    size_t helen = static_cast<size_t>(header_end_pos - data);
    size_t bytes_consumed = helen + 4; // include "\r\n\r\n"

    std::string header_str(data, helen);

    // --- 2) parse status line ---
    size_t line_end = header_str.find("\r\n");
    if (line_end == std::string::npos) {
        excep[HTTPException::Error] << "Can't find end of Status Line!";
        throw excep;
    }

    std::string status_line = header_str.substr(0, line_end);

    size_t v_start = 0;
    size_t v_end = status_line.find(' ', v_start);
    if (v_end == std::string::npos || status_line.substr(0, 5) != "HTTP/") {
        excep[HTTPException::Error] << "HttpResponse no Version Tag found or malformed Status Line!";
        throw excep;
    }

    _Version = status_line.substr(v_start, v_end - v_start);

    // status code
    v_start = v_end + 1;
    v_end = status_line.find(' ', v_start);
    if (v_end == std::string::npos) {
        v_end = status_line.length();
    }
    std::string status_code_str = status_line.substr(v_start, v_end - v_start);
    try {
      _StatusCode = std::stoi(status_code_str);
    } catch (...) {
      _StatusCode = 0;
    }

    // reason phrase
    v_start = v_end + 1;
    std::string reason_phrase;
    if (v_start < status_line.length()) {
        reason_phrase = status_line.substr(v_start);
    } else {
        reason_phrase.clear();
    }
    _State = reason_phrase;

    // --- 3) parse headers ---
    size_t cursor = line_end + 2; // after "\r\n"

    while (cursor < helen) {
        size_t next_line = header_str.find("\r\n", cursor);
        if (next_line == std::string::npos) {
            next_line = helen;
        }

        std::string hline = header_str.substr(cursor, next_line - cursor);
        cursor = next_line + 2;

        if (hline.empty())
            continue;

        size_t colon = hline.find(':');
        if (colon == std::string::npos) {
            // malformed; skip
            continue;
        }

        std::string key   = hline.substr(0, colon);
        std::string value = (colon + 1 < hline.size()) ? hline.substr(colon + 1) : std::string();

        trim_inplace(key);
        trim_inplace(value);

        // lowercase key
        for (size_t i = 0; i < key.size(); ++i) {
            key[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(key[i])));
        }

        HeaderData* ncontent = setHeaderData(key.c_str());
        if (!ncontent)
            continue;

        // Split value on commas (HTTP list headers: TE, Accept, etc.)
        size_t oldiv = 0;
        size_t iv = 0;

        while ((iv = value.find(',', oldiv)) != std::string::npos) {
            std::string token = value.substr(oldiv, iv - oldiv);
            trim_inplace(token);
            if (!token.empty()) {
                ncontent->push_back(token);
            }
            oldiv = iv + 1;
        }

        // last (or only) token
        std::string final_token = value.substr(oldiv);
        trim_inplace(final_token);
        if (!final_token.empty()) {
            ncontent->push_back(final_token);
        }
    }

    // --- 4) populate cached header pointers ---
    _ContentLength    = getHeaderData("content-length");
    _Connection       = getHeaderData("connection");
    _ContentType      = getHeaderData("content-type");
    _TransferEncoding = getHeaderData("transfer-encoding");

    return bytes_consumed;
}

// parseH2/parseH3: HPACK/QPACK already split each field into a clean
// name/value pair, so ingestion here is a straight copy into the header
// list -- no CRLF/colon scanning, and (unlike the old synthetic-text
// approach this replaces) no way for a value to be misinterpreted as
// introducing new header lines.
size_t libhttppp::HttpResponse::parseH2(const std::vector<hpack::HeaderField> &headers) {
    int statusCode = 0;
    for (const auto &h : headers) {
        if (h.name == ":status") {
            try { statusCode = std::stoi(h.value); } catch (...) { statusCode = 0; }
            break;
        }
    }
    _Version = "HTTP/2";
    _StatusCode = statusCode;
    _State.clear();

    for (const auto &h : headers) {
        if (h.name.empty() || h.name[0] == ':') continue;
        std::string key = h.name;
        for (char &c : key)
            c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
        setHeaderData(key)->push_back(h.value);
    }

    _ContentLength    = getHeaderData("content-length");
    _Connection       = getHeaderData("connection");
    _ContentType      = getHeaderData("content-type");
    _TransferEncoding = getHeaderData("transfer-encoding");
    return headers.size();
}

size_t libhttppp::HttpResponse::parseH3(const std::vector<qpack::HeaderField> &headers) {
    int statusCode = 0;
    for (const auto &h : headers) {
        if (h.name == ":status") {
            try { statusCode = std::stoi(h.value); } catch (...) { statusCode = 0; }
            break;
        }
    }
    _Version = "HTTP/3";
    _StatusCode = statusCode;
    _State.clear();

    for (const auto &h : headers) {
        if (h.name.empty() || h.name[0] == ':') continue;
        std::string key = h.name;
        for (char &c : key)
            c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
        setHeaderData(key)->push_back(h.value);
    }

    _ContentLength    = getHeaderData("content-length");
    _Connection       = getHeaderData("connection");
    _ContentType      = getHeaderData("content-type");
    _TransferEncoding = getHeaderData("transfer-encoding");
    return headers.size();
}

void libhttppp::HttpResponse::setTransferEncoding(const std ::string &enc){
  _TransferEncoding=setHeaderData("transfer-encoding");
  _TransferEncoding->push_back(enc);
}

libhttppp::HttpHeader::HeaderData::Values *libhttppp::HttpResponse::getTransferEncoding() const{
  if(_TransferEncoding)
    return _TransferEncoding->getfirstValue();
  return nullptr;
}


libhttppp::HttpResponse::~HttpResponse() {

}

libhttppp::HttpRequest::HttpRequest() : HttpHeader() {
  _RequestType=PARSEREQUEST;
  _MaxUploadSize=DEFAULT_UPLOADSIZE;
};

libhttppp::HttpRequest::~HttpRequest() = default;

libhttppp::HttpRequest::HttpRequest(netplus::eventapi *evapi) : HttpHeader(), con(evapi){
  _RequestType=PARSEREQUEST;
  _MaxUploadSize=DEFAULT_UPLOADSIZE;
}

void libhttppp::HttpRequest::clear(){
  HttpHeader::clear();
  _RequestType=PARSEREQUEST;
  _cachedRequestURL.clear();
  _cachedRequest.clear();
  _cachedRequestVersion.clear();
  _httpProtocol = 0;
  // Reset H2 state: release the heap block entirely so that
  // any corrupted deque/map internals are freed as a unit.
  _h2.reset();
}

size_t libhttppp::HttpRequest::parse() {
  switch (_httpProtocol) {
    case 1: // HTTP/2: already parsed via parseH2()
    case 2: // HTTP/3: already parsed via parseH3()
      return 1;
    default:
      return parseH1();
  }
}

std::string libhttppp::HttpRequest::extractPath(const std::string &target) {
  // Strip query
  const size_t q = target.find('?');
  const std::string no_query = (q == std::string::npos) ? target : target.substr(0, q);

  if (no_query.empty())
    return "/";

  // origin-form: starts with "/"
  if (no_query[0] == '/')
    return no_query;

  // asterisk-form: "*"
  if (no_query == "*")
    return "*";

  // absolute-form: "scheme://authority/path"
  const size_t scheme = no_query.find("://");
  if (scheme != std::string::npos) {
    const size_t after_scheme = scheme + 3;
    const size_t slash = no_query.find('/', after_scheme);
    if (slash == std::string::npos)
      return "/";
    return no_query.substr(slash);
  }

  return "/";
}

size_t libhttppp::HttpRequest::parseH2(const std::vector<hpack::HeaderField> &headers,
                                       uint32_t stream_id) {
  clear();
  _httpProtocol = 1;
  auto &h2 = h2state();
  h2.streamId = stream_id;
  h2.headersSent = false;
  h2.bodyBytesSent = 0;
  SendData.pos = 0;

  // Store ALL headers (including pseudo-headers) in _firstHeaderData.
  // Split values by ';' (matching parseH1 behaviour) so that cookie
  // parsing and other code that iterates individual Values works.
  auto trim = [](std::string& s) {
    size_t i = 0;
    while (i < s.size() && (s[i] == ' ' || s[i] == '\t')) ++i;
    if (i) s.erase(0, i);
    size_t e = s.size();
    while (e && (s[e-1] == ' ' || s[e-1] == '\t')) --e;
    if (e != s.size()) s.erase(e);
  };

  for (const auto &h : headers) {
    // Use getHeaderData first so that duplicate headers (e.g. multiple
    // cookie fields per RFC 7540 §8.1.2.5) APPEND values instead of
    // clearing them.  Only create a new entry when the key is new.
    auto *hd = getHeaderData(h.name);
    if (!hd) hd = setHeaderData(h.name);
    if (!hd) continue;
    const std::string &val = h.value;
    size_t oldiv = 0, iv = 0;
    while (iv < val.size()) {
      if (val[iv] == ';') {
        std::string token = val.substr(oldiv, iv - oldiv);
        trim(token);
        if (!token.empty()) hd->push_back(token);
        ++iv;
        while (iv < val.size() && val[iv] == ' ') ++iv;
        oldiv = iv;
      } else {
        ++iv;
      }
    }
    std::string token = val.substr(oldiv);
    trim(token);
    if (!token.empty()) hd->push_back(token);
  }

  // Store :authority as host header too (RFC 7540 §8.1.2.3)
  auto *auth = getHeaderData(":authority");
  if (auth && auth->getfirstValue()) {
    auto *host = setHeaderData("host");
    if (host) host->push_back(auth->getfirstValue()->getvalue());
  }

  // Derive cached fields and request type from stored headers
  auto *methodHd = getHeaderData(":method");
  std::string method = (methodHd && methodHd->getfirstValue()) ? methodHd->getfirstValue()->getvalue() : "GET";
  if (method == "GET")          _RequestType = GETREQUEST;
  else if (method == "POST")    _RequestType = POSTREQUEST;
  else if (method == "PUT")     _RequestType = PUTREQUEST;
  else if (method == "DELETE")  _RequestType = DELETEREQUEST;
  else                          _RequestType = GETREQUEST;

  auto *pathHd = getHeaderData(":path");
  std::string path = (pathHd && pathHd->getfirstValue()) ? pathHd->getfirstValue()->getvalue() : "/";
  _cachedRequest = path;
  _cachedRequestURL = extractPath(path);
  _cachedRequestVersion = "HTTP/2";

  return 1;
}

size_t libhttppp::HttpRequest::parseH3(const std::vector<qpack::HeaderField> &headers) {
  clear();
  _httpProtocol = 2;
  SendData.pos = 0;

  // Store ALL headers (including pseudo-headers) in _firstHeaderData.
  // Split values by ';' (matching parseH1 behaviour) so that cookie
  // parsing and other code that iterates individual Values works.
  auto trim = [](std::string& s) {
    size_t i = 0;
    while (i < s.size() && (s[i] == ' ' || s[i] == '\t')) ++i;
    if (i) s.erase(0, i);
    size_t e = s.size();
    while (e && (s[e-1] == ' ' || s[e-1] == '\t')) --e;
    if (e != s.size()) s.erase(e);
  };

  for (const auto &h : headers) {
    // Use getHeaderData first so that duplicate headers (e.g. multiple
    // cookie fields per RFC 9114 §4.3.1) APPEND values instead of
    // clearing them.  Only create a new entry when the key is new.
    auto *hd = getHeaderData(h.name);
    if (!hd) hd = setHeaderData(h.name);
    if (!hd) continue;
    const std::string &val = h.value;
    size_t oldiv = 0, iv = 0;
    while (iv < val.size()) {
      if (val[iv] == ';') {
        std::string token = val.substr(oldiv, iv - oldiv);
        trim(token);
        if (!token.empty()) hd->push_back(token);
        ++iv;
        while (iv < val.size() && val[iv] == ' ') ++iv;
        oldiv = iv;
      } else {
        ++iv;
      }
    }
    std::string token = val.substr(oldiv);
    trim(token);
    if (!token.empty()) hd->push_back(token);
  }

  // Store :authority as host header too (RFC 9114 §4.3.1)
  auto *auth = getHeaderData(":authority");
  if (auth && auth->getfirstValue()) {
    auto *host = setHeaderData("host");
    if (host) host->push_back(auth->getfirstValue()->getvalue());
  }

  // Derive cached fields and request type from stored headers
  auto *methodHd = getHeaderData(":method");
  std::string method = (methodHd && methodHd->getfirstValue()) ? methodHd->getfirstValue()->getvalue() : "GET";
  if (method == "GET")          _RequestType = GETREQUEST;
  else if (method == "POST")    _RequestType = POSTREQUEST;
  else if (method == "PUT")     _RequestType = PUTREQUEST;
  else if (method == "DELETE")  _RequestType = DELETEREQUEST;
  else                          _RequestType = GETREQUEST;

  auto *pathHd = getHeaderData(":path");
  std::string path = (pathHd && pathHd->getfirstValue()) ? pathHd->getfirstValue()->getvalue() : "/";
  _cachedRequest = path;
  _cachedRequestURL = extractPath(path);
  _cachedRequestVersion = "HTTP/3";

  return 1;
}

size_t libhttppp::HttpRequest::parseH1() {
  HTTPException excep;
  std::vector<char> header;

  // small helpers
  auto ltrim_inplace = [](std::string& s) {
    size_t i = 0;
    while (i < s.size() && std::isspace(static_cast<unsigned char>(s[i]))) ++i;
    if (i) s.erase(0, i);
  };
  auto rtrim_inplace = [](std::string& s) {
    size_t i = s.size();
    while (i && std::isspace(static_cast<unsigned char>(s[i-1]))) --i;
    if (i != s.size()) s.erase(i);
  };
  auto trim_inplace = [&](std::string& s) { rtrim_inplace(s); ltrim_inplace(s); };

  try {
    // 1) Find end of headers: "\r\n\r\n"
    size_t endpos = RecvData.search("\r\n\r\n");
    if (endpos == std::string::npos) {
      // Not enough data yet
      _RequestType = PARSEREQUEST;
      return 0;
    }

    // 2) Copy header bytes (include the first \r\n of the separator so
    //    every header line, including the last one, ends with \r\n)
    header.assign(RecvData.begin(), RecvData.begin() + endpos + 2);

    // 3) Consume header (and the delimiter) from RecvData
    endpos += 4;            // include "\r\n\r\n"
    RecvData.erase(RecvData.begin(),RecvData.begin()+endpos);

    // 4) Parse the request line (first line until CRLF)
    size_t line_end = 0;
    while (line_end < header.size() && header[line_end] != '\r') ++line_end;
    if (line_end == 0 || line_end >= header.size()) {
      excep[HTTPException::Error] << "can't parse http head";
      throw excep;
    }

    // request line as string
    std::string reqline(header.begin(), header.begin() + line_end);
    // advance past "\r\n"
    size_t cursor = line_end + 2;

    // split request line into method, target, version
    std::string method, target, version;
    {
      size_t a = 0;
      // method
      size_t sp1 = reqline.find(' ', a);
      if (sp1 == std::string::npos) {
        excep[HTTPException::Error] << "invalid request line (no spaces)";
        throw excep;
      }
      method = reqline.substr(a, sp1 - a);

      // target
      size_t sp2 = reqline.find(' ', sp1 + 1);
      if (sp2 == std::string::npos) {
        excep[HTTPException::Error] << "invalid request line (missing HTTP version)";
        throw excep;
      }
      target = reqline.substr(sp1 + 1, sp2 - (sp1 + 1));

      // version
      version = reqline.substr(sp2 + 1);
      trim_inplace(version);
    }

    // 5) Set request type
    if (method == "GET")          _RequestType = GETREQUEST;
    else if (method == "POST")    _RequestType = POSTREQUEST;
    else if (method == "PUT")     _RequestType = PUTREQUEST;
    else if (method == "DELETE")  _RequestType = DELETEREQUEST;
    else                          _RequestType = PARSEREQUEST; // or a dedicated enum for others

    // 6) Store pseudo-headers in _firstHeaderData (unified storage)
    {
      auto *hMethod = setHeaderData(":method");
      if (hMethod) hMethod->push_back(method);

      auto *hPath = setHeaderData(":path");
      if (hPath) hPath->push_back(target);

      auto *hVersion = setHeaderData(":version");
      if (hVersion) hVersion->push_back(version);
    }

    // 7) Derive cached fields from stored pseudo-headers
    _cachedRequest = target;
    _cachedRequestVersion = version;
    _cachedRequestURL = extractPath(target);

    size_t line_start = cursor;

    while (line_start < header.size()) {
      // find CR
      size_t cr = line_start;
      while (cr < header.size() && header[cr] != '\r') ++cr;

      // bounds check: ensure we have at least \r\n
      if (cr >= header.size()) {
        // no complete line found, stop parsing
        break;
      }

      // empty line guard (shouldn't occur inside since we cut at \r\n\r\n, but just in case)
      if (cr == line_start) {
        if (cr + 1 < header.size()) {
          line_start = cr + 2; // skip "\r\n"
        } else {
          break;
        }
        continue;
      }

      // extract raw line (without CR)
      std::string hline(header.begin() + line_start, header.begin() + cr);

      // move to next line (skip "\r\n")
      if (cr + 1 < header.size() && header[cr + 1] == '\n') {
        line_start = cr + 2;
      } else if (cr + 1 == header.size()) {
        // only CR at end, no LF
        break;
      } else {
        line_start = cr + 1; // tolerant
      }

      // find delimiter ':'
      size_t colon = hline.find(':');
      if (colon == std::string::npos) {
        // malformed header line; skip or treat as error
        // We'll be tolerant and skip
        continue;
      }

      std::string key = hline.substr(0, colon);
      std::string value = (colon + 1 < hline.size()) ? hline.substr(colon + 1) : std::string();

      // trim spaces around key/value
      trim_inplace(key);
      trim_inplace(value);

      // lowercase key safely
      for (size_t it = 0; it < key.size(); ++it) {
        key[it] = static_cast<char>(std::tolower(static_cast<unsigned char>(key[it])));
      }

      // store header: split value on ';' into tokens (preserving case)
      HeaderData* ncontent = setHeaderData(key);
      if (!ncontent) continue;

      size_t oldiv = 0, iv = 0;
      while (iv < value.size()) {
        if (value[iv] == ';') {
          // substring [oldiv, iv)
          std::string token = value.substr(oldiv, iv - oldiv);
          trim_inplace(token);             // remove leading/trailing spaces
          if (!token.empty()) {
            ncontent->push_back(token);
          }

          ++iv;                            // skip ';'
          while (iv < value.size() && value[iv] == ' ')
            ++iv;                        // skip spaces after ';'
          oldiv = iv;
        } else {
          ++iv;
        }
      }

      // last token (after last ';' or whole value if no ';')
      std::string token = value.substr(oldiv);
      trim_inplace(token);
      if (!token.empty()) {
        ncontent->push_back(token);
      }

    }

  } catch (netplus::NetException& e) {
    if (e.getErrorType() != netplus::NetException::Note) {
      RecvData.clear();
    }
    excep[HTTPException::Error] << "netplus error: " << e.what();
    throw excep;
  } catch (...) {
    // keep behavior similar to your original: on parse error, clean up input
    RecvData.clear();
    throw;
  }

  return header.size();
}

void libhttppp::HttpRequest::printHeader(std::string &buffer){
  if(_RequestType==GETREQUEST)
    buffer="GET ";
  else if(_RequestType==POSTREQUEST)
    buffer="POST ";
  else if(_RequestType==PUTREQUEST)
    buffer="PUT ";
  else if(_RequestType==DELETEREQUEST)
    buffer="DELETE ";

  buffer.append(_cachedRequest);
  buffer.append(" ");
  buffer.append(_cachedRequestVersion);
  buffer.append("\r\n");

  for(HeaderData *curdat=getfirstHeaderData(); curdat; curdat=curdat->nextHeaderData()){
    // Skip pseudo-headers and response pseudo-headers in H1 output
    const std::string &key = curdat->getkey();
    if (!key.empty() && key[0] == ':') continue;

    buffer.append(key);
    buffer.append(": ");

    for(HeaderData::Values *cval=curdat->getfirstValue(); cval; cval=cval->nextvalue()){
      buffer.append(cval->getvalue());
      if(cval->nextvalue())
        buffer.append("; ");
    }
    buffer.append("\r\n");
  }

  buffer.append("\r\n");
}

int libhttppp::HttpRequest::getRequestType(){
  return _RequestType;
}

const std::string &libhttppp::HttpRequest::getRequestURL(){
  if (_cachedRequestURL.empty() && !_cachedRequest.empty()) {
    _cachedRequestURL = extractPath(_cachedRequest);
  }
  if (_cachedRequestURL.empty()) {
    auto *hPath = getHeaderData(":path");
    if (hPath && hPath->getfirstValue()) {
      _cachedRequest = hPath->getfirstValue()->getvalue();
      _cachedRequestURL = extractPath(_cachedRequest);
    }
  }
  return _cachedRequestURL;
}

const std::string &libhttppp::HttpRequest::getRequest(){
  return _cachedRequest;
}

size_t libhttppp::HttpRequest::getRequestLength() {
  return _cachedRequest.length();
}

const std::string & libhttppp::HttpRequest::getRequestVersion(){
  return _cachedRequestVersion;
}

const std::string &libhttppp::HttpRequest::getHost(){
  if(_cachedHost.empty()){
    auto *hHost = getHeaderData("host");
    if(!hHost)
      hHost = getHeaderData(":authority");
    if(hHost && hHost->getfirstValue())
      _cachedHost = hHost->getfirstValue()->getvalue();
  }
  return _cachedHost;
}

size_t libhttppp::HttpRequest::getContentLength(){
  HttpHeader::HeaderData *clen=getHeaderData("content-length");
  if(!clen)
    return 0;
  return clen->at(0).getSizetValue();
}

bool libhttppp::HttpRequest::isChunkedRequest(){
  HttpHeader::HeaderData *te=getHeaderData("transfer-encoding");
  if(!te)
    return false;
  for(HeaderData::Values *v=te->getfirstValue(); v; v=v->nextvalue()){
    std::string val;
    std::transform(v->getvalue().begin(), v->getvalue().end(),
                   std::back_inserter(val),
                   [](unsigned char c){ return std::tolower(c); });
    if(val.find("chunked") != std::string::npos)
      return true;
  }
  return false;
}

// Decode an HTTP/1.1 chunked request body sitting in RecvData (the header has
// already been consumed by parse()). Idempotent across calls: while the full
// chunked body has not yet arrived RecvData is left untouched so the server
// loop can wait for more data, exactly like the Content-Length path.
//   returns  1 : body fully decoded; RecvData now holds the decoded body
//                (followed by any pipelined bytes) and Content-Length is set
//            0 : incomplete, need more data (RecvData untouched)
//           -1 : malformed chunked framing
int libhttppp::HttpRequest::decodeChunkedBody(){
  const char *d = RecvData.data();
  const size_t n = RecvData.size();
  std::string body;
  body.reserve(n); // decoded body can't exceed the raw (chunk-framed) input size
  size_t pos = 0;

  // Hard cap to avoid unbounded growth from a hostile peer (2 GiB).
  const size_t MAXBODY = static_cast<size_t>(1) << 31;

  auto findCRLF = [&](size_t from) -> size_t {
    for(size_t i = from; i + 1 < n; ++i){
      if(d[i] == '\r' && d[i+1] == '\n')
        return i;
    }
    return std::string::npos;
  };

  while(true){
    size_t lineEnd = findCRLF(pos);
    if(lineEnd == std::string::npos)
      return 0; // chunk-size line not complete yet

    // Parse the hex chunk size, ignoring any ";chunk-extension".
    size_t sz = 0;
    bool anyDigit = false;
    for(size_t i = pos; i < lineEnd; ++i){
      char c = d[i];
      if(c == ';' || c == ' ' || c == '\t')
        break;
      int hv;
      if(c >= '0' && c <= '9')      hv = c - '0';
      else if(c >= 'a' && c <= 'f') hv = c - 'a' + 10;
      else if(c >= 'A' && c <= 'F') hv = c - 'A' + 10;
      else return -1;
      sz = sz * 16 + static_cast<size_t>(hv);
      anyDigit = true;
      if(sz > MAXBODY || body.size() > MAXBODY)
        return -1;
    }
    if(!anyDigit)
      return -1;

    size_t dataStart = lineEnd + 2;

    if(sz == 0){
      // Last chunk. Consume any trailer header lines up to the final blank
      // line ("\r\n") that terminates the message.
      size_t tpos = dataStart;
      while(true){
        size_t tEnd = findCRLF(tpos);
        if(tEnd == std::string::npos)
          return 0; // trailers / terminator not fully arrived
        if(tEnd == tpos){
          // Empty line: end of body. Rebuild RecvData as decoded body plus
          // any trailing pipelined bytes after the terminator.
          size_t consumed = tEnd + 2;
          std::vector<char> trailing;
          if(consumed < n)
            trailing.assign(d + consumed, d + n);

          RecvData.clear();
          if(!body.empty())
            RecvData.append(body.data(), body.size());
          if(!trailing.empty())
            RecvData.append(trailing.data(), trailing.size());

          // Replace framing headers so downstream code (webproxy, HttpForm)
          // uses the now-known Content-Length and never sees chunked framing.
          HeaderData *cl = setHeaderData("content-length");
          if(cl){
            cl->clear();
            cl->push_back(std::to_string(body.size()));
          }
          HeaderData *te = getHeaderData("transfer-encoding");
          if(te)
            te->clear();
          return 1;
        }
        tpos = tEnd + 2; // skip one trailer line
      }
    }

    // Need the chunk data plus its trailing CRLF.
    if(dataStart + sz + 2 > n)
      return 0;
    if(d[dataStart + sz] != '\r' || d[dataStart + sz + 1] != '\n')
      return -1;
    body.append(d + dataStart, sz);
    if(body.size() > MAXBODY)
      return -1;
    pos = dataStart + sz + 2;
  }
}

size_t libhttppp::HttpRequest::getMaxUploadSize(){
  return _MaxUploadSize;
}

bool libhttppp::HttpRequest::isMobile(){
  for(HeaderData *curdat=getfirstHeaderData(); curdat; curdat=curdat->nextHeaderData()){
    if(curdat->getkey()=="user-agent"){
      for(HeaderData::Values *cval=curdat->getfirstValue(); cval; cval=cval->nextvalue()){
        std::string search;
        std::transform(cval->getvalue().begin(), cval->getvalue().end(), std::back_inserter(search),
                       [](unsigned char c){ return std::tolower(c); });
        if(search.find("mobi") != std::string::npos){
          return true;
        }
      }
      break;
    }
  }
  return false;
}

void libhttppp::HttpRequest::setRequestData(const std::string &data, size_t len){
  _cachedRequest.clear();
  _cachedRequest.resize(len);
  _cachedRequest.insert(0,data);
  _cachedRequestURL = extractPath(_cachedRequest);
  // Update :path pseudo-header in _firstHeaderData
  auto *hPath = setHeaderData(":path");
  if (hPath) { hPath->clear(); hPath->push_back(_cachedRequest); }
}

void libhttppp::HttpRequest::setMaxUploadSize(size_t upsize){
  _MaxUploadSize=upsize;
}


void libhttppp::HttpRequest::setRequestVersion(const std::string& version){
  if(!version.empty())
    _cachedRequestVersion=version;
  else
    _cachedRequestVersion.clear();
  // Update :version pseudo-header in _firstHeaderData
  auto *hVer = setHeaderData(":version");
  if (hVer) { hVer->clear(); hVer->push_back(_cachedRequestVersion); }
}

void libhttppp::HttpRequest::send(const HttpUrl &dest,
                                 std::unique_ptr<netplus::socket> &sock)
{
    std::string header;
    printHeader(header);

    if(!sock || sock->fd()<0){
        if(!sock){
            if(dest.getProtocol()==HttpUrl::HTTPS) {
                std::map<std::string, netplus::ssl::CertificateBundle> certs;
                sock=std::make_unique<netplus::ssl>(certs,-1);
            } else {
                sock=std::make_unique<netplus::tcp>(-1);
            }
        }
        sock->connect(dest.getHost(),dest.getPort(),true);
    }
    netplus::buffer buf(header.c_str(),header.length());
    sock->sendData(buf,0);
}

void libhttppp::HttpRequest::setRequestType(int req){
  if(req == POSTREQUEST || req == GETREQUEST || req == PUTREQUEST || req == DELETEREQUEST){
    _RequestType=req;
    return;
  }
  HTTPException excep;
  excep[HTTPException::Error] << "setRequestType: " << "Unknown Request will not set !";
  throw excep;
}

void libhttppp::HttpRequest::setRequestURL(const std::string &url){
  if(!url.empty()){
    _cachedRequest = url;
    _cachedRequestURL = extractPath(url);
  }else{
    _cachedRequest.clear();
    _cachedRequestURL.clear();
  }
  // Update :path pseudo-header in _firstHeaderData with the full target
  auto *hPath = setHeaderData(":path");
  if (hPath) { hPath->clear(); hPath->push_back(_cachedRequest); }
}


void libhttppp::HttpForm::parse(libhttppp::HttpRequest &request){
  // Clear previous results so re-parsing doesn't duplicate entries
  _multipartEntries.clear();
  _urlEntries.clear();
  _boundary.clear();
  _contentType.clear();

  if(request.getRequestType()==POSTREQUEST || request.getRequestType()==PUTREQUEST){
    HttpHeader::HeaderData *ctype=request.getHeaderData("content-type");

    if(!ctype){
      HTTPException excep;
      excep[HTTPException::Error] << "Http Form Parse Error: content type";
      throw excep;
    }

    // Use content-length if available, otherwise fall back to RecvData size
    // (HTTP/2 and HTTP/3 may omit Content-Length)
    size_t bodyLen = request.getContentLength();
    if(bodyLen == 0)
      bodyLen = request.RecvData.size();

    for(auto *cval=ctype->getfirstValue(); cval; cval=cval->nextvalue()){
      if(bodyLen <= request.RecvData.size()){
        if(cval->getvalue().find("multipart/form-data") != std::string::npos){
          _contentType = "multipart/form-data";
          for(auto *cbod=ctype->getfirstValue(); cbod; cbod=cbod->nextvalue()){
            size_t fcbod = cbod->getvalue().find("boundary=");
            if(fcbod != std::string::npos){
              fcbod += 9;
              std::string bval = cbod->getvalue().substr(fcbod);
              if(auto sc = bval.find(';'); sc != std::string::npos)
                bval.resize(sc);
              // trim
              bval.erase(0, bval.find_first_not_of(" \t"));
              bval.erase(bval.find_last_not_of(" \t") + 1);
              // remove quotes
              if(bval.size() >= 2 && bval.front() == '"' && bval.back() == '"')
                bval = bval.substr(1, bval.size() - 2);
              _boundary = std::move(bval);
              _parseMultipart(request.RecvData.data(), request.RecvData.size());
              break;
            }
          }
          break;
        }
        if(cval->getvalue().find("application/x-www-form-urlencoded") != std::string::npos){
          _contentType = "application/x-www-form-urlencoded";
          _parseUrlDecode(request.RecvData.data(), bodyLen);
        }
      }
    }
  }

  // Parse URL query string
  const auto &req = request.getRequest();
  if(auto qpos = req.find('?'); qpos != std::string::npos && qpos + 1 < req.size()){
    _parseUrlDecode(req.c_str() + qpos + 1, req.size() - qpos - 1);
  }
}

void libhttppp::HttpForm::urlEncode(const std::string &in, std::string &out) {
  // Indexed directly by (unsigned char) instead of a std::map<char,...> --
  // O(1) array lookup with no pointer-chasing tree traversal per input byte.
  static const std::array<std::string_view, 256> codetable = [] {
    std::array<std::string_view, 256> t{};

    // Unsafe/Form-Encoded Characters (Commonly Replaced)
    t[static_cast<unsigned char>(' ')]  = "+";

    // General Delimiters (Must be encoded if used literally)
    t[static_cast<unsigned char>('#')]  = "%23";
    t[static_cast<unsigned char>('%')]  = "%25";
    t[static_cast<unsigned char>('&')]  = "%26";
    t[static_cast<unsigned char>('+')]  = "%2B";
    t[static_cast<unsigned char>('/')]  = "%2F";
    t[static_cast<unsigned char>(':')]  = "%3A";
    t[static_cast<unsigned char>(';')]  = "%3B";
    t[static_cast<unsigned char>('=')]  = "%3D";
    t[static_cast<unsigned char>('?')]  = "%3F";
    t[static_cast<unsigned char>('@')]  = "%40";

    // Sub-Delimiters (Must be encoded if used literally)
    t[static_cast<unsigned char>('$')]  = "%24";
    t[static_cast<unsigned char>(',')]  = "%2C";
    t[static_cast<unsigned char>('(')]  = "%28";
    t[static_cast<unsigned char>(')')]  = "%29";
    t[static_cast<unsigned char>('*')]  = "%2A";
    t[static_cast<unsigned char>('\'')] = "%27";

    // Other Unsafe/Control Characters
    t[static_cast<unsigned char>('"')]  = "%22";
    t[static_cast<unsigned char>('<')]  = "%3C";
    t[static_cast<unsigned char>('>')]  = "%3E";
    t[static_cast<unsigned char>('[')]  = "%5B";
    t[static_cast<unsigned char>(']')]  = "%5D";
    t[static_cast<unsigned char>('^')]  = "%5E";
    t[static_cast<unsigned char>('`')]  = "%60";
    t[static_cast<unsigned char>('{')]  = "%7B";
    t[static_cast<unsigned char>('|')]  = "%7C";
    t[static_cast<unsigned char>('}')]  = "%7D";
    return t;
  }();

  out.clear();
  out.reserve(in.length() * 3); // Reserve space for potential growth

  for (char c : in) {
    const std::string_view &encoded = codetable[static_cast<unsigned char>(c)];
    if (!encoded.empty())
      out += encoded;
    else
      out += c;
  }
}

void libhttppp::HttpForm::urlDecode(const std::string &in,std::string &out){
    for (auto i = in.begin(), nd = in.end(); i < nd; ++i)
    {
        auto c = ( *i );

        switch(c)
        {
            case '%':
                if ( ( i + 2 < nd ) && ( std::isxdigit(*(i + 1)) && std::isxdigit(*(i + 2)) ) ) {
                    char hs[]{ i[1], i[2], '\0' };
                    char decoded_char = static_cast<char>(strtol(hs, nullptr, 16));

                    if (decoded_char == '\0') {
                        HTTPException e;
                        e[HTTPException::Error] << "Url decoded result contained null character ('%00')!";
                        throw e;
                    }

                    out.push_back(decoded_char);
                    i += 2;
                }else{
                    HTTPException e;
                    e[HTTPException::Error] << "Wrong Decoded Url!";
                    throw e;
                }
                break;
            case '+':
                out.push_back(' ');
                break;
            default:
                out.push_back(c);
        }
    }
}

void libhttppp::HttpForm::_parseMultipart(const char *data, size_t len) {
  // Build "--" + boundary
  const std::string delim = "--" + _boundary;
  const size_t B = delim.size();

  auto matchAt = [&](size_t pos) -> bool {
    if (pos + B > len) return false;
    return std::memcmp(data + pos, delim.c_str(), B) == 0;
  };

  size_t i = 0;
  size_t partStart = std::string::npos;

  // Find first boundary (may appear at offset 0 or after CRLF preamble)
  if (matchAt(0)) {
    size_t p = B;
    if (p + 1 < len && data[p] == '-' && data[p + 1] == '-') return;
    if (p + 1 >= len || data[p] != '\r' || data[p + 1] != '\n') return;
    i = p + 2;
    partStart = i;
  } else {
    while (true) {
      size_t crlf = std::string::npos;
      for (size_t k = i; k + 1 < len; ++k) {
        if (data[k] == '\r' && data[k + 1] == '\n') { crlf = k; break; }
      }
      if (crlf == std::string::npos) return;
      size_t bpos = crlf + 2;
      if (!matchAt(bpos)) { i = bpos; continue; }

      size_t p = bpos + B;
      if (p + 1 < len && data[p] == '-' && data[p + 1] == '-') return;
      if (p + 1 >= len || data[p] != '\r' || data[p + 1] != '\n') return;
      i = p + 2;
      partStart = i;
      break;
    }
  }

  // Now iterate over parts: each ends just before "\r\n--boundary" or "\r\n--boundary--"
  while (true) {
    // Find next delimiter line: look for "\r\n--boundary" from i onward
    size_t delimPos = std::string::npos;
    size_t closePos = std::string::npos;
    // scan for CRLF then boundary
    for (size_t k = i; k + 2 + B <= len; ++k) {
      if (data[k] == '\r' && data[k + 1] == '\n' && matchAt(k + 2)) {
        size_t after = k + 2 + B;
        if (after + 1 < len && data[after] == '-' && data[after + 1] == '-') {
          closePos = k; // boundary that closes the multipart
          break;        // closing takes precedence
        } else {
          delimPos = k; // normal part delimiter
          break;
        }
      }
    }

    if (delimPos == std::string::npos && closePos == std::string::npos) {
      // No terminating boundary found → malformed
      return;
    }

    size_t partEnd = (closePos != std::string::npos) ? closePos : delimPos; // points to CR of the CRLF before boundary

    if (partStart != std::string::npos && partEnd >= partStart) {
      _parseMultiSection(data, len, partStart, partEnd);
    }

    if (closePos != std::string::npos) {
      // Reached closing boundary (“--boundary--”), done
      return;
    }

    // Move i to start of next part: skip CRLF + "--boundary" + CRLF
    size_t after = delimPos + 2 + B;
    if (after + 1 >= len || data[after] != '\r' || data[after + 1] != '\n') {
      // malformed separator
      return;
    }
    i = after + 2;          // start of next part headers
    partStart = i;
  }
}

void libhttppp::HttpForm::_parseMultiSection(const char *data, size_t /*len*/, size_t start, size_t end) {
  if (!data || end <= start) return;

  // Find header/body separator (\r\n\r\n or \n\n)
  size_t headerEnd = std::string::npos;
  size_t sepSize = 4;

  for (size_t i = start; i + 3 < end; ++i) {
    if (data[i] == '\r' && data[i+1] == '\n' && data[i+2] == '\r' && data[i+3] == '\n') {
      headerEnd = i;
      break;
    }
  }

  if (headerEnd == std::string::npos) {
    sepSize = 2;
    for (size_t i = start; i + 1 < end; ++i) {
      if (data[i] == '\n' && data[i+1] == '\n') {
        headerEnd = i;
        break;
      }
    }
  }

  if (headerEnd == std::string::npos || headerEnd + sepSize >= end) return;

  MultipartEntry entry;

  // Parse MIME headers
  size_t lineStart = start;
  while (lineStart < headerEnd) {
    while (lineStart < headerEnd && (data[lineStart] == ' ' || data[lineStart] == '\r' || data[lineStart] == '\n'))
      ++lineStart;
    if (lineStart >= headerEnd) break;

    size_t lineEnd = lineStart;
    while (lineEnd < headerEnd && data[lineEnd] != '\r' && data[lineEnd] != '\n')
      ++lineEnd;

    size_t colon = lineStart;
    while (colon < lineEnd && data[colon] != ':') ++colon;

    if (colon < lineEnd) {
      MultipartEntry::Header hdr;
      hdr.key.assign(data + lineStart, data + colon);
      std::transform(hdr.key.begin(), hdr.key.end(), hdr.key.begin(),
                     [](unsigned char c) { return static_cast<char>(std::tolower(c)); });

      size_t valStart = colon + 1;
      while (valStart < lineEnd && data[valStart] == ' ') ++valStart;
      hdr.value.assign(data + valStart, data + lineEnd);

      entry.headers.push_back(std::move(hdr));
    }

    lineStart = lineEnd;
  }

  // Parse Content-Disposition parameters
  for (const auto &hdr : entry.headers) {
    if (hdr.key != "content-disposition") continue;

    const std::string &dval = hdr.value;
    size_t pos = dval.find("form-data");
    if (pos == std::string::npos) break;

    size_t sc = pos + 9;
    while (sc < dval.size()) {
      while (sc < dval.size() && (dval[sc] == ';' || dval[sc] == ' ')) ++sc;
      if (sc >= dval.size()) break;

      size_t keyStart = sc;
      while (sc < dval.size() && dval[sc] != '=' && dval[sc] != ' ' && dval[sc] != ';') ++sc;
      if (keyStart == sc) break;

      MultipartEntry::Disposition dispo;
      dispo.key = dval.substr(keyStart, sc - keyStart);

      if (sc < dval.size() && dval[sc] == '=') {
        ++sc;
        if (sc < dval.size() && dval[sc] == '"') {
          ++sc;
          size_t valStart = sc;
          while (sc < dval.size() && dval[sc] != '"') ++sc;
          dispo.value = dval.substr(valStart, sc - valStart);
          if (sc < dval.size()) ++sc;
        } else {
          size_t valStart = sc;
          while (sc < dval.size() && dval[sc] != ';' && dval[sc] != ' ') ++sc;
          dispo.value = dval.substr(valStart, sc - valStart);
        }
      }

      entry.dispositions.push_back(std::move(dispo));
    }
    break;
  }

  // Copy body data
  size_t bodyStart = headerEnd + sepSize;
  if (bodyStart < end) {
    entry.value.assign(data + bodyStart, data + end);
  }

  _multipartEntries.push_back(std::move(entry));
}

void libhttppp::HttpForm::_parseUrlDecode(const char *data, size_t csize) {
  size_t fdatstpos = 0;
  size_t keyendpos = 0;

  for (size_t fdatpos = 0; fdatpos <= csize; ++fdatpos) {
    if (fdatpos == csize || data[fdatpos] == '&') {
      if (keyendpos > fdatstpos && keyendpos < fdatpos) {
        std::string ukey(data + fdatstpos, data + keyendpos);
        std::string key;
        urlDecode(ukey, key);

        std::string value;
        size_t vlstpos = keyendpos + 1;
        if (vlstpos <= fdatpos) {
          std::string uvalue(data + vlstpos, data + fdatpos);
          urlDecode(uvalue, value);
        }

        _urlEntries.push_back({std::move(key), std::move(value)});
      }
      fdatstpos = fdatpos + 1;
      keyendpos = 0;
    } else if (data[fdatpos] == '=') {
      keyendpos = fdatpos;
    }
  }
}

libhttppp::HttpCookie::CookieData::~CookieData(){
}

libhttppp::HttpCookie::CookieData * libhttppp::HttpCookie::CookieData::nextCookieData() const{
  return _nextCookieData.get();
}

const std::string &libhttppp::HttpCookie::CookieData::getKey() const{
  return _Key;
}

const std::string &libhttppp::HttpCookie::CookieData::getValue() const{
  return _Value;
}



libhttppp::HttpCookie::HttpCookie(){
  _firstCookieData=nullptr;
  _lastCookieData=nullptr;
}

libhttppp::HttpCookie::~HttpCookie(){
}


void libhttppp::HttpCookie::setcookie(libhttppp::HttpResponse &curresp,
                                      const std::string & key, const std::string &value,
                                      const std::string &comment,const std::string &domain,
                                      int maxage, const std::string & path,
                                      bool secure,const std::string &version,const std::string &samesite,bool httponly){
  HTTPException httpexception;
  if(key.empty()){
    httpexception[HTTPException::Note] << "no key or value set in cookie!";
    throw httpexception;
  }
  HttpHeader::HeaderData *dat=curresp.setHeaderData("set-cookie");

  std::string buf=key;
  buf+="=";
  buf+=value;
  dat->push_back(buf);

  if(!comment.empty()){
    buf="comment=";
    buf+=comment;
    dat->push_back(buf);
  }
  if(!domain.empty()){
    buf="domain=";
    buf+=domain;
    dat->push_back(buf);
  }
  if(maxage>=0){
    dat->push_back("max-age=" + std::to_string(maxage));
  }
  if(!path.empty()){
    buf="path=";
    buf+=path;
    dat->push_back(buf);
  }
  if(!samesite.empty()){
    buf="sameSite=";
    buf+=samesite;
    dat->push_back(buf);
  }else{
    buf="sameSite=Lax";
    dat->push_back(buf);
  }
  if(secure){
    buf="secure";
    buf+=secure;
    dat->push_back(buf);
  }
  if(!version.empty()){
    buf="version=";
    buf+=version;
    dat->push_back(buf);
  }

  if(httponly){
    dat->push_back("httponly");
  }
}

void libhttppp::HttpCookie::parse(libhttppp::HttpRequest &curreq){
    HttpHeader::HeaderData *hc = curreq.getHeaderData("cookie");

    if(!hc){
        HTTPException httpexception;
        httpexception[HTTPException::Note] << "no cookies are set!";
        throw httpexception;
    }

    std::string raw_cookie_string = hc->getfirstValue()->getvalue();

    // Helper to trim leading/trailing whitespace
    auto trim = [](std::string& s) {
        s.erase(0, s.find_first_not_of(" \t"));
        s.erase(s.find_last_not_of(" \t") + 1);
    };

    // Loop through the raw string, splitting by semicolon (';')
    for(HttpHeader::HeaderData::Values *curval=curreq.getHeaderData("cookie")->getfirstValue(); curval; curval=curval->nextvalue()){
        std::string cpair = curval->getvalue();
        trim(cpair); // Trim whitespace around the entire pair

        size_t eq_pos = cpair.find("=");

        if (eq_pos != std::string::npos) {
            std::string key = cpair.substr(0, eq_pos);
            std::string value = cpair.substr(eq_pos + 1);

            trim(key);
            // NOTE: Cookie values are generally NOT trimmed, but if you expect quotes,
            // you should handle them here. Trimming spaces for safety.
            trim(value);

            if (!key.empty()) {
                // Assuming CookieData* cdat=addCookieData() exists and works
                CookieData *cdat = addCookieData();
                cdat->_Key = key;
                cdat->_Value = value;
            }
        }
    }
}

libhttppp::HttpCookie::CookieData *libhttppp::HttpCookie::getfirstCookieData(){
  return _firstCookieData.get();
}

libhttppp::HttpCookie::CookieData *libhttppp::HttpCookie::getlastCookieData(){
  return _lastCookieData;
}


libhttppp::HttpCookie::CookieData  *libhttppp::HttpCookie::addCookieData(){
  if(!_firstCookieData){
    _firstCookieData= std::make_unique<CookieData>();
    _lastCookieData=_firstCookieData.get();
  }else{
    _lastCookieData->_nextCookieData=std::make_unique<CookieData>();
    _lastCookieData=_lastCookieData->_nextCookieData.get();
  }
  return _lastCookieData;
}

libhttppp::HttpAuth::HttpAuth(){
  _Authtype=BASICAUTH;
}

libhttppp::HttpAuth::~HttpAuth(){
}


void libhttppp::HttpAuth::parse(libhttppp::HttpRequest &curreq) {
  HttpHeader::HeaderData *hau = curreq.getHeaderData("authorization");
  if (!hau) return;

  auto starts_with_icase = [](const std::string& s, const char* p){
    size_t n = std::strlen(p); if (s.size() < n) return false;
    for (size_t i = 0; i < n; ++i)
      if (std::tolower((unsigned char)s[i]) != std::tolower((unsigned char)p[i])) return false;
    return true;
  };

  std::string token_data;            // raw (un-lowercased) token/params
  netplus::base64 base64str;
  _Authtype = -1;

  for (auto *cval = hau->getfirstValue(); cval; cval = cval->nextvalue()) {
    const std::string& raw = cval->getvalue();

    // trim leading spaces
    size_t i = 0; while (i < raw.size() && std::isspace((unsigned char)raw[i])) ++i;
    std::string s = (i ? raw.substr(i) : raw);

    if (starts_with_icase(s, "Basic ")) {
      _Authtype = BASICAUTH;
      token_data = s.substr(6);
      // trim left spaces of token
      size_t j = 0; while (j < token_data.size() && token_data[j] == ' ') ++j;
      token_data.erase(0, j);
      base64str.setEncodedData(token_data);
      break;
    }
    if (starts_with_icase(s, "Digest ")) {
      _Authtype = DIGESTAUTH;
      token_data = s.substr(7);
      size_t j = 0; while (j < token_data.size() && token_data[j] == ' ') ++j;
      token_data.erase(0, j);
      break;
    }
  }

  auto parseDigestParameters = [this](const std::string& params) {
    size_t p = 0;
    while (p < params.size()) {
      p = params.find_first_not_of(" ,", p);
      if (p == std::string::npos) break;
      size_t eq = params.find('=', p);
      if (eq == std::string::npos) break;
      std::string key = params.substr(p, eq - p);
      key.erase(std::remove_if(key.begin(), key.end(), ::isspace), key.end());

      size_t v0 = eq + 1;
      std::string value;
      if (v0 < params.size() && params[v0] == '"') {
        ++v0;
        size_t v1 = params.find('"', v0);
        if (v1 == std::string::npos) break;
        value = params.substr(v0, v1 - v0);
        p = v1 + 1;
      } else {
        size_t v1 = params.find(',', v0);
        if (v1 == std::string::npos) v1 = params.size();
        value = params.substr(v0, v1 - v0);
        value.erase(std::remove_if(value.begin(), value.end(), ::isspace), value.end());
        p = v1;
      }

      if (key == "realm") setRealm(value);
      // add more keys as needed
    }
  };

  switch (_Authtype) {
    case BASICAUTH: {
      std::string clearstr;
      base64str >> clearstr;                     // "user:pass"
      size_t colon = clearstr.find(':');
      if (colon == std::string::npos) {
        HTTPException e; e[HTTPException::Error] << "Basic Auth payload missing separator ':'";
        throw e;
      }
      setUsername(clearstr.substr(0, colon));
      setPassword(clearstr.substr(colon + 1));
    } break;

    case DIGESTAUTH: {
      if (token_data.empty()) {
        HTTPException e; e[HTTPException::Error] << "Digest Auth header is empty"; throw e;
      }
      parseDigestParameters(token_data);
    } break;

    default: {
      HTTPException e; e[HTTPException::Error] << "No valid authorization method found"; throw e;
    }
  }
}



void libhttppp::HttpAuth::setAuth(libhttppp::HttpResponse &curresp){
  HttpHeader::HeaderData *dat = curresp.setHeaderData("WWW-Authenticate");
  HttpHeader::HeaderData::Values cval;

  auto random_hex = [] (size_t n) {
    static thread_local std::mt19937_64 rng{std::random_device{}()};
    static const char* hex = "0123456789abcdef";
    std::string s; s.reserve(n);
    for (size_t i = 0; i < n; ++i) s.push_back(hex[rng() & 0xF]);
    return s;
  };

  switch (_Authtype) {
    case BASICAUTH: {
      cval << "Basic";
      if (!_Realm.empty()) cval << " realm=\"" + _Realm + "\"";
    } break;
    case DIGESTAUTH: {
      cval << "Digest";
      if (!_Realm.empty()) cval << " realm=\"" + _Realm + "\"";
      std::string nonce = random_hex(32);
      cval << " nonce=\"" + nonce + "\"";
    } break;
    default: {
      HTTPException e; e[HTTPException::Error] << "no auth method choose !"; throw e;
    }
  }
  dat->push_back(cval);
}


void libhttppp::HttpAuth::setAuthType(int authtype){
  _Authtype=authtype;
}

void libhttppp::HttpAuth::setRealm(const std::string &realm){
  _Realm.clear();
  _Realm=realm;
}

void libhttppp::HttpAuth::setUsername(const std::string &username){
  _Username.clear();
  _Username=username;
}

void libhttppp::HttpAuth::setPassword(const std::string & password){
  _Password.clear();
  _Password=password;
}


const std::string &libhttppp::HttpAuth::getUsername(){
  return _Username;
}

const std::string &libhttppp::HttpAuth::getPassword(){
  return _Password;
}

int libhttppp::HttpAuth::getAuthType(){
  return _Authtype;
}
