/*
 * This file is part of blabouncer (https://www.blatech.co.uk/l_bratch/blabouncer).
 * Copyright (C) 2019 Luke Bratch <luke@bratch.co.uk>.
 *
 * Blabouncer 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, version 3.
 *
 * Blabouncer 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 blabouncer. If not, see <http://www.gnu.org/licenses/>.
*/

// TODO:
// - Perhaps rename clients.ssl and server_ssl since they may not even be OpenSSL sockets

// "server" means the real IRC server
// "client" means bouncer clients

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <time.h>
#include <sys/stat.h>
#include <limits.h>
#include <signal.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/bio.h>
#include <locale.h>

#include "functions.h"
#include "sockets.h"
#include "config.h"
#include "replay.h"
#include "logging.h"
#include "structures.h"
#include "message.h"

#define SOURCE_SERVER 0
#define SOURCE_CLIENT 1
#define EXCEPT_NONE 0
#define LOG_PRIVMSG 0
#define LOG_JOINPART 1
#define LOG_TOPIC 2
#define DEBUG_CRIT 0
#define DEBUG_SOME 1
#define DEBUG_FULL 2
#define ECONFINT 1 // errno value if getconfint() failed

#define STDIN 0 // stdin is fd 0

// Various important limits - note that several related ones are defined in functions.h and structures.h

// It seems to be that *message length* is max 512 bytes, but a socket read from at least UnrealIRCd seems to be up to at least 2416 (+1 for null) bytes.
// 1208 bytes with OpenSSL, 2416 bytes with plain text.
#define MAXRCVSIZE 2417
#define MAXTOKENS 100 // maximum number of (CRLF or space) separated tokens per server response we expect (TODO - check this is reasonable) (CRLF and spaces just grouped here due to laziness)
#define SERVERTIMEOUT 300 // How many seconds to wait without hearing from the server before assuming a timeout
#define SELECTTIMEOUT 60 // How many seconds to wait before our pselect() times out, useful for detecting server timeouts

// Global debug control
int debug = 0; // Debug verbosity ("0" for critical only, "1" for some extra info, "2" for full debug mode)
char debugpath[PATH_MAX]; // Path to debug file

// Set if certain signal (just SIGINT and SIGKILL at time of writing) is received
int signum = 0;

// Signal handler
// We don't actually do anything in here, the main pselect() notice signals
void sighandler(int sig) {
  signum = sig;
}

// Connect to the real IRC server.
// Returns 1 on success or 0 on failure.
int connecttoircserver(SSL_CTX **serverctx, SSL **server_ssl, int *serversockfd, struct ircdstate *ircdstate, struct settings *settings, struct client *clients) {
  char outgoingmsg[MAXDATASIZE]; // String to send to server

  if (settings->servertls) {
    debugprint(DEBUG_FULL, "Server OpenSSL start.\n");
    *serverctx = create_openssl_context(SOURCE_SERVER);
    configure_openssl_context(*serverctx, NULL, NULL);
    *server_ssl = SSL_new(*serverctx);
    SSL_set_fd(*server_ssl, *serversockfd);
    if (SSL_connect(*server_ssl) == -1) {
      char* errstr = openssl_error_string();
      debugprint(DEBUG_CRIT, "SSL_connect failed - %s", errstr);
      if (errstr != NULL) free(errstr);
      return 0;
    } else {
      debugprint(DEBUG_FULL, "SSL_connect() success.\n");

    }
    debugprint(DEBUG_FULL, "Server OpenSSL complete.\n");
  } else {
    // If not using TLS then just slap the serversockfd into server_ssl by casting it
    *server_ssl = (SSL*)(long int)*serversockfd;
  }

  // <=============================================
  // Initialise IRC connecting/registration state

  // Set ircdstate to zero-length strings for now
  ircdstate->greeting001[0] = '\0';
  ircdstate->greeting002[0] = '\0';
  ircdstate->greeting003[0] = '\0';
  ircdstate->greeting004[0] = '\0';
  ircdstate->greeting005a[0] = '\0';
  ircdstate->greeting005b[0] = '\0';
  ircdstate->greeting005c[0] = '\0';
  ircdstate->ircdname[0] = '\0';
  ircdstate->nickuserhost[0] = '\0';
  ircdstate->ircnick[0] = '\0';
  ircdstate->ircusername[0] = '\0';
  ircdstate->currentmsg[0] = '\0';
  ircdstate->mode[0] = '\0';
  // ircdstate->oldnick is not set here as we want to track reconnections separately
  // And set non-string things to zero
  ircdstate->capmultiprefix = 0;
  ircdstate->autonicknum = 0;
  ircdstate->lastmessagetime = time(NULL);
  ircdstate->timeoutcheck = 0;
  // ircdstate.reconnecting is not set here as we want to track reconnections separately
  // ircdstate.clientchangetime and ircdstate.clientsnonetime not set here as they are set at startup and only changed when clients connect/disconnect
  // ircdstate.clientcodes not set here, set on startup and whenever a client sets one
  // ircdstate->maxchannelcount not set here, set on startup in dochat()

  // Populate nicks[0] and username from our configuration file for now, real IRCd may change them later (TODO - Is this true of username?)
  strcpy(ircdstate->ircnick, settings->ircnicks[0]);
  strcpy(ircdstate->ircusername, settings->ircusername);

  // Send the server password if one was configured
  if (settings->ircserverpassword[0]) {
    snprintf(outgoingmsg, MAXDATASIZE, "PASS %s", settings->ircserverpassword);
    // sourcefd = 0 as this is a trusted message
    sendtoserver(*server_ssl, outgoingmsg, strlen(outgoingmsg), 0, clients, settings);
  }

  // Send our NICK
  snprintf(outgoingmsg, MAXDATASIZE, "NICK %s", ircdstate->ircnick); // TODO - Check for success (with return code)
  // sourcefd = 0 as this is a trusted message
  sendtoserver(*server_ssl, outgoingmsg, strlen(outgoingmsg), 0, clients, settings);

  // Send our USER
  snprintf(outgoingmsg, MAXDATASIZE, "USER %s 8 * : %s", ircdstate->ircusername, settings->ircrealname); // TODO - Check for success (with return code)
                                                                                // TODO - Send a more intelligent/correct USER string
  // sourcefd = 0 as this is a trusted message
  sendtoserver(*server_ssl, outgoingmsg, strlen(outgoingmsg), 0, clients, settings);

  // =============================================>

  return 1;
}

// Figure out what to do with each CRLF-split IRC message (if anything)
// by splitting out the different components by space character (ASCII 0x20).
//
// serversockfd, clientsockfd, fdmax, arr_clients, arr_authed, all passed to here so we can
// send/relay a response to server or (other) client(s) directly from this function if
// we want to.
//
// str is the raw string
//
// source is either 0 (SOURCE_SERVER) (server - from the real IRC server) or 1 (SOURCE_CLIENT) (client - from a real IRC client)
//
// Return 1 if we processed something and expect the caller to not need to do anything more
// Return 0 if we didn't process it and the caller might want to do something
//int processircmessage(int *serversockfd, int *clientsockfd, char *str, int source) {
int processircmessage(SSL *server_ssl, char *str, int source, struct client *clients, int sourcefd, struct ircdstate *ircdstate, struct channel *channels,
                      struct settings *settings, struct clientcodes *clientcodes) {
  // Track which space-separated token within this response we're on
  int counter = 0;

  // Build array of each space-separated token (TODO - Use counter to stop splitting once we reach some reasonable value - i.e. once we're definitely past commands and into just free text)
  char tokens[MAXTOKENS][MAXDATASIZE];

  debugprint(DEBUG_FULL, "  >> processircmessage(): Processing source %d message \"%s\"...\n", source, str);

  // Copy to a temporary string so we still have the original in case it's not processed
  char *strcopy = strdup(str);
  // Keep track of initial pointer for free()ing later
  char *strcopyPtr = strcopy;

  char *token;

  while ((token = strsep(&strcopy, " ")) != NULL) {
    if (*token  == '\0') continue; // Skip consecutive matches
    if (counter >= MAXTOKENS) break; // Too many tokens
    debugprint(DEBUG_FULL, "  >> Message Token: \"%s\", length %zd.\n", token, strlen(token));
    // Copy into the token array (strlen + 1 to get the NULL terminator)
    strncpy(tokens[counter], token, strlen(token) + 1);
    counter++;
  }

  // <=============================================
  // IRC command processing (commands from server or client)

  switch(source) {
    case SOURCE_SERVER: // If message(s) were from the real IRC server
      if (processservermessage(server_ssl, str, clients, sourcefd, ircdstate, channels, settings, tokens, counter)) {
        // We processed something so return true
        free(strcopyPtr);
        return 1;
      }

      // Don't return if we got here because this means we didn't process something in processservermessage()
      break;
    case SOURCE_CLIENT: // If message(s) were from a real IRC client
      if (processclientmessage(server_ssl, str, clients, sourcefd, ircdstate, channels, settings, tokens, counter, clientcodes)) {
        // We processed something so return true
        free(strcopyPtr);
        return 1;
      }

      // Don't return if we got here because this means we didn't process something in processclientmessage()
      break;
    default:
      fprintf(stderr, "Unexpected raw IRC string source!\n");
      debugprint(DEBUG_CRIT, "Unexpected raw IRC string source!\n");
      free(strcopyPtr);
      return 0;
  }
  // =============================================>


  debugprint(DEBUG_FULL, "Done with processircmessage(), didn't process anything, logging to network log.\n");


  // Write to normal log if logging enabled
  if (settings->logging) {
    logline(str, ircdstate, settings->basedir, LOG_NETWORK);
  }

  // If we got here then we didn't process anything
  free(strcopyPtr);
  return 0;
}

// Figure out what the client or server sent and decide what to do with it (if anything)
// First by splitting out the different messages by CRLF, process if we know how to,
// or send on to all other clients (if from the server) or to the server (if from a client)
// if we don't know how to.
//
// serversockfd, clientsockfd, fdmax, arr_clients, arr_authed all passed to here so we can
// send/relay a response to server or (other) client(s) for things we don't know how to process here,
// or for any other reasons we need to send stuff for
//
// str is the raw string
//
// source is either 0 (SOURCE_SERVER) (server - from the real IRC server) or 1 (SOURCE_CLIENT) (client - from a real IRC client)
//
// Return 0 if something went wrong
// Return 1 if everything OK
int processrawstring(SSL *server_ssl, char *str, int source, struct client *clients, int sourcefd, struct ircdstate *ircdstate, struct channel *channels,
                     struct settings *settings, struct clientcodes *clientcodes) {
  // Copy to a temporary string so we still have the original in case it's not processed
  char *strcopy = strdup(str);
  // Keep track of initial pointer for free()ing later
  char *strcopyPtr = strcopy;

  debugprint(DEBUG_FULL, "processrawstring(): Source type %d sent: \"%s\".  Processing it...\n", source, strcopy);

  // Track which CLRF-separated message within this string we're on
  int messagecount = 0;

  // Build array of each space-separated token
  char messages[MAXTOKENS][MAXDATASIZE];

  // Split the string by CRLF and add each CRLF-separated IRC message to an array
  char *token;

  while ((token = strsep(&strcopy, "\r\n")) != NULL) {
    if (*token == '\0') continue; // Skip consecutive matches
    if (messagecount >= MAXTOKENS) break; // Too many tokens
    debugprint(DEBUG_FULL, "String Token: \"%s\", length %zd.\n", token, strlen(token));
    // Make sure it's not too long
    if (strlen(token) > MAXDATASIZE - 1) {
      debugprint(DEBUG_CRIT, "Token too long, discarding.\n");
      continue;
    }
    // Copy into the token array (strlen + 1 to get the NULL terminator)
    strncpy(messages[messagecount], token, strlen(token) + 1);
    messagecount++;
  }

  free(strcopyPtr);

  // If there is a previous possibly truncated message still in the holding area, the prepend that to the first message of this new lot
  // (Only if source was the server since we always strip \r\n from client messages when recving - TODO - Should we be doing that?
  if (ircdstate->currentmsg[0] && source == SOURCE_SERVER) {
    // Make a copy since we can't have source and destination the same with snprintf
    char *strtmp = strdup(messages[0]);
    debugprint(DEBUG_FULL, "processrawstring(): Previous truncated message detected, combining old '%s' with new '%s'...\n", ircdstate->currentmsg, strtmp);
    snprintf(messages[0], strlen(ircdstate->currentmsg) + strlen(strtmp) + 1, "%s%s", ircdstate->currentmsg, strtmp);
    messages[0][strlen(ircdstate->currentmsg) + strlen(strtmp)] = '\0'; // Make sure it's null terminated
    debugprint(DEBUG_FULL, "...into new string '%s' and clearing currentmsg holding area.\n", messages[0]);
    ircdstate->currentmsg[0] = '\0';
    free(strtmp);
    // Make sure the resulting message isn't too long
    if (strlen(messages[0]) >= MAXDATASIZE - 1) {
      debugprint(DEBUG_CRIT, "processrawstring(): combined truncated message '%d' is too long (%ld out of a maximum of %d characters), clearing it.\n", messages[0], strlen(messages[0]), MAXDATASIZE - 1);
      messages[0][0] = '\0';
    }
  }

  // If the final characters of the raw string weren't \r\n then assume the final token is a truncated message
  // Copy to a holding area for continuation next time
  // (Only if source was the server since we always strip \r\n from client messages when recving - TODO - Should we be doing that?
  if (strlen(str) > 2 && (str[strlen(str)-2] != 13 || str[strlen(str)-1] != 10) && source == SOURCE_SERVER) {
    debugprint(DEBUG_FULL, "processrawstring(): Truncated message detected, storing final token for later.\n");
    strncpy(ircdstate->currentmsg, messages[messagecount - 1], strlen(messages[messagecount - 1]));
    ircdstate->currentmsg[strlen(messages[messagecount - 1])] = '\0';
    // Remove it from the message count so it's not processed time time
    messagecount--;
  } else {
    // Otherwise, clear the holding area
    ircdstate->currentmsg[0] = '\0';
  }

  // Go through each message, figure out what it is and if we're doing anything with it
  for (int i = 0; i < messagecount; i++) {
    // Copy to a temporary string so we still have the original in case it's not processed
    char *messagecopy = strdup(messages[i]);
    if (processircmessage(server_ssl, messagecopy, source, clients, sourcefd, ircdstate, channels, settings, clientcodes)) {
      debugprint(DEBUG_FULL, "Message processed: \"%s\", NULLing...\n", messages[i]);
      messages[i][0] = '\0';
    }
    free(messagecopy);
  }

  // Deal with any unprocessed messages (send on to server/client(s) since we haven't dealt with them)
  for (int i = 0; i < messagecount; i++) {
    if (messages[i][0] != '\0') {
      debugprint(DEBUG_FULL, "Message %d (\"%s\") remains unprocessed...\n", i, messages[i]);

      switch(source) {
        case SOURCE_SERVER: // If message(s) were from the real IRC server
          // Relay/send to all clients ("except" = 0 because this should send to all clients)
          // TODO - Is this really going to send the original string if we have messed it with it in processrawstring() and friends!?
          debugprint(DEBUG_FULL, "bouncer-server: sending unprocessed server message '%s' to all clients, length %zd.\n", messages[i], strlen(messages[i]));
          sendtoallclients(clients, messages[i], EXCEPT_NONE, settings);
          break;
        case SOURCE_CLIENT: // If message(s) were from a real IRC client
          // Send to server
          debugprint(DEBUG_FULL, "bouncer-client: sending unprocessed client message '%s' to the server, length %zd.\n", messages[i], strlen(messages[i]));
          sendtoserver(server_ssl, messages[i], strlen(messages[i]), sourcefd, clients, settings);

          debugprint(DEBUG_FULL, "bouncer-client: sending unprocessed client message '%s' to all other clients, length %zd.\n", messages[i], strlen(messages[i]));
          // send the same thing to all *other* clients (all except for source fd)
          sendtoallclients(clients, messages[i], sourcefd, settings);
          break;
        default:
          fprintf(stderr, "Unexpected raw IRC string source for unprocessed message \"%s\", length %zd.!\n", messages[i], strlen(messages[i]));
          debugprint(DEBUG_CRIT, "Unexpected raw IRC string source for unprocessed message \"%s\", length %zd.!\n", messages[i], strlen(messages[i]));
          return 0;
      }
    }
  }

  debugprint(DEBUG_FULL, "Done with processrawstring()\n");

  return 1;
}

// Where the big bouncing loop is
void dochat(int *serversockfd, int *clientsockfd, struct settings *settings) {
  char serverbuf[MAXRCVSIZE]; // buffer for receiving data on server socket
  char clientbuf[MAXRCVSIZE]; // buffer for receiving data on client socket(s)
  int servernumbytes; // Number of bytes received from remote server

  int fdmax; // highest numbered socket fd

  socklen_t addrlen; // client remote address size
  char remoteIP[INET6_ADDRSTRLEN]; // remote IP (assume up to IPv6 size)
  int newfd;  // newly accept()ed socket descriptor
  struct sockaddr_storage remoteaddr; // client address
  int clientnumbytes;

  fdmax = *clientsockfd; // keep track of highest fd number, currently client socket as created last (after server socket)

  fd_set rfds; // set of read fds to monitor with pselect() - 0: stdin, 1: stdout, 2: stderr, 3 and higher: sockets (at time of writing, 3: real IRC server, 4: client listener, 5 and higher: clients)

  // Set up clients structure
  struct client clients[MAXCLIENTS];

  // Set all the clients to be "not connected", "not authenticated", "not registered", "not pending replies"
  for (int i = 0; i < MAXCLIENTS; i++) {
    clients[i].fd = 0;
    clients[i].authed = 0;
    clients[i].registered = 0;
    clients[i].pendingsslaccept = 0;
    clients[i].pendingchannelmode = 0;
    clients[i].pendingban = 0;
    clients[i].pendingwho = 0;
    clients[i].pendinglist = 0;
    clients[i].pendingwhois = 0;
    clients[i].pendingwhowas = 0;
    clients[i].pendingnames = 0;
    clients[i].pendingcap = 0;
    clients[i].clientcode[0] = '\0';
    clients[i].remoteip[0] = '\0';
  }

  // Struct of various strings from and for the real IRCd (such as the greeting strings, the real IRCd's name,
  // our nick!user@host string, our nick, username, real name, etc.)
  struct ircdstate ircdstate;
  // Set reconnection and other things to null/zero for now (not used unless reconnecting to server)
  ircdstate.oldnick[0] = '\0';
  ircdstate.reconnecting = 0;
  ircdstate.clientchangetime = time(NULL);
  ircdstate.clientsnonetime = time(NULL);
  // Record the time we launched (for comparisons later)
  ircdstate.launchtime = time(NULL);

  // Struct of client codes
  // Used to track the last time a client identifying as a given client connected to handle auto replay for a known client.
  struct clientcodes clientcodes[MAXCLIENTCODES];
  for (int i = 0; i < MAXCLIENTCODES; i++) {
    clientcodes[i].code[0] = '\0';
    clientcodes[i].lastdisconnected = 0;
  }

  // Struct of channels we're in
  struct channel *channels;
  channels = malloc(sizeof(struct channel) * MAXCHANNELS);
  ircdstate.maxchannelcount = 0;
  // Each channel struct element is only initialised upon channel creation to avoid consuming lots of memory here.
  // The memory is never released once a channel is parted - TODO - Can this channel struct memory usage be improved?

  // Initialise OpenSSL (used for both client and server)
  init_openssl();

  // OpenSSL for server side if configured
  SSL_CTX *serverctx = NULL;
  SSL *server_ssl = NULL; // Need to create this either way as referenced later

  // Try to connect to IRC!
  if (!connecttoircserver(&serverctx, &server_ssl, serversockfd, &ircdstate, settings, clients)) {
    fprintf(stderr, "Failed to connect to IRC server, exiting.\n");
    debugprint(DEBUG_CRIT, "Failed to connect to IRC server, exiting.\n");
    exit(EXIT_FAILURE);
  }

  // OpenSSL context for client side (that clients connect to) (need to create this whether or not using TLS as it is referenced later)
  SSL_CTX *ctx;

  // If using client TLS
  if (settings->clienttls) {
    // Set up and configure client OpenSSL context
    ctx = create_openssl_context(SOURCE_CLIENT);
    configure_openssl_context(ctx, settings->certfile, settings->keyfile);
  }

  // Let's set up signal handling stuff here since we're about to enter The Big Loop (TM)
  // We'll handle SIGHUP (for rehashing), SIGINT (Ctrl-C), and SIGTERM (default signal of `kill`)
  signal(SIGHUP, sighandler); // SIGHUP (1)
  signal(SIGINT, sighandler); // SIGINT (2)
  signal(SIGTERM, sighandler); // SIGTERM (15)

  // Block those signals
  sigset_t sigset, oldset;
  sigemptyset(&sigset);
  sigaddset(&sigset, SIGHUP);
  sigaddset(&sigset, SIGINT);
  sigaddset(&sigset, SIGTERM);
  sigprocmask(SIG_BLOCK, &sigset, &oldset);

  // Extra signal handling to ignore SIGPIPE so failing send() and/or SSL_write() doesn't terminate blabouncer with SIGPIPE
  signal(SIGPIPE, SIG_IGN);

  while (1) {
    debugprint(DEBUG_FULL, "top of loop, fdmax %d.\n", fdmax);
    FD_ZERO(&rfds); // clear entries from fd set

     // Add STDIN (fd 0) to read fds to monitor if we're not in background/daemon mode
    if (!settings->background) {
      FD_SET(STDIN, &rfds);
    }
    FD_SET(*serversockfd, &rfds); // add our server network socket to monitor
    FD_SET(*clientsockfd, &rfds); // add our client network socket to monitor

    // Add all connected clients to monitor (only add ones that are connected (clients[i] > 0))
    // TODO - make sure *serversockfd stays at the same value (probably 3?) in all cases - what if the server disconnects/reconnects/etc.
    // TODO - now that only connected clients are monitored, perhaps tracking using both fdmax and num_client loops is unnecessary?
    for (int i = 0; i < MAXCLIENTS; i++) {
      if (clients[i].fd > 0) {
        debugprint(DEBUG_FULL, "monitoring fd %d.\n", clients[i].fd);
        FD_SET(clients[i].fd, &rfds);
      }
    }

    debugprint(DEBUG_FULL, "pselect()ing...\n");
    // Check to see if any fd in the fd_set is waiting or a signal happened - blocks here until one one of those things happens
    // (pselect() to do signal handling in addition to fd monitoring)
    struct timespec timeout = {SELECTTIMEOUT, 0};  // pselect() should timeout after SELECTTIMEOUT seconds
    int pselret = pselect(fdmax + 1, &rfds, NULL, NULL, &timeout, &oldset); // network socket + 1, rfds, no writes, no exceptions/errors, no timeout, original sigmask
    if (pselret < 0) { // Error or signal interrupt
      if (errno == EINTR) {
        // Signal caught, do signal handling
        debugprint(DEBUG_CRIT, "signal '%d' happened, exiting!\n", signum);
        if (signum == SIGHUP) { // REHASH requested
          // TODO - This code is duplicated between here and BLABOUNCER REHASH handling
          char outgoingmsg[MAXDATASIZE];
          char failuremsg[MAXDATASIZE];
          failuremsg[0] = '\0';

          // Try to rehash...
          if (!rehash(settings, failuremsg)) {
            // ...or log and tell all clients if it failed
            debugprint(DEBUG_CRIT, "SIGHUP REHASH failed: %s.\n", failuremsg);
            if (!snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :SIGHUP REHASH failed: %s.", ircdstate.ircnick, failuremsg)) {
              debugprint(DEBUG_CRIT, "Error while preparing SIGHUP REHASH failure message response!\n");
              outgoingmsg[0] = '\0';
            }
            sendtoallclients(clients, outgoingmsg, 0, settings);
          } else {
            // ...or tell all clients it worked
            snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :SIGUP REHASH complete!", ircdstate.ircnick);
            sendtoallclients(clients, outgoingmsg, 0, settings);
          }
          // Then go back to the top of the loop
          continue;
        } else if (signum == SIGINT) { // Probably Ctrl+C
          cleanexit(server_ssl, clients, 0, &ircdstate, settings, "SIGINT received");
        } else if (signum == SIGTERM) { // Probably `kill`
          cleanexit(server_ssl, clients, 0, &ircdstate, settings, "SIGTERM received");
        } else {
          cleanexit(server_ssl, clients, 0, &ircdstate, settings, "Unexpected signal received");
        }
      } else {
        // Some other error
        perror("pselect");
        debugprint(DEBUG_CRIT, "pselect() error, errno '%d'.\n", errno);
        continue;
      }
    }

    // pselect() timed out, let's see if a server timeout might be happening, if not, just continue to the top of the loop again
    if (pselret == 0) {
      debugprint(DEBUG_CRIT, "pselect() timed out.\n", errno);

      // SERVERTIMEOUT seconds have expired...
      if (ircdstate.lastmessagetime < time(NULL) - SERVERTIMEOUT && !FD_ISSET(*serversockfd, &rfds)) {
        if (ircdstate.timeoutcheck == 0) {
          // ...and we haven't tried a PING yet, so let's PING the server to see if things are still working
          debugprint(DEBUG_CRIT, "Server might have timed out after %ld seconds, PINGing it...\n", time(NULL) - ircdstate.lastmessagetime);
          ircdstate.timeoutcheck = 1;
          char outgoingmsg[MAXDATASIZE];
          if (!snprintf(outgoingmsg, MAXDATASIZE, "PING %s", ircdstate.ircdname)) {
            fprintf(stderr, "Error while preparing timeout testing PING message!\n");
            debugprint(DEBUG_CRIT, "Error while preparing timeout testing PING message\n");
            snprintf(outgoingmsg, MAXDATASIZE, "PING timeouttest");
          }
          sendtoserver(server_ssl, outgoingmsg, strlen(outgoingmsg), 0, clients, settings);
          // Back to top of loop
          continue;
        } else {
          // ...and we've already PINGed the server and haven't heard back yet, so let's assume we've timed out
          // TODO - Code duplication, make a function and share with socket error code below
          debugprint(DEBUG_CRIT, "Server has timed out (%ld seconds), reconnecting!\n", time(NULL) - ircdstate.lastmessagetime);
          // Tell all clients if we timed out
          char alertmsg[MAXDATASIZE];
          snprintf(alertmsg, MAXDATASIZE, "NOTICE %s :Server has timed out (%ld seconds), reconnecting!", ircdstate.ircnick, time(NULL) - ircdstate.lastmessagetime);
          sendtoallclients(clients, alertmsg, 0, settings);
          if (settings->servertls) {
            // Finish up with OpenSSL if using server TLS
            if (server_ssl != NULL) SSL_free(server_ssl);
            // Set to NULL so we can check if we already free()d this on a previous attempt
            server_ssl = NULL;
          }
          // Close the socket
          close(*serversockfd);
          // Make a new one and reconnect
          if ((*serversockfd = createserversocket(settings->ircserver, settings->ircserverport)) == -1) {
            // We failed
            debugprint(DEBUG_CRIT, "dochat(): Couldn't reconnect to server, will try again.\n");
            // Tell clients
            char alertmsg[MAXDATASIZE];
            snprintf(alertmsg, MAXDATASIZE, "NOTICE %s :Couldn't reconnect to server, will try again.", ircdstate.ircnick);
            sendtoallclients(clients, alertmsg, 0, settings);
            // Don't go back to the top of the loop yet, perhaps some clients are waiting to tell us something - TODO - Could this ever happen in this state?
          } else {
            // We succeeded
            // Set reconnection marker for other functions to know we're reconnecting
            ircdstate.reconnecting = 1;
            // Clear gotnames from each channel since we'll need to (re-)send RPL_NAMREPLYs to all clients after reconnecting
            int channelcount = getchannelcount(channels, ircdstate.maxchannelcount);
            for (int i = 0; i < ircdstate.maxchannelcount; i++) {
              debugprint(DEBUG_FULL, "dochat(): timeout reconnecting: checking channel[%d] out of %d.\n", i, channelcount);
              // Skip this one if it's a blank channel
              if (!channels[i].name[0]) {
                  debugprint(DEBUG_FULL, "dochat(): timeout reconnecting: skipping blank channel channel[%d].\n", i);
                  continue;
              }

              debugprint(DEBUG_FULL, "dochat(): timeout reconnecting: clearing gotnames in channel '%s'.\n", channels[i].name);
              channels[i].gotnames = 0;
            }
            // Set oldnick in case we change nick when reconnecting so we can inform existing clients
            strcpy(ircdstate.oldnick, ircdstate.ircnick);
            if (!connecttoircserver(&serverctx, &server_ssl, serversockfd, &ircdstate, settings, clients)) {
              // Let clients know if we failed
              char alertmsg[MAXDATASIZE];
              snprintf(alertmsg, MAXDATASIZE, "NOTICE %s :Failed to reconnect to server, will try again.", ircdstate.ircnick);
              sendtoallclients(clients, alertmsg, 0, settings);
              // Then wait a few seconds and try again
              sleep(5);
              connecttoircserver(&serverctx, &server_ssl, serversockfd, &ircdstate, settings, clients);
            }
            // Back to top of loop
            continue;
          }
        }
      } else {
        // No timeout is occurring, the pselect() just timed out (which is fine and expected), back to the top of the loop
        continue;
      }
    }

    // TODO - switch around the serversockfd and STDIN FD_ISSET if-statements?  They feel the wrong way round.  Are they like this on purpose?  I can't remember.
    // (although actually stdin may go once its not wanted for possible direct interaction for debugging)

    // See if there's anything to read from the server side (the real IRCd)
    if (FD_ISSET(*serversockfd, &rfds)) {
      debugprint(DEBUG_FULL, "reading server socket!\n");

      if ((servernumbytes = sockread(server_ssl, serverbuf, MAXRCVSIZE - 1, settings->servertls)) == -1) {
        debugprint(DEBUG_CRIT, "dochat(): server sockread() error (-1) (%s), skipping loop iteration.\n", strerror(errno));
      } else if (servernumbytes == 0) {
        debugprint(DEBUG_CRIT, "dochat(): server sockread() closed or no data received (0) (%s), skipping loop iteration.\n", strerror(errno));
      }

      // If there was a socket error (receive error or socket closed)
      // TODO - Code duplication, make a function and share with timeout code above
      if (servernumbytes < 1) {
        debugprint(DEBUG_CRIT, "Server socket had an error (sockread return code %d), reconnecting!\n", servernumbytes);
        // Tell all clients if we had an error
        char alertmsg[MAXDATASIZE];
        snprintf(alertmsg, MAXDATASIZE, "NOTICE %s :Server socket had an error (sockread return code %d), reconnecting!", ircdstate.ircnick, servernumbytes);
        sendtoallclients(clients, alertmsg, 0, settings);
        if (settings->servertls) {
          // Finish up with OpenSSL if using server TLS
          if (server_ssl != NULL) SSL_free(server_ssl);
          // Set to NULL so we can check if we already free()d this on a previous attempt
          server_ssl = NULL;
        }
        // Close the socket
        close(*serversockfd);
        // Make a new one and reconnect
        if ((*serversockfd = createserversocket(settings->ircserver, settings->ircserverport)) == -1) {
          // We failed
          debugprint(DEBUG_CRIT, "dochat(): Couldn't reconnect to server, will try again.\n");
          // Tell clients
          char alertmsg[MAXDATASIZE];
          snprintf(alertmsg, MAXDATASIZE, "NOTICE %s :Couldn't reconnect to server, will try again.", ircdstate.ircnick);
          sendtoallclients(clients, alertmsg, 0, settings);
          // Don't go back to the top of the loop yet, perhaps some clients are waiting to tell us something - TODO - Could this ever happen in this state?
        } else {
          // We succeeded
          // Set reconnection marker for other functions to know we're reconnecting
          ircdstate.reconnecting = 1;
          // Clear gotnames from each channel since we'll need to (re-)send RPL_NAMREPLYs to all clients after reconnecting
          int channelcount = getchannelcount(channels, ircdstate.maxchannelcount);
          for (int i = 0; i < ircdstate.maxchannelcount; i++) {
            debugprint(DEBUG_FULL, "dochat(): socket error reconnecting: checking channel[%d] out of %d.\n", i, channelcount);
            // Skip this one if it's a blank channel
            if (!channels[i].name[0]) {
                debugprint(DEBUG_FULL, "dochat(): socket error reconnecting: skipping blank channel channel[%d].\n", i);
                continue;
            }

            debugprint(DEBUG_FULL, "dochat(): socket error reconnecting: clearing gotnames in channel '%s'.\n", channels[i].name);
            channels[i].gotnames = 0;
          }
          // Set oldnick in case we change nick when reconnecting so we can inform existing clients
          strcpy(ircdstate.oldnick, ircdstate.ircnick);
          if (!connecttoircserver(&serverctx, &server_ssl, serversockfd, &ircdstate, settings, clients)) {
            // Let clients know if we failed
            char alertmsg[MAXDATASIZE];
            snprintf(alertmsg, MAXDATASIZE, "NOTICE %s :Failed to reconnect to server, will try again.", ircdstate.ircnick);
            sendtoallclients(clients, alertmsg, 0, settings);
            // Then wait a few seconds and try again
            sleep(5);
            connecttoircserver(&serverctx, &server_ssl, serversockfd, &ircdstate, settings, clients);
          }
          // Back to top of loop
          continue;
        }
      }

      serverbuf[servernumbytes] = '\0';

      debugprint(DEBUG_SOME, "BOUNCER-SERVER RECEIVED: '%s', length '%d'.\n", serverbuf, servernumbytes);

      // Try to process received string (which should contain one or more server responses/commands)
      // TODO - What if there were two server responses/commands and only one didn't need relaying?
      if (!processrawstring(server_ssl, serverbuf, SOURCE_SERVER, clients, EXCEPT_NONE, &ircdstate, channels, settings, clientcodes)) {
        fprintf(stderr, "Error: bouncer-server failed to process raw string.\n");
        debugprint(DEBUG_CRIT, "Error: bouncer-server failed to process raw string.\n");
      }
    }

    // see if there's anything from stdin (unless we're in background/daemon mode)
    if (!settings->background && FD_ISSET(STDIN, &rfds)) {
      debugprint(DEBUG_FULL, "reading stdin!\n");

      char outgoingmsg[MAXDATASIZE]; // String to send to server
      int outgoingmsgrc; // Return code from getstdin() for outgoing message

      outgoingmsgrc = getstdin(NULL, outgoingmsg, sizeof(outgoingmsg));

      if (outgoingmsgrc == NO_INPUT) {
        printf("\nError!  No input.\n");
      } else if (outgoingmsgrc == TOO_LONG) {
        printf ("Error!  Too long.  Would have allowed up to: [%s]\n", outgoingmsg);
      }

      // STDIN based commands for debugging
      if (strncmp(outgoingmsg, "listchannels", strlen("listchannels")) == 0) {
        printf("STDIN command starting: listchannels\n");

        int channelcount = getchannelcount(channels, ircdstate.maxchannelcount);

        for (int i = 0; i < ircdstate.maxchannelcount; i++) {
          printf("Checking channel[%d] out of %d.\n", i, channelcount);
          // Skip this one if it's a blank channel
          if (!channels[i].name[0]) {
              debugprint(DEBUG_FULL, "Skipping blank channel channel[%d].\n", i);
              continue;
          }

          printf("Found channel '%s'.\n", channels[i].name);
        }

        printf("STDIN command complete: listchannels\n");
        continue;
      }

      // sourcefd = 0 as this is a trusted message
      sendtoserver(server_ssl, outgoingmsg, strlen(outgoingmsg), 0, clients, settings);
    }

    // go through all the remaining sockets to see if there's anything from the client sockets (either new connections or existing clients sending messages)
    // (clear newfd before doing this so we can tell if we're querying a new client or not)
    newfd = 0;
    for (int i = *clientsockfd; i <= fdmax; i++) {
      // skip if newfd is the current iteration of this loop, since we know we have just accept()ed it
      if (i == newfd) {
        continue;
      }
      debugprint(DEBUG_FULL, "checking client socket %d out of %d.\n", i, fdmax);
      if (FD_ISSET(i, &rfds)) {
        debugprint(DEBUG_FULL, "fd %d is FD_ISSET and it is a...\n", i);

        // Index of client fd in clients array for use later
        int clientindex = arrindex(clients, i);
        if (clientindex < 0 && i != *clientsockfd) {
          debugprint(DEBUG_CRIT, "dochat(): error: arrindex() returned '%d', exiting!\n", clientindex);
          exit(1);
        }

        // if value of clientsockfd then must be a new connection, if greater must be an existing connection
        if (i == *clientsockfd) {
          debugprint(DEBUG_SOME, "...new connection!\n");
          // handle new connections
          if (numclients(clients) >= MAXCLIENTS) {
            fprintf(stderr, "too many clients, disconnecting and skipping loop iteration!\n");
            debugprint(DEBUG_CRIT, "too many clients, disconnecting and skipping loop iteration!\n");
            disconnectclient(i, clients, &ircdstate, settings, clientcodes);
            continue;
          }
          addrlen = sizeof remoteaddr;
          newfd = accept(*clientsockfd, (struct sockaddr *)&remoteaddr, &addrlen);
          if (newfd == -1) {
            // something went wrong when accept()ing
            perror("accept");
          } else {
            FD_SET(newfd, &rfds); // add to master set // TODO - needed?  going to be re-done at the top anyway...
            if (newfd > fdmax) {    // keep track of the max
              fdmax = newfd;
            }

            // If openssl_accept fails later on, this is the message we'll append to the usual connection announcement
            char opensslfailmsg[] = ", but was disconnected due to a failure in SSL_accept()";
            // Store the client's IP address for now, since we may need to refer to it after disconnecting
            // them (thus clearing the array entry that the IP is read from) if something goes wrong
            char remoteip[INET6_ADDRSTRLEN];
            strncpy(remoteip, inet_ntop(remoteaddr.ss_family, get_in_addr((struct sockaddr*)&remoteaddr), remoteIP, INET6_ADDRSTRLEN), INET6_ADDRSTRLEN);

            // Find a free element in the clients array and set to new fd value (plus start SSL_accept() if using client TLS)
            for (int j = 0; j < MAXCLIENTS; j++) {
              if (clients[j].fd == 0) {
                clients[j].fd = newfd;
                // Ensure its authentication status is set to 0
                clients[j].authed = 0;

                // Record the remote IP address of this client in the clients array
                strncpy(clients[j].remoteip, remoteip, INET6_ADDRSTRLEN);

                // If using TLS then...
                if (settings->clienttls) {
                  // ...set as OpenSSL FD and SSL_accept it
                  clients[j].ssl = SSL_new(ctx);
                  SSL_set_fd(clients[j].ssl, newfd);
                  // Set that we are pending SSL_accept()
                  clients[j].pendingsslaccept = 1;
                  // Change the socket to non-blocking so SSL_accept() can't hang (e.g. if it disconnects mid-conversation or if it's some bad client)
                  if (!fd_toggle_blocking(clients[j].fd, 0)) {
                    debugprint(DEBUG_CRIT, "fd_toggle_blocking on failed for fd %d: %s.\n", clients[j].fd, strerror(errno));
                    disconnectclient(clients[j].fd, clients, &ircdstate, settings, clientcodes);
                  }
                  // Try to SSL_accept()
                  if (openssl_accept(clients[j].fd, clients, &ircdstate, settings, clientcodes)) {
                    // It succeeded, so clear the failure message
                    opensslfailmsg[0] = '\0';
                  }
                } else {
                  // If not using TLS then cast newfd to SSL* even though it will just be the original newfd int really
                  clients[j].ssl = (SSL*)(long int)newfd;
                  // There can't be an openssl_accept failure if we're not using TLS
                  opensslfailmsg[0] = '\0';
                }

                break;
              }
            }

            // TODO - Handle the "find a free element" loop not finding a free element
            debugprint(DEBUG_FULL, "bouncer-client: new connection from %s on socket %d%s\n", remoteip, newfd, opensslfailmsg);
            // Alert other clients about the new connection
            char alertmsg[MAXDATASIZE];
            if (!snprintf(alertmsg, MAXDATASIZE, "NOTICE %s :blabouncer: new client connected from %s%s.", ircdstate.ircnick,
                          remoteip, opensslfailmsg)) {
              fprintf(stderr, "Error while preparing new client connection NOTICE!\n");
              debugprint(DEBUG_CRIT, "Error while preparing new client connection NOTICE!\n");
              alertmsg[0] = '\0';
            }
            // "except" 0 since we trust this message
            sendtoallclients(clients, alertmsg, 0, settings);
            debugprint(DEBUG_FULL, "bouncer-client: total client connections: %d\n", numclients(clients));
          }
        // If using client TLS and still pending SSL_accept() then re-try SSL_accept() (it can't be real client data yet)
        } else if (clients[clientindex].pendingsslaccept) {
          debugprint(DEBUG_FULL, "...previous connection pending SSL_accept!\n");
          // Try to SSL_accept(), not interested in return code here since openssl_accept() does the right thing.
          openssl_accept(clients[clientindex].fd, clients, &ircdstate, settings, clientcodes);
        } else {
          debugprint(DEBUG_FULL, "...previous connection!\n");

          // handle data from a client
          if ((clientnumbytes = sockread(clients[clientindex].ssl, clientbuf, MAXRCVSIZE - 1, settings->clienttls)) <= 0) {
            // got error or connection closed by client
            if (clientnumbytes == 0) {
              // connection closed
              debugprint(DEBUG_SOME, "bouncer-client: socket %d hung up\n", i);
            } else {
              debugprint(DEBUG_CRIT, "dochat(): client sockread() error fd '%d'.\n", i);
            }
            // Disconnect the client
            disconnectclient(i, clients, &ircdstate, settings, clientcodes);
            FD_CLR(i, &rfds); // remove from master set - TODO is this needed at the moment since we just add everything from *clientsockfd to fdmax to rfds
            // TODO - Handle the "remove the client" loop not finding the old fd
            debugprint(DEBUG_FULL, "bouncer-client: total client connections: %d\n", numclients(clients));
          } else {
            // we got some data from a client

            // Make sure it's not too long
            if (clientnumbytes > MAXRCVSIZE - 1) {
              debugprint(DEBUG_CRIT, "bouncer-client: too many bytes received (%d out of a max of %d).\n", clientnumbytes, MAXRCVSIZE - 1);
              // Clear clientbuf since it's overflowed
              clientbuf[0] = '\0';
              // And go back to the top of the loop
              continue;
            }

            // null terminate that baby
            clientbuf[clientnumbytes] = '\0'; // TODO make sure this can't overrun if some super long line (max bytes?) was received
            // clear up any newlines - TODO - Should we be doing this?  If not, we can stop only doing truncation checks for the server in processrawstring().
            // Only check for newlines if the string length is at least one!
            while (strlen(clientbuf) > 0 && (clientbuf[strlen(clientbuf) - 1] == '\n' || clientbuf[strlen(clientbuf) - 1] == '\r')) {
              clientbuf[strlen(clientbuf) - 1] = '\0';
            }
            debugprint(DEBUG_SOME, "BOUNCER-CLIENT RECEIVED: '%s'\n", clientbuf);

            // Try to process received string (which should contain one or more client responses/commands)
            // TODO - What if there were two client responses/commands and only one didn't need relaying?
            if (!processrawstring(server_ssl, clientbuf, SOURCE_CLIENT, clients, i, &ircdstate, channels, settings, clientcodes)) {
              fprintf(stderr, "Error: bouncer-client failed to process raw string.\n");
              debugprint(DEBUG_CRIT, "Error: bouncer-client failed to process raw string.\n");
            }
          }
        }
      }
    }
  }

  free(channels);
}

int main(int argc, char *argv[]) {
  // Structure of our various settings which are to either be read from the configuration file or set at runtime
  struct settings settings;

  // Adopt the locale of the environment for locale-dependent things like date formats later on
  setlocale(LC_ALL, "");

  // Terminate our global debug file string in case it's referenced before being read from file
  debugpath[0] = '\0';

  // Assume background/daemon mode unless specified otherwise on the command line
  settings.background = 1;
  // Set configuration file to string to blank for now
  settings.conffile[0] = '\0';

  // Check to see what was specified on the command line
  // TODO - Do better command line argument handling (no required order)
  char helptext[] = "usage: %s [-f] [-c /path/to/blabouncer.conf] (-f for foreground mode)\n";
  if (argc == 2) {
    if (strcmp(argv[1], "-f") && strcmp(argv[1], "-v") && strcmp(argv[1], "--version")) {
      fprintf(stderr, helptext, argv[0]);
      exit(1);
    } else if (!strcmp(argv[1], "-v") || !strcmp(argv[1], "--version")) {
      printf("blabouncer version %s\n", VERSION);
      printf("Copyright (C) 2019 Luke Bratch <luke@bratch.co.uk>.\n");
      printf("blabouncer is free software, and you are welcome to redistribute it under\n");
      printf("the terms of the GNU General Public License, version 3.\n");
      printf("See the file named COPYING for details.\n");
      exit(0);
    } else {
      settings.background = 0;
    }
  } else if (argc == 3) {
    if (strcmp(argv[1], "-c")) {
      fprintf(stderr, helptext, argv[0]);
      exit(1);
    } else {
      strcpy(settings.conffile, argv[2]);
    }
  } else if (argc == 4) {
    if (strcmp(argv[1], "-f") || strcmp(argv[2], "-c")) {
      fprintf(stderr, helptext, argv[0]);
      exit(1);
    } else {
      settings.background = 0;
      strcpy(settings.conffile, argv[3]);
    }
  } else if (argc > 4) {
    fprintf(stderr, helptext, argv[0]);
    exit(1);
  }

  // If a configuration file wasn't set on the command line, set it now
  if (!settings.conffile[0]) {
    snprintf(settings.conffile, PATH_MAX, "%s/.blabouncer/blabouncer.conf", getenv("HOME"));
    // Since this is the default, it might not exist yet, so let's check...
    struct stat st = {0};
    if (stat(settings.conffile, &st) == -1) {
      // ...it doesn't exist, so let's create it
      printf("Creating default configuration file '%s'.\n", settings.conffile);
      createconfigfile(settings.conffile);
    }
  }

  // Populate settings from configuration file
  // TODO - Try to share some/all of this code with the rehash() settings loading

  // What is the auto replay mode?
  if (!getconfstr("replaymode", settings.conffile, settings.replaymode)) {
    printf("main(): error getting 'replaymode' from configuration file.\n");
    exit(1);
  } else {
    if (strcmp(settings.replaymode, "none") && strcmp(settings.replaymode, "time") && strcmp(settings.replaymode, "lastspoke") &&
        strcmp(settings.replaymode, "noclients") && strcmp(settings.replaymode, "lastchange") && strcmp(settings.replaymode, "perclient")) {
      printf("main(): replaymode in configuration file must be one of \"none\", \"time\", \"lastspoke\", \"noclients\", \"lastchange\" or \"perclient\".\n");
      exit(1);
    }
  }

  // How many seconds of replay log should automatically be replayed - TODO - Can we do error checking on this?
  settings.replayseconds = getconfint("replayseconds", settings.conffile);
  // Don't worry if replaymode != "time"
  if (errno == ECONFINT && strcmp(settings.replaymode, "time") == 0) {
    printf("main(): error getting 'replayseconds' from configuration file.\n");
    exit(1);
  }

  // Should sending replay logs include a datestamp?
  settings.replaydates= getconfint("replaydates", settings.conffile);
  if (errno == ECONFINT) {
    printf("main(): error getting 'replaydates' from configuration file.\n");
    exit(1);
  }

  // What is the bouncer password?
  if (!getconfstr("password", settings.conffile, settings.password)) {
    printf("main(): error getting 'password' from configuration file.\n");
    exit(1);
  }

  // What port should the bouncer listen on?
  if (!getconfstr("clientport", settings.conffile, settings.clientport)) {
    printf("main(): error getting 'clientport' from configuration file.\n");
    exit(1);
  }

  // What are the configured nick(s)?
  int ret = getconfarr("nicks", settings.conffile, settings.ircnicks);
  if (!ret) {
    printf("main(): error getting any 'nicks' from configuration file.\n");
  } else if (ret == -1) {
    // Error reading an array line from the configuration file
    // Remove any newlines from the middle of the string so error printing works nicely
    for (size_t i = 0; i < strlen(settings.ircnicks[0]) - 1; i++) {
      if (settings.ircnicks[0][i] == '\n') {
        settings.ircnicks[0][i] = ' ';
      }
    }
    printf("main(): error getting 'nicks' from configuration file: %s", settings.ircnicks[0]);
    exit(1);
  }
  // Make sure nicks aren't too long (since getconfarr() has to use MAXDATASIZE for all string lengths)
  for (int i = 0; i < MAXCONFARR; i++) {
    if (settings.ircnicks[i][0] && strlen(settings.ircnicks[i]) > MAXNICKLENGTH) {
      printf("main(): error: specified nick '%s' is too long, maximum length is %d.\n", settings.ircnicks[i], MAXNICKLENGTH);
      exit(1);
    }
  }

  // What is the configured username?
  if (!getconfstr("username", settings.conffile, settings.ircusername)) {
    printf("main(): error getting 'username' from configuration file.\n");
    exit(1);
  }

  // What is the configured real name?
  if (!getconfstr("realname", settings.conffile, settings.ircrealname)) {
    printf("main(): error getting 'realname' from configuration file.\n");
    exit(1);
  }

  // What, if anything, are the configured auto channels?
  ret = getconfarr("channels", settings.conffile, settings.autochannels);
  if (!ret) {
    for (int i = 0; i < MAXCONFARR; i++) {
      settings.autochannels[i][0] = '\0';
    }
  } else if (ret == -1) {
    // Remove any newlines from the middle of the string so error printing works nicely
    for (size_t i = 0; i < strlen(settings.autochannels[0]) - 1; i++) {
      if (settings.autochannels[0][i] == '\n') {
        settings.autochannels[0][i] = ' ';
      }
    }
    printf("main(): error getting 'autochannels' from configuration file: %s", settings.autochannels[0]);
    exit(1);
  }

  // Make sure channel/key pairs aren't too long (since getconfarr() has to use MAXDATASIZE for all string lengths)
  for (int i = 0; i < MAXCONFARR; i++) {
    // +1 for the space between channel name and keys
    if (settings.autochannels[i][0] && strlen(settings.autochannels[i]) > MAXCHANLENGTH + 1 + MAXCHANKEYLEN) {
      printf("main(): error: specified channel name/key pair '%s' is too long, maximum lengths are %d (name) and %d (key).\n", settings.autochannels[i], MAXCHANLENGTH, MAXCHANKEYLEN);
      exit(1);
    }
  }

  // What is the real IRC server address?
  if (!getconfstr("ircserver", settings.conffile, settings.ircserver)) {
    printf("main(): error getting 'ircserver' from configuration file.\n");
    exit(1);
  }

  // What is the real IRC server port?
  if (!getconfstr("ircserverport", settings.conffile, settings.ircserverport)) {
    printf("main(): error getting 'ircserverport' from configuration file.\n");
    exit(1);
  }

  // What is the real IRC server password, if any?
  if (!getconfstr("ircserverpassword", settings.conffile, settings.ircserverpassword)) {
    settings.ircserverpassword[0] = '\0';
  }

  // What are the connect commands, if any?
  ret = getconfarr("connectcommands", settings.conffile, settings.connectcommands);
  if (!ret) {
    for (int i = 0; i < MAXCONFARR; i++) {
      settings.connectcommands[i][0] = '\0';
    }
  } else if (ret == -1) {
    // Remove any newlines from the middle of the string so error printing works nicely
    for (size_t i = 0; i < strlen(settings.connectcommands[0]) - 1; i++) {
      if (settings.connectcommands[0][i] == '\n') {
        settings.connectcommands[0][i] = ' ';
      }
    }
    printf("main(): error getting 'commandcommands' from configuration file: %s", settings.connectcommands[0]);
    exit(1);
  }

  // Is the base directory set?  If not, use the default.
  if (!getconfstr("basedir", settings.conffile, settings.basedir)) {
    snprintf(settings.basedir, PATH_MAX, "%s/.blabouncer/", getenv("HOME"));
  }

  // Should the bouncer use TLS for the IRC server?
  settings.servertls = getconfint("servertls", settings.conffile);
  if (errno == ECONFINT) {
    printf("main(): error getting 'servertls' from configuration file.\n");
    exit(1);
  }

  // Should the bouncer use TLS for clients?
  settings.clienttls = getconfint("clienttls", settings.conffile);
  if (errno == ECONFINT) {
    printf("main(): error getting 'clienttls' from configuration file.\n");
    exit(1);
  }

  // If so, load the certificates
  if (settings.clienttls) {
    // What is the certificate file path?
    if (!getconfstr("certfile", settings.conffile, settings.certfile)) {
      // If none provided, set to default
      if (!snprintf(settings.certfile, PATH_MAX, "%s/cert.pem", settings.basedir)) {
        fprintf(stderr, "Error while preparing default certfile location!\n");
        exit(1);
      }
    }

    // What is the certificate key file path?
    if (!getconfstr("keyfile", settings.conffile, settings.keyfile)) {
      // If none provided, set to default
      if (!snprintf(settings.keyfile, PATH_MAX, "%s/key.pem", settings.basedir)) {
        fprintf(stderr, "Error while preparing default certfile location!\n");
        exit(1);
      }
    }
  }

  // Make sure the base directory exists
  struct stat st = {0};
  if (stat(settings.basedir, &st) == -1) {
    if (mkdir(settings.basedir, 0700)) {
      printf("Error creating base directory '%s'.\n", settings.basedir);
      exit(1);
    } else {
      printf("Created base directory '%s'.\n", settings.basedir);
    }
  }

  // Is logging enabled?
  settings.logging = getconfint("logging", settings.conffile);
  if (errno == ECONFINT) {
    printf("main(): error getting 'logging' from configuration file.\n");
    exit(1);
  }

  // Is replay logging enabled?
  settings.replaylogging = getconfint("replaylogging", settings.conffile);
  if (errno == ECONFINT) {
    printf("main(): error getting 'replaylogging' from configuration file.\n");
    exit(1);
  }

  // How many debug logs should we keep?
  settings.debugkeep = getconfint("debugkeep", settings.conffile);
  if (errno == ECONFINT) {
    printf("main(): error getting 'debugkeep' from configuration file.\n");
    exit(1);
  }

  // Is debugging enabled?
  debug = getconfint("debug", settings.conffile);
  if (errno == ECONFINT) {
    printf("main(): error getting 'debug' from configuration file.\n");
    exit(1);
  }
  if (!snprintf(debugpath, PATH_MAX, "%s/debug/debug.txt", settings.basedir)) {
    fprintf(stderr, "Error while preparing debug path location!\n");
    exit(1);
  }
  // debugfile goes in its own directory
  char debugdir[PATH_MAX];
  if (!snprintf(debugdir, PATH_MAX, "%s/debug/", settings.basedir)) {
    fprintf(stderr, "Error while preparing debug path location!\n");
    exit(1);
  }

  // Make sure the debug directory exists
  if (stat(debugdir, &st) == -1) {
    if (mkdir(debugdir, 0700)) {
      printf("Error creating debug directory '%s'.\n", debugdir);
      exit(1);
    } else {
      printf("Created debug directory '%s'.\n", debugdir);
    }
  }
  // Prepare the debug file
  // (Keep settings.debugkeep number of debug files around for past debugging)
  if (settings.debugkeep > 0) {
    // Check each possible debug file and rename it to one higher
    char tmppath[PATH_MAX];
    char tmppathnew[PATH_MAX];
    // Delete or rename numbered debug files first
    for (int i = settings.debugkeep; i > 0; i--) {
      if (!snprintf(tmppath, PATH_MAX - 1, "%s.%d", debugpath, i)) {
        fprintf(stderr, "Error while preparing to check old debug files!\n");
        exit(1);
      }
      if (!access(tmppath, F_OK)) {
        if (i == settings.debugkeep) {
          if (remove(tmppath)) {
            printf("error deleting old debug file '%s'.\n", tmppath);
            exit(1);
          }
        } else {
          if (!snprintf(tmppathnew, PATH_MAX - 1, "%s.%d", debugpath, i + 1)) {
            fprintf(stderr, "Error while preparing to rename old debug file!\n");
            exit(1);
          }
          if (rename(tmppath, tmppathnew)) {
            printf("error renaming old debug file '%s' to '%s'.\n", tmppath, tmppathnew);
            exit(1);
          }
        }
      }
    }
    // Rename the previous debug file next
    if (!access(debugpath, F_OK)) {
      if (rename(debugpath, tmppath)) {
        printf("error renaming old debug file '%s' to '%s'.\n", debugpath, tmppath);
        exit(1);
      }
    }
  }

  // Write a friendly debug message to file (even if debug to file is disabled)
  // Prepare a friendly time string
  time_t rawtime;
  struct tm * timeinfo;
  time(&rawtime);
  timeinfo = localtime(&rawtime);
  // Strip the trailing newline
  char timestr[MAXCHAR];
  snprintf(timestr, MAXCHAR, "%s", asctime(timeinfo));
  timestr[strlen(timestr) - 1] = '\0';
  // Note the old debug setting
  int debugold = debug;
  // Temporarily enable debugging to file
  debug = 1;
  // Print it
  debugprint(DEBUG_CRIT, "blabouncer started at %s, debugging is set to %d.\n", timestr, debugold);
  // Set debugging back to whatever it was
  debug = debugold;

  debugprint(DEBUG_SOME, "Using configuration file '%s'.\n", settings.conffile);

  // Unless specified otherwise on the command line, fork to background
  if (settings.background) {
    pid_t pid, sid;
    // Fork from parent
    if ((pid = fork()) < 0) {
      printf("Couldn't fork, exiting.\n");
      debugprint(DEBUG_CRIT, "Couldn't fork, exiting.\n");
      exit(1);
    }
    // Exit parent (pid will be > 0 for the child)
    if (pid > 0) {
      exit(0);
    }
    umask(0);
    // Let child become process group leader
    if ((sid = setsid()) < 0) {
      printf("Couldn't setsid() child, exiting.\n");
      debugprint(DEBUG_CRIT, "Couldn't setsid() child, exiting.\n");
      exit(1);
    }
    // In case our cwd changes
    chdir("/");
    // TODO - close() STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO here, might need to change fd number
    // logic/assumptions elsewhere (such as fd 3 being server, fd 4 being clients, etc.)
  }

  // TODO: see if any of this can be shared (i.e. 1. avoid code duplication, and 2. see if variables can be shared between client/server sockets)

  // I will try to keep to the notation of "server" meaning the real IRCd, "bouncer" meaning the bouncer, and "client" meaning the real IRC client

  // Create server socket
  int serversockfd;
  if ((serversockfd = createserversocket(settings.ircserver, settings.ircserverport)) == -1) {
    fprintf(stderr, "main(): Couldn't connect to server, exiting.\n");
    debugprint(DEBUG_CRIT, "main(): Couldn't connect to server, exiting.\n");
    exit(EXIT_FAILURE);
  }

  // Create client socket (after server so we can use its fd number later as fdmax)
  int clientsockfd = createclientsocket(settings.clientport);

  dochat(&serversockfd, &clientsockfd, &settings);

  printf("dochat() complete, closing socket...\n");

  close(serversockfd);

  return 0;
}