'use strict';

const db = require('../db');
const tools = require('../tools');
const consts = require('../consts');

/**
 * Returns an array of matching UID values
 */
module.exports = server => (mailbox, options, session, callback) => {
    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');
            }

            // prepare query

            let query = {
                mailbox: mailboxData._id
            };

            let returned = false;
            let walkQuery = (parent, ne, node) => {
                if (returned) {
                    return;
                }
                node.forEach(term => {
                    switch (term.key) {
                        case 'all':
                            if (ne) {
                                parent.push({
                                    // should not match anything
                                    _id: -1
                                });
                            }
                            break;

                        case 'not':
                            walkQuery(parent, !ne, [].concat(term.value || []));
                            break;

                        case 'or': {
                            let $or = [];

                            [].concat(term.value || []).forEach(entry => {
                                walkQuery($or, false, [].concat(entry || []));
                            });

                            if ($or.length) {
                                parent.push({
                                    $or
                                });
                            }

                            break;
                        }

                        case 'text': // search over entire email
                        case 'body': // search over email body
                            if (term.value && !ne) {
                                // fulltext can only be in the root of the query, not in $not, $or expressions
                                // https://docs.mongodb.com/v3.4/tutorial/text-search-in-aggregation/#restrictions
                                query.user = session.user.id;
                                query.searchable = true;
                                query.$text = {
                                    $search: term.value
                                };
                            } else {
                                // can not search by text
                                parent.push({
                                    // should not match anything
                                    _id: -1
                                });
                            }
                            break;

                        case 'modseq':
                            parent.push({
                                modseq: {
                                    [!ne ? '$gte' : '$lt']: term.value
                                }
                            });
                            break;

                        case 'uid':
                            if (Array.isArray(term.value)) {
                                if (!term.value.length) {
                                    // trying to find a message that does not exist
                                    returned = true;
                                    return callback(null, {
                                        uidList: [],
                                        highestModseq: 0
                                    });
                                }
                                if (term.value.length !== session.selected.uidList.length) {
                                    // not 1:*
                                    parent.push({
                                        uid: tools.checkRangeQuery(term.value, ne)
                                    });
                                } else if (ne) {
                                    parent.push({
                                        // should not match anything
                                        _id: -1
                                    });
                                }
                            } else {
                                parent.push({
                                    uid: {
                                        [!ne ? '$eq' : '$ne']: term.value
                                    }
                                });
                            }
                            break;

                        case 'flag':
                            {
                                switch (term.value) {
                                    case '\\Seen':
                                    case '\\Deleted':
                                        // message object has "unseen" and "undeleted" properties
                                        if (term.exists) {
                                            parent.push({
                                                ['un' + term.value.toLowerCase().substr(1)]: ne
                                            });
                                        } else {
                                            parent.push({
                                                ['un' + term.value.toLowerCase().substr(1)]: !ne
                                            });
                                        }
                                        break;
                                    case '\\Flagged':
                                    case '\\Draft':
                                        if (term.exists) {
                                            parent.push({
                                                [term.value.toLowerCase().substr(1)]: !ne
                                            });
                                        } else {
                                            parent.push({
                                                [term.value.toLowerCase().substr(1)]: ne
                                            });
                                        }
                                        break;
                                    default:
                                        if (term.exists) {
                                            parent.push({
                                                flags: {
                                                    [!ne ? '$eq' : '$ne']: term.value
                                                }
                                            });
                                        } else {
                                            parent.push({
                                                flags: {
                                                    [!ne ? '$ne' : '$eq']: term.value
                                                }
                                            });
                                        }
                                }
                            }
                            break;

                        case 'header':
                            {
                                let regex = Buffer.from(term.value, 'binary')
                                    .toString()
                                    .replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
                                let entry = term.value
                                    ? {
                                          headers: {
                                              $elemMatch: {
                                                  key: term.header,
                                                  value: !ne
                                                      ? {
                                                            $regex: regex,
                                                            $options: 'i'
                                                        }
                                                      : {
                                                            // not can not have a regex, so try exact match instead even if it fails
                                                            $not: {
                                                                $eq: Buffer.from(term.value, 'binary').toString().toLowerCase().trim()
                                                            }
                                                        }
                                              }
                                          }
                                      }
                                    : {
                                          'headers.key': !ne
                                              ? term.header
                                              : {
                                                    $ne: term.header
                                                }
                                      };
                                parent.push(entry);
                            }
                            break;

                        case 'internaldate':
                            {
                                let op = false;
                                let value = new Date(term.value + ' GMT');
                                switch (term.operator) {
                                    case '<':
                                        op = '$lt';
                                        break;
                                    case '<=':
                                        op = '$lte';
                                        break;
                                    case '>':
                                        op = '$gt';
                                        break;
                                    case '>=':
                                        op = '$gte';
                                        break;
                                }
                                let entry = !op
                                    ? [
                                          {
                                              $gte: value
                                          },
                                          {
                                              $lt: new Date(value.getTime() + 24 * 3600 * 1000)
                                          }
                                      ]
                                    : {
                                          [op]: value
                                      };

                                entry = {
                                    idate: !ne
                                        ? entry
                                        : {
                                              $not: entry
                                          }
                                };

                                parent.push(entry);
                            }
                            break;

                        case 'headerdate':
                            {
                                let op = false;
                                let value = new Date(term.value + ' GMT');
                                switch (term.operator) {
                                    case '<':
                                        op = '$lt';
                                        break;
                                    case '<=':
                                        op = '$lte';
                                        break;
                                    case '>':
                                        op = '$gt';
                                        break;
                                    case '>=':
                                        op = '$gte';
                                        break;
                                }
                                let entry = !op
                                    ? [
                                          {
                                              $gte: value
                                          },
                                          {
                                              $lt: new Date(value.getTime() + 24 * 3600 * 1000)
                                          }
                                      ]
                                    : {
                                          [op]: value
                                      };

                                entry = {
                                    hdate: !ne
                                        ? entry
                                        : {
                                              $not: entry
                                          }
                                };

                                parent.push(entry);
                            }
                            break;

                        case 'size':
                            {
                                let op = '$eq';
                                let value = Number(term.value) || 0;
                                switch (term.operator) {
                                    case '<':
                                        op = '$lt';
                                        break;
                                    case '<=':
                                        op = '$lte';
                                        break;
                                    case '>':
                                        op = '$gt';
                                        break;
                                    case '>=':
                                        op = '$gte';
                                        break;
                                }

                                let entry = {
                                    [op]: value
                                };

                                entry = {
                                    size: !ne
                                        ? entry
                                        : {
                                              $not: entry
                                          }
                                };

                                parent.push(entry);
                            }
                            break;
                    }
                });
            };

            let $and = [];
            walkQuery($and, false, options.query);
            if (returned) {
                return;
            }

            if ($and.length) {
                query.$and = $and;
            }

            server.logger.info(
                {
                    tnx: 'search',
                    cid: session.id
                },
                '[%s] SEARCH %s',
                session.id,
                JSON.stringify(query)
            );

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

            let highestModseq = 0;
            let uidList = [];

            let processNext = () => {
                cursor.next((err, message) => {
                    if (err) {
                        server.logger.error(
                            {
                                tnx: 'search',
                                cid: session.id
                            },
                            '[%s] SEARCHFAIL %s error="%s"',
                            session.id,
                            JSON.stringify(query),
                            err.message
                        );
                        return callback(new Error('Can not make requested search query'));
                    }
                    if (!message) {
                        return cursor.close(() =>
                            callback(null, {
                                uidList,
                                highestModseq
                            })
                        );
                    }

                    if (highestModseq < message.modseq) {
                        highestModseq = message.modseq;
                    }

                    uidList.push(message.uid);
                    setImmediate(processNext);
                });
            };

            setImmediate(processNext);
        }
    );
};
