// Copyright 2025 Bloomberg Finance L.P
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <buildboxcommon_exception.h>
#include <buildboxcommon_httpclient.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_stringutils.h>
#include <buildboxcommon_version.h>
#include <stdexcept>

#include <curl/curl.h>
#include <iostream>
#include <sstream>
#include <string>
#include <thread>

namespace buildboxcommon {
// RAII wrapper for CURL easy handle and header list
class CurlWrapper {
  public:
    CurlWrapper() : d_curl(curl_easy_init())
    {
        if (!d_curl) {
            throw HTTPException("Failed to initialize curl handle");
        }
    }
    ~CurlWrapper()
    {
        if (d_headerList) {
            curl_slist_free_all(d_headerList);
        }
        if (d_curl) {
            curl_easy_cleanup(d_curl);
        }
    }
    CurlWrapper(const CurlWrapper &) = delete;
    CurlWrapper &operator=(const CurlWrapper &) = delete;
    CurlWrapper(CurlWrapper &&other) = delete;
    CurlWrapper &operator=(CurlWrapper &&other) = delete;
    CURL *get() const { return d_curl; }
    struct curl_slist **headerListPtr() { return &d_headerList; }

  private:
    CURL *d_curl;
    struct curl_slist *d_headerList = nullptr;
};
} // namespace buildboxcommon

namespace {
struct curl_slist *
headersToList(const buildboxcommon::HTTPClient::HeaderMap &headers)
{
    struct curl_slist *headerList = NULL;
    for (const auto &[key, value] : headers) {
        std::string header = key + ": " + value;
        headerList = curl_slist_append(headerList, header.c_str());
    }
    return headerList;
}

std::string retryingRequestInfoMessage(const std::string &url,
                                       const std::string &errorMessage,
                                       unsigned int attemptNumber,
                                       unsigned int totalAttempts,
                                       int retryDelayMs)
{
    std::stringstream ss;
    ss << "HTTP request attempt " << attemptNumber + 1 << "/"
       << totalAttempts + 1 << " to URL \"" << url
       << "\" failed with error: " << errorMessage << ", retrying in "
       << retryDelayMs << " ms...";
    return ss.str();
}

std::string retryingRequestErrorMessage(const std::string &url,
                                        const std::string &errorMessage,
                                        unsigned int retryLimit)
{
    std::stringstream ss;
    ss << "HTTP request to URL \"" << url << "\" failed after "
       << retryLimit + 1 << " attempts. "
       << "Last error: " << errorMessage;
    return ss.str();
}

// Internal struct for streaming state
struct StreamingData {
    explicit StreamingData(
        const buildboxcommon::StreamCallback *cb,
        std::map<std::string, std::string> *headers = nullptr)
        : d_callback(cb), d_headers(headers)
    {
    }
    const buildboxcommon::StreamCallback *d_callback;
    bool d_aborted = false;
    std::map<std::string, std::string> *d_headers;
};

size_t streamingWriteCallback(char *ptr, size_t size, size_t nmemb,
                              void *userdata)
{
    StreamingData *data = static_cast<StreamingData *>(userdata);
    const size_t realSize = size * nmemb;
    if (data->d_aborted) {
        return CURL_WRITEFUNC_ERROR; // Abort transfer if already aborted
    }
    bool continueProcessing = (*data->d_callback)(ptr, realSize);
    if (!continueProcessing) {
        data->d_aborted = true;
        return CURL_WRITEFUNC_ERROR; // Abort transfer on user request
    }
    return realSize;
}
} // anonymous namespace

namespace buildboxcommon {

std::string getDefaultUserAgent()
{
    return "BuildBox/" + VERSION +
           " (+https://gitlab.com/BuildGrid/buildbox/buildbox)";
}

// Initialize static members
const std::set<long> HTTPClient::DEFAULT_RETRY_CODES = {
    408, // Request Timeout
    429, // Too Many Requests
    500, // Internal Server Error
    502, // Bad Gateway
    503, // Service Unavailable
    504  // Gateway Timeout
};

// Set of CURLcode values considered retriable for network errors
const std::set<CURLcode> HTTPClient::CURL_RETRIABLE_CODES = {
    CURLE_COULDNT_RESOLVE_HOST, CURLE_COULDNT_CONNECT,
    CURLE_OPERATION_TIMEDOUT,   CURLE_SEND_ERROR,
    CURLE_RECV_ERROR,           CURLE_PARTIAL_FILE,
    CURLE_GOT_NOTHING};

HTTPClient::HTTPClient(const HTTPClientConfig &config)
    : d_maxRetries(config.maxRetries), d_initialDelayMs(config.initialDelayMs),
      d_backoffFactor(config.backoffFactor),
      d_retryCodes(config.retryCodes.empty() ? DEFAULT_RETRY_CODES
                                             : config.retryCodes),
      d_timeoutSeconds(config.timeoutSeconds),
      d_verifyCertificate(config.verifyCertificate),
      d_caCertPath(config.caCertPath), d_clientCertPath(config.clientCertPath),
      d_clientKeyPath(config.clientKeyPath), d_proxy(config.proxy),
      d_userAgent(config.userAgent.value_or(getDefaultUserAgent()))
{
    // Check if curl is thread-safe
    curl_version_info_data *vinfo = curl_version_info(CURLVERSION_NOW);
    if (!(vinfo->features & CURL_VERSION_THREADSAFE)) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            HTTPException,
            "libcurl was not compiled with thread-safe support. "
            "This is required for HTTPClient to function properly.");
    }

    CURLcode result = curl_global_init(CURL_GLOBAL_ALL);
    if (result != CURLE_OK) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            HTTPException,
            "Failed to initialize libcurl: " << curl_easy_strerror(result));
    }
}

HTTPClient::HTTPClient(unsigned int maxRetries, int initialDelayMs,
                       double backoffFactor)
    : HTTPClient(HTTPClientConfig{.maxRetries = maxRetries,
                                  .initialDelayMs = initialDelayMs,
                                  .backoffFactor = backoffFactor,
                                  .retryCodes = DEFAULT_RETRY_CODES,
                                  .timeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
                                  .verifyCertificate = true,
                                  .caCertPath = {},
                                  .clientCertPath = {},
                                  .clientKeyPath = {},
                                  .proxy = {},
                                  .userAgent = {}})
{
}

HTTPClient::HTTPClient(unsigned int maxRetries, int initialDelayMs,
                       double backoffFactor, const std::set<long> &retryCodes)
    : HTTPClient(HTTPClientConfig{.maxRetries = maxRetries,
                                  .initialDelayMs = initialDelayMs,
                                  .backoffFactor = backoffFactor,
                                  .retryCodes = retryCodes,
                                  .timeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
                                  .verifyCertificate = true,
                                  .caCertPath = {},
                                  .clientCertPath = {},
                                  .clientKeyPath = {},
                                  .proxy = {},
                                  .userAgent = {}})
{
}

HTTPClient::HTTPClient(unsigned int maxRetries, int initialDelayMs,
                       double backoffFactor, const std::set<long> &retryCodes,
                       long timeoutSeconds)
    : HTTPClient(HTTPClientConfig{.maxRetries = maxRetries,
                                  .initialDelayMs = initialDelayMs,
                                  .backoffFactor = backoffFactor,
                                  .retryCodes = retryCodes,
                                  .timeoutSeconds = timeoutSeconds,
                                  .verifyCertificate = true,
                                  .caCertPath = {},
                                  .clientCertPath = {},
                                  .clientKeyPath = {},
                                  .proxy = {},
                                  .userAgent = {}})
{
}

HTTPClient::~HTTPClient() { curl_global_cleanup(); }

HTTPResponse HTTPClient::get(const std::string &url, const HeaderMap &headers)
{
    CurlWrapper curlGuard;
    RequestData requestData = {.d_responseBody = "", .d_responseHeaders = {}};

    CURL *curl = curlGuard.get();
    struct curl_slist **headerList = curlGuard.headerListPtr();

    CURLcode result = setupRequest(headerList, url, headers, curl);
    if (result != CURLE_OK) {
        throw HTTPException(std::string("Failed to set curl options: ") +
                            curl_easy_strerror(result));
    }

    // No need to set HTTP GET method explicitly; GET is the default for
    // libcurl.

    result = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
    if (result != CURLE_OK) {
        throw HTTPException(std::string("Failed to set write callback: ") +
                            curl_easy_strerror(result));
    }

    result = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &requestData);
    if (result != CURLE_OK) {
        throw HTTPException(std::string("Failed to set write data: ") +
                            curl_easy_strerror(result));
    }

    result = curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headerCallback);
    if (result != CURLE_OK) {
        throw HTTPException(std::string("Failed to set header callback: ") +
                            curl_easy_strerror(result));
    }

    result = curl_easy_setopt(curl, CURLOPT_HEADERDATA, &requestData);
    if (result != CURLE_OK) {
        throw HTTPException(std::string("Failed to set header data: ") +
                            curl_easy_strerror(result));
    }

    return performRequest(&requestData, url, curl);
}

HTTPResponse HTTPClient::post(const std::string &url, const std::string &data,
                              const HeaderMap &headers)
{
    CurlWrapper curlGuard;
    RequestData requestData = {.d_responseBody = "", .d_responseHeaders = {}};

    CURL *curl = curlGuard.get();
    struct curl_slist **headerList = curlGuard.headerListPtr();

    CURLcode result = setupRequest(headerList, url, headers, curl);
    if (result != CURLE_OK) {
        throw HTTPException(std::string("Failed to set curl options: ") +
                            curl_easy_strerror(result));
    }

    // Set HTTP POST method with data
    result = curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
    if (result != CURLE_OK) {
        throw HTTPException(std::string("Failed to set POST data: ") +
                            curl_easy_strerror(result));
    }

    result = curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, data.length());
    if (result != CURLE_OK) {
        throw HTTPException(std::string("Failed to set POST size: ") +
                            curl_easy_strerror(result));
    }

    // Set up callbacks
    result = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
    if (result != CURLE_OK) {
        throw HTTPException(std::string("Failed to set write callback: ") +
                            curl_easy_strerror(result));
    }

    result = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &requestData);
    if (result != CURLE_OK) {
        throw HTTPException(std::string("Failed to set write data: ") +
                            curl_easy_strerror(result));
    }

    result = curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headerCallback);
    if (result != CURLE_OK) {
        throw HTTPException(std::string("Failed to set header callback: ") +
                            curl_easy_strerror(result));
    }

    result = curl_easy_setopt(curl, CURLOPT_HEADERDATA, &requestData);
    if (result != CURLE_OK) {
        throw HTTPException(std::string("Failed to set header data: ") +
                            curl_easy_strerror(result));
    }

    return performRequest(&requestData, url, curl);
}

HTTPResponse HTTPClient::head(const std::string &url, const HeaderMap &headers)
{
    CurlWrapper curlGuard;
    RequestData requestData = {.d_responseBody = "", .d_responseHeaders = {}};

    CURL *curl = curlGuard.get();
    struct curl_slist **headerList = curlGuard.headerListPtr();

    CURLcode result = setupRequest(headerList, url, headers, curl);
    if (result != CURLE_OK) {
        throw HTTPException(std::string("Failed to set curl options: ") +
                            curl_easy_strerror(result));
    }

    // Set HTTP HEAD method (no body is returned)
    result = curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
    if (result != CURLE_OK) {
        throw HTTPException(std::string("Failed to set HTTP HEAD method: ") +
                            curl_easy_strerror(result));
    }

    // Set up callbacks (only for headers, no body for HEAD)
    result = curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headerCallback);
    if (result != CURLE_OK) {
        throw HTTPException(std::string("Failed to set header callback: ") +
                            curl_easy_strerror(result));
    }

    result = curl_easy_setopt(curl, CURLOPT_HEADERDATA, &requestData);
    if (result != CURLE_OK) {
        throw HTTPException(std::string("Failed to set header data: ") +
                            curl_easy_strerror(result));
    }

    return performRequest(&requestData, url, curl);
}

HTTPResponse HTTPClient::streamDownload(const std::string &url,
                                        const HeaderMap &headers,
                                        const StreamCallback &callback)
{
    if (url.empty()) {
        throw std::logic_error("streamDownload: URL must not be empty");
    }
    if (!callback) {
        throw std::logic_error("streamDownload: callback must not be null");
    }

    // HEAD request to check for Accept-Ranges support (for future resume
    // capability)
    HTTPResponse headResponse = head(url, headers);
    // Accept-Ranges check is for future use; suppress unused warning
    (void)headResponse;

    unsigned int attempt = 0;
    while (true) {
        CurlWrapper curlGuard;
        CURL *curl = curlGuard.get();
        struct curl_slist **headerList = curlGuard.headerListPtr();

        CURLcode result = setupRequest(headerList, url, headers, curl);
        if (result != CURLE_OK) {
            throw HTTPException(std::string("Failed to set curl options: ") +
                                curl_easy_strerror(result));
        }

        auto streamingData = std::make_unique<StreamingData>(&callback);
        result = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION,
                                  streamingWriteCallback);
        if (result != CURLE_OK) {
            throw HTTPException(
                std::string("Failed to set streaming write callback: ") +
                curl_easy_strerror(result));
        }
        result =
            curl_easy_setopt(curl, CURLOPT_WRITEDATA, streamingData.get());
        if (result != CURLE_OK) {
            throw HTTPException(
                std::string("Failed to set streaming write data: ") +
                curl_easy_strerror(result));
        }

        RequestData requestData = {.d_responseBody = "",
                                   .d_responseHeaders = {}};
        result =
            curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headerCallback);
        if (result != CURLE_OK) {
            throw HTTPException(
                std::string("Failed to set header callback: ") +
                curl_easy_strerror(result));
        }
        result = curl_easy_setopt(curl, CURLOPT_HEADERDATA, &requestData);
        if (result != CURLE_OK) {
            throw HTTPException(std::string("Failed to set header data: ") +
                                curl_easy_strerror(result));
        }

        CURLcode performResult = curl_easy_perform(curl);
        long statusCode = 0;
        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode);

        if (performResult != CURLE_OK) {
            if (streamingData->d_aborted &&
                performResult == CURLE_WRITE_ERROR) {
                throw HTTPStreamAborted("Download aborted by user callback");
            }
            // Retry on retriable CURL error codes
            if (attempt < d_maxRetries && isCurlRetriable(performResult)) {
                const int delayMs = static_cast<int>(
                    d_initialDelayMs * std::pow(d_backoffFactor, attempt));
                BUILDBOX_LOG_INFO(retryingRequestInfoMessage(
                    url, curl_easy_strerror(performResult), attempt,
                    d_maxRetries, delayMs));
                std::this_thread::sleep_for(
                    std::chrono::milliseconds(delayMs));
                ++attempt;
                continue;
            }
            throw HTTPException(
                std::string("HTTP streaming request failed: ") +
                    curl_easy_strerror(performResult),
                statusCode);
        }

        HTTPResponse response;
        response.d_statusCode = statusCode;
        response.d_headers = requestData.d_responseHeaders;
        response.d_body = "";

        // Retry on retryable status codes
        if (d_retryCodes.find(statusCode) != d_retryCodes.end()) {
            if (attempt < d_maxRetries) {
                const int delayMs = static_cast<int>(
                    d_initialDelayMs * std::pow(d_backoffFactor, attempt));
                BUILDBOX_LOG_INFO(retryingRequestInfoMessage(
                    url, "HTTP status code: " + std::to_string(statusCode),
                    attempt, d_maxRetries, delayMs));
                std::this_thread::sleep_for(
                    std::chrono::milliseconds(delayMs));
                ++attempt;
                continue;
            }
            else {
                BUILDBOX_LOG_ERROR(retryingRequestErrorMessage(
                    url,
                    "Max retries exceeded for HTTP status code: " +
                        std::to_string(statusCode),
                    d_maxRetries));
                throw HTTPException(
                    "Max retries exceeded for HTTP status code: " +
                        std::to_string(statusCode),
                    statusCode);
            }
        }
        else if (statusCode < HTTP_SUCCESS_MIN ||
                 statusCode > HTTP_SUCCESS_MAX) {
            BUILDBOX_LOG_ERROR("Non-retryable HTTP status code: " +
                               std::to_string(statusCode));
            throw HTTPException("Non-retryable HTTP status code: " +
                                    std::to_string(statusCode),
                                statusCode);
        }

        return response;
    }
}

HTTPResponse HTTPClient::performRequest(RequestData *requestData,
                                        const std::string &url, CURL *curl)
{
    HTTPResponse response;
    unsigned int attempt = 0;

    while (true) {
        requestData->d_responseBody.clear();
        requestData->d_responseHeaders.clear();

        CURLcode result = curl_easy_perform(curl);
        if (result != CURLE_OK) {
            // Retry on retriable CURL error codes
            if (attempt < d_maxRetries && isCurlRetriable(result)) {
                const int delayMs = static_cast<int>(
                    d_initialDelayMs * std::pow(d_backoffFactor, attempt));
                BUILDBOX_LOG_INFO(retryingRequestInfoMessage(
                    url, curl_easy_strerror(result), attempt, d_maxRetries,
                    delayMs));
                std::this_thread::sleep_for(
                    std::chrono::milliseconds(delayMs));
                attempt++;
                continue;
            }
            throw HTTPException(std::string("HTTP request failed: ") +
                                curl_easy_strerror(result));
        }

        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE,
                          &response.d_statusCode);
        response.d_body = requestData->d_responseBody;
        response.d_headers = requestData->d_responseHeaders;

        if (d_retryCodes.find(response.d_statusCode) != d_retryCodes.end()) {
            if (attempt < d_maxRetries) {
                const int delayMs = static_cast<int>(
                    d_initialDelayMs * std::pow(d_backoffFactor, attempt));

                BUILDBOX_LOG_INFO(retryingRequestInfoMessage(
                    url,
                    "HTTP status code: " +
                        std::to_string(response.d_statusCode),
                    attempt, d_maxRetries, delayMs));

                std::this_thread::sleep_for(
                    std::chrono::milliseconds(delayMs));
                attempt++;
                continue;
            }
            else {
                BUILDBOX_LOG_ERROR(retryingRequestErrorMessage(
                    url,
                    "Max retries exceeded for HTTP status code: " +
                        std::to_string(response.d_statusCode),
                    d_maxRetries));
                throw HTTPException(
                    "Max retries exceeded for HTTP status code: " +
                        std::to_string(response.d_statusCode),
                    response.d_statusCode);
            }
        }
        else if (response.d_statusCode < HTTP_SUCCESS_MIN ||
                 response.d_statusCode > HTTP_SUCCESS_MAX) {
            BUILDBOX_LOG_ERROR("Non-retryable HTTP status code: " +
                               std::to_string(response.d_statusCode));
            throw HTTPException("Non-retryable HTTP status code: " +
                                    std::to_string(response.d_statusCode),
                                response.d_statusCode);
        }

        return response;
    }
}

CURLcode HTTPClient::setupRequest(struct curl_slist **headerList,
                                  const std::string &url,
                                  const HeaderMap &headers, CURL *curl)
{
    CURLcode result = CURLE_OK;

    // Set URL and other basic options
    result = curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
    if (result != CURLE_OK) {
        return result;
    }

    result = curl_easy_setopt(curl, CURLOPT_TIMEOUT, d_timeoutSeconds);
    if (result != CURLE_OK) {
        return result;
    }

    result = curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L);
    if (result != CURLE_OK) {
        return result;
    }

    // Disable signal handling to prevent races in multithreaded apps
    // See: https://curl.se/libcurl/c/threadsafe.html
    result = curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
    if (result != CURLE_OK) {
        return result;
    }

    // Set SSL verification options
    result = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER,
                              d_verifyCertificate ? 1L : 0L);
    if (result != CURLE_OK) {
        return result;
    }

    result = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST,
                              d_verifyCertificate ? 2L : 0L);
    if (result != CURLE_OK) {
        return result;
    }

    // Set CA certificate path if provided
    if (d_caCertPath.has_value()) {
        result = curl_easy_setopt(curl, CURLOPT_CAINFO, d_caCertPath->c_str());
        if (result != CURLE_OK) {
            return result;
        }
    }

    // Set client certificate if provided
    if (d_clientCertPath.has_value()) {
        result =
            curl_easy_setopt(curl, CURLOPT_SSLCERT, d_clientCertPath->c_str());
        if (result != CURLE_OK) {
            return result;
        }
    }

    // Set client key if provided
    if (d_clientKeyPath.has_value()) {
        result =
            curl_easy_setopt(curl, CURLOPT_SSLKEY, d_clientKeyPath->c_str());
        if (result != CURLE_OK) {
            return result;
        }
    }

    // Set proxy if provided
    if (d_proxy.has_value()) {
        result = curl_easy_setopt(curl, CURLOPT_PROXY, d_proxy->c_str());
        if (result != CURLE_OK) {
            return result;
        }
    }

    // Set user agent
    result = curl_easy_setopt(curl, CURLOPT_USERAGENT, d_userAgent.c_str());
    if (result != CURLE_OK) {
        return result;
    }

    // Follow redirects
    result = curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
    if (result != CURLE_OK) {
        return result;
    }

    result = curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 10L);
    if (result != CURLE_OK) {
        return result;
    }

    // Allow sending auth headers to redirected URLs
    result = curl_easy_setopt(curl, CURLOPT_UNRESTRICTED_AUTH, 1L);
    if (result != CURLE_OK) {
        return result;
    }

    // Add headers if provided
    *headerList = headersToList(headers);
    if (*headerList != NULL) {
        result = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, *headerList);
        if (result != CURLE_OK) {
            return result;
        }
    }

    // Enable curl verbose debugging only at TRACE level only
    if (logging::Logger::getLoggerInstance().getLogLevel() <=
        LogLevel::TRACE) {
        result = curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
        if (result != CURLE_OK) {
            return result;
        }
    }

    return CURLE_OK;
}

// Callback function for writing response data
size_t HTTPClient::writeCallback(char *ptr, size_t size, size_t nmemb,
                                 void *userdata)
{
    RequestData *requestData = static_cast<RequestData *>(userdata);
    const size_t realSize = size * nmemb;

    // For all requests, append to response body
    requestData->d_responseBody.append(ptr, realSize);
    return realSize;
}

// Callback function for processing headers
size_t HTTPClient::headerCallback(char *buffer, size_t size, size_t nitems,
                                  void *userdata)
{
    RequestData *requestData = static_cast<RequestData *>(userdata);
    const size_t bytes = size * nitems;
    std::string header(buffer, bytes);

    // Remove trailing CR/LF
    if (!header.empty() && header.back() == '\n') {
        header.pop_back();
    }
    if (!header.empty() && header.back() == '\r') {
        header.pop_back();
    }

    // Parse and store header if not empty and contains a colon
    if (!header.empty()) {
        size_t colonPos = header.find(':');
        if (colonPos != std::string::npos) {
            std::string key = header.substr(0, colonPos);
            std::string value = header.substr(colonPos + 1);

            // Trim leading/trailing whitespace and CR/LF from the value
            buildboxcommon::StringUtils::trim(&value);

            requestData->d_responseHeaders[key] = value;
        }
    }

    return bytes;
}

// Check if a CURL error code is retriable
bool HTTPClient::isCurlRetriable(CURLcode code) const
{
    return CURL_RETRIABLE_CODES.find(code) != CURL_RETRIABLE_CODES.end();
}

} // namespace buildboxcommon
