/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
#pragma once

#include <assert.h>
#include <curl/curl.h>

#include <mutex>
#include <string>

namespace pulsar {

class CurlWrapper {
   public:
    CurlWrapper() noexcept {}
    ~CurlWrapper() {
        if (handle_) {
            curl_easy_cleanup(handle_);
        }
    }

    char* escape(const std::string& s) const {
        assert(handle_);
        return curl_easy_escape(handle_, s.c_str(), s.length());
    }

    // It must be called before calling other methods
    bool init() {
        static std::once_flag initFlag;
        std::call_once(initFlag, [] { curl_global_init(CURL_GLOBAL_ALL); });
        handle_ = curl_easy_init();
        return handle_ != nullptr;
    }

    struct Options {
        std::string method;
        std::string postFields;
        std::string userAgent;
        int timeoutInSeconds{0};
        int maxLookupRedirects{-1};
    };

    struct TlsContext {
        std::string trustCertsFilePath;
        bool validateHostname{true};
        bool allowInsecure{false};
        std::string certPath;
        std::string keyPath;
    };

    struct Result {
        CURLcode code;
        std::string responseData;
        long responseCode;
        std::string redirectUrl;
        std::string error;
        std::string serverError;
    };

    Result get(const std::string& url, const std::string& header, const Options& options,
               const TlsContext* tlsContext) const;

   private:
    CURL* handle_;

    struct CurlListGuard {
        curl_slist*& headers;

        CurlListGuard(curl_slist*& headers) : headers(headers) {}
        ~CurlListGuard() {
            if (headers) {
                curl_slist_free_all(headers);
            }
        }
    };
};

inline CurlWrapper::Result CurlWrapper::get(const std::string& url, const std::string& header,
                                            const Options& options, const TlsContext* tlsContext) const {
    assert(handle_);
    curl_easy_setopt(handle_, CURLOPT_URL, url.c_str());

    if (!options.postFields.empty()) {
        curl_easy_setopt(handle_, CURLOPT_CUSTOMREQUEST, "POST");
        curl_easy_setopt(handle_, CURLOPT_POSTFIELDS, options.postFields.c_str());
    }

    // Write response
    curl_easy_setopt(
        handle_, CURLOPT_WRITEFUNCTION,
        +[](char* buffer, size_t size, size_t nitems, void* outstream) -> size_t {
            static_cast<std::string*>(outstream)->append(buffer, size * nitems);
            return size * nitems;
        });
    std::string response;
    curl_easy_setopt(handle_, CURLOPT_WRITEDATA, &response);

    // New connection is made for each call
    curl_easy_setopt(handle_, CURLOPT_FRESH_CONNECT, 1L);
    curl_easy_setopt(handle_, CURLOPT_FORBID_REUSE, 1L);

    // Skipping signal handling - results in timeouts not honored during the DNS lookup
    // Without this config, Curl_resolv_timeout might crash in multi-threads environment
    curl_easy_setopt(handle_, CURLOPT_NOSIGNAL, 1L);

    curl_easy_setopt(handle_, CURLOPT_TIMEOUT, options.timeoutInSeconds);
    if (!options.userAgent.empty()) {
        curl_easy_setopt(handle_, CURLOPT_USERAGENT, options.userAgent.c_str());
    }
    curl_easy_setopt(handle_, CURLOPT_FAILONERROR, 1L);

    // Redirects
    curl_easy_setopt(handle_, CURLOPT_FOLLOWLOCATION, 1L);
    curl_easy_setopt(handle_, CURLOPT_MAXREDIRS, options.maxLookupRedirects);

    char errorBuffer[CURL_ERROR_SIZE] = "";
    curl_easy_setopt(handle_, CURLOPT_ERRORBUFFER, errorBuffer);

    curl_slist* headers = nullptr;
    CurlListGuard headersGuard{headers};
    if (!header.empty()) {
        headers = curl_slist_append(headers, header.c_str());
        curl_easy_setopt(handle_, CURLOPT_HTTPHEADER, headers);
    }

    if (tlsContext) {
        CURLcode code;
        code = curl_easy_setopt(handle_, CURLOPT_SSLENGINE, nullptr);
        if (code != CURLE_OK) {
            return {code, "", -1, "",
                    "Unable to load SSL engine for url " + url + ": " + curl_easy_strerror(code)};
        }
        code = curl_easy_setopt(handle_, CURLOPT_SSLENGINE_DEFAULT, 1L);
        if (code != CURLE_OK) {
            return {code, "", -1, "",
                    "Unable to load SSL engine as default for url " + url + ": " + curl_easy_strerror(code)};
        }
        curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYHOST, tlsContext->validateHostname ? 1L : 0L);
        curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYPEER, tlsContext->allowInsecure ? 0L : 1L);
        if (!tlsContext->trustCertsFilePath.empty()) {
            curl_easy_setopt(handle_, CURLOPT_CAINFO, tlsContext->trustCertsFilePath.c_str());
        }
        if (!tlsContext->certPath.empty() && !tlsContext->keyPath.empty()) {
            curl_easy_setopt(handle_, CURLOPT_SSLCERT, tlsContext->certPath.c_str());
            curl_easy_setopt(handle_, CURLOPT_SSLKEY, tlsContext->keyPath.c_str());
        }
    }

    auto res = curl_easy_perform(handle_);
    long responseCode;
    curl_easy_getinfo(handle_, CURLINFO_RESPONSE_CODE, &responseCode);
    Result result{res, response, responseCode, "", "", std::string(errorBuffer)};
    if (responseCode == 307 || responseCode == 302 || responseCode == 301) {
        char* url;
        curl_easy_getinfo(handle_, CURLINFO_REDIRECT_URL, &url);
        // `url` is null when the host of the redirect URL cannot be resolved
        if (url) {
            result.redirectUrl = url;
        }
    }
    return result;
}

}  // namespace pulsar
