#include "server/application.hpp"
#include <ctime>

using namespace binom;

namespace {

std::string getHostStr(TcpServer::Client& client) {
  uint32_t ip = client.getHost ();
  return std::to_string(int(reinterpret_cast<char*>(&ip)[0])) + '.' +
         std::to_string(int(reinterpret_cast<char*>(&ip)[1])) + '.' +
         std::to_string(int(reinterpret_cast<char*>(&ip)[2])) + '.' +
         std::to_string(int(reinterpret_cast<char*>(&ip)[3])) + ':' +
         std::to_string(client.getPort());
}

}

bool strIsNumber(const std::string& s) {
    return !s.empty() && std::find_if(s.begin(),
        s.end(), [](unsigned char c) { return !std::isdigit(c); }) == s.end();
}

int main(int argc, char* argv[]) {Application(argc, argv); return EXIT_SUCCESS;}

Application::AppIniter Application::processArgs(int argc, char* argv[]) {
#define ifeq(str_1, str_2) if(isstreq(str_1, str_2))
#define elifeq(str_1, str_2) else if(isstreq(str_1, str_2))
  if(argc <= 1) return AppIniter{};

  enum class Token {
    flag,
    port,
    db_path,
    name
  }token = Token::flag;

  AppIniter app_init;
  for((--argc, ++argv); argc ;(--argc, ++argv)) {
    switch (token) {
      case Token::flag:
        ifeq(*argv, "--port") {
          token = Token::port;
        } elifeq(*argv, "-p") {
          token = Token::port;
        } elifeq(*argv, "--db-path") {
          token = Token::db_path;
        } elifeq(*argv, "-d") {
          token = Token::db_path;
        } elifeq(*argv, "--name") {
          token = Token::name;
        } elifeq(*argv, "-n") {
          token = Token::name;
        } elifeq(*argv, "--init-db") {
          std::string input;
          std::clog << "Do you rally want to initialize the database?\n"
                       "Print \"yes\" if you want initialize the database\n"
                       "Other answers will be perceived as no\n"
                       "> ";
          std::getline(std::cin, input, '\n');
          app_init.init_db = !strcmp(input.c_str(), "yes");
        }
      continue;
      case Token::port: {
        if(strIsNumber(*argv))
          app_init.server_port = atol(*argv);
        token = Token::flag;
      }continue;
      case Token::db_path:
        app_init.db_path = *argv;
        token = Token::flag;
      continue;
      case Token::name:
        app_init.server_name = *argv;
        token = Token::flag;
      continue;
    }
  }
  return app_init;
#undef ifeq
#undef elifeq
}


void Application::dataHandler(DataBuffer& data, TcpServer::Client& client) {
  try {
    std::clog << getHostStr(client) << " (sent data of "<< data.size << " bytes)\n";
    char connection_uuid[6];
    *reinterpret_cast<uint32_t*>(connection_uuid) = client.getHost();
    *reinterpret_cast<uint16_t*>(connection_uuid + 4) = client.getPort();
    RequestHandler(db,
                   server,
                   server_key,
                   client,
                   Variable::deserialize(ByteArray(data.data_ptr, data.size)),
                   db.getRoot()({"session", BufferArray(ValType::byte, connection_uuid, sizeof(connection_uuid))})
                   );
  } catch(Exception& e) {
    std::cerr << getHostStr(client) << " (err): " << e.what() << '\n';
    ByteArray result = Variable(vobj{{"act", ui8(ServerAction::err)}}).serialize();
    client.sendData(result.begin(), result.length());
  }
}

void Application::connectHandler(TcpServer::Client& client) {
  std::clog << getHostStr(client) << " (connected)\n";
  FileNodeVisitor conn_data = db.getRoot()("session");
  char uid[6];
  *reinterpret_cast<uint32_t*>(uid) = client.getHost();
  *reinterpret_cast<uint16_t*>(uid + 4) = client.getPort();
  conn_data.insert(BufferArray(ValType::byte, uid, sizeof (uid)), vobj{{"state",ui8(RequestHandler::ClientState::connected)}});
}

void Application::disconnectHandler(TcpServer::Client& client) {
  std::clog << getHostStr(client) << " (disconnected)\n";
  char uid[6];
  *reinterpret_cast<uint32_t*>(uid) = client.getHost();
  *reinterpret_cast<uint16_t*>(uid + 4) = client.getPort();
  db.getRoot()("session").remove(BufferArray(ValType::byte, uid, sizeof (uid)));
}


Application::Application(int argc, char* argv[]) try : Application(processArgs(argc, argv)) {}
catch(const Exception& e) {
  std::cerr << e.full() << '\n';
  std::exit(EXIT_FAILURE);
} catch(const std::exception& e) {
  std::cerr << e.what() << '\n';
  std::exit(EXIT_FAILURE);
} catch(...) {
  std::cerr << "Unexpected exception!\n";
  std::exit(EXIT_FAILURE);
}

Application::Application(Application::AppIniter app_initer)
  : db(app_initer.db_path,
       #include "server_db.hpp"
       , app_initer.init_db),

    server(app_initer.server_port,
           [this](DataBuffer data, TcpServer::Client& client){dataHandler(data, client);},
           [this](TcpServer::Client& client){connectHandler(client);},
           [this](TcpServer::Client& client){disconnectHandler(client);}),
    server_key(Security::genKey()) {

  // Server name init (will be used in the server federation system)
  if(FileNodeVisitor name = db.getRoot()({"server_data","name"}); app_initer.server_name) {
    if(name.getVariable().toBufferArray() != BufferArray(app_initer.server_name)) {
      name.setVariable(app_initer.server_name);
      std::clog << "Server human-readable name assigned\n"
                   "current server name = " << name.getVariable().toBufferArray().toString() << '\n';
    }
  } else if(!name.getElementCount()) {
    name.setVariable(
          BufferArray(
            Security::encodeBase64(
              Security::getHashOf(
                Security::extractPublicKey(server_key),
                Security::HashMethod::shake128
                )
              )
            )
          );
    std::clog << "Server base64 name assigned\n"
                 "current server name = " << name.getVariable().toBufferArray().toString() << "\n"
                 "Use `librehubd -n <your-server-name>` to assign human-readable name\n";
  } else std::clog << "current server name = " << name.getVariable().toBufferArray().toString() << '\n';

  // Server start
  if(server.start() == TcpServer::status::up) {
    std::clog<<"Server up on port: "<< app_initer.server_port <<std::endl;
    server.joinLoop(); // Joing to the client handling loop
  } else {
    std::cerr<<"Server start error! Error code:"<< int(server.getStatus()) << std::endl;
    std::exit(-1);
  }
}

