#include "terminal.h"
#include "utils.h"
#include "wordplay.h"
#include <ctype.h>
#include <fcntl.h>
#include <fstream>
#include <iostream>
#include <pty.h>
#include <sstream>
#include <stdio.h>
#include <stdlib.h> // random()
#include <string.h>
#include <string>
#include <strings.h> // strcasecmp
#include <sys/select.h>
#include <sys/wait.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>

struct console_details console;

/*
We get the username/fullname from tailing the node logs,
and reading the user.dat file.
 */
std::string username;
std::string fullname;

// #include <signal.h> // handle Ctrl-C/SIGINT

/* Log level guideline:
 * - ZF_LOG_FATAL - happened something impossible and absolutely unexpected.
 *   Process can't continue and must be terminated.
 *   Example: division by zero, unexpected modifications from other thread.
 * - ZF_LOG_ERROR - happened something possible, but highly unexpected. The
 *   process is able to recover and continue execution.
 *   Example: out of memory (could also be FATAL if not handled properly).
 * - ZF_LOG_WARN - happened something that *usually* should not happen and
 *   significantly changes application behavior for some period of time.
 *   Example: configuration file not found, auth error.
 * - ZF_LOG_INFO - happened significant life cycle event or major state
 *   transition.
 *   Example: app started, user logged in.
 * - ZF_LOG_DEBUG - minimal set of events that could help to reconstruct the
 *   execution path. Usually disabled in release builds.
 * - ZF_LOG_VERBOSE - all other events. Usually disabled in release builds.
 *
 * *Ideally*, log file of debugged, well tested, production ready application
 * should be empty or very small. Choosing a right log level is as important as
 * providing short and self descriptive log message.
 */
/*
#define ZF_LOG_VERBOSE 1
#define ZF_LOG_DEBUG   2
#define ZF_LOG_INFO    3
#define ZF_LOG_WARN    4
#define ZF_LOG_ERROR   5
#define ZF_LOG_FATAL   6
*/

// When debugging low-level, use this:
// #define ZF_LOG_LEVEL ZF_LOG_VERBOSE
// Except this doesn't work.  It needs to be anywere the
// zf_log.h is included.

// LOGGING with file output

#include "zf_log.h"

FILE *g_log_file;

static void file_output_callback(const zf_log_message *msg, void *arg) {
  (void)arg;
  *msg->p = '\n';
  fwrite(msg->buf, msg->p - msg->buf + 1, 1, g_log_file);
  fflush(g_log_file);
}

static void file_output_close(void) { fclose(g_log_file); }

static int file_output_open(const char *const log_path) {
  g_log_file = fopen(log_path, "a");
  if (!g_log_file) {
    ZF_LOGW("Failed to open log file %s", log_path);
    return 0;
  }
  atexit(file_output_close);
  zf_log_set_output_v(ZF_LOG_PUT_STD, 0, file_output_callback);
  return 1;
}

void log_flush(void) { fflush(g_log_file); }

// END LOGGING

/*
What is the name of the actual, real Mystic executable
that we'll be executing and mangling?
*/

#define TARGET "./mySTIC"
// Size of our input and output buffers.
#define BSIZE 1024

/*
    These are harry "timeout" events.

    These happen when we've been sitting around awhile.

 */

int node;

/*
This only works for those few idiots that use the
horribly broken SSH crap that Mystic uses.
 */
int locate_user(const char *alias) {
  FILE *user;
  char buffer[0x600];
  char temp[100];

  user = fopen("data/users.dat", "rb");
  if (user == NULL)
    return 0;

  // Carry on!
  while (fread(buffer, 0x600, 1, user) == 1) {
    pcopy(buffer + 0x6d, temp);
    if (strcasecmp(temp, username.c_str()) == 0) {
      pcopy(buffer + 0x8c, temp);
      fullname.assign(temp);
      break;
    }
    /*
    printf("Alias: %s\n", temp);
    pcopy(buffer + 0x8c, temp );
    printf("Full Name: %s\n", temp );
    */
  }
  fclose(user);
  return 1;
}

std::ifstream logfile;
std::streampos log_pos;

void open_mystic_log(void) {
  std::string mystic_logfile;
  {
    std::ostringstream buffer;

    buffer << "logs/node" << node << ".log";
    mystic_logfile = buffer.str();
  };
  logfile.open(mystic_logfile, std::ios_base::in | std::ios_base::ate);
  // Ok, we're at the end of the file.  Or should be.
  if (logfile.is_open()) {
    ZF_LOGD("Log %s open", (const char *)mystic_logfile.c_str());
    log_pos = logfile.tellg();
  } else {
    ZF_LOGE("Failed to open: %s", (const char *)mystic_logfile.c_str());
  }
}

void scan_mystic_log(void) {

  if (logfile.is_open()) {
    int again = 0;
    do {
      std::string line = find_new_text(logfile, log_pos);
      if (line.empty())
        return;
      again = 1;

      ZF_LOGD("mystic log: %s", (const char *)line.c_str());
      // Ok, we have a line, look for interesting details
      if (line.find("New user application") != std::string::npos) {
        ZF_LOGE("New User");
      }

      size_t pos;
      pos = line.find("Created Account: ");
      if (pos != std::string::npos) {
        pos += 18 - 1;
        // Ok, find the end '#'
        size_t len = line.find('#', pos);
        if (len != std::string::npos) {
          username = line.substr(pos, len - pos - 1);
          ZF_LOGE("New User: %s", (const char *)username.c_str());
          // once we know this works -- lookup user's record
          locate_user(username.c_str());
          ZF_LOGD("Username: [%s] A.K.A. [%s]", (const char *)username.c_str(),
                  (const char *)fullname.c_str());
        }
      }
      pos = line.find(" logged in");
      if (pos != std::string::npos) {
        --pos;
        size_t len = line.rfind(' ', pos);
        if (len != std::string::npos) {
          len++;
          username = line.substr(len, pos + 1 - len);
          ZF_LOGE("User: %s", (const char *)username.c_str());
          // verify this works, lookup
          locate_user(username.c_str());
          ZF_LOGD("Username: [%s] A.K.A. [%s]", (const char *)username.c_str(),
                  (const char *)fullname.c_str());
        }
      }
    } while (again);
  }
}

/*

This is done.  :D  My buffering system works with stack'em.

TO FIX:  Stop using c strings, must use char * buffer + int length.
MAY CONTAIN NULL VALUES.

Rework some things here.

Here's the "plan":

  if buffer is EMPTY:
      time_idle = 1;
      // setup for "random timeout value mess"
        // we're in luck!  The last parameter is time interval/timeout.  :D
      timeout.tv_sec = 10; // randrange(10-25)
      timeout.tv_usec = 0;

  NOT EMPTY:
      // we're in luck!  The last parameter is time interval/timeout.  :D
      timeout.tv_sec = 0;
      timeout.tv_usec = 10;   // Wild Guess Here?  Maybe higher, maybe
lower? time_idle = 0;

  ON READ:
    read/append to current buffer.
    We can't use nulls -- what if they are using ZModem, there's nulls in
the file! Look for trailing  / the very last "\r\n".

    (I could mangle/chunk it line by line.  But I'm not sure I'd need to do
that.)

    Optional "mangle" buffer up to that very point -- and send up to that
point.

    Option #2:  Maybe we send everything if program has been running for
under 20 seconds.  This would allow the ANSI detect to not get screwed up by
this new idea.

  ON TIMEOUT:
    if time_idle:
      Activate funny harry timeout events.
    else:
      Ok, we *STILL* haven't received any more characters into the buffer --
      even after waiting.  (Maybe we haven't waited long enough?)
      send the pending information in the buffer and clear it out.
      Maybe this is a prompt, and there won't be a \r\n.

This allows for cleaner process of "lines" of buffer.  We shouldn't break
in the midDLE OF A WORD.  Downside is that we sit on buffer contents a
little while / some amount of time -- which will add some lag to prompts
showing up.

(LAG?  Are you kidding?)


ZModem:

    start:  "rz^M**"...

    05-12 18:12:15.916 >> rz^M**^XB00000000000000^M<8A>^Q
    05-12 18:12:15.928 << **\x18B0100000023be50\r\n\x11
    05-12 18:12:15.928 >> *^XC^D
    05-12 18:12:15.939 << **\x18B0900000000a87c\r\n\x11
    05-12 18:12:15.940 >> *^XC
    # Start of PK zipfile.
    05-12 18:12:15.941 >> PK^C^D^T

    end:
    05-12 18:26:38.700 << **\x18B0100000023be50\r\n\x11
    05-12 18:26:38.700 >> **^XB0823a77600344c^M<8A>
    05-12 18:26:38.711 << **\x18B0800000000022d\r\n
    05-12 18:26:38.712 >> OO^MESC[0m

 */

// TODO:  Get everything above this -- into another file.

int main(int argc, char *argv[]) {
  int master;
  pid_t pid;
  node = -1;

  init_harry();
  srandom(time(NULL));

  // ./mystic -TID7 -IP192.168.0.1 -HOSTUnknown -ML1 -SL0 -ST2 -CUnknown
  // -Ubugz -PUWISHPASSWORD
  // ./mystic -TID7 -IP192.168.0.1 -HOSTUnknown -ML0 -SL0 -ST0 -CUnknown

  // ./mystic -TID7 -IP192.168.0.1 -HOSTUnknown -ML1 -SL0 -ST2 -CUnknown
  // -Ubugz -PUWISH
  // ./mystic -TID7 -IP192.168.0.1 -HOSTUnknown -ML0 -SL0 -ST0 -CUnknown

  // ./mystic -TID7 -IP192.168.0.1 -HOSTUnknown -ML0 -SL0 -ST0 -CUnknown
  // ./mystic -TID9 -IP192.168.0.1 -HOSTUnknown -ML0 -SL1 -ST0 -CUnknown

  // ./mystic -TID7 -IP192.168.0.1 -HOSTUnknown -ML1 -SL0 -ST2 -CUnknown
  // -Ubugz -PDUMBWAYTODOTHIS
  // ./mystic -TID9 -IP192.168.0.1 -HOSTUnknown -ML1 -SL1 -ST2 -CUnknown
  // -Ubugz -PIDONTUSEPASCAL

  // SSH:  -ML1 -ST2
  // Telnet: -ML0 -ST0

  // Locate username (if given) in the command line
  // -U<username>
  for (int x = 0; x < argc; x++) {
    /*
    // This doesn't work:  You can give username + wrong password.
    // You will be identified as the wrong user at the login screen!
    if (strncmp("-U", argv[x], 2) == 0) {
      username.assign(argv[x] + 2);
    }
    */
    /* Changed in latest version
    if (strncmp("-SL", argv[x], 3) == 0) {
      node = atoi(argv[x] + 3) + 1;
    }
    */
    // -TID7, -TID9, -TID11 for node 1, 2, 3
    if (strncmp("-TID", argv[x], 4) == 0) {
      node = (atoi(argv[x] + 4) - 5) / 2;
    }
  }

  if (node == -1) {
    // likely this is someone trying to run something
    char *args[20]; // max 20 args
    int x;
    char new_exec[] = TARGET;

    // build new args list
    args[0] = new_exec;

    for (x = 1; x < argc; x++) {
      args[x] = argv[x];
    };

    // null term the list
    args[x] = NULL;

    // run Mystic, run!
    execvp(TARGET, args);
    return 2;
  }

  std::string logfile;
  {
    std::ostringstream buffer;

    buffer << "horrible_harry_" << node << ".log";
    logfile = buffer.str();
  };

  if (!file_output_open((const char *)logfile.c_str()))
    return 2;

  for (auto cit = CONFIG.begin(); cit != CONFIG.end(); ++cit) {
    ZF_LOGD("Config {%s}:{%s}", (const char *)cit->first.c_str(),
            (const char *)cit->second.c_str());
  }

  ZF_LOGI("Node: %d", node);
  if (!username.empty()) {
    locate_user(username.c_str());
    ZF_LOGD("Username: [%s] A.K.A. [%s]", (const char *)username.c_str(),
            (const char *)fullname.c_str());
  }
  open_mystic_log();

  pid = forkpty(&master, NULL, NULL, NULL);

  // impossible to fork
  if (pid < 0) {
    return 1;
  }

  // child
  else if (pid == 0) {
    char *args[20]; // max 20 args
    int x;
    char new_exec[] = TARGET;

    // build new args list
    args[0] = new_exec;

    for (x = 1; x < argc; x++) {
      args[x] = argv[x];
    };

    // null term the list
    args[x] = NULL;

    // run Mystic, run!
    execvp(TARGET, args);
  }

  // parent
  else {
    struct termios tios, orig1;
    struct timeval timeout;
    time_t last_logscan = time(NULL);

    ZF_LOGD("starting");

    tcgetattr(master, &tios);
    tios.c_lflag &= ~(ECHO | ECHONL | ICANON);
    /*
    tios.c_iflag &= ~(ICRNL | IXON | BRKINT);
    tios.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    tios.c_oflag &= ~(OPOST);
    */
    tcsetattr(master, TCSAFLUSH, &tios);

    tcgetattr(1, &orig1);
    tios = orig1;
    tios.c_iflag &= ~(ICRNL | IXON | BRKINT);
    tios.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    tios.c_oflag &= ~(OPOST);

    // https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html
    tcsetattr(1, TCSAFLUSH, &tios);

    /*
    This doesn't need to be static -- because it is part of
    main.  Once main ends, we're done.
    */
    std::string buffer;
    buffer.reserve(BSIZE * 2);
    std::string play;
    play.reserve(4096);

    int zmodem = 0;

    // int size = 0;  // use buffer.size() instead

    for (;;) {
      int time_idle;

      // define estruturas para o select, que serve para verificar qual
      // se tornou "pronto pra uso"
      fd_set read_fd;
      fd_set write_fd;
      fd_set except_fd;

      // inicializa as estruturas
      FD_ZERO(&read_fd);
      FD_ZERO(&write_fd);
      FD_ZERO(&except_fd);

      // atribui o descritor master, obtido pelo forkpty, ao read_fd
      FD_SET(master, &read_fd);
      // atribui o stdin ao read_fd
      FD_SET(STDIN_FILENO, &read_fd);

      // o descritor tem que ser unico para o programa, a documentacao
      // recomenda um calculo entre os descritores sendo usados + 1

      /*
       TODO:  Figure out how this would work.

       I'm thinking something like timeouts 30-50 seconds?
       And as we get closer, 15-25 seconds.

       if zmodem, buffer will always be empty -- we won't hold anything.
      */

      if (buffer.size() == 0) {
        // buffer is empty
        if (zmodem) {
          timeout.tv_sec = 5;
        } else {
          timeout.tv_sec = randrange(10, 20);
        };
        timeout.tv_usec = 0;
        time_idle = 1;
      } else {
        // buffer is not empty
        timeout.tv_sec = 0;
        timeout.tv_usec = 1;
        time_idle = 0;
      }

      if (last_logscan < time(NULL)) {
        scan_mystic_log();
        if (username.empty())
          last_logscan = time(NULL) + 2;
        else
          last_logscan = time(NULL) + 10;
      }

      if (select(master + 1, &read_fd, &write_fd, &except_fd, &timeout) == 0) {
        ZF_LOGI("TIMEOUT");
        // This means timeout!
        if (time_idle) {
          if (harry_level() && !zmodem)
            harry_idle_event(STDOUT_FILENO);
        } else {
          ZF_LOGV("TIMEOUT buffer: %s", logrepr(buffer.c_str()));
          /*
          ZF_LOGI_MEM(buffer.data(), buffer.size(), "TIMEOUT buffer size=%lu",
                      buffer.size());
          */
          play.assign(buffer);
          if (harry_level())
            mangle(STDOUT_FILENO, play);
          else {
            write(STDOUT_FILENO, play.data(), play.size());
            console_receive(&console, play);
          }

          /*
          ZF_LOGI("console_receive");
          console_receive(&console, buffer);
          ZF_LOGI("write buffer");
          write(STDOUT_FILENO, buffer.data(), buffer.size());
          */
          ZF_LOGI("buffer clear");
          buffer.clear();
          // size = 0;
          // buffer is empty now
        }
      }

      // read_fd esta atribuido com read_fd?
      if (FD_ISSET(master, &read_fd)) {
        // leia o que bc esta mandando
        // ZF_LOGD("read (%d) %d bytes", size, BSIZE - size);
        char read_buffer[BSIZE + 1];
        int total;

        // We may adjust this later on (adjusting read length).
        if ((total = read(master, read_buffer, BSIZE)) != -1) {

          // Ok, we've read more into the buffer.
          ZF_LOGV("Read %d bytes", total);
          buffer.append(read_buffer, total);

          if (zmodem) {
            // Ok, we're zmodem mode -- is it time to exit?
            size_t zend = buffer.find("\x1b[0m");
            if (zend != std::string::npos)
              zmodem = 0;
            zend = buffer.find("\x1b[1;1H");
            if (zend != std::string::npos)
              zmodem = 0;
            if (!zmodem)
              ZF_LOGD("Zmodem end");
          } else {
            // Should we be in zmodem mode?
            size_t zstart = buffer.find("**\x18"
                                        "B0");
            if (zstart != std::string::npos) {
              zmodem = 1;
              ZF_LOGD("Zmodem start");
            }
          }

          if (zmodem) {
            // ZF_LOGI("Buffer %lu bytes, zmodem...", buffer.size());

            write(STDOUT_FILENO, buffer.data(), buffer.size());
            // console_receive(&console, buffer);
            buffer.clear();
          } else {

            // ZF_LOGV_MEM(buffer + size, total, "Read %d bytes:", total);
            // size += total;
            // ZF_LOGV_MEM(buffer, size, "Buffer now:");

            size_t pos = buffer.rfind("\r\n");
            // rstrnstr(buffer, size, "\r\n");
            //  >= 0) {
            if (pos != std::string::npos) {
              // found something!

              pos += 2;
              // play = buffer.substr() wipes out play's reserve.
              // play = buffer.substr(0, pos);
              play.assign(buffer, 0, pos);
              ZF_LOGI("play %lu size, %lu cap", play.size(), play.capacity());
              // play.copy(buffer.data(), pos);
              //) = buffer.substr(0, pos);
              buffer.erase(0, pos);

              mangle(STDOUT_FILENO, play);

              // ZF_LOGD_MEM(buffer, pos, "mangle buffer %d bytes:", pos);
              // mangle(STDOUT_FILENO, buffer, pos);
              // memmove(buffer, buffer + pos, size - pos);
              // size -= pos;
              // } else {
              //   ZF_LOGV("position of /r/n not found.");
            }

            // Ok, we failed to find CR+NL.  What's the buffer size at?

            if (buffer.size() > BSIZE) {
              // Ok, there's something going on, and it doesn't look good
              // unsure if I want to feed this into the console
              // my guess at this point would be zmodem xfer
              ZF_LOGI("Buffer %lu bytes, write only...", buffer.size());

              write(STDOUT_FILENO, buffer.data(), buffer.size());
              console_receive(&console, buffer);
              buffer.clear();
            }
          }
        } else
          break;
      }

      // read_fd esta atribuido com a entrada padrao?
      if (FD_ISSET(STDIN_FILENO, &read_fd)) {
        // leia a entrada padrao
        char input[BSIZE];
        int r = read(STDIN_FILENO, &input, BSIZE);
        input[r] = 0;
        // e escreva no bc
        if (!zmodem) {
          if (r > 50) {
            ZF_LOGI("<< %d bytes", r);
          } else {
            ZF_LOGI("<< %s", repr(input));
          }
        }

        write(master, &input, r);

        // This is INPUT from the USER
        // ZF_LOGI_MEM( input, strlen(input), "<< ");
      }
    }

    // Restore terminal
    tcsetattr(1, TCSAFLUSH, &orig1);
    ZF_LOGD("exit");
  }

  return 0;
}