'use strict';

const ObjectID = require('mongodb').ObjectID;
const ImapNotifier = require('./imap-notifier');
const { publish, MAILBOX_CREATED, MAILBOX_RENAMED, MAILBOX_DELETED } = require('./events');

class MailboxHandler {
    constructor(options) {
        this.database = options.database;
        this.users = options.users || options.database;
        this.redis = options.redis;

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

        this.notifier =
            options.notifier ||
            new ImapNotifier({
                database: options.database,
                redis: this.redis,
                pushOnly: true
            });
    }

    create(user, path, opts, callback) {
        this.database.collection('mailboxes').findOne(
            {
                user,
                path
            },
            (err, mailboxData) => {
                if (err) {
                    return callback(err);
                }
                if (mailboxData) {
                    return callback(null, 'ALREADYEXISTS');
                }

                this.users.collection('users').findOne(
                    {
                        _id: user
                    },
                    {
                        projection: {
                            retention: true
                        }
                    },
                    (err, userData) => {
                        if (err) {
                            return callback(err);
                        }

                        if (!userData) {
                            return callback(new Error('User not found'));
                        }

                        mailboxData = {
                            _id: new ObjectID(),
                            user,
                            path,
                            uidValidity: Math.floor(Date.now() / 1000),
                            uidNext: 1,
                            modifyIndex: 0,
                            subscribed: true,
                            flags: [],
                            retention: userData.retention
                        };

                        Object.keys(opts || {}).forEach(key => {
                            if (!['_id', 'user', 'path'].includes(key)) {
                                mailboxData[key] = opts[key];
                            }
                        });

                        this.database.collection('mailboxes').insertOne(mailboxData, { writeConcern: 'majority' }, (err, r) => {
                            if (err) {
                                if (err.code === 11000) {
                                    return callback(null, 'ALREADYEXISTS');
                                }
                                return callback(err);
                            }

                            publish(this.redis, {
                                ev: MAILBOX_CREATED,
                                user,
                                mailbox: r.insertedId,
                                path: mailboxData.path
                            }).catch(() => false);

                            return this.notifier.addEntries(
                                mailboxData,
                                {
                                    command: 'CREATE',
                                    mailbox: r.insertedId,
                                    path
                                },
                                () => {
                                    this.notifier.fire(user);
                                    return callback(null, true, mailboxData._id);
                                }
                            );
                        });
                    }
                );
            }
        );
    }

    rename(user, mailbox, newname, opts, callback) {
        this.database.collection('mailboxes').findOne(
            {
                _id: mailbox,
                user
            },
            (err, mailboxData) => {
                if (err) {
                    return callback(err);
                }
                if (!mailboxData) {
                    return callback(null, 'NONEXISTENT');
                }
                if (mailboxData.path === 'INBOX' || mailboxData.hidden) {
                    return callback(null, 'CANNOT');
                }
                this.database.collection('mailboxes').findOne(
                    {
                        user: mailboxData.user,
                        path: newname
                    },
                    (err, existing) => {
                        if (err) {
                            return callback(err);
                        }
                        if (existing) {
                            return callback(null, 'ALREADYEXISTS');
                        }

                        let $set = { path: newname };

                        Object.keys(opts || {}).forEach(key => {
                            if (!['_id', 'user', 'path'].includes(key)) {
                                $set[key] = opts[key];
                            }
                        });

                        this.database.collection('mailboxes').findOneAndUpdate(
                            {
                                _id: mailbox
                            },
                            {
                                $set
                            },
                            {},
                            (err, item) => {
                                if (err) {
                                    return callback(err);
                                }

                                if (!item || !item.value) {
                                    // was not able to acquire a lock
                                    return callback(null, 'NONEXISTENT');
                                }

                                publish(this.redis, {
                                    ev: MAILBOX_RENAMED,
                                    user,
                                    mailbox,
                                    previous: mailboxData.path,
                                    current: newname
                                }).catch(() => false);

                                this.notifier.addEntries(
                                    mailboxData,
                                    {
                                        command: 'RENAME',
                                        path: newname
                                    },
                                    () => {
                                        this.notifier.fire(mailboxData.user);
                                        return callback(null, true, mailbox);
                                    }
                                );
                            }
                        );
                    }
                );
            }
        );
    }

    /**
     * Deletes a mailbox. Does not immediatelly release quota as the messages get deleted after a while
     */
    del(user, mailbox, callback) {
        this.database.collection('mailboxes').findOne(
            {
                _id: mailbox,
                user
            },
            (err, mailboxData) => {
                if (err) {
                    return callback(err);
                }
                if (!mailboxData) {
                    return callback(null, 'NONEXISTENT');
                }
                if (mailboxData.specialUse || mailboxData.path === 'INBOX' || mailboxData.hidden) {
                    return callback(null, 'CANNOT');
                }

                this.database.collection('mailboxes').deleteOne(
                    {
                        _id: mailbox
                    },
                    { writeConcern: 'majority' },
                    (err, r) => {
                        if (err) {
                            return callback(err);
                        }

                        if (r.deletedCount) {
                            publish(this.redis, {
                                ev: MAILBOX_DELETED,
                                user,
                                mailbox,
                                path: mailboxData.path
                            }).catch(() => false);
                        }

                        let deleteFilters = async () => {
                            try {
                                let filters = await this.database
                                    .collection('filters')
                                    .find({
                                        user,
                                        'action.mailbox': mailbox
                                    })
                                    .toArray();
                                if (!filters) {
                                    return;
                                }
                                for (let filterData of filters) {
                                    // delete one by one for logging
                                    try {
                                        let r = await this.database.collection('filters').deleteOne({
                                            _id: filterData._id
                                        });
                                        if (r && r.deletedCount) {
                                            await publish(this.redis, {
                                                ev: `filter.deleted`,
                                                user,
                                                filter: filterData._id
                                            });
                                        }
                                    } catch (err) {
                                        this.loggelf({
                                            user,
                                            mailbox,
                                            action: 'delete_filter',
                                            filter: filterData._id,
                                            error: err.message
                                        });
                                    }
                                }
                            } catch (err) {
                                this.loggelf({
                                    user,
                                    mailbox,
                                    action: 'delete_filter',
                                    error: err.message
                                });
                            }
                        };

                        deleteFilters()
                            .then(() => {
                                // send information about deleted mailbox straight to connected clients
                                this.notifier.fire(mailboxData.user, {
                                    command: 'DROP',
                                    mailbox
                                });

                                this.notifier.addEntries(
                                    mailboxData,
                                    {
                                        command: 'DELETE',
                                        mailbox
                                    },
                                    () => {
                                        this.database.collection('messages').updateMany(
                                            {
                                                mailbox
                                            },
                                            {
                                                $set: {
                                                    exp: true,
                                                    // make sure the messages are in top of the expire queue
                                                    rdate: Date.now() - 24 * 3600 * 1000
                                                }
                                            },
                                            {
                                                multi: true,
                                                writeConcern: 1
                                            },
                                            err => {
                                                if (err) {
                                                    return callback(err);
                                                }

                                                let done = () => {
                                                    this.notifier.fire(mailboxData.user);
                                                    callback(null, true, mailbox);
                                                };

                                                return done();
                                            }
                                        );
                                    }
                                );
                            })
                            .catch(() => false /* should not happen */);
                    }
                );
            }
        );
    }

    update(user, mailbox, updates, callback) {
        if (!updates) {
            return callback(null, false);
        }

        this.database.collection('mailboxes').findOne(
            {
                _id: mailbox
            },
            (err, mailboxData) => {
                if (err) {
                    return callback(err);
                }
                if (!mailboxData) {
                    return callback(null, 'NONEXISTENT');
                }

                if (updates.path && updates.path !== mailboxData.path) {
                    return this.rename(user, mailbox, updates.path, updates, callback);
                }

                let $set = {};
                let hasChanges = false;

                Object.keys(updates || {}).forEach(key => {
                    if (!['_id', 'user', 'path'].includes(key)) {
                        $set[key] = updates[key];
                        hasChanges = true;
                    }
                });

                if (!hasChanges) {
                    return callback(null, true);
                }

                this.database.collection('mailboxes').findOneAndUpdate(
                    {
                        _id: mailbox
                    },
                    {
                        $set
                    },
                    {},
                    (err, item) => {
                        if (err) {
                            return callback(err);
                        }

                        if (!item || !item.value) {
                            return callback(null, 'NONEXISTENT');
                        }

                        return callback(null, true);
                    }
                );
            }
        );
    }
}

module.exports = MailboxHandler;
