import {injectable, inject} from 'inversify';
import {spawn} from 'child_process';
import {VitaDecryptUnTar} from "../interfaces/vita-decrypt-untar";
import {DecryptAndUnTarOptions, DecryptAndUnTarResult, DecryptAndUnTarStatus} from "../interfaces/vita-options-results";
import {ForceErrorImpl, CommandUtil} from "firmament-yargs";
import {VitaFileUtil} from "../interfaces/vita-file-util";
import {VitaSpawn} from "../interfaces/vita-spawn";
const fs = require('fs');
const path = require('path');
const async = require('async');
const mkdirp = require('mkdirp');
const through2Filter = require('through2-filter');
const _ = require('lodash');

@injectable()
export class VitaDecryptUnTarImpl extends ForceErrorImpl implements VitaDecryptUnTar {

  constructor(@inject('VitaSpawn') private vitaSpawn: VitaSpawn,
              @inject('CommandUtil') private commandUtil: CommandUtil,
              @inject('VitaFileUtil') private vitaFileUtil: VitaFileUtil) {
    super();
  }

  process(options: DecryptAndUnTarOptions,
          cbStatus: (err: Error, decryptAndUnTarStatus: DecryptAndUnTarStatus) => void,
          cbFinal: (err: Error, decryptAndUnTarResults: DecryptAndUnTarResult[]) => void) {
    cbFinal = this.checkCallback(cbFinal);
    cbStatus = this.checkCallback(cbStatus);
    let fnArray = [];
    options.targetFolder = options.targetFolder || null;
    options.encryptedFiles = options.encryptedFiles || [];
    options.encryptedFilePattern = options.encryptedFilePattern || /\.enc$/;
    options.foldersOfEncryptedFiles = options.foldersOfEncryptedFiles || [];

    options.encryptedFiles = this.vitaFileUtil.findFilesSync({
      pattern: options.encryptedFilePattern,
      files: options.encryptedFiles,
      folders: options.foldersOfEncryptedFiles
    });
    options.encryptedFiles.forEach(file => {
      fnArray.push(async.apply(this.spawnDecryptAndUnTarOperation.bind(this),
        file,
        options.password,
        options.targetFolder,
        (err: Error, decryptAndUnTarStatus: DecryptAndUnTarStatus) => {
          decryptAndUnTarStatus.decryptAndUnTarOptions = <DecryptAndUnTarOptions>_.omit(options, ['password', 'encryptedFilePattern']);
          cbStatus(err, decryptAndUnTarStatus);
        }
      ));
    });
    async.parallelLimit(fnArray, 3, cbFinal);
  }

  private spawnDecryptAndUnTarOperation(inFile: string,
                                        password: string,
                                        targetFolder: string,
                                        cbStatus: (err: Error, decryptAndUnTarStatus: DecryptAndUnTarStatus) => void,
                                        cbFinal: (err: Error, decryptAndUnTarResult: DecryptAndUnTarResult) => void) {
    cbStatus = this.checkCallback(cbStatus);
    cbFinal = this.checkCallback(cbFinal);
    if (!targetFolder) {
      //Put decrypted file in same folder as encrypted file
      targetFolder = path.dirname(inFile);
    }
    mkdirp(targetFolder, err => {
      if (this.commandUtil.callbackIfError(cbFinal, err)) {
        return;
      }
      let inputFileSize = fs.statSync(inFile)['size'];
      let totalBytesStreamed = 0;
      let openSslProcessExited = false;
      let tarProcessExited = false;
      let tarArgs = [
        'x',
        '--strip-components=3',
        '-C',
        targetFolder
      ];
      let tarProcess = spawn('tar', tarArgs);
      let openSslArgs =
        [
          'aes-256-cbc',
          '-d',
          '-in',
          inFile,
          '-k',
          password];
      let openSslProcess = spawn('openssl', openSslArgs);

      tarProcess.on('close', (code: number, signal: string) => {
        tarProcessExited = true;
        if (openSslProcessExited) {
          this.callbackFromDecryptAndUnTarFiles(cbFinal, targetFolder);
        }
      });

      openSslProcess.on('close', (code: number, signal: string) => {
        openSslProcessExited = true;
        if (tarProcessExited) {
          this.callbackFromDecryptAndUnTarFiles(cbFinal, targetFolder);
        }
      });

      openSslProcess.stdout.pipe(
        through2Filter((dataChunk) => {
          totalBytesStreamed += dataChunk.length;
          let taskName = `Decrypting [${path.basename(inFile)}] `;
          let current = totalBytesStreamed;
          let total = inputFileSize;
          cbStatus(null, {taskName, current, total});
          return true;
        })).pipe(tarProcess.stdin);
    });
  }

  private callbackFromDecryptAndUnTarFiles(cb: (err: Error, decryptAndUnTarResult: DecryptAndUnTarResult) => void, folder: string) {
    this.vitaFileUtil.findFiles({pattern: /\.gz$/, folders: [folder]}, (err, files) => {
      cb(err, {exportDir: folder, zipFiles: files});
    });
  }
}
