#pragma once
#include <drogon/exports.h>
#include <drogon/nosql/RedisResult.h>
#include <drogon/nosql/RedisException.h>
#include <drogon/nosql/RedisSubscriber.h>
#include <string_view>
#include <trantor/net/InetAddress.h>
#include <trantor/utils/Logger.h>
#include <memory>
#include <functional>
#include <future>
#ifdef __cpp_impl_coroutine
#include <drogon/utils/coroutine.h>
#endif
namespace drogon
{
namespace nosql
{
#ifdef __cpp_impl_coroutine
class RedisClient;
class RedisTransaction;
namespace internal
{
struct [[nodiscard]] RedisAwaiter : public CallbackAwaiter<RedisResult>
{
    using RedisFunction =
        std::function<void(RedisResultCallback &&, RedisExceptionCallback &&)>;
    explicit RedisAwaiter(RedisFunction &&function)
        : function_(std::move(function))
    {
    }
    void await_suspend(std::coroutine_handle<> handle)
    {
        function_(
            [handle, this](const RedisResult &result) {
                this->setValue(result);
                handle.resume();
            },
            [handle, this](const RedisException &e) {
                LOG_ERROR << e.what();
                this->setException(std::make_exception_ptr(e));
                handle.resume();
            });
    }
  private:
    RedisFunction function_;
};
struct [[nodiscard]] RedisTransactionAwaiter
    : public CallbackAwaiter<std::shared_ptr<RedisTransaction>>
{
    RedisTransactionAwaiter(RedisClient *client) : client_(client)
    {
    }
    void await_suspend(std::coroutine_handle<> handle);
  private:
    RedisClient *client_;
};
}  
#endif
class RedisTransaction;
class DROGON_EXPORT RedisClient
{
  public:
    static std::shared_ptr<RedisClient> newRedisClient(
        const trantor::InetAddress &serverAddress,
        size_t numberOfConnections = 1,
        const std::string &password = "",
        unsigned int db = 0,
        const std::string &username = "");
    virtual void execCommandAsync(RedisResultCallback &&resultCallback,
                                  RedisExceptionCallback &&exceptionCallback,
                                  std::string_view command,
                                  ...) noexcept = 0;
    template <typename T, typename... Args>
    T execCommandSync(std::function<T(const RedisResult &)> &&processFunc,
                      std::string_view command,
                      Args &&...args)
    {
        return execCommandSync<std::decay_t<decltype(processFunc)>>(
            std::move(processFunc), command, std::forward<Args>(args)...);
    }
    template <typename F, typename... Args>
    std::invoke_result_t<F, const RedisResult &> execCommandSync(
        F &&processFunc,
        std::string_view command,
        Args &&...args)
    {
        using Ret = std::invoke_result_t<F, const RedisResult &>;
        std::promise<Ret> prom;
        execCommandAsync(
            [&processFunc, &prom](const RedisResult &result) {
                try
                {
                    prom.set_value(processFunc(result));
                }
                catch (...)
                {
                    prom.set_exception(std::current_exception());
                }
            },
            [&prom](const RedisException &err) {
                prom.set_exception(std::make_exception_ptr(err));
            },
            command,
            std::forward<Args>(args)...);
        return prom.get_future().get();
    }
    virtual std::shared_ptr<RedisSubscriber> newSubscriber() noexcept = 0;
    virtual std::shared_ptr<RedisTransaction> newTransaction() noexcept(
        false) = 0;
    virtual void newTransactionAsync(
        const std::function<void(const std::shared_ptr<RedisTransaction> &)>
            &callback) = 0;
    virtual void setTimeout(double timeout) = 0;
    virtual ~RedisClient() = default;
    virtual void closeAll() = 0;
#ifdef __cpp_impl_coroutine
    template <typename... Arguments>
    internal::RedisAwaiter execCommandCoro(std::string_view command,
                                           Arguments... args)
    {
        return internal::RedisAwaiter(
            [command,
             this,
             args...](RedisResultCallback &&commandCallback,
                      RedisExceptionCallback &&exceptionCallback) {
                execCommandAsync(std::move(commandCallback),
                                 std::move(exceptionCallback),
                                 command,
                                 args...);
            });
    }
    internal::RedisTransactionAwaiter newTransactionCoro()
    {
        return internal::RedisTransactionAwaiter(this);
    }
#endif
};
class DROGON_EXPORT RedisTransaction : public RedisClient
{
  public:
    virtual void execute(RedisResultCallback &&resultCallback,
                         RedisExceptionCallback &&exceptionCallback) = 0;
#ifdef __cpp_impl_coroutine
    internal::RedisAwaiter executeCoro()
    {
        return internal::RedisAwaiter(
            [this](RedisResultCallback &&resultCallback,
                   RedisExceptionCallback &&exceptionCallback) {
                execute(std::move(resultCallback),
                        std::move(exceptionCallback));
            });
    }
#endif
    void closeAll() override
    {
    }
};
using RedisClientPtr = std::shared_ptr<RedisClient>;
using RedisTransactionPtr = std::shared_ptr<RedisTransaction>;
#ifdef __cpp_impl_coroutine
inline void internal::RedisTransactionAwaiter::await_suspend(
    std::coroutine_handle<> handle)
{
    assert(client_ != nullptr);
    client_->newTransactionAsync(
        [this, handle](const std::shared_ptr<RedisTransaction> &transaction) {
            if (transaction == nullptr)
                setException(std::make_exception_ptr(RedisException(
                    RedisErrorCode::kTimeout,
                    "Timeout, no connection available for transaction")));
            else
                setValue(transaction);
            handle.resume();
        });
}
#endif
}  
}  