#include <xmpp/biboumi_component.hpp>
#include <utils/timed_events.hpp>
#include <network/poller.hpp>
#include <config/config.hpp>
#include <logger/logger.hpp>
#include <utils/xdg.hpp>
#include <utils/reload.hpp>

#ifdef UDNS_FOUND
# include <network/dns_handler.hpp>
#endif

#include <atomic>
#include <csignal>

#include <identd/identd_server.hpp>

// A flag set by the SIGINT signal handler.
static std::atomic<bool> stop(false);
// Flag set by the SIGUSR1/2 signal handler.
static std::atomic<bool> reload(false);
// A flag indicating that we are wanting to exit the process. i.e: if this
// flag is set and all connections are closed, we can exit properly.
static bool exiting = false;

/**
 * Provide an helpful message to help the user write a minimal working
 * configuration file.
 */
int config_help(const std::string& missing_option)
{
  if (!missing_option.empty())
    log_error("Configuration error: empty value for option ", missing_option, ".");
  log_error("Please provide a configuration file filled like this:\n\n"
            "hostname=irc.example.com\npassword=S3CR3T");
  return 1;
}

int display_help()
{
  std::cout << "Usage: biboumi [-ht] [configuration_file]" << std::endl;
  return 0;
}

static void sigint_handler(int sig, siginfo_t*, void*)
{
  // In 2 seconds, repeat the same signal, to force the exit
  TimedEventsManager::instance().add_event(TimedEvent(std::chrono::steady_clock::now() + 2s,
                                    [sig]() { raise(sig); }));
  stop.store(true);
}

static void sigusr_handler(int, siginfo_t*, void*)
{
  reload.store(true);
}

static void setup_signals()
{
  // Block the signals we want to manage. They will be unblocked only during
  // the epoll_pwait or ppoll calls. This avoids some race conditions,
  // explained in man 2 pselect on linux
  sigset_t mask{};
  sigemptyset(&mask);
  sigaddset(&mask, SIGINT);
  sigaddset(&mask, SIGTERM);
  sigaddset(&mask, SIGUSR1);
  sigaddset(&mask, SIGUSR2);
  sigaddset(&mask, SIGHUP);
  sigprocmask(SIG_BLOCK, &mask, nullptr);

  // Install the signals used to exit the process cleanly, or reload the
  // config
  struct sigaction on_sigint;
  on_sigint.sa_sigaction = &sigint_handler;
  // All signals must be blocked while a signal handler is running
  sigfillset(&on_sigint.sa_mask);
  // we want to catch that signal only once.
  // Sending SIGINT again will "force" an exit
  on_sigint.sa_flags = 0 & SA_RESETHAND;
  sigaction(SIGINT, &on_sigint, nullptr);
  sigaction(SIGTERM, &on_sigint, nullptr);

  // Install a signal to reload the config on SIGUSR1/2
  struct sigaction on_sigusr;
  on_sigusr.sa_sigaction = &sigusr_handler;
  sigfillset(&on_sigusr.sa_mask);
  on_sigusr.sa_flags = 0;
  sigaction(SIGUSR1, &on_sigusr, nullptr);
  sigaction(SIGUSR2, &on_sigusr, nullptr);
  sigaction(SIGHUP, &on_sigusr, nullptr);
}

static int main_loop(std::string hostname, std::string password)
{
  auto p = std::make_shared<Poller>();

#ifdef UDNS_FOUND
  DNSHandler dns_handler(p);
#endif

  auto xmpp_component =
    std::make_shared<BiboumiComponent>(p, hostname, password);
  xmpp_component->start();

  std::unique_ptr<IdentdServer> identd;
  if (Config::get_int("identd_port", 113) != 0)
    identd = std::make_unique<IdentdServer>(*xmpp_component, p, static_cast<uint16_t>(Config::get_int("identd_port", 113)));

  auto timeout = TimedEventsManager::instance().get_timeout();
  while (p->poll(timeout) != -1)
  {
    TimedEventsManager::instance().execute_expired_events();
    // Check for empty irc_clients (not connected, or with no joined
    // channel) and remove them
    xmpp_component->clean();
    if (identd)
      identd->clean();
    if (stop)
    {
      log_info("Signal received, exiting...");
#ifdef SYSTEMD_FOUND
      sd_notify(0, "STOPPING=1");
#endif
      exiting = true;
      stop.store(false);
      xmpp_component->shutdown();
#ifdef UDNS_FOUND
      dns_handler.destroy();
#endif
      if (identd)
        identd->shutdown();
      // Cancel the timer for a potential reconnection
      TimedEventsManager::instance().cancel("XMPP reconnection");
    }
    if (reload)
    {
      log_info("Signal received, reloading the config...");
      ::reload_process();
      reload.store(false);
    }
    // Reconnect to the XMPP server if this was not intended.  This may have
    // happened because we sent something invalid to it and it decided to
    // close the connection.  This is a bug that should be fixed, but we
    // still reconnect automatically instead of dropping everything
    if (!exiting &&
        !xmpp_component->is_connected() &&
        !xmpp_component->is_connecting())
    {
      if (xmpp_component->ever_auth)
        {
          static const std::string reconnect_name{"XMPP reconnection"};
          if (xmpp_component->first_connection_try == true)
            { // immediately re-try to connect
              xmpp_component->reset();
              xmpp_component->start();
            }
          else if (!TimedEventsManager::instance().find_event(reconnect_name))
            { // Re-connecting failed, we now try only each few seconds
              auto reconnect_later = [xmpp_component]()
              {
                xmpp_component->reset();
                xmpp_component->start();
              };
              TimedEvent event(std::chrono::steady_clock::now() + 2s, reconnect_later, reconnect_name);
              TimedEventsManager::instance().add_event(std::move(event));
            }
        }
      else
        {
#ifdef UDNS_FOUND
          dns_handler.destroy();
#endif
          if (identd)
            identd->shutdown();
        }
    }
    // If the only existing connection is the one to the XMPP component:
    // close the XMPP stream.
    if (exiting && xmpp_component->is_connecting())
      xmpp_component->close();
    if (exiting && p->size() == 1 && xmpp_component->is_document_open())
      xmpp_component->close_document();
    if (exiting) // If we are exiting, do not wait for any timed event
      timeout = utils::no_timeout;
    else
      timeout = TimedEventsManager::instance().get_timeout();
  }
  if (!xmpp_component->ever_auth)
    return 1; // To signal that the process did not properly start
  log_info("All connections cleanly closed, have a nice day.");
  return 0;
}

int main(int ac, char** av)
{
  std::string conf_filename{};
  bool test_conf = false;
  if (ac > 1)
    {
      for (int i = 1; i < ac; i++)
        {
          const std::string arg = av[i];
          if ((arg == "-h") || (arg == "--help"))
            return display_help();
          else if ((arg == "-t") || (arg == "--test-config"))
            test_conf = true;
          else if (i + 1 == ac)
            conf_filename = arg;
          else
            {
              std::cerr << "Unknow command line option: " << arg
                        << std::endl;
              return 1;
            }
        }
    }
  if (conf_filename.empty())
    conf_filename = xdg_config_path("biboumi.cfg");
  std::cout << "Using configuration file: " << conf_filename << std::endl;

  if (!Config::read_conf(conf_filename))
    return config_help("");

  const std::string password = Config::get("password", "");
  if (password.empty())
    return config_help("password");
  const std::string hostname = Config::get("hostname", "");
  if (hostname.empty())
    return config_help("hostname");
  if (test_conf)
    {
      std::cout << "biboumi: the configuration file " << conf_filename
        << " syntax is ok" << std::endl;
      return 0;
    }

#ifdef USE_DATABASE
  try
    {
      open_database();
    }
  catch (const std::exception& e)
    {
      log_error(e.what());
      return 1;
    }
#endif

  setup_signals();

  return main_loop(std::move(hostname), std::move(password));
}
