/*
 * last modified---
 * 	06-07-23 replace window.crypto.subtle with node-forge
 *
 * purpose---
 * 	class to handle sending submissions to MVOs, including encryption layer
 */

import { PublicKey, PrivateKey, utils } from 'eciesjs';
import { createHash } from 'crypto-browserify';
import { random, cipher, util } from 'node-forge';

class MVOComm {
	/* constructor
	 * @param mvoId the MVO selected to talk to (usually at random)
	 * @param mvoURL the URL:port where this MVO listens
	 * @param purpose the reason we're talking to this MVO (one of: "wallet",
	 * "receipts", "deposit", "withdraw", "spend")
	 * @param encrypt transaction messages (bool)
	 */
	constructor(mvoId, mvoURL, purpose, encrypt = false) {
		this.mvoId = mvoId;
		this.mvoURL = mvoURL;
		this.purpose = purpose;
		this.mvoReply = '';
		this.doEncrypt = encrypt;
		this.replyKey = undefined;
		this.replyKeyB64 = '';
		this.mvoPubkey = undefined;
		this.reqChain = 0;
	}

	// reset the MVOId
	setMVOId(mvoId) {
		this.mvoId = mvoId;
	}

	// reset the MVO's http access URL
	setMVOURL(url) {
		this.mvoURL = url;
	}

	// record the MVO's reply
	setMVOReply(reply) {
		this.mvoReply = reply;
	}

	// configure the ECDSA pubkey for this MVO (value is per-chain)
	setMVOPubkey(pubkey) {
		// the passed value will be hex
		this.mvoPubkey = pubkey;
	}

	// configure the chainId context for the next request to this MVO
	setReqChain(chain) {
		this.reqChain = chain;
	}

	// obtain the MVO's latest reply
	get reply() {
		return this.mvoReply;
	}

	// obtain the MVO's Id
	get mvo() {
		return this.mvoId;
	}

	// obtain the latest purpose for the connection
	get operation() {
		return this.purpose;
	}

	// obtain the key we must use to decrypt the MVO's reply, in Base64 format
	get decryptKey() {
		return this.replyKeyB64;
	}

	// determine whether communications to this MVO are meant to be encrypted
	get encrypted() {
		return this.doEncrypt;
	}

	// obtain the MVO's public ECDSA key
	get mvoKey() {
		return this.mvoPubkey;
	}

	/* Send a request to our configured MVO passing pre-generated key, and
	 * handle the response, decrypting with same.  If the connection is to be
	 * encrypted, also encrypt req to the MVO's pubkey on the current chain.
	 */
	sendToMVO(data, callback) {
		const XHR = new XMLHttpRequest();

		// define successful submit handler
		XHR.addEventListener('load', (event) => {
			// see whether we expect the reply to be encrypted
			if (!this.doEncrypt) {
				// transaction was not set for encryption
				this.setMVOReply(XHR.responseText);
				callback(XHR, this);
			}
			else {
				if (XHR.status !== 200) {
					// errors will not be encrypted
					this.setMVOReply(XHR.responseText);
					callback(XHR, this);
					return;
				}
				if (XHR.responseText === '') {
					console.error("MVO " + this.mvo + " returned empty reply");
					return;
				}

				/* The text received should be Base64-encoded and represents an
				 * encrypted reply made to this.replyKey (which was passed in
				 * the data sent to the MVO side).  The initialization vector
				 * will be found in the first 12 bytes of the ciphertext.
				 */
				const encBinary = atob(XHR.responseText);
				// convert from binary string to ArrayBuffer
				var encData = Uint8Array.from(encBinary, x => x.charCodeAt(0));
				const iv = encData.slice(0, 12);
				// NB: Java AES encryption appends the tag to the ciphertext
				const cipherText = encData.slice(12, encData.length-16);
				const tag = encData.slice(encData.length-16);
				this.decryptAes(cipherText, iv, tag).then(res => {
					const decPlain = res;
					this.setMVOReply(decPlain);
					callback(XHR, this);
				}).catch(err => console.error("error decrypting MVO "
						+ this.mvoId + " reply: " + err));
			}
		});

		// define error handler
		XHR.addEventListener('error', (event) => {
			alert('Error sending request to ' + this.mvoId);
		});

		// define timeout handler
		XHR.addEventListener('timeout', (event) => {
			alert('Timeout sending request to ' + this.mvoId);
		});

		// set up request (asynchronous)
		XHR.open('POST', this.mvoURL, true);
		XHR.setRequestHeader("Content-Type",
							 "application/x-www-form-urlencoded");
		XHR.responseType = "text";

		// see if we're using encryption
		if (this.doEncrypt
			&& this.mvoPubkey !== undefined
			&& this.reqChain > 0)
		{
			// encrypt payload to MVO's ECDSA pubkey
			const mvoKey = PublicKey.fromHex(this.mvoPubkey);
			var secret = utils.getValidSecret();
			var ephemeralKey = new PrivateKey(secret);
			const encaps = ephemeralKey.multiply(mvoKey);
			var aesMaster = Buffer.concat([ephemeralKey.publicKey.uncompressed,
											encaps]);
			// hash it
			var aesKey = createHash("sha256").update(aesMaster).digest();
			var encrypted = utils.aesEncrypt(aesKey, data);
			var encBuffer = Buffer.concat([ephemeralKey.publicKey.uncompressed,
										  encrypted]);

			// apply base64Url encoding
			var binary = '';
			var bytes = new Uint8Array(encBuffer);
			var blen = bytes.byteLength;
			for (var iii = 0; iii < blen; iii++) {
				binary += String.fromCharCode(bytes[iii]);
			}
			var encData = btoa(binary);
			encData = encData.replace(/[+]/g, "-");
			encData = encData.replace(/[/]/g, "_");
			//console.log("Encrypted and B64url data = " + encData);

			// tell the MVO that we encrypted by passing chainId as a param
			XHR.setRequestHeader("encrchain", this.reqChain);

			//console.log("MVO clear data = \"" + data + "\"");
			// send encrypted request data
			XHR.send(encData);
		}
		else {
			console.warn("Sending req unencrypted");
			// send unencrypted request data
			XHR.send(data);
		}
		return XHR;
	}

	// method to generate a fresh AES-256 key for this interaction
	async generateAesKey(len = 256) {
		const key = random.getBytesSync(len / 8);
		this.replyKey = key;
		var replyKey = btoa(key);

		// convert to base64Url encoding so we can pass key in an HTTP request
		replyKey = replyKey.replace(/[+]/g, "-");
		replyKey = replyKey.replace(/[/]/g, "_");
    	this.replyKeyB64 = replyKey;
	}

	// method to decrypt a message encrypted with AES-GCM (using Forge)
	async decryptAes(cipherText, iv, tag) {
		// get tag from first 16 bytes of cipherText
		var decipher = cipher.createDecipher('AES-GCM', this.replyKey);
		decipher.start({
			iv: iv,
			tagLength: 128,
			tag: tag,
		});
		// avoids an error "buffer.getBytes() is not a function":
		var message = new util.ByteStringBuffer(cipherText);
		decipher.update(message);
		const pass = decipher.finish();
		if (pass) {
			return decipher.output;
		}
		throw new Error("Error decrypting using AES-GCM");
	}
}

export default MVOComm;
