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

#ifndef INCLUDED_BUILDBOXCOMMON_HTTPCLIENT
#define INCLUDED_BUILDBOXCOMMON_HTTPCLIENT

#include <buildboxcommon_logging.h>
#include <buildboxcommon_version.h>

#include <curl/curl.h>

#include <functional>
#include <map>
#include <optional>
#include <set>
#include <stdexcept>
#include <string>

namespace buildboxcommon {

// Get default User-Agent string for buildbox-common
std::string getDefaultUserAgent();

static constexpr long HTTP_SUCCESS_MIN = 200;
static constexpr long HTTP_SUCCESS_MAX = 299;

// Exception for HTTP client errors. Thrown on HTTP failures.
class HTTPException : public std::runtime_error {
  public:
    explicit HTTPException(const std::string &message)
        : std::runtime_error(message)
    {
    }

    explicit HTTPException(const std::string &message, long statusCode)
        : std::runtime_error(message), d_statusCode(statusCode)
    {
    }

    long statusCode() const { return d_statusCode; }

  private:
    long d_statusCode = 0;
};

// Exception thrown when streaming is aborted by the user's callback
class HTTPStreamAborted : public HTTPException {
  public:
    // Construct with a message
    explicit HTTPStreamAborted(const std::string &message)
        : HTTPException(message)
    {
    }
};

// Callback type for streaming download chunks. Returns true to continue, false
// to abort.
using StreamCallback = std::function<bool(const char *data, size_t size)>;

// Represents an HTTP response, including status, body, and headers.
struct HTTPResponse {
    long d_statusCode = 0;
    std::string d_body;
    std::map<std::string, std::string> d_headers;

    bool isSuccess() const
    {
        return d_statusCode >= HTTP_SUCCESS_MIN &&
               d_statusCode <= HTTP_SUCCESS_MAX;
    }
};

// Configuration struct for HTTPClient construction
struct HTTPClientConfig {
    unsigned int maxRetries = 3;
    int initialDelayMs = 1000; // 1 second
    double backoffFactor = 2.0;
    std::set<long> retryCodes;
    long timeoutSeconds = 300; // 5 minutes
    bool verifyCertificate = true;
    std::optional<std::string> caCertPath;
    std::optional<std::string> clientCertPath;
    std::optional<std::string> clientKeyPath;
    std::optional<std::string> proxy;
    std::optional<std::string> userAgent;
};

// HTTP client for making HTTP requests with retry and streaming support.
class HTTPClient {
  public:
    using HeaderMap = std::map<std::string, std::string>;

    // Default HTTP status codes that should trigger a retry
    static const std::set<long> DEFAULT_RETRY_CODES;

    // Create HTTP client with configuration struct (recommended)
    explicit HTTPClient(const HTTPClientConfig &config = {});

    // Common constructor for basic retry configuration
    HTTPClient(unsigned int maxRetries, int initialDelayMs,
               double backoffFactor);

    // Constructor with custom retry codes
    HTTPClient(unsigned int maxRetries, int initialDelayMs,
               double backoffFactor, const std::set<long> &retryCodes);

    // Constructor with timeout configuration
    HTTPClient(unsigned int maxRetries, int initialDelayMs,
               double backoffFactor, const std::set<long> &retryCodes,
               long timeoutSeconds);

    // Destructor
    ~HTTPClient();

    // Send HTTP GET request to the given URL with optional headers
    HTTPResponse get(const std::string &url,
                     const HeaderMap &headers = HeaderMap());

    // Send HTTP HEAD request to the given URL with optional headers
    HTTPResponse head(const std::string &url,
                      const HeaderMap &headers = HeaderMap());

    // Send HTTP POST request to the given URL with data and optional headers
    HTTPResponse post(const std::string &url, const std::string &data,
                      const HeaderMap &headers = HeaderMap());

    // Stream a GET request, invoking the callback for each chunk of data
    // received.
    HTTPResponse streamDownload(const std::string &url,
                                const HeaderMap &headers,
                                const StreamCallback &callback);

  private:
    // Default retry parameters
    static constexpr unsigned int DEFAULT_MAX_RETRIES = 3;
    static constexpr int DEFAULT_INITIAL_DELAY_MS = 1000; // 1 second
    static constexpr double DEFAULT_BACKOFF_FACTOR = 2.0;
    static constexpr long DEFAULT_TIMEOUT_SECONDS = 300; // 5 minutes

    // Set of CURLcode values considered retriable for network errors
    static const std::set<CURLcode> CURL_RETRIABLE_CODES;

    // Retry configuration
    unsigned int d_maxRetries;
    int d_initialDelayMs;
    double d_backoffFactor;
    std::set<long> d_retryCodes;

    // Request configuration
    long d_timeoutSeconds;
    bool d_verifyCertificate;
    std::optional<std::string> d_caCertPath;
    std::optional<std::string> d_clientCertPath;
    std::optional<std::string> d_clientKeyPath;
    std::optional<std::string> d_proxy;
    std::string d_userAgent;

    // Callback data structure for libcurl
    struct RequestData {
        std::string d_responseBody;
        std::map<std::string, std::string> d_responseHeaders;
    };

    // Static callback functions for libcurl
    static size_t writeCallback(char *ptr, size_t size, size_t nmemb,
                                void *userdata);
    static size_t headerCallback(char *buffer, size_t size, size_t nitems,
                                 void *userdata);

    // Perform the actual HTTP request with retry logic
    HTTPResponse performRequest(RequestData *requestData,
                                const std::string &url, CURL *curl);

    // Configure the curl handle with common options
    CURLcode setupRequest(struct curl_slist **headerList,
                          const std::string &url, const HeaderMap &headers,
                          CURL *curl);

    // Check if a CURL error code is retriable
    bool isCurlRetriable(CURLcode code) const;
};

} // namespace buildboxcommon

#endif // INCLUDED_BUILDBOXCOMMON_HTTPCLIENT
