const { spawn } = require('child_process'),
      path = require('path'),
      fs = require('fs'),

      FILE_ACCESS_ERROR = 'FILE_ACCESS_ERROR',
      INTEGRITY_ERROR = 'INTEGRITY_ERROR',
      INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT_ERROR',
      SWAP_INITIALIZED = 'SWAP_INITIALIZED';

/**
 * @typedef {extractOutputData} swapInputData
 * Defines the basic properties needed for swapping the packages and launching the new app.
 *
 * @example
 * {
 *   eID: 'somePackage-xxxxxxxxxxxxx',
 *   packageName: 'somePackage',
 *   downloadURL: 'http://sample.com/foo.tar.gz',
 *   downloadChecksum: '2927b7c6ed350687446b7eba58bd74535afd411b'
 *   downloadChecksumAlgorithm: 'sha1',
 *   downloadInfo: {
 *     status: 'DOWNLOAD_COMPLETED',
 *     error: null,
 *     downloadDirectory: 'User/PathForDownloadDirectory'
 *   },
 *   extractInfo: {
 *     status: 'EXTRACTION_SUCCESSFUL',
 *     error: null,
 *     extractDirectory: 'User/PathForExtractDirectory'
 *   }
 * }
 *
 */

/**
 * @typedef {String} swapStatus
 * It holds the value of either one of
 * 'INVALID_ARGUMENT_ERROR', 'FILE_ACCESS_ERROR', 'INTEGRITY_ERROR', 'SWAP_COMPLETED'
 */

/**
 * @typedef swapInfo
 * The below object holds key properties defining the swap information
 *
 * @property {swapStatus} status
 * @property {?Error} [error]
 *
 * @example
 * {
 *   status: 'SWAP_COMPLETED',
 *   error: null
 * }
 */

/**
 * @typedef swapOutputData
 * All the properties present in the return value
 *
 * @extends swapInputData
 * @property {swapInfo} swapInfo
 *
 * @example
 * {
 *   eID: 'somePackage-xxxxxxxxxxxxx',
 *   packageName: 'somePackage',
 *   downloadURL: 'http://sample.com/foo.tar.gz',
 *   downloadChecksum: '2927b7c6ed350687446b7eba58bd74535afd411b'
 *   downloadChecksumAlgorithm: 'sha1',
 *   downloadInfo: {
 *     status: 'DOWNLOAD_COMPLETED',
 *     error: null,
 *     downloadDirectory: 'User/PathForDownloadDirectory'
 *   },
 *   extractInfo: {
 *     status: 'EXTRACTION_SUCCESSFUL',
 *     error: null,
 *     extractDirectory: 'User/PathForExtractDirectory'
 *   },
 *   swapInfo: {
 *     status: 'SWAP_COMPLETED',
 *     error: null
 *   }
 * }
 *
 */

/**
 * @callback swapResponseCallback
 * @param {?Error} error
 * @param {swapOutputData} data
 *
 */

/**
 * @method swap
 * @description This will swap the old app with the new app and then launch the new app
 * @param {swapInputData} swapInputData
 * @param {swapResponseCallback} callback
 * @throws {InvalidArgumentException}
 */
function swap(swapInputData, callback, options = {}) {
  if (!callback) {
    throw new Error('InvalidArgumentException: Hermes~swap - callback is a required parameter');
  }

  if (typeof callback !== 'function') {
    throw new Error('InvalidArgumentException: Hermes~swap - callback should be of type function');
  }

  const swapOutputData = Object.assign({}, swapInputData, {
          swapInfo: {
            status: null,
            error: null,
          },
        }),
        { relauncher } = options;

  if (!swapInputData) {
    const err = new Error('InvalidArgumentException: Hermes~swap - swapInputData is a required parameter');
    swapOutputData.swapInfo = { status: INVALID_ARGUMENT_ERROR, error: err };
    return callback(err, swapOutputData);
  }

  if (typeof swapInputData !== 'object' || Array.isArray(swapInputData)) {
    const err = new Error('InvalidArgumentException: Hermes~swap - swapInputData should be of type object');
    swapOutputData.swapInfo = { status: INVALID_ARGUMENT_ERROR, error: err };
    return callback(err, swapOutputData);
  }

  if (!swapInputData.eID) {
    const err = new Error('InvalidArgumentException: Hermes~swap - swapInputData.eID is a required field');
    swapOutputData.swapInfo = { status: INVALID_ARGUMENT_ERROR, error: err };
    return callback(err, swapOutputData);
  }

  if (typeof swapInputData.eID !== 'string') {
    const err = new Error('InvalidArgumentException: Hermes~swap - swapInputData.eID should be of type string');
    swapOutputData.swapInfo = { status: INVALID_ARGUMENT_ERROR, error: err };
    return callback(err, swapOutputData);
  }

  if (!swapInputData.packageName) {
    const err = new Error('InvalidArgumentException: Hermes~swap - swapInputData.packageName is a required field');
    swapOutputData.swapInfo = { status: INVALID_ARGUMENT_ERROR, error: err };
    return callback(err, swapOutputData);
  }

  if (typeof swapInputData.packageName !== 'string') {
    const err = new Error('InvalidArgumentException: Hermes~swap - swapInputData.packageName should be of type string');
    swapOutputData.swapInfo = { status: INVALID_ARGUMENT_ERROR, error: err };
    return callback(err, swapOutputData);
  }

  if (!swapInputData.extractInfo) {
    const err = new Error('InvalidArgumentException: Hermes~swap - swapInputData.extractInfo is a required field');
    swapOutputData.swapInfo = { status: INVALID_ARGUMENT_ERROR, error: err };
    return callback(err, swapOutputData);
  }

  if (typeof swapInputData.extractInfo !== 'object' || Array.isArray(swapInputData.extractInfo)) {
    const err = new Error('InvalidArgumentException: Hermes~swap - swapInputData.extractInfo should be of type object');
    swapOutputData.swapInfo = { status: INVALID_ARGUMENT_ERROR, error: err };
    return callback(err, swapOutputData);
  }

  if (!swapInputData.extractInfo.extractDirectory) {
    const err = new Error('InvalidArgumentException: Hermes~swap - swapInputData.extractInfo.extractDirectory is a required field');
    swapOutputData.swapInfo = { status: INVALID_ARGUMENT_ERROR, error: err };
    return callback(err, swapOutputData);
  }

  if (typeof swapInputData.extractInfo.extractDirectory !== 'string') {
    const err = new Error('InvalidArgumentException: Hermes~swap - swapInputData.extractInfo.extractDirectory should be of type string');
    swapOutputData.swapInfo = { status: INVALID_ARGUMENT_ERROR, error: err };
    return callback(err, swapOutputData);
  }

  const extractedDirectory = swapInputData.extractInfo.extractDirectory,
        newAppDirectory = path.resolve(extractedDirectory, swapInputData.eID),
        executablePath = path.resolve(newAppDirectory, swapInputData.packageName);

  let packageExecutableAvailable = false,
      childProcess = null,
      relauncherProgram = null,
      relauncherArgs = [];

  try {
    const fileStat = fs.statSync(executablePath);
    // Check whether it is executable as well, @todo
    packageExecutableAvailable = fileStat.isFile();
  }
  catch (err) {
    swapOutputData.swapInfo = { status: FILE_ACCESS_ERROR, error: err };
    return callback(err, swapOutputData);
  }

  if (!packageExecutableAvailable) {
    const err = new Error('IntegrityError: Hermes~swap downloaded package is not having the executable file to run after relaunch');
    swapOutputData.swapInfo = { status: INTEGRITY_ERROR, error: err };
    return callback(err, swapOutputData);
  }

  swapOutputData.swapInfo = { status: SWAP_INITIALIZED, error: null };
  // Calling the callback here itself since after this step the parent process will quit
  callback(null, swapOutputData);

  // Reaching here specifies, that new version is safe to be swapped
  relauncherProgram = path.resolve(__dirname, 'swap_and_relaunch.sh');
  relauncherArgs = [
    swapInputData.packageName,
    swapInputData.eID,
    extractedDirectory,
    process.pid,
    swapInputData.restart];

  const externalLauncherPresent = relauncher && (typeof relauncher === 'object' && !Array.isArray(relauncher));
  if (externalLauncherPresent) {
    relauncherProgram = relauncher.program; // should be a program path.
    if (relauncher.args && Array.isArray(relauncher.args)) {
      relauncherArgs = relauncherArgs.concat(relauncher.args); // append the args provided.
    }
  }

  // Logic added to keep running child process even if the parent process is killed
  childProcess = spawn(relauncherProgram, relauncherArgs, { detached: true, stdio: 'ignore' });

  childProcess.on('error', (err) => {
    // @todo use logger
    console.error('Hermes~swap Error in child-process', err);
  });
}

module.exports = swap;
