var libPath         = require('path');
var libUtil         = require('util');
var libFS           = require('fs');
var libChildProcess = require('child_process');
var libMySQL        = require('mysql2');
var libReadline     = require('readline');
var libNet          = require('net');
var libShaper       = require('shaper');

const gReadline = libReadline.createInterface({
    input: process.stdin,
    output: process.stdout
});


var gCommand              = process.argv[2];
var gExecutableDirectory  = libPath.dirname(libFS.realpathSync(__filename));
var gFSTemplatesDirectory = libPath.join(libPath.join(gExecutableDirectory, "mngr"), "templates");
var gDirectoryFromProject = "";
var gDirectoryProject     = "";
var gDirectoryFromPackage = "";
var gDirectoryFromFCF     = "";
var gPackage              = "";
var gLang                 = "en";
process.env.NODE_PATH     = gExecutableDirectory;
require("module").Module._initPaths();
var fcf                   = require('fcf');



var dir = process.cwd();
while(true) {
  var projectFile = libPath.join(dir, "__fcfproject");
  if (libFS.existsSync(projectFile)){
    gDirectoryProject = libPath.dirname(projectFile);
    break;
  }
  gDirectoryFromProject = libPath.join(libPath.basename(dir), gDirectoryFromProject);
  if (dir == "/" || (dir.indexOf(":\\") != 0 && dir.length == 3))
    break;
  dir = libPath.dirname(dir);
}

var dir = process.cwd();
while(true) {
  var projectFile = libPath.join(dir, "__fcfproject");
  if (libFS.existsSync(projectFile)){
    gDirectoryFromPackage = "";
    gDirectoryProject = libPath.dirname(projectFile);
    break;
  }

  var existsPackFile = libFS.existsSync(libPath.join(dir, "package.config")) ||
                       libFS.existsSync(libPath.join(dir, "theme.config"));

  if (existsPackFile && libFS.existsSync(libPath.join(dir, "fcfnode")))
    gPackage = "fcf";
  else if (existsPackFile)
    gPackage = fcf.getShortFileName(dir);

  if (gPackage != ""){
    break;
  }

  gDirectoryFromPackage = libPath.join(libPath.basename(dir), gDirectoryFromPackage);
  if (dir == "/" || (dir.indexOf(":\\") != 0 && dir.length == 3)) {
    gDirectoryFromPackage = "";
    break;
  }
  dir = libPath.dirname(dir);
}

if (process.platform == "linux"){
  var localeOutput = libChildProcess.execSync("locale").toString();
  var reg   = new RegExp("LANG=([a-z]{2})", "i");
  var match = localeOutput.match(reg);
  if (match !== null)
    gLang = match[1].toLowerCase();
}

var gTranslate = {
  ru: {
    "The template name for created files is not specified":
      "Не указано имя шаблона для создаваемых файлов",
    "Using: fcfmng COMMAND [COMMAND_ARGUMENTS]":
      "Использование: fcfmng КОМАНДА [АРГУМЕНТЫ_КОМАНДЫ]",
    "Performs auxiliary actions for creating and managing the FCF application":
      "Выполняет вспомогательные действия по созданию и управлению приложением FCF",
    "Commands:":
      "Команды:",
    "help|h                           Display this message":
      "help|h                           Выводит инструкцию использования",
    "create|c NAME [ARGUMENTS]        Creates files using the template specified in the NAME parameter":
      "create|c ИМЯ [АРГУМЕНТЫ]         Создает файлы по шаблону указанному в параметре ИМЯ",
    "show-templates|s                 Displays a list of available file templates with a description":
      "show-templates|s                 Выводит перечень доступных шаблонов файлов с описанием",
    "create-db JSAPPSCRIPT            Creates a database and user":
      "create-db JSAPPSCRIPT [OPTIONS]  Создает базу данных и пользователя",
    "                                 JSAPPSCRIPT - Javascript application file":
      "                                 JSAPPSCRIPT - Файл приложения Javascript ",
    "                                 OPTIONS - Advenced options:":
      "                                 OPTIONS - Расширенные опции:",
    "                                   --connections CONNECTIONS a string with information about connections":
      "                                   --connections CONNECTIONS строка с информацией о соединениях",
    "                                                             If the connection parameters are not set,":
      "                                                             Если параметры соединения не установлены,",
    "                                                             then a application settings are used":
      "                                                             то исполузуются настройки приложения",
    "                                   Example:":
      "                                   Пример:",
    "clear-node-modules               Deletes the installed NODEJS modules in the project":
      "clear-node-modules               Удаляет установленные NODEJS модули в проекте",
    "clear-cache                      Clearing application cache":
      "clear-cache                      Очистка кеша приложения",

    "Getting application settings...":
      "Получение настроек приложения...",
    "Getting information about databases...":
      "Получение информации о базах данных...",
    "Creating {1} database on the {2} host":
      "Создание дазы данных {1} на хосте {2}",
    "Update users...":
      "Обновление пользователей...",
    "Creating user {1}@{2}":
      "Создание пользователя {1}@{2}",
    "Configuring privileges for {1}@{2}":
      "Настройка прав доступа для {1}@{2}",
    "Update is completed":
      "Обновление завершено",
    "Application failed:":
      "Ошибка приложения:",
    "Please enter user for ":
      "Пожалуйста введите пользователя для ",
    "Please enter password for ":
      "Пожалуйста введите пароль для ",
    "Error: Invalid '{1}' database type for '{2}'@'{3}'":
      "Ошибка: Недопустимый тип базы данных '{1}' для '{2}'@'{3}'",


  }
}
for(var lang in gTranslate)
  for(var str in gTranslate[lang])
    gTranslate[lang][str.toLowerCase()] = gTranslate[lang][str];
function _t(a_str){
  var key = a_str.toLowerCase();
  return gLang in gTranslate && key in gTranslate[gLang]
    ? gTranslate[gLang][key]
    : a_str;
}

function _f(a_str) {
  for (var k = 1; k < arguments.length; ++k)
    a_str = a_str.replace("{" + k + "}", arguments[k]);
  return a_str;
}


function asyncQuery(a_connection, a_sql, a_act, a_cb){
  return a_connection.query(a_sql, function (a_err, a_result) {
    if (a_err){
      a_connection.end();
      if (a_act)
        a_act.error(a_err);
      return;
    }
    if (a_cb)
      a_cb(a_result);
  });
}

async function connectMySQL(a_connectionInfo){
  let connection = libMySQL.createConnection(a_connectionInfo);
  let firstError = undefined;
  try {
    await libUtil.promisify(connection.connect).bind(connection)();
  } catch(e){
    if (process.platform == "win32"){
      throw e;
    } else {
      let socket = "/run/mysqld/mysqld.sock";
      let sockets = ["/run/mysqld/mysqld.sock", "/tmp/mysql.sock", "/var/run/mysqld/mysql.sock", "/var/mysql/mysql.sock", "/private/tmp/mysql.sock"];
      fcf.each(sockets, (a_key, a_path)=>{
        if (libFS.existsSync(a_path)){
          socket = a_path;
          return false;
        }
      })
      let connectionInfo = fcf.append({}, a_connectionInfo, {socketPath: socket});
      connection = libMySQL.createPool(connectionInfo);
    }
  }
  return connection;
}

function deleteDir(a_path) {
  if (libFS.existsSync(a_path)) {
    if (libFS.lstatSync(a_path).isDirectory()) {
      libFS.readdirSync(a_path).forEach((file, index) => {
        const curPath = libPath.join(a_path, file);
        if (libFS.lstatSync(curPath).isDirectory())
          deleteDir(curPath);
        else
          libFS.unlinkSync(curPath);
      });
      libFS.rmdirSync(a_path);
    } else {
      libFS.unlinkSync(a_path);
    }
  }
}

function commandHelp(){
    console.log(_t("Using: fcfmng COMMAND [COMMAND_ARGUMENTS]"));
    console.log(_t("Performs auxiliary actions for creating and managing the FCF application"));
    console.log(_t(""));
    console.log(_t("Commands:"));
    console.log(_t("help|h                           Display this message"));
    console.log(_t("create|c NAME [ARGUMENTS]        Creates files using the template specified in the NAME parameter"));
    console.log(_t("show-templates|s                 Displays a list of available file templates with a description"));
    console.log(_t("create-db JSAPPSCRIPT            Creates a database and user"));
    console.log(_t("                                 JSAPPSCRIPT - Javascript application file"));
    console.log(_t("clear-node-modules               Deletes the installed NODEJS modules in the project"));
    console.log(_t("clear-cache                      Clearing application cache"));
    // console.log(_t("                                 OPTIONS - Advenced options:"));
    // console.log(_t("                                   --connections CONNECTIONS a string with information about connections"));
    // console.log(_t("                                                             If the connection parameters are not set,"));
    // console.log(_t("                                                             then a application settings are used"));
    // console.log(_t("                                   Example:"));
    // console.log(_t("                                   >$ fcfmngr create-db app.js --connections host1:user1:pass1,host2:user2:pass2"));
    console.log(_t(""));
}

function commandShowTemplates(){
  var items = libFS.readdirSync(gFSTemplatesDirectory);
  items.forEach(item => {
    console.log("Files template: " + item);
    var infoFilePath = libPath.join(libPath.join(gFSTemplatesDirectory, item), "info");
    if (libFS.existsSync(infoFilePath)){
      console.log("Info: ");
      var info = libFS.readFileSync(infoFilePath, {encoding: 'utf8'})
      console.log(info);
    }
    console.log("");

  });
}

function commandRemoveNodeModules(){
  try {
    let packageLockData = JSON.parse(libFS.readFileSync("package-lock.json", {encoding: 'utf8'}));
    if (packageLockData.dependencies || packageLockData.packages) {
      packageLockData.dependencies = {};
      packageLockData.packages = {};
      libFS.writeFileSync("package-lock.json", JSON.stringify(packageLockData, false, 2), {encoding: 'utf8'});
    }
  } catch(e) {
    console.warn("Failed to clear package-lock.json file.", e);
  }

  try {
    let packageLockData = JSON.parse(libFS.readFileSync("package.json", {encoding: 'utf8'}));
    if (packageLockData.dependencies) {
      packageLockData.dependencies = {};
      libFS.writeFileSync("package.json", JSON.stringify(packageLockData, false, 2), {encoding: 'utf8'});
    }
  } catch(e) {
    console.warn("Failed to clear package.json file.", e);
  }


  try {
    var items = libFS.readdirSync("node_modules");
    items.forEach((a_item) => {
      deleteDir(libPath.join("node_modules", a_item));
    });
  } catch(e) {
    console.warn("Failed to clear node_modules directory.", e);
  }
}

function commandCreateDB(a_jsScript, a_connectionsString){
  var auth = {};
  a_connectionsString.split(",").forEach((str) => {
    var host;
    str.split(":").forEach((value, key) => {
      if (key == 0)       { host = value; auth[host] = {host: host}; }
      else if (key == 1)  { auth[host].user = value; }
      else if (key == 2)  { auth[host].pass = value; }
    });
  });

  console.log(_t("Getting application settings..."));
  var fcfnodePath   = libPath.join(gExecutableDirectory, "fcfnode");
  var settings      = libChildProcess.execSync(fcfnodePath + " \""+ a_jsScript + "\" --get-settings").toString();
  var jsonSettings  = fcf.trim(settings.substring(settings.indexOf("FCF-SETTINGS:") + 13, settings.indexOf("FCF-ENDSETTINGS")));
  var settings      = JSON.parse(jsonSettings);
  var dbConnectionSettings = settings.dataClient && settings.dataClient.connections
                                  ? settings.dataClient.connections
                                  : {};


  var databasesWaitingCreated = {};
  var actions = fcf.actions();


  console.log(_t("Getting information about databases..."));

  actions.each(dbConnectionSettings, (a_cikey, a_ci, a_res, a_act) => {
    if (a_ci.type != "mysql" && a_ci.type != "mariadb") {
      console.error(_f(_t("Error: Invalid '{1}' database type for '{2}'@'{3}'"), a_ci.type,  a_ci.db, a_ci.host))
      process.exit(1);
    }
    a_act.complete();
  });


  actions.each(dbConnectionSettings, (a_cikey, a_ci, a_res, a_act) => {
    var ai = a_ci.host in auth ? auth[a_ci.host] : a_ci;

    async function clAction(a_ai, a_act){
      if (a_ci.type == "mysql" || a_ci.type == "mariadb") {
        var connectionInfo = {host: a_ai.host};
        if (a_ai.user != "")
          connectionInfo.user = a_ai.user
        if (!!a_ai.pass)
          connectionInfo.password = a_ai.pass;

        let connection = undefined;
        try {
          connection = await connectMySQL(connectionInfo)
        }catch(e){
          a_act.error(e);
          return;
        }

        asyncQuery(connection, "SHOW DATABASES", a_act, (a_result) =>{
          var found = fcf.find(a_result, (k, v)=>{ return v.Database == a_ci.db; }) != undefined;
          if (!found)
            databasesWaitingCreated[a_cikey] = a_ci;
          connection.end();
          a_act.complete();
        });

      } else {
        a_act.error(new Error(`Unknown type of database: "${a_ci.type}"`));
      }
    }

    if (!(a_ci.host in auth)){
      auth[a_ci.host] = fcf.append({}, a_ci);
      var property = {name: "user", message: _t("Please enter user for ") + a_ci.db + "@" + a_ci.host + _t(" database") };
      gReadline.question(_t("Please enter user for ") + a_ci.db + "@" + a_ci.host + _t(" database") + ": ", (a_user)=>{
        auth[a_ci.host].user = a_user;
        auth[a_ci.host].pass = undefined;
        fcf.actions()
        .then((a_res, a_subact)=>{
          clAction(auth[a_ci.host], a_subact);
        })
        .then(()=>{
          ai = auth[a_ci.host];
          ai.pass = undefined;
          a_act.complete();
        })
        .catch(()=>{
          var property = {name: "pass", message: _t("Please enter password for ") + a_ci.db + "@" + a_ci.host + _t(" database") };
          gReadline.question(_t("Please enter password for ") + a_ci.db + "@" + a_ci.host + _t(" database") + ": ", (a_pass)=>{
            auth[a_ci.host].pass = a_pass;
            ai = auth[a_ci.host];
            gReadline.close();
            clAction(auth[a_ci.host], a_act);
          })
        })
      });
    }


  })

  // Creation of databases
  actions.each(()=>{return databasesWaitingCreated}, async (a_cikey, a_ci, a_res, a_act) => {
    var ai = a_ci.host in auth ? auth[a_ci.host] : a_ci;
    if (a_ci.type == "mysql" || a_ci.type == "mariadb") {
      var connection = undefined;
      try {
        connection = await connectMySQL({ host: ai.host, user: ai.user, password: ai.pass })
      } catch(e){
        a_act.error(e);
        return;
      }
      console.log(_f(_t("Creating {1} database on the {2} host"), a_ci.db, a_ci.host));
      var sql = _f("CREATE DATABASE `{1}` CHARACTER SET utf8 COLLATE utf8_unicode_ci", a_ci.db);
      console.log("SQL>",sql);
      asyncQuery(connection, sql, a_act, (a_result) =>{
        connection.end();
        a_act.complete();
      });

    } else {
      a_act.complete();
    }
  })

  // Update users
  .then(()=>{
    console.log(_t("Update users..."));
    return fcf.actions().each(dbConnectionSettings, async (a_cikey, a_ci, a_res, a_act) => {
      var ai = a_ci.host in auth ? auth[a_ci.host] : a_ci;
      if (a_ci.type == "mysql" || a_ci.type == "mariadb") {
        var connection = undefined;
        try {
          connection = await connectMySQL({ host: ai.host, user: ai.user, password: ai.pass });
        } catch (e){
          a_act.error(e);
          return;
        }

        asyncQuery(connection, _f("select user, host from mysql.user"), a_act, (a_result) =>{
          var subActions = fcf.actions();
          fcf.each(a_result, (k,obj) => {
            fcf.each(obj, (k,v) => {
              obj[k.toLowerCase()] = v;
            });

          })
          if (fcf.find(a_result, (k,v)=>{ return a_ci.user == v.user && a_ci.host == v.host }) === undefined) {
            subActions.then(async (a_res, a_subact) => {
              connection.end();
              try {
                connection = await connectMySQL({ host: ai.host, user: ai.user, password: ai.pass });
              } catch (e){
                a_subact.error(e);
                return;
              }
              console.log(_f(_t("Creating user {1}@{2}"), a_ci.user, a_ci.host));
              var sql = _f("CREATE USER '{1}'@'{2}' IDENTIFIED BY '{3}'", a_ci.user, a_ci.host, a_ci.pass);


              console.log("SQL>",sql);
              asyncQuery(connection, sql, a_subact, (a_result) =>{
                connection.end();
                a_subact.complete();
              });
            });
          }

          if (fcf.find(a_result, (k,v)=>{ return a_ci.user == v.user && a_ci.host == v.host && a_ci.type == "mysql" }) === undefined ||
              fcf.find(databasesWaitingCreated, (k,v)=>{return a_ci.host == v.host && a_ci.db == v.db && a_ci.type == "mysql"}) !== undefined) {
            subActions.then(async (a_res, a_subact) => {
              try {
                connection = await connectMySQL({ host: ai.host, user: ai.user, password: ai.pass });
              } catch (e){
                a_subact.error(e);
                return;
              }
              console.log(_f(_t("Configuring privileges for {1}@{2}"), a_ci.user, a_ci.host));
              var sql = _f("GRANT ALL PRIVILEGES ON `{1}`.* TO '{2}'@'{3}';", a_ci.db, a_ci.user, a_ci.host);
              console.log("SQL>",sql);
              asyncQuery(connection, sql, a_subact, (a_result) =>{
                connection.end();
                a_subact.complete();
              });
            });
          }

          subActions.then(() => {
            a_act.complete();
          });

          subActions.catch((a_error)=>{
            a_act.error(a_error);
          })
        });

      } else {
        a_act.complete();
      }
    });
  })


  .then(()=>{
    console.log(_t("Update is completed"));
    process.exit(0);
  })

  .catch((a_error)=>{
    console.error("");
    console.error(_t("Application failed:"));
    console.error(a_error.message);
    process.exit(1);
  })
}

function commandClearCache(){
  if (!gDirectoryProject)
    throw new Error("Can't find root project directory");

  console.log("Cache clearing ...");

  let cacheDirectory = libPath.join(gDirectoryProject, "cache");
  let cacheFSItems = ["js", "jscompress", "jses5"];
  for(let i = 0; i < cacheFSItems.length; ++i) {
    let path = libPath.join(cacheDirectory, cacheFSItems[i]);
    deleteDir(path);
  }
}

function commandCreateTemplate(a_templateName, a_templateArgs){
  var templateDirectory = libPath.join(gFSTemplatesDirectory, a_templateName);

  function replaceContext(a_data){
    var reg   = new RegExp("__aFCFGENARG_([0-9A-Z_]+)z__", "g");
    var match = a_data.match(reg);
    if (match !== null){
      var keys = {};
      match.forEach(substr => {
        var key = substr.substr("__aFCFGENARG_".length, substr.length - "__aFCFGENARG_z__".length);
        keys[key] = true;
      });

      for(var k in keys){
        let argValue;
        var replaceSubstr = "__aFCFGENARG_" + k + "z__";
        if (!isNaN(Number.parseInt(k))){
          argValue = a_templateArgs[Number.parseInt(k)-1];
        } else if (k == "DIRECTORY") {
          argValue = gPackage == "" ? gDirectoryFromProject : gDirectoryFromPackage;
        } else if (k == "DIRECTORY_BEFORE_SLASH") {
          argValue = gPackage == "" ? gDirectoryFromProject : gDirectoryFromPackage;
          argValue = argValue != "" && gPackage != "fcf" ? "/" + argValue : argValue;
        } else if (k == "DIRECTORY_SLASH") {
          argValue = gPackage == ""    ? gDirectoryFromProject :
                                         gDirectoryFromPackage;
          argValue = argValue == ""     ? "" :
                                          argValue + "/";
        } else if (k == "PACKAGE") {
          argValue = gPackage;
        } else if (k == "PACKAGE_COLON") {
          argValue = gPackage != "" ? gPackage + ":" : "";
        } else if (k == "ROOTOBJECTPATH") {
          argValue = gPackage == "fcf" ? "" :
                         gPackage != ""    ? "packages/" + gPackage :
                                             "root";
        } else if (k == "EXTENDS_TEMPLATE") {
          let file = a_templateArgs[1];
          if (!fcf.empty(file))
            argValue = `extends:"${file}"`;
          else
            argValue = `// extends:"<super_template.tmpl>"`;
        } else if (k == "EXTENDS_WRAPPER") {
          let file = a_templateArgs[1];
          if (!fcf.empty(file))
            argValue = file.substr(file.length - ".tmpl".length, ".tmpl".length) == ".tmpl"
                              ? "\"" + file.substr(0, file.length - ".tmpl".length) + ".wrapper.js" + "\"" :
                       file.substr(file.length - ".wrapper.js".length, ".wrapper.js".length) == ".wrapper.js"
                              ? "\"" + file + "\"" :
                                "\"" + file  + ".wrapper.js" + "\"";
          else
            argValue = '"fcf:NClient/Wrapper.js"';
        } else {
          continue;
        }
        a_data  = a_data.split(replaceSubstr).join(argValue);
      }
    }

    return a_data;
  }

  function copyFile(a_relativePath, a_relativeDir) {
    var realFilePath    = libPath.join(templateDirectory, a_relativePath);
    var dstFilePath     = replaceContext(a_relativePath);
    var dstDir          = replaceContext(a_relativeDir);

    var data;
    if (["ico", "png", "jpg", "jpeg"].indexOf(fcf.getExtension(a_relativePath).toLowerCase()) == -1){
      data = replaceContext(libFS.readFileSync(realFilePath, {encoding: 'utf8'}));
    } else {
      data = libFS.readFileSync(realFilePath);
    }
    if (dstDir !== "" && !libFS.existsSync(dstDir)) {
      libFS.mkdirSync(dstDir, { recursive: true });
    }
    libFS.writeFileSync(dstFilePath, data);
  }

  function readDirectory(a_dir, a_root) {
    var realDir = a_dir == "" ? templateDirectory
                              : libPath.join(templateDirectory, a_dir);
    var items = libFS.readdirSync(realDir);
    items.forEach(element => {
      if (a_root && element=="info")
        return;
      var elementPath = libPath.join(realDir, element);
      var elementStat = libFS.lstatSync(elementPath);
      if (elementStat.isDirectory()){
        readDirectory(libPath.join(a_dir, element));
      } else {
        copyFile(libPath.join(a_dir, element), a_dir);
      }
    });
  }

  readDirectory("", true);
}

try {
  if (gCommand == "create" || gCommand == "--create" || gCommand == "-c" || gCommand == "c"){
    if (!process.argv[3]){
      console.error(_t("The template name for created files is not specified"));
      process.exit(1);
    }
    commandCreateTemplate(process.argv[3], process.argv.slice(4));
    process.exit(0);
  } else if (gCommand == "show-templates" || gCommand == "--show-templates" || gCommand == "-s" || gCommand == "s"){
    commandShowTemplates();
    process.exit(0);
  } else if (gCommand == "clear-node-modules") {
    commandRemoveNodeModules();
    process.exit(0);
  } else if (gCommand == "create-db" || gCommand == "--create-db"){
    var connectionsArgKey = fcf.inc(fcf.find(process.argv, "--connections"));
    var connections       = fcf.empty(process.argv[connectionsArgKey]) ? "" : process.argv[connectionsArgKey];
    commandCreateDB(process.argv[3], connections);
  } else if (gCommand == "clear-cache") {
    commandClearCache();
    process.exit(0);
  } else if (gCommand == "help" || gCommand == "--help" || gCommand == "-h" || gCommand == "h"){
    commandHelp();
    process.exit(1);
  } else {
    commandHelp();
    process.exit(1);
  }
} catch(e){
  console.error(e.message);
  process.exit(1);
}
