'use strict';

let
	_ = require('underscore'),
	$ = require('jquery'),
	ko = require('knockout'),

	AddressUtils = require('%PathToCoreWebclientModule%/js/utils/Address.js'),
	TextUtils = require('%PathToCoreWebclientModule%/js/utils/Text.js'),
	Types = require('%PathToCoreWebclientModule%/js/utils/Types.js'),

	openpgp = require('%PathToCoreWebclientModule%/js/vendors/openpgp.js'),
	Ajax = require('%PathToCoreWebclientModule%/js/Ajax.js'),
	App = require('%PathToCoreWebclientModule%/js/App.js'),
	ModulesManager = require('%PathToCoreWebclientModule%/js/ModulesManager.js'),
	Popups = require('%PathToCoreWebclientModule%/js/Popups.js'),
	Screens = require('%PathToCoreWebclientModule%/js/Screens.js'),

	ErrorsUtils = require('modules/%ModuleName%/js/utils/Errors.js'),

	COpenPgpKey = require('modules/%ModuleName%/js/COpenPgpKey.js'),
	COpenPgpResult = require('modules/%ModuleName%/js/COpenPgpResult.js'),
	Enums = require('modules/%ModuleName%/js/Enums.js'),
	PGPKeyPasswordPopup = require('modules/%ModuleName%/js/popups/PGPKeyPasswordPopup.js'),
	Settings = require('modules/%ModuleName%/js/Settings.js'),

	isTeamContactsAvailable = ModulesManager.isModuleAvailable('TeamContacts')
;

async function getKeysFromArmors (armorsData) {
	const openPgpKeys = [];
	for (let key of armorsData) {
		const nativeKey = await openpgp.key.readArmored(key.PublicPgpKey);
		if (nativeKey && !nativeKey.err && nativeKey.keys && nativeKey.keys[0]) {
			const openPgpKey = new COpenPgpKey(nativeKey.keys[0]);
			if (openPgpKey) {
				openPgpKey.isFromContacts = true;
				openPgpKeys.push(openPgpKey);
			}
		}
	}
	return openPgpKeys;
};

async function importExternalKeys (externalKeys)
{
	let addKeysPromise = new Promise((resolve, reject) => {
		const
			parameters = {
				Keys: externalKeys.map(key => ({
					Email: key.getEmail(),
					Key: key.getArmor(),
					Name: key.getUserName()
				}))
			},
			responseHandler = response => resolve(response && response.Result)
		;
		Ajax.send('%ModuleName%', 'AddPublicKeysToContacts', parameters, responseHandler);
	});
	return await addKeysPromise;
}

async function updateContactPublicKey (publicPgpKeyArmor, UUID)
{
	const updateOwnPublicKeyPromise = new Promise((resolve, reject) => {
		const
			parameters = {
				UUID,
				Key: publicPgpKeyArmor
			},
			responseHandler = response => resolve(response && response.Result)
		;
		Ajax.send('%ModuleName%', 'AddPublicKeyToContactWithUUID', parameters, responseHandler);
	});
	return await updateOwnPublicKeyPromise;
}

async function updateOwnContactPublicKey (publicPgpKeyArmor)
{
	const updateOwnPublicKeyPromise = new Promise((resolve, reject) => {
		const
			parameters = {
				PublicPgpKey: publicPgpKeyArmor
			},
			responseHandler = response => resolve(response && response.Result)
		;
		Ajax.send('%ModuleName%', 'UpdateOwnContactPublicKey', parameters, responseHandler);
	});
	return await updateOwnPublicKeyPromise;
}

async function getPublicKeysFromContacts ()
{
	return new Promise((resolve) => {
		const responseHandler = async response => {
			const armors = response && response.Result;
			if (Array.isArray(armors)) {
				const openPgpKeys = await getKeysFromArmors(armors);
				resolve(openPgpKeys);
			}
		};
		Ajax.send('%ModuleName%', 'GetPublicKeysFromContacts', {}, responseHandler);
	});
}

async function getOwnPublicKeyFromTeamContacts ()
{
	return new Promise((resolve, reject) => {
		if (isTeamContactsAvailable) {
			const responseHandler = async response => {
				const
					result = response && response.Result,
					armors = Types.isNonEmptyString(result)
						? [{
							Email: App.getUserPublicId(),
							PublicPgpKey: result
						}]
						: null
				;
				if (Array.isArray(armors)) {
					const openPgpKeys = await getKeysFromArmors(armors);
					resolve(openPgpKeys.length > 0 && openPgpKeys[0] || false);
				} else {
					resolve(false);
				}
			};
			Ajax.send('%ModuleName%', 'GetOwnContactPublicKey', {}, responseHandler);
		} else {
			resolve(null);
		}
	});
}

/**
 * @constructor
 */
function COpenPgp()
{
	this.sPrefix = 'user_' + (App.getUserId() || '0') + '_';

	this.oKeyring = new openpgp.Keyring(new openpgp.Keyring.localstore(this.sPrefix));
	this.keys = ko.observableArray([]);
	this.ownKeyFromTeamContacts = ko.observable(null);
	this.oPromiseInitialised = this.initKeys();

	App.subscribeEvent('ContactsWebclient::createContactResponse', aParams => {
		let
			responseResult = aParams[0]
		;
		if (responseResult)
		{
			this.reloadKeysFromStorage();
		}
	});

	App.subscribeEvent('ContactsWebclient::updateContactResponse', aParams => {
		let
			responseResult = aParams[0]
		;
		if (responseResult)
		{
			this.reloadKeysFromStorage();
		}
	});

	App.subscribeEvent('ContactsWebclient::deleteContactsResponse', aParams => {
		let
			responseResult = aParams[0]
		;
		if (responseResult)
		{
			this.reloadKeysFromStorage();
		}
	});
}

COpenPgp.prototype.oKeyring = null;

COpenPgp.prototype.initKeys = async function ()
{
	await this.oKeyring.load();
	await this.reloadKeysFromStorage();
};

/**
 * @return {Array}
 */
COpenPgp.prototype.getKeys = function ()
{
	return this.keys();
};

/**
 * @return {Array}
 */
COpenPgp.prototype.getPublicKeys = function ()
{
	return _.filter(this.keys(), oKey => {
		return oKey && oKey.isPublic() === true;
	});
};

/**
 * @return {Array}
 */
COpenPgp.prototype.getPrivateKeys = function ()
{
	return _.filter(this.keys(), oKey => {
		return oKey && oKey.isPublic() !== true;
	});
};

/**
 * @return {mixed}
 */
COpenPgp.prototype.getKeysObservable = function ()
{
	return this.keys;
};

/**
 * @private
 */
COpenPgp.prototype.reloadKeysFromStorage = async function ()
{
	if (App.isUserNormalOrTenant()) {
		const
			keysFromLocalstorage = this.oKeyring.getAllKeys()
					.filter(key => key && key.primaryKey)
					.map(key => new COpenPgpKey(key)),
			keysFromContacts = await getPublicKeysFromContacts()
		;

		this.keys([...keysFromLocalstorage, ...keysFromContacts]);
		this.ownKeyFromTeamContacts(await getOwnPublicKeyFromTeamContacts());
	} else {
		this.keys([]);
	}
};

COpenPgp.prototype.getKeysFromArmors = getKeysFromArmors;

/**
 * @private
 * @param {Array} aKeys
 * @return {Array}
 */
COpenPgp.prototype.convertToNativeKeys = function (aKeys)
{
	return _.map(aKeys, oItem => {
		return (oItem && oItem.pgpKey) ? oItem.pgpKey : oItem;
	});
};

/**
 * @private
 * @param {Object} oKey
 */
COpenPgp.prototype.cloneKey = async function (oKey)
{
	let oPrivateKey = null;
	if (oKey)
	{
		oPrivateKey = await openpgp.key.readArmored(oKey.armor());
		if (oPrivateKey && !oPrivateKey.err && oPrivateKey.keys && oPrivateKey.keys[0])
		{
			oPrivateKey = oPrivateKey.keys[0];
			if (!oPrivateKey || !oPrivateKey.primaryKey)
			{
				oPrivateKey = null;
			}
		}
		else
		{
			oPrivateKey = null;
		}
	}

	return oPrivateKey;
};

/**
 * @private
 * @param {Object} oResult
 * @param {Object} oKey
 * @param {string} sPassword
 * @param {string} sKeyEmail
 */
COpenPgp.prototype.decryptKeyHelper = async function (oResult, oKey, sPassword, sKeyEmail)
{
	if (oKey && oKey.primaryKey && oKey.primaryKey.isDecrypted() && sPassword === '')
	{
		//key is encoded with an empty password
	}
	else if(oKey)
	{
		try
		{
			await oKey.decrypt(Types.pString(sPassword));
			if (!oKey || !oKey.primaryKey || !oKey.primaryKey.isDecrypted())
			{
				oResult.addError(Enums.OpenPgpErrors.KeyIsNotDecodedError, sKeyEmail || '');
			}
		}
		catch (e)
		{
			oResult.addExceptionMessage(e, Enums.OpenPgpErrors.KeyIsNotDecodedError, sKeyEmail || '');
		}
	}
	else
	{
		oResult.addError(Enums.OpenPgpErrors.KeyIsNotDecodedError, sKeyEmail || '');
	}
};

/**
 * @private
 * @param {Object} oResult
 * @param {string} sFromEmail
 * @param {Object} oDecryptedMessage
 */
COpenPgp.prototype.verifyMessageHelper = async function (oResult, sFromEmail, oDecryptedMessage)
{
	let
		bResult = false,
		oValidKey = null,
		aVerifyResult = [],
		aVerifyKeysId = [],
		aPublicKeys = []
	;

	if (oDecryptedMessage && oDecryptedMessage.getSigningKeyIds)
	{
		aVerifyKeysId = oDecryptedMessage.getSigningKeyIds();
		if (aVerifyKeysId && 0 < aVerifyKeysId.length)
		{
			aPublicKeys = this.findKeysByEmails([sFromEmail], true);
			if (!aPublicKeys || 0 === aPublicKeys.length)
			{
				oResult.addNotice(Enums.OpenPgpErrors.PublicKeyNotFoundNotice, sFromEmail);
			}
			else
			{
				aVerifyResult = [];
				try
				{
					aVerifyResult = await oDecryptedMessage.verify(this.convertToNativeKeys(aPublicKeys));
				}
				catch (e)
				{
					oResult.addNotice(Enums.OpenPgpErrors.VerifyErrorNotice, sFromEmail);
				}

				if (aVerifyResult && 0 < aVerifyResult.length)
				{
					let aValidityPromises = [];
					for (let oKey of aVerifyResult)
					{
						aValidityPromises.push(
							oKey.verified
							.then(validity => {
								return oKey && oKey.keyid && validity ? oKey : null
							})
						);
					}
					await Promise.all(aValidityPromises)
					.then(aKeys => {
						oValidKey = _.find(aKeys, oKey => {
							return oKey !== null;
						});
						if (oValidKey && oValidKey.keyid &&
							aPublicKeys && aPublicKeys[0] &&
							aPublicKeys[0].hasId(oValidKey.keyid.toHex()))
						{
							bResult = true;
						}
						else
						{
							oResult.addNotice(Enums.OpenPgpErrors.VerifyErrorNotice, sFromEmail);
						}
					});
				}
			}
		}
		else
		{
			oResult.addNotice(Enums.OpenPgpErrors.NoSignDataNotice);
		}
	}
	else
	{
		oResult.addError(Enums.OpenPgpErrors.UnknownError);
	}

	if (!bResult && !oResult.hasNotices())
	{
		oResult.addNotice(Enums.OpenPgpErrors.VerifyErrorNotice);
	}

	return bResult;
};

/**
 * @param {string} sUserID
 * @param {string} sPassword
 * @param {number} nKeyLength
 * @param {Function} fOkHandler
 * @param {Function} fErrorHandler
 *
 * @return {COpenPgpResult}
 */
COpenPgp.prototype.generateKey = function (sUserID, sPassword, nKeyLength, fOkHandler, fErrorHandler)
{
	let
		oEmailParts = AddressUtils.getEmailParts(sUserID),
		oOptions = {
			userIds: [{ name: oEmailParts.name, email: oEmailParts.email }],
			numBits: nKeyLength,
			passphrase: sPassword
		}
	;

	openpgp.generateKey(oOptions).then(async oKeyPair => {
			await this.oKeyring.privateKeys.importKey(oKeyPair.privateKeyArmored);
			await this.oKeyring.publicKeys.importKey(oKeyPair.publicKeyArmored);
			await this.oKeyring.store();
			if (_.isFunction(fOkHandler))
			{
				fOkHandler();
			}
			this.reloadKeysFromStorage();
		},
		err => {
			if (_.isFunction(fErrorHandler))
			{
				fErrorHandler();
			}
		}
	);
};

/**
 * @private
 * @param {string} sArmor
 * @return {Array}
 */
COpenPgp.prototype.splitKeys = function (sArmor)
{
	let
		aResult = [],
		iCount = 0,
		iLimit = 30,
		aMatch = null,
		sKey = $.trim(sArmor),
		oReg = /[\-]{3,6}BEGIN[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[\-]{3,6}[\s\S]+?[\-]{3,6}END[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[\-]{3,6}/gi
	;

//	If the key doesn't have any additional fields (for example "Version: 1.1"), this transformation corrupts the key.
//	Seems like it is unnecessary transformation. Everything works fine without it.
//	sKey = sKey.replace(/[\r\n]([a-zA-Z0-9]{2,}:[^\r\n]+)[\r\n]+([a-zA-Z0-9\/\\+=]{10,})/g, '\n$1---xyx---$2')
//		.replace(/[\n\r]+/g, '\n').replace(/---xyx---/g, '\n\n');

	do
	{
		aMatch = oReg.exec(sKey);
		if (!aMatch || 0 > iLimit)
		{
			break;
		}

		if (aMatch[0] && aMatch[1] && aMatch[2] && aMatch[1] === aMatch[2])
		{
			if ('PRIVATE' === aMatch[1] || 'PUBLIC' === aMatch[1])
			{
				aResult.push([aMatch[1], aMatch[0]]);
				iCount++;
			}
		}

		iLimit--;
	}
	while (true);

	return aResult;
};

COpenPgp.prototype.isOwnEmail = function (sEmail)
{
	if (sEmail === App.getUserPublicId())
	{
		return true;
	}
	let
		ModulesManager = require('%PathToCoreWebclientModule%/js/ModulesManager.js'),
		aOwnEmails = ModulesManager.run('MailWebclient', 'getAllAccountsFullEmails') || []
	;
	return (_.find(aOwnEmails, sOwnEmail => {
		let oEmailParts = AddressUtils.getEmailParts(sOwnEmail);
		return sEmail === oEmailParts.email;
	}) !== undefined) ? true : false;
};

/**
 * Imports keys only to personal contatcs.
 * @param {string} armor
 * @param {string} contactUUID
 * @param {boolean} isOwnContact
 * @return {COpenPgpResult}
 */
COpenPgp.prototype.addKeyToContact = async function (armor, contactUUID = '', isOwnContact = false)
{
	armor = $.trim(armor);
	const importResult = new COpenPgpResult();
	if (!armor) {
		return importResult.addError(Enums.OpenPgpErrors.InvalidArgumentErrors);
	}

	let
		armorsData = this.splitKeys(armor),
		armorData = armorsData.length === 1 ? armorsData[0] : null
	;

	if (Array.isArray(armorData) && armorData.length === 2 && 'PUBLIC' === armorData[0]) {
		const armorData = armorsData[0];
		const publicKey = await openpgp.key.readArmored(armorData[1]);
		if (publicKey && !publicKey.err && publicKey.keys && publicKey.keys[0]) {
			if (contactUUID) {
				if (!(await updateContactPublicKey(armorData[1], contactUUID))) {
					importResult.addError(Enums.OpenPgpErrors.ImportKeyError);
				}
				this.reloadKeysFromStorage();
			} else if (isOwnContact) {
				if (!(await updateOwnContactPublicKey(armorData[1]))) {
					importResult.addError(Enums.OpenPgpErrors.ImportKeyError);
				}
				this.reloadKeysFromStorage();
			}
		}
	}

	return importResult;
};

/**
 * @param {string} armorsText
 * @return {COpenPgpResult}
 */
COpenPgp.prototype.importKeys = async function (armorsText)
{
	armorsText = $.trim(armorsText);
	const importResult = new COpenPgpResult();
	if (!armorsText) {
		return importResult.addError(Enums.OpenPgpErrors.InvalidArgumentErrors);
	}

	let
		importedToLocalstorageCount = 0,
		importedToContactsCount = 0,
		armorsData = this.splitKeys(armorsText),
		externalKeys = []
	;

	for (let index = 0; index < armorsData.length; index++) {
		const armorData = armorsData[index];
		if ('PRIVATE' === armorData[0]) {
			try {
				await this.oKeyring.privateKeys.importKey(armorData[1]);
				importedToLocalstorageCount++;
			} catch (error) {
				importResult.addExceptionMessage(error, Enums.OpenPgpErrors.ImportKeyError, 'private');
			}
		} else if ('PUBLIC' === armorData[0]) {
			const publicKey = await openpgp.key.readArmored(armorData[1]);
			if (publicKey && !publicKey.err && publicKey.keys && publicKey.keys[0]) {
				const
					openPgpKey = new COpenPgpKey(publicKey.keys[0]),
					keyEmail = openPgpKey.getEmail()
				;
				if (this.isOwnEmail(keyEmail)) {
					try {
						await this.oKeyring.publicKeys.importKey(armorData[1]);
						importedToLocalstorageCount++;
					} catch (error) {
						importResult.addExceptionMessage(error, Enums.OpenPgpErrors.ImportKeyError, 'public');
					}
				} else {
					externalKeys.push(openPgpKey);
					importedToContactsCount++;
				}
			}
		}
	}

	if ((importedToLocalstorageCount + importedToContactsCount) === 0) {
		importResult.addError(Enums.OpenPgpErrors.ImportNoKeysFoundError);
	}

	if (importedToLocalstorageCount > 0) {
		await this.oKeyring.store();
	}

	if (externalKeys.length > 0) {
		if (await importExternalKeys(externalKeys)) {
			this.reloadKeysFromStorage();
		}
	} else {
		this.reloadKeysFromStorage();
	}

	return importResult;
};

/**
 * @param {string} sArmor
 * @return {Array|boolean}
 */
COpenPgp.prototype.getArmorInfo = async function (sArmor)
{
	sArmor = $.trim(sArmor);

	let
		iIndex = 0,
		iCount = 0,
		oKey = null,
		aResult = [],
		aData = null,
		aKeys = []
	;

	if (!sArmor)
	{
		return false;
	}

	aKeys = this.splitKeys(sArmor);

	for (iIndex = 0; iIndex < aKeys.length; iIndex++)
	{
		aData = aKeys[iIndex];
		if ('PRIVATE' === aData[0])
		{
			try
			{
				oKey = await openpgp.key.readArmored(aData[1]);
				if (oKey && !oKey.err && oKey.keys && oKey.keys[0])
				{
					aResult.push(new COpenPgpKey(oKey.keys[0]));
				}

				iCount++;
			}
			catch (e) {}
		}
		else if ('PUBLIC' === aData[0])
		{
			try
			{
				oKey = await openpgp.key.readArmored(aData[1]);
				if (oKey && !oKey.err && oKey.keys && oKey.keys[0])
				{
					aResult.push(new COpenPgpKey(oKey.keys[0]));
				}

				iCount++;
			}
			catch (e) {}
		}
	}

	return aResult;
};

/**
 * @param {string} sID
 * @param {boolean} bPublic
 * @return {COpenPgpKey|null}
 */
COpenPgp.prototype.findKeyByID = function (sID, bPublic)
{
	bPublic = !!bPublic;
	sID = sID.toLowerCase();

	let oKey = _.find(this.keys(), oKey => {
		return bPublic === oKey.isPublic() && oKey.hasId(sID);
	});
	
	return oKey ? oKey : null;
};

/**
 * @param {array} emails
 * @param {boolean} isPublicKey
 * @param {COpenPgpResult=} findKeysResult
 * @return {array}
 */
COpenPgp.prototype.findKeysByEmails = function (emails, isPublicKey = true, findKeysResult = null)
{
	const openPgpKeys = this.keys().filter(key => {
		return key && isPublicKey === key.isPublic() && emails.includes(key.getEmail());
	});
	if (findKeysResult) {
		const
			emailsFromKeys = openPgpKeys.map(key => key.getEmail()),
			diffEmails = emails.filter(email => !emailsFromKeys.includes(email))
		;
		diffEmails.forEach(email => {
			const errorCode = isPublicKey
					? Enums.OpenPgpErrors.PublicKeyNotFoundError
					: Enums.OpenPgpErrors.PrivateKeyNotFoundError;
			findKeysResult.addError(errorCode, email);
		});
	}
	return openPgpKeys;
};

/**
 * @param {string} email
 * @returns {array}
 */
COpenPgp.prototype.getPublicKeysIfExistsByEmail = function (email)
{
	const publicKeys = this.findKeysByEmails([email], true);
	return publicKeys.length > 1 ? [publicKeys[0]] : publicKeys;
};

/**
 * @param {object} oKey
 * @param {string} sPrivateKeyPassword
 * @returns {object}
 */
COpenPgp.prototype.verifyKeyPassword = async function (oKey, sPrivateKeyPassword)
{
	let
		oResult = new COpenPgpResult(),
		oPrivateKey = this.convertToNativeKeys([oKey])[0],
		oPrivateKeyClone = await this.cloneKey(oPrivateKey)
	;

	await this.decryptKeyHelper(oResult, oPrivateKeyClone, sPrivateKeyPassword, '');
	if (
		!oResult.hasErrors()
		&& !oKey.getPassphrase()
		&& Settings.rememberPassphrase()
	)
	{
		oKey.setPassphrase(sPrivateKeyPassword);
	}

	return oResult;
};

/**
 * @param {string} sData
 * @param {object} oEncryptionKey
 * @param {string} sFromEmail
 * @param {string} sPrivateKeyPassword = ''
 * @param {Function} fOkHandler
 * @param {Function} fErrorHandler
 * @return {string}
 */
COpenPgp.prototype.decryptAndVerify = async function (sData, oEncryptionKey, sFromEmail, sPrivateKeyPassword, fOkHandler, fErrorHandler)
{
	let
		oResult = new COpenPgpResult(),
		aPublicKeys = this.getPublicKeysIfExistsByEmail(sFromEmail)
	;

	try
	{
		const oDecryptionResult = await this.decryptData(
			sData,
			sPrivateKeyPassword,
			false, //bPasswordBasedEncryption
			[oEncryptionKey],
			aPublicKeys
		);

		if (oDecryptionResult.result && _.isFunction(fOkHandler))
		{
			fOkHandler(oDecryptionResult);
		}
		else if (_.isFunction(fErrorHandler))
		{
			fErrorHandler(oDecryptionResult);
		}
	}
	catch (e)
	{
		oResult.addExceptionMessage(e, Enums.OpenPgpErrors.VerifyAndDecryptError);
		if (_.isFunction(fErrorHandler))
		{
			fErrorHandler(oResult);
		}
	}
};

/**
 * @param {string} sData
 * @param {string} sFromEmail
 * @param {Function} fOkHandler
 * @param {Function} fErrorHandler
 * @return {string}
 */
COpenPgp.prototype.verify = async function (sData, sFromEmail, fOkHandler, fErrorHandler)
{
	let
		oMessage = await openpgp.cleartext.readArmored(sData),
		oResult = new COpenPgpResult(),
		aPublicKeys = this.findKeysByEmails([sFromEmail], true, oResult),
		oOptions = {
			message: oMessage,
			publicKeys: this.convertToNativeKeys(aPublicKeys) // for verification
		}
	;

	openpgp.verify(oOptions).then(_.bind(async function(oPgpResult) {
		let aValidityPromises = [];
		let aValidSignatures = [];
		for (let oSignature of oPgpResult.signatures)
		{
			aValidityPromises.push(
				oSignature.verified
				.then(validity => {
					return oSignature && validity === true ? oSignature : null
				})
			);
		}
		await Promise.all(aValidityPromises)
		.then(aSignatures => {
			aValidSignatures = _.filter(aSignatures, function (oSignature) {
				return oSignature !== null;
			});
		});
		if (aValidSignatures.length)
		{
			await this.verifyMessageHelper(oResult, sFromEmail, oMessage);
			oResult.result = oMessage.getText();
			if (oResult.notices && _.isFunction(fErrorHandler))
			{
				fErrorHandler(oResult);
			}
			else if (_.isFunction(fOkHandler))
			{
				fOkHandler(oResult);
			}
		}
		else
		{
			oResult.addError(Enums.OpenPgpErrors.CanNotReadMessage);
			if (_.isFunction(fErrorHandler))
			{
				fErrorHandler(oResult);
			}
		}
	}, this), function (e) {
		oResult.addExceptionMessage(e, Enums.OpenPgpErrors.CanNotReadMessage);
		if (_.isFunction(fErrorHandler))
		{
			fErrorHandler(oResult);
		}
	});
};

COpenPgp.prototype.getPublicKeysByContactsAndEmails = async function (contactUUIDs, emails)
{
	return new Promise((resolve, reject) => {
		const
			parameters = {
				ContactUUIDs: contactUUIDs
			},
			responseHandler = async response => {
				const
					publicKeysArmorsFromContacts = Array.isArray(response.Result) ? response.Result : [],
					publicKeysFromContacts = await getKeysFromArmors(publicKeysArmorsFromContacts),
					publicKeysFromContactsEmails = publicKeysFromContacts.map(publicKey => publicKey.emailParts.email),
					notFoundPrincipalsEmails = emails.filter(email => !publicKeysFromContactsEmails.includes(email)),
					publicKeysFromLocalStorage = this.findKeysByEmails(notFoundPrincipalsEmails),
					allPublicKeys = publicKeysFromContacts.concat(publicKeysFromLocalStorage)
				;
				resolve(allPublicKeys);
			}
		;
		Ajax.send('OpenPgpWebclient', 'GetPublicKeysByCountactUUIDs', parameters, responseHandler);
	});
};

/**
 * @param {string} dataToEncrypt
 * @param {array} principalsEmails
 * @param {function} successCallback
 * @param {function} errorCallback
 * @param {array} contactsUUIDs
 * @return {string}
 */
COpenPgp.prototype.encrypt = async function (dataToEncrypt, principalsEmails, successCallback,
		errorCallback, contactsUUIDs = [])
{
	const
		findKeysResult = new COpenPgpResult(),
		allPublicKeys = await this.getPublicKeysByContactsAndEmails(contactsUUIDs, principalsEmails)
	;

	if (findKeysResult.hasErrors()) {
		if (_.isFunction(errorCallback)) {
			errorCallback(findKeysResult);
		}
		return;
	}

	try {
		const oEncryptionResult = await this.encryptData(dataToEncrypt, allPublicKeys);
		if (oEncryptionResult.result) {
			const { data, password } = oEncryptionResult.result;
			oEncryptionResult.result = data;
			if (_.isFunction(successCallback)) {
				successCallback(oEncryptionResult);
			}
		} else if (_.isFunction(errorCallback)) {
			errorCallback(oEncryptionResult);
		}
	} catch (e) {
		findKeysResult.addExceptionMessage(e, Enums.OpenPgpErrors.EncryptError);
		if (_.isFunction(errorCallback)) {
			errorCallback(findKeysResult);
		}
	}
};

/**
 * @param {string} dataToSign
 * @param {string} fromEmail
 * @param {function} successCallback
 * @param {function} errorCallback
 * @param {string} passphrase
 * @return {string}
 */
COpenPgp.prototype.sign = async function (dataToSign, fromEmail, successCallback, errorCallback,
		passphrase = '')
{
	const
		findKeysResult = new COpenPgpResult(),
		aPrivateKeys = this.findKeysByEmails([fromEmail], false, findKeysResult)
	;

	if (findKeysResult.hasErrors()) {
		if (_.isFunction(errorCallback)) {
			errorCallback(findKeysResult);
		}
		return;
	}

	const
		privateKey = this.convertToNativeKeys(aPrivateKeys)[0],
		privateKeyClone = await this.cloneKey(privateKey)
	;

	if (passphrase === '') {
		passphrase = await this.askForKeyPassword(aPrivateKeys[0].getUser());
		if (passphrase === false) {
			// returning userCanceled status so that error message won't be shown
			findKeysResult.userCanceled = true;
			return findKeysResult;
		} else {
			// returning passphrase so that it won't be asked again until current action popup is closed
			findKeysResult.passphrase = passphrase;
		}
	}

	await this.decryptKeyHelper(findKeysResult, privateKeyClone, passphrase, fromEmail);

	if (privateKeyClone && !findKeysResult.hasErrors()) {
		let oOptions = {
			message: openpgp.cleartext.fromText(dataToSign),
			privateKeys: privateKeyClone
		};
		openpgp.sign(oOptions).then(
			signResult => {
				findKeysResult.result = signResult.data;
				if (_.isFunction(successCallback)) {
					successCallback(findKeysResult);
				}
			},
			error => {
				findKeysResult.addExceptionMessage(error, Enums.OpenPgpErrors.SignError, fromEmail);
				if (_.isFunction(errorCallback)) {
					errorCallback(findKeysResult);
				}
			}
		);
	} else if (_.isFunction(errorCallback)) {
		errorCallback(findKeysResult);
	}
};

/**
 * @param {string} dataToEncrypt
 * @param {string} fromEmail
 * @param {Array} principalsEmails
 * @param {string} passphrase
 * @param {Function} successCallback
 * @param {Function} errorHandler
 * @param {Array} contactsUUIDs
 * @return {string}
 */
COpenPgp.prototype.signAndEncrypt = async function (dataToEncrypt, fromEmail, principalsEmails, passphrase,
		successCallback, errorHandler, contactsUUIDs = [])
{
	const
		findKeysResult = new COpenPgpResult(),
		privateKeys = this.findKeysByEmails([fromEmail], false, findKeysResult),
		allPublicKeys = await this.getPublicKeysByContactsAndEmails(contactsUUIDs, principalsEmails)
	;

	if (findKeysResult.hasErrors()) {
		if (_.isFunction(errorHandler)) {
			errorHandler(findKeysResult);
		}
		return;
	}

	try {
		const
			isPasswordBasedEncryption = false,
			needToSign = true,
			encryptionResult = await this.encryptData(dataToEncrypt, allPublicKeys, privateKeys,
				isPasswordBasedEncryption, needToSign, passphrase
			)
		;
		if (encryptionResult.result) {
			const { data, password } = encryptionResult.result;
			if (_.isFunction(successCallback)) {
				successCallback({result: data});
			}
		} else if (_.isFunction(errorHandler)) {
			errorHandler(encryptionResult);
		}
	} catch (e) {
		findKeysResult.addExceptionMessage(e, Enums.OpenPgpErrors.SignAndEncryptError);
		if (_.isFunction(errorHandler)) {
			errorHandler(findKeysResult);
		}
	}
};

/**
 * @param {blob|string} Data
 * @param {array} aPublicKeys
 * @param {array} aPrivateKeys
 * @param {string} sPrincipalsEmail
 * @param {boolean} bPasswordBasedEncryption
 * @param {boolean} bSign
 * @param {string} sPassphrase
 * @return {COpenPgpResult}
 */
COpenPgp.prototype.encryptData = async function (Data, aPublicKeys = [], aPrivateKeys = [],
	bPasswordBasedEncryption = false, bSign = false, sPassphrase = '')
{
	let
		oResult = new COpenPgpResult(),
		sPassword = '',
		bIsBlob = Data instanceof Blob,
		buffer = null,
		oOptions = {}
	;

	oResult.result = false;
	if (bIsBlob)
	{
		buffer = await new Response(Data).arrayBuffer();
		oOptions.message = openpgp.message.fromBinary(new Uint8Array(buffer));
		oOptions.armor = false;
		Data = null;
		buffer = null;
	}
	else
	{
		oOptions.message = openpgp.message.fromText(Data);
	}

	if (bPasswordBasedEncryption)
	{
		sPassword = this.generatePassword();
		oOptions.passwords = [sPassword];
	}
	else if (Types.isNonEmptyArray(aPublicKeys))
	{
		oOptions.publicKeys = this.convertToNativeKeys(aPublicKeys);
	}

	if (bSign && aPrivateKeys && aPrivateKeys.length > 0)
	{
		let
			oPrivateKey = this.convertToNativeKeys(aPrivateKeys)[0],
			oPrivateKeyClone = await this.cloneKey(oPrivateKey),
			sStoredPassphrase = aPrivateKeys[0].getPassphrase()
		;

		if (sStoredPassphrase && !sPassphrase)
		{
			sPassphrase = sStoredPassphrase;
		}

		if (!sPassphrase)
		{
			sPassphrase = await this.askForKeyPassword(aPrivateKeys[0].getUser());
			if (sPassphrase === false)
			{
				// returning userCanceled status so that error message won't be shown
				oResult.userCanceled = true;
				return oResult;
			}
			else
			{
				// returning passphrase so that it won't be asked again until current action popup is closed
				oResult.passphrase = sPassphrase;
			}
		}
		await this.decryptKeyHelper(oResult, oPrivateKeyClone, sPassphrase, aPrivateKeys[0].getEmail());
		if (
			!oResult.hasErrors()
			&& !sStoredPassphrase
			&& Settings.rememberPassphrase()
		)
		{
			aPrivateKeys[0].setPassphrase(sPassphrase);
		}
		oOptions.privateKeys = [oPrivateKeyClone];
	}
	if (!oResult.hasErrors())
	{
		try
		{
			let oPgpResult = await openpgp.encrypt(oOptions);

			oResult.result = {
				data:		bIsBlob ? oPgpResult.message.packets.write() : oPgpResult.data,
				passphrase: sPassphrase,
				password:	sPassword
			};
		}
		catch (e)
		{
			oResult.addExceptionMessage(e, Enums.OpenPgpErrors.EncryptError);
		}
	}

	return oResult;
};

/**
 * @param {blob|string} Data
 * @param {string} sPassword
 * @param {boolean} bPasswordBasedEncryption
 * @param {array} aPublicKeys
 * @return {string}
 */
COpenPgp.prototype.decryptData = async function (Data, sPassword = '', bPasswordBasedEncryption = false, aPrivateKeys = [], aPublicKeys = [])
{
	let
		oResult = new COpenPgpResult(),
		bIsBlob = Data instanceof Blob,
		buffer = null,
		sEmail = ''
	;

	//if public keys are not defined - use all public keys for verification
	aPublicKeys = Types.isNonEmptyArray(aPublicKeys) ? aPublicKeys : this.getPublicKeys();
	let oOptions = {
		publicKeys: this.convertToNativeKeys(aPublicKeys) // for verification
	};

	if (bIsBlob)
	{
		buffer = await new Response(Data).arrayBuffer();
		oOptions.message = await openpgp.message.read(new Uint8Array(buffer));
		oOptions.format = 'binary';
	}
	else
	{
		oOptions.message = await openpgp.message.readArmored(Data);
	}

	if (!Types.isNonEmptyArray(aPrivateKeys))
	{
		let aKeyIds = oOptions.message.getEncryptionKeyIds().map(oKeyId => oKeyId.toHex());
		aPrivateKeys = aKeyIds
			.map(sKeyId => this.findKeyByID(sKeyId, /*bPublic*/false))
			.filter(oKey => oKey !== null);
	}
	oResult.result = false;

	if (bPasswordBasedEncryption)
	{
		oOptions.passwords = [sPassword];
	}
	else
	{
		if (aPrivateKeys && aPrivateKeys.length > 0)
		{
			let
				oPrivateKey = this.convertToNativeKeys(aPrivateKeys)[0],
				oPrivateKeyClone = await this.cloneKey(oPrivateKey),
				sStoredPassphrase = aPrivateKeys[0].getPassphrase(),
				sPassphrase = sPassword
			;

			if (sStoredPassphrase && !sPassphrase)
			{
				sPassphrase = sStoredPassphrase;
			}

			if (!sPassphrase)
			{
				sPassphrase = await this.askForKeyPassword(aPrivateKeys[0].getUser());
				if (sPassphrase === false)
				{
					// returning userCanceled status so that error message won't be shown
					oResult.userCanceled = true;
					return oResult;
				}
				else
				{
					// returning passphrase so that it won't be asked again until current action popup is closed
					oResult.passphrase = sPassphrase;
				}
			}
			sEmail = aPrivateKeys[0].getEmail();
			await this.decryptKeyHelper(oResult, oPrivateKeyClone, sPassphrase, sEmail);
			if (
				!oResult.hasErrors()
				&& !sStoredPassphrase
				&& Settings.rememberPassphrase()
			)
			{
				aPrivateKeys[0].setPassphrase(sPassphrase);
			}
			oOptions.privateKeys = oPrivateKeyClone;
		}
		else
		{
			oResult.addError(Enums.OpenPgpErrors.PrivateKeyNotFoundError);
			return oResult;
		}
	}

	if (!oResult.hasErrors())
	{
		try
		{
			let oPgpResult = await openpgp.decrypt(oOptions);
			oResult.result = await openpgp.stream.readToEnd(oPgpResult.data);
			//if result contains invalid signatures
			let aValidityPromises = [];
			for (let oSignature of oPgpResult.signatures)
			{
				aValidityPromises.push(
					oSignature.verified
					.then(validity => {
						oSignature.is_valid = validity;
						return oSignature;
					})
				);
			}
			await Promise.all(aValidityPromises)
			.then(aSignatures => {
				const aInvalidSignatures = _.filter(aSignatures, oSignature => {
					return oSignature !== null && oSignature.is_valid !== true;
				});
				const aValidSignatures = _.filter(aSignatures, oSignature => {
					return oSignature !== null && oSignature.is_valid === true;
				});

				if (oPgpResult.signatures.length && aInvalidSignatures.length > 0)
				{
					oResult.addNotice(Enums.OpenPgpErrors.VerifyErrorNotice, sEmail);
				}
				else if (aValidSignatures.length > 0)
				{
					const aKeyNames = _.map(aValidSignatures, oSignature => {
						const sKeyID = oSignature.keyid.toHex();
						const oKey = this.findKeyByID(sKeyID, true);
						return oKey.getUser();
					});
					oResult.validKeyNames = aKeyNames;
				}
			});
		}
		catch (e)
		{
			oResult.addExceptionMessage(e, Enums.OpenPgpErrors.VerifyAndDecryptError);
		}
	}

	return oResult;
};

COpenPgp.prototype.getPrivateKeyPassword = async function (sEmail)
{
	let
		oResult = new COpenPgpResult(),
		aPrivateKeys = this.findKeysByEmails([sEmail], false, oResult)
	;

	if (Types.isNonEmptyArray(aPrivateKeys))
	{
		let
			oPrivateKey = this.convertToNativeKeys(aPrivateKeys)[0],
			oPrivateKeyClone = await this.cloneKey(oPrivateKey),
			sStoredPassphrase = aPrivateKeys[0].getPassphrase(),
			sPassphrase = null
		;

		if (sStoredPassphrase)
		{
			sPassphrase = sStoredPassphrase;
		}

		if (!sPassphrase)
		{
			sPassphrase = await this.askForKeyPassword(aPrivateKeys[0].getUser());
			if (sPassphrase === false)
			{//user cancel operation
				return null;
			}
		}

		await this.decryptKeyHelper(oResult, oPrivateKeyClone, sPassphrase, sEmail);

		if (
			!oResult.hasErrors()
			&& !sStoredPassphrase
			&& Settings.rememberPassphrase()
		)
		{
			aPrivateKeys[0].setPassphrase(sPassphrase);
		}

		if (!oResult.hasErrors())
		{
			return sPassphrase;
		}
	}

	return null;
};

COpenPgp.prototype.askForKeyPassword = async function (sKeyName)
{
	let oPromiseKeyPassword = new Promise( (resolve, reject) => {
		const fOnPasswordEnterCallback = sKeyPassword => {
			resolve(sKeyPassword);
		};
		const fOnCancellCallback = () => {
			resolve(false);
		};
		//showing popup
		Popups.showPopup(PGPKeyPasswordPopup, [
				sKeyName,
				fOnPasswordEnterCallback,
				fOnCancellCallback
		]);
	});

	let sPassword = await oPromiseKeyPassword;

	return sPassword;
};

/**
 * @param {COpenPgpKey} openPgpKey
 */
COpenPgp.prototype.removeKeyFromContacts = async function (openPgpKey)
{
	const result = new COpenPgpResult();
	if (!openPgpKey) {
		return result.addError(Enums.OpenPgpErrors.InvalidArgumentError);
	}

	if (isTeamContactsAvailable && openPgpKey.emailParts.email === App.getUserPublicId() && !openPgpKey.isPrivate()) {
		if (!(await updateOwnContactPublicKey(''))) {
			result.addError(Enums.OpenPgpErrors.DeleteError);
		}
		this.reloadKeysFromStorage();
	} else {
		const
			parameters = { 'Email': openPgpKey.getEmail() },
			responseHandler = response => {
				if (!response || !response.Result) {
					result.addError(Enums.OpenPgpErrors.DeleteError);
				}
				this.reloadKeysFromStorage();
			}
		;
		Ajax.send('%ModuleName%', 'RemovePublicKeyFromContact', parameters, responseHandler);
	}

	return result;
};

/**
 * @param {COpenPgpKey} openPgpKey
 */
COpenPgp.prototype.removeKeyFromThisDevice = async function (openPgpKey)
{
	const result = new COpenPgpResult();
	if (!openPgpKey) {
		return result.addError(Enums.OpenPgpErrors.InvalidArgumentError);
	}

	try {
		this.oKeyring[openPgpKey.isPrivate() ? 'privateKeys' : 'publicKeys'].removeForId(openPgpKey.getFingerprint());
		await this.oKeyring.store();
		this.reloadKeysFromStorage();
	} catch (e) {
		result.addExceptionMessage(e, Enums.OpenPgpErrors.DeleteError);
	}

	return result;
};

COpenPgp.prototype.getEncryptionKeyFromArmoredMessage = async function (sArmoredMessage)
{
	let oMessage = await openpgp.message.readArmored(sArmoredMessage);
	let aEncryptionKeys = oMessage.getEncryptionKeyIds();
	let oEncryptionKey = null;

	if (aEncryptionKeys.length > 0)
	{
		for (let key of aEncryptionKeys)
		{
			let oKey = this.findKeyByID(key.toHex(), false);
			if (oKey)
			{
				oEncryptionKey = oKey;
				break;
			}
		}
	}

	return oEncryptionKey;
};

COpenPgp.prototype.generatePassword = function ()
{
	let sPassword = "";

	if (window.crypto)
	{
		let password = window.crypto.getRandomValues(new Uint8Array(10));
		sPassword = btoa(String.fromCharCode.apply(null, password));
		sPassword = sPassword.replace(/[^A-Za-z0-9]/g, "");
	}
	else
	{
		const sSymbols = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!;%:?*()_+=";

		for (let i = 0; i < this.iPasswordLength; i++)
		{
			sPassword += sSymbols.charAt(Math.floor(Math.random() * sSymbols.length));
		}
	}

	return sPassword;
};

COpenPgp.prototype.getCurrentUserPrivateKey = async function ()
{
	let mResult = null;
	let sUserEmail = App.getUserPublicId ? App.getUserPublicId() : '';
	let aPrivateKeys = this.findKeysByEmails([sUserEmail], /*bIsPublic*/false);

	if (aPrivateKeys.length < 1)
	{
		const sError = TextUtils.i18n('%MODULENAME%/ERROR_NO_PRIVATE_KEYS_FOR_USERS_PLURAL',
			{'USERS': sUserEmail}, null, 1);
		Screens.showError(sError);
	}
	else
	{
		mResult = aPrivateKeys[0];
	}

	return mResult;
};

COpenPgp.prototype.getCurrentUserPublicKey = async function ()
{
	let mResult = null;
	let sUserEmail = App.getUserPublicId ? App.getUserPublicId() : '';
	let aPrivateKeys = this.findKeysByEmails([sUserEmail], /*bIsPublic*/false);

	if (aPrivateKeys.length > 0)
	{
		let aNativePrivateKeys = this.convertToNativeKeys(aPrivateKeys);
		mResult = aNativePrivateKeys[0].toPublic();
	}
	else
	{
		let aPublicKeys = this.findKeysByEmails([sUserEmail], /*bIsPublic*/true);
		if (aPublicKeys.length > 0)
		{
			mResult = aPublicKeys[0];
		}
	}
	if (!mResult)
	{
		const sError = TextUtils.i18n('%MODULENAME%/ERROR_NO_PUBLIC_KEYS_FOR_USERS_PLURAL',
			{'USERS': sUserEmail}, null, 1);
		Screens.showError(sError);
	}

	return mResult;
};

COpenPgp.prototype.isPrivateKeyAvailable = async function ()
{
	await this.oPromiseInitialised;
	let sUserEmail = App.getUserPublicId ? App.getUserPublicId() : '';
	let aPrivateKeys = this.findKeysByEmails([sUserEmail], /*bIsPublic*/false);

	return !!aPrivateKeys.length;
};

COpenPgp.prototype.showPgpErrorByCode = function (oOpenPgpResult, sPgpAction, sDefaultError)
{
	ErrorsUtils.showPgpErrorByCode(oOpenPgpResult, sPgpAction, sDefaultError);
};

/**
 * @param {string} messageToEncrypt
 * @param {string} aPrincipalsEmail
 * @param {boolean} needToSign
 * @param {string} passphrase
 * @param {string} fromEmail
 * @param {string} contactUUID
 * @return {COpenPgpResult}
 */
COpenPgp.prototype.encryptMessage = async function (messageToEncrypt, principalEmail, needToSign,
		passphrase, fromEmail, contactUUID = '')
{
	const
		publicKeys = await this.getPublicKeysByContactsAndEmails([contactUUID], [principalEmail]),
		privateKeys = this.findKeysByEmails([fromEmail], false),
		isPasswordBasedEncryption = false,
		encryptionResult = await this.encryptData(messageToEncrypt, publicKeys, privateKeys,
			isPasswordBasedEncryption, needToSign, passphrase)
	;

	if (encryptionResult.result) {
		let {data, password} = encryptionResult.result;
		encryptionResult.result = data;
	}

	return encryptionResult;
};

module.exports = new COpenPgp();
