/**
 * collectd - src/teamspeak2.c
 * Copyright (C) 2008  Stefan Hacker
 * Copyright (C) 2008  Florian Forster
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; only version 2 of the License is applicable.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 *
 * Authors:
 *   Stefan Hacker <d0t at dbclan dot de>
 *   Florian Forster <octo at collectd.org>
 **/

#include "collectd.h"

#include "plugin.h"
#include "utils/common/common.h"

#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/types.h>

/*
 * Defines
 */
/* Default host and port */
#define DEFAULT_HOST "127.0.0.1"
#define DEFAULT_PORT "51234"

/*
 * Variables
 */
/* Server linked list structure */
typedef struct vserver_list_s {
  int port;
  struct vserver_list_s *next;
} vserver_list_t;
static vserver_list_t *server_list;

/* Host data */
static char *config_host;
static char *config_port;

static FILE *global_read_fh;
static FILE *global_write_fh;

/* Config data */
static const char *config_keys[] = {"Host", "Port", "Server"};
static int config_keys_num = STATIC_ARRAY_SIZE(config_keys);

/*
 * Functions
 */
static int tss2_add_vserver(int vserver_port) {
  /*
   * Adds a new vserver to the linked list
   */
  vserver_list_t *entry;

  /* Check port range */
  if ((vserver_port <= 0) || (vserver_port > 65535)) {
    ERROR("teamspeak2 plugin: VServer port is invalid: %i", vserver_port);
    return -1;
  }

  /* Allocate memory */
  entry = calloc(1, sizeof(*entry));
  if (entry == NULL) {
    ERROR("teamspeak2 plugin: calloc failed.");
    return -1;
  }

  /* Save data */
  entry->port = vserver_port;

  /* Insert to list */
  if (server_list == NULL) {
    /* Add the server as the first element */
    server_list = entry;
  } else {
    vserver_list_t *prev;

    /* Add the server to the end of the list */
    prev = server_list;
    while (prev->next != NULL)
      prev = prev->next;
    prev->next = entry;
  }

  INFO("teamspeak2 plugin: Registered new vserver: %i", vserver_port);

  return 0;
} /* int tss2_add_vserver */

static void tss2_submit_gauge(const char *plugin_instance, const char *type,
                              const char *type_instance, gauge_t value) {
  /*
   * Submits a gauge value to the collectd daemon
   */
  value_list_t vl = VALUE_LIST_INIT;

  vl.values = &(value_t){.gauge = value};
  vl.values_len = 1;
  sstrncpy(vl.plugin, "teamspeak2", sizeof(vl.plugin));

  if (plugin_instance != NULL)
    sstrncpy(vl.plugin_instance, plugin_instance, sizeof(vl.plugin_instance));

  sstrncpy(vl.type, type, sizeof(vl.type));

  if (type_instance != NULL)
    sstrncpy(vl.type_instance, type_instance, sizeof(vl.type_instance));

  plugin_dispatch_values(&vl);
} /* void tss2_submit_gauge */

static void tss2_submit_io(const char *plugin_instance, const char *type,
                           derive_t rx, derive_t tx) {
  /*
   * Submits the io rx/tx tuple to the collectd daemon
   */
  value_list_t vl = VALUE_LIST_INIT;
  value_t values[] = {
      {.derive = rx},
      {.derive = tx},
  };

  vl.values = values;
  vl.values_len = STATIC_ARRAY_SIZE(values);
  sstrncpy(vl.plugin, "teamspeak2", sizeof(vl.plugin));

  if (plugin_instance != NULL)
    sstrncpy(vl.plugin_instance, plugin_instance, sizeof(vl.plugin_instance));

  sstrncpy(vl.type, type, sizeof(vl.type));

  plugin_dispatch_values(&vl);
} /* void tss2_submit_gauge */

static void tss2_close_socket(void) {
  /*
   * Closes all sockets
   */
  if (global_write_fh != NULL) {
    fputs("quit\r\n", global_write_fh);
  }

  if (global_read_fh != NULL) {
    fclose(global_read_fh);
    global_read_fh = NULL;
  }

  if (global_write_fh != NULL) {
    fclose(global_write_fh);
    global_write_fh = NULL;
  }
} /* void tss2_close_socket */

static int tss2_get_socket(FILE **ret_read_fh, FILE **ret_write_fh) {
  /*
   * Returns connected file objects or establishes the connection
   * if it's not already present
   */
  struct addrinfo *ai_head;
  int sd = -1;
  int status;

  /* Check if we already got opened connections */
  if ((global_read_fh != NULL) && (global_write_fh != NULL)) {
    /* If so, use them */
    if (ret_read_fh != NULL)
      *ret_read_fh = global_read_fh;
    if (ret_write_fh != NULL)
      *ret_write_fh = global_write_fh;
    return 0;
  }

  /* Get all addrs for this hostname */
  struct addrinfo ai_hints = {.ai_family = AF_UNSPEC,
                              .ai_flags = AI_ADDRCONFIG,
                              .ai_socktype = SOCK_STREAM};

  status = getaddrinfo((config_host != NULL) ? config_host : DEFAULT_HOST,
                       (config_port != NULL) ? config_port : DEFAULT_PORT,
                       &ai_hints, &ai_head);
  if (status != 0) {
    ERROR("teamspeak2 plugin: getaddrinfo failed: %s", gai_strerror(status));
    return -1;
  }

  /* Try all given hosts until we can connect to one */
  for (struct addrinfo *ai_ptr = ai_head; ai_ptr != NULL;
       ai_ptr = ai_ptr->ai_next) {
    /* Create socket */
    sd = socket(ai_ptr->ai_family, ai_ptr->ai_socktype, ai_ptr->ai_protocol);
    if (sd < 0) {
      WARNING("teamspeak2 plugin: socket failed: %s", STRERRNO);
      continue;
    }

    /* Try to connect */
    status = connect(sd, ai_ptr->ai_addr, ai_ptr->ai_addrlen);
    if (status != 0) {
      WARNING("teamspeak2 plugin: connect failed: %s", STRERRNO);
      close(sd);
      sd = -1;
      continue;
    }

    /*
     * Success, we can break. Don't need more than one connection
     */
    break;
  } /* for (ai_ptr) */

  freeaddrinfo(ai_head);

  /* Check if we really got connected */
  if (sd < 0)
    return -1;

  /* Create file objects from sockets */
  global_read_fh = fdopen(sd, "r");
  if (global_read_fh == NULL) {
    ERROR("teamspeak2 plugin: fdopen failed: %s", STRERRNO);
    close(sd);
    return -1;
  }

  global_write_fh = fdopen(sd, "w");
  if (global_write_fh == NULL) {
    ERROR("teamspeak2 plugin: fdopen failed: %s", STRERRNO);
    tss2_close_socket();
    return -1;
  }

  { /* Check that the server correctly identifies itself. */
    char buffer[4096];
    char *buffer_ptr;

    buffer_ptr = fgets(buffer, sizeof(buffer), global_read_fh);
    if (buffer_ptr == NULL) {
      WARNING("teamspeak2 plugin: Unexpected EOF received "
              "from remote host %s:%s.",
              config_host ? config_host : DEFAULT_HOST,
              config_port ? config_port : DEFAULT_PORT);
    }
    buffer[sizeof(buffer) - 1] = '\0';

    if (memcmp("[TS]\r\n", buffer, 6) != 0) {
      ERROR("teamspeak2 plugin: Unexpected response when connecting "
            "to server. Expected ``[TS]'', got ``%s''.",
            buffer);
      tss2_close_socket();
      return -1;
    }
    DEBUG("teamspeak2 plugin: Server send correct banner, connected!");
  }

  /* Copy the new filehandles to the given pointers */
  if (ret_read_fh != NULL)
    *ret_read_fh = global_read_fh;
  if (ret_write_fh != NULL)
    *ret_write_fh = global_write_fh;
  return 0;
} /* int tss2_get_socket */

static int tss2_send_request(FILE *fh, const char *request) {
  /*
   * This function puts a request to the server socket
   */
  int status;

  status = fputs(request, fh);
  if (status < 0) {
    ERROR("teamspeak2 plugin: fputs failed.");
    tss2_close_socket();
    return -1;
  }
  fflush(fh);

  return 0;
} /* int tss2_send_request */

static int tss2_receive_line(FILE *fh, char *buffer, int buffer_size) {
  /*
   * Receive a single line from the given file object
   */
  char *temp;

  /*
   * fgets is blocking but much easier then doing anything else
   * TODO: Non-blocking Version would be safer
   */
  temp = fgets(buffer, buffer_size, fh);
  if (temp == NULL) {
    ERROR("teamspeak2 plugin: fgets failed: %s", STRERRNO);
    tss2_close_socket();
    return -1;
  }

  buffer[buffer_size - 1] = '\0';
  return 0;
} /* int tss2_receive_line */

static int tss2_select_vserver(FILE *read_fh, FILE *write_fh,
                               vserver_list_t *vserver) {
  /*
   * Tell the server to select the given vserver
   */
  char command[128];
  char response[128];
  int status;

  /* Send request */
  snprintf(command, sizeof(command), "sel %i\r\n", vserver->port);

  status = tss2_send_request(write_fh, command);
  if (status != 0) {
    ERROR("teamspeak2 plugin: tss2_send_request (%s) failed.", command);
    return -1;
  }

  /* Get answer */
  status = tss2_receive_line(read_fh, response, sizeof(response));
  if (status != 0) {
    ERROR("teamspeak2 plugin: tss2_receive_line failed.");
    return -1;
  }
  response[sizeof(response) - 1] = '\0';

  /* Check answer */
  if ((strncasecmp("OK", response, 2) == 0) &&
      ((response[2] == 0) || (response[2] == '\n') || (response[2] == '\r')))
    return 0;

  ERROR("teamspeak2 plugin: Command ``%s'' failed. "
        "Response received from server was: ``%s''.",
        command, response);
  return -1;
} /* int tss2_select_vserver */

static int tss2_vserver_gapl(FILE *read_fh, FILE *write_fh,
                             gauge_t *ret_value) {
  /*
   * Reads the vserver's average packet loss and submits it to collectd.
   * Be sure to run the tss2_read_vserver function before calling this so
   * the vserver is selected correctly.
   */
  gauge_t packet_loss = NAN;
  int status;

  status = tss2_send_request(write_fh, "gapl\r\n");
  if (status != 0) {
    ERROR("teamspeak2 plugin: tss2_send_request (gapl) failed.");
    return -1;
  }

  while (42) {
    char buffer[4096];
    char *value;
    char *endptr = NULL;

    status = tss2_receive_line(read_fh, buffer, sizeof(buffer));
    if (status != 0) {
      /* Set to NULL just to make sure no one uses these FHs anymore. */
      read_fh = NULL;
      write_fh = NULL;
      ERROR("teamspeak2 plugin: tss2_receive_line failed.");
      return -1;
    }
    buffer[sizeof(buffer) - 1] = '\0';

    if (strncmp("average_packet_loss=", buffer,
                strlen("average_packet_loss=")) == 0) {
      /* Got average packet loss, now interpret it */
      value = &buffer[20];
      /* Replace , with . */
      while (*value != 0) {
        if (*value == ',') {
          *value = '.';
          break;
        }
        value++;
      }

      value = &buffer[20];

      packet_loss = strtod(value, &endptr);
      if (value == endptr) {
        /* Failed */
        WARNING("teamspeak2 plugin: Could not read average package "
                "loss from string: %s",
                buffer);
        continue;
      }
    } else if (strncasecmp("OK", buffer, 2) == 0) {
      break;
    } else if (strncasecmp("ERROR", buffer, 5) == 0) {
      ERROR("teamspeak2 plugin: Server returned an error: %s", buffer);
      return -1;
    } else {
      WARNING("teamspeak2 plugin: Server returned unexpected string: %s",
              buffer);
    }
  }

  *ret_value = packet_loss;
  return 0;
} /* int tss2_vserver_gapl */

static int tss2_read_vserver(vserver_list_t *vserver) {
  /*
   * Poll information for the given vserver and submit it to collect.
   * If vserver is NULL the global server information will be queried.
   */
  int status;

  gauge_t users = NAN;
  gauge_t channels = NAN;
  gauge_t servers = NAN;
  derive_t rx_octets = 0;
  derive_t tx_octets = 0;
  derive_t rx_packets = 0;
  derive_t tx_packets = 0;
  gauge_t packet_loss = NAN;
  int valid = 0;

  char plugin_instance[DATA_MAX_NAME_LEN] = {0};

  FILE *read_fh;
  FILE *write_fh;

  /* Get the send/receive sockets */
  status = tss2_get_socket(&read_fh, &write_fh);
  if (status != 0) {
    ERROR("teamspeak2 plugin: tss2_get_socket failed.");
    return -1;
  }

  if (vserver == NULL) {
    /* Request global information */
    status = tss2_send_request(write_fh, "gi\r\n");
  } else {
    /* Request server information */
    snprintf(plugin_instance, sizeof(plugin_instance), "vserver%i",
             vserver->port);

    /* Select the server */
    status = tss2_select_vserver(read_fh, write_fh, vserver);
    if (status != 0)
      return status;

    status = tss2_send_request(write_fh, "si\r\n");
  }

  if (status != 0) {
    ERROR("teamspeak2 plugin: tss2_send_request failed.");
    return -1;
  }

  /* Loop until break */
  while (42) {
    char buffer[4096];
    char *key;
    char *value;
    char *endptr = NULL;

    /* Read one line of the server's answer */
    status = tss2_receive_line(read_fh, buffer, sizeof(buffer));
    if (status != 0) {
      /* Set to NULL just to make sure no one uses these FHs anymore. */
      read_fh = NULL;
      write_fh = NULL;
      ERROR("teamspeak2 plugin: tss2_receive_line failed.");
      break;
    }

    if (strncasecmp("ERROR", buffer, 5) == 0) {
      ERROR("teamspeak2 plugin: Server returned an error: %s", buffer);
      break;
    } else if (strncasecmp("OK", buffer, 2) == 0) {
      break;
    }

    /* Split line into key and value */
    key = strchr(buffer, '_');
    if (key == NULL) {
      DEBUG("teamspeak2 plugin: Cannot parse line: %s", buffer);
      continue;
    }
    key++;

    /* Evaluate assignment */
    value = strchr(key, '=');
    if (value == NULL) {
      DEBUG("teamspeak2 plugin: Cannot parse line: %s", buffer);
      continue;
    }
    *value = 0;
    value++;

    /* Check for known key and save the given value */
    /* global info: users_online,
     * server info: currentusers. */
    if ((strcmp("currentusers", key) == 0) ||
        (strcmp("users_online", key) == 0)) {
      users = strtod(value, &endptr);
      if (value != endptr)
        valid |= 0x01;
    }
    /* global info: channels,
     * server info: currentchannels. */
    else if ((strcmp("currentchannels", key) == 0) ||
             (strcmp("channels", key) == 0)) {
      channels = strtod(value, &endptr);
      if (value != endptr)
        valid |= 0x40;
    }
    /* global only */
    else if (strcmp("servers", key) == 0) {
      servers = strtod(value, &endptr);
      if (value != endptr)
        valid |= 0x80;
    } else if (strcmp("bytesreceived", key) == 0) {
      rx_octets = strtoll(value, &endptr, 0);
      if (value != endptr)
        valid |= 0x02;
    } else if (strcmp("bytessend", key) == 0) {
      tx_octets = strtoll(value, &endptr, 0);
      if (value != endptr)
        valid |= 0x04;
    } else if (strcmp("packetsreceived", key) == 0) {
      rx_packets = strtoll(value, &endptr, 0);
      if (value != endptr)
        valid |= 0x08;
    } else if (strcmp("packetssend", key) == 0) {
      tx_packets = strtoll(value, &endptr, 0);
      if (value != endptr)
        valid |= 0x10;
    } else if ((strncmp("allow_codec_", key, strlen("allow_codec_")) == 0) ||
               (strncmp("bwinlast", key, strlen("bwinlast")) == 0) ||
               (strncmp("bwoutlast", key, strlen("bwoutlast")) == 0) ||
               (strncmp("webpost_", key, strlen("webpost_")) == 0) ||
               (strcmp("adminemail", key) == 0) ||
               (strcmp("clan_server", key) == 0) ||
               (strcmp("countrynumber", key) == 0) ||
               (strcmp("id", key) == 0) || (strcmp("ispname", key) == 0) ||
               (strcmp("linkurl", key) == 0) ||
               (strcmp("maxusers", key) == 0) || (strcmp("name", key) == 0) ||
               (strcmp("password", key) == 0) ||
               (strcmp("platform", key) == 0) ||
               (strcmp("server_platform", key) == 0) ||
               (strcmp("server_uptime", key) == 0) ||
               (strcmp("server_version", key) == 0) ||
               (strcmp("udpport", key) == 0) || (strcmp("uptime", key) == 0) ||
               (strcmp("users_maximal", key) == 0) ||
               (strcmp("welcomemessage", key) == 0))
      /* ignore */;
    else {
      INFO("teamspeak2 plugin: Unknown key-value-pair: "
           "key = %s; value = %s;",
           key, value);
    }
  } /* while (42) */

  /* Collect vserver packet loss rates only if the loop above did not exit
   * with an error. */
  if ((status == 0) && (vserver != NULL)) {
    status = tss2_vserver_gapl(read_fh, write_fh, &packet_loss);
    if (status == 0) {
      valid |= 0x20;
    } else {
      WARNING("teamspeak2 plugin: Reading package loss "
              "for vserver %i failed.",
              vserver->port);
    }
  }

  if ((valid & 0x01) == 0x01)
    tss2_submit_gauge(plugin_instance, "users", NULL, users);

  if ((valid & 0x06) == 0x06)
    tss2_submit_io(plugin_instance, "io_octets", rx_octets, tx_octets);

  if ((valid & 0x18) == 0x18)
    tss2_submit_io(plugin_instance, "io_packets", rx_packets, tx_packets);

  if ((valid & 0x20) == 0x20)
    tss2_submit_gauge(plugin_instance, "percent", "packet_loss", packet_loss);

  if ((valid & 0x40) == 0x40)
    tss2_submit_gauge(plugin_instance, "gauge", "channels", channels);

  if ((valid & 0x80) == 0x80)
    tss2_submit_gauge(plugin_instance, "gauge", "servers", servers);

  if (valid == 0)
    return -1;
  return 0;
} /* int tss2_read_vserver */

static int tss2_config(const char *key, const char *value) {
  /*
   * Interpret configuration values
   */
  if (strcasecmp("Host", key) == 0) {
    char *temp;

    temp = strdup(value);
    if (temp == NULL) {
      ERROR("teamspeak2 plugin: strdup failed.");
      return 1;
    }
    sfree(config_host);
    config_host = temp;
  } else if (strcasecmp("Port", key) == 0) {
    char *temp;

    temp = strdup(value);
    if (temp == NULL) {
      ERROR("teamspeak2 plugin: strdup failed.");
      return 1;
    }
    sfree(config_port);
    config_port = temp;
  } else if (strcasecmp("Server", key) == 0) {
    /* Server variable found */
    int status;

    status = tss2_add_vserver(atoi(value));
    if (status != 0)
      return 1;
  } else {
    /* Unknown variable found */
    return -1;
  }

  return 0;
} /* int tss2_config */

static int tss2_read(void) {
  /*
   * Poll function which collects global and vserver information
   * and submits it to collectd
   */
  int success = 0;
  int status;

  /* Handle global server variables */
  status = tss2_read_vserver(NULL);
  if (status == 0) {
    success++;
  } else {
    WARNING("teamspeak2 plugin: Reading global server variables failed.");
  }

  /* Handle vservers */
  for (vserver_list_t *vserver = server_list; vserver != NULL;
       vserver = vserver->next) {
    status = tss2_read_vserver(vserver);
    if (status == 0) {
      success++;
    } else {
      WARNING("teamspeak2 plugin: Reading statistics "
              "for vserver %i failed.",
              vserver->port);
      continue;
    }
  }

  if (success == 0)
    return -1;
  return 0;
} /* int tss2_read */

static int tss2_shutdown(void) {
  /*
   * Shutdown handler
   */
  vserver_list_t *entry;

  tss2_close_socket();

  entry = server_list;
  server_list = NULL;
  while (entry != NULL) {
    vserver_list_t *next;

    next = entry->next;
    sfree(entry);
    entry = next;
  }

  /* Get rid of the configuration */
  sfree(config_host);
  sfree(config_port);

  return 0;
} /* int tss2_shutdown */

void module_register(void) {
  /*
   * Mandatory module_register function
   */
  plugin_register_config("teamspeak2", tss2_config, config_keys,
                         config_keys_num);
  plugin_register_read("teamspeak2", tss2_read);
  plugin_register_shutdown("teamspeak2", tss2_shutdown);
} /* void module_register */
