// ===============================
//  PC-BSD REST API Server
// Available under the 3-clause BSD License
// Written by: Ken Moore <ken@pcbsd.org> July 2015
// =================================
#include "globals.h"

#include <unistd.h>
#include <sys/types.h>

#include "WebServer.h"

#define CONFFILE "/usr/local/etc/sysadm.conf"
#define SETTINGSFILE "/var/db/sysadm.ini"

#define DEBUG 0

//Create any global classes
QSettings *CONFIG = new QSettings(SETTINGSFILE, QSettings::IniFormat);
EventWatcher *EVENTS = new EventWatcher();
Dispatcher *DISPATCHER = new Dispatcher();
bool WS_MODE = false;

//Set the defail values for the global config variables
int BlackList_BlockMinutes = 60;
int BlackList_AuthFailsToBlock = 5;
int BlackList_AuthFailResetMinutes = 10;
bool BRIDGE_ONLY = false;

//Create the default logfile
QFile logfile;
void MessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg){
  QString txt;
  switch(type){
  case QtDebugMsg:
  	  txt = msg;
  	  break;
  case QtWarningMsg:
  	  txt = QString("WARNING: %1").arg(msg);
          txt += "\n Context: "+QString(context.file)+" Line: "+QString(context.line)+" Function: "+QString(context.function);
  	  break;
  case QtCriticalMsg:
  	  txt = QString("CRITICAL: %1").arg(msg);
	  txt += "\n Context: "+QString(context.file)+" Line: "+QString(context.line)+" Function: "+QString(context.function);
  	  break;
  case QtFatalMsg:
  	  txt = QString("FATAL: %1").arg(msg);
	  txt += "\n Context: "+QString(context.file)+" Line: "+QString(context.line)+" Function: "+QString(context.function);
  	  break;
  }

  QTextStream out(&logfile);
  out << txt;
  if(!txt.endsWith("\n")){ out << "\n"; }
}

inline QString ReadFile(QString path){
  QFile file(path);
  if( !file.open(QIODevice::ReadOnly) ){ return ""; }
  QTextStream in(&file);
  QString str = in.readAll();
  file.close();
  return str;
}

void showUsage(){
qDebug() << "sysadm-binary usage:";
qDebug() << "Starting the server:";
qDebug() << "    \"sysadm-binary [-rest] [-port <portnumber>]\"";
qDebug() << "CLI flags for configuring the server:";
qDebug() << "  \"-h\" or \"help\": Show this help text";
qDebug() << "  \"import_ssl_file <username> <filepath> <nickname> [<email>]\": Loads a .crt or .key file and enables the public key for authorization access later";
qDebug() << "Configuring server->bridge connections (websockets only):";
qDebug() << "  \"bridge_list\": Show all bridges that are currently setup";
qDebug() << "  \"bridge_add <nickname> <url>\":  Create a new bridge connection with the given nickname";
qDebug() << "  \"bridge_remove <nickname>\": Remove the bridge connection with the given nickname";
qDebug() << "  \"bridge_export_key [file]\": Export the public SSL key the server uses to connect to bridges";
}

int main( int argc, char ** argv )
{
    
    //Check whether running as root
    if( getuid() != 0){
      qDebug() << "sysadm-server must be started as root!";
      return 1;
    }

    //Evaluate input arguments
    bool websocket = true;
    bool setonly = false;
    quint16 port = 0;
    for(int i=1; i<argc; i++){
      if( QString(argv[i])=="-rest" ){ websocket = false;}
      else if( QString(argv[i])=="-p" && (i+1<argc) ){ i++; port = QString(argv[i]).toUInt(); }
      else if( QString(argv[i])=="-h" || QString(argv[i]).contains("help") ){ showUsage(); return 0; }
      else if( QString(argv[i]).startsWith("bridge_") ){
        setonly = true;
        QString opt = QString(argv[i]).section("_",1,-1);
        if(opt=="list"){
          QStringList bridges = CONFIG->allKeys().filter("bridge_connections/");
          qDebug() << "Current Bridges:";
          for(int i=0; i<bridges.length(); i++){
            qDebug() << bridges[i].section("/",1,-1) + " ("+CONFIG->value(bridges[i]).toString()+")";
          }
        }else if(opt=="add" && argc > i+2){
          QString name = QString(argv[i+1]);
          QString url = QString(argv[i+2]);
          CONFIG->setValue("bridge_connections/"+name, url);
	  qDebug() << "New Bridge Added:" << name+" ("+url+")";
          i=i+2;
        }else if(opt=="remove" && argc>i+1){
          QString name = QString(argv[i+1]);
          CONFIG->remove("bridge_connections/"+name);
          qDebug() << "Bridge Removed:" << name;
          i=i+1;
        }else if(opt=="export_key"){
          //Export the public SSL cert used for establishing a connection with a bridge
          QFile cfile("/usr/local/etc/sysadm/ws_bridge.crt");
          if( cfile.open(QIODevice::ReadOnly) ){
            QSslCertificate cert(&cfile);
            cfile.close();
            if(!cert.isNull()){
              if(i+1<argc){ 
                i++; QString filepath = argv[i];
	        QFile outfile(filepath);
                  outfile.open(QIODevice::WriteOnly | QIODevice::Truncate);
                  outfile.write(cert.publicKey().toPem());
                outfile.close();
                qDebug() << "Public Key Saved to file:" << filepath;
              }else{
                //Output to std out instead
               qDebug() << cert.publicKey().toPem();
              }
            }
          }
        }else{
          qDebug() << "Unknown option:" << argv[i];
          return 1;
        }
      }else if(QString(argv[i])=="import_ssl_file" && i+3<argc){
        setonly = true;
        //Load CLI inputs
        i++; QString user(argv[i]); //username
        i++; QByteArray key(argv[i]); //key file
        i++; QString nickname(argv[i]); // nickname for key
        QString email; if(i+1<argc){ i++; email = QString(argv[i]); } //email address
        //Read the key file
        QFile file(key);
 	if(!file.open(QIODevice::ReadOnly)){ qDebug() << "Could not open file:" << file.fileName(); }
        else{
          QByteArray enc_key;
          if(file.fileName().endsWith(".crt")){ 
            QSslCertificate cert(&file, QSsl::Pem); 
            if(!cert.isNull()){ enc_key = cert.publicKey().toPem(); }
          }else if(file.fileName().endsWith(".key")){ 
            QSslKey key( &file, QSsl::Rsa, QSsl::Pem, QSsl::PublicKey); 
            if(!key.isNull()){ enc_key = key.toPem(); }
           }else{
             qDebug() << "Error: Unknown file type (need .crt or .key file)";
           }
          file.close();
          if(enc_key.isEmpty()){ qDebug() << "ERROR: Could not read file"; }
          else{
            bool ok = AuthorizationManager::RegisterCertificateInternal(user, enc_key, nickname, email);
            if(ok){ qDebug() << "Key Added" << user << nickname; }
            else{ qDebug() << "Could not add key"; }
          }
	}
	//See if the key is a file instead - then read it
        /*bool ok = true;
	if(QFile::exists(key)){
	  QFile file(key);
          QByteArray pubkey;
          if(file.open(QIODevice::ReadOnly)){  
            QSslKey sslkey( &file, QSsl::Rsa, QSsl::Pem, QSsl::PublicKey); 
            if(!key.isNull()){ pubkey = sslkey.toPem(); }
            else{ qDebug() << "Invalid Key file:" << file.fileName(); ok = false; }
            file.close();
          }else{ qDebug() << "Could not open file:" << file.fileName(); ok = false; }
        }	
	if(ok){ ok = AuthorizationManager::RegisterCertificateInternal(user, key, nickname, email); }
	if(ok){ qDebug() << "Key Added" << user << nickname; }
        else{ qDebug() << "Could not add key"; } */

      }else{
        qDebug() << "Unknown option:" << argv[1];
        return 1;
      }
    }
    if(setonly){ CONFIG->sync(); return 0; }
    WS_MODE = websocket; //set the global variable too

    QCoreApplication a(argc, argv);
    //Now load the config file
    QStringList conf = ReadFile(CONFFILE).split("\n");
    if(!conf.filter("[internal]").isEmpty()){
      //Older QSettings file - move it to the new location
      if(QFile::exists(SETTINGSFILE)){ QFile::remove(SETTINGSFILE); } //remove the new/empty settings file
      QFile::rename(CONFFILE, SETTINGSFILE);
      CONFIG->sync(); //re-sync settings structure
      conf.clear(); //No config yet
    }
    //Load the settings from the config file
    // - port number
    if(port==0){
      if(websocket){ 
	int index = conf.indexOf(QRegExp("PORT=*",Qt::CaseSensitive,QRegExp::Wildcard));
	bool ok = false;
	if(index>=0){ port = conf[index].section("=",1,1).toInt(&ok); }
	if(port<=0 || !ok){ port = WSPORTNUMBER;  }
      }else{
	int index = conf.indexOf(QRegExp("PORT_REST=*",Qt::CaseSensitive,QRegExp::Wildcard));
	bool ok = false;
	if(index>=0){ port = conf[index].section("=",1,1).toInt(&ok); }
	if(port<=0 || !ok){ port = PORTNUMBER;  }	      
      }
    }
    // - Blacklist options
    QRegExp rg = QRegExp("BLACKLIST_BLOCK_MINUTES=*",Qt::CaseSensitive,QRegExp::Wildcard);
    if(!conf.filter(rg).isEmpty()){
      bool ok = false;
      int tmp = conf.filter(rg).first().section("=",1,1).simplified().toInt(&ok);
      if(ok){ BlackList_BlockMinutes = tmp; }
    }
    rg = QRegExp("BLACKLIST_AUTH_FAIL_LIMIT=*",Qt::CaseSensitive,QRegExp::Wildcard);
    if(!conf.filter(rg).isEmpty()){
      bool ok = false;
      int tmp = conf.filter(rg).first().section("=",1,1).simplified().toInt(&ok);
      if(ok){ BlackList_AuthFailsToBlock = tmp; }
    }
    rg = QRegExp("BLACKLIST_AUTH_FAIL_RESET_MINUTES=*",Qt::CaseSensitive,QRegExp::Wildcard);
    if(!conf.filter(rg).isEmpty()){
      bool ok = false;
      int tmp = conf.filter(rg).first().section("=",1,1).simplified().toInt(&ok);
      if(ok){ BlackList_AuthFailResetMinutes = tmp; }
    }
    rg = QRegExp("BRIDGE_CONNECTIONS_ONLY=*",Qt::CaseSensitive,QRegExp::Wildcard);
    if(!conf.filter(rg).isEmpty()){
      BRIDGE_ONLY = conf.filter(rg).first().section("=",1,1).simplified().toLower()=="true";
    }    

    //Setup the log file
    LogManager::checkLogDir(); //ensure the logging directory exists
    if(!websocket){ logfile.setFileName("/var/log/sysadm-server-tcp.log"); }
    else{ logfile.setFileName("/var/log/sysadm-server-ws.log"); }
    if(DEBUG){ qDebug() << "Log File:" << logfile.fileName(); }
      if(QFile::exists(logfile.fileName()+".old")){ QFile::remove(logfile.fileName()+".old"); }
      if(logfile.exists()){ QFile::rename(logfile.fileName(), logfile.fileName()+".old"); }
      //Make sure the parent directory exists
      if(!QFile::exists("/var/log")){
        QDir dir;
        dir.mkpath("/var/log");
      }
      logfile.open(QIODevice::WriteOnly | QIODevice::Append);
      qInstallMessageHandler(MessageOutput);
      
    //Connect the background classes
    QObject::connect(DISPATCHER, SIGNAL(DispatchEvent(QJsonObject)), EVENTS, SLOT(DispatchEvent(QJsonObject)) );
    QObject::connect(DISPATCHER, SIGNAL(DispatchStarting(QString)), EVENTS, SLOT(DispatchStarting(QString)) );
      
    //Create the daemon
    qDebug() << "Starting the PC-BSD sysadm server...." << (websocket ? "(WebSocket)" : "(TCP)");
    WebServer *w = new WebServer(); 
    //Start the daemon
    int ret = 1; //error return value
    if( w->startServer(port, websocket) ){
      qDebug() << " - Configuration File:" << CONFIG->fileName();
      QThread TBACK, TBACK2;
      EVENTS->moveToThread(&TBACK);
      DISPATCHER->moveToThread(&TBACK2);
      TBACK.start();
      TBACK2.start();
      QTimer::singleShot(0,EVENTS, SLOT(start()) );
      //Now start the main event loop
      ret = a.exec();
      qDebug() << "Server Stopped:" << QDateTime::currentDateTime().toString(Qt::ISODate);
      //TBACK.stop();
    }else{
      qDebug() << "[FATAL] Server could not be started:" << QDateTime::currentDateTime().toString(Qt::ISODate);
      qDebug() << " - Tried port:" << port;
    }
    //Cleanup any globals
    delete CONFIG;
    logfile.close();
    
    //Return
    return ret;
}
