/*
{{copyright}} 
*/

/*
{{version}} 
*/

/*
{{license}} 
*/

#pragma once

#include <curl/curl.h>
#include <filesystem>
#include <memory>
#include <mutex>
#include <ostream>
#include <sdi_toolBox/logs/reLog.h>
#include <string>
#include <unordered_map>

namespace sdi_ToolBox::communication
{
//--------------------------------------------------------------
class CurlGlobalInitializer
{
  public:
  static void init()     // Initialize libcurl globally
  {
    static auto instance = std::unique_ptr<CurlGlobalInitializer>(new CurlGlobalInitializer());
  }

  public:
  virtual ~CurlGlobalInitializer()     // Default destructor
  {
    curl_global_cleanup();
  }
  CurlGlobalInitializer(const CurlGlobalInitializer &obj)                = delete;     // Copy constructor
  CurlGlobalInitializer(CurlGlobalInitializer &&obj) noexcept            = delete;     // Move constructor
  CurlGlobalInitializer &operator=(const CurlGlobalInitializer &obj)     = delete;     // Copy assignment operator
  CurlGlobalInitializer &operator=(CurlGlobalInitializer &&obj) noexcept = delete;     // Move assignment operator

  protected:
  CurlGlobalInitializer()     // Default constructor
  {
    if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK)
      throw std::runtime_error("ERROR: Failed to initialize cURL");
  }
};
//--------------------------------------------------------------

/* --- */

//--------------------------------------------------------------
class CurlHttpClient
{
  public:
  enum class Method : uint8_t
  {
    http_GET,
    http_POST,
    http_PUT,
    http_DELETE,
  };

  struct Settings
  {
    bool                  enableSSL      = true;
    bool                  ssl_verifyPeer = true;
    bool                  ssl_verifyHost = true;
    std::filesystem::path ssl_caInfo;
    std::string           ssl_certBlob;
    std::chrono::seconds  timeout{ 10 };

    bool operator==(const Settings &other) const
    {
      // Compare all fields against the other object
      return (enableSSL == other.enableSSL &&
              ssl_verifyPeer == other.ssl_verifyPeer &&
              ssl_verifyHost == other.ssl_verifyHost &&
              ssl_caInfo == other.ssl_caInfo &&
              ssl_certBlob == other.ssl_certBlob);
    }
  };

  using Headers = std::unordered_map<std::string, std::string>;

  struct Request
  {
    std::string url;
    Method      method = Method::http_GET;
    std::string body;
    Headers     headers;
    Settings    settings;

    bool operator==(const Request &other) const
    {
      // Compare all fields against the other object
      return (url == other.url &&
              method == other.method &&
              body == other.body &&
              headers == other.headers &&
              settings == other.settings);
    }
  };
  struct Response
  {
    int         returnCode = -1;
    std::string content;
  };

  public:
  CurlHttpClient();                                                       // Default constructor
  virtual ~CurlHttpClient()                                = default;     // Default destructor
  CurlHttpClient(const CurlHttpClient &obj)                = delete;      // Copy constructor
  CurlHttpClient(CurlHttpClient &&obj) noexcept            = delete;      // Move constructor
  CurlHttpClient &operator=(const CurlHttpClient &obj)     = delete;      // Copy assignment operator
  CurlHttpClient &operator=(CurlHttpClient &&obj) noexcept = delete;      // Move assignment operator

  const Response &performRequest(const Request &request = {});             // Perform HTTP request
  const Response &performRequest_nothrow(const Request &request = {});     // Perform HTTP request

  const Request  &getRequest() const;      // Retrieves curl request
  const Response &getResponse() const;     // Retrieves curl response

  protected:
  mutable std::mutex m_mtx;

  CURL       *m_curl       = nullptr;
  curl_slist *m_headerList = nullptr;
  Request     m_request;
  Response    m_response;

  private:
  CURLcode setRequestOptions();     // Set cURL request options

  static size_t write_callback(void  *contents,     // Callback for writing received data
                               size_t size,
                               size_t itemCount,
                               void  *userData);
};
//--------------------------------------------------------------
/* Default constructor */
inline CurlHttpClient::CurlHttpClient()
{
  CurlGlobalInitializer::init();
}
//--------------------------------------------------------------
/* Perform HTTP request */
inline const CurlHttpClient::Response &CurlHttpClient::performRequest(const Request &request)
{
  std::lock_guard lock(m_mtx);

  if (const Request default_req = {}; request != default_req)
    m_request = request;

  try
  {
    m_curl = curl_easy_init();
    if (!m_curl)
      throw std::runtime_error("unable to initialize curl library");

    // Set request options
    if (const auto res = setRequestOptions(); res != CURLE_OK)
      throw std::runtime_error(std::format("ERROR: Failed to set up CURL options: {}", curl_easy_strerror(res)));

    // Perform request
    if (const auto res = curl_easy_perform(m_curl); res != CURLE_OK)
      throw std::runtime_error(std::format("ERROR: curl_easy_perform() failed: {}", curl_easy_strerror(res)));
    if (const auto res = curl_easy_getinfo(m_curl, CURLINFO_RESPONSE_CODE, &m_response.returnCode); res != CURLE_OK)
      throw std::runtime_error(std::format("ERROR: Failed to get content code: {}", curl_easy_strerror(res)));
  }
  catch (const std::exception &e)
  {
    m_response.returnCode = -1;
    m_response.content    = e.what();
  }

  // Free resources
  if (m_curl)
  {
    curl_easy_cleanup(m_curl);     // free the easy curl handle
    m_curl = nullptr;
  }
  if (m_headerList)
  {
    curl_slist_free_all(m_headerList);     // free the custom headers
    m_curl = nullptr;
  }

  if (m_response.returnCode == -1)
    throw std::runtime_error(m_response.content);

  return m_response;
}
//--------------------------------------------------------------
/* Perform HTTP request */
inline const CurlHttpClient::Response &CurlHttpClient::performRequest_nothrow(const Request &request)
{
  try
  {
    performRequest(request);
  }
  catch (const std::exception &e)
  {
    LogError() << e.what() << std::endl;
  }

  return m_response;
}
//--------------------------------------------------------------
/* Retrieves curl request */
inline const CurlHttpClient::Request &CurlHttpClient::getRequest() const
{
  std::lock_guard lock(m_mtx);

  return m_request;
}
//--------------------------------------------------------------
/* Retrieves curl response */
inline const CurlHttpClient::Response &CurlHttpClient::getResponse() const
{
  std::lock_guard lock(m_mtx);

  return m_response;
}
//--------------------------------------------------------------
/* Set cURL request options */
inline CURLcode CurlHttpClient::setRequestOptions()
{
  // --- Common Options ---
  // Set the target URL
  if (const auto res = curl_easy_setopt(m_curl, CURLOPT_URL, m_request.url.c_str()); res != CURLE_OK)
    return res;

  // Set the write callback function
  if (const auto res = curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, write_callback); res != CURLE_OK)
    return res;

  // Set the user data pointer for the callback (our content buffer)
  if (const auto res = curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, &m_response.content); res != CURLE_OK)
    return res;

  // Set custom headers if any
  if (m_headerList)
  {
    // Prepare header list
    for (const auto &[key, value] : m_request.headers)
    {
      const auto header = format("{0:}: {1:}", key, value);
      m_headerList      = curl_slist_append(m_headerList, header.c_str());
    }

    // Append headers to the request
    if (const auto res = curl_easy_setopt(m_curl, CURLOPT_HTTPHEADER, m_headerList); res != CURLE_OK)
      return res;
  }

  // Set the max time in seconds for the entire operation (including connection and transfer)
  if (const auto res = curl_easy_setopt(m_curl, CURLOPT_TIMEOUT, m_request.settings.timeout.count()); res != CURLE_OK)
    return res;

  // --- SSL/TLS Options ---
  // curl documentation recommends setting both for full control
  if (m_request.settings.enableSSL)
  {
    // Enable peer certificate verification
    if (const auto res = curl_easy_setopt(m_curl, CURLOPT_SSL_VERIFYPEER, 1L); res != CURLE_OK)
      return res;

    // Enable host name verification (prevents 'Man-in-the-middle' attacks)
    if (const auto res = curl_easy_setopt(m_curl, CURLOPT_SSL_VERIFYHOST, 2L); res != CURLE_OK)
      return res;

    if (!m_request.settings.ssl_caInfo.empty())
    {
      // Optionally, set the path to a CA bundle file (often not needed
      // if curl is built with a system-wide CA store)
      if (const auto res = curl_easy_setopt(m_curl, CURLOPT_CAINFO, m_request.settings.ssl_caInfo.c_str()); res != CURLE_OK)
        return res;
    }
    else if (!m_request.settings.ssl_certBlob.empty())
    {
      if (const auto res = curl_easy_setopt(m_curl, CURLOPT_SSLKEY_BLOB, &m_request.settings.ssl_certBlob); res != CURLE_OK)
        return res;
    }
  }
  else
  {
    // Disable verification (USE WITH CAUTION: Insecure!)
    if (const auto res = curl_easy_setopt(m_curl, CURLOPT_SSL_VERIFYPEER, 0L); res != CURLE_OK)
      return res;
    if (const auto res = curl_easy_setopt(m_curl, CURLOPT_SSL_VERIFYHOST, 0L); res != CURLE_OK)
      return res;
  }

  // --- Method-Specific Options ---
  switch (m_request.method)
  {
    using enum Method;
    case http_GET:
    {
      if (const auto res = curl_easy_setopt(m_curl, CURLOPT_HTTPGET, 1L); res != CURLE_OK)
        return res;
      break;
    }
    case http_POST:
    {
      if (const auto res = curl_easy_setopt(m_curl, CURLOPT_POST, 1L); res != CURLE_OK)
        return res;

      // Set the data to be sent
      if (const auto res = curl_easy_setopt(m_curl, CURLOPT_POSTFIELDS, m_request.body.c_str()); res != CURLE_OK)
        return res;
      break;
    }
    case http_PUT:
    {
      if (const auto res = curl_easy_setopt(m_curl, CURLOPT_PUT, 1L); res != CURLE_OK)
        return res;

      if (const auto res = curl_easy_setopt(m_curl, CURLOPT_CUSTOMREQUEST, "PUT"); res != CURLE_OK)
        return res;
      if (const auto res = curl_easy_setopt(m_curl, CURLOPT_POSTFIELDS, m_request.body.c_str()); res != CURLE_OK)
        return res;
      break;
    }
    case http_DELETE:
    {
      if (const auto res = curl_easy_setopt(m_curl, CURLOPT_CUSTOMREQUEST, "DELETE"); res != CURLE_OK)
        return res;
      break;
    }
  }

  return CURLE_OK;
}
//--------------------------------------------------------------
/* Callback for writing received data */
inline size_t CurlHttpClient::write_callback(void *contents, const size_t size, const size_t itemCount, void *userData)
{
  // Total size of the incoming buffer
  const auto realSize = size * itemCount;

  // Append the received data to the content string
  const auto &response = static_cast<std::string *>(userData);
  response->append(static_cast<char *>(contents), realSize);

  return realSize;
}
//--------------------------------------------------------------

/* --- */

//--------------------------------------------------------------
class CurlInfo
{
  public:
  CurlInfo();                                                 // Default constructor
  virtual ~CurlInfo()                          = default;     // Default destructor
  CurlInfo(const CurlInfo &obj)                = delete;      // Copy constructor
  CurlInfo(CurlInfo &&obj) noexcept            = delete;      // Move constructor
  CurlInfo &operator=(const CurlInfo &obj)     = delete;      // Copy assignment operator
  CurlInfo &operator=(CurlInfo &&obj) noexcept = delete;      // Move assignment operator

  void toStream(std::ostream &stream);     // Print cURL information to stream object
  void print();                            // Print cURL information to cout

  std::string version;
  uint32_t    features;

  bool        ssl_support = false;
  std::string ssl_version;

  std::vector<std::string> availableProtocols;
};
//--------------------------------------------------------------
/* Default constructor */
inline CurlInfo::CurlInfo()
{
  CurlGlobalInitializer::init();

  // Retrieves information from the library
  const auto info = curl_version_info(CURLVERSION_NOW);

  version  = info->version;
  features = info->features;

  ssl_support = info->features & CURL_VERSION_SSL;
  ssl_version = info->ssl_version;

  if (info->protocols)
  {
    for (const char *const *proto = info->protocols; *proto; ++proto)
      availableProtocols.emplace_back(*proto);
  }
}
//--------------------------------------------------------------
/* Print cURL information to stream object */
inline void CurlInfo::toStream(std::ostream &stream)
{
  stream << "--- libcurl Version Info ---" << "\n";
  stream << "Version            : " << version << "\n";
  stream << "Features (bitmask) : " << std::format("0x{0:X}", features) << "\n";
  stream << "SSL/TLS support    : " << ((ssl_support) ? std::format("**Yes** [{0:}]", ssl_version) : "No") << "\n";
  if (!availableProtocols.empty())
  {
    stream << "Available protocols: ";
    bool first = true;
    for (const auto protocol : availableProtocols)
    {
      if (!first)
        stream << ", ";
      stream << protocol;
      first = false;
    }
    stream << "\n";
  }
}
//--------------------------------------------------------------
/* Print cURL information to cout */
inline void CurlInfo::print()
{
  toStream(std::cout);
  std::cout << std::flush;
}
//--------------------------------------------------------------
}     // namespace sdi_ToolBox::communication
