#!/usr/bin/env node

import * as fs from 'fs-extra';
import * as path from 'path';
import { exec } from 'child_process';
import StdOutUtil from '../utils/StdOutUtil';
const ProgressBar = require('progress');
const commandExistsSync = require('command-exists').sync;
import { IMachine, IDeployParams } from '../models/storage/StoredObjects';
import CliApiManager from '../api/CliApiManager';
import SpinnerHelper from '../utils/SpinnerHelper';
import IBuildLogs from '../models/IBuildLogs';
import StorageHelper from './StorageHelper';

export default class DeployHelper {
	private lastLineNumberPrinted = -10000; // we want to show all lines to begin with!

	constructor(private deployParams: IDeployParams) {
		//
	}

	private gitArchiveFile(zipFileFullPath: string, branchToPush: string) {
		const self = this;
		return new Promise<string>(function(resolve, reject) {
			// Removes the temporary file created
			if (fs.pathExistsSync(zipFileFullPath)) fs.removeSync(zipFileFullPath);

			if (!commandExistsSync('git')) {
				StdOutUtil.printError(
					"'git' command not found...\nCaptain needs 'git' to create tar file of your source files...",
					true
				);
				reject("Captain needs 'git' to create tar file of your source files...");
				return;
			}

			exec(`git archive --format tar --output "${zipFileFullPath}" ${branchToPush}`, (err, stdout, stderr) => {
				if (err) {
					StdOutUtil.printError(`TAR file failed\n${err}\n`);

					fs.removeSync(zipFileFullPath);

					reject(new Error('TAR file failed'));
					return;
				}

				exec(`git rev-parse ${branchToPush}`, (err, stdout, stderr) => {
					const gitHash = (stdout || '').trim();

					if (err || !/^[a-f0-9]{40}$/.test(gitHash)) {
						StdOutUtil.printError(
							`Cannot find hash of last commit on this branch: ${branchToPush}\n${gitHash}\n${err}\n`
						);
						reject(new Error('rev-parse failed'));

						return;
					}

					StdOutUtil.printMessage(`Pushing last commit on ${branchToPush}: ${gitHash}`);
					resolve(gitHash);
				});
			});
		});
	}

	private getFileStream(zipFileFullPath: string) {
		const fileSize = fs.statSync(zipFileFullPath).size;
		const fileStream = fs.createReadStream(zipFileFullPath);
		const barOpts = {
			width: 20,
			total: fileSize,
			clear: false
		};
		const bar = new ProgressBar(' uploading [:bar] :percent  (ETA :etas)', barOpts);

		fileStream.on('data', (chunk) => {
			bar.tick(chunk.length);
		});

		fileStream.on('end', () => {
			StdOutUtil.printMessage('This might take several minutes. PLEASE BE PATIENT...');

			SpinnerHelper.start('Building your source code...\n');

			SpinnerHelper.setColor('yellow');
		});

		return fileStream;
	}

	async startDeploy() {
		const appName = this.deployParams.appName;
		const branchToPush = this.deployParams.deploySource.branchToPush;
		const tarFilePath = this.deployParams.deploySource.tarFilePath;
		const machineToDeploy = this.deployParams.captainMachine;
		const deploySource = this.deployParams.deploySource;

		if (!appName || (!branchToPush && !tarFilePath) || !machineToDeploy) {
			StdOutUtil.printError(
				'Default deploy failed. Missing appName or branchToPush/tarFilePath or machineToDeploy.',
				true
			);
			return;
		}

		if (branchToPush && tarFilePath) {
			StdOutUtil.printError('Default deploy failed. branchToPush/tarFilePath cannot both be present.', true);
			return;
		}

		let tarFileCreatedByCli = false;
		const tarFileNameToDeploy = tarFilePath ? tarFilePath : 'temporary-captain-to-deploy.tar';

		const tarFileFullPath = tarFileNameToDeploy.startsWith('/')
			? tarFileNameToDeploy // absolute path
			: path.join(process.cwd(), tarFileNameToDeploy); // relative path

		let gitHash = '';

		if (branchToPush) {
			tarFileCreatedByCli = true;

			StdOutUtil.printMessage(`Saving tar file to:\n${tarFileFullPath}\n`);

			gitHash = await this.gitArchiveFile(tarFileFullPath, branchToPush);
		}

		StdOutUtil.printMessage(`Deploying ${appName} to ${machineToDeploy.name}`);

		try {
			StdOutUtil.printMessage(`Uploading the file to ${machineToDeploy.baseUrl}`);

			await CliApiManager.get(machineToDeploy).uploadAppData(appName, this.getFileStream(tarFileFullPath));

			StdOutUtil.printMessage(`Upload done.`);

			StorageHelper.get().saveDeployedDirectory({
				appName: appName,
				cwd: process.cwd(),
				deploySource: deploySource,
				machineNameToDeploy: machineToDeploy.name
			});

			if (tarFileCreatedByCli && fs.pathExistsSync(tarFileFullPath)) fs.removeSync(tarFileFullPath);

			this.startFetchingBuildLogs(machineToDeploy, appName);
		} catch (e) {
			if (tarFileCreatedByCli && fs.pathExistsSync(tarFileFullPath)) fs.removeSync(tarFileFullPath);

			throw e;
		}
	}

	private async onLogRetrieved(data: IBuildLogs | undefined, machineToDeploy: IMachine, appName: string) {
		const self = this;
		if (data) {
			const lines = data.logs.lines;
			const firstLineNumberOfLogs = data.logs.firstLineNumber;
			let firstLinesToPrint = 0;

			if (firstLineNumberOfLogs > this.lastLineNumberPrinted) {
				if (firstLineNumberOfLogs < 0) {
					// This is the very first fetch, probably firstLineNumberOfLogs is around -50
					firstLinesToPrint = -firstLineNumberOfLogs;
				} else {
					StdOutUtil.printMessage('[[ TRUNCATED ]]');
				}
			} else {
				firstLinesToPrint = this.lastLineNumberPrinted - firstLineNumberOfLogs;
			}

			this.lastLineNumberPrinted = firstLineNumberOfLogs + lines.length;

			for (let i = firstLinesToPrint; i < lines.length; i++) {
				StdOutUtil.printMessage((lines[i] || '').trim());
			}
		}

		if (data && !data.isAppBuilding) {
			if (!data.isBuildFailed) {
				const appUrl = self.deployParams.captainMachine!.baseUrl
					.replace('https://', 'http://')
					.replace('//captain.', '//' + appName + '.');
				StdOutUtil.printGreenMessage(`\n\n\nDeployed successfully: ${appName}`);
				StdOutUtil.printMagentaMessage(`App is available at ${appUrl}`, true);
			} else {
				StdOutUtil.printError(`\n\nSomething bad happened. Cannot deploy "${appName}"\n`, true);
			}
		} else {
			setTimeout(() => {
				this.startFetchingBuildLogs(machineToDeploy, appName);
			}, 2000);
		}
	}

	private async startFetchingBuildLogs(machineToDeploy: IMachine, appName: string) {
		const self = this;
		try {
			const data = await CliApiManager.get(machineToDeploy).fetchBuildLogs(appName);
			this.onLogRetrieved(data, machineToDeploy, appName);
		} catch (error) {
			StdOutUtil.printError(`\nSomething while retrieving app build logs.. ${error}\n`);
			this.onLogRetrieved(undefined, machineToDeploy, appName);
		}
	}
}
