/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
 *
 * Distributed under the Boost Software License, Version 1.0. (See
 * accompanying file LICENSE.txt)
 */

#ifndef BOOST_REDIS_RUNNER_HPP
#define BOOST_REDIS_RUNNER_HPP

#include <boost/redis/detail/health_checker.hpp>
#include <boost/redis/config.hpp>
#include <boost/redis/response.hpp>
#include <boost/redis/detail/helper.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/logger.hpp>
#include <boost/redis/operation.hpp>
#include <boost/redis/detail/connector.hpp>
#include <boost/redis/detail/resolver.hpp>
#include <boost/redis/detail/handshaker.hpp>
#include <boost/asio/compose.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/experimental/parallel_group.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/steady_timer.hpp>
#include <string>
#include <memory>
#include <chrono>

namespace boost::redis::detail
{

template <class Runner, class Connection, class Logger>
struct hello_op {
   Runner* runner_ = nullptr;
   Connection* conn_ = nullptr;
   Logger logger_;
   asio::coroutine coro_{};

   template <class Self>
   void operator()(Self& self, system::error_code ec = {}, std::size_t = 0)
   {
      BOOST_ASIO_CORO_REENTER (coro_)
      {
         runner_->hello_req_.clear();
         if (runner_->hello_resp_.has_value())
            runner_->hello_resp_.value().clear();
         runner_->add_hello();

         BOOST_ASIO_CORO_YIELD
         conn_->async_exec(runner_->hello_req_, runner_->hello_resp_, std::move(self));
         logger_.on_hello(ec, runner_->hello_resp_);

         if (ec || runner_->has_error_in_response() || is_cancelled(self)) {
            logger_.trace("hello-op: error/canceled. Exiting ...");
            conn_->cancel(operation::run);
            self.complete(!!ec ? ec : asio::error::operation_aborted);
            return;
         }

         self.complete({});
      }
   }
};

template <class Runner, class Connection, class Logger>
class runner_op {
private:
   Runner* runner_ = nullptr;
   Connection* conn_ = nullptr;
   Logger logger_;
   asio::coroutine coro_{};

public:
   runner_op(Runner* runner, Connection* conn, Logger l)
   : runner_{runner}
   , conn_{conn}
   , logger_{l}
   {}

   template <class Self>
   void operator()( Self& self
                  , std::array<std::size_t, 3> order = {}
                  , system::error_code ec0 = {}
                  , system::error_code ec1 = {}
                  , system::error_code ec2 = {}
                  , std::size_t = 0)
   {
      BOOST_ASIO_CORO_REENTER (coro_)
      {
         BOOST_ASIO_CORO_YIELD
         asio::experimental::make_parallel_group(
            [this](auto token) { return runner_->async_run_all(*conn_, logger_, token); },
            [this](auto token) { return runner_->health_checker_.async_check_health(*conn_, logger_, token); },
            [this](auto token) { return runner_->async_hello(*conn_, logger_, token); }
         ).async_wait(
            asio::experimental::wait_for_all(),
            std::move(self));

         logger_.on_runner(ec0, ec1, ec2);

         if (is_cancelled(self)) {
            self.complete(asio::error::operation_aborted);
            return;
         }

         if (ec0 == error::connect_timeout || ec0 == error::resolve_timeout) {
            self.complete(ec0);
            return;
         }

         if (order[0] == 2 && !!ec2) {
            self.complete(ec2);
            return;
         }

         if (order[0] == 1 && ec1 == error::pong_timeout) {
            self.complete(ec1);
            return;
         }

         self.complete(ec0);
      }
   }
};

template <class Runner, class Connection, class Logger>
struct run_all_op {
   Runner* runner_ = nullptr;
   Connection* conn_ = nullptr;
   Logger logger_;
   asio::coroutine coro_{};

   template <class Self>
   void operator()(Self& self, system::error_code ec = {}, std::size_t = 0)
   {
      BOOST_ASIO_CORO_REENTER (coro_)
      {
         BOOST_ASIO_CORO_YIELD
         runner_->resv_.async_resolve(std::move(self));
         logger_.on_resolve(ec, runner_->resv_.results());
         BOOST_REDIS_CHECK_OP0(conn_->cancel(operation::run);)

         BOOST_ASIO_CORO_YIELD
         runner_->ctor_.async_connect(conn_->next_layer().next_layer(), runner_->resv_.results(), std::move(self));
         logger_.on_connect(ec, runner_->ctor_.endpoint());
         BOOST_REDIS_CHECK_OP0(conn_->cancel(operation::run);)

         if (conn_->use_ssl()) {
            BOOST_ASIO_CORO_YIELD
            runner_->hsher_.async_handshake(conn_->next_layer(), std::move(self));
            logger_.on_ssl_handshake(ec);
            BOOST_REDIS_CHECK_OP0(conn_->cancel(operation::run);)
         }

         BOOST_ASIO_CORO_YIELD
         conn_->async_run_lean(runner_->cfg_, logger_, std::move(self));
         BOOST_REDIS_CHECK_OP0(;)
         self.complete(ec);
      }
   }
};

template <class Executor>
class runner {
public:
   runner(Executor ex, config cfg)
   : resv_{ex}
   , ctor_{ex}
   , hsher_{ex}
   , health_checker_{ex}
   , cfg_{cfg}
   { }

   std::size_t cancel(operation op)
   {
      resv_.cancel(op);
      ctor_.cancel(op);
      hsher_.cancel(op);
      health_checker_.cancel(op);
      return 0U;
   }

   void set_config(config const& cfg)
   {
      cfg_ = cfg;
      resv_.set_config(cfg);
      ctor_.set_config(cfg);
      hsher_.set_config(cfg);
      health_checker_.set_config(cfg);
   }

   template <class Connection, class Logger, class CompletionToken>
   auto async_run(Connection& conn, Logger l, CompletionToken token)
   {
      return asio::async_compose
         < CompletionToken
         , void(system::error_code)
         >(runner_op<runner, Connection, Logger>{this, &conn, l}, token, conn);
   }

   config const& get_config() const noexcept {return cfg_;}

private:
   using resolver_type = resolver<Executor>;
   using connector_type = connector<Executor>;
   using handshaker_type = detail::handshaker<Executor>;
   using health_checker_type = health_checker<Executor>;
   using timer_type = typename connector_type::timer_type;

   template <class, class, class> friend struct run_all_op;
   template <class, class, class> friend class runner_op;
   template <class, class, class> friend struct hello_op;

   template <class Connection, class Logger, class CompletionToken>
   auto async_run_all(Connection& conn, Logger l, CompletionToken token)
   {
      return asio::async_compose
         < CompletionToken
         , void(system::error_code)
         >(run_all_op<runner, Connection, Logger>{this, &conn, l}, token, conn);
   }

   template <class Connection, class Logger, class CompletionToken>
   auto async_hello(Connection& conn, Logger l, CompletionToken token)
   {
      return asio::async_compose
         < CompletionToken
         , void(system::error_code)
         >(hello_op<runner, Connection, Logger>{this, &conn, l}, token, conn);
   }

   void add_hello()
   {
      if (!cfg_.username.empty() && !cfg_.password.empty() && !cfg_.clientname.empty())
         hello_req_.push("HELLO", "3", "AUTH", cfg_.username, cfg_.password, "SETNAME", cfg_.clientname);
      else if (cfg_.username.empty() && cfg_.password.empty() && cfg_.clientname.empty())
         hello_req_.push("HELLO", "3");
      else if (cfg_.clientname.empty())
         hello_req_.push("HELLO", "3", "AUTH", cfg_.username, cfg_.password);
      else
         hello_req_.push("HELLO", "3", "SETNAME", cfg_.clientname);

      if (cfg_.database_index && cfg_.database_index.value() != 0)
         hello_req_.push("SELECT", cfg_.database_index.value());
   }

   bool has_error_in_response() const noexcept
   {
      if (!hello_resp_.has_value())
         return true;

      auto f = [](auto const& e)
      {
         switch (e.data_type) {
            case resp3::type::simple_error:
            case resp3::type::blob_error: return true;
            default: return false;
         }
      };

      return std::any_of(std::cbegin(hello_resp_.value()), std::cend(hello_resp_.value()), f);
   }

   resolver_type resv_;
   connector_type ctor_;
   handshaker_type hsher_;
   health_checker_type health_checker_;
   request hello_req_;
   generic_response hello_resp_;
   config cfg_;
};

} // boost::redis::detail

#endif // BOOST_REDIS_RUNNER_HPP
