#include "snmp-agent.hh"
#include "misc.hh"
#include "threadname.hh"
#ifdef RECURSOR
#include "logger.hh"
#else
#include "dolog.hh"
#endif

#ifdef HAVE_NET_SNMP

#ifndef HAVE_SNMP_SELECT_INFO2
/* that's terrible, because it means we are going to have trouble with large
   FD numbers at some point.. */
# define netsnmp_large_fd_set fd_set
# define snmp_read2 snmp_read
# define snmp_select_info2 snmp_select_info
# define netsnmp_large_fd_set_init(...)
# define netsnmp_large_fd_set_cleanup(...)
# define NETSNMP_LARGE_FD_SET FD_SET
# define NETSNMP_LARGE_FD_CLR FD_CLR
# define NETSNMP_LARGE_FD_ZERO FD_ZERO
# define NETSNMP_LARGE_FD_ISSET FD_ISSET
#else
# include <net-snmp/library/large_fd_set.h>
#endif

const oid SNMPAgent::snmpTrapOID[] = { 1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0 };
const size_t SNMPAgent::snmpTrapOIDLen = OID_LENGTH(SNMPAgent::snmpTrapOID);

int SNMPAgent::setCounter64Value(netsnmp_request_info* request,
                                 uint64_t value)
{
  struct counter64 val64;
  val64.high = value >> 32;
  val64.low = value & 0xffffffff;
  snmp_set_var_typed_value(request->requestvb,
                           ASN_COUNTER64,
                           &val64,
                           sizeof(val64));
  return SNMP_ERR_NOERROR;
}

bool SNMPAgent::sendTrap(int fd,
                         netsnmp_variable_list* varList)
{
  ssize_t written = write(fd, &varList, sizeof(varList));

  if (written != sizeof(varList)) {
    snmp_free_varbind(varList);
    return false;
  }
  return true;
}

void SNMPAgent::handleTrapsEvent()
{
  netsnmp_variable_list* varList = nullptr;
  ssize_t got = 0;

  do {
    got = read(d_trapPipe[0], &varList, sizeof(varList));

    if (got == sizeof(varList)) {
      send_v2trap(varList);
      snmp_free_varbind(varList);
    }
  }
  while (got > 0);
}

void SNMPAgent::handleSNMPQueryEvent(int fd)
{
  netsnmp_large_fd_set fdset;
  netsnmp_large_fd_set_init(&fdset, FD_SETSIZE);
  NETSNMP_LARGE_FD_ZERO(&fdset);
  NETSNMP_LARGE_FD_SET(fd, &fdset);
  snmp_read2(&fdset);
}

void SNMPAgent::handleTrapsCB(int fd, FDMultiplexer::funcparam_t& var)
{
  SNMPAgent** agent = boost::any_cast<SNMPAgent*>(&var);
  if (!agent || !*agent)
    throw std::runtime_error("Invalid value received in SNMP trap callback");

  (*agent)->handleTrapsEvent();
}

void SNMPAgent::handleSNMPQueryCB(int fd, FDMultiplexer::funcparam_t& var)
{
  SNMPAgent** agent = boost::any_cast<SNMPAgent*>(&var);
  if (!agent || !*agent)
    throw std::runtime_error("Invalid value received in SNMP trap callback");

  (*agent)->handleSNMPQueryEvent(fd);
}

#endif /* HAVE_NET_SNMP */

void SNMPAgent::worker()
{
#ifdef HAVE_NET_SNMP
  FDMultiplexer* mplexer = FDMultiplexer::getMultiplexerSilent();
  if (mplexer == nullptr) {
    throw std::runtime_error("No FD multiplexer found for the SNMP agent!");
  }

#ifdef RECURSOR
  string threadName = "pdns-r/SNMP";
#else
  string threadName = "dnsdist/SNMP";
#endif
  setThreadName(threadName);

  int maxfd = 0;
  int block = 1;
  netsnmp_large_fd_set fdset;
  struct timeval timeout = { 0, 0 };
  struct timeval now;

  /* we want to be notified if a trap is waiting
   to be sent */
  mplexer->addReadFD(d_trapPipe[0], &handleTrapsCB, this);

  while(true) {
    netsnmp_large_fd_set_init(&fdset, FD_SETSIZE);
    NETSNMP_LARGE_FD_ZERO(&fdset);

    block = 1;
    timeout = { 0, 0 };
    snmp_select_info2(&maxfd, &fdset, &timeout, &block);

    for (int fd = 0; fd < maxfd; fd++) {
      if (NETSNMP_LARGE_FD_ISSET(fd, &fdset)) {
        mplexer->addReadFD(fd, &handleSNMPQueryCB, this);
      }
    }

    /* run updates now */
    int res = mplexer->run(&now, (timeout.tv_sec * 1000) + (timeout.tv_usec / 1000));

    /* we handle timeouts here, the rest has already been handled by callbacks */
    if (res == 0) {
      snmp_timeout();
      run_alarms();
    }

    for (int fd = 0; fd < maxfd; fd++) {
      if (NETSNMP_LARGE_FD_ISSET(fd, &fdset)) {
        try {
          mplexer->removeReadFD(fd);
        }
        catch(const FDMultiplexerException& e) {
          /* we might get an exception when removing a closed file descriptor,
             just ignore it */
        }
      }
    }
  }
#endif /* HAVE_NET_SNMP */
}

SNMPAgent::SNMPAgent(const std::string& name, const std::string& daemonSocket)
{
#ifdef HAVE_NET_SNMP
  netsnmp_enable_subagent();
  snmp_disable_log();
  if (!daemonSocket.empty()) {
    netsnmp_ds_set_string(NETSNMP_DS_APPLICATION_ID,
                          NETSNMP_DS_AGENT_X_SOCKET,
                          daemonSocket.c_str());
  }
  /* no need to load any MIBS,
     and it causes import errors if some modules are not present */
  setenv("MIBS", "", 1);

  init_agent(name.c_str());

  /* we use select() so don't use SIGALARM to handle alarms.
     Note that we need to handle alarms for automatic reconnection
     to the daemon to work.
  */
  netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID,
                         NETSNMP_DS_LIB_ALARM_DONT_USE_SIG,
                         1);

  init_snmp(name.c_str());

  if (pipe(d_trapPipe) < 0)
    unixDie("Creating pipe");

  if (!setNonBlocking(d_trapPipe[0])) {
    close(d_trapPipe[0]);
    close(d_trapPipe[1]);
    unixDie("Setting pipe non-blocking");
  }

  if (!setNonBlocking(d_trapPipe[1])) {
    close(d_trapPipe[0]);
    close(d_trapPipe[1]);
    unixDie("Setting pipe non-blocking");
  }

#endif /* HAVE_NET_SNMP */
}
