
import * as path from 'path';
import * as openpgp from 'openpgp';
import * as fs from 'fs';
import * as osenv from "osenv";
import * as os from 'os';
import * as event from 'events';
import * as Async from 'async';
import * as crypto from 'crypto'
import * as child_process from 'child_process';

import * as ramdonString from './randomString';
import * as testImapAccount from './testImapAccount';
import * as smtp from './smtpClass';

interface IpAddress_v4 {
    localIpAddress:string;
    mask:string;
    mac:string;
    routeIpAddress:string;
}

export class runCallBack {
    private grep: child_process.ChildProcess = null;
    constructor ( command: string, option: any, messageBack:  ( message ) => void, private CallBack: ICallBack ) {
        const envString = JSON.stringify ( process.env )
        const optionString = JSON.stringify ( option )
        const env = ( option && typeof option === 'object' && Object.keys ( option ).length)
            ? JSON.parse( '{ "env": '
                .concat ( envString.substring ( 0, envString.length - 1 ) + "," )
                .concat ( optionString.substring ( 1, optionString.length ))
                .concat ( '}' ))
            : null
        
        try {
            this.grep = child_process.fork ( command, [], env )

            this.grep.on ( 'message', msg => {
                if ( messageBack && typeof messageBack === 'function' )
                    return messageBack ( msg )
                console.log ('grep.on message: ',msg)
            })
            this.grep.on ( 'close',  ( code, signal ) => {
                console.log ('runCallBack exit with code:', code, signal )
                if ( code ) {
                    
                    if ( CallBack )
                        CallBack ( new Error ( 'Exit with code:' + code ))
                    return
                }
                if ( signal ) {
                    if ( CallBack )
                        CallBack ( new Error ( 'Exit with cancel' ))
                    return
                }
                if ( CallBack )
                    CallBack ()
            });

        } catch ( ex ) {
            if ( ex.code === 'ENOENT' ) {
                console.log ( 'runCallBack have not find command: ', command )
            }
            console.log ( 'runCallBack catch ex:', ex )
            if ( CallBack )
                CallBack ( ex )
        }
        
    }
    public stop () {
        this.grep.kill ( 'SIGINT' )
    }
} 

export const uuid_generate = () => {
      let lut: Array < string > = [];
      for (let i = 0; i < 256; i ++ ) {
			   lut [i] = ( i < 16 ? '0' : '' ) + ( i ).toString (16);
		  }
		  var d0 = Math.random() * 0xffffffff | 0;
	    var d1 = Math.random() * 0xffffffff | 0;
	    var d2 = Math.random() * 0xffffffff | 0;
	    var d3 = Math.random() * 0xffffffff | 0;
	    return  lut [d0 & 0xff ]+ lut[ d0 >> 8 & 0xff ]+ lut [ d0 >> 16 & 0xff ]+ lut [ d0 >> 24 & 0xff ] + '-' +
	       lut [ d1 & 0xff ]+ lut [ d1 >> 8 & 0xff ]+'-'+ lut[ d1 >> 16 & 0x0f | 0x40 ]+ lut [ d1 >> 24 & 0xff ] + '-' +
	       lut [ d2 & 0x3f | 0x80 ]+ lut [ d2 >> 8 & 0xff ]+'-'+ lut[ d2 >> 16 & 0xff ]+ lut [ d2 >> 24 & 0xff ]+
	       lut [ d3 & 0xff ]+ lut[ d3 >> 8 & 0xff ]+ lut [ d3 >> 16 & 0xff ]+ lut [ d3 >> 24 & 0xff ];

}

export const uuidFileName = () => {
    const file = uuid_generate().split ('-')
    
    return file.join ( '' ).substr ( 0, 2 + Math.random () * 30 )
}

const setupFolderName = '/Vpn.Email'
const localPath = path.parse ( osenv.home() + setupFolderName )
const lettersRegexp = /^[A-Za-z]+$/
const numberRegexp = /^[0-9]+$/
const SystemInitFilePath = path.parse ( path.format ( localPath ) + '/initFile' )
const FileErr = 'fileSystemError'
const cer_PrivfileName = path.parse ( path.format ( localPath ) + '/systemCerPriv.key.pem' )
const cer_PubfileName = path.parse ( path.format ( localPath ) + '/systemCerPub.key.pem' )
const EmailRegexp = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i

export const saveConfig = ( session: ISockSession, CallBack: ICallBack ) => {

    const key = session.keyPair;
    const _session: ISockSession = {
        imapArray: session.imapArray,
        imapConnected: false,
        keyPair: null,
        keyPairPassword: null,
        sentEmail: null,
        vpnEmailPublicKey: null,
        newKeyPairProcessRunning: null,
        vpnEmailServerKeepConnected: null,
        newKeyProcessPid: null,
        vpnServerConnectData: session.vpnServerConnectData,
        dockerPublicKey:session.dockerPublicKey
    }
    if ( _session.imapArray &&  _session.imapArray.dummyData )
        _session.imapArray.dummyData = null;
    Async.waterfall ([
        next => _encrypt ( JSON.stringify ( _session ), key.publicKey, key.privateKey, next ),
        ( data, next ) => fs.writeFile ( path.format ( SystemInitFilePath ), data, { encoding: 'utf8' }, next )
    ], CallBack )
}

/**
 *          read the config file with a password
 *          @param password <string>
 */
const readConfig = ( sockSession: ISockSession, CallBack: ICallBack ) => {

    const pathname = path.format ( SystemInitFilePath );

    //          read INIT data from SystemInitFilePath
    fs.readFile ( pathname, { encoding: 'utf8' }, ( err, data ) => {
        console.log ('open System InitFilePath success!')
        if ( err )
            return CallBack ( null, sockSession )

        const key = sockSession.keyPair;

        if ( data && data.length && key.keyPasswordOK ) {

            return _Decryption ( data, key.privateKey, key.publicKey, ( err, initData ) => {
                if ( err )
                    return CallBack ( err )
                const ses:ISockSession = JSON.parse ( initData.data )
                sockSession.imapArray = ses.imapArray;
                sockSession.vpnServerConnectData = ses.vpnServerConnectData;
                CallBack ( null, sockSession )
            })
            
        }
        
        CallBack ( null, sockSession )
    
    })
    
}


/**
 *      check the file system ready or not
 *      if have password, administratorEmail ... will check is it working if already init file, will make new key pair if haven't init 
 *      @param sockSession <ISockSession>
 *      @param CallBack <function>
 */
export const checkInitFile = ( sockSession: ISockSession, CallBack: ICallBack ) => { 

    //    check & make the system path

    const keyPair = sockSession.keyPair;
    Async.parallel ([
        next => fs.access ( path.format ( cer_PrivfileName ), next ),
        next => fs.access ( path.format ( cer_PubfileName ), next )
    ], err => {
        if ( err )
            return CallBack ( null, sockSession )

        keyPair.privateKeyUTF8= fs.readFileSync ( path.format ( cer_PrivfileName ), 'utf8' )
        keyPair.publicKeyUTF8 = fs.readFileSync ( path.format ( cer_PubfileName ), 'utf8' )

        constructorViaKeyPair ( keyPair, err => {


            //          if vpn.email setup error delete all setup
            if ( err ) {
                deleteSystemPassword ( sockSession, CallBack )
            }
                

            if ( sockSession.keyPairPassword && sockSession.keyPairPassword.length && checkPassword ( keyPair, sockSession.keyPairPassword )) {

                return readConfig ( sockSession, CallBack )
            }
                
        
            CallBack ( null, sockSession )
        })

        
    })


    
    //          already have key pair

    
    //      Try use password to open PEM
    
}

export const deleteSystemPassword = ( session: ISockSession, CallBack: ICallBack ) => {
    session.keyPairPassword = null
    session.imapArray = null
    session.vpnServerConnectData = null;
    session.keyPair = generatePem ()
    Async.parallel([
        next => fs.unlink ( path.format ( cer_PrivfileName ), next ),
        next => fs.unlink ( path.format ( cer_PubfileName ), next) ,
        next => fs.unlink ( path.format ( SystemInitFilePath ), next ) 
    ],  (err) => {
        console.log ('Async.parallel deleteSystemPassword success', err)
        CallBack()
    } );
}

const generatePem = () => {
    
    const ret: IKeyInformation  = {
        userId: null,
        keyPassword: null,
        privateKeyUTF8:null,
        publicKeyUTF8: null,
        privateKey:null,
        publicKey: null,
        userName: null,
        email: null,
        keyBitLength: null,
        createDate: null,
        keyPasswordOK: false
    }
    return ( ret )
}
    /**------------------------------------------------------------------------------------------------------
     * 
     *                              check the private key password
     *                              input:
     *                                  Password
     * 
     ------------------------------------------------------------------------------------------------------*/
const checkPassword = ( keyPair: IKeyInformation, password: string ) => {
    return keyPair.keyPasswordOK = keyPair.privateKey.decrypt ( password )
}
/**
 *          
 */
export const constructorViaKeyPair = ( keyPair: IKeyInformation, CallBack: ICallBack ) => {
        
    let privateKey1 = openpgp.key.readArmored ( keyPair.privateKeyUTF8 );
    let pubvateKey1 = openpgp.key.readArmored ( keyPair.privateKeyUTF8 );
    
    if ( privateKey1.err || pubvateKey1.err ) {
        
        return CallBack ( new Error ('key pair error'));
    }

    keyPair.privateKey = privateKey1.keys[0];
    keyPair.publicKey = pubvateKey1.keys;

    var user = keyPair.privateKey.users;

    if ( user && user.length ) {
        keyPair.userId = user[0].userId.userid;
        getInfoFromUserId ( keyPair );
    }
    keyPair.keyBitLength = getBitLength ( keyPair );
    keyPair.createDate =  getCreateDate ( keyPair );

    CallBack ( null )
}
/**
 *        get key information
 *        @param key <IKeyInformation>
 */
const getInfoFromUserId = ( key: IKeyInformation ) => {
    if ( key.userId.length ) {
        let temp = key.userId.split ( ' <' );
        let temp1 = temp[0].split ( ' (' );
        let temp2 = temp1.length > 1 
            ? temp1[1].split ( '||' )
            : '';
        key.email = temp.length > 1
            ? temp [1].slice ( 0, temp [1].length - 1 )
            : '';
        key.userName = temp1 [0];
    }
}
/**
 *      get Bit Length
 *      @param key <IKeyInformation>
 */
const getBitLength = ( key: IKeyInformation ) => {
    let size = -1;
    if ( key.privateKey.primaryKey.mpi.length ) {
        size = ( key.privateKey.primaryKey.mpi [0].byteLength () * 8 );
    }
    return size;
}

const getCreateDate = ( key: IKeyInformation ) => {
    return key.privateKey.primaryKey.created;
}

/**
 *  @param text {string}
 *  @param pubKey {string}
 *  @param privateKey {string} use signatures
 *  @param password {string} privateKey
 *  @param callBack {callback function}
 */
export const _encrypt = (text: string, publicKey, privateKey, callback) => {

    const optionQQ = {
        data: text,
        publicKeys: publicKey,
        privateKeys: privateKey
    }
    openpgp.encrypt ( optionQQ ).then (( m ) => {
        callback( null, m.data );
    }).catch (( err ) => {
        callback ( err );
    });
}
export const Encrypt = ( text: string, pubKey, privateKey, password: string, 
    CallBack: ( err?: Error, data?: string ) => void ) => {

        const publicKeys = ( typeof pubKey === 'string' )
            ? openpgp.key.readArmored ( pubKey ).keys
            : pubKey
        
        const privateKeys = ( typeof privateKey === 'string' )
            ? openpgp.key.readArmored ( privateKey ).keys[0]
            : privateKey

        if ( ! publicKeys || ! privateKey || ! privateKeys.decrypt )
            return CallBack ( new Error ( 'util Encrypt got ERROR: have not key pair information!' ))

        if ( password && password.length && !privateKeys.decrypt ( password )) {
            return CallBack ( new Error ('password not matched') );
        };
        
        const optionQQ = {
            data: text,
            publicKeys: publicKeys,
            privateKeys: privateKeys,
            passwords: password
        }
        
        openpgp.encrypt ( optionQQ ).then ( m  => {
            CallBack ( null, m.data );
        }).catch ( err => {
            CallBack ( err );
        });
    }
/**
 *  @param encryptMessage {string}
 *  @param password {string} private pem password
 *  @param privatePem {string} private key
 *  @param publicKey {string} public key for check signatures
 *  @param fb {callBack function}
 */
export const Decryption = ( encryptMessage: string, password: string, privatePem: string, publicKey: string, 
    CallBack: ( err?: Error, data?: any ) => void ) => {
        
    try {

        const privateKey = openpgp.key.readArmored ( privatePem ).keys[0];
        
        const PublicKey = publicKey
            ? openpgp.key.readArmored ( publicKey ).keys
            : null;
     
        if ( ! privateKey.decrypt ( password )) {
         
            return CallBack ( new Error ( 'password not matched' ));
        }

        const DMessage = openpgp.message.readArmored ( encryptMessage );

        const DecryptionOption = {
            message: DMessage,
            privateKey: privateKey,
            publicKeys: PublicKey,
            //  password:password    if have will be ERROR as **** No symmetrically encrypted session key packet found.****
        }

        openpgp.decrypt ( DecryptionOption ).then ( plaintext => {
            
            if ( publicKey && publicKey.length && !plaintext.signatures[0].valid )
                return CallBack ( new Error ( 'Signatures error' ))
            CallBack ( null, plaintext );
            
        }).catch (err  => {
            // failure
            CallBack ( err );
        })
            
    } catch ( ex ) {
        console.log ( 'error =>', ex )
        CallBack ( ex );
    }
    
}
export const _Decryption = (encryptMessage: string, privateKey, publicKeys, CallBack: ICallBack) => {

    const DecryptionOption = {
        message: openpgp.message.readArmored ( encryptMessage ),
        privateKey: privateKey,
        publicKeys: publicKeys
    }

    openpgp.decrypt (DecryptionOption).then (( plaintext ) => {

        return CallBack ( null, plaintext );
        
    }).catch ((err)  => {

        // failure
        return CallBack ( err );
    })
}
export const defineSession = (  password: string , CallBack: ICallBack ) => {
    
    const sockSession: ISockSession = {
        keyPair: {
            createDate: null,
            email: null,
            keyPasswordOK: null,
            privateKey: null,
            privateKeyUTF8: null,
            userName: null,
            publicKey: null,
            publicKeyUTF8: null,
            userId: null,
            keyBitLength: null,
            keyPassword: password
        },
        imapConnected: false,
        vpnEmailPublicKey: fs.readFileSync ( 'BFC2ABE0.pub.asc', 'utf8' ),
        imapArray: null,
        vpnEmailServerKeepConnected: false,
        vpnServerConnectData: null,
        keyPairPassword: password,
        sentEmail: null,
        newKeyPairProcessRunning: false,
        newKeyProcessPid: null,
        dockerPublicKey: null
    }
    checkInitFile ( sockSession, CallBack )
    
}

export const machineuuid = () => {
    let _ret = os.totalmem() + os.cpus().map( function( cpu ){ return cpu.model } ).join( ":" ) + JSON.stringify (os.networkInterfaces());
    const ret = crypto.createHash( "md5" ).update( _ret ).digest( "HEX" );
    return ret;
}
