'use strict';

const ObjectId = require('mongodb').ObjectId;
const fingerprint = require('key-fingerprint').fingerprint;
const forge = require('node-forge');
const crypto = require('crypto');
const tools = require('./tools');
const { publish, DKIM_CREATED, DKIM_UPDATED, DKIM_DELETED } = require('./events');
const { encrypt, decrypt } = require('./encrypt');

const { promisify } = require('util');
const generateKeyPair = promisify(crypto.generateKeyPair);

class DkimHandler {
    constructor(options) {
        options = options || {};
        this.cipher = options.cipher;
        this.secret = options.secret;

        this.database = options.database;
        this.redis = options.redis;

        this.loggelf = options.loggelf || (() => false);
    }

    async generateKey(keyBits, keyExponent) {
        const { privateKey, publicKey } = await generateKeyPair('rsa', {
            modulusLength: keyBits || 2048, // options
            publicExponent: keyExponent || 65537,
            publicKeyEncoding: {
                type: 'spki',
                format: 'pem'
            },
            privateKeyEncoding: {
                type: 'pkcs8',
                format: 'pem'
            }
        });

        return { privateKey, publicKey };
    }

    async set(options) {
        const domain = tools.normalizeDomain(options.domain);
        const selector = options.selector;
        const description = options.description;

        let privateKeyPem = options.privateKey;
        let publicKeyPem;

        if (!privateKeyPem) {
            let keyPair = await this.generateKey();
            if (!keyPair || !keyPair.privateKey || !keyPair.publicKey) {
                let err = new Error('Failed to generate key pair');
                err.responseCode = 500;
                err.code = 'KeyGenereateError';
                throw err;
            }
            privateKeyPem = keyPair.privateKey;
            publicKeyPem = keyPair.publicKey;
        }

        if (!publicKeyPem) {
            // extract public key from private key using Forge
            let privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
            let publicKey = forge.pki.setRsaPublicKey(privateKey.n, privateKey.e);
            publicKeyPem = forge.pki.publicKeyToPem(publicKey);

            if (!publicKeyPem) {
                let err = new Error('Failed to generate public key');
                err.responseCode = 500;
                err.code = 'KeyGenereateError';
                throw err;
            }
        }

        let fp;
        try {
            fp = fingerprint(privateKeyPem, 'sha256', true);

            let ciphered = crypto.publicEncrypt(publicKeyPem, Buffer.from('secretvalue'));
            let deciphered = crypto.privateDecrypt(privateKeyPem, ciphered);
            if (deciphered.toString() !== 'secretvalue') {
                throw new Error('Was not able to use key for encryption');
            }
        } catch (E) {
            let err = new Error('Invalid or incompatible private key. ' + E.message);
            err.responseCode = 400;
            err.code = 'InputValidationError';
            throw err;
        }

        // encrypt if needed
        privateKeyPem = await encrypt(privateKeyPem, this.secret);

        let dkimData = {
            domain,
            selector,
            privateKey: privateKeyPem,
            publicKey: publicKeyPem,
            fingerprint: fp,
            created: new Date(),
            latest: true
        };

        if (description) {
            dkimData.description = description;
        }

        let r;

        try {
            r = await this.database.collection('dkim').findOneAndReplace(
                {
                    domain
                },
                dkimData,
                {
                    upsert: true,
                    returnDocument: 'after'
                }
            );
        } catch (err) {
            err.responseCode = 500;
            err.code = 'InternalDatabaseError';
            throw err;
        }

        if (!r.value) {
            let err = new Error('Failed to insert DKIM key');
            err.responseCode = 500;
            err.code = 'InternalDatabaseError';
            throw err;
        }

        if (this.redis) {
            if (r.lastErrorObject.upserted) {
                try {
                    await publish(this.redis, {
                        ev: DKIM_CREATED,
                        dkim: r.value._id,
                        domain,
                        selector,
                        fingerprint: fp
                    });
                } catch (err) {
                    // ignore?
                }
            } else if (r.lastErrorObject.updatedExisting) {
                try {
                    await publish(this.redis, {
                        ev: DKIM_UPDATED,
                        dkim: r.value._id,
                        domain,
                        selector,
                        fingerprint: fp
                    });
                } catch (err) {
                    // ignore?
                }
            }
        }

        return {
            id: r.value._id.toString(),
            domain: dkimData.domain,
            selector: dkimData.selector,
            description: dkimData.description,
            fingerprint: dkimData.fingerprint,
            publicKey: dkimData.publicKey,
            dnsTxt: {
                name: dkimData.selector + '._domainkey.' + dkimData.domain,
                value: 'v=DKIM1;t=s;p=' + dkimData.publicKey.replace(/^-.*-$/gm, '').replace(/\s/g, '')
            }
        };
    }

    async get(options, includePrivateKey) {
        let query = {};
        options = options || {};

        if (options.domain) {
            query.domain = tools.normalizeDomain(options.domain);
        } else if (options._id && tools.isId(options._id)) {
            query._id = new ObjectId(options._id);
        } else {
            let err = new Error('Invalid or unknown DKIM key');
            err.responseCode = 404;
            err.code = 'DkimNotFound';
            throw err;
        }

        let dkimData;
        try {
            dkimData = await this.database.collection('dkim').findOne(query);
        } catch (err) {
            err.responseCode = 500;
            err.code = 'InternalDatabaseError';
            throw err;
        }
        if (!dkimData) {
            let err = new Error('Invalid or unknown DKIM key');
            err.responseCode = 404;
            err.code = 'DkimNotFound';
            throw err;
        }

        let privateKey;
        if (includePrivateKey) {
            privateKey = await decrypt(dkimData.privateKey, this.secret, this.cipher);
        }

        return {
            id: dkimData._id.toString(),
            domain: dkimData.domain,
            selector: dkimData.selector,
            description: dkimData.description,
            fingerprint: dkimData.fingerprint,
            publicKey: dkimData.publicKey,
            privateKey,
            dnsTxt: {
                name: dkimData.selector + '._domainkey.' + dkimData.domain,
                value: 'v=DKIM1;t=s;p=' + dkimData.publicKey.replace(/^-.*-$/gm, '').replace(/\s/g, '')
            },
            created: dkimData.created
        };
    }

    async del(options) {
        let query = {};

        if (options.domain) {
            query.domain = tools.normalizeDomain(options.domain);
        } else if (options._id && tools.isId(options._id)) {
            query._id = new ObjectId(options._id);
        } else {
            let err = new Error('Invalid or unknown DKIM key');
            err.responseCode = 404;
            err.code = 'DkimNotFound';
            throw err;
        }

        // delete dkim key from database
        let r;
        try {
            r = await this.database.collection('dkim').findOneAndDelete(query);
        } catch (err) {
            err.responseCode = 500;
            err.code = 'InternalDatabaseError';
            throw err;
        }

        if (!r.value) {
            let err = new Error('Invalid or unknown DKIM key');
            err.responseCode = 404;
            err.code = 'DkimNotFound';
            throw err;
        }

        try {
            await publish(this.redis, {
                ev: DKIM_DELETED,
                dkim: r.value._id,
                domain: r.value.domain,
                selector: r.value.selector,
                fingerprint: r.value.fingerprint
            });
        } catch (err) {
            // ignore?
        }

        return true;
    }
}

module.exports = DkimHandler;
