#include "tile/base/internal/curl.h"

#include "tile/base/deferred.h"
#include "tile/base/internal/utility.h"
#include "tile/base/logging.h"
#include "tile/base/never_destroyed.h"
#include "tile/base/thread/mutex.h"

#include "curl/curl.h"

#include <array>
#include <chrono>
#include <memory>
#include <string>
#include <utility>
#include <vector>

namespace tile {
namespace internal {
namespace {
HttpPostMockHandler s_http_post_mock_handler;

HttpGetMockHandler s_http_get_mock_handler;

size_t
HttpWriteCallback(char *ptr, size_t size, size_t nmemb, void *pstr)
{
    auto bytes = size * nmemb;
    // What ?
    static_cast<std::string *>(pstr)->append(ptr, bytes);
    return bytes;
}

#define CURL_SHARE_SETOPT(handle, opt, val) TILE_CHECK_EQ(CURLSHE_OK, curl_share_setopt(handle, opt, val))

static NeverDestroyed<std::array<Mutex, CURL_LOCK_DATA_LAST>> share_handle_mutex;

void
AcquireLockOfShareHandle(CURL *handle, curl_lock_data data, curl_lock_access access, void *userptr)
{
    (*share_handle_mutex)[data].Lock();
}

void
ReleaseLockOfShareHandle(CURL *handle, curl_lock_data data, void *userptr)
{
    (*share_handle_mutex)[data].Unlock();
}

CURLSH *
GetCurlShareHandle()
{
    using Handle = std::unique_ptr<CURLSH, decltype(&curl_share_cleanup)>;
    static NeverDestroyed<Handle> share([] {
        Handle share{curl_share_init(), &curl_share_cleanup};
        CURL_SHARE_SETOPT(share.get(), CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
        CURL_SHARE_SETOPT(share.get(), CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);

        CURL_SHARE_SETOPT(share.get(), CURLSHOPT_LOCKFUNC, AcquireLockOfShareHandle);
        CURL_SHARE_SETOPT(share.get(), CURLSHOPT_UNLOCKFUNC, ReleaseLockOfShareHandle);

        return share;
    }());

    return share->get();
}

#define CURL_SETOPT(handle, opt, val) TILE_CHECK_EQ(CURLE_OK, curl_easy_setopt(handle, opt, val))

}// namespace

std::expected<std::string, int>
HttpPost(const std::string &uri,
         const std::vector<std::string> &headers,
         const std::string &body,
         std::chrono::nanoseconds timeout)
{
    if (s_http_post_mock_handler) { return s_http_post_mock_handler(uri, headers, body, timeout); }

    std::string result;
    curl_slist *hdrs = curl_slist_append(nullptr, "Expect:");
    ScopedDeferred _{[&] { curl_slist_free_all(hdrs); }};
    for (const auto &header : headers) { hdrs = curl_slist_append(hdrs, header.c_str()); }

    std::unique_ptr<CURL, decltype(&curl_easy_cleanup)> curl{curl_easy_init(), &curl_easy_cleanup};

    CURL_SETOPT(curl.get(), CURLOPT_URL, uri.c_str());
    CURL_SETOPT(curl.get(), CURLOPT_POSTFIELDS, body.c_str());
    CURL_SETOPT(curl.get(), CURLOPT_POSTFIELDSIZE_LARGE, body.size());
    CURL_SETOPT(curl.get(), CURLOPT_HTTPHEADER, hdrs);
    CURL_SETOPT(curl.get(), CURLOPT_WRITEFUNCTION, HttpWriteCallback);
    CURL_SETOPT(curl.get(), CURLOPT_WRITEDATA, &result);
    CURL_SETOPT(curl.get(), CURLOPT_NOSIGNAL, 1L);
    CURL_SETOPT(curl.get(), CURLOPT_TIMEOUT_MS, timeout / std::chrono::milliseconds(1));
    CURL_SETOPT(curl.get(), CURLOPT_SHARE, GetCurlShareHandle());
    // 10 min
    CURL_SETOPT(curl.get(), CURLOPT_DNS_CACHE_TIMEOUT, std::chrono::minutes(10) / std::chrono::seconds(1));

    auto rc = curl_easy_perform(curl.get());
    if (rc != CURLE_OK) {
        TILE_LOG_WARNING_EVERY_SECOND("Failed to call [{}]: [#{}] {}", uri, (int) rc, curl_easy_strerror(rc));
        return std::make_unexpected(-rc);
    }

    long resp_code;
    curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &resp_code);
    if (resp_code != 200) { return std::make_unexpected(resp_code); }
    return result;
}

std::expected<std::string, int>
HttpGet(const std::string &uri, std::chrono::nanoseconds timeout)
{
    if (s_http_get_mock_handler) { return s_http_get_mock_handler(uri, timeout); }

    std::string result;
    std::unique_ptr<CURL, decltype(&curl_easy_cleanup)> curl{curl_easy_init(), &curl_easy_cleanup};
    CURL_SETOPT(curl.get(), CURLOPT_URL, uri.c_str());
    CURL_SETOPT(curl.get(), CURLOPT_WRITEFUNCTION, HttpWriteCallback);
    CURL_SETOPT(curl.get(), CURLOPT_WRITEDATA, &result);
    CURL_SETOPT(curl.get(), CURLOPT_NOSIGNAL, 1);
    CURL_SETOPT(curl.get(), CURLOPT_TIMEOUT_MS, timeout / std::chrono::milliseconds(1));
    auto rc = curl_easy_perform(curl.get());
    if (rc != CURLE_OK) {
        TILE_LOG_WARNING_EVERY_SECOND("Failed to call [{}]: [#{}] {}", uri, (int) rc, curl_easy_strerror(rc));
        return std::make_unexpected(-rc);
    }
    long resp_code;// NOLINT: `CURLINFO_RESPONSE_CODE` needs a `long`..
    curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &resp_code);
    if (resp_code != 200) { return std::make_unexpected(resp_code); }
    return result;
}

void
SetHttpPostMockHandler(HttpPostMockHandler &&http_post_mock_handler)
{
    s_http_post_mock_handler = std::move(http_post_mock_handler);
}

void
SetHttpGetMockHandler(HttpGetMockHandler &&http_get_mock_handler)
{
    s_http_get_mock_handler = std::move(http_get_mock_handler);
}

}// namespace internal
}// namespace tile
