const { generateKeyPairSync, publicEncrypt, privateDecrypt, randomBytes, createPublicKey } = require('crypto');
const selfsigned = require('selfsigned');
const aesjs = require('aes-js');

class CryptorException extends Error {
	constructor (opt) {
		super(opt);
		this.name = 'CryptorException';
	}
}


class RSA {
	constructor (keys, passphrase = null) {
		if (typeof keys !== "object" && keys !== null)
			throw new TypeError("keys must be object: { public_key: <null|undefined|CryptKey>, private_key: <null|undefined|CryptKey> } ");
		if (![undefined, null].includes(keys.public_key) && keys?.public_key?.__proto__ !== CryptKey.prototype) {
			throw new TypeError("public_key must be a CryptKey object");
		}
		if (![undefined, null].includes(keys.private_key) && keys?.private_key?.__proto__ !== CryptKey.prototype) {
			throw new TypeError("private_key must be a CryptKey object");
		}
		
		this.private_key = keys.private_key;
		this.public_key = keys.public_key;
		this.passphrase = passphrase;
	}
	
	enterPassphrase (passphrase) {
		this.passphrase = passphrase;
	}
	
	encrypt (data) {
		if (!this.public_key) throw new CryptorException('Can not find public key from encryption');
		const toEncrypt = data;
		const encryptBuffer = Buffer.from(toEncrypt);
		
		if (encryptBuffer.length > 117)
			throw new CryptorException("RSA can encrypt maximum 117 byte");
		
		let encrypted;
		if (!this.passphrase) encrypted = publicEncrypt(this.public_key.bytes.toString('utf8'), encryptBuffer);
		else encrypted = publicEncrypt({key: this.public_key.bytes.toString('utf8'), passphrase: this.passphrase}, encryptBuffer);
		return encrypted;
	}
	
	decrypt (data, toStringMethod = false) {
		if (!this.private_key) throw new CryptorException('Can not find private key from encryption');
		let decryptBuffer = Buffer.from(data.toString("base64"), "base64");
		
		let decrypted;
		if (!this.passphrase) decrypted = privateDecrypt(this.private_key.bytes.toString('utf8'), decryptBuffer);
		else decrypted = privateDecrypt({key: this.private_key.bytes.toString('utf8'), passphrase: this.passphrase}, decryptBuffer);
		if (toStringMethod) return decrypted.toString('utf8');
		else return decrypted;
	}
}


class AES {
	constructor (key, counter=5) {
		if (key.__proto__ !== CryptKey.prototype) {
			throw new TypeError("key must be a CryptKey object");
		}
		if (!Number.isInteger(counter)) {
			throw new TypeError("counter must be integer");
		}
		this.key = key;
		this.counter = counter;
	}
	
	encrypt (data) {
		data = Buffer.from(data);
		const aesCtr = new aesjs.ModeOfOperation.ctr(this.key.bytes, new aesjs.Counter(this.counter));
		return Buffer.from(aesCtr.encrypt(data));
	}
	
	decrypt (data, toStringMethod = true) {
		const aesCtr = new aesjs.ModeOfOperation.ctr(this.key.bytes, new aesjs.Counter(this.counter));
		if (toStringMethod) return Buffer.from(aesCtr.decrypt(data)).toString('utf-8');
		else return Buffer.from(aesCtr.decrypt(data));
	}
}


class AESLegacy {
	constructor (key, iv) {
		if (key.__proto__ !== CryptKey.prototype) {
			throw new TypeError("key must be a CryptKey object");
		}
		if (!Array.isArray(iv)) {
			throw new TypeError("iv (initialization vector) must be Array!");
		}
		else if (iv.length !== 16 || iv.map(i => Number.isInteger(i) && (i >= 0 && i < 256)).includes(false)) {
			// console.log({ iv });
			throw new TypeError("iv (initialization vector) must be have 16 int(0-256) elements!");
		}
		this.key = key;
		this.iv = iv;
		
		// Init test
		// const testStr = `{"response":"ack"}`;
		// const encryptedStr = this.encrypt(testStr);
		// if (this.decrypt(encryptedStr, true) !== testStr)
			// throw new Error("Failed to check unit test");
	}
	encrypt (data) {
		// let formatedKey = ...this.key;
		const dataArray = [...Buffer.from(data)];
		// let aesCtr = new aesjs.ModeOfOperation.ctr(this.key.bytes, new aesjs.Counter(5));
		const aesCbc = new aesjs.ModeOfOperation.cbc(this.key.bytes, this.iv);
		const result = new Array();
		let offset = 0;
		let addedEndless = false;
		let lastByteFirst = 0;
		let fromMain = false;
		do {
			let slicedData = dataArray.slice(offset, offset + 16);
			if (slicedData.length === 0) break;
			if (slicedData.length !== 16) {
				const difference = 16 - slicedData.length;
				slicedData = [...slicedData];
				slicedData.push(...[...new Array(difference)].map((_, i) => i !== (difference - 1) ? 0 : difference));
				lastByteFirst = slicedData.at(-1);
				slicedData = Buffer.from(slicedData);
				addedEndless = true;
				fromMain = true;
			}
			const pushData = Buffer.from(aesCbc.encrypt(Buffer.from(slicedData)));
			result.push(...pushData);
			if (addedEndless) break;
			offset += 16;
		} while (true);
		if (!addedEndless) {
			fromMain = false;
			const unencryptedChunks = [...new Array(16)].map((_, i) => i !== 15 ? 0 : 16);
			const pushData = Buffer.from(aesCbc.encrypt(Buffer.from(unencryptedChunks)));
			lastByteFirst = pushData.at(-1);
			result.push(...pushData);
		}
		// return Buffer.from(aesCtr.encrypt(data));
		const lastDecryptedByte = [...this.decrypt(Buffer.from(result), false, false)].at(-1);
		if (lastDecryptedByte > 16)
		throw new Error(`Last byte error (${lastDecryptedByte}) last writted byte: (${lastByteFirst}) last encrypted byte: (${result.at(-1)}) from main: ${fromMain ? 'true' : 'false'} allstring: ${this.decrypt(Buffer.from(result), false).toString("hex")}`);
		return Buffer.from(result);
	}
	decrypt (data, toStringMethod = true, sliceAfterEncryption = false) {
		const aesCbc = new aesjs.ModeOfOperation.cbc(this.key.bytes, this.iv);
		// const aesCtr = new aesjs.ModeOfOperation.ctr(this.key.bytes, new aesjs.Counter(5));
		// if (toStringMethod) return Buffer.from(aesCtr.decrypt(data)).toString('utf-8');
		// else return Buffer.from(aesCtr.decrypt(data));
		let offset = 0;
		let decryptedChunks = new Array();
		const dataArray = [...data];
		do {
			const slicedData = dataArray.slice(offset, offset + 16);
			if (![16, 0].includes(slicedData.length))
				throw new Error("data length must be multiple of 16");
			else if (slicedData.length === 0)
				break;
			decryptedChunks.push(...aesCbc.decrypt(slicedData));
			offset += 16;
		} while (true);
		if (sliceAfterEncryption) {
			const sliceLength = decryptedChunks.at(-1);
			decryptedChunks = decryptedChunks.slice(0, decryptedChunks.length - sliceLength);
		}
		
		// if (toStringMethod) return Buffer.from(aesCbc.decrypt(data)).toString('utf-8');
		// else return Buffer.from(aesCbc.decrypt(data));
		if (toStringMethod) return Buffer.from(decryptedChunks).toString('utf-8');
		else return Buffer.from(decryptedChunks);
	}
}


class OpenSSH extends RSA {
	constructor (keys) {
		if (keys.private_key === undefined && keys.cert?.__proto__ === CryptKey.prototype)
			keys.private_key = OpenSSH.parsePubKeyFromCrt(keys.cert);
		
		super(keys);
		
		this.private_key = keys.private_key;
		this.public_key = keys.public_key;
		this.cert = keys.cert;
		this.fingerprint = keys.fingerprint;
	}
	
	static parsePubKeyFromCrt (cert, options = {}) {
		const result = createPublicKey(cert).export({type:'pkcs1', format:'pem', ...options});
		// console.log(result);
		return new CryptKey(result);
	}
}


class CryptKey {
	constructor (key) {
		this.bytes = Buffer.from(key);
	}
	
	toString () {
		return this.bytes.toString();
	}
}

/*
Example of options
{
  location: __dirname + '/foo_rsa',
  comment: 'joe@foobar.com',
  password: '123321',
  read: true,
  format: "PEM"
}
*/
function generateOpenSSH (attrs = {}, options = { days: 365 }) {
	attrs = Object.entries(attrs).map(([name, value]) => ({ name, value }));
	const keys = selfsigned.generate(attrs, options);
	console.log(keys);
	console.log(OpenSSH.parsePubKeyFromCrt(keys.cert));
	
	return new OpenSSH({
		public_key: new CryptKey(Buffer.from(keys.public)),
		private_key: new CryptKey(Buffer.from(keys.private)),
		cert: keys.cert,
		fingerprint: keys.fingerprint,
	});
}


module.exports = {
	RSA,
	OpenSSH,
	AES,
	CryptKey,
	generateOpenSSH,
	generateRSA: (keySize = 1024, passphrase = null) => {
		let keysObj;
		if (!passphrase) keysObj = generateKeyPairSync('rsa', {
			modulusLength: keySize,
			publicKeyEncoding: {
				type: 'pkcs1',
				format: 'pem'
			},
			privateKeyEncoding: {
				type: 'pkcs1',
				format: 'pem'
			}
		});
		else keysObj = generateKeyPairSync('rsa', {
			modulusLength: keySize,
			publicKeyEncoding: {
				type: 'pkcs1',
				format: 'pem'
			},
			privateKeyEncoding: {
				type: 'pkcs1',
				format: 'pem',
				cipher: 'aes-256-cbc',
				passphrase
			}
		});
		let { privateKey, publicKey } = keysObj;
		
		return new RSA({
			private_key : new CryptKey(Buffer.from(privateKey)),
			public_key  : new CryptKey(Buffer.from(publicKey))
		});
	},
	generateAES: (length = 32) => {
		if (![24, 32, 16].includes(length)) throw new CryptorException('AES key must be 16, 24 or 32 bytes.');
		return new AES(new CryptKey(randomBytes(length)));
	}
}