'use strict';

const config = require('wild-config');
const imapTools = require('../../imap-core/lib/imap-tools');
const db = require('../db');
const tools = require('../tools');
const consts = require('../consts');
const base32 = require('base32.js');
const crypto = require('crypto');

// STORE / UID STORE, updates flags for selected UIDs
module.exports = server => (mailbox, update, session, callback) => {
    server.logger.debug(
        {
            tnx: 'store',
            cid: session.id
        },
        '[%s] Updating messages in "%s"',
        session.id,
        mailbox
    );
    db.database.collection('mailboxes').findOne(
        {
            _id: mailbox
        },
        {
            maxTimeMS: consts.DB_MAX_TIME_MAILBOXES
        },
        (err, mailboxData) => {
            if (err) {
                return callback(err);
            }

            if (!mailboxData) {
                return callback(null, 'NONEXISTENT');
            }

            let unseenChange = update.value.includes('\\Seen');

            let modified = [];

            let newModseq = false;
            let getModseq = next => {
                if (newModseq) {
                    return next(null, newModseq);
                }
                db.database.collection('mailboxes').findOneAndUpdate(
                    { _id: mailboxData._id },
                    {
                        $inc: {
                            modifyIndex: 1
                        }
                    },
                    {
                        returnOriginal: false,
                        maxTimeMS: consts.DB_MAX_TIME_MAILBOXES
                    },
                    (err, item) => {
                        if (err) {
                            return next(err);
                        }

                        newModseq = item && item.value && item.value.modifyIndex;

                        next(null, newModseq);
                    }
                );
            };

            let query = {
                mailbox
            };

            let queryAll = false;
            if (update.messages.length !== session.selected.uidList.length) {
                // do not use uid selector for 1:*
                query.uid = tools.checkRangeQuery(update.messages);
            } else {
                // 1:*
                queryAll = true;
            }

            let condstoreEnabled = !!session.selected.condstoreEnabled;

            let cursor = db.database
                .collection('messages')
                .find(query)
                .project({
                    _id: true,
                    uid: true,
                    flags: true,
                    modseq: true
                })
                .maxTimeMS(consts.DB_MAX_TIME_MESSAGES)
                .sort({ uid: 1 });

            let shouldExpunge = false;
            let updateEntries = [];
            let notifyEntries = [];

            let done = (...args) => {
                if (updateEntries.length) {
                    return db.database.collection('messages').bulkWrite(
                        updateEntries,
                        {
                            ordered: false,
                            writeConcern: 1
                        },
                        () => {
                            updateEntries = [];
                            server.notifier.addEntries(mailboxData, notifyEntries, () => {
                                notifyEntries = [];
                                server.notifier.fire(session.user.id);
                                if (args[0]) {
                                    // first argument is an error
                                    return callback(...args);
                                } else {
                                    if (config.imap.autoExpunge && shouldExpunge) {
                                        // shcedule EXPUNGE command for current folder
                                        let expungeOptions = {
                                            // create new temporary session so it would not mix with the active one
                                            id: 'auto.' + base32.encode(crypto.randomBytes(10)).toLowerCase(),
                                            user: {
                                                id: session.user.id,
                                                username: session.user.username
                                            }
                                        };
                                        setImmediate(() => {
                                            server.onExpunge(mailbox, { silent: true }, expungeOptions, () => false);
                                        });
                                    }
                                    updateMailboxFlags(mailboxData, update, () => callback(...args));
                                }
                            });
                        }
                    );
                }
                server.notifier.fire(session.user.id);
                if (args[0]) {
                    // first argument is an error
                    return callback(...args);
                } else {
                    updateMailboxFlags(mailboxData, update, () => callback(...args));
                }
            };

            // We have to process all messages one by one instead of just calling an update
            // for all messages as we need to know which messages were exactly modified,
            // otherwise we can't send flag update notifications and modify modseq values
            let processNext = () => {
                cursor.next((err, message) => {
                    if (err) {
                        return done(err);
                    }
                    if (!message) {
                        return cursor.close(() => done(null, true, modified));
                    }
                    if (queryAll && !session.selected.uidList.includes(message.uid)) {
                        // skip processing messages that we do not know about yet
                        return setImmediate(processNext);
                    }

                    if (update.unchangedSince && message.modseq > update.unchangedSince) {
                        modified.push(message.uid);
                        return setImmediate(processNext);
                    }

                    let flagsupdate = false; // query object for updates

                    let updated = false;
                    let existingFlags = message.flags.map(flag => flag.toLowerCase().trim());
                    switch (update.action) {
                        case 'set':
                            // check if update set matches current or is different
                            if (
                                // if length does not match
                                existingFlags.length !== update.value.length ||
                                // or a new flag was found
                                update.value.filter(flag => !existingFlags.includes(flag.toLowerCase().trim())).length
                            ) {
                                updated = true;
                            }

                            message.flags = [].concat(update.value);

                            // set flags
                            if (updated) {
                                flagsupdate = {
                                    $set: {
                                        flags: message.flags,
                                        unseen: !message.flags.includes('\\Seen'),
                                        flagged: message.flags.includes('\\Flagged'),
                                        undeleted: !message.flags.includes('\\Deleted'),
                                        draft: message.flags.includes('\\Draft')
                                    }
                                };

                                if (message.flags.includes('\\Deleted')) {
                                    shouldExpunge = true;
                                }

                                if (!message.flags.includes('\\Deleted')) {
                                    flagsupdate.$set.searchable = true;
                                } else {
                                    flagsupdate.$unset = {
                                        searchable: ''
                                    };
                                }
                            }
                            break;

                        case 'add': {
                            let newFlags = [];
                            message.flags = message.flags.concat(
                                update.value.filter(flag => {
                                    if (!existingFlags.includes(flag.toLowerCase().trim())) {
                                        updated = true;
                                        newFlags.push(flag);
                                        return true;
                                    }
                                    return false;
                                })
                            );

                            // add flags
                            if (updated) {
                                flagsupdate = {
                                    $addToSet: {
                                        flags: {
                                            $each: newFlags
                                        }
                                    }
                                };

                                if (
                                    newFlags.includes('\\Seen') ||
                                    newFlags.includes('\\Flagged') ||
                                    newFlags.includes('\\Deleted') ||
                                    newFlags.includes('\\Draft')
                                ) {
                                    flagsupdate.$set = {};
                                    if (newFlags.includes('\\Seen')) {
                                        flagsupdate.$set = {
                                            unseen: false
                                        };
                                    }
                                    if (newFlags.includes('\\Flagged')) {
                                        flagsupdate.$set = {
                                            flagged: true
                                        };
                                    }
                                    if (newFlags.includes('\\Deleted')) {
                                        shouldExpunge = true;
                                        flagsupdate.$set = {
                                            undeleted: false
                                        };
                                        flagsupdate.$unset = {
                                            searchable: ''
                                        };
                                    }
                                    if (newFlags.includes('\\Draft')) {
                                        flagsupdate.$set = {
                                            draft: true
                                        };
                                    }
                                }
                            }
                            break;
                        }

                        case 'remove': {
                            // We need to use the case of existing flags when removing
                            let oldFlags = [];
                            let flagsUpdates = update.value.map(flag => flag.toLowerCase().trim());
                            message.flags = message.flags.filter(flag => {
                                if (!flagsUpdates.includes(flag.toLowerCase().trim())) {
                                    return true;
                                }
                                oldFlags.push(flag);
                                updated = true;
                                return false;
                            });

                            // remove flags
                            if (updated) {
                                flagsupdate = {
                                    $pull: {
                                        flags: {
                                            $in: oldFlags
                                        }
                                    }
                                };
                                if (
                                    oldFlags.includes('\\Seen') ||
                                    oldFlags.includes('\\Flagged') ||
                                    oldFlags.includes('\\Deleted') ||
                                    oldFlags.includes('\\Draft')
                                ) {
                                    flagsupdate.$set = {};
                                    if (oldFlags.includes('\\Seen')) {
                                        flagsupdate.$set = {
                                            unseen: true
                                        };
                                    }
                                    if (oldFlags.includes('\\Flagged')) {
                                        flagsupdate.$set = {
                                            flagged: false
                                        };
                                    }
                                    if (oldFlags.includes('\\Deleted')) {
                                        flagsupdate.$set = {
                                            undeleted: true
                                        };
                                        if (!['\\Junk', '\\Trash'].includes(mailboxData.specialUse)) {
                                            flagsupdate.$set.searchable = true;
                                        }
                                    }
                                    if (oldFlags.includes('\\Draft')) {
                                        flagsupdate.$set = {
                                            draft: false
                                        };
                                    }
                                }
                            }
                            break;
                        }
                    }

                    if (updated) {
                        getModseq((err, modseq) => {
                            if (err) {
                                return cursor.close(() => done(err));
                            }

                            if (!update.silent || condstoreEnabled) {
                                // print updated state of the message
                                session.writeStream.write(
                                    session.formatResponse('FETCH', message.uid, {
                                        uid: update.isUid ? message.uid : false,
                                        flags: message.flags,
                                        modseq: condstoreEnabled ? modseq : false
                                    })
                                );
                            }

                            if (!flagsupdate.$set) {
                                flagsupdate.$set = {};
                            }
                            flagsupdate.$set.modseq = modseq;

                            updateEntries.push({
                                updateOne: {
                                    filter: {
                                        _id: message._id,
                                        // include shard key data as well
                                        mailbox: mailboxData._id,
                                        uid: message.uid,
                                        modseq: {
                                            $lt: modseq
                                        }
                                    },
                                    update: flagsupdate
                                }
                            });

                            notifyEntries.push({
                                command: 'FETCH',
                                ignore: session.id,
                                uid: message.uid,
                                flags: message.flags,
                                message: message._id,
                                modseq,
                                unseenChange
                            });

                            if (updateEntries.length >= consts.BULK_BATCH_SIZE) {
                                return db.database.collection('messages').bulkWrite(
                                    updateEntries,
                                    {
                                        ordered: false,
                                        writeConcern: 1
                                    },
                                    err => {
                                        updateEntries = [];
                                        if (err) {
                                            return cursor.close(() => done(err));
                                        }

                                        server.notifier.addEntries(mailboxData, notifyEntries, () => {
                                            notifyEntries = [];
                                            server.notifier.fire(session.user.id);
                                            return setImmediate(processNext);
                                        });
                                    }
                                );
                            } else {
                                return setImmediate(processNext);
                            }
                        });
                    } else {
                        return setImmediate(processNext);
                    }
                });
            };

            return setImmediate(processNext);
        }
    );
};

function updateMailboxFlags(mailbox, update, callback) {
    if (update.action === 'remove') {
        // we didn't add any new flags, so there's nothing to update
        return callback();
    }

    let mailboxFlags = imapTools.systemFlags.concat(mailbox.flags || []).map(flag => flag.trim().toLowerCase());
    let newFlags = [];

    // find flags that are not listed with mailbox
    update.value.forEach(flag => {
        // limit mailbox flags by 100
        if (mailboxFlags.length + newFlags.length >= 100) {
            return;
        }
        // if mailbox does not have such flag, then add it
        if (!mailboxFlags.includes(flag.toLowerCase().trim())) {
            newFlags.push(flag);
        }
    });

    // nothing new found
    if (!newFlags.length) {
        return callback();
    }

    // found some new flags not yet set for mailbox
    // FIXME: Should we send unsolicited FLAGS and PERMANENTFLAGS notifications? Probably not
    return db.database.collection('mailboxes').updateOne(
        {
            _id: mailbox._id
        },
        {
            $addToSet: {
                flags: {
                    $each: newFlags
                }
            }
        },
        {
            maxTimeMS: consts.DB_MAX_TIME_MAILBOXES
        },
        callback
    );
}
