#pragma once

#include <atomic>
#include <boost/asio/deadline_timer.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/signal_set.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <chrono>
#include <cstdint>
#include <future>
#include <memory>
#include <utility>
#include <vector>

#include "crow/http_connection.h"
#include "crow/logging.h"
#include "crow/timer_queue.h"
#ifdef BMCWEB_ENABLE_SSL
#include <boost/asio/ssl/context.hpp>
#endif

namespace crow
{
using namespace boost;
using tcp = asio::ip::tcp;

template <typename Handler, typename Adaptor = SocketAdaptor,
          typename... Middlewares>
class Server
{
  public:
    Server(Handler* handler, std::unique_ptr<tcp::acceptor>&& acceptor,
           std::tuple<Middlewares...>* middlewares = nullptr,
           typename Adaptor::context* adaptor_ctx = nullptr,
           std::shared_ptr<boost::asio::io_context> io =
               std::make_shared<boost::asio::io_context>()) :
        ioService(std::move(io)),
        acceptor(std::move(acceptor)), signals(*ioService, SIGINT, SIGTERM),
        tickTimer(*ioService), handler(handler), middlewares(middlewares),
        adaptorCtx(adaptor_ctx)
    {
    }

    Server(Handler* handler, const std::string& bindaddr, uint16_t port,
           std::tuple<Middlewares...>* middlewares = nullptr,
           typename Adaptor::context* adaptor_ctx = nullptr,
           std::shared_ptr<boost::asio::io_context> io =
               std::make_shared<boost::asio::io_context>()) :
        Server(handler,
               std::make_unique<tcp::acceptor>(
                   *io,
                   tcp::endpoint(
                       boost::asio::ip::address::from_string(bindaddr), port)),
               middlewares, adaptor_ctx, io)
    {
    }

    Server(Handler* handler, int existing_socket,
           std::tuple<Middlewares...>* middlewares = nullptr,
           typename Adaptor::context* adaptor_ctx = nullptr,
           std::shared_ptr<boost::asio::io_context> io =
               std::make_shared<boost::asio::io_context>()) :
        Server(handler,
               std::make_unique<tcp::acceptor>(*io, boost::asio::ip::tcp::v6(),
                                               existing_socket),
               middlewares, adaptor_ctx, io)
    {
    }

    void setTickFunction(std::chrono::milliseconds d, std::function<void()> f)
    {
        tickInterval = d;
        tickFunction = f;
    }

    void onTick()
    {
        tickFunction();
        tickTimer.expires_from_now(
            boost::posix_time::milliseconds(tickInterval.count()));
        tickTimer.async_wait([this](const boost::system::error_code& ec) {
            if (ec)
            {
                return;
            }
            onTick();
        });
    }

    void updateDateStr()
    {
        auto lastTimeT = time(0);
        tm myTm{};

#ifdef _MSC_VER
        gmtime_s(&my_tm, &last_time_t);
#else
        gmtime_r(&lastTimeT, &myTm);
#endif
        dateStr.resize(100);
        size_t dateStrSz =
            strftime(&dateStr[0], 99, "%a, %d %b %Y %H:%M:%S GMT", &myTm);
        dateStr.resize(dateStrSz);
    };

    void run()
    {
        updateDateStr();

        getCachedDateStr = [this]() -> std::string {
            static std::chrono::time_point<std::chrono::steady_clock>
                lastDateUpdate = std::chrono::steady_clock::now();
            if (std::chrono::steady_clock::now() - lastDateUpdate >=
                std::chrono::seconds(10))
            {
                lastDateUpdate = std::chrono::steady_clock::now();
                updateDateStr();
            }
            return this->dateStr;
        };

        boost::asio::deadline_timer timer(*ioService);
        timer.expires_from_now(boost::posix_time::seconds(1));

        std::function<void(const boost::system::error_code& ec)> handler;
        handler = [&](const boost::system::error_code& ec) {
            if (ec)
            {
                return;
            }
            timerQueue.process();
            timer.expires_from_now(boost::posix_time::seconds(1));
            timer.async_wait(handler);
        };
        timer.async_wait(handler);

        if (tickFunction && tickInterval.count() > 0)
        {
            tickTimer.expires_from_now(
                boost::posix_time::milliseconds(tickInterval.count()));
            tickTimer.async_wait([this](const boost::system::error_code& ec) {
                if (ec)
                {
                    return;
                }
                onTick();
            });
        }

        BMCWEB_LOG_INFO << serverName << " server is running, local endpoint "
                        << acceptor->local_endpoint();

        signals.async_wait([&](const boost::system::error_code& /*error*/,
                               int /*signal_number*/) { stop(); });

        doAccept();
    }

    void stop()
    {
        ioService->stop();
    }

    void doAccept()
    {
        auto p = new Connection<Adaptor, Handler, Middlewares...>(
            *ioService, handler, serverName, middlewares, getCachedDateStr,
            timerQueue, adaptorCtx);
        acceptor->async_accept(
            p->socket(), [this, p](boost::system::error_code ec) {
                if (!ec)
                {
                    this->ioService->post([p] { p->start(); });
                }
                else
                {
                    delete p;
                }
                doAccept();
            });
    }

  private:
    std::shared_ptr<asio::io_context> ioService;
    detail::TimerQueue timerQueue;
    std::function<std::string()> getCachedDateStr;
    std::unique_ptr<tcp::acceptor> acceptor;
    boost::asio::signal_set signals;
    boost::asio::deadline_timer tickTimer;

    std::string dateStr;

    Handler* handler;
    std::string serverName = "iBMC";

    std::chrono::milliseconds tickInterval{};
    std::function<void()> tickFunction;

    std::tuple<Middlewares...>* middlewares;

#ifdef BMCWEB_ENABLE_SSL
    bool useSsl{false};
    boost::asio::ssl::context sslContext{boost::asio::ssl::context::sslv23};
#endif
    typename Adaptor::context* adaptorCtx;
}; // namespace crow
} // namespace crow
