'use strict';

var MSG_PART_ATTR_CONTENT = 'content';
var MSG_PART_TYPE_TEXT = 'text';

var ngModule = angular.module('woServices');
ngModule.service('publickeyVerifier', PublickeyVerifier);
module.exports = PublickeyVerifier;

var ImapClient = require('imap-client');

function PublickeyVerifier(auth, appConfig, mailreader, keychain) {
    this._appConfig = appConfig;
    this._mailreader = mailreader;
    this._keychain = keychain;
    this._auth = auth;
    this._workerPath = appConfig.config.workerPath + '/tcp-socket-tls-worker.min.js';
    this._keyServerUrl = this._appConfig.config.keyServerUrl;
}

//
// Public API
//

PublickeyVerifier.prototype.configure = function() {
    var self = this;

    return self._auth.getCredentials().then(function(credentials) {
        // tls socket worker path for multithreaded tls in non-native tls environments
        credentials.imap.tlsWorkerPath = self._appConfig.config.workerPath + '/tcp-socket-tls-worker.min.js';
        self._imap = new ImapClient(credentials.imap);
    });
};

PublickeyVerifier.prototype.uploadPublicKey = function() {
    if (this.keypair) {
        return this._keychain.uploadPublicKey(this.keypair.publicKey);
    }
    return new Promise(function(resolve) {
        resolve();
    });
};

PublickeyVerifier.prototype.persistKeypair = function() {
    if (this.keypair) {
        return this._keychain.putUserKeyPair(this.keypair);
    }
    return new Promise(function(resolve) {
        resolve();
    });
};

PublickeyVerifier.prototype.verify = function() {
    var self = this,
        verificationSuccessful = false;

    // have to wrap it in a promise to catch .onError of imap-client
    return new Promise(function(resolve, reject) {
        self._imap.onError = reject;

        // login
        self._imap.login().then(function() {
            // list folders
            return self._imap.listWellKnownFolders();
        }).then(function(wellKnownFolders) {
            var paths = []; // gathers paths

            // extract the paths from the folder arrays
            for (var folderType in wellKnownFolders) {
                if (wellKnownFolders.hasOwnProperty(folderType) && Array.isArray(wellKnownFolders[folderType])) {
                    paths = paths.concat(_.pluck(wellKnownFolders[folderType], 'path'));
                }
            }
            return paths;

        }).then(function(paths) {
            return self._searchAll(paths); // search

        }).then(function(candidates) {
            if (!candidates.length) {
                // nothing here to potentially verify
                verificationSuccessful = false;
                return;
            }

            // verify everything that looks like a verification mail
            return self._verifyAll(candidates).then(function(success) {
                verificationSuccessful = success;
            });

        }).then(function() {
            // at this point, we don't care about errors anymore
            self._imap.onError = function() {};
            self._imap.logout();

            if (!verificationSuccessful) {
                // nothing unexpected went wrong, but no public key could be verified
                throw new Error();
            }

            resolve(); // we're done

        }).catch(reject);
    });
};

PublickeyVerifier.prototype._searchAll = function(paths) {
    var self = this,
        candidates = []; // gather matching uids

    // async for-loop inside a then-able
    return new Promise(next);

    // search each path for the relevant email
    function next(resolve) {
        if (!paths.length) {
            resolve(candidates);
            return;
        }

        var path = paths.shift();
        self._imap.search({
            path: path,
            header: ['Subject', self._appConfig.string.verificationSubject]
        }).then(function(uids) {
            uids.forEach(function(uid) {
                candidates.push({
                    path: path,
                    uid: uid
                });
            });
            next(resolve); // keep on searching
        }).catch(function() {
            next(resolve); // if there's an error, just search the next inbox
        });
    }
};

PublickeyVerifier.prototype._verifyAll = function(candidates) {
    var self = this;

    // async for-loop inside a then-able
    return new Promise(next);

    function next(resolve) {
        if (!candidates.length) {
            resolve(false);
            return;
        }

        var candidate = candidates.shift();
        self._verify(candidate.path, candidate.uid).then(function(success) {
            if (success) {
                resolve(success); // we're done here
            } else {
                next(resolve);
            }
        }).catch(function() {
            next(resolve); // ignore
        });
    }
};


PublickeyVerifier.prototype._verify = function(path, uid) {
    var self = this,
        message;

    // get the metadata for the message
    return self._imap.listMessages({
        path: path,
        firstUid: uid,
        lastUid: uid
    }).then(function(messages) {
        if (!messages.length) {
            // message has been deleted in the meantime
            throw new Error('Message has already been deleted');
        }

        // remember in scope
        message = messages[0];

    }).then(function() {
        // get the body for the message
        return self._imap.getBodyParts({
            path: path,
            uid: uid,
            bodyParts: message.bodyParts
        });

    }).then(function() {
        // parse the message
        return new Promise(function(resolve, reject) {
            self._mailreader.parse(message, function(err, root) {
                if (err) {
                    reject(err);
                } else {
                    resolve(root);
                }
            });
        });

    }).then(function(root) {
        // extract the nonce
        var body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'),
            verificationUrlPrefix = self._keyServerUrl + self._appConfig.config.verificationUrl,
            uuid = body.split(verificationUrlPrefix).pop().substr(0, self._appConfig.config.verificationUuidLength),
            uuidRegex = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/;

        // there's no valid uuid in the message, so forget about it
        if (!uuidRegex.test(uuid)) {
            throw new Error('No public key verifier found!');
        }

        // there's a valid uuid in the message, so try to verify it
        return self._keychain.verifyPublicKey(uuid).catch(function(err) {
            throw new Error('Verifying your public key failed: ' + err.message);
        });
        return self._imap.deleteMessage({
            path: path,
            uid: uid
        }).catch(function() {}); // ignore error here
    }).then(function() {
        return true;
    });
};

/**
 * Helper function that recursively traverses the body parts tree. Looks for bodyParts that match the provided type and aggregates them
 *
 * @param {Array} bodyParts The bodyParts array
 * @param {String} type The type to look up
 * @param {undefined} result Leave undefined, only used for recursion
 */
function filterBodyParts(bodyParts, type, result) {
    result = result || [];
    bodyParts.forEach(function(part) {
        if (part.type === type) {
            result.push(part);
        } else if (Array.isArray(part.content)) {
            filterBodyParts(part.content, type, result);
        }
    });
    return result;
}