/**
 *
 *  @file HttpClient.h
 *
 *  @author An Tao
 *
 *  Copyright 2018, An Tao.  All rights reserved.
 *  https://github.com/an-tao/drogon
 *  Use of this source code is governed by the MIT license
 *  that can be found in the License file.
 *
 *  Drogon
 *
 */
#pragma once

#include <drogon/exports.h>
#include <drogon/HttpTypes.h>
#include <drogon/drogon_callbacks.h>
#include <drogon/HttpResponse.h>
#include <drogon/HttpRequest.h>
#include <trantor/utils/NonCopyable.h>
#include <trantor/net/EventLoop.h>
#include <functional>
#include <memory>
#include <future>
#include "drogon/HttpBinder.h"

#ifdef __cpp_impl_coroutine
#include <drogon/utils/coroutine.h>
#endif

namespace drogon
{
class HttpClient;
using HttpClientPtr = std::shared_ptr<HttpClient>;
#ifdef __cpp_impl_coroutine
namespace internal
{
struct HttpRespAwaiter : public CallbackAwaiter<HttpResponsePtr>
{
    HttpRespAwaiter(HttpClient *client, HttpRequestPtr req, double timeout)
        : client_(client), req_(std::move(req)), timeout_(timeout)
    {
    }

    void await_suspend(std::coroutine_handle<> handle);

  private:
    HttpClient *client_;
    HttpRequestPtr req_;
    double timeout_;
};

}  // namespace internal
#endif

/// Asynchronous http client
/**
 * HttpClient implementation object uses the HttpAppFramework's event loop by
 * default, so you should call app().run() to make the client work.
 * Each HttpClient object establishes a persistent connection with the server.
 * If the connection is broken, the client attempts to reconnect
 * when calling the sendRequest method.
 *
 * Using the static mathod newHttpClient(...) to get shared_ptr of the object
 * implementing the class, the shared_ptr is retained in the framework until all
 * response callbacks are invoked without fear of accidental deconstruction.
 *
 */
class DROGON_EXPORT HttpClient : public trantor::NonCopyable
{
  public:
    /**
     * @brief Send a request asynchronously to the server
     *
     * @param req The request sent to the server.
     * @param callback The callback is called when the response is received from
     * the server.
     * @param timeout In seconds. If the response is not received within the
     * timeout, the callback is called with `ReqResult::Timeout` and an empty
     * response. The zero value by default disables the timeout.
     *
     * @note
     * The request object is altered(some headers are added to it) before it is
     * sent, so calling this method with a same request object in different
     * thread is dangerous.
     * Please be careful when using timeout on an non-idempotent request.
     */
    virtual void sendRequest(const HttpRequestPtr &req,
                             const HttpReqCallback &callback,
                             double timeout = 0) = 0;

    /**
     * @brief Send a request asynchronously to the server
     *
     * @param req The request sent to the server.
     * @param callback The callback is called when the response is received from
     * the server.
     * @param timeout In seconds. If the response is not received within
     * the timeout, the callback is called with `ReqResult::Timeout` and an
     * empty response. The zero value by default disables the timeout.
     *
     * @note
     * The request object is altered(some headers are added to it) before it is
     * sent, so calling this method with a same request object in different
     * thread is dangerous.
     * Please be careful when using timeout on an non-idempotent request.
     */
    virtual void sendRequest(const HttpRequestPtr &req,
                             HttpReqCallback &&callback,
                             double timeout = 0) = 0;

    /**
     * @brief Send a request synchronously to the server and return the
     * response.
     *
     * @param req
     * @param timeout In seconds. If the response is not received within the
     * timeout, the `ReqResult::Timeout` and an empty response is returned. The
     * zero value by default disables the timeout.
     *
     * @return std::pair<ReqResult, HttpResponsePtr>
     * @note Never call this function in the event loop thread of the
     * client (partially in the callback function of the asynchronous
     * sendRequest method), otherwise the thread will be blocked forever.
     * Please be careful when using timeout on an non-idempotent request.
     */
    std::pair<ReqResult, HttpResponsePtr> sendRequest(const HttpRequestPtr &req,
                                                      double timeout = 0)
    {
        assert(!getLoop()->isInLoopThread() &&
               "Deadlock detected! Calling a sync API from the same loop as "
               "the HTTP client processes on will deadlock the event loop");
        std::promise<std::pair<ReqResult, HttpResponsePtr>> prom;
        auto f = prom.get_future();
        sendRequest(
            req,
            [&prom](ReqResult r, const HttpResponsePtr &resp) {
                prom.set_value({r, resp});
            },
            timeout);
        return f.get();
    }

#ifdef __cpp_impl_coroutine
    /**
     * @brief Send a request via coroutines to the server and return an
     * awaiter what could be `co_await`-ed to retrieve the response
     * (HttpResponsePtr)
     *
     * @param req
     * @param timeout In seconds. If the response is not received within the
     * timeout, A `drogon::HttpException` with `ReqResult::Timeout` is thrown.
     * The zero value by default disables the timeout.
     *
     * @return internal::HttpRespAwaiter. Await on it to get the response
     */
    internal::HttpRespAwaiter sendRequestCoro(HttpRequestPtr req,
                                              double timeout = 0)
    {
        return internal::HttpRespAwaiter(this, std::move(req), timeout);
    }
#endif

    /// Set socket options(before connecting)
    /**
     * @brief Set the callback which is called before connecting to the
     * server. The callback is used to set socket options on the socket fd.
     *
     * @code
       auto client = HttpClient::newHttpClient("http://www.baidu.com");
       client->setSockOptCallback([](int fd) {});
       auto req = HttpRequest::newHttpRequest();
       client->sendRequest(req, [](ReqResult result, const HttpResponsePtr&
       response) {});
       @endcode
     */
    virtual void setSockOptCallback(std::function<void(int)> cb) = 0;

    /// Set the pipelining depth, which is the number of requests that are not
    /// responding.
    /**
     * If this method is not called, the default depth value is 0 which means
     * the pipelining is disabled. For details about pipelining, see
     * rfc2616-8.1.2.2
     */
    virtual void setPipeliningDepth(size_t depth) = 0;

    /// Enable cookies for the client
    /**
     * @param flag if the parameter is true, all requests sent by the client
     * carry the cookies set by the server side. Cookies are disabled by
     * default.
     */
    virtual void enableCookies(bool flag = true) = 0;

    /// Add a cookie to the client
    /**
     * @note
     * These methods are independent of the enableCookies() method. Whether the
     * enableCookies() is called with true or false, the cookies added by these
     * methods will be sent to the server.
     */
    virtual void addCookie(const std::string &key,
                           const std::string &value) = 0;

    /// Add a cookie to the client
    /**
     * @note
     * These methods are independent of the enableCookies() method. Whether the
     * enableCookies() is called with true or false, the cookies added by these
     * methods will be sent to the server.
     */
    virtual void addCookie(const Cookie &cookie) = 0;

    /**
     * @brief Set the user_agent header, the default value is 'DrogonClient' if
     * this method is not used.
     *
     * @param userAgent The user_agent value, if it is empty, the user_agent
     * header is not sent to the server.
     */
    virtual void setUserAgent(const std::string &userAgent) = 0;

    /**
     * @brief Creaet a new HTTP client which use ip and port to connect to
     * server
     *
     * @param ip The ip address of the HTTP server
     * @param port The port of the HTTP server
     * @param useSSL if the parameter is set to true, the client connects to the
     * server using HTTPS.
     * @param loop If the loop parameter is set to nullptr, the client uses the
     * HttpAppFramework's event loop, otherwise it runs in the loop identified
     * by the parameter.
     * @param useOldTLS If the parameter is set to true, the TLS1.0/1.1 are
     * eanbled for HTTPS.
     * @param validateCert If the parameter is set to true, the client validates
     * the server certificate when SSL handshaking.
     * @return HttpClientPtr The smart pointer to the new client object.
     * @note: The ip parameter support for both ipv4 and ipv6 address
     */
    static HttpClientPtr newHttpClient(const std::string &ip,
                                       uint16_t port,
                                       bool useSSL = false,
                                       trantor::EventLoop *loop = nullptr,
                                       bool useOldTLS = false,
                                       bool validateCert = true);

    /// Get the event loop of the client;
    virtual trantor::EventLoop *getLoop() = 0;

    /// Get the number of bytes sent or received
    virtual size_t bytesSent() const = 0;
    virtual size_t bytesReceived() const = 0;

    virtual std::string host() const = 0;
    std::string getHost() const
    {
        return host();
    }

    virtual uint16_t port() const = 0;
    uint16_t getPort() const
    {
        return port();
    }

    virtual bool secure() const = 0;

    bool onDefaultPort() const
    {
        if (secure())
            return port() == 443;
        return port() == 80;
    }

    /**
     * @brief Set the client certificate used by the HTTP connection
     *
     * @param cert Path to the certificate
     * @param key Path to the certificate's private key
     * @note this method has no effect if the HTTP client is communicating via
     * unencrypted HTTP
     */
    virtual void setCertPath(const std::string &cert,
                             const std::string &key) = 0;

    /**
     * @brief Supplies command style options for `SSL_CONF_cmd`
     *
     * @param sslConfCmds options for SSL_CONF_cmd
     * @note this method has no effect if the HTTP client is communicating via
     * unencrypted HTTP
     * @code
       addSSLConfigs({{"-dhparam", "/path/to/dhparam"}, {"-strict", ""}});
     * @endcode
     */
    virtual void addSSLConfigs(
        const std::vector<std::pair<std::string, std::string>>
            &sslConfCmds) = 0;

    /// Create a Http client using the hostString to connect to server
    /**
     *
     * @param hostString this parameter must be prefixed by 'http://' or
     * 'https://'.
     *
     * Examples for hostString:
     * @code
       https://www.baidu.com
       http://www.baidu.com
       https://127.0.0.1:8080/
       http://127.0.0.1
       http://[::1]:8080/   //IPv6 address must be enclosed in [], rfc2732
       @endcode
     *
     * @param loop If the loop parameter is set to nullptr, the client uses the
     * HttpAppFramework's event loop, otherwise it runs in the loop identified
     * by the parameter.
     *
     * @param useOldTLS If the parameter is set to true, the TLS1.0/1.1 are
     * enabled for HTTPS.
     * @note
     *
     * @param validateCert If the parameter is set to true, the client validates
     * the server certificate when SSL handshaking.
     *
     * @note Don't add path and parameters in hostString, the request path and
     * parameters should be set in HttpRequestPtr when calling the sendRequest()
     * method.
     *
     */
    static HttpClientPtr newHttpClient(const std::string &hostString,
                                       trantor::EventLoop *loop = nullptr,
                                       bool useOldTLS = false,
                                       bool validateCert = true);

    virtual ~HttpClient()
    {
    }

  protected:
    HttpClient() = default;
};

#ifdef __cpp_impl_coroutine

class HttpException : public std::exception
{
  public:
    HttpException() = delete;
    explicit HttpException(ReqResult res)
        : resultCode_(res), message_(to_string_view(res))
    {
    }
    const char *what() const noexcept override
    {
        return message_.data();
    }
    ReqResult code() const
    {
        return resultCode_;
    }

  private:
    ReqResult resultCode_;
    std::string_view message_;
};

inline void internal::HttpRespAwaiter::await_suspend(
    std::coroutine_handle<> handle)
{
    assert(client_ != nullptr);
    assert(req_ != nullptr);
    client_->sendRequest(
        req_,
        [handle, this](ReqResult result, const HttpResponsePtr &resp) {
            if (result == ReqResult::Ok)
                setValue(resp);
            else
                setException(std::make_exception_ptr(HttpException(result)));
            handle.resume();
        },
        timeout_);
}
#endif

}  // namespace drogon
