#ifndef __VEREIGN_RESTAPI_CLIENT_HH
#define __VEREIGN_RESTAPI_CLIENT_HH

#include <chrono>
#include <type_traits>
#include <vereign/restapi/detail/post_task.hh>
#include <vereign/restapi/http_header.hh>

#include <boost/asio/io_context.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/http.hpp>
#include <google/protobuf/message.h>

#include <string>
#include <future>
#include <deque>

namespace vereign {
namespace restapi {

namespace beast = boost::beast;
namespace asio = boost::asio;

using tcp = asio::ip::tcp;
using RequestPtr = std::unique_ptr<google::protobuf::Message>;

namespace detail {

class HttpReader;

} // namespace detail

/**
 * Client is a http client for the Vereign Restful API.
 *
 * Internally the Client uses the [boost::beast][] library.
 *
 * The client provides both async and blocking APIs for making POST requests.
 *
 * The blocking methods return futures, and must not be called inside the
 * async callbacks.Their purpose is to be used in threads outside of the
 * Client's executor.
 *
 * The POST requests are queued and executed sequentially reusing single
 * connection, reconnecting if needed.
 *
 * The connection is kept alive until there are requests, or when idle until
 * specified timeout is expired.
 *
 * [boost::beast]: \boostlib/libs/beast/doc/html/index.html
 *
 * ### Thread Safety
 *
 * All public methods are thread safe.
 */
class Client {
public:
  /**
   * Constructs Client instance with default timeout (30s).
   *
   * @param ioc IO context.
   * @param ssl_ctx SSL context.
   * @param host Vereign restapi host.
   * @param port Vereign restapi port. For example "https" or "443".
   * @returns Client instance.
   */
  Client(
    asio::io_context& ioc,
    asio::ssl::context& ssl_ctx,
    const std::string& host,
    const std::string& port
  );

  /**
   * Constructs Client instance.
   *
   * @param ioc IO context.
   * @param ssl_ctx SSL context.
   * @param host Vereign restapi host.
   * @param port Vereign restapi port. For example "https" or "443".
   * @param expiry_time Connect, read, write expiration time.
   * @returns Client instance.
   */
  Client(
    asio::io_context& ioc,
    asio::ssl::context& ssl_ctx,
    const std::string& host,
    const std::string& port,
    std::chrono::nanoseconds expiry_time
  );

  /**
   * Closes the connection.
   *
   * @see Client::Close
   */
  ~Client() noexcept;

  // Disable copying
  Client(const Client&) = delete;
  Client& operator=(const Client&) = delete;

  /**
   * Close closes the connection.
   *
   * The connection to the server is closed, and the current pending request
   * is cancelled with an error. All other requests in the queue will be also
   * cancelled. Subsequent calls to Client::Post or Client::PostAsync methods
   * will return with error, i.e. the client won't execute any requests anymore.
   */
  void Close();

  /**
   * GetExecutor returns the client's strand executor.
   *
   * @returns the client's strand executor.
   */
  const asio::executor& GetExecutor() const;

  /**
   * Post makes a blocking post request.
   *
   * The passed `req` and `resp` parameters are moved in and returned back once,
   * the returned future is resolved.
   *
   * @param path HTTP path, for example `/api/identity/loginWithPreviouslyAddedDevice`.
   * @param req Request object that will be serialized as JSON body.
   *    It can be a const pointer or std::unique_ptr to protobuf message.
   *
   * @param resp Response object that will be used to decode the response.
   *    It can be a  pointer or std::unique_ptr to protobuf message.
   *
   * @returns future that will be resolved with a result containing both
   *    the request and response objects originally passed to the `Post` call.
   */
  template <class RequestPtrType, class ResponsePtrType>
  auto Post(
    const std::string& path,
    RequestPtrType&& req,
    ResponsePtrType&& resp
  ) {
    using RequestPtr = typename std::remove_reference<RequestPtrType>::type;
    using ResponsePtr = typename std::remove_reference<ResponsePtrType>::type;
    using Result = PostResult<RequestPtr, ResponsePtr>;
    std::promise<Result> promise{};
    auto future = promise.get_future();

    PostAsync(
      path,
      std::move(req),
      std::move(resp),
      [promise = std::move(promise)] (Result&& result) mutable {
        promise.set_value(std::move(result));
      }
    );

    return future;
  }

  /**
   * Post makes a blocking post request with additional http headers.
   *
   * The passed `req` and `resp` parameters are moved in and returned back once,
   * the returned future is resolved.
   *
   * @param path HTTP path, for example `/api/identity/loginWithPreviouslyAddedDevice`.
   * @param req Request object that will be serialized as JSON body.
   *    It can be a const pointer or std::unique_ptr to protobuf message.
   *
   * @param resp Response object that will be used to decode the response.
   *    It can be a  pointer or std::unique_ptr to protobuf message.
   *
   * @param headers HTTP headers to include in the request.
   *
   * @returns future that will be resolved with a result containing both
   *    the request and response objects originally passed to the `Post` call.
   */
  template <class RequestPtrType, class ResponsePtrType>
  auto Post(
    const std::string& path,
    RequestPtrType&& req,
    ResponsePtrType&& resp,
    std::vector<HttpHeader>&& headers
  ) {
    using RequestPtr = typename std::remove_reference<RequestPtrType>::type;
    using ResponsePtr = typename std::remove_reference<ResponsePtrType>::type;
    using Result = PostResult<RequestPtr, ResponsePtr>;

    std::promise<Result> promise{};
    auto future = promise.get_future();

    PostAsync(
      path,
      std::move(req),
      std::move(resp),
      std::move(headers),
      [promise = std::move(promise)] (Result&& result) mutable {
        promise.set_value(std::move(result));
      }
    );

    return future;
  }

  /**
   * PostAsync makes non blocking post request.
   *
   * The passed `req` and `resp` parameters are moved in and returned back once
   * the CompletionFunc is called.
   *
   * @tparam CompletionFunc A completion functor - `void (Result&&)`, where
   *    the `Result` is restapi::PostResult <RequestPtr, ResponsePtr>, where
   *    RequestPtr is `std::remove_reference<RequestPtrType>::type` and
   *    ResponsePtr is `std::remove_reference<RequestPtrType>::type`.
   *
   * @param path HTTP path, for example `/api/identity/loginWithPreviouslyAddedDevice`.
   * @param req Request object that will be serialized as JSON body.
   *    It can be a const pointer or std::unique_ptr to protobuf message.
   *
   * @param resp Response object that will be used to decode the response.
   *    It can be a  pointer or std::unique_ptr to protobuf message.
   *
   * @param func Completion func, that will be called when the post request
   *    is finished.
   */
  template <class RequestPtrType, class ResponsePtrType, class CompletionFunc>
  void PostAsync(
    const std::string& path,
    RequestPtrType&& req,
    ResponsePtrType&& resp,
    CompletionFunc&& func
  ) {
    using RequestPtr = typename std::remove_reference<RequestPtrType>::type;
    using ResponsePtr = typename std::remove_reference<ResponsePtrType>::type;

    addPostTask(
      std::make_unique<detail::PostTask<RequestPtr, ResponsePtr, CompletionFunc>>(
        path,
        std::move(req),
        std::move(resp),
        std::move(func)
      )
    );
  }

  /**
   * PostAsync makes non blocking post request with additional http headers.
   *
   * The passed `req` and `resp` parameters are moved in and returned back once
   * the CompletionFunc is called.
   *
   * @tparam CompletionFunc A completion functor - `void (Result&&)`, where
   *    the `Result` is restapi::PostResult <RequestPtr, ResponsePtr>, where
   *    RequestPtr is `std::remove_reference<RequestPtrType>::type` and
   *    ResponsePtr is `std::remove_reference<RequestPtrType>::type`.
   *
   * @param path HTTP path, for example `/api/identity/loginWithPreviouslyAddedDevice`.
   * @param req Request object that will be serialized as JSON body.
   *    It can be a const pointer or std::unique_ptr to protobuf message.
   *
   * @param resp Response object that will be used to decode the response.
   *    It can be a  pointer or std::unique_ptr to protobuf message.
   *
   * @param headers HTTP headers to include in the request.
   *
   * @param func Completion func, that will be called when the post request
   *    is finished.
   */
  template <class RequestPtrType, class ResponsePtrType, class CompletionFunc>
  void PostAsync(
    const std::string& path,
    RequestPtrType&& req,
    ResponsePtrType&& resp,
    std::vector<HttpHeader>&& headers,
    CompletionFunc&& func
  ) {
    using RequestPtr = typename std::remove_reference<RequestPtrType>::type;
    using ResponsePtr = typename std::remove_reference<ResponsePtrType>::type;

    addPostTask(
      std::make_unique<detail::PostTask<RequestPtr, ResponsePtr, CompletionFunc>>(
        path,
        std::move(req),
        std::move(resp),
        std::move(headers),
        std::move(func)
      )
    );
  }

private:
  using streamType = beast::ssl_stream<beast::tcp_stream>;

  void handleError(const boost::optional<std::string>& err);
  void addPostTask(detail::PostTaskBasePtr&& task);

  void completeTask(
    const boost::optional<detail::HttpResponse>& resp,
    const boost::optional<std::string>& err
  );

  void doPost();
  void resolve();
  void connect(tcp::resolver::results_type results);
  void handshake();
  void readResponse();
  void resetStream();
  void cancelAllTasks(const detail::PostError& err);

private:
  // a strand that calls all completion handlers in single thread.
  asio::executor executor_;

  // ssl context used for the https connection.
  asio::ssl::context& ssl_ctx_;

  // all post requests are added as tasks in this queue.
  std::deque<detail::PostTaskBasePtr> task_queue_;

  // ssl stream used for the http requests.
  std::shared_ptr<streamType> stream_;

  // used for resolving the host name into ip address.
  tcp::resolver resolver_;

  // request object used for the async_write calls of the post requests.
  boost::optional<detail::HttpRequest> req_;

  // reads the requests responses.
  std::shared_ptr<detail::HttpReader> reader_;

  // http server host name.
  std::string host_;

  // http server port - https, 443 ...
  std::string port_;

  // connecting, read, write expiration time.
  std::chrono::nanoseconds expiry_time_;

  // indicates that the client is currently in connecting state.
  bool connecting_;

  // indicates that the client is currently writing a post request.
  bool writing_;

  // indicates that the client is currently waiting for response to be read.
  bool reading_;

  // indicates that the client is closed.
  bool closed_;
};

} // namespace restapi
} // namespace vereign

#endif // __VEREIGN_RESTAPI_CLIENT_HH