const path          = require('path');
const fs            = require('fs');
const os            = require('os');
const spawn         = require('child_process').spawn;
const chalk         = require('ansis');

const readline = require('readline')
const which = require('../../tools/which.js')
const sexec = require('../../tools/sexec.js')
const copydirSync = require('../../tools/copydirSync.js')
const deleteFolderRecursive = require('../../tools/deleteFolderRecursive.js')

var Configuration = require('../../Configuration.js');
var cst           = require('../../../constants.js');
var Common        = require('../../Common');
var Utility       = require('../../Utility.js');

module.exports = {
  install,
  uninstall,
  start,
  publish,
  generateSample,
  localStart,
  getModuleConf
}

/**
 * PM2 Module System.
 * Features:
 * - Installed modules are listed separately from user applications
 * - Always ON, a module is always up along PM2, to stop it, you need to uninstall it
 * - Install a runnable module from NPM/Github/HTTP (require a package.json only)
 * - Some modules add internal PM2 depencencies (like typescript, profiling...)
 * - Internally it uses NPM install (https://docs.npmjs.com/cli/install)
 * - Auto discover script to launch (first it checks the apps field, then bin and finally main attr)
 * - Generate sample module via pm2 module:generate <module_name>
 */

function localStart(PM2, opts, cb) {
  var proc_path = '',
      cmd  = '',
      conf = {};

  Common.printOut(cst.PREFIX_MSG_MOD + 'Installing local module in DEVELOPMENT MODE with WATCH auto restart');
  proc_path = process.cwd();

  cmd = path.join(proc_path, cst.DEFAULT_MODULE_JSON);

  Common.extend(opts, {
    cmd : cmd,
    development_mode : true,
    proc_path : proc_path
  });

  return StartModule(PM2, opts, function(err, dt) {
    if (err) return cb(err);
    Common.printOut(cst.PREFIX_MSG_MOD + 'Module successfully installed and launched');
    return cb(null, dt);
  });
}

function generateSample(app_name, cb) {
  var rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  });

  function samplize(module_name) {
    var cmd1 = 'git clone https://github.com/pm2-hive/sample-module.git ' + module_name + '; cd ' + module_name + '; rm -rf .git';
    var cmd2 = 'cd ' + module_name + ' ; sed -i "s:sample-module:'+ module_name  +':g" package.json';
    var cmd3 = 'cd ' + module_name + ' ; npm install';

    Common.printOut(cst.PREFIX_MSG_MOD + 'Getting sample app');

    sexec(cmd1, function(err) {
      if (err) Common.printError(cst.PREFIX_MSG_MOD_ERR + err.message);
      sexec(cmd2, function(err) {
        console.log('');
        sexec(cmd3, function(err) {
          console.log('');
          Common.printOut(cst.PREFIX_MSG_MOD + 'Module sample created in folder: ', path.join(process.cwd(), module_name));
          console.log('');
          Common.printOut('Start module in development mode:');
          Common.printOut('$ cd ' + module_name + '/');
          Common.printOut('$ pm2 install . ');
          console.log('');

          Common.printOut('Module Log: ');
          Common.printOut('$ pm2 logs ' + module_name);
          console.log('');
          Common.printOut('Uninstall module: ');
          Common.printOut('$ pm2 uninstall ' + module_name);
          console.log('');
          Common.printOut('Force restart: ');
          Common.printOut('$ pm2 restart ' + module_name);
          return cb ?  cb() : false;
        });
      });
    });
  }

  if (app_name) return samplize(app_name);

  rl.question(cst.PREFIX_MSG_MOD + "Module name: ", function(module_name) {
    samplize(module_name);
  });
}

function publish(opts, cb) {
  var rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  });

  var semver = require('semver');

  var package_file = path.join(process.cwd(), 'package.json');

  var package_json = require(package_file);

  package_json.version = semver.inc(package_json.version, 'minor');
  Common.printOut(cst.PREFIX_MSG_MOD + 'Incrementing module to: %s@%s',
                  package_json.name,
                  package_json.version);


  rl.question("Write & Publish? [Y/N]", function(answer) {
    if (answer != "Y")
      return cb();


    fs.writeFile(package_file, JSON.stringify(package_json, null, 2), function(err, data) {
      if (err) return cb(err);

      Common.printOut(cst.PREFIX_MSG_MOD + 'Publishing module - %s@%s',
                      package_json.name,
                      package_json.version);

      sexec('npm publish', function(code) {
        Common.printOut(cst.PREFIX_MSG_MOD + 'Module - %s@%s successfully published',
                        package_json.name,
                        package_json.version);

        Common.printOut(cst.PREFIX_MSG_MOD + 'Pushing module on Git');
        sexec('git add . ; git commit -m "' + package_json.version + '"; git push origin master', function(code) {

          Common.printOut(cst.PREFIX_MSG_MOD + 'Installable with pm2 install %s', package_json.name);
          return cb(null, package_json);
        });
      });
    });

  });
}

function moduleExistInLocalDB(CLI, module_name, cb) {
  var modules = Configuration.getSync(cst.MODULE_CONF_PREFIX);
  if (!modules) return cb(false);
  var module_name_only = Utility.getCanonicModuleName(module_name)
  modules = Object.keys(modules);
  return cb(modules.indexOf(module_name_only) > -1 ? true : false);
};

function install(CLI, module_name, opts, cb) {
  moduleExistInLocalDB(CLI, module_name, function (exists) {
    if (exists) {
      Common.logMod('Module already installed. Updating.');

      Rollback.backup(module_name);

      return uninstall(CLI, module_name, function () {
        return continueInstall(CLI, module_name, opts, cb);
      });
    }
    return continueInstall(CLI, module_name, opts, cb);
  })
}

// Builtin Node Switch
function getNPMCommandLine(module_name, install_path) {
  if (which('bun')) {
    return spawn.bind(this, 'bun', ['install', module_name, '--loglevel=error', '--cwd', install_path], {
      stdio : 'inherit',
      env: process.env,
      windowsHide: true
    })
  }
  else if (which('npm')) {
    return spawn.bind(this, cst.IS_WINDOWS ? 'npm.cmd' : 'npm', ['install', module_name, '--loglevel=error', '--prefix', install_path], {
      stdio : 'inherit',
      env: process.env,
      windowsHide: true,
      shell : cst.IS_WINDOWS
    })
  }
  else {
    return spawn.bind(this, cst.BUILTIN_NODE_PATH, [cst.BUILTIN_NPM_PATH, 'install', module_name, '--loglevel=error', '--prefix', install_path], {
      stdio : 'inherit',
      env: process.env,
      windowsHide: true
    })
  }
}

function continueInstall(CLI, module_name, opts, cb) {
  Common.printOut(cst.PREFIX_MSG_MOD + 'Calling ' + chalk.bold.red('[NPM]') + ' to install ' + module_name + ' ...');

  var canonic_module_name = Utility.getCanonicModuleName(module_name);
  var install_path = path.join(cst.DEFAULT_MODULE_PATH, canonic_module_name);

  fs.promises.mkdir(install_path, { recursive: true })
    .then(function() {
      process.chdir(os.homedir());

      var install_instance = getNPMCommandLine(module_name, install_path)();

      install_instance.on('close', finalizeInstall);

      install_instance.on('error', function (err) {
        console.error(err.stack || err);
      });
    });

  function finalizeInstall(code) {
    if (code != 0) {
      // If install has failed, revert to previous module version
      return Rollback.revert(CLI, module_name, function() {
        return cb(new Error('Installation failed via NPM, module has been restored to prev version'));
      });
    }

    Common.printOut(cst.PREFIX_MSG_MOD + 'Module downloaded');

    var proc_path = path.join(install_path, 'node_modules', canonic_module_name);
    var package_json_path = path.join(proc_path, 'package.json');

    // Append default configuration to module configuration
    try {
      var conf = JSON.parse(fs.readFileSync(package_json_path).toString()).config;

      if (conf) {
        Object.keys(conf).forEach(function(key) {
          Configuration.setSyncIfNotExist(canonic_module_name + ':' + key, conf[key]);
        });
      }
    } catch(e) {
      Common.printError(e);
    }

    opts = Common.extend(opts, {
      cmd : package_json_path,
      development_mode : false,
      proc_path : proc_path
    });

    Configuration.set(cst.MODULE_CONF_PREFIX + ':' + canonic_module_name, {
      uid : opts.uid,
      gid : opts.gid
    }, function(err, data) {
      if (err) return cb(err);

      StartModule(CLI, opts, function(err, dt) {
        if (err) return cb(err);

        if (process.env.PM2_PROGRAMMATIC === 'true')
          return cb(null, dt);

        CLI.conf(canonic_module_name, function() {
          Common.printOut(cst.PREFIX_MSG_MOD + 'Module successfully installed and launched');
          Common.printOut(cst.PREFIX_MSG_MOD + 'Checkout module options: `$ pm2 conf`');
          return cb(null, dt);
        });
      });
    });
  }
}

function start(PM2, modules, module_name, cb) {
  Common.printOut(cst.PREFIX_MSG_MOD + 'Starting NPM module ' + module_name);

  var install_path = path.join(cst.DEFAULT_MODULE_PATH, module_name);
  var proc_path = path.join(install_path, 'node_modules', module_name);
  var package_json_path = path.join(proc_path, 'package.json');

  var opts = {};

  // Merge with embedded configuration inside module_conf (uid, gid)
  Common.extend(opts, modules[module_name]);

  // Merge meta data to start module properly
  Common.extend(opts, {
    // package.json path
    cmd : package_json_path,
    // starting mode
    development_mode : false,
    // process cwd
    proc_path : proc_path
  });

  StartModule(PM2, opts, function(err, dt) {
    if (err) console.error(err);
    return cb();
  })
}

function uninstall(CLI, module_name, cb) {
  var module_name_only = Utility.getCanonicModuleName(module_name)
  var proc_path = path.join(cst.DEFAULT_MODULE_PATH, module_name_only);
  Configuration.unsetSync(cst.MODULE_CONF_PREFIX + ':' + module_name_only);

  CLI.deleteModule(module_name_only, function(err, data) {
    console.log('Deleting', proc_path)
    if (module_name != '.' && proc_path.includes('modules') === true) {
      deleteFolderRecursive(proc_path)
    }

    if (err) {
      Common.printError(err);
      return cb(err);
    }

    return cb(null, data);
  });
}

function getModuleConf(app_name) {
  if (!app_name) throw new Error('No app_name defined');

  var module_conf = Configuration.getAllSync();

  var additional_env = {};

  if (!module_conf[app_name]) {
    additional_env = {};
    additional_env[app_name] = {};
  }
  else {
    additional_env = Common.clone(module_conf[app_name]);
    additional_env[app_name] = JSON.stringify(module_conf[app_name]);
  }
  return additional_env;
}

function StartModule(CLI, opts, cb) {
  if (!opts.cmd && !opts.package) throw new Error('module package.json not defined');
  if (!opts.development_mode) opts.development_mode = false;

  var package_json = require(opts.cmd || opts.package);

  /**
   * Script file detection
   * 1- *apps* field (default pm2 json configuration)
   * 2- *bin* field
   * 3- *main* field
   */
  if (!package_json.apps && !package_json.pm2) {
    package_json.apps = {};

    if (package_json.bin) {
      var bin = Object.keys(package_json.bin)[0];
      package_json.apps.script = package_json.bin[bin];
    }
    else if (package_json.main) {
      package_json.apps.script = package_json.main;
    }
  }

  Common.extend(opts, {
    cwd               : opts.proc_path,
    watch             : opts.development_mode,
    force_name        : package_json.name,
    started_as_module : true
  });

  // Start the module
  CLI.start(package_json, opts, function(err, data) {
    if (err) return cb(err);

    if (opts.safe) {
      Common.printOut(cst.PREFIX_MSG_MOD + 'Monitoring module behavior for potential issue (5secs...)');

      var time = typeof(opts.safe) == 'boolean' ? 3000 : parseInt(opts.safe);
      return setTimeout(function() {
        CLI.describe(package_json.name, function(err, apps) {
          if (err || apps[0].pm2_env.restart_time > 2) {
            return Rollback.revert(CLI, package_json.name, function() {
              return cb(new Error('New Module is instable, restored to previous version'));
            });
          }
          return cb(null, data);
        });
      }, time);
    }

    return cb(null, data);
  });
};



var Rollback = {
  revert : function(CLI, module_name, cb) {
    var canonic_module_name = Utility.getCanonicModuleName(module_name);
    var backup_path = path.join(require('os').tmpdir(), canonic_module_name);
    var module_path = path.join(cst.DEFAULT_MODULE_PATH, canonic_module_name);

    try {
      fs.statSync(backup_path)
    } catch(e) {
      return cb(new Error('no backup found'));
    }

    Common.printOut(cst.PREFIX_MSG_MOD + chalk.bold.red('[[[[[ Module installation failure! ]]]]]'));
    Common.printOut(cst.PREFIX_MSG_MOD + chalk.bold.red('[RESTORING TO PREVIOUS VERSION]'));

    CLI.deleteModule(canonic_module_name, function() {
      // Delete failing module

      if (module_name.includes('modules') === true)
        deleteFolderRecursive(module_path)
      // Restore working version
      copydirSync(backup_path, path.join(cst.DEFAULT_MODULE_PATH, canonic_module_name));

      var proc_path = path.join(module_path, 'node_modules', canonic_module_name);
      var package_json_path = path.join(proc_path, 'package.json');

      // Start module
      StartModule(CLI, {
        cmd : package_json_path,
        development_mode : false,
        proc_path : proc_path
      }, cb);
    });
  },
  backup : function(module_name) {
    // Backup current module
    var tmpdir = require('os').tmpdir();
    var canonic_module_name = Utility.getCanonicModuleName(module_name);
    var module_path = path.join(cst.DEFAULT_MODULE_PATH, canonic_module_name);
    copydirSync(module_path, path.join(tmpdir, canonic_module_name));
  }
}
