/*
 * 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/>.
*/

#include "functions.h"

// Global debug control
extern int debug;
extern char debugpath[PATH_MAX];
extern int background;

// Internal function just to replace nick in server greeting strings
// (as in ":servername 00x oldnick :Blablabla" -> ":servername 00x newnick :Blablabla")
void updategreetingnick(char *greeting, char *greetingnum, char *newnick, char *oldnick) {
  debugprint(DEBUG_FULL, "updategreetingnick(): '%s' '%s' '%s' '%s'.\n", greeting, greetingnum, newnick, oldnick);

  // Find the position of the old nick in the current greeting
  char searchstr[MAXDATASIZE];
  snprintf(searchstr, MAXDATASIZE, " %s %s :", greetingnum, oldnick);
  char *ret;
  ret = strstr(greeting, searchstr);

  // If ret not found, try again without the colon (e.g. for greeting 004)
  if (ret == NULL) {
    snprintf(searchstr, MAXDATASIZE, " %s %s ", greetingnum, oldnick);
    ret = strstr(greeting, searchstr);
  }

  // Perhaps the new nick is already present (seen for instance when connecting to another bouncer like Miau)
  if (ret == NULL) {
    snprintf(searchstr, MAXDATASIZE, " %s %s ", greetingnum, newnick);
    ret = strstr(greeting, searchstr);
    if (ret != NULL) {
      debugprint(DEBUG_FULL, "updategreetingnick(): newnick is already present, returning.\n");
      return;
    }
  }

  // If ret *still* not found, abandon ship
  if (ret == NULL) {
    debugprint(DEBUG_CRIT, "Error updating greeting string, substring not found.  Exiting!\n");
    printf("Error updating greeting string, substring not found.  Exiting!\n");
    exit(1);
  }

  int pos = ret - greeting + 5; // +5 for " 001 "

  // Copy the start of the old greeting into a new string
  char greetingtmp[MAXDATASIZE];
  strncpy(greetingtmp, greeting, pos);
  // Terminate it
  greetingtmp[pos] = '\0';

  // Now smash everything (start of old greeting + new nick + remainder of old greeting)
  // together into the new greeting, put in a new temporary string
  char greetingtmp2[MAXDATASIZE];
  if (!snprintf(greetingtmp2, MAXDATASIZE, "%s%s %s", greetingtmp, newnick, greeting + pos + strlen(oldnick) + 1)) {
    fprintf(stderr, "Error while preparing new greeting string!\n");
    debugprint(DEBUG_CRIT, "Error while preparing new greeting string!\n");
    exit(1);
  }

  // And finally copy back to source string
  strcpy(greeting, greetingtmp2);

  debugprint(DEBUG_FULL, "updategreetingnick(): Built new greeting '%s' '%s', length '%ld'.\n", greetingnum, greeting, strlen(greeting));
}

// Write debug string to file.
// Debug level is provided by level, set to one of DEBUG_CRIT, DEBUG_SOME or DEBUG_FULL.
// Debug is only written if the global int "debug" is greater than or equal to the level.
void debugprint(int level, char *format, ...) {
  // Stop here if the user's debug level is less than the level of the current message
  if (debug < level) return;

  if (strlen(debugpath) < 1) {
    // debugpath isn't set, we can't do anything here
    return;
  }

  va_list args;
  va_start(args, format);

  FILE *fp;
  int bytes = 0;
  fp = fopen(debugpath, "a");

  if (fp == NULL) {
    printf("Couldn't open debugpath '%s'!\n", debugpath);
    return;
  }

  // Prepend a timestamp to each line
  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';
  if ((bytes = fprintf(fp, "%s: ", timestr)) < 0) {
    debugprint(DEBUG_CRIT, "error: could not write timestamp to debug file.\n"); // TODO - This might not be useful if we can't write
  }

  if ((bytes = vfprintf(fp, format, args)) < 0) {
    debugprint(DEBUG_CRIT, "error: could not write to debug file.\n"); // TODO - This might not be useful if we can't write
  }

  // Make sure the finished log line ended with a trailing newline
  if (format[strlen(format) - 1] != '\n') {
    if ((bytes = fprintf(fp, "%c", '\n')) < 0) {
      debugprint(DEBUG_CRIT, "error: could not write trailing newline to debug file.\n"); // TODO - This might not be useful if we can't write
    }
  }

  fclose(fp);

  va_end(args);
}

// Get stdin line with buffer overrun protection.
// Returns OK, NO_INPUT, or TOO_LONG as appropriate.
int getstdin(char *prompt, char *buff, size_t sz) {
  if (prompt != NULL) {
    debugprint(DEBUG_FULL, "getstdin(): '%s' (len %d), '%s' (len %d), size %zu.\n", prompt, strlen(prompt), buff, strlen(buff), sz);
  } else {
    debugprint(DEBUG_FULL, "getstdin(): '<null>' (len <null>), '%s' (len %d), size %zu.\n", buff, strlen(buff), sz);
  }

  int ch, extra;

  // Print optional prompt
  if (prompt != NULL) {
    printf ("%s", prompt);
    fflush (stdout);
  }

  // Get the intput from stdin
  if (fgets (buff, sz, stdin) == NULL) {
    if (feof(stdin)) {
      debugprint(DEBUG_FULL, "getstdin(): Clearing EOF indicator on stdin.\n");
      clearerr(stdin);
    }

    return NO_INPUT;
  }

  // If it was too long, there'll be no newline. In that case, we flush
  // to end of line so that excess doesn't affect the next call.
  if (buff[strlen(buff) - 1] != '\n') { // strlen of the actually entered line, not the original array size
    extra = 0;
    while (((ch = getchar()) != '\n') && (ch != EOF)) {
      extra = 1;
    }
    return (extra == 1) ? TOO_LONG : OK;
  }

  // Otherwise remove newline and give string back to caller.
  buff[strlen(buff) - 1] = '\0';
  return OK;
}

// Append CR-LF to the end of a string (after cleaning up any existing trailing CR or LF)
void appendcrlf(char *string) {
  // Make sure it doesn't already end with CR or LF
  // (But only if string is at least two characters long already)
  if (strlen(string) >= 2) {
    while (string[strlen(string) - 1] == '\r' || string[strlen(string) - 1] == '\n') {
      string[strlen(string) - 1] = '\0';
    }
  }

  int startlen = strlen(string);
  string[startlen] = '\r'; // Add CR
  string[startlen + 1] = '\n'; // Add LF
  string[startlen + 2] = '\0'; // Finish with null terminator
}

// Remove leading colon ':' which is the starting character of a prefix in an IRC message
// If no leading colon present, string is left unchanged.
// "debug" is used to determine whether stripprefix() should produce all debug output,
// used as debug output with stripprefix() can be particularly noisy.
void stripprefix(char *string, int debug) {
  // Make a copy to work with
  char string2[strlen(string)];

  if (debug) {
    debugprint(DEBUG_FULL, "stripprefix(): starting with '%s', strlen: %zd.\n", string, strlen(string));
  }

  // Don't bother if this isn't a prefix with a leading ':'
  if (string[0] != ':') {
    debugprint(DEBUG_FULL, "stripprefix(): no leading ':', returning.\n");
    return;
  }

  // Copy the old string into a new one, but...
  for (size_t i = 1; i < strlen(string); i++) {
    string2[i - 1] = string[i];
  }

  // Copy result back to original string
  strncpy(string, string2, strlen(string) - 1);

  // Finish with null terminator
  string[strlen(string) - 1] = '\0';

  if (debug) {
    debugprint(DEBUG_FULL, "stripprefix(): finishing with '%s', strlen: %zd.\n", string, strlen(string));
  }
}

// Extract final parameter from IRC message, removing the leading colon ':'
// e.g. given either of these strings:
// ":irc.tghost.co.uk 332 blabounce #test :foo:bar topic!"
// ":nick!user@fe80:1:2:3:5:6:7:8 TOPIC #test :foo:bar topic!"
// We want to end up with:
// "foo:bar topic!"
void extractfinalparameter(char *string) {
  // The method used is to look for the first space (" ") followed by a colon (":")
  // and take everything after that colon as the final parameter.

  // Make a copy to work with
  char string2[strlen(string)];

  // Position of final parameter's leading colon
  int colonpos = -1;

  debugprint(DEBUG_FULL, "extractfinalparameter(): starting with '%s', strlen: %zd.\n", string, strlen(string));

  // Strip the colon at position 0 if there is one
  stripprefix(string, 1);

  // Look for spaces...
  for (size_t i = 0; i < strlen(string); i++) {
    if (string[i] == ' ') {
      debugprint(DEBUG_FULL, "extractfinalparameter(): found space at position %zd!  Checking for colon...\n", i);
      // ...and check if the next character is a colon
      if (string[i + 1] == ':')  {
        colonpos = i + 1;
        break;
      }
    }
  }

  if (colonpos == -1) {
    debugprint(DEBUG_SOME, "extractfinalparameter(): no space followed by a colon found, returning.\n");
    return;
  }

  // Build a new string starting from the next position after the colon
  int counter = 0;
  for (size_t i = colonpos + 1; i < strlen(string); i++) {
    string2[counter] = string[i];
    counter++;
  }

  // Copy result back to original string
  strncpy(string, string2, counter);

  // Finish with null terminator
  string[counter] = '\0';

  debugprint(DEBUG_FULL, "extractfinalparameter(): finishing with '%s', strlen: %zd.\n", string, strlen(string));
}

// Extract the IRC nick from a prefix
// e.g. given this string:
// ":foo!bar@baz"
// We want to end up with:
// "foo"
// "debug" is used to determine whether extractnickfromprefix() should produce all debug output,
// used as debug output with extractnickfromprefix() can be particularly noisy.
void extractnickfromprefix(char *string, int debug) {
  // Position of bang
  int bangpos = -1;

  if (debug) {
    debugprint(DEBUG_FULL, "extractnickfromprefix(): starting with '%s', strlen: %zd.\n", string, strlen(string));
  }

  // Strip the colon at position 0 if there is one, debugging enabled/disabled based on "debug" pass to us
  stripprefix(string, debug);

  // Find the bang
  for (size_t i = 0; i < strlen(string); i++) {
    if (string[i] == '!') {
      if (debug) {
        debugprint(DEBUG_FULL, "extractnickfromprefix(): found bang at position %zd!\n", i);
      }
      bangpos = i;
      break;
    }
  }

  if (bangpos == -1) {
    debugprint(DEBUG_FULL, "extractnickfromprefix(): no bang found, returning...\n");
    return;
  }

  // Terminate the string at whatever position we found the bang
  string[bangpos] = '\0';

  if (debug) {
    debugprint(DEBUG_FULL, "extractnickfromprefix(): finishing with '%s', strlen: %zd.\n", string, strlen(string));
  }
}

// Update an existing nickuserhost string with a new nick
void updatenickuserhost(char *nickuserhost, char *nick) {
  debugprint(DEBUG_FULL, "updatenickuserhost(): updating '%s' with '%s'.\n", nickuserhost, nick);

  // Position of bang
  int bangpos = -1;

  // Find the bang
  for (size_t i = 0; i < strlen(nickuserhost); i++) {
    if (nickuserhost[i] == '!') {
      debugprint(DEBUG_FULL, "updatenickuserhost(): found bang at position %ld!\n", i);
      bangpos = i;
      break;
    }
  }

  // No bang found...
  if (bangpos == -1) {
    // ...assume the old nickuserhost was just the nick (e.g. if the server's greeting
    // 001 ended with nick rather than nick!user@host) then just update it directly
    debugprint(DEBUG_FULL, "updatenickuserhost(): no bang found in existing nickuserhost, assuming a nick only nickuserhost\n");
    strcpy(nickuserhost, nick);
    debugprint(DEBUG_FULL, "updatenickuserhost(): new (nick only) nickuserhost '%s', length '%ld'.\n", nickuserhost, strlen(nickuserhost));
    return;
  }

  // Make a new string combining the new nick and the old nickuserhost + the offset of the bang
  char newstr[MAXDATASIZE];
  snprintf(newstr, MAXDATASIZE, "%s%s", nick, nickuserhost + bangpos);
  newstr[strlen(nickuserhost) - bangpos + strlen(nick)] = '\0';

  // Copy back to source string
  strcpy(nickuserhost, newstr);

  debugprint(DEBUG_FULL, "updatenickuserhost(): new nickuserhost '%s', length '%ld'.\n", nickuserhost, strlen(nickuserhost));
}

// Update existing greeting strings with a new nickuserhost and new nick
void updategreetings(char *greeting001, char *greeting002, char *greeting003, char *greeting004, char *greeting005a, char *greeting005b,
                     char *greeting005c, char *newnickuserhost, char *oldnickuserhost, char *newnick, char *oldnick) {
  debugprint(DEBUG_FULL, "updategreetings(): updating greetings with new nickuserhost '%s' and nick '%s'.\n", newnickuserhost, newnick);
  debugprint(DEBUG_FULL, "updategreetings(): existing greeting001: '%s'.\n", greeting001);
  debugprint(DEBUG_FULL, "updategreetings(): existing greeting002: '%s'.\n", greeting002);
  debugprint(DEBUG_FULL, "updategreetings(): existing greeting003: '%s'.\n", greeting003);
  debugprint(DEBUG_FULL, "updategreetings(): existing greeting004: '%s'.\n", greeting004);
  debugprint(DEBUG_FULL, "updategreetings(): existing greeting005a: '%s'.\n", greeting005a);
  debugprint(DEBUG_FULL, "updategreetings(): existing greeting005b: '%s'.\n", greeting005b);
  debugprint(DEBUG_FULL, "updategreetings(): existing greeting005c: '%s'.\n", greeting005c);

  // nickuserhost and greeting001's final component first
  // (final component as in ":servername 001 nick :Blablabla final!com@ponent"

  // Make copies of the nickuserhosts to work with
  char newnickuserhostcpy[MAXDATASIZE];
  strcpy(newnickuserhostcpy, newnickuserhost);
  stripprefix(newnickuserhostcpy, 1);
  char oldnickuserhostcpy[MAXDATASIZE];
  strcpy(oldnickuserhostcpy, oldnickuserhost);
  stripprefix(oldnickuserhostcpy, 1);

  // Find the position of the old nickuserhost in the current greeting 001
  char *ret;
  ret = strstr(greeting001, oldnickuserhostcpy);
  int pos = ret - greeting001;

  // Ensure that was the last occurrence in the greeting
  debugprint(DEBUG_FULL, "updategreetings(): entering last occurrence check loop...\n");
  while (ret) {
    ret = strstr(greeting001 + pos + strlen(oldnickuserhostcpy), oldnickuserhostcpy);
    if (!ret) {
      break;
    }
    pos = ret - greeting001;
  }

  // Terminate greeting001 where the nickuserhost begins
  greeting001[pos] = '\0';

  // Stick the new nickuserhost in place in a temporary greeting 001 string
  char greetingtmp[MAXDATASIZE];
  snprintf(greetingtmp, MAXDATASIZE, "%s%s", greeting001, newnickuserhostcpy);

  // Terminate it
  greetingtmp[pos + strlen(newnickuserhostcpy)] = '\0';

  // Copy back to source greeting 001
  strcpy(greeting001, greetingtmp);

  debugprint(DEBUG_FULL, "updategreetings(): new greeting 001 '%s', length '%ld'.\n", greeting001, strlen(greeting001));

  // Get the new nick

  char newnickcpy[MAXDATASIZE];
  strcpy(newnickcpy, newnick);
  extractnickfromprefix(newnickcpy, 1);

  // greeting nicks next
  // (as in ":servername 00x oldnick :Blablabla" -> ":servername 00x newnick :Blablabla")
  updategreetingnick(greeting001, "001", newnickcpy, oldnick);
  updategreetingnick(greeting002, "002", newnickcpy, oldnick);
  updategreetingnick(greeting003, "003", newnickcpy, oldnick);
  updategreetingnick(greeting004, "004", newnickcpy, oldnick);
  if (greeting005a[0]) {
    updategreetingnick(greeting005a, "005", newnickcpy, oldnick);
  }
  if (greeting005b[0]) {
    updategreetingnick(greeting005b, "005", newnickcpy, oldnick);
  }
  if (greeting005c[0]) {
    updategreetingnick(greeting005c, "005", newnickcpy, oldnick);
  }
}

// Return index of requested client FD within the clients array.
// Returns 0 or more on success, or -1 on failure.
// TODO - Use this wherever we are calculating the position (various places) instead of
// duplicating code.
int arrindex(struct client *clients, int clientfd) {
  // Make sure the client fd requested is >0 (less than 0 is nonsense, 0 is almost certainly standard input)
  if (clientfd < 1) {
    debugprint(DEBUG_CRIT, "arrindex(): error: requested client fd '%d' is less than 1, returning -1.\n", clientfd);
    return -1;
  }

  // Find the client in the clients array
  for (int i = 0; i < MAXCLIENTS; i++) {
    if (clients[i].fd == clientfd) {
      return i;
    }
  }

  // Something went wrong, we didn't find it
  debugprint(DEBUG_CRIT, "arrindex(): error: got to MAXCLIENTS (%d) but didn't find clientfd '%d' in clients struct.\n", MAXCLIENTS, clientfd);
  return -1;
}

// Send whatever string to a specific client by providing the FD
// If "bypass" == 1 then permit sending to client even if unauthenticated (for instance for a CAP LS response)
int sendtoclient(int fd, char *strsrc, struct client *clients, struct settings *settings, int bypass) {
  if (strlen(strsrc) >= MAXDATASIZE) {
    // String is too long!
    debugprint(DEBUG_CRIT, "sendtoclient(): strsrc '%s' was too long (%d out of a max of %d characters).\n", strsrc, strlen(strsrc), MAXDATASIZE - 1);
    return 0;
  } else if (strlen(strsrc) >= MAXDATASIZE - 2 && strsrc[strlen(strsrc) - 1] != '\r' && strsrc[strlen(strsrc)] != '\n') {
    // Ensure string can fit CRLF at the end if it doesn't already have it
    debugprint(DEBUG_CRIT, "sendtoclient(): non-CRLF message too long to append CRLF to.\n");
    return 0;
  }

  // Copy to new string for passing to appendcrlf() to avoid overrun in appendcrlf()
  char str[MAXDATASIZE];
  strcpy(str, strsrc);

  appendcrlf(str); // Do this just before sending so callers don't need to worry about it

  int i = 0;
  // Find the client in the clients array and make sure they are authenticated
  for (i = 0; i < MAXCLIENTS; i++) {
    if (clients[i].fd == fd) {
      // Found client in array, check authentication status
      if (!clients[i].authed && !bypass) {
        debugprint(DEBUG_SOME, "sendtoclient(): skipping unauthenticated client with fd %d.\n", clients[i].fd);
        return 0;
      }
      // Break when we get to the correct fd, "i" is now the position in the clients array of our client
      break;
    }
  }
  if (i == MAXCLIENTS ) {
    debugprint(DEBUG_CRIT, "error: sendtoclient() client fd %d not found in clients array, returning 0.\n", fd);
    return 0;
  }

  debugprint(DEBUG_SOME, "sendtoclient(): sending \"%s\" (length %zd) to client with fd %d.\n", str, strlen(str), fd);
  if (socksend(clients[i].ssl, str, strlen(str), settings->clienttls) < 0) {
    debugprint(DEBUG_CRIT, "error: sendtoclient() socksend() error sending to client with fd '%d', errno '%d'.\n", fd, errno);
    return 0;
  }

  return 1;
}

// Relay/send message to all clients (optionally except one)
// "except" is used to send to all clients _except_ the fd provided (except = 0 (EXCEPT_NONE) avoids this, i.e. sends to all)
// "except" is really the "sourcefd" and is also used as part of the authentication check - this is messy and they should perhaps be two separate arguments.
int sendtoallclients(struct client *clients, char *strsrc, int except, struct settings *settings) {
  char *sendertype;

  if (strlen(strsrc) >= MAXDATASIZE) {
    // String is too long!
    debugprint(DEBUG_CRIT, "sendtoallclients(): strsrc '%s' was too long (%d out of a max of %d characters).\n", strsrc, strlen(strsrc), MAXDATASIZE - 1);
    return 0;
  } else if (strlen(strsrc) >= MAXDATASIZE - 2 && strsrc[strlen(strsrc) - 1] != '\r' && strsrc[strlen(strsrc)] != '\n') {
    // Ensure string can fit CRLF at the end if it doesn't already have it
    debugprint(DEBUG_CRIT, "sendtoallclients(): non-CRLF message too long to append CRLF to.\n");
    return 0;
  }

  // Copy to new string for passing to appendcrlf() to avoid overrun in appendcrlf()
  char str[MAXDATASIZE];
  strcpy(str, strsrc);

  appendcrlf(str); // Do this just before sending so callers don't need to worry about it

  // Decide what sort of text to prefix the debug output with
  // At the moment if non-zero "except" is specified then it must be a message from a bouncer client
  // and if "except" is zero then it must be a message from the real IRC server
  if (except) {
    sendertype = "bouncer-client";
  } else {
    sendertype = "bouncer-server";
  }

  // Find the sending client in the clients array and make sure they are authenticated
  for (int i = 0; i < MAXCLIENTS; i++) {
    // Trust clientfd of 0, only we can set that ourselves
    if (!except) {
      debugprint(DEBUG_FULL, "sendtoallclients(): trusting clientfd of 0.\n");
      break;
    }
    if (clients[i].fd == except) {
      // Found client in array, check authentication status
      if (!clients[i].authed) {
        debugprint(DEBUG_SOME, "sendtoallclients(): skipping unauthenticated client with fd %d.\n", clients[i].fd);
        return 0;
      }
    }
  }

  // Relay/send to all clients...
  for (int i = 0; i < MAXCLIENTS; i++) {
    // Skip the current client if "except" non-zero (no need to send back to itself)
    if (clients[i].fd == except) {
      continue;
    }
    // ...but only if they are connected...
    if (clients[i].fd > 0) {
      // ...and authenticated
      if (!clients[i].authed) {
        debugprint(DEBUG_SOME, "sendtoallclients(): skipping unauthenticated client with fd %d.\n", clients[i].fd);
        continue;
      }
      debugprint(DEBUG_SOME, "sendtoallclients(): %s: sending '%s' to client with fd %d.\n", sendertype, str, clients[i].fd);
      if (socksend(clients[i].ssl, str, strlen(str), settings->clienttls) < 0) {
        debugprint(DEBUG_CRIT, "error: sendtoallclients() socksend() error sending to client with fd '%d', errno '%d'.\n", clients[i].fd, errno);
      }
    }
  }

  return 1;
}

// Send whatever string to the real IRC server
// Client FD and arrays needed to make sure anything relayed from a client is from an authenticated client.
// clientfd of "0" means trusted, used when we are sending things ourselves that weren't relayed
// from a real client.
// Returns 1 on success or 0 on failure. - TODO - Make callers care about this.
int sendtoserver(SSL *server_ssl, char *strsrc, int str_len, int clientfd, struct client *clients, struct settings *settings) {
  if (strlen(strsrc) >= MAXDATASIZE) {
    // String is too long!
    debugprint(DEBUG_CRIT, "sendtoserver(): strsrc '%s' was too long (%d out of a max of %d characters).\n", strsrc, strlen(strsrc), MAXDATASIZE - 1);
    return 0;
  } else if (strlen(strsrc) >= MAXDATASIZE - 2 && strsrc[strlen(strsrc) - 1] != '\r' && strsrc[strlen(strsrc)] != '\n') {
    // Ensure string can fit CRLF at the end if it doesn't already have it
    debugprint(DEBUG_CRIT, "sendtoserver(): non-CRLF message too long to append CRLF to.\n");
    return 0;
  }

  // Copy to new string for passing to appendcrlf() to avoid overrun in appendcrlf()
  char str[MAXDATASIZE];
  strcpy(str, strsrc);

  appendcrlf(str); // Do this just before sending so callers don't need to worry about it
  str_len = strlen(str); // Recalculate str_len in case it changed (TODO: so do we even need to pass it to this function?)

  // Find the sending client in the clients array and make sure they are authenticated
  for (int i = 0; i < MAXCLIENTS; i++) {
    // Trust clientfd of 0, only we can set that ourselves
    if (!clientfd) {
      debugprint(DEBUG_FULL, "sendtoserver(): trusting clientfd of 0.\n");
      break;
    }
    if (clients[i].fd == clientfd) {
      // Found client in array, check authentication status
      if (!clients[i].authed) {
        debugprint(DEBUG_SOME, "sendtoserver(): skipping unauthenticated client with fd %d.\n", clients[i].fd);
        return 0;
      }
    }
  }

  debugprint(DEBUG_SOME, "sendtoserver(): sending '%s' to IRC server (length %d).\n", str, str_len);
  if (socksend(server_ssl, str, str_len, settings->servertls) < 0) {
    debugprint(DEBUG_CRIT, "error: sendtoserver() socksend() error sending to server, errno '%d'.\n", clientfd, errno);
    return 0;
  }

  return 1;
}

// Disconnect the client fd "fd" by close()ing it and remove
// it from the array of clients.
// Also set its authentication and registration statuses to 0.
// Also set the pending statuses to 0
int disconnectclient(int fd, struct client *clients, struct ircdstate *ircdstate, struct settings *settings, struct clientcodes *clientcodes) {
  // Index of client fd in clients array for use later
  int clientindex = arrindex(clients, fd);
  if (clientindex < 0) {
    debugprint(DEBUG_CRIT, "disconnectclient(): error: arrindex() returned '%d', exiting!\n", clientindex);
    exit(1);
  }
  debugprint(DEBUG_SOME, "disconnectclient(): disconnecting client %s with fd '%d'\n", clients[clientindex].remoteip, fd);

  // Alert other clients about the disconnection (don't send yet, we haven't removed from the clients array yet)
  char alertmsg[MAXDATASIZE];
  if (!snprintf(alertmsg, MAXDATASIZE, "NOTICE %s :blabouncer: client %s has disconnected.", ircdstate->ircnick, clients[clientindex].remoteip)) {
    fprintf(stderr, "Error while preparing authentication failure NOTICE!\n");
    debugprint(DEBUG_CRIT, "Error while preparing authentication failure NOTICE!\n");
    alertmsg[0] = '\0';
  }
  // Remove the client from the clients array
  for (int i = 0; i < MAXCLIENTS; i++) {
    if (clients[i].fd == fd) {
      debugprint(DEBUG_FULL, "found and clearing fd %d from clients[%d]\n", fd, i);
      // If the client was registered, record the time of the last client disconnect
      if (clients[i].registered) {
        ircdstate->clientchangetime = time(NULL);
        // And set their client code, if any, to last disconnecting at this time.
        if (clients[i].clientcode[0]) {
          setclientcodetime(clients[i].clientcode, clientcodes);
        }
      }
      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';
      if (settings->clienttls) {
        // Finish up with OpenSSL if using client TLS
        SSL_free(clients[i].ssl);
      }
      // Close the socket
      close(fd);
      // Now clients array is cleared, inform all other clients (source "0" since we trust this message)
      sendtoallclients(clients, alertmsg, 0, settings);
      // If there are now no clients connected, record the time
      if (numclients(clients) == 0) {
        ircdstate->clientsnonetime = time(NULL);
      }
      return 1;
    }
  }

  // If we got here, we didn't find and clear the client
  // TODO - Do something with a failed return code
  return 0;
}

int createchannel(struct channel *channels, struct ircdstate *ircdstate, char *name, char *topic, char *topicwho, char *topicwhen) {
  debugprint(DEBUG_FULL, "createchannel(): given '%s', '%s', '%s', and '%s'.\n", name, topic, topicwho, topicwhen);

  // Make sure the channel doesn't already exist
  for (int i = 0; i < ircdstate->maxchannelcount; i++) {
    if (strlen(channels[i].name) == strlen(name) && strncmp(channels[i].name, name, strlen(name)) == 0) {
      // Note that this may be happening because we just reconnected
      debugprint(DEBUG_CRIT, "error: createchannel(): channel name already exists.\n");
      return 0;
      break;
    }
  }

  // Make sure we aren't in too many channels already
  if (getchannelcount(channels, ircdstate->maxchannelcount) >= MAXCHANNELS - 1) {
    debugprint(DEBUG_CRIT, "error: createchannel(): already in too many channels (MAXCHANNELS = %d!\n", MAXCHANNELS);
    return 0;
  }

  int arrslot = -1;

  // See if there's a free slot in the already used section of the channels array
  for (int i = 0; i < ircdstate->maxchannelcount; i++) {
    if (!channels[i].name[0]) {
      // We found one, use this slot
      arrslot = i;
      debugprint(DEBUG_FULL, "createchannel(): re-using existing slot %d.\n", arrslot);
      break;
    }
  }

  // If we didn't find one, increment the total count and use the next unused slot
  if (arrslot < 0) {
    arrslot = ircdstate->maxchannelcount;
    ircdstate->maxchannelcount++;
    debugprint(DEBUG_FULL, "createchannel(): using new slot %d.\n", arrslot);
  }

  // If we got a valid slot...
  if (arrslot >= 0) {
    // ...set the name and topic
    strncpy(channels[arrslot].name, name, strlen(name));
    channels[arrslot].name[strlen(name)] = '\0';
    debugprint(DEBUG_FULL, "createchannel(): name given was '%s', length '%ld'.\n", name, strlen(name));
    debugprint(DEBUG_FULL, "createchannel(): name set to '%s', length '%ld'.\n", channels[arrslot].name, strlen(channels[arrslot].name));
    strncpy(channels[arrslot].topic, topic, strlen(topic));
    channels[arrslot].topic[strlen(topic)] = '\0';
    debugprint(DEBUG_FULL, "createchannel(): topic given was '%s', length '%ld'.\n", topic, strlen(topic));
    debugprint(DEBUG_FULL, "createchannel(): topic set to '%s', length '%ld'.\n", channels[arrslot].topic, strlen(channels[arrslot].topic));
    strncpy(channels[arrslot].topicwho, topicwho, strlen(topicwho));
    channels[arrslot].topicwho[strlen(topicwho)] = '\0';
    strncpy(channels[arrslot].topicwhen, topicwhen, strlen(topicwhen));
    channels[arrslot].topicwhen[strlen(topicwhen)] = '\0';
    channels[arrslot].gotnames = 0;
    // Set nicks to blank
    for (int i = 0; i < MAXCHANNICKS; i++) {
      channels[arrslot].nicks[i][0] = '\0';
    }
    return 1;
  }

  debugprint(DEBUG_CRIT, "error: createchannel() didn't create a channel\n");

  // TODO - Make a failed return do something to callers
  return 0;
}

int setchanneltopicwhotime(struct channel *channels, int maxchannelcount, char *channelname, char *who, char *when) {
  debugprint(DEBUG_FULL, "setchanneltopicwhotime(): given '%s', '%s', and '%s'.\n", channelname, who, when);
  debugprint(DEBUG_FULL, "setchanneltopicwhotime(): who: '%s' with length '%ld'.\n", who, strlen(who));
  debugprint(DEBUG_FULL, "setchanneltopicwhotime(): when: '%s' with length '%ld'.\n", when, strlen(when));

  for (int i = 0; i < maxchannelcount; i++) {
    if ((strlen(channels[i].name) == strlen(channelname)) && (strncmp(channels[i].name, channelname, strlen(channelname)) == 0)) {
      strncpy(channels[i].topicwho, who, strlen(who));
      channels[i].topicwho[strlen(who)] = '\0';
      strncpy(channels[i].topicwhen, when, strlen(when));
      channels[i].topicwhen[strlen(when)] = '\0';
      debugprint(DEBUG_FULL, "setchanneltopicwhotime(): set channel '%s' to '%s', '%s'.\n", channels[i].name, channels[i].topicwho, channels[i].topicwhen);
      return 1;
    }
  }

  // TODO - Make a failed return do something to callers
  debugprint(DEBUG_FULL, "setchanneltopicwhotime(): failed to set channel topic who/when.\n");
  return 0;
}

int setchanneltopic(struct channel *channels, int maxchannelcount, char *channelname, char *topic) {
  debugprint(DEBUG_FULL, "setchanneltopic(): given '%s' and '%s'.\n", channelname, topic);

  for (int i = 0; i < maxchannelcount; i++) {
    if ((strlen(channels[i].name) == strlen(channelname)) && (strncmp(channels[i].name, channelname, strlen(channelname)) == 0)) {
      strncpy(channels[i].topic, topic, strlen(topic));
      channels[i].topic[strlen(topic)] = '\0';
      debugprint(DEBUG_FULL, "setchanneltopic(): set channel '%s' to '%s'.\n", channels[i].name, channels[i].topic);
      return 1;
    }
  }

  // TODO - Make a failed return do something to callers
  return 0;
}

int getchannelcount(struct channel *channels, int maxchannelcount) {
  int count = 0;

  for (int i = 0; i < maxchannelcount; i++) {
    if (channels[i].name[0]) {
      count++;
    }
  }

  debugprint(DEBUG_FULL, "getchannelcount(): counted %d channels.\n", count);

  return count;
}

int removechannel(struct channel *channels, int maxchannelcount, char *name) {
  debugprint(DEBUG_FULL, "removechannel(): given '%s'.\n", name);

  // Clear its topic setter and timestamp...
  setchanneltopicwhotime(channels, maxchannelcount, name, "", "0");

  // Find the channel in the channel array...
  for (int i = 0; i < maxchannelcount; i++) {
    if ((strlen(channels[i].name) == strlen(name)) && (strncmp(channels[i].name, name, strlen(name)) == 0)) {
      // ..and NULL its name (0th character = '\0')
      channels[i].name[0] = '\0';
      // Set nicks to blank
      for (int j = 0; j < MAXCHANNICKS; j++) {
        channels[i].nicks[j][0] = '\0';
      }
      debugprint(DEBUG_FULL, "removechannel(): channel '%s' removed and topicwhen set to '%s'.\n", name, channels[i].topicwhen);
      return 1;
    }
  }

  debugprint(DEBUG_CRIT, "error: removechannel() didn't remove a channel\n");

  // TODO - Make a failed return do something to callers
  return 0;
}

// Check if we have the NAMES for the channel 'name' already.
// Return 1 if we do, 0 if we don't, or -1 if there's an error.
int channelgotnames(struct channel *channels, int maxchannelcount, char *name) {
  debugprint(DEBUG_FULL, "channelgotnames(): given '%s'.\n", name);
  for (int i = 0; i < maxchannelcount; i++) {
    if ((strlen(channels[i].name) == strlen(name)) && strncmp(channels[i].name, name, strlen(name)) == 0) {
      if (channels[i].gotnames) {
        debugprint(DEBUG_FULL, "channelgotnames(): channel '%s' gotnames was set, returning '%d'.\n", channels[i].name, channels[i].gotnames);
        return 1;
      } else {
        debugprint(DEBUG_FULL, "channelgotnames(): channel '%s' gotnames was not set, returning '%d'.\n", channels[i].name, channels[i].gotnames);
        return 0;
      }
    }
  }

  // We didn't find the channel, this isn't good!  TODO - Do something if this happens.
  debugprint(DEBUG_CRIT, "channelgotnames(): channel '%s' not found, this is bad, returning -1.\n", name);
  return -1;
}

// Check if we are in a channel named "name" or not.
// Return 1 if we are, or 0 if not.
int inchannel(struct channel *channels, int maxchannelcount, char *name) {
  // Make sure the name doesn't have any trailing CR or LF
  // (But only if name is at least two characters long already)
  if (strlen(name) >= 2) {
    while (name[strlen(name) - 1] == '\r' || name[strlen(name) - 1] == '\n') {
      name[strlen(name) - 1] = '\0';
    }
  }

  for (int i = 0; i < maxchannelcount; i++) {
    if ((strlen(channels[i].name) == strlen(name)) && (strncmp(channels[i].name, name, strlen(name)) == 0)) {
      debugprint(DEBUG_FULL, "inchannel(): in channel '%s'.\n", name);
      return 1;
    }
  }

  // We're not in the channel
  debugprint(DEBUG_FULL, "inchannel(): NOT in channel '%s'.\n", name);
  return 0;
}

// Returns the array index in the 'channels' array of the channel
// named 'channel'.
// Returns -1 if there was an error.
int channelindex(struct channel *channels, int maxchannelcount, char *name) {
  debugprint(DEBUG_FULL, "channelindex(): given '%s'.\n", name);
  for (int i = 0; i < maxchannelcount; i++) {
    if ((strlen(channels[i].name) == strlen(name)) && (strncmp(channels[i].name, name, strlen(name)) == 0)) {
      return i;
    }
  }

  // We didn't find the channel, this isn't good!  TODO - Do something if this happens.
  debugprint(DEBUG_CRIT, "channelindex(): channel '%s' not found, this is bad, returning -1.\n", name);
  return -1;
}

// Send the auto replay to the requested client, where 'sourcefd' is the client
// to send to.  The type of replay will depend on the user's settings.
// Returns 1 for success or 0 for failure.
int doautoreplay(int sourcefd, struct client *clients, struct settings *settings, struct ircdstate *ircdstate, struct channel *channels) {
  // If replaymode = "none" then don't send anything
  if (!strncmp(settings->replaymode, "none", strlen("none"))) {
    debugprint(DEBUG_FULL, "doautoreplay() requested but replaymode = \"none\".\n");
    return 1;
  }

  // If replaymode = "time" then send whatever replayseconds is set to
  if (!strncmp(settings->replaymode, "time", strlen("time"))) {
    debugprint(DEBUG_FULL, "doautoreplay(): replaymode = \"time\", calling doreplaytime() with '%d' seconds.\n", settings->replayseconds);
    if (!doreplaytime(sourcefd, settings->replayseconds, clients, settings, ircdstate, channels)) {
      debugprint(DEBUG_SOME, "doautoreplay(): doreplaytime() returned 0, returning 0 to caller...\n");
      return 0;
    }
    return 1;
  }

  // If replaymode = "lastspoke" then send whatever happened since the user's current nick last spoke
  if (!strncmp(settings->replaymode, "lastspoke", strlen("lastspoke"))) {
    debugprint(DEBUG_FULL, "doautoreplay(): replaymode = \"lastspoke\", calling doreplaylastspoke().\n");
    long linenumber = lastspokelinenumber(ircdstate->ircnick, settings->basedir);
    if (linenumber < 1) {
      debugprint(DEBUG_SOME, "doautoreplay(): lastspokelinenumber() returned < 1, returning 0 to caller...\n");
      return 0;
    }
    debugprint(DEBUG_FULL, "doautoreplay(): replaymode = \"lastspoke\", replaying from line '%ld' to sourcefd '%d'.\n", linenumber, sourcefd);
    if (!doreplaylastspoke(sourcefd, linenumber, clients, settings, ircdstate, channels)) {
      debugprint(DEBUG_SOME, "doautoreplay(): doreplaylastspoke() returned 0, returning 0 to caller...\n"); // CORRECT?
      return 0;
    }
    return 1;
  }

  // If replaymode = "noclients" and there is currently only one client connected, then send whatever happened since there were last no clients connected
  if (!strncmp(settings->replaymode, "noclients", strlen("noclients")) && numclients(clients) == 1) {
    debugprint(DEBUG_FULL, "doautoreplay(): replaymode = \"noclients\", sending '%d' seconds of replay.\n", time(NULL) - ircdstate->clientsnonetime);
    if (!doreplaytime(sourcefd, time(NULL) - ircdstate->clientsnonetime, clients, settings, ircdstate, channels)) {
      debugprint(DEBUG_SOME, "doautoreplay(): doreplaytime() returned 0, returning 0 to caller...\n");
      return 0;
    }
    return 1;
  }

  // If replaymode = "lastchange" then send whatever happened since the last client registration or disconnection
  if (!strncmp(settings->replaymode, "lastchange", strlen("lastchange"))) {
    debugprint(DEBUG_FULL, "doautoreplay(): replaymode = \"lastchange\", sending '%d' seconds of replay.\n", time(NULL) - ircdstate->clientchangetime);
    if (!doreplaytime(sourcefd, time(NULL) - ircdstate->clientchangetime, clients, settings, ircdstate, channels)) {
      debugprint(DEBUG_SOME, "doautoreplay(): doreplaytime() returned 0, returning 0 to caller...\n");
      return 0;
    }
    return 1;
  }

  // If replaymode = "perclient" then don't handle this here, it is done when the client registers its client code
  if (!strncmp(settings->replaymode, "perclient", strlen("perclient"))) {
    return 1;
  }

  // We shouldn't get here
  return 0;
}

// Return a count of the number of connected clients
int numclients(struct client *clients) {
  int count = 0;

  for (int i = 0; i < MAXCLIENTS; i++) {
    if (clients[i].fd) {
      count++;
    }
  }

  debugprint(DEBUG_FULL, "numclients(): '%d' clients connected.\n", count);
  return count;
}

// Join any channels that were configured to be automatically
// joined in the configuration file.
// Returns 1 on success or 0 on failure.
int joinautochannels(SSL *server_ssl, struct client *clients, struct settings *settings) {
  if (settings->autochannels[0][0] == '\0') {
    // None configured
    debugprint(DEBUG_FULL, "joinautochannels(): none configured.\n");
    return 1;
  }

  // Join all the channels
  for (int i = 0; i < MAXCONFARR; i++) {
    // Unless there are none left in the array
    if (settings->autochannels[i][0] == '\0') {
      debugprint(DEBUG_FULL, "joinautochannels(): Finishing joining %d channels.\n", i);
      return 1;
    }

    debugprint(DEBUG_FULL, "joinautochannels(): Joining '%s'.\n", settings->autochannels[i]);
    char joinmsg[MAXDATASIZE];
    if (!snprintf(joinmsg, MAXDATASIZE, "JOIN %s", settings->autochannels[i])) {
      fprintf(stderr, "joinautochannels(): Error while preparing JOIN message!\n");
      debugprint(DEBUG_CRIT, "joinautochannels(): Error while preparing JOIN message\n");
      joinmsg[0] = '\0';
    } else {
      sendtoserver(server_ssl, joinmsg, strlen(joinmsg), 0, clients, settings);
    }
  }

  // TODO - Can we fail here?  Return 0 if so and make callers handle this if so.
  return 1;
}

// Try to make a new nick if no configured are available or liked by the server
// Do this by sticking a number on the end of the current nick and trying numbers
// 1 through to 9.
void tryautonick(struct ircdstate *ircdstate) {
  // Increment the attempts counter
  ircdstate->autonicknum++;

  if (ircdstate->autonicknum == 10) {
    // We've already tried 9 nicks and failed, give up
    printf("tryautonick(): Tried 9 automatic nicks and the server didn't like any, giving up.\n");
    debugprint(DEBUG_CRIT, "tryautonick(): Tried 9 automatic nicks and the server didn't like any, giving up.\n");
    exit(1);
  }

  int oldlen = strlen(ircdstate->ircnick);

  // If we've already started trying autonick, just replace the last character a the new number
  if (ircdstate->autonicknum > 1) {
    debugprint(DEBUG_FULL, "tryautonick(): already started autonick, starting with '%s' length '%ld'.\n", ircdstate->ircnick, strlen(ircdstate->ircnick));
    ircdstate->ircnick[oldlen - 1] = ircdstate->autonicknum + '0';
    // And null terminate
    ircdstate->ircnick[oldlen] = '\0';
  // If the nick is longer than or equal to the RFC 1459 max nick
  // length then try sticking the number at the end
  } else if (oldlen >= MAXRFCNICKLEN) {
    debugprint(DEBUG_FULL, "tryautonick(): long old nick, starting with '%s' length '%ld'.\n", ircdstate->ircnick, strlen(ircdstate->ircnick));
    // (+ '0' to make char from int)
    ircdstate->ircnick[MAXRFCNICKLEN] = ircdstate->autonicknum + '0';
    // And null terminate
    ircdstate->ircnick[MAXRFCNICKLEN + 1] = '\0';
  // Otherwise, just stick it on the end (+ '0' to make char from int)
  } else {
    debugprint(DEBUG_FULL, "tryautonick(): short old nick, starting with '%s' length '%ld'.\n", ircdstate->ircnick, strlen(ircdstate->ircnick));
    ircdstate->ircnick[oldlen] = ircdstate->autonicknum + '0';
    // And null terminate
    ircdstate->ircnick[oldlen + 1] = '\0';
  }

  debugprint(DEBUG_FULL, "tryautonick(): set irdstrings->ircnick to '%s'.\n", ircdstate->ircnick);
}

// Exit the program cleanly - tell clients, tell the server, then exit(0)
// Optional quit message string "quitmsg"
// "sourcefd" of 0 means the request didn't come from a client
void cleanexit(SSL *server_ssl, struct client *clients, int sourcefd, struct ircdstate *ircdstate, struct settings *settings, char *quitmsg) {
  char outgoingmsg[MAXDATASIZE];

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

  // Tell clients and debug log
  if (sourcefd) {
    snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Exiting on request from client %s, message '%s'.", ircdstate->ircnick, clients[clientindex].remoteip, quitmsg);
    debugprint(DEBUG_CRIT, "Exiting on request from client %s with fd '%d', message '%s'.\n", clients[clientindex].remoteip, sourcefd, quitmsg);
  } else {
    snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Exiting on request (not from a client), message '%s'.", ircdstate->ircnick, quitmsg);
    debugprint(DEBUG_CRIT, "Exiting on request (not from a client), message '%s'.\n", quitmsg);
  }
  sendtoallclients(clients, outgoingmsg, EXCEPT_NONE, settings);

  // Tell the server
  // Combine "QUIT :" with any (optional) quit message
  snprintf(outgoingmsg, MAXDATASIZE, "QUIT :%s", quitmsg);
  sendtoserver(server_ssl, outgoingmsg, strlen(outgoingmsg), sourcefd, clients, settings);

  exit(0);
}

// Re-read the configuration file, setting 'failuremsg' to a failure message on failure.
// 'ctx' is the client OpenSSL context for changing the certificate/key.
// Returns 1 on success or 0 on failure.
int rehash(struct settings *settings, char *failuremsg, SSL_CTX *ctx) {
  // TODO - Try to share some/all of this code with the initial main() settings loading

  // What are the configured nick(s)?
  char oldircnicks[MAXCONFARR][MAXDATASIZE];
  // Backup the existing configured nicks in case this rehash fails
  for (int i = 0; i < MAXCONFARR; i++) {
    strcpy(oldircnicks[i], settings->ircnicks[i]);
  }
  int ret = getconfarr("nicks", settings->conffile, settings->ircnicks);
  if (!ret) {
    // No nicks read, copy the old ones back
    for (int i = 0; i < MAXCONFARR; i++) {
      strcpy(settings->ircnicks[i], oldircnicks[i]);
    }
    strcpy(failuremsg, "error getting any 'nicks' from configuration file");
    return 0;
  } else if (ret == -1) {
    // Error reading an array line from the configuration file
    // Remove any newlines from the string so error printing works nicely
    for (size_t i = 0; i < strlen(settings->ircnicks[0]); i++) {
      if (settings->ircnicks[0][i] == '\n') {
        settings->ircnicks[0][i] = ' ';
      }
    }
    if (!snprintf(failuremsg, MAXDATASIZE, "error getting 'nicks' from configuration file: %s", settings->ircnicks[0])) {
      debugprint(DEBUG_CRIT, "Error while preparing nick error response!\n");
      strcpy(failuremsg, "Error while preparing nick error response!");
    }
    // Copy the old ones back (after setting failuremsg so we can read the error string from element 0)
    for (int i = 0; i < MAXCONFARR; i++) {
      strcpy(settings->ircnicks[i], oldircnicks[i]);
    }
    return 0;
  }
  // 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) {
      // A nick is too long, copy the old ones back
      for (int i = 0; i < MAXCONFARR; i++) {
        strcpy(settings->ircnicks[i], oldircnicks[i]);
      }
      if (!snprintf(failuremsg, MAXDATASIZE, "error: specified nick '%s' is too long, maximum length is %d.\n", settings->ircnicks[i], MAXNICKLENGTH)) {
        debugprint(DEBUG_CRIT, "Error while preparing nick too long response!\n");
        strcpy(failuremsg, "Error while preparing nick too long response!");
      }
      return 0;
    }
  }

  // What is the auto replay mode?
  char oldreplaymode[MAXDATASIZE];
  strcpy(oldreplaymode, settings->replaymode);
  if (!getconfstr("replaymode", settings->conffile, settings->replaymode)) {
    strcpy(settings->replaymode, oldreplaymode);
    strcpy(failuremsg, "error getting 'replaymode' from configuration file");
    return 0;
  } 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")) {
      strcpy(settings->replaymode, oldreplaymode);
      strcpy(failuremsg, "replaymode in configuration file must be one of \"none\", \"time\", \"lastspoke\", \"noclients\", \"lastchange\", or \"perclient\"");
      return 0;
    }
  }

  // How many seconds of replay log should automatically be replayed - TODO - Can we do error checking on this?
  int oldreplayseconds = settings->replayseconds;
  settings->replayseconds = getconfint("replayseconds", settings->conffile);
  // Don't worry if replaymode != "time"
  if (errno == ECONFINT && strcmp(settings->replaymode, "time") == 0) {
    settings->replayseconds = oldreplayseconds;
    strcpy(failuremsg, "error getting 'replayseconds' from configuration file");
    return 0;
  }

  // Should sending replay logs include a datestamp?
  int oldreplaydates = settings->replaydates;
  settings->replaydates = getconfint("replaydates", settings->conffile);
  if (errno == ECONFINT) {
    settings->replaydates = oldreplaydates;
    strcpy(failuremsg, "error getting 'replaydates' from configuration file");
    return 0;
  }

  // What is the password?
  char oldpassword[MAXCHAR];
  strcpy(oldpassword, settings->password);
  if (!getconfstr("password", settings->conffile, settings->password)) {
    strcpy(settings->password, oldpassword);
    strcpy(failuremsg, "error getting 'password' from configuration file");
    return 0;
  }

  // Is logging enabled?
  int oldlogging = settings->logging;
  settings->logging = getconfint("logging", settings->conffile);
  if (errno == ECONFINT) {
    settings->logging = oldlogging;
    strcpy(failuremsg, "error getting 'logging' from configuration file");
    return 0;
  }

  // Is replay logging enabled?
  int oldreplaylogging = settings->replaylogging;
  settings->replaylogging = getconfint("replaylogging", settings->conffile);
  if (errno == ECONFINT) {
    settings->replaylogging = oldreplaylogging;
    strcpy(failuremsg, "error getting 'replaylogging' from configuration file");
    return 0;
  }

  // Is debugging enabled?
  int olddebug = debug;
  debug = getconfint("debug", settings->conffile);
  if (errno == ECONFINT) {
    debug = olddebug;
    strcpy(failuremsg, "error getting 'debug' from configuration file");
    return 0;
  }

  // If clienttls = 1, re-read the certificate and key file paths (we don't support switching between TLS and non-TLS)
  if (settings->clienttls) {

    // What is the certificate file path?
    char oldcertfile[PATH_MAX];
    strcpy(oldcertfile, settings->certfile);
    if (!getconfstr("certfile", settings->conffile, settings->certfile)) {
      // If none provided, set to default
      if (!snprintf(settings->certfile, PATH_MAX, "%s/cert.pem", settings->basedir)) {
        strcpy(settings->certfile, oldcertfile);
        strcpy(failuremsg, "didn't get 'certfile' from configuration file and failed to prepare default certfile location");
        return 0;
      }
    }

    // What is the key file path?
    char oldkeyfile[PATH_MAX];
    strcpy(oldkeyfile, settings->keyfile);
    if (!getconfstr("keyfile", settings->conffile, settings->keyfile)) {

      // If none provided, set to default
      if (!snprintf(settings->keyfile, PATH_MAX, "%s/key.pem", settings->basedir)) {
        strcpy(settings->keyfile, oldkeyfile);
        strcpy(failuremsg, "didn't get 'keyfile' from configuration file and failed to prepare default keyfile location");
        return 0;
      }
    }

    // Reconfigure OpenSSL context in case the certificate or the key changed
    configure_openssl_context(ctx, settings->certfile, settings->keyfile);
  }

  // All is good, no failure message, return 1.
  failuremsg[0] = '\0';
  return 1;
}

// Check the password provided in the string 'str' against what is in
// the settings structure 'settings'.
// Return 0 for password mismatch, or 1 for password match.
int checkpassword(char *password, struct settings *settings) {
  // Ensure passwords are the same length
  if (strlen(password) != strlen(settings->password)) {
    debugprint(DEBUG_SOME, "Password length mismatch!\n");
    return 0;
  }
  // Ensure passwords match
  if (strncmp(password, settings->password, strlen(password)) == 0) {
    debugprint(DEBUG_FULL, "settings->password matches password.\n");
    return 1;
  } else {
    debugprint(DEBUG_SOME, "settings->password does NOT match password!\n");
    return 0;
  }

  printf("checkpassword(): unexpectedly got to end of function, quitting.\n");
  debugprint(DEBUG_CRIT, "checkpassword(): unexpectedly got to end of function, quitting.\n");
  // No messing around with password stuff
  exit(EXIT_FAILURE);
}

// Adds a client code to the clientcode structure if it doesn't already exist.
// On success, copy it to the client's clientcode field.
// Returns 1 on adding a new code, 0 if the code already existed, or -1 on error.
int addclientcode(int sourcefd, char *code, struct clientcodes *clientcodes, struct client *clients) {
  debugprint(DEBUG_SOME, "addclientcode(): Adding client code '%s' if it doesn't already exist.\n", code);

  // Make sure there aren't too many client codes already
  int counter = 0;
  for (int i = 0; i < MAXCLIENTCODES; i++) {
    if (clientcodes[i].code[0]) {
      counter++;
    }
  }
  if (counter >= MAXCLIENTCODES) {
    debugprint(DEBUG_CRIT, "addclientcode(): too many client codes.\n");
    return -1;
  }

  // Copy it to the current client
  for (int j = 0; j < MAXCLIENTS; j++) {
    if (clients[j].fd == sourcefd) {
      strcpy(clients[j].clientcode, code);
      debugprint(DEBUG_FULL, "addclientcode(): set client code for fd '%d' to '%s'.\n", clients[j].fd, clients[j].clientcode);
    }
  }

  // And add it to the clientcode structure if it doesn't already exist
  for (int i = 0; i < MAXCLIENTCODES; i++) {
    if (strlen(code) == strlen(clientcodes[i].code) && (strncmp(code, clientcodes[i].code, strlen(code)) == 0)) {
      debugprint(DEBUG_FULL, "addclientcode(): client code already existed.\n");
      // It already exists
      return 0;
    } else {
      // It doesn't, add it
      strcpy(clientcodes[i].code, code);
      return 1;
    }
  }

  // We shouldn't get here
  debugprint(DEBUG_CRIT, "addclientcode(): we shouldn't get here");
  return -1;
}

// Sets a given client code as last disconnecting at the current time.
// Returns 1 on success or 0 on failure. - TODO have callers do something on failure, or change this to a void.
int setclientcodetime(char *code, struct clientcodes *clientcodes) {
  debugprint(DEBUG_FULL, "setclientcodetime(): Setting disconnect time for '%s'.\n", code);

  // Look for this code and set it as the current time if found
  for (int i = 0; i < MAXCLIENTCODES; i++) {
    if (strlen(code) == strlen(clientcodes[i].code) && (strncmp(code, clientcodes[i].code, strlen(code)) == 0)) {
      clientcodes[i].lastdisconnected = time(NULL);
      debugprint(DEBUG_FULL, "setclientcodetime(): Set time to '%d'.\n", clientcodes[i].lastdisconnected);
      return 1;
    }
  }

  // If we got here, the code was never found
  return 0;
}

// Return the timestamp that a given client last disconnected, or 0 on failure.
int getclientcodetime(char *code, struct clientcodes *clientcodes) {
  debugprint(DEBUG_FULL, "getclientcodetime(): looking for '%s'.\n", code);
  // Look for this code and set it as the current time if found
  for (int i = 0; i < MAXCLIENTCODES; i++) {
//printf("%s.\n", clientcodes[i].code);
    if (strlen(code) == strlen(clientcodes[i].code) && (strncmp(code, clientcodes[i].code, strlen(code)) == 0)) {
      debugprint(DEBUG_FULL, "getclientcodetime(): code '%s' found at index '%d', timestamp '%d'.\n", code, i, clientcodes[i].lastdisconnected);
      return clientcodes[i].lastdisconnected;
    }
  }

  // If we got here, the code was never found
  return 0;
}

// Replace any instances of "find" with "replace" in the string "str"
void replacechar(char *str, char find, char replace) {
  for (size_t i = 0; i < strlen(str); i++) {
    if (str[i] == find) {
      str[i] = replace;
    }
  }
}

// Add nick (passed as a :nick!user@host) to channel 'channel'
// Returns 1 on success or 0 on failure
int addnicktochannel(char *nickuserhost, char *channel, struct channel *channels, int maxchannelcount) {
  debugprint(DEBUG_FULL, "addnicktochannel(): given '%s' and '%s'.\n", nickuserhost, channel);

  // Get the nick from the prefix
  extractnickfromprefix(nickuserhost, 1);

  // Make sure the channel exists
  int chanfound = 0;
  int chanindex;
  for (chanindex = 0; chanindex < maxchannelcount; chanindex++) {
    if (strlen(channels[chanindex].name) == strlen(channel) && !strcmp(channels[chanindex].name, channel)) {
      chanfound = 1;
      break;
    }
  }
  if (!chanfound) {
    debugprint(DEBUG_CRIT, "addnicktochannel(): channel '%s' not found in channel struct.\n", channel);
    return 0;
  }

  // Add the nick to the channel
  for (int i = 0; i < MAXCHANNICKS; i++) {
    // Make sure the nick isn't already in the channel struct
    if (strlen(channels[chanindex].nicks[i]) == strlen(nickuserhost) && !strcmp(channels[chanindex].nicks[i], nickuserhost)) {
      // Unexectedly the nick is already here, hopefully it's OK so let's return 1
      debugprint(DEBUG_FULL, "addnicktochannel(): nick '%s' already in channel '%s', returning.\n", nickuserhost, channel);
      return 1;
    }

    // Find the first unoccupied slot and put the nick in
    if (!channels[chanindex].nicks[i][0]) {
      strcpy(channels[chanindex].nicks[i], nickuserhost);
      debugprint(DEBUG_FULL, "addnicktochannel(): added nick '%s' to channel '%s'.\n", nickuserhost, channel);
      return 1;
    }
  }

  // We shouldn't get here, return error
  debugprint(DEBUG_CRIT, "addnicktochannel(): got to the end of the function without adding nick '%s' to channel '%s', returning error.\n",
             nickuserhost, channel);
  return 0;
}

// Remove nick (passed as a :nick!user@host) from channel 'channel'
// Returns 1 on success or 0 on failure
int removenickfromchannel(char *nickuserhost, char *channel, struct channel *channels, int maxchannelcount) {
  debugprint(DEBUG_FULL, "removenickfromchannel(): given '%s' and '%s'.\n", nickuserhost, channel);

  // Get the username from the prefix
  extractnickfromprefix(nickuserhost, 1);

  // Make sure the channel exists
  int chanfound = 0;
  int chanindex;
  for (chanindex = 0; chanindex < maxchannelcount; chanindex++) {
    if (strlen(channels[chanindex].name) == strlen(channel) && !strcmp(channels[chanindex].name, channel)) {
      chanfound = 1;
      break;
    }
  }
  if (!chanfound) {
    debugprint(DEBUG_CRIT, "removenickfromchannel(): channel '%s' not found in channel struct.\n", channel);
    return 0;
  }

  // Remove the nick from the channel
  for (int i = 0; i < MAXCHANNICKS; i++) {
    // Remove the the nick
    if (strlen(channels[chanindex].nicks[i]) == strlen(nickuserhost) && !strcmp(channels[chanindex].nicks[i], nickuserhost)) {
      // By null terminating its string
      debugprint(DEBUG_FULL, "removenickfromchannel(): nick '%s' removed from channel '%s'.\n", nickuserhost, channel);
      channels[chanindex].nicks[i][0] = '\0';
      return 1;
    }
  }

  // We shouldn't get here, return error
  debugprint(DEBUG_CRIT, "removenickfromchannel(): got to the end of the function without removing nick '%s' from channel '%s', returning error.\n",
             nickuserhost, channel);
  return 0;
}

// Remove nick (passed as a :nick!user@host) from all channels
// Returns 1 on success or 0 on failure
int removenickfromallchannels(char *nickuserhost, struct channel *channels, int maxchannelcount) {
  debugprint(DEBUG_FULL, "removenickfromallchannels(): given '%s'.\n", nickuserhost);

  // Get the nick from the prefix
  extractnickfromprefix(nickuserhost, 1);

  // Make sure the nick has a length of at least one
  if (strlen(nickuserhost) < 1) {
    debugprint(DEBUG_CRIT, "removenickfromallchannels(): nick has no length, returning 0!\n");
    return 0;
  }

  // Go through all channels and remove nick if present
  for (int i = 0; i < maxchannelcount; i++) {
    // Don't bother checking this channel index if it isn't used
    if (!channels[i].name[0]) {
      continue;
    }
    // Go through all nicks in channel
    for (int j = 0; j < MAXCHANNICKS; j++) {
      // Remove the nick from the channel if present
      if (strlen(channels[i].nicks[j]) == strlen(nickuserhost) && !strcmp(channels[i].nicks[j], nickuserhost)) {
        // By null terminating its string
        channels[i].nicks[j][0] = '\0';
        debugprint(DEBUG_FULL, "removenickfromallchannels(): nick '%s' removed from channel '%s'.\n", nickuserhost, channels[i].name);
      }
    }
  }

  return 1;
}

// Update old nick (passed as a :nick!user@host) to 'newnick' in all channels
// Returns 1 on success or 0 on failure
int updatenickinallchannels(char *nickuserhost, char *newnick, struct channel *channels, int maxchannelcount) {
  debugprint(DEBUG_FULL, "updatenickinallchannels(): given '%s' and '%s'.\n", nickuserhost, newnick);

  // Get the nick from the prefix
  extractnickfromprefix(nickuserhost, 1);

  // Strip prefix from newnick
  stripprefix(newnick, 1);

  // Make sure the old and new nicks have a length of at least one
  if (strlen(nickuserhost) < 1 || strlen(newnick) < 1) {
    debugprint(DEBUG_CRIT, "updatenickinallchannels(): nick has no length, returning 0!\n");
    return 0;
  }

  // Go through all channels and update nick if present
  for (int i = 0; i < maxchannelcount; i++) {
    // Go through all nicks in channel
    for (int j = 0; j < MAXCHANNICKS; j++) {
      // Update the nick in the channel if present
      if (strlen(channels[i].nicks[j]) == strlen(nickuserhost) && !strcmp(channels[i].nicks[j], nickuserhost)) {
        strcpy(channels[i].nicks[j], newnick);
        debugprint(DEBUG_FULL, "updatenickinallchannels(): nick '%s' updated to '%s' in channel '%s'.\n", nickuserhost, newnick, channels[i].name);
      }
    }
  }

  return 1;
}

// Check if "nick" is in any channel or not.
// Return 1 if it is, or 0 if not.
int isnickinanychannel(struct channel *channels, int maxchannelcount, char *nick) {
  debugprint(DEBUG_FULL, "isnickinanychannel(): given '%s'.\n", nick);

  // Make sure the nick has a length of at least one
  if (strlen(nick) < 1) {
    debugprint(DEBUG_CRIT, "isnickinanychannel(): nick has no length, returning 0!\n");
    return 0;
  }

  // Go through all channels and see if nick is present
  for (int i = 0; i < maxchannelcount; i++) {
    // Don't bother checking this channel index if it isn't used
    if (!channels[i].name[0]) {
      continue;
    }
    // Go through all nicks in channel
    for (int j = 0; j < MAXCHANNICKS; j++) {
      // See if the nick is here
      if (strlen(channels[i].nicks[j]) == strlen(nick) && !strcmp(channels[i].nicks[j], nick)) {
        // Found it!
        debugprint(DEBUG_FULL, "isnickinanychannel(): nick '%s' found in channel '%s', returning 1.\n", nick, channels[i].name);
        return 1;
      }
    }
  }

  debugprint(DEBUG_FULL, "isnickinanychannel(): nick '%s' not found in any channel, returning 0.\n", nick);
  return 0;
}

// Populate our channels struct with all nicks in a RPL_NAMREPLY
// Returns 1 on success or 0 on failure
int addnamereplytochannel(char *namereply, struct channel *channels, int maxchannelcount) {
//:irc.tghost.co.uk 353 blabounce = #blabouncer :blabounce bbnick ~@l_bratch @l_blabnc Hughbla Bratchbot ars
  debugprint(DEBUG_FULL, "addnamereplytochannel(): given '%s'.\n", namereply);

  // Make a copy since we don't need to modify the original
  char strcopy[MAXDATASIZE];
  strcpy(strcopy, namereply);

  // Strip the leading ':'
  stripprefix(strcopy, 1);

  // Find the start of the channel name, which comes after the first '=' followed by a space
  int channelpos = -1;
  for (size_t i = 0; i < strlen(strcopy) - 2; i++) {
    if (strcopy[i] == '=' && strcopy[i + 1] == ' ' && strcopy[i + 2] != '\0') {
      // Name found
      channelpos = i + 2;
      break;
    }
  }
  if (channelpos == -1) {
    // Didn't find the name, abort
    debugprint(DEBUG_FULL, "addnamereplytochannel(): couldn't find start of channel name in '%s'.\n", namereply);
    return 0;
  }

  // Find the end of the channel name
  char channelname[MAXCHANLENGTH];
  for (size_t i = channelpos; i < strlen(strcopy); i++) {
    // Stop when a space is found or if we're going to exceed MAXCHANLENGTH
    if (strcopy[i] == ' ' || i - channelpos == MAXCHANLENGTH - 2) {
      break;
    }
    channelname[i - channelpos] = strcopy[i];
    channelname[i - channelpos + 1] = '\0';
  }

  // Start with a nice clean string that just consists of nicks at the end of the string
  char nickstr[MAXDATASIZE];
  strcpy(nickstr, strcopy + channelpos + strlen(channelname) + 1);

  // Split nickstr up into its space-separated nick components

  // Copy to a temporary string for feeding to strsep
  char *nickcopy = strdup(nickstr);
  // Keep track of initial pointer for free()ing later
  char *nickcopyPtr = nickcopy;

  // Track which CLRF-separated nick we're on
  int nickcount = 0;
  // Build array of each space-separated token
  char nicks[MAXTOKENS][MAXDATASIZE];
  // Split the string by ' ' and add each space-separated nick to an array
  char *token;
  while ((token = strsep(&nickcopy, " ")) != NULL) {
    if (*token == '\0') continue; // Skip consecutive matches
    if (nickcount >= MAXTOKENS) break; // Too many tokens
    debugprint(DEBUG_FULL, "addnamereplytochannel(): Token: '%s', length '%ld'.\n", token, strlen(token));
    // Make sure it's not too long
    if (strlen(token) > MAXNICKLENGTH - 1) {
      debugprint(DEBUG_CRIT, "addnamereplytochannel(): nick too long, discarding.\n");
      continue;
    }
    // Copy into the token array (strlen + 1 to get the NULL terminator)
    strncpy(nicks[nickcount], token, strlen(token) + 1);
    nickcount++;
  }

  free(nickcopyPtr);

  // Clean up each nick (remove prefixes and such)
  for (int i = 0; i < nickcount; i++) {
    stripprefixesfromnick(nicks[i]);
    // And add to the channel
    addnicktochannel(nicks[i], channelname, channels, maxchannelcount);
  }

  return 1;
}

// Strips all leading prefixes (colons, user modes) from a nick
void stripprefixesfromnick(char *nick) {
  debugprint(DEBUG_FULL, "stripprefixesfromnick(): given '%s'.\n", nick);

  char nicktmp[MAXNICKLENGTH];
  int pos = 0;

  for (size_t i = 0; i < strlen(nick); i++) {
    // Only copy non-prefix chars
    if (nick[i] != ':' && nick[i] != '~' && nick[i] != '&' && nick[i] != '@' && nick[i] != '%' && nick[i] != '+') {
      nicktmp[pos] = nick[i];
      pos++;
    }
  }

  // Null terminate
  nicktmp[pos] = '\0';

  debugprint(DEBUG_FULL, "stripprefixesfromnick(): produced '%s'.\n", nicktmp);

  // Copy back to source string
  strcpy(nick, nicktmp);
}

// Convert the given 'string' into lowercase
void strlower(char *string) {
  for (int i = 0; string[i]; i++) {
    string[i] = tolower(string[i]);
  }
}