import { createECDH, ECDH, createCipheriv, createDecipheriv, randomFillSync, createHash } from 'crypto';
import { strict } from 'assert';
import { stringify } from 'querystring';

/**
 * JSON Wek Token
 */
export interface JWK {
    "kty": string;
    "d"?: string;
    "crv": string;
    "kid": string;
    "x": string;
    "y"?: string;
}

/**
 * Hybrid EC encryption scheme that EC curve secp256k1, and chacha20-poly1305 or aes-256-gcm to encrypt data.
 * The returned data is a packed Buffer with the public key, nonce/iv, tag, and encrypted data.
 */
export class ECIES {
    /**
     * This creates a EC secp256k1 key pair and returns the private key as a buffer.
     * @returns EC Private Key as a Buffer
     */
    createKeyPair(): Buffer {
        let ec: ECDH = createECDH('secp256k1');
        ec.generateKeys();
        return ec.getPrivateKey();
    }

    /**
     * This returns the calculated secret from a private and public key.
     * 
     * @param privateKey: Buffer
     * @param publicKey: Buffer
     * @returns secret
     */
    getSecret(privateKey: Buffer, publicKey: Buffer): Buffer {
        let ec: ECDH = createECDH('secp256k1');
        ec.setPrivateKey(privateKey);
        return ec.computeSecret(publicKey);
    }

    /**
     * Takes EC private key and returns the public key.
     * 
     * @param privateKey EC Private Key
     * @param compress If true return only the x value
     * @returns publicKey X,Y buffer
     */
    getPublicKey(privateKey: Buffer, compress?: Boolean): Buffer {
        let ec: ECDH = createECDH('secp256k1');
        ec.setPrivateKey(privateKey);
       // console.log('pub', ec.getPublicKey('hex'));
       // console.log('pub',Buffer.from(ec.getPublicKey('latin1'), 'latin1').toString('hex'));
        return (compress === true ?  Buffer.from(ec.getPublicKey('hex','compressed'), 'hex') : ec.getPublicKey());
    }

    /**
     * This takes an EC private key and returns the JWK.
     * 
     * @param privateKey EC private key
     * @returns Json Web Token
     */
    privateJWK(privateKey: Buffer): JWK {
        let ec: ECDH = createECDH('secp256k1');
        ec.setPrivateKey(privateKey);
        let jwk = this.publicJWK(ec.getPublicKey());
        jwk.d = privateKey.toString('base64');
        return jwk;
    }

    /**
     * This takes an EC public key and returns the JWK.
     * 
     * @param publicKey EC Public Key
     * @returns Json Web Token
     */
    publicJWK(publicKey: Buffer): JWK {
        let x:string;
        let y:string;

        let jwk: JWK = {
            "kty": "EC",
            "crv": "secp256k1",
            "kid": "1",
            "x": ""
        }

        switch (publicKey.length) {
            case 33: 
                jwk.x = publicKey.toString('base64');
                break;
            case 65:
                var bufX = Buffer.alloc(32);
                var bufY = Buffer.alloc(32);
                publicKey.copy(bufX,0,1,33);
                publicKey.copy(bufY,0,33);
                jwk.x = bufX.toString('base64');
                jwk.y = bufY.toString('base64');
                break;
            case 64:
                bufX = Buffer.alloc(32);
                bufY = Buffer.alloc(32);
                publicKey.copy(bufX,0,0,32);
                publicKey.copy(bufY,0,32);
                jwk.x = bufX.toString('base64');
                jwk.y = bufY.toString('base64');
                break;
            default:
                let err = new Error('Invalid Key');
                err.name = 'Invalid_Key';
                throw err;
        }
        jwk.kid = createHash('sha256').update(publicKey).digest().toString('base64');

        return jwk;

    }

    /**
     * Return a Buffer from either a public or private JWK.
     *  
     * @param jwk  public or private JSON Web Key
     * @returns Buffer of either public or private key
     */
    JWKtoBuffer(jwk: JWK ): Buffer {

        if(jwk.d) {
            return Buffer.from(jwk.d, 'base64');
        } else if (jwk.y) {
            return Buffer.concat([Buffer.alloc(1, 0x04),Buffer.from(jwk.x,'base64'),Buffer.from(jwk.y,'base64')]);
        } else {
            return Buffer.from(jwk.x,'base64');
        }
    }

    getPEM(ecKey: Buffer, encoding: 'RAW' | 'DER' ,type: 'Private' | 'Public'): string {
        let PEM: string = '';
        let pemStr = '';

        (encoding === 'RAW' ? pemStr = this.getDER(ecKey,type).toString('base64') : pemStr = ecKey.toString('base64'));

       // console.log(pemStr);
        let pemForm:string = '';
        let i = 0;
        let c = 64;
        do {
            var j = i + 64;
            (j >= pemStr.length ? c = pemStr.length - i : c = 64);
            pemForm = `${pemForm}${pemStr.substr(i,c)}\n`;
            i = j;
        //    console.log(i);
        } while (i < pemStr.length);

    

        if(type === "Private") {
           
            PEM = `-----BEGIN EC PRIVATE KEY-----\n${pemForm}----END EC PRIVATE KEY-----`
        } else {
            PEM = `-----BEGIN PUBLIC KEY-----\n${pemForm}-----END PUBLIC KEY-----`
        }
        // console.log(PEM);
        return PEM;
    }

    getDER(ecKey: Buffer, type: 'Private' | 'Public'): Buffer {
        let packDER: Buffer;
        if(type === 'Private') {
            packDER = Buffer.concat([Buffer.from('30740201010420','hex'),ecKey,Buffer.from('a00706052b8104000aa144034200','hex'),this.getPublicKey(ecKey)]);
         
        } else {
            let pre: string;
            
            (ecKey.length === 33 ? (pre = '3036301006072a8648ce3d020106052b8104000a032200') : (pre = '3056301006072a8648ce3d020106052b8104000a034200'));

            if(ecKey.length > 65)
                throw new Error('Invalid key');    
            packDER = Buffer.concat([Buffer.from(pre,'hex'),ecKey]);
        }
       
        return packDER;
    }


   /**
     * This takes an EC public key as input, creates an unique EC pair to encrypt the data.
     * Returns a packed buffer of the EC public key, nonce, tag, and encrypted data.
     * Optional to supply Private Key 
     * @param publicKey EC Public Key
     * @param privateKey Optional
     * @param data Data to encrypt
     * @returns Buffer(Bytes) - ECPubKey(33) iv(12) tag(16) encData(variable)
     */
    encryptAES256(publicKey: Buffer, data: Buffer): Buffer
    encryptAES256(publicKey: Buffer, privateKey: Buffer, data: Buffer): Buffer
    encryptAES256(publicKey: Buffer, arg2: Buffer, arg3?: Buffer): Buffer {

        let privateKey: Buffer;
        let data: Buffer;

        if(arg2 && arg3) {
            privateKey = arg2;
            data = arg3;
        } else {
            privateKey = this.createKeyPair();
            data = arg2;
        }

        let iv = Buffer.alloc(12);
        randomFillSync(iv);
        // console.log('nonce', nonce.toString('hex'));
        
        let key = this.getSecret(privateKey,publicKey);
        // console.log('key', key.toString('hex'));
        let aes = createCipheriv('aes-256-gcm',key,iv);
        let encData = aes.update(data);
        aes.final();
        let tag = aes.getAuthTag();
        let pack = Buffer.concat([this.getPublicKey(privateKey,true),iv,tag,encData]);
        return pack;
    }

     /**
     * Takes private EC key of the public key used to encrypt the data and decrypts it.
     * 
     * @param privateKey EC Key used to encrypt the data.
     * @param encodedData Buffer(Bytes) - ECPubKey(33) iv(12) tag(16) encData(variable)
     * @returns Buffer of decrypted data. 
     */
    decryptAES256(privateKey: Buffer, encodedData: Buffer): Buffer {
        let pubKey = Buffer.alloc(33);
        encodedData.copy(pubKey,0,0,33);
        let iv = Buffer.alloc(12);
        encodedData.copy(iv,0,33,(33+12));
        let tag = Buffer.alloc(16);
        encodedData.copy(tag,0,(33+12),(33+12+16));
        let encData = Buffer.alloc(encodedData.length-(33+12+16));
        encodedData.copy(encData,0,(33+12+16));
        let key = this.getSecret(privateKey,pubKey);
        
        // console.log('key', key.toString('hex'));
        let aes = createDecipheriv('aes-256-gcm',key,iv);
        aes.setAuthTag(tag);
        let data = aes.update(encData);
        aes.final();

        return data;
    }


    /**
     * This takes an EC public key as input, creates an EC pair to encrypt the data.
     * Returns a packed buffer of the EC public key, nonce, tag, and encrypted data. 
     * Optional to supply Private Key 
     * @param publicKey EC Public Key
     * @param privateKey Optional
     * @param data Data to encrypt
     * @returns Buffer(Bytes) - ECPubKey(33) nonce(12) tag(16) encData(variable)
     */
    encryptChaCha20(publicKey: Buffer, data: any): Buffer
    encryptChaCha20(publicKey: Buffer, privateKey: Buffer, data: any): Buffer 
    encryptChaCha20(publicKey: Buffer, arg2: any, arg3?: any): Buffer {

        let privateKey: Buffer;
        let data: Buffer;

        if(arg2 && arg3) {
            privateKey = arg2;
            data = arg3;
        } else {
            privateKey = this.createKeyPair();
            data = arg2;
        }

        let nonce = Buffer.alloc(12);
        randomFillSync(nonce);
        // console.log('nonce', nonce.toString('hex'));
        // let tempKey: Buffer = Buffer.alloc(0);
        
        let key = this.getSecret(privateKey,publicKey);
        // console.log('key', key.toString('hex'));
        let cipher = createCipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
        let encData = cipher.update(data);
        cipher.final();
        let tag = cipher.getAuthTag();
        // console.log('data enc ', encData.toString('hex'));
        // console.log('tag', tag.toString('hex'));
        // console.log('enc pub', this.getPublicKey(tempKey, true).toString('hex'));
        let pack = Buffer.concat([this.getPublicKey(privateKey,true),nonce,tag,encData]);
        // console.log(pack.toString('hex'));
        // console.log(pack.toString('base64'));
        return pack;
    }

    /**
     * Takes private EC key of the public key used to encrypt the data and decrypts it.
     * 
     * @param privateKey EC Key used to encrypt the data.
     * @param encodedData Buffer(Bytes) - ECPubKey(33) nonce(12) tag(16) encData(variable)
     * @returns Buffer of decrypted data. 
     */
    decryptChaCha20(privateKey: Buffer, encodedData: Buffer): Buffer {
        let pubKey = Buffer.alloc(33);
        encodedData.copy(pubKey,0,0,33);
        let nonce = Buffer.alloc(12);
        encodedData.copy(nonce,0,33,(33+12));
        let tag = Buffer.alloc(16);
        encodedData.copy(tag,0,(33+12),(33+12+16));
        let data = Buffer.alloc(encodedData.length-(33+12+16));
        encodedData.copy(data,0,(33+12+16));
        let key = this.getSecret(privateKey,pubKey);
        // console.log('key', key.toString('hex'));
        // console.log('pubKey', pubKey.toString('hex'));
        // console.log('nonce', nonce.toString('hex'));
        // console.log('tag', tag.toString('hex'));

        let dec = createDecipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
        dec.setAuthTag(tag);
        let decData = dec.update(data);
        dec.final();
        // console.log('mdg', decData.toString());
        return decData;
        
    }
}