#include <boost/bind.hpp>
#include <boost/format.hpp>
#include <functional>
#include <iostream>
// #include <boost/log/core.hpp>
// #include <boost/log/trivial.hpp>

#include <regex>
#include <string>

#include "config.h"
#include "galaxy.h"
#include "logging.h"
#include "session.h"
#include "utils.h"

// #include <boost/log/attributes/named_scope.hpp>

Session::Session(boost::asio::ip::tcp::socket socket,
                 boost::asio::io_service &io_service, std::string hostname,
                 std::string port, bool server_telnet_)
    : server_telnet{server_telnet_},
      socket_(std::move(socket)),
      io_service_{io_service},
      resolver_{io_service},
      server_{io_service},
      prompt_timer_{io_service},
      keep_alive_{io_service},
      host{hostname},
      port{port} {
  BUGZ_LOG(info) << "Session::Session()";
  // server_sent = 0;
  time_ms = 50;
  if (CONFIG["prompt_timeout"]) time_ms = CONFIG["prompt_timeout"].as<int>();

  keepalive_secs = 45;
  if (CONFIG["keepalive"]) keepalive_secs = CONFIG["keepalive"].as<int>();

  // Initialize the director
  director.to_server = boost::bind(&Session::to_server, this, _1);
  director.to_client = boost::bind(&Session::to_client, this, _1);
  director.post = boost::bind(&Session::post, this, _1);

  // too soon!
  // director.username = rlogin_name;

  // replace emit_ with below:  if (director.server_line)
  // director.server_line(s);
  /*
    emit_server_line = [this](const std::string &s) {
      if (director.server_line) {
        director.server_line(s);
      }
    };
    emit_server_prompt = [this](const std::string &s) {
      if (director.server_prompt) {
        director.server_prompt(s);
      }
    };
    emit_client_input ...  => director.client_input
  */
}

void Session::start(void) {
  BUGZ_LOG(info) << "Session::start()";
  // auto self(shared_from_this());

  client_read();
}

Session::~Session() { BUGZ_LOG(info) << "~Session"; }

/**
 * Returns the current server prompt.
 *
 * NOTE:  This is the raw string from the server, so it can contain
 * color codes.  Make sure you clean it before trying to test it for
 * any text.
 *
 * @return const std::string&
 */
const std::string &Session::get_prompt(void) { return server_prompt; }
void Session::set_prompt(const std::string &prompt) { server_prompt = prompt; }

void Session::post(notifyFunc nf) {
  if (nf) {
    BUGZ_LOG(info) << "Session::post()";
    io_service_.post(nf);
  } else {
    BUGZ_LOG(error) << "Session::post( nullptr )";
  }
}

void Session::parse_auth(void) {
  // how many nulls should I be seeing?
  // \0user\0pass\0terminal/SPEED\0
  // If I don't have a proper rlogin value here, it isn't going
  // to work when I try to connect to the rlogin server.

  if (rlogin_auth.size() > 10)
    rlogin_name = rlogin_auth.c_str() + 1;
  else
    rlogin_name = "?";
  director.username = rlogin_name;
}

void Session::on_connect(const boost::system::error_code error) {
  // We've connected to the server!  WOOT WOOT!
  // BOOST_LOG_NAMED_SCOPE("Session");

  if (!error) {
    BUGZ_LOG(info) << "Connected to " << host;
    to_client("Connected...\n\r");
    connected = true;
    if (!server_telnet) {
      if (rlogin_auth[0] != 0) {
        // Ok, the rlogin information was junk --
        to_client("Let me make up some fake rlogin data for you...\n\r");
        char temp[] = "\0test\0test\0terminal/9600\0";
        std::string tmp(temp, sizeof(temp));
        to_server(tmp);
      } else {
        to_server(rlogin_auth);
      }
    } else {
      // server_telnet.  Step 1: negotiate a telnet connection with TWGS.
      director.show_client = false;
    }

    server_read();
  } else {
    std::string output =
        str(boost::format("Failed to connect: %1%:%2%\n\r") % host % port);
    to_client(output);
    BUGZ_LOG(error) << "Failed to connect to " << host << ":" << port;
    BUGZ_LOG(warning) << "socket.shutdown()";
    socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
  }
}

/**
 * Called with the current line received from the server.
 *
 * This will do server parsing.  Sector/Ports/Connecting Sectors.
 * Port status/inventory/%.
 *
 * See \ref split_lines()
 * @param line
 */
void Session::on_server_line(const std::string &line,
                             const std::string &raw_line) {
  BUGZ_LOG(info) << "SL: [" << line << "]";
  director.server_line(line, raw_line);
}

/**
 * Split server input into lines.
 *
 * @param line
 */
void Session::split_lines(std::string line) {
  // Does this have \n\r still on it?  I don't want them.

  // cleanup backspaces
  size_t pos;
  while ((pos = line.find('\b')) != std::string::npos) {
    // backspace?  OK!  (unless)
    if (pos == 0) {
      // first character, so there's nothing "extra" to erase.
      line = line.erase(pos, 1);
    } else
      line = line.erase(pos - 1, 2);
  }

  std::string temp = clean_string(line);
  on_server_line(temp, line);
}

/*
Call this with whatever we just received.

That will allow me to send "just whatever I got"
this time around, rather then trying to figure out
what was just added to server_prompt.

What about \r, \b ?  Should that "reset" the server_prompt?

\r should not, because it is followed by \n (eventually)
and that completes my line.

 */
void Session::process_lines(std::string &received) {
  // break server_prompt into lines and send/process one by one.

  size_t pos, rpos;
  server_prompt.append(received);

  // I also need to break on r"\x1b[\[0-9;]*JK", treat these like \n

  while ((pos = server_prompt.find('\n', 0)) != std::string::npos) {
    std::string line;

    std::smatch m = ansi_newline(server_prompt);
    if (!m.empty()) {
      // We found one.
      size_t mpos = m.prefix().length();
      // int mlen = m[0].length();

      if (mpos < pos) {
        // Ok, the ANSI newline is before the \n

        // perform this process with the received line
        std::smatch rm = ansi_newline(received);
        if (!rm.empty()) {
          size_t rpos = rm.prefix().length();
          int rlen = rm[0].length();
          if (director.show_client) {
            line = received.substr(0, rpos + rlen);
            to_client(line);
          }
          received = rm.suffix();
        }

        // perform this on the server_prompt line
        line = m.prefix();
        split_lines(line);
        server_prompt = m.suffix();
        // redo this loop -- there's still a \n in there
        continue;
      }
    }
    // process "line" in received

    rpos = received.find('\n', 0);

    // get line to send to the client
    if (director.show_client) {
      // that is, if we're sending to the client!
      line = received.substr(0, rpos + 1);
      /*
      std::string clean = clean_string(line);
      BUGZ_LOG(error) << "rpos/show_client:" << clean;
      */
      if (server_telnet) {
        if (line.find('\xff') != std::string::npos) {
          remove_telnet_commands(line);
        }
      }

      to_client(line);
    }
    received = received.substr(rpos + 1);

    // process "line" in server_prompt

    line = server_prompt.substr(0, pos + 1);
    server_prompt = server_prompt.substr(pos + 1);

    // Remove \n for dispatching
    std::string part = line.substr(0, pos);

    /*
    if (server_sent != 0) {
      line = line.substr(server_sent);
      server_sent = 0;
    };
    */

    // display on?
    // to_client(line);

    // How should I handle \r in lines?  For now, remove it
    // but LOG that we did.

    replace(part, "\r", "");
    /*
    if (replace(part, "\r", "")) {
      BUGZ_LOG(warning) << "\\r removed from line";
    }
    */

    split_lines(part);
  }

  // Ok, we have sent all of the \n lines.

  if (!received.empty())
    if (director.show_client) {
      to_client(received);
      // std::string clean = clean_string(received);
      // BUGZ_LOG(error) << "show_client/leftovers:" << clean;
    }

  // This is eating the entire string.  String is partial line
  // portcim line, ending with '\r', this eats the line.
  /*
  // check the server prompt here:
  if ((pos = server_prompt.rfind('\r')) != std::string::npos) {
    // server_prompt contains \r, remove it.
    server_prompt = server_prompt.substr(pos + 1);
  }
  */

  while ((pos = server_prompt.find('\b')) != std::string::npos) {
    // backspace?  OK!  (unless)
    if (pos == 0) {
      // first character, so there's nothing "extra" to erase.
      server_prompt = server_prompt.erase(pos, 1);
    } else
      server_prompt = server_prompt.erase(pos - 1, 2);
  }

  if (!server_prompt.empty()) {
    // We have something remaining -- start the timer!
    set_prompt_timer();
  }
}

void Session::set_prompt_timer(void) {
  prompt_timer_.expires_after(std::chrono::milliseconds(time_ms));
  prompt_timer_.async_wait(boost::bind(&Session::on_prompt_timeout, this,
                                       boost::asio::placeholders::error));
}

void Session::reset_prompt_timer(void) { prompt_timer_.cancel(); }

// probably no longer needed --
void Session::on_server_prompt(const std::string &prompt,
                               const std::string &raw_prompt) {
  std::string temp = repr(prompt);
  BUGZ_LOG(warning) << "SP: [" << temp << "]";
  director.server_prompt(prompt, raw_prompt);
  if (server_telnet) {
    std::string ayt = std::string((const char *)"\x00\xff\xfd\xf6", 4);
    std::string ayt_resp = std::string((const char *)"\xff\xfb\x00", 3);
    std::string ayt2 = std::string((const char *)"\xff\xfb\x00", 3);
    std::string ayt2_resp = std::string((const char *)"\xff\xfd\x00", 3);

    /*
    for( const char & c : prompt ) {
      BUGZ_LOG(fatal) << "IS? " << (unsigned int)c << " " << std::hex <<
    (unsigned int)c;
    }

    for( const char & c : ayt ) {
      BUGZ_LOG(fatal) << "AYT? " << (unsigned int)c << " " << std::hex <<
    (unsigned int)c;
    }
    */

    if (prompt == ayt) {
      to_server(ayt_resp);
      BUGZ_LOG(fatal) << "AYT?";
      server_prompt.clear();
    }
    if (prompt == ayt2) {
      to_server(ayt2_resp);
      BUGZ_LOG(fatal) << "AYT2??";
      server_prompt.clear();
      // let the user see what is happening...  We're done negotiating (I think)
      director.show_client = true;
    }
  }
}

void Session::on_prompt_timeout(const boost::system::error_code error) {
  if (error != boost::asio::error::operation_aborted) {
    // Ok, VALID timeout
    if (!server_prompt.empty()) {
      // Here's what is happening:
      // SP: [ESC[2JESC[H]
      // which after clean_string is empty.

      std::string clean = server_prompt;  // clean_string(server_prompt);
      ansi_clean(clean);
      if (!clean.empty()) {
        on_server_prompt(clean, server_prompt);
      }
      // BUGZ_LOG(trace) << "SP: [" << server_prompt << "]";
    }
  }
}

void Session::server_read(void) {
  auto self(shared_from_this());

  boost::asio::async_read(
      server_, boost::asio::buffer(server_buffer, sizeof(server_buffer) - 1),
      boost::asio::transfer_at_least(1),
      [this, self](boost::system::error_code ec, std::size_t length) {
        if (!ec) {
          // server_buffer[length] = 0;

          // server_prompt.append(server_buffer, length);
          std::string received(server_buffer, length);
          process_lines(received);

          /*
          I don't believe I need to consume this,
          I'm not async_reading from a stream.
          */

          /*
            if (length) {
              // std::cout << length << std::endl;
              std::cout << "S: " << server_buffer << std::endl;
              do_write(server_buffer);
            }
            */

          server_read();
        } else {
          BUGZ_LOG(warning) << "S: read_failed: socket.shutdown()";
          connected = false;
          socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
        }
      });
}

void Session::on_resolve(
    const boost::system::error_code error,
    const boost::asio::ip::tcp::resolver::results_type results) {
  //
  auto self(shared_from_this());

  if (!error) {
    // Take the first endpoint.
    boost::asio::ip::tcp::endpoint const &endpoint = *results;

    server_.async_connect(endpoint,
                          boost::bind(&Session::on_connect, this,
                                      boost::asio::placeholders::error));

  } else {
    // TO DO:
    // BOOST_LOG_NAMED_SCOPE("Session");
    BUGZ_LOG(error) << "Unable to resolve: " << host;
    std::string output =
        str(boost::format("Unable to resolve: %1%\n\r") % host);
    to_client(output);
    BUGZ_LOG(warning) << "socket.shutdown()";
    socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
  }
}

void Session::client_input(const std::string &input) {
  std::string temp = repr(input);
  BUGZ_LOG(info) << "CI: [" << temp << "]";
  director.client_input(input);

#ifdef DECOUPLE
  // Is "proxy" active
  if (active) {
    // do something amazing with the user's input.
  } else {
    if (input == "\x1b" || input == "~") {
      std::string prompt = clean_string(get_prompt());
      BUGZ_LOG(trace) << "CI: ACTIVATE prompt shows: [" << prompt << "]";

      if (prompt == "Selection (? for menu): ") {
        to_client(
            "\n\rThere's not much we can do here.  Activate in-game at a "
            "Command prompt.\n\r");
        to_client(get_prompt());
        return;
      }

      // easter-eggs:

      if (prompt == "Enter your choice: ") {
        to_client(
            "\n\r\x1b[1;36mI'd choose \x1b[1;37m`T`\x1b[1;36m, but "
            "that's how I was coded.\n\r");
        to_client(get_prompt());
        return;
      }

      // easter-egg
      if (prompt == "[Pause]") {
        to_client(" \x1b[1;36mMeow\x1b[0m\n\r");
        to_client(get_prompt());
        return;
      }

      //
      // The command prompt that we're looking for:
      //
      // "Command [TL=00:00:00]:[242] (?=Help)? : "
      // the time, and the sector number vary...
      if (prompt.substr(0, 9) == "Command [") {
        int len = prompt.length();
        if (prompt.substr(len - 14) == "] (?=Help)? : ") {
          proxy_activate();
          /*
          to_client("\n\r\x1b[1;34mWELCOME!  This is where the proxy would "
                    "activate.\n\r");
          // active = true;
          // show_client = true;  // because if something comes (unexpected)
          // from the server? talk_direct = false;

          // but we aren't activating (NNY)
          to_client(get_prompt());
          */
          return;
        }
      }

      // eat this input.
      BUGZ_LOG(warning) << "CI: unable to activate, prompt was: [" << prompt
                        << "]";
      return;
    }
  }

  // as the above code matures, talk_direct might get changed.
  // keep this part here (and not above).
  if (talk_direct) {
    to_server(input);
  }

  if (emit_client_input) {
    emit_client_input(input);
  }
#endif
}

/*
DispatchSettings Session::save_settings(void) {
  DispatchSettings ss{emit_server_line, emit_server_prompt, emit_client_input,
                      show_client, talk_direct};
  return ss;
}

void Session::restore_settings(const DispatchSettings &ss) {
  emit_server_line = ss.server_line;
  emit_server_prompt = ss.server_prompt;
  emit_client_input = ss.client_input;
  show_client = ss.show_client;
  talk_direct = ss.talk_direct;
}

void Session::proxy_activate(void) {
  active = true;
  start_keepin_alive();  // kickstart the keepalive timer
  main.setNotify([this](void) { this->proxy_deactivate(); });
  main.activate();
}

void Session::proxy_deactivate(void) {
  // Ok, how do we return?
  active = false;
  to_client(get_prompt());
  // to_client(" \b");
}
*/

void Session::client_read(void) {
  auto self(shared_from_this());

  boost::asio::async_read(  // why can't I async_read_some here?
      socket_, boost::asio::buffer(read_buffer, sizeof(read_buffer) - 1),
      boost::asio::transfer_at_least(1),
      [this, self](boost::system::error_code ec, std::size_t length) {
        if (!ec) {
          // read_buffer[length] = 0;
          if (rlogin_auth.empty()) {
            // first read should be rlogin information
            rlogin_auth.assign(read_buffer, length);

            // parse authentication information
            parse_auth();
            to_client(std::string(1, 0));

            to_client("Welcome, ");
            to_client(rlogin_name);
            to_client("\n\r");

            // Activate the connection to the server

            /* // this fails, and I'm not sure why.  I've used code like this
               before. resolver_.async_resolve( host, port, std::bind(
               &Session::on_resolve, this, _1, _2)); */

            // This example shows using boost::bind, which WORKS.
            // https://stackoverflow.com/questions/6025471/bind-resolve-handler-to-resolver-async-resolve-using-boostasio
            resolver_.async_resolve(
                host, port,
                boost::bind(&Session::on_resolve, this,
                            boost::asio::placeholders::error,
                            boost::asio::placeholders::iterator));

          } else if (length) {
            // Proxy Active?
            // BOOST_LOG_NAMED_SCOPE("Session");

            std::string line(read_buffer, length);
            client_input(line);

            // do_write(output);
          }
          client_read();
        } else {
          BUGZ_LOG(warning) << "C: read_failed " << ec;
          if (connected) {
            BUGZ_LOG(warning) << "Server.shutdown()";
            server_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
          }
        }
      });
}

void Session::to_client(const std::string &message) {
  auto self(shared_from_this());
  // output the cleaned string (so I can see what we're sending in the
  // logs)

  std::string clean = clean_string(message);
  BUGZ_LOG(trace) << "2C: " << clean;

  boost::asio::async_write(
      socket_, boost::asio::buffer(message),
      [this, self](boost::system::error_code ec, std::size_t /*length*/) {
        if (!ec) {
        } else {
          BUGZ_LOG(warning)
              << "2C: write failed? closed? Server.shutdown()" << ec;
          if (connected) {
            BUGZ_LOG(warning) << "Server.shutdown()";
            server_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
          }
        }
      });
}

void Session::to_server(const std::string &message) {
  auto self(shared_from_this());
  BUGZ_LOG(trace) << "2S: " << message;
  boost::asio::async_write(
      server_, boost::asio::buffer(message),
      [this, self](boost::system::error_code ec, std::size_t /*length*/) {
        if (!ec) {
        } else {
          BUGZ_LOG(warning)
              << "S: write failed? closed? socket.shutdown() " << ec;
          // we're no longer connected.
          connected = false;
          socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
        }
      });

  if (director.active) {
    start_keepin_alive();
  }
}

void Session::start_keepin_alive(void) {
  // keep alive timer
  keep_alive_.expires_after(std::chrono::seconds(keepalive_secs));
  keep_alive_.async_wait(boost::bind(&Session::stayin_alive, this,
                                     boost::asio::placeholders::error));
}

void Session::stayin_alive(const boost::system::error_code error) {
  if (error != boost::asio::error::operation_aborted) {
    // stayin' alive, stayin' alive...
    if (director.active) {
      to_server(" ");
      BUGZ_LOG(warning) << "Session::stayin_alive()";
    }
  }
}

Server::Server(boost::asio::io_service &io_service,
               const boost::asio::ip::tcp::endpoint &endpoint,
               const std::string &host, const std::string &port,
               bool server_telnet_)
    : server_telnet{server_telnet_},
      io_service_{io_service},
      acceptor_{io_service_, endpoint},
      signal_{io_service, SIGUSR1, SIGTERM},
      host_{host},
      port_{port} {
  keep_accepting = true;

  BUGZ_LOG(info) << "Server::Server()";
  signal_.async_wait(boost::bind(&Server::on_signal, this,
                                 boost::asio::placeholders::error,
                                 boost::asio::placeholders::signal_number));
  do_accept();
}

void Server::on_signal(const boost::system::error_code &ec, int signal) {
  BUGZ_LOG(info) << "on_signal() :" << signal;
  keep_accepting = false;
  boost::system::error_code error;
  acceptor_.cancel(error);
  BUGZ_LOG(info) << "cancel: " << error;
  acceptor_.close(error);
  BUGZ_LOG(info) << "close: " << error;
}

Server::~Server() {
  CONFIG = YAML::Node();
  BUGZ_LOG(info) << "Server::~Server()";
}
/**
 * setup async connect accept
 *
 * This creates a session for each connection.  Using make_shared allows the
 * session to automatically clean up when it is no longer active/has anything
 * running in the reactor.
 */
void Server::do_accept(void) {
  acceptor_.async_accept([this](boost::system::error_code ec,
                                boost::asio::ip::tcp::socket socket) {
    if (!ec) {
      BUGZ_LOG(info) << "Server::do_accept()";
      std::make_shared<Session>(std::move(socket), io_service_, host_, port_,
                                server_telnet)
          ->start();
    }

    if (keep_accepting) {
      BUGZ_LOG(info) << "do_accept()";
      do_accept();
    }
  });
}