import {spawn,spawnSync} from 'child_process'; 
import fs from 'fs';
import https from 'https';
import jsonwebtoken from 'jsonwebtoken';
import crypto from 'crypto';

export class CommonUtils{
	constructor(){
		this.port = process.env.HANDYHOST_PORT || 8008;
		this.sslPort = process.env.HANDYHOST_SSL_PORT || 58008;
		this.redlistPortsPath = process.env.HOME+'/.HandyHost/ports.json';
	}
	escapeBashString(str){
		//escape strings for bash
		return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\!\|\#\&\~\"\'\`\ ]/g, "\\$&");
	}
	getSafePort(interimPorts){
		//get a random port outside of the range that may be used in the services
		//for config generation, mainly in dvpn
		const interim = typeof interimPorts == "undefined" ? [] : interimPorts;
		let ports = {}
		if(fs.existsSync(this.redlistPortsPath)){
			ports = JSON.parse(fs.readFileSync(this.redlistPortsPath,'utf8'));
		}
		else{
			ports = JSON.parse(fs.readFileSync('./reservedPortsDefault.json','utf8'));
		}
		return getRandomPort();
		function getRandomPort(){
			//recursively check for a free port that's not in cusrom
			const port = Math.floor((Math.random() * 19999) + 10000);
			if(typeof ports.custom[port.toString()] == "undefined" && typeof ports.default[port.toString()] == "undefined" && interim.indexOf(port) == -1){
				return port;
			}
			else{
				console.log('port was taken, try again');
				return getRandomPort();
			}
		}
	}
	getGlobalIP(){
		return new Promise((resolve,reject)=>{
			//get public IP for them at least..
			const options = {
				host: 'api.ipify.org',
				port:'443',
				path: `/`,
				method:'GET',
				rejectUnauthorized: true,
				requestCert: true,
				agent: false
			};
			let output = '';
			const request = https.request(options,response=>{
				response.on('data', (chunk) => {
					output += chunk;
				});

				//the whole response has been received, so we just print it out here
				response.on('end', () => {
					resolve({global_ip:output});
				});

				if(response.statusCode.toString() != '200'){
					//something went wrong
					console.log('error getting public ip',response.statusCode.toString());
					reject(output);
				}
			});
			request.end();
			
		})
	}
	getIPForDisplay(){
		return new Promise((resolve,reject)=>{
			let getIPCommand;
			let getIPOpts;
			let ipCommand;
			let ipOut;

			if(process.platform == 'darwin'){
			  getIPCommand = 'ipconfig';
			  getIPOpts =  ['getifaddr', 'en0'];
			}
			if(process.platform == 'linux'){
			  //hostname -I [0]
			  getIPCommand = 'hostname';
			  getIPOpts = ['-I'];
			}

			ipCommand = spawn(getIPCommand,getIPOpts); 
			ipCommand.stdout.on('data',d=>{
			  ipOut = d.toString('utf8').trim();
			});
			ipCommand.on('close',()=>{
			  if(process.platform == 'linux'){
			    ipOut = ipOut.split(' ')[0];
			  }
			  resolve({ip:ipOut,port:this.port,sslPort:this.sslPort});
			});
		});

	}
	checkForUpdates(){
		return new Promise((resolve,reject)=>{
			let host = 'raw.githubusercontent.com';
			if(typeof process.env.HANDYHOST_PRIVATE_REPO_TOKEN != "undefined"){
				host = process.env.HANDYHOST_PRIVATE_REPO_TOKEN+'@'+host;
			}
			const path = '/HandyOSS/HandyHost/master/VERSION';
			return this.queryHTTPSResponse(host,path).then(versionRepo=>{
				const localVersion = fs.readFileSync("./VERSION",'utf8').trim();
				if(typeof process.env.HANDYHOST_TEST_BRANCH != "undefined"){
					versionRepo = process.env.HANDYHOST_TEST_BRANCH;
				}
				resolve({
					latest:versionRepo,
					local:localVersion,
					isUpToDate: (localVersion == versionRepo)
				});
			}).catch(error=>{
				console.log('error checking for handyhost updates',error);
			})
		})
		
	}
	updateHandyHost(){
		return new Promise((resolve,reject)=>{
			this.checkForUpdates().then(updateData=>{
				const target = updateData.latest;
				console.log('starting updater','pid',process.pid,'path',process.argv.join(' '))
				const update = spawn('./update.sh',[target,process.argv.join(' '),process.pid],{env:process.env,detached:true});
				update.stdout.on('data',(d)=>{
					console.log('update stdout',d.toString());
					if(d.toString().trim() == 'restarting handyhost'){
						resolve(true);
					}
				})
				update.stderr.on('data',(d)=>{
					console.log('update stderr',d.toString());
					if(d.toString().trim() == 'restarting handyhost'){
						resolve(true);
					}
				})
				update.on('close',()=>{
					console.log('done with update');
					resolve(true);
				})
			})
		});
	}
	queryHTTPSResponse(host,path){
		return new Promise((resolve,reject)=>{
			let RESP = '';
			const request = https.request('https://'+host+path,response=>{
				//another chunk of data has been received, so append it to `str`
				
				response.on('data', (chunk) => {
					RESP += chunk.toString().replace(/\n/gi,'').trim();
				});

				//the whole response has been received, so we just print it out here
				response.on('end', () => {
					
					resolve(RESP)
					
				});

				if(response.statusCode.toString() != '200'){
					//something went wrong
					reject(response.statusCode.toString());
				}
			});
			request.on('error',e=>{
				console.log('error w request',e);
				reject(e);
			})
			request.end();
		})
	}
	checkForM1RosettaFun(){
		//check if this is running in macos rosetta, nice.
		return new Promise((resolve,reject)=>{
			if(process.platform != 'darwin'){
				resolve(false);
				return;
			}

			const s = spawn('sysctl', ['sysctl.proc_translated']);
			let out = '';
			s.stdout.on('data',d=>{
				out += d.toString();
			})
			s.on('close',()=>{
				let o = out.split(':');
				o = o[o.length-1];
				if(o.trim() == '1'){
					//is fn arm64
					resolve(true);
				}
				else{
					if(process.arch == 'arm64'){
						resolve(true);
					}
					resolve(false);
				}
			})
		})
		
	}
	initKeystore(){
		if(!fs.existsSync(process.env.HOME+'/.HandyHost/keystore')){
			fs.mkdirSync(process.env.HOME+'/.HandyHost/keystore','0700');
		  	//create certs
		  	const keyPath = process.env.HOME+'/.HandyHost/keystore/handyhost.key';
		  	const pubPath = process.env.HOME+'/.HandyHost/keystore/handyhost.pub';
		  	this.checkForM1RosettaFun().then(isRosetta=>{
		  		const homebrewPrefixMAC = isRosetta ? '/opt/homebrew' : '/usr/local';
			  	const openssl = process.platform == 'darwin' ? homebrewPrefixMAC+'/opt/[email protected]/bin/openssl' : 'openssl';
			  	const create = spawn(openssl,['genrsa', '-out', keyPath, '4096']);
			  	create.on('close',()=>{
			  		const createPub = spawn(openssl,['rsa', '-in', keyPath, '-pubout', '-out', pubPath])
			  		createPub.on('close',()=>{
			  			fs.chmodSync(keyPath,'0600');
			  			fs.chmodSync(pubPath,'0644');

			  		})
			  	})
		  	});
		  	
		}
	}
	encrypt(value,isForDaemon,daemonServiceName,isForHealthcheck){
		return new Promise((resolve,reject)=>{
			
			const pubKeyName = isForDaemon ? ( isForHealthcheck ? 'handyhost.pub' : 'daemon.pub' ) : 'handyhost.pub';
			const basePath = process.env.HOME+'/.HandyHost/keystore/';
			const pubPath = basePath+pubKeyName;
			const encryptedOutPath = isForDaemon ? basePath+'daemon_'+daemonServiceName : basePath+'k'+(new Date().getTime());
			this.checkForM1RosettaFun().then(isRosetta=>{
				const homebrewPrefixMAC = isRosetta ? '/opt/homebrew' : '/usr/local';
				const openssl = process.platform == 'darwin' ? homebrewPrefixMAC+'/opt/[email protected]/bin/openssl' : 'openssl';
				const args = ['rsautl', '-pubin', '-inkey', pubPath, '-encrypt', '-pkcs','-out',encryptedOutPath];
				
				const enc = spawn(openssl,args)
				enc.stdin.write(`${value}`);
				let resp = '';
				enc.stdout.on('data',d=>{
					//console.log('stdout',d.toString());
					resp += d.toString();
				})
				enc.stderr.on('data',d=>{
					console.log('stderr',d.toString());
				})
				enc.on('close',()=>{
					
					fs.chmodSync(encryptedOutPath,'0600');
					resolve(encryptedOutPath)
				})
				enc.stdin.end();
			});
		});
	}
	decrypt(encpath,isDaemon){
		return new Promise((resolve,reject)=>{
			const keyPath = process.env.HOME+'/.HandyHost/keystore/handyhost.key';
			this.checkForM1RosettaFun().then(isRosetta=>{
		  		const homebrewPrefixMAC = isRosetta ? '/opt/homebrew' : '/usr/local';
				//const homebrewPrefixMAC = process.arch == 'arm64' ? '/opt/homebrew' : '/usr/local';
			  	const openssl = process.platform == 'darwin' ? homebrewPrefixMAC+'/opt/[email protected]/bin/openssl' : 'openssl';
			  	const dec = spawn(openssl,['rsautl','-inkey',keyPath, '-decrypt', '-in', encpath]);
				let out = '';
				dec.stdout.on('data',d=>{
					out += d.toString();
				})
				dec.on('close',()=>{
					if(!isDaemon){
						fs.unlinkSync(encpath);
					}
					if(out.length > 0){
						if(out[out.length-1] == '\n'){
							//in some cases can add a newline to end of string...
							//and the gui doesnt allow newlines in fields
							out = out.slice(0,-1);
						}
					}
					resolve(out);
				})
			});
		})
	}
	getDarwinKeychainPW(serviceName){
		return new Promise((resolve,reject)=>{
			const getpw = spawn('security',['find-generic-password','-s',serviceName,'-a',process.env.USER,'-w']);
			let exists = true;
			getpw.stderr.on('data',d=>{
				console.log('err',d.toString());
				exists = false;
			});
			let out = '';
			getpw.stdout.on('data',d=>{
				out += d.toString();
			})
			getpw.on('close',()=>{
				resolve({
					exists,
					value:out.trim()
				});
			})
		})
		

	}
	setDarwinKeychainPW(pw,serviceName){
		const getpw = spawn('security',['find-generic-password','-a',process.env.USER,'-s',serviceName,'-w']);
		let exists = true;
		getpw.stderr.on('data',d=>{
			exists = false;
		})
		getpw.on('close',()=>{
			if(!exists){
				spawn('security',['add-generic-password','-a',process.env.USER,'-s',serviceName, '-w',pw])
			}
			else{
				const del = spawn('security',['delete-generic-password','-a',process.env.USER,'-s',serviceName]);
				del.on('close',()=>{
					spawn('security',['add-generic-password','-a',process.env.USER,'-s',serviceName, '-w',pw])
				})
			}
		})
		
				
	}
					
	encryptToBase64(value){
		return new Promise((resolve,reject)=>{
			const basePath = process.env.HOME+'/.HandyHost/keystore/';
			const encryptedOutPath = basePath+'temp'+(new Date().getTime());
			this.checkForM1RosettaFun().then(isRosetta=>{
		  		const homebrewPrefixMAC = isRosetta ? '/opt/homebrew' : '/usr/local';
				//const homebrewPrefixMAC = process.arch == 'arm64' ? '/opt/homebrew' : '/usr/local';
			  	const openssl = process.platform == 'darwin' ? homebrewPrefixMAC+'/opt/[email protected]/bin/openssl' : 'openssl';
				const enc = spawn(openssl,['rsautl','-pubin','-inkey',process.env.HOME+'/.HandyHost/keystore/daemon.pub', '-encrypt','-pkcs']);
				const toBase64 = spawn(openssl,['enc','-base64']);
				enc.stdin.write(`${value}`);
				enc.stdin.end();
				enc.stdout.pipe(toBase64.stdin);
				
				let out = '';
				toBase64.stdout.on('data',d=>{
					out += d.toString();
				})

				toBase64.on('close',()=>{
					resolve(out);
				})
			});
			
		})
	}
	isAuthEnabled(){
		//check if auth is enabled
		return true; //enable auth always
		const settingsPath = process.env.HOME+'/.HandyHost/authSettings.json';
		let isEnabled = false;
		if(fs.existsSync(settingsPath)){
			let settings = {};
			try{
				settings = JSON.parse(fs.readFileSync(settingsPath,'utf8'));
			}
			catch(e){
				console.log("ERROR PARSING DEFAULT AUTH FILE AT "+settingsPath,e);
			}
			isEnabled = typeof settings.enabled != "undefined" ? settings.enabled : isEnabled;
		}
		return isEnabled;
	}
	getAuthDefaultExpiry(){
		const settingsPath = process.env.HOME+'/.HandyHost/authSettings.json';
		let expiry = '30d';
		if(fs.existsSync(settingsPath)){
			let settings = {};
			try{
				settings = JSON.parse(fs.readFileSync(settingsPath,'utf8'));
			}
			catch(e){
				console.log("ERROR PARSING DEFAULT AUTH FILE AT "+settingsPath,e);
			}
			expiry = typeof settings.tokenTTL != "undefined" ? settings.tokenTTL : expiry;
		}
		return expiry;
	}
	initJWTKey(){
		const jwtPath = process.env.HOME+'/.HandyHost/keystore/jwt.key';
		const authPath = process.env.HOME+'/.HandyHost/keystore/auth.key';
		const settingsPath = process.env.HOME+'/.HandyHost/authSettings.json';
		
		const jwtKeyDefault = crypto.createHash('sha256').update( (Math.floor(new Date().getTime()*Math.random()) - Math.floor(Math.random()*new Date().getTime())).toString() ).digest('hex');
		const defaultKey = crypto.createHash('sha256').update('changemeplease').update(jwtKeyDefault).digest('hex');
		
		if(!fs.existsSync(jwtPath)){
			//init jwt key it it doesnt exist
			fs.writeFileSync(jwtPath,jwtKeyDefault,'utf8');
		}
		if(!fs.existsSync(authPath)){
			//set a default password
			fs.writeFileSync(authPath,defaultKey,'utf8');
		}
		if(!fs.existsSync(settingsPath)){
			//init default settings
			const settings = {
				enabled:true,
				initialPassword:'changemeplease',
				hasInitialized:false,
				tokenTTL:'30d'
			}
			fs.writeFileSync(settingsPath,JSON.stringify(settings,null,2),'utf8');
		}
		else{
			//settings exist, check if auth is enabled or needs enabled
			let settings = JSON.parse(fs.readFileSync(settingsPath,'utf8'));
			//enable auth always
			if(/*settings.enabled && */!settings.hasInitialized){
				fs.writeFileSync(jwtPath,jwtKeyDefault,'utf8'); //bounce the jwtKey in case we were trying to lock out other users
				const jwtKey = fs.readFileSync(jwtPath,'utf8').trim();
				//somebody just turned on auth, lets enable it
				const initialKey = crypto.createHash('sha256').update(settings.initialPassword).update(jwtKey).digest('hex');
				fs.writeFileSync(authPath,initialKey,'utf8');
				settings.hasInitialized = true;
				fs.writeFileSync(settingsPath,JSON.stringify(settings,null,2),'utf8')
			}
		}
		
	}
	hasDefaultAuth(){
		const authPath = process.env.HOME+'/.HandyHost/keystore/auth.key';
		const jwtPath = process.env.HOME+'/.HandyHost/keystore/jwt.key';
		const jwtKey = fs.readFileSync(jwtPath,'utf8').trim();
		const settingsPath = process.env.HOME+'/.HandyHost/authSettings.json';
		let defaultpw = 'changemeplease';
		if(fs.existsSync(settingsPath)){
			let settings = {};
			try{
				settings = JSON.parse(fs.readFileSync(settingsPath,'utf8'));
			}
			catch(e){
				console.log("ERROR PARSING DEFAULT AUTH FILE AT "+settingsPath,e);
			}
			defaultpw = typeof settings.initialPassword != "undefined" ? settings.initialPassword : defaultpw;
		}
		const defaultPass = crypto.createHash('sha256').update(defaultpw).update(jwtKey).digest('hex');
		const currentPass = fs.readFileSync(authPath,'utf8').trim();
		return defaultPass == currentPass;
	}
	checkAuth(pw){
		return new Promise((resolve,reject)=>{
			const authPath = process.env.HOME+'/.HandyHost/keystore/auth.key';
			const jwtPath = process.env.HOME+'/.HandyHost/keystore/jwt.key';
			const jwtKey = fs.readFileSync(jwtPath,'utf8').trim();
			const auth = fs.readFileSync(authPath,'utf8').trim();
			const pwHashed = crypto.createHash('sha256').update(pw).update(jwtKey).digest('hex');
			if(auth != pwHashed){
				resolve(false);
			}
			else{
				resolve(true);
			}
		});
	}
	changeAuth(newPass,oldPass){
		return new Promise((resolve,reject)=>{
			const authPath = process.env.HOME+'/.HandyHost/keystore/auth.key';
			const jwtPath = process.env.HOME+'/.HandyHost/keystore/jwt.key';
			const jwtKey = fs.readFileSync(jwtPath,'utf8').trim();
			const settingsPath = process.env.HOME+'/.HandyHost/authSettings.json';
			let defaultpw = 'changemeplease';
			if(fs.existsSync(settingsPath)){
				let settings = {};
				try{
					settings = JSON.parse(fs.readFileSync(settingsPath,'utf8'));
				}
				catch(e){
					console.log("ERROR PARSING DEFAULT AUTH FILE AT "+settingsPath,e);
				}
				defaultpw = typeof settings.initialPassword != "undefined" ? settings.initialPassword : defaultpw;
			}
			const defaultPass = crypto.createHash('sha256').update(defaultpw).update(jwtKey).digest('hex');
			const currentPass = fs.readFileSync(authPath,'utf8').trim();
			const newPassHashed = crypto.createHash('sha256').update(newPass).update(jwtKey).digest('hex');
			const oldPassHashed = crypto.createHash('sha256').update(oldPass).update(jwtKey).digest('hex');
			if(currentPass == defaultPass){
				//is default/brand new, change it
				fs.writeFileSync(authPath,newPassHashed,'utf8');
				resolve(true);
			}
			else{
				if(oldPassHashed == currentPass){
					//do change
					fs.writeFileSync(authPath,newPassHashed,'utf8');
					resolve(true);
				}
				else{
					resolve(false);
				}
			}
		});
	}
	checkAuthToken(token){
		const jwtKey = fs.readFileSync(process.env.HOME+'/.HandyHost/keystore/jwt.key','utf8').trim();
		let verified = false;
		try{
			verified = jsonwebtoken.verify(token, jwtKey);
		}
		catch(e){
			verified = false;
		}
		return verified;
	}

	bumpToken(){
		return new Promise((resolve,reject)=>{
			const jwtKey = fs.readFileSync(process.env.HOME+'/.HandyHost/keystore/jwt.key','utf8').trim();
			const data = {
		        timestamp:new Date().getTime()
		    }
		    const tokenTTL = this.getAuthDefaultExpiry();
		    const token = jsonwebtoken.sign(data, jwtKey, { expiresIn: tokenTTL }); //likely their browser sesh will last 30 days..
	        resolve(token);
		});
		
	}
	getLocalIPRange(){
		return new Promise((resolve,reject)=>{
			let getIPCommand;
			let getIPOpts;
			let ipCommand;
			let ipRangeOut;
			
			if(process.platform == 'darwin'){
				getIPCommand = 'ipconfig';
				getIPOpts =  ['getifaddr', 'en0'];
			}
			if(process.platform == 'linux'){
				//hostname -I [0]
				getIPCommand = 'hostname';
				getIPOpts = ['-I'];
			}

			ipCommand = spawn(getIPCommand,getIPOpts); 
			ipCommand.stdout.on('data',d=>{
				ipRangeOut = d.toString('utf8').trim();
			});
			ipCommand.on('close',()=>{
				if(process.platform == 'linux'){
					ipRangeOut = ipRangeOut.split(' ')[0];
				}
				ipRangeOut = ipRangeOut.split('.').slice(0,-1).join('.')
				ipRangeOut += '.0/24';
				console.log('ip range ',ipRangeOut);
				resolve(ipRangeOut);
			});
		})
		
	}
}