Source: moment.js

/**
 * @fileOverview Moment API Controller
 * @class moment
 * @author Calvin Ho
 */
const User = require('../models/user');
const Moment = require('../models/moment');
const Response = require('../models/response');
const SystemlogController = require('../controllers/systemlog');
const ScheduleController = require('../controllers/schedule');
const ResourceController = require('../controllers/resource');
const async = require('async');
const randtoken = require('rand-token');
const pushSettings = require('../../config/push-notifications');
const PushNotifications = require('@calvinckho/node-pushnotifications');
const push = new PushNotifications(pushSettings);
const Conversation = require('../models/conversation');
const UserData = require('../controllers/user-data');
const Calendar = require('../models/calendar');
const GeoSpatial = require('../models/geospatial');
const mongoose = require('mongoose');

/**
 * @desc Load all notes of a given Relationship
 * @memberof moment
 * @param req
 * @param res
 * @param next
 * @param req.query.relationship: notes in this relationship (type: ObjectID) will be loaded
 * @returns {Array} an array of Notes object
 *  updated by Calvin Ho 2/14/2020
 */
exports.loadNotes = async (req, res, next) => {
    try {
        let hasSuperAdminPermission;
        if (req.query.relationship) {
            if (await ResourceController.checkMomentPermission(req.query.relationship, 'user_list_2', null, null, req.user, null)) {
                hasSuperAdminPermission = true;
            }
        }
        query = req.query.relationship && req.query.relationship.length ? [
            {
                $match: {
                    _id: mongoose.Types.ObjectId(req.query.relationship)
                }
            },
        ] : [];
        results = await Moment.aggregate(
            query.concat([
                {
                    $match: { // filter relationship that the user is a part of
                        $expr: {
                            $or: [
                                { $in: [ req.user._id, { $ifNull: ["$user_list_1", []] }] },
                                { $in: [ req.user._id, { $ifNull: ["$user_list_2", []] }] },
                                { $in: [ req.user._id, { $ifNull: ["$user_list_3", []] }] },
                                hasSuperAdminPermission
                            ]
                        }
                    }
                },
                {
                    $match: { // filter out deleted Activities
                        $expr: {
                            $cond: [
                                {$ifNull: ['$deletedAt', false]}, // if
                                false, // then
                                true // else
                            ]
                        }
                    }
                },
                {
                    $lookup: { // load responses in the context of a relationship
                        from: "responses",
                        localField: "_id",
                        foreignField: "relationship",
                        as: "response"
                    }
                },
                {
                    $unwind: "$response"
                },
                {
                    $match: { // only choose responses to public and privateNotes, i.e. moment: ObjectId('5e4b39cd76ffc96ae6aba323')
                        $expr: {
                            $in: [ '$response.moment', [mongoose.Types.ObjectId('5e4b39cd76ffc96ae6aba323'), mongoose.Types.ObjectId('5e4c28d694fd1963ac0a5b1e') ]]
                        }
                    }
                },
                {
                    $lookup: { // load content calendars
                        from: "calendars",
                        localField: "response.calendar",
                        foreignField: "_id",
                        as: "content_calendar"
                    }
                },
                {
                    $unwind: {
                        path: "$content_calendar",
                        // for Note whose content calendar has been deleted, do not return Note (it has been deleted)
                        preserveNullAndEmptyArrays: false
                    },
                },
                {
                    $addFields: {
                        answer_array: "$response.matrix_string",
                        responseUpdatedAt: "$response.updatedAt"
                    }
                },
                {
                    $sort: { // sort the responses from the latest to the oldest. This is needed for the later group stage when only the latest doc is kept
                        responseUpdatedAt: -1
                    }
                },
                {
                    $unwind: "$answer_array" // unwind the responses text answers settings matrix
                },
                {
                    $addFields: {
                        question_id: {
                            $arrayElemAt: [ "$answer_array", 0 ]
                        }
                    }
                },
                {
                    $match: { // only question ID that is a valid 16-digit int will pass
                        $expr: {
                            $gt: [ { $convert: { input: "$question_id", to: "double", onError: 0, onNull: 0 } }, 0 ]
                        }
                    }
                },
                {
                    $addFields: {
                        question_id: {
                            $toDouble: "$question_id"
                        }
                    }
                },
                {
                    $lookup: { // check if response should be returned (i.e. if the question is collaborative)
                        from: "moments",
                        let: { contentId: "$content_calendar.moment", question_id: "$question_id", authorId: "$response.user" },
                        pipeline: [
                            {
                                $match: { // filter the corresponding relationship
                                    $expr: {
                                        $eq: [ "$_id", "$$contentId" ]
                                    }
                                }
                            },
                            {
                                $lookup: { // load content's resource
                                    from: "resources",
                                    localField: "resource",
                                    foreignField: "_id",
                                    as: "resource"
                                }
                            },
                            {
                                $unwind: "$resource"
                            },
                            {
                                $match: {
                                    $expr: {
                                        $and: [
                                            { // if question id's corresponds to an text answer component (40010)
                                                $eq: [ {$arrayElemAt: [ { $arrayElemAt: [ "$resource.matrix_number", 0 ]}, {$indexOfArray: [ {$arrayElemAt: [ "$resource.matrix_number", 2 ]}, "$$question_id" ]} ]}, 40010 ]
                                            },
                                            {
                                                $switch: { // check the user's permission to access the response
                                                    branches: [
                                                        {
                                                            case: { // if question setting is private
                                                                $eq: [{$arrayElemAt: [{$arrayElemAt: [ "$matrix_number", {$indexOfArray: [ {$arrayElemAt: [ "$resource.matrix_number", 2 ]}, "$$question_id" ]} ]}, 1]}, 0]
                                                            },
                                                            then: { // the user is the author
                                                                $eq: [ "$$authorId", req.user._id ]
                                                            },
                                                        },
                                                        {
                                                            case: { // if question setting is collaborative
                                                                $eq: [{$arrayElemAt: [{$arrayElemAt: [ "$matrix_number", {$indexOfArray: [ {$arrayElemAt: [ "$resource.matrix_number", 2 ]}, "$$question_id" ]} ]}, 1]}, 1]
                                                            },
                                                            then: true // for collaborative answers, return it
                                                        }
                                                    ],
                                                    default: { // default is private, i.e. check user is the author
                                                        $eq: [ "$$authorId", req.user._id ]
                                                    }
                                                }
                                            }
                                        ]
                                    }
                                }
                            },
                            {
                                $addFields: {
                                    title: {
                                        $arrayElemAt: [ { $arrayElemAt: [ "$matrix_string", 0 ]}, 0 ]
                                    },
                                    question: {
                                        $arrayElemAt: [ { $arrayElemAt: [ "$matrix_string", {$indexOfArray: [ {$arrayElemAt: [ "$resource.matrix_number", 2 ]}, "$$question_id" ]} ]}, 0 ]
                                    },
                                }
                            },
                        ],
                        as: "question_settings"
                    }
                },
                {
                    $unwind: {
                        path: "$question_settings",
                        // DO not preserve empty array! because of privacy settings, private answer is supposed to be weeded out here
                        preserveNullAndEmptyArrays: false
                    }
                },
                {
                    $lookup: { // load author
                        from: "users",
                        localField: "response.user",
                        foreignField: "_id",
                        as: "response_author"
                    }
                },
                {
                    $unwind: {
                        path: "$response_author",
                        //preserveNullAndEmptyArrays: true
                    },
                },
                {
                    $addFields: {
                        doc: {
                            group_id: { // calendar Id is used. If null, use response id
                                $cond: {
                                    if: {
                                        $ifNull: [ "$content_calendar", false]
                                    },
                                    then: "$content_calendar._id",
                                    else: "$response._id"
                                }
                            },
                            content_id: "$question_settings._id",
                            calendar_id: { // real calendar Id exists without response Id
                                $cond: {
                                    if: {
                                        $ifNull: [ "$content_calendar", false ]
                                    },
                                    then: "$content_calendar._id",
                                    else: null
                                }
                            },
                            response_id: "$response._id",
                            question_id: "$question_id",
                            calendar_title: {
                                $cond: {
                                    if: {
                                        $ifNull: [ "$content_calendar", false]
                                    },
                                    then: "$content_calendar.title",
                                    else: null
                                }
                            },
                            content_title: "$question_settings.title",
                            question: "$question_settings.question",
                            answer: "$answer_array",
                            updatedAt: "$responseUpdatedAt"
                        },
                        title: {
                            $arrayElemAt: [ { $arrayElemAt: [ "$matrix_string", 0 ]}, 0 ]
                        },
                        author: {
                            _id: "$response_author._id",
                            first_name: "$response_author.first_name",
                            last_name: "$response_author.last_name",
                            avatar: "$response_author.avatar"
                        }
                    }
                },
                {
                    $group: {
                        _id: "$doc.group_id", // group it by group id (calendar Id or response Id)
                        relationship: {
                            $first: "$_id"
                        },
                        doc: {
                            $first: "$doc"
                        },
                        authors: {
                            $addToSet: "$author"
                        },
                        title: {
                            $first: "$title"
                        }
                    }
                },
                {
                    $addFields: {
                        "doc.authors": "$authors"
                    }
                },
                {
                    $group: {
                        _id: "$relationship", // group it by relationship Id
                        title: {
                            $first: "$title"
                        },
                        docs: {
                            $addToSet: "$doc"
                        }
                    }
                }
            ])
        );//.allowDiskUse(true);
        res.send(results);
    } catch (err) {
        next({topic: 'System Error', title: 'loadNotes', err: err});
    }
};

/**
 * @desc load all the onboarding answers of a given user
 * @memberof moment
 * @param user: the user (type: ObjectID)
 * @param showOnlyPublicProfile: whether to show only public profile or full private profile (type: Boolean)
 * @returns {Array} an array of Notes object
 *  updated by Calvin Ho 4/21/2020
 */
exports.loadUserOnboardingAnswers = async (user, showOnlyPublicProfile) => {
    try {
        results = await Moment.aggregate([
            {
                $match: {
                    $expr: {
                        $or: [
                            { $in: [ mongoose.Types.ObjectId(user), { $ifNull: ["$user_list_1", []] }] },
                            { $in: [ mongoose.Types.ObjectId(user), { $ifNull: ["$user_list_2", []] }] },
                            { $in: [ mongoose.Types.ObjectId(user), { $ifNull: ["$user_list_3", []] }] }
                        ]
                    }
                }
            },
            {
                $match: { // if show only public profile, only show Activity with array_boolean.0 === true
                    $expr: {
                        $cond: {
                            if: {
                                $eq: [ showOnlyPublicProfile, true ]
                            },
                            then: {
                                $arrayElemAt: [ { $ifNull: ["$array_boolean", [ false ]] }, 0 ]
                            },
                            else: true
                        }
                    }
                }
            },
            {
                $match: { // if show only public profile, only show Activity with array_boolean.0 === true
                    $expr: {
                        $cond: [ // filter out deleted Activities
                            {$ifNull: ['$deletedAt', false]}, // if
                            false, // then
                            true // else
                        ]
                    }
                }
            },
            {
                $lookup: { // load program's resource
                    from: "resources",
                    localField: "resource",
                    foreignField: "_id",
                    as: "resource"
                }
            },
            {
                $unwind: "$resource"
            },
            {
                $lookup: { // load onboarding process' resource
                    from: "moments",
                    let: { programId: "$_id"},
                    pipeline: [
                        {
                            $match: {
                                $expr: {
                                    $eq: [ "$program", "$$programId" ]
                                }
                            }
                        },
                        {
                            $lookup: { // load program's resource
                                from: "resources",
                                localField: "resource",
                                foreignField: "_id",
                                as: "resource"
                            }
                        },
                        {
                            $unwind: "$resource"
                        },
                        /*{
                            $unwind: "$resource.matrix_number"
                        },*/
                    ],
                    as: "onboarding_process"
                }
            },
            {
                $unwind: "$onboarding_process"
            },
            {
                $lookup: { // look up responses based on onboarding processes
                    from: "responses",
                    let: { onboarding_process: "$onboarding_process" },
                    pipeline: [
                        {
                            $match: { // filter the current Program's matched user's text answers
                                $expr: {
                                    $and: [
                                        {
                                            $eq: ["$user", mongoose.Types.ObjectId(user) ]
                                        },
                                        {
                                            $eq: ["$moment", "$$onboarding_process._id" ]
                                        },
                                    ]
                                }
                            }
                        },
                        {
                            $unwind: {
                                path: "$matrix_number",
                                preserveNullAndEmptyArrays: true
                            }
                        },
                        {
                            $unwind: {
                                path: "$matrix_string",
                                preserveNullAndEmptyArrays: true
                            }
                            // only text answers are kept. m.c. and tile choice are excluded since they don't have their matrix_string is []
                        },
                        {
                            $addFields: {
                                multiple_choice: {
                                    $cond: {
                                        if: {
                                            $ifNull: [ "$matrix_number", false ]
                                        },
                                        then: {
                                            $cond: {
                                                if: {
                                                    $gt: [ { $size: "$matrix_number" }, 5 ]
                                                },
                                                then: {
                                                    $slice: [ "$matrix_number", 5, { $subtract: [ { $size: "$matrix_number" }, 5 ] } ]
                                                },
                                                else: []
                                            }
                                        },
                                        else: []
                                    }
                                }
                            }
                        },
                        {
                            $unwind: {
                                path: "$multiple_choice",
                                preserveNullAndEmptyArrays: true
                            }
                        },
                        {
                            $project: {
                                _id: 1,
                                question_id: {
                                    $switch: {
                                        branches: [
                                            {
                                                case: { // text answer
                                                    $ifNull: [ "$matrix_string", false ]
                                                },
                                                then: { // question id
                                                    $toLong: { $arrayElemAt: [ "$matrix_string", 0 ] } // using matrix_string[0] since it is valid for both m.c. and tile choice.
                                                },
                                            },
                                            {
                                                case: { // multiple choice and tile choices
                                                    $ifNull: [ "$matrix_number", false ]
                                                },
                                                then: { // question id
                                                    $arrayElemAt: [ "$matrix_number", 0 ]
                                                }
                                            }
                                        ],
                                        default: null
                                    }
                                },
                                user_answer: {
                                    $switch: {
                                        branches: [
                                            {
                                                case: { // text answer type
                                                    $ifNull: [ "$matrix_string", false ]
                                                },
                                                then: { // textanswer
                                                    $arrayElemAt: [ "$matrix_string", 1 ]
                                                    //$slice: [ "$matrix_string", 1, { $subtract: [ { $size: "$matrix_string" }, 1 ] } ]
                                                },
                                            },
                                            {
                                                case: { // multiple choice and tile choices
                                                    $ifNull: [ "$matrix_number", false ]
                                                },
                                                then: {
                                                    $switch: {
                                                        branches: [
                                                            {
                                                                case: {
                                                                    $and: [
                                                                        { // tile choices. the user selection is stored as a option timestamp, hence a 15 digit number
                                                                            $gte: [ "$multiple_choice", 100000 ]
                                                                        },
                                                                        { // componentIndex needs to be greater than -1
                                                                            $gt: [{$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]}, -1]
                                                                        },
                                                                        { // option index needs to be greater than -1
                                                                            $gt: [{$indexOfArray: [{$arrayElemAt: [ "$$onboarding_process.matrix_number", {$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]} ]}, "$multiple_choice"]}, -1]
                                                                        }
                                                                    ]

                                                                },
                                                                then: { // get user answer by reading the label from onboarding_process.matrix_string[componentIndex][optionIndex]
                                                                    $arrayElemAt: [ {$arrayElemAt: [ "$$onboarding_process.matrix_string",
                                                                            // componentIndex
                                                                            { $indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]} ]},
                                                                        // option index
                                                                        { $subtract: [{$indexOfArray: [{$arrayElemAt: [ "$$onboarding_process.matrix_number", {$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]} ]}, "$multiple_choice"]}, 5]}
                                                                    ]
                                                                },
                                                            },
                                                            {
                                                                case: {
                                                                    $and: [
                                                                        { // multiple choice
                                                                            $lt: [ "$multiple_choice", 100000 ]
                                                                        },
                                                                        { // componentIndex needs to be greater than -1
                                                                            $gt: [{$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]}, -1]
                                                                        },
                                                                        { // size of the array needs to be greater than the multiple_choice, which is a position index
                                                                            $gt: [{$size:{$arrayElemAt: [ "$$onboarding_process.matrix_string", {$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]} ]}}, "$multiple_choice"]
                                                                        }
                                                                    ]
                                                                },
                                                                then: { // get user answer by reading the label from onboarding_process.matrix_string[componentIndex][multiple_choice]
                                                                    $arrayElemAt: [ {$arrayElemAt: [ "$$onboarding_process.matrix_string", {$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]} ]}, "$multiple_choice" ]
                                                                },
                                                            }
                                                        ],
                                                        default: []
                                                    }
                                                }
                                            }
                                        ],
                                        default: []
                                    }
                                },
                                question: {
                                    $switch: {
                                        branches: [
                                            {
                                                case: {
                                                    $and: [
                                                        { // text answer
                                                            $ifNull: [ "$matrix_string", false ]
                                                        },
                                                        { // componentIndex needs to be greater than -1
                                                            $gt: [{$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$toLong: { $arrayElemAt: [ "$matrix_string", 0 ] }}]}, -1]
                                                        }
                                                    ]
                                                },
                                                then: {
                                                    $arrayElemAt: [ {$arrayElemAt: [ "$$onboarding_process.matrix_string", {$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$toLong: { $arrayElemAt: [ "$matrix_string", 0 ] }}]} ]}, 0 ]
                                                },
                                            },
                                            {
                                                case: {
                                                    $and: [
                                                        { // multiple choice and tile choices
                                                            $ifNull: [ "$matrix_number", false ]
                                                        },
                                                        { // componentIndex needs to be greater than -1
                                                            $gt: [{$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]}, -1]
                                                        }
                                                    ]
                                                },
                                                then: {
                                                    $arrayElemAt: [ {$arrayElemAt: [ "$$onboarding_process.resource.en-US.matrix_string", {$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]} ]}, 1 ]
                                                },
                                            }
                                        ],
                                        default: null
                                    }
                                }
                            }
                        },
                        {
                            $addFields: {
                                participation_type: {
                                    $slice: [ "$$onboarding_process.array_boolean", 2, 4]
                                }
                            }
                        },
                        {
                            $group: {
                                _id: "$question_id",
                                response_id: {
                                    $first: "$_id"
                                },
                                onboarding_id: {
                                    $first: "$$onboarding_process._id"
                                },
                                participation_type: {
                                    $first: "$participation_type"
                                },
                                question: {
                                    $first: "$question"
                                },
                                user_answer: {
                                    $addToSet: "$user_answer"
                                }
                            }
                        },
                    ],
                    as: "user_data"
                }
            },
            {
                $unwind: "$user_data"
            },
            {
                $group: {
                    _id: "$_id",
                    resource: {
                        $first: "$resource"
                    },
                    matrix_string: {
                        $first: "$matrix_string"
                    },
                    matrix_number: {
                        $first: "$matrix_number"
                    },
                    assets: {
                        $first: "$assets"
                    },
                    user_data: {
                        $addToSet: "$user_data"
                    }
                }
            },
        ]);
        //console.log("res", results)
        programs = [];
        results.forEach((result) => {
            temp_program = {   _id: result._id,
                name: result.matrix_string[0][0],
                assets: result.assets,
                participant: [],
                organizer: [],
                leader: []
            };
            componentIndex = result.resource.matrix_number[0].indexOf(10500);
            if (componentIndex > -1) {
                joinedandResponded = false;
                for (i = 0; i < result.matrix_number[componentIndex].length - 11; i++) {
                    result.user_data.forEach((question_data) => {
                        if (result.matrix_number[componentIndex][i + 11] === question_data._id) {
                            if (i % 3 === 0 && question_data.participation_type[0]) {
                                temp_program.participant.push(question_data);
                            } else if (i % 3 === 1 && question_data.participation_type[1]) {
                                temp_program.organizer.push(question_data);
                            } else if (i % 3 === 2 && question_data.participation_type[2]) {
                                question_data.role = result.matrix_string[componentIndex][0];
                                temp_program.leader.push(question_data);
                                joinedandResponded = true;
                            }
                        }
                    })
                }
            }
            if (joinedandResponded || showOnlyPublicProfile) {
                programs.push(temp_program);
            }
        });
        return programs;
    } catch (err) {
        SystemlogController.logFunctionError({topic: 'System Error', title: 'loadUserOnboardingAnswers', err: err});
    }
};

/**
 * @desc load all Activities shared in the marketplace
 * @memberof moment
 * @param req
 * @param res
 * @param next
 * @params req.query.category: requested category (type ObjectId)
 * @param req.query.version: API version
 * @returns {Array} an array of marketplace activities
 * req.body.categories is an array of categories the user is requesting. Typical use case in Picker expects an array of 1 category
 * created by Calvin Ho 1/9/20
 */
exports.loadSampleActivities = async (req, res, next) => {
    try {
        if (req.query.version === '1') { // version 1 for 1.7.4+
            let requested_categories;
            const userId = req.user ? req.user._id : 'unauthenticated';
            // all types allowed to be returned: Community, Program, Journey, Relationship, Mentoring, Group, Content, Onboarding Process
            //const all_child_categories = [mongoose.Types.ObjectId('5c915324e172e4e64590e346'),mongoose.Types.ObjectId('5c915475e172e4e64590e348'),mongoose.Types.ObjectId('5e9f46e1c8bf1a622fec69d5'),mongoose.Types.ObjectId('5dfdbb547b00ea76b75e5a70'),mongoose.Types.ObjectId('5e9fe35cc8bf1a622fec69d7'),mongoose.Types.ObjectId('5e9fe372c8bf1a622fec69d8'),mongoose.Types.ObjectId('5c915476e172e4e64590e349'),mongoose.Types.ObjectId('5e1bbda67b00ea76b75e5a73')];
            if (req.query.category && req.query.category.length) {
                requested_categories = [mongoose.Types.ObjectId(req.query.category)];
            } else { // if none is provided, return these categories: Program, Journey, Relationship, Mentoring, Group. Use case: Admin Choose Plans viewer
                requested_categories = [mongoose.Types.ObjectId('5c915475e172e4e64590e348'),mongoose.Types.ObjectId('5e9f46e1c8bf1a622fec69d5'),mongoose.Types.ObjectId('5dfdbb547b00ea76b75e5a70'),mongoose.Types.ObjectId('5e9fe35cc8bf1a622fec69d7'),mongoose.Types.ObjectId('5e9fe372c8bf1a622fec69d8')];
            }
            const sample_activities = await Moment.aggregate([
                {
                    $match: { // find all communities or programs a user is a part of
                        $expr:
                            { $cond: {
                                if: { // if querying onboarding Activity
                                        $in: [ mongoose.Types.ObjectId('5e17acd47b00ea76b75e5a71'), requested_categories ]
                                    },
                                then: // only select Restvo defaults
                                    {   $eq: [ "$_id", mongoose.Types.ObjectId('5d5785b462489003817fee18') ] },
                                else: // select all Programs and Communities a user is a part of
                                    {
                                        $and: [
                                            {
                                                $or: [ // either a community or a program
                                                    { $in: [ mongoose.Types.ObjectId('5c915324e172e4e64590e346'), { $ifNull: ["$categories", []] }] },
                                                    { $in: [ mongoose.Types.ObjectId('5c915475e172e4e64590e348'), { $ifNull: ["$categories", []] }] },
                                                ]
                                            }
                                        ]
                                    }
                            }},
                    }
                },
                {
                    $match: { // show only non-deleted Activities
                        $expr: {
                            $cond: [ // filter out deleted Activities
                                {$ifNull: ['$deletedAt', false]}, // if
                                false, // then
                                true // else
                            ]
                        }
                    }
                },
                {
                    $lookup: { // load child/onboarding Activities that are hosted by the parent Programs/Communities
                        from: "moments",
                        let: { parent_program_id: "$_id", parent_user_list_1: "$user_list_1", parent_user_list_2: "$user_list_2", parent_user_list_3: "$user_list_3" },
                        pipeline: [
                            {
                                $match: { // select activities by parent program ID and matching at least 1 requested category
                                    $expr: {
                                        $and: [
                                            {
                                                $cond: [ // filter out deleted Activities
                                                    {$ifNull: ['$deletedAt', false]}, // if
                                                    false, // then
                                                    true // else
                                                ]
                                            },
                                            { // if categories match the queried params
                                                $gt: [ { $size: { $setIntersection: [ { $ifNull: [ "$categories", []] }, requested_categories ] }}, 0 ]
                                            },
                                            {
                                                $switch: {
                                                    branches: [
                                                        {
                                                            case: { // if querying onboarding Activity
                                                                $in: [ mongoose.Types.ObjectId('5e17acd47b00ea76b75e5a71'), requested_categories ]
                                                            },
                                                            then: { // if onboarding Activity has been shared
                                                                $arrayElemAt: [ "$array_boolean", 1 ]
                                                            }
                                                        },
                                                        {
                                                            case: { // if public access and unauthenticated
                                                                $not: [ { $ifNull: [!!req.user, false] } ]
                                                            },
                                                            then: { // only return child activities if a child of Restvo, shared with marketplace is on, or not a child of Restvo, with discoverable turned on
                                                                $and: [
                                                                    {
                                                                        $in: [ "$$parent_program_id", { $ifNull: ["$parent_programs", []] }]
                                                                    },
                                                                    {
                                                                        $or: [
                                                                            {
                                                                                $and: [
                                                                                    { $eq: [ "$$parent_program_id", mongoose.Types.ObjectId('5d5785b462489003817fee18') ] },
                                                                                    {
                                                                                        $or: [  { $arrayElemAt: [ "$array_boolean", 0 ] },
                                                                                            { $arrayElemAt: [ "$array_boolean", 1 ] } ]
                                                                                    }
                                                                                ]
                                                                            },
                                                                            {
                                                                                $and: [
                                                                                    { $ne: [ "$$parent_program_id", mongoose.Types.ObjectId('5d5785b462489003817fee18') ] },
                                                                                    { $arrayElemAt: [ "$array_boolean", 0 ] }
                                                                                ]
                                                                            }]
                                                                    }
                                                                ]
                                                            }
                                                        },
                                                        {
                                                            case: { // if user has joined parent program
                                                                $or: [
                                                                    { $in: [ userId, { $ifNull: ["$$parent_user_list_1", []] }] },
                                                                    { $in: [ userId, { $ifNull: ["$$parent_user_list_2", []] }] },
                                                                    { $in: [ userId, { $ifNull: ["$$parent_user_list_3", []] }] },
                                                                ]
                                                            },
                                                            then: { // only return child activities if shared with marketplace is on, or if discoverable is on
                                                                $and: [
                                                                    {
                                                                        $in: [ "$$parent_program_id", { $ifNull: ["$parent_programs", []] }]
                                                                    },
                                                                    {
                                                                        $or: [  { $arrayElemAt: [ "$array_boolean", 0 ] },
                                                                            { $arrayElemAt: [ "$array_boolean", 1 ] } ]
                                                                    }
                                                                ]
                                                            }
                                                        },
                                                        {
                                                            case: { // if user has not joined parent program
                                                                $eq: [{
                                                                    $and: [
                                                                        { $in: [ userId, { $ifNull: ["$$parent_user_list_1", []] }] },
                                                                        { $in: [ userId, { $ifNull: ["$$parent_user_list_2", []] }] },
                                                                        { $in: [ userId, { $ifNull: ["$$parent_user_list_3", []] }] },
                                                                    ]
                                                                }, false ]
                                                            },
                                                            then: { // only return child activities that has discoverable turned on
                                                                $and: [
                                                                    {
                                                                        $in: [ "$$parent_program_id", { $ifNull: ["$parent_programs", []] }]
                                                                    },
                                                                    {
                                                                         $arrayElemAt: [ "$array_boolean", 0 ]
                                                                    }
                                                                ]
                                                            }
                                                        },
                                                        /*{
                                                            case: { // if querying child activity, return those matching parent Activity ID
                                                                $gt: [ { $size: { $setIntersection: [ all_child_categories, requested_categories ] }}, 0 ]
                                                            },
                                                            then: { // only return child activities if user is enrolled their the parent program
                                                                $in: [ "$$parent_program_id", { $ifNull: ["$parent_programs", []] }]
                                                            }
                                                        }*/
                                                    ],
                                                    default: false
                                                }
                                            },
                                        ]
                                    }
                                }
                            },
                            {
                                $lookup: { // load program's resource
                                    from: "resources",
                                    localField: "resource",
                                    foreignField: "_id",
                                    as: "resource"
                                }
                            },
                            {
                                $unwind: "$resource"
                            },
                        ],
                        as: "sample_activities"
                    }
                },
                {
                    $unwind: {
                        path: "$sample_activities",
                        preserveNullAndEmptyArrays: false // since categories is an optional field and can be empty
                    }
                },
                {
                    $group: {
                        _id: "$_id",
                        resource: {
                            $first: "$resource"
                        },
                        categories: {
                            $first: "$categories"
                        },
                        matrix_string: {
                            $first: "$matrix_string"
                        },
                        matrix_number: {
                            $first: "$matrix_number"
                        },
                        assets: {
                            $first: "$assets"
                        },
                        sample_activities: {
                            $addToSet: "$sample_activities"
                        }
                    }
                },
            ]);
            res.json(sample_activities);
        } else { // backward compatibility for < 1.7.4
            const moments = await Moment.find({
                "array_boolean.1": true,
                deletedAt: { $exists: false }
            }).populate('resource categories');
            res.json(moments);
        }
    } catch (err) {
        next({topic: 'System Error', title: 'loadSampleActivities', err: err});
    }
};

/** 
 * @desc Loads all the child Activities hosted by the given program
 * @memberof moment
 * @param req
 * @param req.params.programId: Activity (type: ObjectId) of interest
 * @param res
 * @param next
 * @returns {array} Array of child Activities
 *
 * created by Calvin Ho 1/9/20
 */

exports.loadChildActivities = async (req, res, next) => {
    try {
        if (req.params.programId && req.query.category) { // load all child Activities hosted by a parent Program
            let query = {
                parent_programs: req.params.programId,
                categories: req.query.category,
            };
            if (!['owner','admin','staff'].includes(req.user.role)) { // if not admin, returns only live Activities
                query.deletedAt = { $exists: false }
            }
            const moments = await Moment.find(query).populate('resource categories');
            res.json(moments);
        } else { // program ID or category ID missing
            next({topic: 'System Error', title: 'loadChildActivities: mission Parent Program ID or category', err: err});
        }
    } catch (err) {
        next({topic: 'System Error', title: 'loadChildActivities', err: err});
    }
};

/**
 * @desc Load the list of onboarding flow that the program (req.params.dependentMomentId) associates with
 * @memberof moment
 * @param req
 * @param res
 * @param next
 * @returns {Promise<void>}
 *
 * created by Calvin Ho 10/14/19
 */
//
exports.loadOnboardActivities = async (req, res, next) => {
    try {
        const programId = req.params.programId || "5d5785b462489003817fee18"; // the program Id, if not provided, use the default Restvo Mentoring Program
        query = [
            {
                $match: { // find Onboarding process that matches the program Id
                    "program": mongoose.Types.ObjectId(programId)
                }
            },
            {
                $match: { // select only Live Activities
                    $expr: {
                        $cond: [ // filter out deleted Activities
                            {$ifNull: ['$deletedAt', false]}, // if
                            false, // then
                            true // else
                        ]
                    },
                }
            },
            {
                $match: { // match the onboarding participant type (req.query.type)
                    $expr: {
                        $or: [
                            { // backward compatibility when none is provided
                                $not: [req.params.programId]
                            },
                            { // when loading type 2, 3, or 4 together, no type value is provided and it returns all types
                                $not: [req.query.type]
                            },
                            { // querying type 2 (participants), type 3 (organizers), type 4 (leaders)
                                $eq: [{ $arrayElemAt: [ "$array_boolean", parseInt(req.query.type, 10) ]}, true ]
                            }
                            ]
                    }
                }
            },
            {
                $lookup: { // load the moment's resource
                    from: "resources",
                    localField: "resource",
                    foreignField: "_id",
                    as: "resource"
                }
            },
            {
                $unwind: "$resource"
            },
            {
                $lookup: { // load the moment's categories
                    from: "resources",
                    localField: "categories",
                    foreignField: "_id",
                    as: "categories"
                }
            },
            {
                $unwind: {
                    path: "$categories",
                    preserveNullAndEmptyArrays: true // since categories is an optional field and can be empty
                }
            },
            {
                $lookup: { // load the moment's date/time data
                    from: "calendar",
                    localField: "calendar",
                    foreignField: "_id",
                    as: "calendar"
                }
            },
            {
                $unwind: {
                    path: "$calendar",
                    preserveNullAndEmptyArrays: true // since calendar is an optional field and can be empty
                }
            }];
        const moments = await Moment.aggregate(
            query.concat([
                {
                    $sort: {
                        updatedAt: -1
                    }
                }
            ])
        );
        if (!req.query.version) { // for version 0 that is < 1.6.3 (30)
            res.json(moments);
        } else { // for version 1 that is 1.6.3 (30)+
            // return both System Activity responses and Matching Configuration records
            results = await Response.find({
                dependent_moment: programId,
            });
            responses = { // responses has two fields
                preferences: [],
                matching_config: []
            };
            for (const response of results) {
                // if Matching Configuration records
                if (response.array_number && response.array_number.length && (response.array_number[0] === 50000)) {
                    responses.matching_config.push(response);
                } else { // for Onboarding Process responses
                    responses.preferences.push(response);
                }
            }
            res.json({preferences: moments, responses: responses});
        }
    } catch (err) {
        next({topic: 'System Error', title: 'loadOnboardActivities', err: err});
    }
};

//
/** load the list of onboarding questionnaires the user is required to respond to
 * @func loadUserPreferences
 * @desc load the list of onboarding questionnaires the user is required to respond to
 * @memberof moment
 * @param req
 * @param res
 * @param next
 * @param req.query.type: type of Onboarding Process (type: Number). Type 2: Participant, Type 3: Organizer, Type 4: Leader
 * @param req.query.pageNum: page number (type: Number)
 * @returns {Array} Array of Onboarding Processes
 * created by Calvin Ho 5/7/19
 */
exports.loadUserPreferences = async (req, res, next) => {
    try {
        itemsPerPage = 10;
        let pageNum = parseInt(req.query.pageNum, 10) || 1;
        let onboarding_processes = [];
        const type = req.query.type ? (parseInt(req.query.type, 10) > 1 ? parseInt(req.query.type, 10) : null) : null;
        if (req.query.programId) {
            userProgramIds = [mongoose.Types.ObjectId(req.query.programId)]
        } else {
            userPrograms = await Moment.find({ $or: [
                    { user_list_1: req.user._id },
                    { user_list_2: req.user._id },
                    { user_list_3: req.user._id },
                ], deletedAt: { $exists: false} }, { _id: 1, matrix_string: 1 });
            userProgramIds = userPrograms.map((c) => c._id );
        }
        if (pageNum === 1) {
            // load all required onboarding processes
            // case 1: program exists and type exists - load onboarding processes for an unenrolled program
            // case 2: program exists but no type - load all onboarding processes for that program, program already enrolled
            // case 3: no program nor type provided - load all onboarding processes for programs enrolled by the user
            // if programId is provided, make it the first stage
            query = req.query.programId && req.query.programId.length ? [
                    {
                        $match: {
                            _id: mongoose.Types.ObjectId(req.query.programId)
                        }
                    }
                ] : [];
            if (!req.query.programId || !req.query.type) { // case 2 or 3, if at least one of the two is not provided
                query = query.concat([
                    {
                        $match: { // find all programs a user is a part of
                            $expr: {
                                $or: [
                                    { $in: [ req.user._id, { $ifNull: ["$user_list_1", []] }] },
                                    { $in: [ req.user._id, { $ifNull: ["$user_list_2", []] }] },
                                    { $in: [ req.user._id, { $ifNull: ["$user_list_3", []] }] }
                                ]
                            }
                        }
                    },
                ]);
            }
            onboarding_processes = await Moment.aggregate(
                query.concat([
                    {
                        $lookup: { // find the corresponding onboarding processes
                            from: "moments",
                            let: { programId: "$_id", user_list_1: "$user_list_1", user_list_2: "$user_list_2", user_list_3: "$user_list_3" },
                            pipeline: [
                                {
                                    $match: { // retain if it finds a parent record
                                        $expr: {
                                            $and: [
                                                {
                                                    $eq: ["$program", "$$programId" ]
                                                },
                                                {
                                                    $cond: [ // filter out deleted Processes
                                                        {$ifNull: ['$deletedAt', false]}, // if
                                                        false, // then
                                                        true // else
                                                    ]
                                                },
                                                {
                                                    $switch: {
                                                        branches: [
                                                            {
                                                                case: { // if both program and type are provided, case 1 - user has not joined program
                                                                    $and: [{$ifNull: [ req.query.programId, false ]}, { $ifNull: [ type, false ] }]
                                                                },
                                                                then: {
                                                                    $eq: [{ $arrayElemAt: [ "$array_boolean", type ]}, true ]
                                                                }
                                                            },
                                                            {
                                                                case: { // case 2 and 3. user has joined these programs, get all the onboarding processes
                                                                    $ifNull: [ type, true ] // if type is not provided
                                                                },
                                                                then: { //
                                                                    $or: [
                                                                        { $and: [{ $arrayElemAt: [ "$array_boolean", 2 ]}, { $in: [ req.user._id, "$$user_list_1" ]} ]},
                                                                        { $and: [{ $arrayElemAt: [ "$array_boolean", 3 ]}, { $in: [ req.user._id, "$$user_list_2" ]} ]},
                                                                        { $and: [{ $arrayElemAt: [ "$array_boolean", 4 ]}, { $in: [ req.user._id, "$$user_list_3" ]} ]},
                                                                    ]
                                                                }
                                                            }
                                                        ]
                                                    }
                                                }
                                            ]
                                        }
                                    }
                                },
                                {
                                    $lookup: { // find all parent activity relationship records
                                        from: "responses",
                                        let: { moment_id: "$_id" },
                                        pipeline: [
                                            {
                                                $match: { // retain if it finds a parent record
                                                    $expr: {
                                                        $eq: ["$dependent_moment", "$$moment_id" ]
                                                    }
                                                }
                                            }
                                        ],
                                        as: "response" // response is an array if it has a parent, empty array if it has no parent
                                    }
                                },
                                {
                                    $unwind: { // unwind and preserve records as null if they have no parent record
                                        path: "$response",
                                        preserveNullAndEmptyArrays: true // response will be false if no parent is found
                                    }
                                },
                                {
                                    $match: { // select moment that has no parent activity, i.e. they are required onboarding processes
                                        response: { $exists: false }
                                    }
                                },
                                {
                                    $lookup: { // load moment's resource field
                                        from: "resources",
                                        localField: "resource",
                                        foreignField: "_id",
                                        as: "resource"
                                    }
                                },
                                {
                                    $unwind: "$resource"
                                },
                                {
                                    $lookup: { // load the current user's responses to the questionnaires
                                        from: "responses",
                                        let: { moment_id: "$_id" },
                                        pipeline: [
                                            {
                                                $match: {
                                                    $expr: {
                                                        $and: [
                                                            {
                                                                $eq: ["$user", req.user._id]
                                                            },
                                                            {
                                                                $eq: ["$moment", "$$moment_id"]
                                                            }
                                                        ]
                                                    }
                                                }
                                            },
                                        ],
                                        as: "response"
                                    }
                                },
                                {
                                    $unwind: {
                                        path: "$response",
                                        preserveNullAndEmptyArrays: true // since response can be empty
                                    }
                                }
                            ],
                            as: "onboarding_process" // onboarding_process is an array if it exists, empty array if there is no onboarding process
                        }
                    },
                    {
                        $unwind: { // unwind and remove programs if they don't have onboarding processes that match the participant status (participant, organizer, leader)
                            path: "$onboarding_process",
                        }
                    },
                    {
                        $replaceRoot: {
                            newRoot: "$onboarding_process"
                        }
                    },
                    {
                        $lookup: { // load the program
                            from: "moments",
                            localField: "program",
                            foreignField: "_id",
                            as: "program"
                        }
                    },
                    {
                        $unwind: "$program"
                    }
                ])
            );
        }
        // load onboarding processes that were unlocked by user's choices
        unlocked_processes = await Response.aggregate([
            {
                $match: { // retain the user's responses
                    user: req.user._id
                }
            },
            {
                $unwind: "$matrix_number"
            },
            {
                $match: { // retain responses to interactables
                    $expr: {
                        $gt: [
                            { $size: "$matrix_number" }, 5
                        ]
                    }
                }
            },
            {
                $addFields: { // add question id field
                    question_id: {
                        $arrayElemAt: [ "$matrix_number", 0 ]
                    }
                },
            },
            {
                $project: { // remove the metadata elements (position 0 - 4)
                    moment: 1,
                    question_id: 1,
                    choices: {
                        $slice: [ "$matrix_number", 5, { $subtract: [ { $size: "$matrix_number" }, 5 ] } ]
                    }
                }
            },
            {
                $lookup: { // find parent/child activity records for the current question (own_question_id)
                    from: "responses",
                    let: { own_question_id: "$question_id", own_choices: "$choices" },
                    pipeline: [
                        {
                            $match: {
                                dependent_moment: { $exists: true }
                            }
                        },
                        {
                            $unwind: "$matrix_number"
                        },
                        {
                            $match: {
                                $expr: {
                                    $gt: [
                                        { $size: "$matrix_number" }, 5
                                    ]
                                }
                            }
                        },
                        {
                            $addFields: {
                                others_question_id: {
                                    $arrayElemAt: [ "$matrix_number", 0 ]
                                }
                            },
                        },
                        {
                            $project: { // clean up others choices by removing the metadata
                                _id: 0,
                                dependent_moment: 1,
                                others_question_id: 1,
                                others_choices: {
                                    $slice: [ "$matrix_number", 5, { $subtract: [ { $size: "$matrix_number" }, 5 ] } ]
                                }
                            }
                        },
                        {
                            $match: {
                                $expr: {
                                    $eq: [ "$others_question_id", "$$own_question_id" ]
                                }
                            }
                        },
                        {
                            $project: { // if child questionnaire matches user's choices, commonToBoth will have an array of those choices
                                dependent_moment: 1,
                                others_choices: 1,
                                others_question_id: 1,
                                commonToBoth: {
                                    $setIntersection: [
                                        "$others_choices", "$$own_choices"
                                    ]
                                }
                            }
                        }
                    ],
                    as: "matched_dependent_moments"
                }
            },
            {
                $match: { // retain if matched dependent moments have at least one element in the array
                    $expr: {
                        $gt: [
                            { $size: "$matched_dependent_moments" }, 0
                        ]
                    }
                }
            },
            {
                $unwind: "$matched_dependent_moments"
            },
            {
                $match: { // retain if there is at least one user's choice that matches a dependent moment
                    $expr: {
                        $gt: [
                            { $size: "$matched_dependent_moments.commonToBoth" }, 0
                        ]
                    }
                }
            },
            {
                $lookup: { // load the dependent moment (it unlocks it for the user)
                    from: "moments",
                    localField: "matched_dependent_moments.dependent_moment",
                    foreignField: "_id",
                    as: "matched_dependent_moments"
                }
            },
            {
                $unwind: "$matched_dependent_moments"
            },
            {
                $replaceRoot: { // promote it
                    newRoot: "$matched_dependent_moments"
                }
            },
            {
                $match: { // matches the program Id
                    //"program": mongoose.Types.ObjectId(programId)
                    $expr: {
                        $and: [
                            {
                                $in: [ "$program", userProgramIds ]
                            },
                            {
                                $switch: {
                                    branches: [
                                        {
                                            case: {
                                                $gt: [ type, 1 ] // if type is 2, 3, or 4
                                            },
                                            then: {
                                                $eq: [{ $arrayElemAt: [ "$array_boolean", type ]}, true ]
                                            }
                                        },
                                        {
                                            case: {
                                                $ifNull: [ type, true ] // if type is missing
                                            },
                                            then: {
                                                $or: [
                                                    { $eq: [{ $arrayElemAt: [ "$array_boolean", 2 ]}, true ]},
                                                    { $eq: [{ $arrayElemAt: [ "$array_boolean", 3 ]}, true ]},
                                                    { $eq: [{ $arrayElemAt: [ "$array_boolean", 4 ]}, true ]},
                                                ]
                                            }
                                        }
                                    ]
                                }
                            }
                        ]
                    }
                }
            },
            {
                $lookup: { // load the dependent moment's resource
                    from: "resources",
                    localField: "resource",
                    foreignField: "_id",
                    as: "resource"
                }
            },
            {
                $unwind: "$resource"
            },
            {
                $lookup: { // load the program
                    from: "moments",
                    localField: "program",
                    foreignField: "_id",
                    as: "program",
                }
            },
            {
                $unwind: "$program"
            },
            {
                $lookup: { // load the user's choices in response to the dependent questionnaire (to display whether the user has at least responded to one question)
                    from: "responses",
                    let: { moment_id: "$_id" },
                    pipeline: [
                        {
                            $match: {
                                $expr: {
                                    $and: [
                                        {
                                            $eq: ["$user", req.user._id]
                                        },
                                        {
                                            $eq: ["$moment", "$$moment_id"]
                                        }
                                    ]
                                }
                            }
                        },
                    ],
                    as: "response"
                }
            },
            {
                $unwind: {
                    path: "$response",
                    preserveNullAndEmptyArrays: true // since response can be empty
                }
            },
            {
                $skip: itemsPerPage * (pageNum - 1)
            },
            {
                $limit: itemsPerPage
            },
        ]);
        onboarding_processes = onboarding_processes.concat(unlocked_processes);
        res.json(onboarding_processes);
    } catch (err) {
        next({topic: 'System Error', title: 'loadUserPreferences', err: err});
    }
};

/**
 * @desc execute the matching algorithm to find users best matched based on how they answered the questions
 * @memberof moment
 * @param req
 * @param res
 * @param next
 * @returns {Promise<void>}
 */
//
exports.computeMatchingUsers = async (req, res, next) => {
    try {
        user = req.user ? req.user._id : mongoose.Types.ObjectId('596dc94782e2eb4512a320b6'); // default user is Gideon Ho, if it is an unauthenticated user
        itemsPerPage = 15;
        pageNum = req.query.pageNum || 1;
        programId = req.query.momentId ? mongoose.Types.ObjectId(req.query.momentId) : mongoose.Types.ObjectId("5d5785b462489003817fee18"); // '' if no momentId is provided, set it to the default Restvo mentorship program

        // load the program's onboarding processes
        const program = await Moment.findOne({ _id: programId }).populate('resource');
        if (!program) return res.json([]); // in cases where the Activity is deleted but the web app still tries to query for it (e.g. click the back button), it returns an empty array
        const onboarding_processes = await Moment.find({ program: programId }).populate('resource');
        let onboarding_processes_ids = onboarding_processes.map((c) => mongoose.Types.ObjectId(c._id) );
        onboarding_processes_ids.push(mongoose.Types.ObjectId('5d9699ae4b43ac3154cba176'));

        const matched_users = await Response.aggregate([
            {
                $match: { // select user's own responses
                    user: user
                }
            },
            { // unwind the list of questions
                $unwind: "$matrix_number"
            },
            {
                $match: { // select only if responses has matrix_number that is longer than length 5 (responses to questionnaire)
                    $expr: {
                        $gt: [
                            { $size: "$matrix_number" }, 5
                        ]
                    }
                }
            },
            {
                $addFields: { // add question id field, stored in matrix_number, position 0
                    question_id: {
                        $arrayElemAt: [ "$matrix_number", 0 ]
                    }
                },
            },
            {
                $project: { // format data by removing first 5 elements in matrix_number (metadata). See schematic for more details
                    moment: 1,
                    question_id: 1,
                    choices: {
                        $slice: [ "$matrix_number", 5, { $subtract: [ { $size: "$matrix_number" }, 5 ] } ]
                    }
                }
            },
            {
                $lookup: { // get a list of moment's participants
                    from: "moments",
                    pipeline: [
                        {
                            $match: {
                                _id: programId
                            }
                        },
                        {
                            $lookup: { // load moment's resource
                                from: "resources",
                                localField: "resource",
                                foreignField: "_id",
                                as: "resource"
                            }
                        },
                        {
                            $unwind: "$resource"
                        },
                        {
                            $addFields: { // fetch the list of component Ids
                                componentIds: {
                                    $arrayElemAt: [ "$resource.matrix_number", 0 ]
                                }
                            },
                        },
                        {
                            $addFields: { // find the index of the show directory component
                                component_index: {
                                    $indexOfArray: [ "$componentIds", 50000 ] // find the index of component Id 50000
                                }
                            },
                        },
                        {
                            $addFields: { // get the show directory config settings
                                directory_config_settings: {
                                    $arrayElemAt: [ "$matrix_number", "$component_index" ]
                                }
                            },
                        },
                        {
                            $project: { // project only the user_list_1, user_list_2, user_list_3 fields which contain the list of participants, organizers, and mentors
                                directory_config_settings: 1,
                                user_list_1: 1,
                                user_list_2: 1,
                                user_list_3: 1
                            }
                        }
                    ],
                    as: "program"
                }
            },
            { // unwind program
                $unwind: "$program"
            },
            {
                $lookup: { // do an outer join with other users' responses to the questionnaires
                    from: "responses",
                    let: { own_question_id: "$question_id", own_choices: "$choices", program: "$program" },
                    pipeline: [
                        {
                            $match: {
                                $expr: {
                                    $and: [
                                        { // exclude user's own responses
                                            $ne: [ "$user", user ]
                                        },
                                        {
                                            $or: [
                                                { // retain the responses of the Activity's participants only (if set in config)
                                                    $and: [
                                                        {
                                                            $eq: [{ $arrayElemAt: [ "$$program.directory_config_settings", 0 ]}, 1 ]
                                                        },
                                                        {
                                                            $in: [ "$user", "$$program.user_list_1" ]
                                                        }]
                                                },
                                                { // retain the responses of the Activity's organizers only (if set in config), or if it is the default Restvo Mentoring program, show all Restvo users
                                                    $and: [
                                                        {
                                                            $eq: [{ $arrayElemAt: [ "$$program.directory_config_settings", 1 ]}, 1 ]
                                                        },
                                                        {
                                                            $in: [ "$user", "$$program.user_list_2" ]
                                                        }]
                                                },
                                                { // retain the responses of the Activity's leaders only (if set in config), or if it is the default Restvo Mentoring program, show all Restvo users
                                                    $and: [
                                                        {
                                                            $eq: [{ $arrayElemAt: [ "$$program.directory_config_settings", 2 ]}, 1 ]
                                                        },
                                                        {
                                                            $in: [ "$user", "$$program.user_list_3" ]
                                                        }]
                                                },
                                                /*{
                                                    $eq: [ programId, mongoose.Types.ObjectId("5d5785b462489003817fee18") ]
                                                }*/
                                            ]
                                        }

                                    ]
                                }
                            }
                        },
                        { // unwind the questions in each response
                            $unwind: "$matrix_number"
                        },
                        {
                            $match: { // filter out records that has matrix_number shorter than 5. They are not valid records.
                                $expr: {
                                    $gt: [
                                        { $size: "$matrix_number" }, 5
                                    ]
                                }
                            }
                        },
                        {
                            $addFields: { // add the others_question_id field
                                others_question_id: {
                                    $arrayElemAt: [ "$matrix_number", 0 ]
                                }
                            },
                        },
                        {
                            $project: { // format data by removing first 5 elements in matrix_number (metadata). See schematic for more details
                                _id: 0,
                                user: 1,
                                others_question_id: 1,
                                others_choices: {
                                    $slice: [ "$matrix_number", 5, { $subtract: [ { $size: "$matrix_number" }, 5 ] } ]
                                },
                            }
                        },
                        {
                            $match: { // keep records with matching question ids or if it is the special Public Listing question (should be unique in Restvo Mentors)
                                $expr: {
                                    $or: [
                                        {
                                            $eq: [ "$others_question_id", "$$own_question_id" ]
                                        },
                                        {
                                            $eq: [ "$others_question_id", 1571716738858625 ] // this should only apply when Program queried is Restvo Mentor
                                        }
                                    ]
                                }
                            }
                        },
                        {
                            $project: {  // format and calculate if the user's choices intersect with the participant's choice (return an array of matched choices in commonToBoth)
                                user: 1,
                                others_choices: 1,
                                others_question_id: 1,
                                commonToBoth: { // an array of matched choices that are common in both arrays, excluding the special Public Listing response which is manually included in the previous stage
                                    $cond: {
                                        if: {
                                            $ne: [ "$others_question_id", 1571716738858625 ] // this is necessary to excluded the Public Sharing response answers from being included in the matched_score calculation, which will skew the results
                                        },
                                        then: {
                                            $setIntersection: [ "$others_choices", "$$own_choices" ]
                                        },
                                        else: []
                                    }
                                },
                                isOthersOnly: { // an array of choices that exists in other's choices only, excluding the special Public Listing response
                                    $cond: {
                                        if: {
                                            $ne: [ "$others_question_id", 1571716738858625 ] // this is necessary to excluded the Public Sharing response answers from being included in the matched_score calculation, which will skew the results
                                        },
                                        then: {
                                            $setDifference: [ "$others_choices", "$$own_choices" ]
                                        },
                                        else: []
                                    }
                                },
                                isOwnOnly: { // an array of choices that exists in own choices only, excluding the special Public Listing response
                                    $cond: {
                                        if: {
                                            $ne: [ "$others_question_id", 1571716738858625 ] // this is necessary to excluded the Public Sharing response answers from being included in the matched_score calculation, which will skew the results
                                        },
                                        then: {
                                            $setDifference: [ "$$own_choices", "$others_choices" ]
                                        },
                                        else: []
                                    }
                                },
                            }
                        }
                    ],
                    as: "matched_users"
                }
            },
            {
                $unwind: "$matched_users"
            },
            {
                $lookup: { // do an outer join to get a list of matching configuration records
                    from: "responses",
                    let: { moment_id: "$moment", question_id: "$matched_users.others_question_id" },
                    pipeline: [
                        {
                            $match: { // select only matching config records
                                "array_number.0": 50000
                            }
                        },
                        {
                            $match: {
                                $expr: {
                                    $and: [
                                        {
                                            $eq: ["$moment", "$$moment_id"]
                                        },
                                        {
                                            $eq: ["$dependent_moment", programId]
                                        }
                                    ]
                                }
                            }
                        },
                        { // unwind the questions in each response
                            $unwind: "$matrix_number"
                        },
                        {
                            $addFields: { // add the others_question_id field
                                question_id: {
                                    $arrayElemAt: [ "$matrix_number", 0 ]
                                }
                            },
                        },
                        {
                            $match: {
                                $expr: {
                                    $eq: ["$question_id", "$$question_id"]
                                }
                            }
                        },
                    ],
                    as: "matching_config"
                }
            },
            {
                $unwind: {
                    path: "$matching_config",
                    preserveNullAndEmptyArrays: true // since matching config can be null (no matching config)
                },
            },
            {
                $project: {
                    user: "$matched_users.user",
                    moment: 1,
                    response: {
                        question_id: "$question_id",
                        selections: "$matched_users.commonToBoth"
                    },
                    matching_config: 1,
                    matched_users: 1,
                    matched_score: {
                        $switch: {
                            branches: [
                                {
                                    case: // when the question is excluded from the cumulative scoring
                                        {
                                            $eq: [{ $arrayElemAt: [ "$matching_config.matrix_number", 1 ]}, -1 ]
                                        },
                                    then: 0
                                },
                                {
                                    case: // when matching similar answers
                                        {
                                            $and: [
                                                {
                                                    $eq: [{ $arrayElemAt: [ "$matching_config.matrix_number", 1 ]}, 1 ]
                                                },
                                                {
                                                    $or: [
                                                        {
                                                            $eq: [{ $arrayElemAt: [ "$matching_config.matrix_number", 2 ]}, 1 ]
                                                        },
                                                        {
                                                            $eq: [{ $arrayElemAt: [ "$matching_config.matrix_number", 2 ]}, 2 ]
                                                        }
                                                    ]
                                                }
                                            ]
                                        },
                                    then: {
                                        $multiply: [
                                            {
                                                $arrayElemAt: [ "$matching_config.matrix_number", 3 ]
                                            },
                                            {
                                                $size: "$matched_users.commonToBoth"
                                            }
                                        ]
                                    }
                                },
                                {
                                    case: // when matching different answers
                                        {
                                            $and: [
                                                {
                                                    $eq: [{ $arrayElemAt: [ "$matching_config.matrix_number", 1 ]}, 1 ]
                                                },
                                                {
                                                    $or: [
                                                        {
                                                            $eq: [{ $arrayElemAt: [ "$matching_config.matrix_number", 2 ]}, 3 ]
                                                        },
                                                        {
                                                            $eq: [{ $arrayElemAt: [ "$matching_config.matrix_number", 2 ]}, 4 ]
                                                        }
                                                    ]
                                                }
                                            ]
                                        },
                                    then: {
                                        $multiply: [
                                            {
                                                $arrayElemAt: [ "$matching_config.matrix_number", 3 ]
                                            },
                                            {
                                                $add: [
                                                    {
                                                        $size: "$matched_users.isOthersOnly"
                                                    },
                                                    {
                                                        $size: "$matched_users.isOwnOnly"
                                                    }
                                                ]
                                            }
                                        ]
                                    }
                                },
                            ],
                            default: { $size: "$matched_users.commonToBoth" }
                        }
                    },
                    user_filter: {
                        $switch: {
                            branches: [
                                {
                                    case: // special case: when a user turns off public sharing for Restvo Mentor
                                        {
                                            $and: [
                                                {
                                                    $eq: [ programId, mongoose.Types.ObjectId("5d5785b462489003817fee18") ]
                                                },
                                                {
                                                    $eq: [ "$matched_users.others_question_id", 1571716738858625 ]
                                                },
                                                {
                                                    $eq: [ { $arrayElemAt: [ "$matched_users.others_choices", 0 ] }, 1 ] // user chooses to turn off listing in Restvo mentor
                                                }
                                            ]
                                        },
                                    then: 1,
                                },
                                {
                                    case: // filter out records that requires an answer but the other user's record is blank
                                        {
                                            $and: [
                                                {
                                                    $eq: [{ $arrayElemAt: ["$matching_config.matrix_number", 1] }, 2]
                                                },
                                                {
                                                    $ne: [{ $arrayElemAt: ["$matching_config.matrix_number", 0] }, "$question_id"]
                                                }
                                            ]
                                        },
                                    then: 1,
                                },
                                {
                                    case: // filter out records that are NOT partially similar match config settings
                                        {
                                            $and: [
                                                {
                                                    $eq: [{ $arrayElemAt: ["$matching_config.matrix_number", 1] }, 2]
                                                },
                                                {
                                                    $eq: [{ $arrayElemAt: ["$matching_config.matrix_number", 2] }, 1]
                                                },
                                                {
                                                    $eq: [{ $size: "$matched_users.commonToBoth" }, 0] // has no overlap
                                                },
                                                {
                                                    $or: [ // either exactly the same or exactly opposite
                                                        {
                                                            $gt: [{ $size: "$matched_users.isOthersOnly" }, 0] // has is others only
                                                        },
                                                        {
                                                            $gt: [{ $size: "$matched_users.isOwnOnly" }, 0] // has is Own only
                                                        },
                                                    ]
                                                },
                                            ]
                                        },
                                    then: 1,
                                },
                                {
                                    case: // filter out records that are NOT exact match config settings
                                        {
                                            $and: [
                                                {
                                                    $and: [
                                                        {
                                                            $eq: [{ $arrayElemAt: ["$matching_config.matrix_number", 1] }, 2]
                                                        },
                                                        {
                                                            $eq: [{ $arrayElemAt: ["$matching_config.matrix_number", 2] }, 2]
                                                        }
                                                    ]
                                                },
                                                {
                                                    $or: [ // either exactly the same or exactly opposite
                                                        {
                                                            $gt: [{ $size: "$matched_users.isOthersOnly" }, 0] // has is others only
                                                        },
                                                        {
                                                            $gt: [{ $size: "$matched_users.isOwnOnly" }, 0] // has is own only
                                                        },
                                                    ]
                                                }
                                            ]
                                        },
                                    then: 1,
                                },
                                {
                                    case: // filter out records that are NOT partially different match config settings
                                        {
                                            $and: [
                                                {
                                                    $eq: [{ $arrayElemAt: ["$matching_config.matrix_number", 1] }, 2]
                                                },
                                                {
                                                    $eq: [{ $arrayElemAt: ["$matching_config.matrix_number", 2] }, 3]
                                                },
                                                {
                                                    $gt: [{ $size: "$matched_users.commonToBoth" }, 0] // has at least one overlap
                                                },
                                                {
                                                    $eq: [{ $size: "$matched_users.isOthersOnly" }, 0]
                                                },
                                                {
                                                    $eq: [{ $size: "$matched_users.isOwnOnly" }, 0]
                                                },
                                            ]
                                        },
                                    then: 1,
                                },
                                {
                                    case: // filter out records that are NOT exact opposite match config settings
                                        {
                                            $and: [
                                                {
                                                    $eq: [{ $arrayElemAt: ["$matching_config.matrix_number", 1] }, 2]
                                                },
                                                {
                                                    $eq: [{ $arrayElemAt: ["$matching_config.matrix_number", 2] }, 4]
                                                },
                                                {
                                                    $gt: [{ $size: "$matched_users.commonToBoth" }, 0] // has at least one overlap
                                                },
                                            ]
                                        },
                                    then: 1,
                                }
                            ],
                            default: 0
                        }
                    }
                }
            },
            {
                $lookup: { // load the response's moment
                    from: "moments",
                    localField: "moment",
                    foreignField: "_id",
                    as: "moment"
                }
            },
            {
                $unwind: "$moment"
            },
            {
                $lookup: { // load moment's resource
                    from: "resources",
                    localField: "moment.resource",
                    foreignField: "_id",
                    as: "resource"
                }
            },
            {
                $unwind: "$resource"
            },
            {
                $group: { // tally up the responses' matched score into the users' matched score
                    _id: "$user", // the matched user's id
                    score: {
                        $sum: "$matched_score"
                    },
                    preferences: {
                        $addToSet: {
                            resource: "$resource",
                            moment: "$moment",
                            response: "$response",
                        }
                    },
                    matching_config: {
                        $addToSet: "$matching_config"
                    },
                    matched_users: {
                        $addToSet: "$matched_users"
                    },
                    remove_user: {
                        $sum: "$user_filter"
                    }
                }
            },
            {
                $match: { // retain user that has no remove_user operator turned on
                    "remove_user": 0
                }
            },
            {
                $sort: { // sort matched scores in reversed order
                    score: -1
                }
            },
            { // pagination
                $skip: itemsPerPage * (pageNum - 1)
            },
            { // limit number of returned items per query
                $limit: itemsPerPage
            },
            { // load the matched user's profile
                $lookup: {
                    from: "users",
                    localField: "_id",
                    foreignField: "_id",
                    as: "user"
                }
            },
            { // remove the container array
                $unwind: "$user"
            },
            {
                $lookup: {
                    from: "moments",
                    pipeline: [
                        {
                            $match: {
                                _id: programId
                            }
                        },
                        {
                            $lookup: { // load program's resource
                                from: "resources",
                                localField: "resource",
                                foreignField: "_id",
                                as: "resource"
                            }
                        },
                        {
                            $unwind: "$resource"
                        },
                    ],
                    as: "program"
                }
            },
            {
                $unwind:  "$program"
            },
            {
                $lookup: { // load onboarding process' resource
                    from: "moments",
                    let: { programId: "$program._id"},
                    pipeline: [
                        {
                            $match: {
                                $expr: {
                                    $eq: [ "$program", "$$programId" ]
                                }
                            }
                        },
                        {
                            $lookup: { // load program's resource
                                from: "resources",
                                localField: "resource",
                                foreignField: "_id",
                                as: "resource"
                            }
                        },
                        {
                            $unwind: "$resource"
                        },
                        /*{
                            $unwind: "$resource.matrix_number"
                        },*/
                    ],
                    as: "onboarding_process"
                }
            },
            {
                $unwind:  "$onboarding_process"
            },
            { // load matched user's answers to the current program of interest
                $lookup: { //
                    from: "responses",
                    let: { userId: "$user._id", onboarding_process: "$onboarding_process" },
                    pipeline: [
                        {
                            $match: { // filter the current Program's matched user's text answers
                                $expr: {
                                    $and: [
                                        {
                                            $eq: ["$user", "$$userId"]
                                        },
                                        {
                                            $in: ["$moment", {$ifNull :[ onboarding_processes_ids,[] ]}]
                                        },
                                        // temp-recovery remove to allow old user bio question to show up. can be reinstated once 1.6.3 (48) is obsolete
                                        /*{
                                            $in: [40010, {$ifNull :[ "$array_number", [] ]}]
                                        }*/
                                    ]
                                }
                            }
                        },
                        {
                            $unwind: {
                                path: "$matrix_number",
                                preserveNullAndEmptyArrays: true
                            }
                        },
                        {
                            $unwind: {
                                path: "$matrix_string",
                                preserveNullAndEmptyArrays: true
                            }
                            // only text answers are kept. m.c. and tile choice are excluded since they don't have their matrix_string is []
                        },
                        {
                            $addFields: {
                                multiple_choice: {
                                    $cond: {
                                        if: {
                                            $ifNull: [ "$matrix_number", false ]
                                        },
                                        then: {
                                            $cond: {
                                                if: {
                                                    $gt: [ { $size: "$matrix_number" }, 5 ]
                                                },
                                                then: {
                                                    $slice: [ "$matrix_number", 5, { $subtract: [ { $size: "$matrix_number" }, 5 ] } ]
                                                },
                                                else: []
                                            }
                                        },
                                        else: []
                                    }
                                }
                            }
                        },
                        {
                            $unwind: {
                                path: "$multiple_choice",
                                preserveNullAndEmptyArrays: true
                            }
                        },
                        {
                            $project: {
                                _id: 1,
                                question_id: {
                                    $switch: {
                                        branches: [
                                            {
                                                case: { // text answer
                                                    $ifNull: [ "$matrix_string", false ]
                                                },
                                                then: { // question id
                                                    $toLong: { $arrayElemAt: [ "$matrix_string", 0 ] } // using matrix_string[0] since it is valid for both m.c. and tile choice.
                                                },
                                            },
                                            {
                                                case: { // multiple choice and tile choices
                                                    $ifNull: [ "$matrix_number", false ]
                                                },
                                                then: { // question id
                                                    $arrayElemAt: [ "$matrix_number", 0 ]
                                                }
                                            }
                                        ],
                                        default: null
                                    }
                                },
                                user_answer: {
                                    $switch: {
                                        branches: [
                                            {
                                                case: { // text answer type
                                                    $ifNull: [ "$matrix_string", false ]
                                                },
                                                then: { // textanswer
                                                    $arrayElemAt: [ "$matrix_string", 1 ]
                                                    //$slice: [ "$matrix_string", 1, { $subtract: [ { $size: "$matrix_string" }, 1 ] } ]
                                                },
                                            },
                                            {
                                                case: { // multiple choice and tile choices
                                                    $ifNull: [ "$matrix_number", false ]
                                                },
                                                then: {
                                                    $switch: {
                                                        branches: [
                                                            {
                                                                case: {
                                                                    $and: [
                                                                        { // tile choices. the user selection is stored as a option timestamp, hence a 15 digit number
                                                                            $gte: [ "$multiple_choice", 100000 ]
                                                                        },
                                                                        { // componentIndex needs to be greater than -1
                                                                            $gt: [{$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]}, -1]
                                                                        },
                                                                        { // option index needs to be greater than -1
                                                                            $gt: [{$indexOfArray: [{$arrayElemAt: [ "$$onboarding_process.matrix_number", {$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]} ]}, "$multiple_choice"]}, -1]
                                                                        }
                                                                    ]

                                                                },
                                                                then: { // get user answer by reading the label from onboarding_process.matrix_string[componentIndex][optionIndex]
                                                                    $arrayElemAt: [ {$arrayElemAt: [ "$$onboarding_process.matrix_string",
                                                                            // componentIndex
                                                                            { $indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]} ]},
                                                                        // option index
                                                                        { $subtract: [{$indexOfArray: [{$arrayElemAt: [ "$$onboarding_process.matrix_number", {$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]} ]}, "$multiple_choice"]}, 5]}
                                                                    ]
                                                                },
                                                            },
                                                            {
                                                                case: {
                                                                    $and: [
                                                                        { // multiple choice
                                                                            $lt: [ "$multiple_choice", 100000 ]
                                                                        },
                                                                        { // componentIndex needs to be greater than -1
                                                                            $gt: [{$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]}, -1]
                                                                        },
                                                                        { // size of the array needs to be greater than the multiple_choice, which is a position index
                                                                            $gt: [{$size:{$arrayElemAt: [ "$$onboarding_process.matrix_string", {$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]} ]}}, "$multiple_choice"]
                                                                        }
                                                                    ]
                                                                },
                                                                then: { // get user answer by reading the label from onboarding_process.matrix_string[componentIndex][multiple_choice]
                                                                    $arrayElemAt: [ {$arrayElemAt: [ "$$onboarding_process.matrix_string", {$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]} ]}, "$multiple_choice" ]
                                                                },
                                                            }
                                                        ],
                                                        default: []
                                                    }
                                                }
                                            }
                                        ],
                                        default: []
                                    }
                                },
                                question: {
                                    $switch: {
                                        branches: [
                                            {
                                                case: {
                                                    $and: [
                                                        { // text answer
                                                            $ifNull: [ "$matrix_string", false ]
                                                        },
                                                        { // componentIndex needs to be greater than -1
                                                            $gt: [{$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$toLong: { $arrayElemAt: [ "$matrix_string", 0 ] }}]}, -1]
                                                        }
                                                    ]
                                                },
                                                then: {
                                                    $arrayElemAt: [ {$arrayElemAt: [ "$$onboarding_process.matrix_string", {$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$toLong: { $arrayElemAt: [ "$matrix_string", 0 ] }}]} ]}, 0 ]
                                                },
                                            },
                                            {
                                                case: {
                                                    $and: [
                                                        { // multiple choice and tile choices
                                                            $ifNull: [ "$matrix_number", false ]
                                                        },
                                                        { // componentIndex needs to be greater than -1
                                                            $gt: [{$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]}, -1]
                                                        }
                                                    ]
                                                },
                                                then: {
                                                    $arrayElemAt: [ {$arrayElemAt: [ "$$onboarding_process.resource.en-US.matrix_string", {$indexOfArray: [ {$arrayElemAt: [ "$$onboarding_process.resource.matrix_number", 2 ]}, {$arrayElemAt: [ "$matrix_number", 0 ]}]} ]}, 1 ]
                                                },
                                            }
                                        ],
                                        default: null
                                    }
                                }
                            }
                        },
                        {
                            $group: {
                                _id: "$question_id",
                                response_id: {
                                    $first: "$_id"
                                },
                                question: {
                                    $first: "$question"
                                },
                                user_answer: {
                                    $addToSet: "$user_answer"
                                }
                            }
                        },
                        {
                            $project: {
                                _id: 1,
                                question_id: "$_id",
                                question: "$question",
                                user_answer: "$user_answer"
                            }
                        }
                        /*{
                            $unwind: "$matrix_string"
                            // only text answers are kept. m.c. and tile choice are excluded since they don't have their matrix_string is []
                        },
                        {
                            $project: { // project only the matrix_string[0][1] element which is the bio field
                                _id: 0,
                                question_id: {
                                    $toLong: { $arrayElemAt: [ "$matrix_string", 0 ] } // using matrix_string[0] since it is valid for both m.c. and tile choice.
                                },
                                user_answer: { // slice out pos 0 which is the
                                    $slice: [ "$matrix_string", 1, { $subtract: [ { $size: "$matrix_string" }, 1 ] } ]
                                }
                            }
                        },
                        {
                            $unwind: {
                                path: "$user_answer",
                                preserveNullAndEmptyArrays: true // shouldn't be empty, but just in case
                            }
                        }*/
                    ],
                    as: "user_data"
                }
            },
            {
                $group: {
                    _id: "$_id",
                    user: {
                        $first: "$user"
                    },
                    score: {
                        $first: "$score"
                    },
                    preferences: {
                        $first: "$preferences"
                    },
                    user_data: {
                        $first: "$user_data"
                    },
                    matched_users: {
                        $first: "$matched_users"
                    },
                }
            },
            /*{ // remove the container array
                $unwind: {
                    path: "$user_data",
                    preserveNullAndEmptyArrays: true // preserve user with no user_data_1 response yet
                },
            },*/
            {
                $project: { // format returned data
                    first_name: "$user.first_name",
                    last_name: "$user.last_name",
                    //user_data_1: "$user_data.user_data_1",
                    avatar: "$user.avatar",
                    score: 1,
                    preferences: 1,
                    user_data: 1,
                    // matching_config: 1, // for debugging purpose
                    matched_users: 1,
                    //remove_user: 1 // for debugging purpose
                }
            }
        ]);
        for (let user of matched_users) {
            user.preferences.forEach((preference) => {
                preference.answers = [];
                componentIndex = preference.resource.matrix_number[2].indexOf(preference.response.question_id);
                if (componentIndex > -1) { // it is possible the question has been deleted
                    preference.question = preference.resource['en-US'].matrix_string[componentIndex][1];
                    preference.response.selections.forEach((selection) => {
                        if (selection > 1000000) { // for tile choice, whose choice is stored as timestamp in the response
                            const index = preference.moment.matrix_number[componentIndex].indexOf(selection) - 5; // get the label index by splicing the first 5 index positions which are settings
                            if (index > -1) {
                                preference.answers.push(preference.moment.matrix_string[componentIndex][index]); // push the answer label to the array
                            }
                        } else { // for multiple choice, whose choice is stored as answer index (0, 1, 2, etc)
                            if (preference.moment.matrix_string[componentIndex].length > selection) {
                                preference.answers.push(preference.moment.matrix_string[componentIndex][selection]);
                            }
                        }
                    });
                }
                delete preference.resource;
                delete preference.moment;
                delete preference.response;
            });
            user.user_data.forEach((data) => {
                if (data.question_id === 1570150762856667) {
                    user.user_data_1 = data.user_answer;
                }
                onboarding_processes.forEach((process) => {
                    componentIndex = process.resource.matrix_number[2].indexOf(data.question_id);
                    if (componentIndex > -1) { // it is possible the question has been deleted
                        data.question = process.matrix_string[componentIndex][0];
                    }
                });
                program.resource.matrix_number[0].forEach((componentId, i) => {
                    if (componentId === 50000) {
                        program.matrix_number[i].forEach((question_id, j) => {
                            if (question_id === data.question_id) {
                                if (j >= 11 && j <= 13) { // question id stored in location 11
                                    user.title = data.user_answer;
                                } else if (j >= 14 && j <= 16) { // question id stored in location 14 - 16
                                    user.bio = data.user_answer;
                                }
                            }
                        })
                    }
                })
            })
        }
        res.json(matched_users);
    } catch (err) {
        next({topic: 'System Error', title: 'computeMatchingUsers', err: err});
    }
};

/**
 * @desc Querying nearby users (obsolete as not using map at this point)
 * @memberof moment
 * @param req
 * @param res
 * @param next
 * @returns {Promise<void>}
 */
exports.loadNearbyPeople = async (req, res, next) => {
    try {
        const itemsPerPage = 10;
        const pageNum = req.query.page || 1;
        const types = req.query.type && req.query.type.length ? req.query.type.split(' ') : [];
        if (req.query.lat > 85) req.query.lat = 85;
        if (req.query.lat < -85) req.query.lat = -85;
        if (req.query.lng > 180) req.query.lng = 180;
        if (req.query.lng < -180) req.query.lng = -180;
        let query = {
            kind: {$in: types},
            metadata: {$regex: req.query.keyword || "", $options: 'i'},
        };
        if (req.query.lat && req.query.lng) {
            query["location.geo"] = {
                $nearSphere: {
                    $geometry: {
                        type: "Point", coordinates: [req.query.lng, req.query.lat]
                    }, $maxDistance: req.query.radius * 1609.34 // in meters
                }
            };
        }
        results = await GeoSpatial.find(query)
            .skip(itemsPerPage * (pageNum - 1))
            .limit(itemsPerPage);
        res.json(results);
    } catch (err) {
        next({topic: 'System Error', title: 'loadPublicActivities', err: err});
    }
};

/** 
 * @desc for querying activity by category
 * @memberof moment
 * @param req
 * @param res
 * @param next
 * @returns {Promise<void>}
 * last worked on by Calvin Ho on 3/20/2019
 */
exports.loadPublicActivityByCategory = async (req, res, next) => {
    try {
        let itemsPerPage = 10;
        let pageNum = req.query.pageNum || 1;
        query = { 'array_boolean.0': true, deletedAt: { $exists: false } };
        if (req.params.category !== 'all') {
            query.categories = req.params.category;
        }
        let moment = await Moment.find(query) //search public activity
            .populate({
                path: 'resource',
                //select: 'matrix_number ' + req.query.language || 'en-US'
            })
            .populate({
                path: 'categories'
            })
            .populate({
                path: 'calendar'
            })
            .populate({
                path: 'array_community',
                select: 'name'
            })
            .populate({ //for Activity that has a plan
                path: 'array_moment',
                populate: [{
                    path: 'calendar'
                }, {
                    path: 'resource'
                }]
            })
            .populate({
                path: 'author',
                select: 'first_name last_name avatar'
            })
            .skip(itemsPerPage * ( pageNum - 1 ))
            .sort('-updatedAt')
            .limit(itemsPerPage);
        res.json(moment);
    } catch (err) {
        console.log(err);
        next({topic: 'System Error', title: 'loadPublicMomentByCategory', err: err});
    }
};

/**
 * @desc Load an Activity
 * @memberof moment
 * @param req
 * @param res
 * @param next
 * @param req.params.momentId (required): the Activity of interest (type: ObjectId)
 * @returns {Object} Object of the Moment
 */
exports.loadMoment = async (req, res, next) => {
    try {
        let moment;
        if (req.params.momentId && req.params.momentId.length && req.params.momentId.length === 24) {
            let query = { _id: req.params.momentId };
            if (!['owner','admin','staff'].includes(req.user.role)) { // only show deleted Moment to Restvo staff
                query.deletedAt = { $exists: false }
            }
            moment = await Moment.findOne(query)
                .populate({
                    path: 'resource'
                })
                .populate({
                    path: 'categories'
                })
                .populate({
                    path: 'calendar'
                })
                .populate({
                    path: 'user_list_1',
                    select: 'first_name last_name avatar'
                })
                .populate({
                    path: 'user_list_2',
                    select: 'first_name last_name avatar'
                })
                .populate({
                    path: 'user_list_3',
                    select: 'first_name last_name avatar'
                })
                .populate({
                    path: 'array_community',
                    select: 'name'
                })
                .populate({ //for Activity that has a plan
                    path: 'array_moment',
                    populate: [{
                        path: 'calendar'
                    }, {
                        path: 'resource'
                    }]
                })
                .populate({ //for Activity that has parent and grandparent Programs
                    path: 'parent_programs',
                    select: 'user_list_2 parent_programs matrix_string resource categories',
                    populate: [{
                        path: 'parent_programs',
                        select: 'user_list_2 parent_programs matrix_string resource categories',
                        populate: {
                            path: 'resource',
                            select: 'en-US'
                        }
                    },
                    {
                        path: 'resource',
                        select: 'en-US'
                    }]
                });
            if (!moment) {
                return res.status(401).json({})
            }
            // if user has organizer's privilege or super admin privileges, keep access tokens
            if (moment && moment.user_list_2 && moment.user_list_2.map((c) => c._id).includes(req.user._id)) {
                // keep access tokens
            } else if (moment && moment.parent_programs && moment.parent_programs.length && moment.parent_programs[0].user_list_2 && moment.parent_programs[0].user_list_2.includes(req.user._id)) {
                // keep access tokens
            } else if (moment && moment.parent_programs && moment.parent_programs.length && moment.parent_programs[0].parent_programs && moment.parent_programs[0].parent_programs.length && moment.parent_programs[0].parent_programs[0] && moment.parent_programs[0].parent_programs[0].user_list_2 && moment.parent_programs[0].parent_programs[0].user_list_2.includes(req.user._id)) {
                // keep access tokens
            } else if (moment && moment.access_tokens) { // if no organizer's or super admin access, delete access tokens
                moment.access_tokens = [];
            }
            if (moment && moment.resource.matrix_number.length === 3) { // for backward compatibility before implementing Tabs
                moment.resource.matrix_number[3] = Array(moment.resource.matrix_number[0].length);
            }
            res.json(moment);
            /*
                log systemlog when user is loading an Activity
             */
            if (moment && moment._id && moment.matrix_string && moment.array_boolean) {
                let parent_programs = []; // array of parent program ids
                if (moment.parent_programs && moment.parent_programs.length) {
                    moment.parent_programs.forEach((program) => {
                        parent_programs.push(program._id); // already populated so need to grab ids
                        if (program.parent_programs && program.parent_programs.length) {
                            parent_programs.push(...program.parent_programs.map((c) => c._id)); // already populated so need to grab ids
                        }
                    })
                }
                await SystemlogController.logActivity({
                    topic: 'Load Activity',
                    user: req.user._id
                }, {
                    activity: moment._id,
                    parent_programs: parent_programs,
                    categories: moment.categories
                });
            }
        } else {
            return res.status(401).json({})
        }
    } catch (err){
        next({topic: 'Mongodb Error', title: 'loadMoment', err: err});
    }
};

/**
 * @desc load an Activity without authentication
 * @memberof moment
 * @param req
 * @param res
 * @param next
 * @param req.params.momentId (required): the Moment (type ObjectId) of interest
 * @returns {Moment} an object of the Moment
 */
exports.loadPublicMoment = async (req, res, next) => {
    try{
        let moment = await Moment.findOne({ _id: req.params.momentId, deletedAt: { $exists: false } }, { author: 0, conversation: 0, user_list_1: 0, access_tokens: 0 })
            .populate({
                path: 'resource'
            })
            .populate({
                path: 'categories'
            })
            .populate({
                path: 'calendar'
            })
            .populate({
                path: 'user_list_2',
                select: 'first_name last_name avatar'
            })
            .populate({
                path: 'user_list_3',
                select: 'first_name last_name avatar'
            })
            .populate({
                path: 'array_community',
                select: 'name'
            })
            .populate({ //for Activity that has a plan
                path: 'array_moment',
                populate: [{
                    path: 'calendar'
                }, {
                    path: 'resource'
                }]
            });
        res.json(moment);
    } catch (err){
        next({topic: 'Mongodb Error', title: 'loadPublicMoment', err: err});
    }
};

/**
 * @desc Create Activity
 * @memberof moment
 * @param req
 * @param res
 * @param next
 * @param req.body: an object of the Activity to be created
 * @returns {Promise<void>}
 *
 * last updated by Calvin Ho on Nov 21
 */

exports.createMoment = async (req, res, next) => {
    try {
        const createdMoment = await exports.create(req.body, req.user, null);
        res.json(createdMoment);
    } catch (err){
        next({topic: 'System Error', title: 'createMoment', err: err});
    }
};

/** 
 * @desc Create Activity
 * @memberof moment
 * @param moment: the Activity object
 * @param user: the User object
 * @param optOutReason
 * null - join as participant and organizer
 * 'admin' - an organizer cloning a sample or an Activity for another leader, therefore no need to join as participant and organizer
 * 'staff' - Restvo staff. same as 'admin' + append a timestamp to the cloned title

 * @returns {Activity} an object of the created Activity
 *
 * last updated by Calvin Ho on 2/25/20
 */

exports.create = async (moment, user, optOutReason) => {
    try {
        // depopulate resource
        if (moment.resource && moment.resource._id) {
            moment.resource = moment.resource._id;
        }
        //console.log("body", moment);
        // if creating an onboarding activity, check permission first
        if (moment.program && !await ResourceController.checkMomentPermission(moment.program, 'user_list_2', null, 'create moment', user, null)) {
            return res.status(401).json("create onboarding activity permission denied");
        }
        let createdCalendarEvent = {};
        // in the case if calendar info is already populated, create a calendar document and send the _id to moment creation
        if (moment.calendar && moment.calendar.title && moment.calendar.startDate) {
            delete moment.calendar._id;
            delete moment.calendar.users;
            createdCalendarEvent = await Calendar.create(moment.calendar);
            // if no opt out reason, or if an admin is adding a child Program to a Community, add user to calendar
            if (!optOutReason || (optOutReason === 'admin' && moment.categories.includes('5c915475e172e4e64590e348'))) {
                await Calendar.updateOne({_id: createdCalendarEvent._id}, {$addToSet: {users: mongoose.Types.ObjectId(user._id)}});
            }
            moment.calendar = createdCalendarEvent._id; // depopulate the calendar object
        } else if (typeof moment.calendar === 'string') {
            const result = await Calendar.findById(moment.calendar);
            if (result) {
                let cachedCalendarEvent = JSON.parse(JSON.stringify(result));
                delete cachedCalendarEvent._id;
                delete cachedCalendarEvent.users;
                createdCalendarEvent = await Calendar.create(cachedCalendarEvent);
                // if no opt out reason, or if an admin is adding a child Program to a Community, add user to calendar
                if (!optOutReason || (optOutReason === 'admin' && moment.categories.includes('5c915475e172e4e64590e348'))) {
                    await Calendar.updateOne({_id: createdCalendarEvent._id}, {$addToSet: {users: mongoose.Types.ObjectId(user._id)}});
                }
                moment.calendar = createdCalendarEvent._id; //depopulate the calendar object
            }
        } else { // if calendar is not set up properly, remove the calendar field
            delete moment.calendar;
        }
        if (optOutReason) { // if opting out, do not join conversations
            //new conversations need to be created
            const conversation = new Conversation({ type: "moment"});
            newConversation = await conversation.save();
            moment.conversation = mongoose.Types.ObjectId(newConversation._id);

            const conversation_2 = new Conversation({ type: "moment"});
            newConversation_2 = await conversation_2.save();
            moment.conversation_2 = mongoose.Types.ObjectId(newConversation_2._id);

            const conversation_3 = new Conversation({ type: "moment"});
            newConversation_3 = await conversation_3.save();
            moment.conversation_3 = mongoose.Types.ObjectId(newConversation_3._id);
        } else { // if no opt out reason, user joins conversations
            //new conversation need to be created with just the user in it and will be associated with the moment
            const conversation = new Conversation({ participants: [user._id], userBadges: [{_id: user._id, unreadConversationBadgeCount: 0}], pushNotifications: [{_id: user._id, preference: 'all'}], type: "moment"});
            newConversation = await conversation.save();
            moment.conversation = mongoose.Types.ObjectId(newConversation._id);

            const conversation_2 = new Conversation({ participants: [user._id], userBadges: [{_id: user._id, unreadConversationBadgeCount: 0}], pushNotifications: [{_id: user._id, preference: 'all'}], type: "moment"});
            newConversation_2 = await conversation_2.save();
            moment.conversation_2 = mongoose.Types.ObjectId(newConversation_2._id);

            const conversation_3 = new Conversation({ participants: [user._id], userBadges: [{_id: user._id, unreadConversationBadgeCount: 0}], pushNotifications: [{_id: user._id, preference: 'all'}], type: "moment"});
            newConversation_3 = await conversation_3.save();
            moment.conversation_3 = mongoose.Types.ObjectId(newConversation_3._id);
        }
        moment.access_tokens = [randtoken.generate(10), randtoken.generate(10), randtoken.generate(10)]; // for access control
        if (optOutReason) { // if opting out, reset all user lists
            moment.user_list_1 = [];
            moment.user_list_2 = [];
            moment.user_list_3 = [];
        } else { // if no opt out reason, add user to the organizer's lists
            moment.user_list_1 = [];
            moment.user_list_2 = [mongoose.Types.ObjectId(user._id)];
            moment.user_list_3 = [];
        }
        let createdMoment = await Moment.create(moment);
        //reset user list 1 (attendees) and list 2 (organizers) to be just the user, list 3 (leaders) to empty
        if (createdCalendarEvent && createdCalendarEvent._id){
            await Calendar.updateOne({ _id: createdCalendarEvent._id }, { $set: { moment: mongoose.Types.ObjectId(createdMoment._id) }});
        }
        //add moment ID into the newly created conversations
        await Conversation.updateOne({ _id: newConversation._id }, { $set: { moment: mongoose.Types.ObjectId(createdMoment._id) }});
        await Conversation.updateOne({ _id: newConversation_2._id }, { $set: { moment: mongoose.Types.ObjectId(createdMoment._id) }});
        await Conversation.updateOne({ _id: newConversation_3._id }, { $set: { moment: mongoose.Types.ObjectId(createdMoment._id) }});
        return createdMoment;
    } catch (err) {
        return SystemlogController.logFunctionError({topic: 'System Error', title: 'create failed', err: err});
    }
};

/** 
 * @desc Clone Activity
 * @memberof moment
 * @param req
 * @param res
 * @param next
 *
 * 1) clone 1 level of associated Plans and their onboarding processes,
 * 2) clone the Activity
 *
 * optOutReason:
 * null - join as participant and organizer
 * 'admin' - an admin cloning a sample or an Activity for another leader, therefore no need to join as participant and organizer
 * 'staff' - Restvo staff. same as 'admin' + append a timestamp to the cloned title
 *
 * last updated by Calvin Ho on Dec 4
 * @returns {Promise<void>}
 */
exports.cloneMoments = async (req, res, next) => {
    try {
        const optOutReason = req.body.optOutReason; 
        const moments = req.body.moments || [];
        const clonedMoments = [];
        const promises1 = moments.map( async (originalParent) => {
            if (optOutReason === 'staff') { // if cloning by Restvo staff, add the time stamp after the name
                originalParent.matrix_string[0][0] += ' ' + new Date().getFullYear().toString() + (new Date().getMonth() + 1).toString() + new Date().getDate().toString() + new Date().getSeconds().toString();
            }
            let cachedParent = JSON.parse(JSON.stringify(originalParent));
            delete cachedParent._id;
            delete cachedParent.updatedAt;
            delete cachedParent.createdAt;
            if (cachedParent.array_boolean && cachedParent.array_boolean.length > 1) {
                cachedParent.array_boolean[0] = null; // turn off show in discovery
                cachedParent.array_boolean[1] = null; // turn off share to Marketplace
            }
            let clonedParent = await exports.create(cachedParent, req.user, optOutReason);
            // if the clonedParent is a Journey or a Relationship, also adopt its schedules
            if (clonedParent && clonedParent.categories && clonedParent.categories.length && (clonedParent.categories.includes('5e9f46e1c8bf1a622fec69d5') || clonedParent.categories.includes('5dfdbb547b00ea76b75e5a70'))) {
                // about permission: optOutOption === 'admin' should still be able to adopt the Plan even though the Program and the Relationship levels have no user_list_2 assigned yet. Since the permission checking should check for the super admin of a Community which is 2 levels up from the relationship level.
                await ScheduleController.adopt({
                    operation: 'adopt plan',
                    startDate: originalParent.startDate ? new Date(originalParent.startDate) : new Date(), // convert to date object
                    planIds: [originalParent._id], // the journey or relationship that provide the schedules to be adopted
                    parent_programId: clonedParent._id // the journey or relationship that receives the schedules of the plan
                }, req.user);
            }
            // clone the Moment's Children.
            await exports.cloneChildren(originalParent, clonedParent, req.user, optOutReason);
            // Note that it is not necessary to return the children (for updating the array_moments if it was modified), since the web app only uses the Parent Moment's name, assets for display purposes and not populated array_moments
            clonedMoments.push(clonedParent);
        });
        await Promise.all(promises1);
        res.json(clonedMoments);
    } catch (err) {
        next({topic: 'System Error', title: 'cloneMoments', err: err});
    }
};

/** 
 * @desc Clone an Activity with its onboarding processes
 * @memberof moment
 * @param originalParent: the original Activity to be cloned
 * @param clonedParent: the cloned Activity
 * @param user: the User object
 * @param optOutReason
 * null - join as participant and organizer
 * 'admin' - an organizer cloning a sample or an Activity for another leader, therefore no need to join as participant and organizer
 * 'staff' - Restvo staff. same as 'admin' + append a timestamp to the cloned title
 * @returns {Void}
 * last updated by Calvin Ho on 1/9/20
 */
exports.cloneChildren = async (originalParent, clonedParent, user, optOutReason) => {
    try {
        const onboardingProcesses = await Moment.find({
            program: originalParent._id,
            deletedAt: { $exists: false }
        });
        // clone onboarding process, which is a type of child Activity
        let promises = onboardingProcesses.map( async (onboardingProcess) => {
            let cachedProcess = JSON.parse(JSON.stringify(onboardingProcess));
            delete cachedProcess._id;
            delete cachedProcess.updatedAt;
            delete cachedProcess.createdAt;
            if (cachedProcess.array_boolean && cachedProcess.array_boolean.length > 1) {
                cachedProcess.array_boolean[1] = null; // turn off share to Marketplace
            }
            cachedProcess.program = clonedParent._id;
            console.log("cloning process", cachedProcess.matrix_string[0][0]);
            clonedProcess = await exports.create(cachedProcess, user, optOutReason);
            //console.log("cloned process", clonedProcess, clonedParent._id);
        });
        await Promise.all(promises);
        const childActivities = await Moment.find({
            "array_boolean.1": true, // only clone child Activity that are marked as listed in Marketplace. i.e. Sample Program is cloned with only those relationships listed as 'shared in marketplace' and not the normal relationships
            parent_programs: originalParent._id,
            categories: { $nin: [ mongoose.Types.ObjectId('5c915476e172e4e64590e349'), mongoose.Types.ObjectId('5e1bbda67b00ea76b75e5a73') ]},  // not a Plan (excluding Plan which is an obsolete data type) or a Content
            deletedAt: { $exists: false }
        });
        promises = childActivities.map( async (childActivity) => {
            let cachedChildActivity = JSON.parse(JSON.stringify(childActivity));
            delete cachedChildActivity._id;
            delete cachedChildActivity.updatedAt;
            delete cachedChildActivity.createdAt;
            // if the child Activity is a Program, turn off share to Marketplace. (i.e. relationship can keep its 'share in marketplace' status)
            if (cachedChildActivity.categories.includes('5c915475e172e4e64590e348') && cachedChildActivity.array_boolean && cachedChildActivity.array_boolean.length > 1) {
                cachedChildActivity.array_boolean[1] = null; // turn off share to Marketplace
            }
            cachedChildActivity.parent_programs = [clonedParent._id];
            console.log("cloning Child Activity", cachedChildActivity.matrix_string[0][0]);
            clonedChildActivity = await exports.create(cachedChildActivity, user, optOutReason);

            // only add the cloned program to the list of Referenced Program if it is hosted by a Community
            if (clonedParent.categories.includes('5c915324e172e4e64590e346') && clonedChildActivity.categories.includes('5c915475e172e4e64590e348')) {
                //console.log("cloned Activity", clonedChildActivity, clonedParent._id);
                await Moment.updateOne({ _id: clonedParent._id }, { $addToSet: { array_moments: clonedChildActivity._id }} );
            }
            // if the cloned activity is a sample Journey or sample Relationship, adopt its schedules
            if (clonedChildActivity && clonedChildActivity.categories && clonedChildActivity.categories.length && (clonedChildActivity.categories.includes('5e9f46e1c8bf1a622fec69d5') || clonedChildActivity.categories.includes('5dfdbb547b00ea76b75e5a70'))) {
                // about permission: optOutOption === 'admin' should still be able to adopt the Plan even though the Program and the Relationship levels have no user_list_2 assigned yet. Since the permission checking should check for the super admin of a Community which is 2 levels up from the relationship level.
                await ScheduleController.adopt({
                    operation: 'adopt plan',
                    planIds: [childActivity._id], // the journey or relationship that provide the schedules to be adopted
                    parent_programId: clonedChildActivity._id // the journey or relationship that receives the schedules of the plan
                }, user);
            }
            await exports.cloneChildren(childActivity, clonedChildActivity, user, optOutReason);
        });
        await Promise.all(promises);
    } catch (err) {
        return SystemlogController.logFunctionError({topic: 'System Error', title: 'clone failed', err: err});
    }
};

/** 
 * @desc Update an Activity
 * @memberof moment
 * @param req
 * @param req.body: Activity to be updated
 * @param res
 * @param next
 * @returns {String} "success"
 * // last updated by Calvin Ho on Oct 6
 */
exports.updateMoment = async (req, res, next) => {
    try {
        // if updating onboarding activity, check program organizer permission
        if (req.body.program && !await ResourceController.checkMomentPermission(req.body.program, 'user_list_2', null, 'update moment', req.user, null)) {
            return res.status(401).json({msg: 'update onboarding activity permission denied'});
            // if updating Restvo activity, check organizer's permission
        } else if (!req.body.program && !await ResourceController.checkMomentPermission(req.body._id, 'user_list_2', null, 'update moment', req.user, null)) {
            return res.status(401).json({msg: 'update activity permission denied'});

        }
        //console.log("Moment ID is: " + req.body._id);
        if (req.body.calendar && req.body.calendar.title && req.body.calendar.startDate) {
            req.body.calendar.moment = req.body._id; // store the Moment _id
            if (req.body.matrix_string && req.body.matrix_string.length && req.body.matrix_string[0] && req.body.matrix_string[0].length && req.body.matrix_string[0][0]) {
                req.body.calendar.title = req.body.matrix_string[0][0];
            }
            if (!req.body.calendar._id) {
                if (req.body.calendar.users && !req.body.calendar.users.length) {
                    delete req.body.calendar.users;
                }
                createdCalendarEvent = await Calendar.create(req.body.calendar);
                await Calendar.updateOne({_id: createdCalendarEvent._id}, { $addToSet: { users: req.user._id }});
                calendarId = createdCalendarEvent._id;
            } else {
                calendarId = req.body.calendar._id;
                await Calendar.updateOne({ _id: calendarId }, {$set: req.body.calendar });
            }
            if (req.body.calendar.options && req.body.calendar.options.reminders && req.body.calendar.options.reminders.length === 0){
                //if there is no reminder, unset the first and second reminder field
                await Calendar.updateOne({ _id: calendarId }, {$unset: { "options.firstReminderMinutes": "", "options.secondReminderMinutes": "" }});
            }
            else if (req.body.calendar.options && req.body.calendar.options.reminders && req.body.calendar.options.reminders.length === 1) {
                await Calendar.updateOne({ _id: calendarId }, {$unset: { "options.secondReminderMinutes": "" }});
            }
            req.body.calendar = calendarId;
        }
        req.body.updatedAt = new Date(); // Moment model timestamp is disabled. There we need to manually update the timestamp. This is performed only when updating moment eg. Event, not when users RSVP in updateMomentUserLists().
        if (!req.body.access_tokens || req.body.access_tokens.length < 3) { // if access tokens is missing, add it to the backend
            req.body.access_tokens = [randtoken.generate(10), randtoken.generate(10), randtoken.generate(10)];
        }
        // refresh chat updatedAt so it refreshes the list of chat view for user
        const moment = await Moment.findOneAndUpdate({_id: req.body._id}, {$set: req.body});
        if (moment.conversation) {
            await Conversation.updateOne({_id: moment.conversation}, {$set:{updatedAt: new Date()}});
        }
        if (moment.conversation_2) {
            await Conversation.updateOne({_id: moment.conversation_2}, {$set:{updatedAt: new Date()}});
        }
        if (moment.conversation_3) {
            await Conversation.updateOne({_id: moment.conversation_3}, {$set:{updatedAt: new Date()}});
        }
        res.json("success");
    } catch (err){
        next({topic: 'System Error', title: 'updateMoment', err: err});
    }
};

/** 
 * @desc Delete an Activity
 * @memberof moment
 * @param req
 * @param req.params.momentId: the Activity (type: ObjectId) of interest
 * @param res
 * @param next
 * @returns {Promise<*>}
 */
// last updated by Calvin Ho on Oct 6
exports.deleteMoment = async (req, res, next) => {
    try {
        if (req.params.momentId !== '5d5785b462489003817fee18') { // safeguard against deleting Restvo Community
            const moment = await Moment.findById(req.params.momentId);
            // if deleting an onboarding activity, check program organizer permission
            if (moment.program && !await ResourceController.checkMomentPermission(moment.program, 'user_list_2', null, 'delete moment', req.user, null)) {
                return res.status(401).json("delete onboarding activity permission denied");
                // if deleting a moment, check permission
            } else if (!moment.program && !await ResourceController.checkMomentPermission(moment._id, 'user_list_2', null, 'delete moment', req.user, null)) {
                return res.status(401).json("delete activity permission denied");
            }
            console.log("about to delete Moment w/ ID: " + req.params.momentId);
            if (['owner','admin','staff'].includes(req.user.role) && !req.query.archive) { //only a Restvo staff has permission to delete the event
                await Calendar.deleteOne({_id: moment.calendar});
                await Conversation.deleteOne({_id: moment.conversation});
                await Conversation.deleteOne({_id: moment.conversation_2});
                await Conversation.deleteOne({_id: moment.conversation_3});
                await GeoSpatial.deleteMany({ moment: req.params.momentId });

                // if it has onboarding activities, delete them and the Geospatial records
                const onboarding_processes = await Moment.find({ program: req.params.momentId }, { _id: 1 });
                if (onboarding_processes && onboarding_processes.length) {
                    await GeoSpatial.deleteMany({ moment: { $in: onboarding_processes.map((c) => c._id) } });
                }
                await Moment.deleteMany({ program: req.params.momentId });

                // if Content, remove depending Content Calendar docs, remove Content from schedules, and remove responses to Content Calendars
                await Calendar.deleteMany({ moment: req.params.momentId }); // remove depending Content Calendar docs
                await Calendar.updateMany({ child_moments: req.params.momentId }, { $pull: { child_moments: req.params.momentId }}); // remove Content from schedules
                await Response.deleteMany({ relationship: req.params.momentId });

                // if it has schedules (i.e. it is a Relationship), delete the schedules and their content calendar items
                const schedules = await Calendar.find({ parent_moments: req.params.momentId }, { _id: 1 });
                if (schedules && schedules.length) {
                    await Calendar.deleteMany({ parent_moments: req.params.momentId });
                    await Calendar.deleteMany({ schedule: { $in: schedules.map((c) => c._id) } });
                }

                // delete all responses to the Relationship (To-Dos, Goals, text answer etc)
                await Response.deleteMany({ moment: req.params.momentId });
                await Moment.deleteOne({ _id: req.params.momentId });
            } else { // an organizer, or staff with archive param. then archive Moment and clear participant lists
                await Moment.updateOne({ _id: req.params.momentId }, {
                    $set: {
                        deletedAt: new Date()
                    }
                });
                // remove users from content calendar of relationships
                await Calendar.updateMany({ moment: req.params.momentId }, { $unset: { users: '' }}); // remove depending Content Calendar docs
            }
        }
        res.json("success");
    } catch (err){
        next({topic: 'System Error', title: 'deleteMoment', err: err});
    }
};

/**
 * @desc update an Activity's users lists
 * @memberof moment
 * @param req
 * @param res
 * @param next
 *
 * @param req.body.operation: operation
 * @param req.body.user_lists: array of list names: ["user_list_1", "user_list_2"] etc.
 * @param req.body.users: array of user IDs or null to be added to the user lists
 *   if null, req.body.conversationId: participants in a chat room to be added to the user lists
 *       if null, req.body.conversations: list of conversations whose participants to be added to the user lists
 * @param req.body.momentId: activity ID
 * @param req.body.calendarId: calendar ID
 *
 * @returns {Promise<void>}
 *
 * Modified by Calvin Ho: 9/5/2019
 */
exports.updateMomentUserLists = async (req, res, next) => {
    try {
        console.log("input body is: " + JSON.stringify(req.body));

        let userObjectIds = [];
        let conversation = {};
        //process req.body.users
        if(req.body.users && req.body.users.length) {
            userObjectIds = req.body.users.map((c) => mongoose.Types.ObjectId(c));
        } else if (req.body.conversationId){
            conversation = await Conversation.findById(req.body.conversationId).select('participants');
            userObjectIds = conversation.participants;
        } else if (req.body.conversations) {
            const promises = req.body.conversations.map( async (conversationId) => {
                conversation = await Conversation.findById(conversationId).select('participants');
                userObjectIds.push(...conversation.participants);
                return conversation.participants;
            });
            await Promise.all(promises);
            console.log("result", userObjectIds);
        }
        if (userObjectIds && userObjectIds.length){
            if (req.body.operation === 'add to lists and calendar') { //add users to lists
                let result = 'success';
                const promises = req.body.user_lists.map( async (user_list) => {
                    if (await ResourceController.checkMomentPermission(req.body.momentId, user_list, userObjectIds, 'add to list', req.user, req.query.token)) {
                        let updateObject = {};
                        updateObject[user_list] = { $each: userObjectIds };
                        let moment = await Moment.findOneAndUpdate({_id: req.body.momentId}, {$addToSet: updateObject})
                            .populate('conversation conversation_2 conversation_3')
                            .populate({
                                path: 'user_list_2',
                                select: 'deviceTokens snoozed pushNotifySystemMessages'
                            });
                        if (user_list === 'user_list_3') { // if adding a leader, also add her to the Restvo community
                            await Moment.updateOne({_id: '5d5785b462489003817fee18'}, {$addToSet: updateObject})
                        }
                        // all user types will be joining the moment's conversation (mandatory)
                        if (moment && moment.conversation) {
                            // build the object arrays to add multiple user to the conversation at once
                            participantObjectList = [];
                            userBadgesObjectList = [];
                            pushNotificationsObjectList = [];
                            userObjectIds.forEach((userId) => {
                                if (!moment.conversation.participants.includes(userId)) {
                                    participantObjectList.push(userId);
                                    userBadgesObjectList.push({ _id : userId, unreadConversationBadgeCount: 0 });
                                    pushNotificationsObjectList.push({ _id : userId, preference: 'leaders-only' });
                                }
                            });
                            if (participantObjectList.length) {
                                await Conversation.updateOne({ _id : moment.conversation },
                                    { $addToSet: {
                                            participants : { $each: participantObjectList },
                                            userBadges : { $each: userBadgesObjectList },
                                            pushNotifications : { $each: pushNotificationsObjectList }
                                        }});
                                await Conversation.updateOne({ _id : moment.conversation },  // update the updatedAt timestamp
                                    { $set: { updatedAt: new Date() }});
                            }
                        }
                        // if joining the organizer's list, add to the organizer's list
                        if (user_list === 'user_list_2' && moment.conversation_2) {
                            // build the object arrays to add multiple user to the conversation at once
                            participantObjectList = [];
                            userBadgesObjectList = [];
                            pushNotificationsObjectList = [];
                            userObjectIds.forEach((userId) => {
                                if (!moment.conversation_2.participants.includes(userId)) {
                                    participantObjectList.push(userId);
                                    userBadgesObjectList.push({ _id : userId, unreadConversationBadgeCount: 0 });
                                    pushNotificationsObjectList.push({ _id : userId, preference: 'leaders-only' });
                                }
                            });
                            if (participantObjectList.length) {
                                await Conversation.updateOne({_id: moment.conversation_2},
                                    {
                                        $addToSet: {
                                            participants: {$each: participantObjectList},
                                            userBadges: {$each: userBadgesObjectList},
                                            pushNotifications: {$each: pushNotificationsObjectList}
                                        }
                                    });
                                await Conversation.updateOne({_id: moment.conversation_2}, // update the updatedAt timestamp
                                    {$set: {updatedAt: new Date()}});
                            }
                        }
                        // if joining the organizer's list, or joining the leader's list, add user to the leader's conversation
                        if ((user_list === 'user_list_2' && moment.conversation_2) || (user_list === 'user_list_3' && moment.conversation_3)) {
                            // build the object arrays to add multiple user to the conversation at once
                            participantObjectList = [];
                            userBadgesObjectList = [];
                            pushNotificationsObjectList = [];
                            userObjectIds.forEach((userId) => {
                                if (!moment.conversation_3.participants.includes(userId)) {
                                    participantObjectList.push(userId);
                                    userBadgesObjectList.push({ _id : userId, unreadConversationBadgeCount: 0 });
                                    pushNotificationsObjectList.push({ _id : userId, preference: 'leaders-only' });
                                }
                            });
                            if (participantObjectList.length) {
                                await Conversation.updateOne({_id: moment.conversation_3},
                                    {
                                        $addToSet: {
                                            participants: {$each: participantObjectList},
                                            userBadges: {$each: userBadgesObjectList},
                                            pushNotifications: {$each: pushNotificationsObjectList}
                                        }
                                    });
                                await Conversation.updateOne({_id: moment.conversation_3},  // update the updatedAt timestamp
                                    {$set: {updatedAt: new Date()}});
                            }
                        }
                        // in a relationship, process all schedules' content calendar items (either add or remove users from the calendars)
                        await exports.touchSchedulesContentCalendarItems(moment._id, req.body.operation, userObjectIds);
                        // notify organizers if a user actively joins this moment (except Restvo Mentor)
                        if (moment._id.toString() !== '5d5785b462489003817fee18' && userObjectIds.length && userObjectIds.length === 1 && userObjectIds[0].toString() === req.user._id.toString()) {
                            // prepare the data object
                            data = {
                                title: moment.matrix_string[0][0],
                                body: req.user.first_name + ' ' + req.user.last_name + ' has joined as ' + (user_list === 'user_list_1' ? 'Participant.' : (user_list ===  'user_list_2' ? 'Organizer.' : 'Leader.')),
                                custom: {
                                    momentId: moment._id,
                                    //featureType: moment.resource.field,
                                    notId: moment._id,
                                    BigTextStyle: true,
                                    page: 'Moment',
                                    createAt: new Date(),
                                    silence: false
                                },
                                badge: 0,
                                topic: "com.restvo.app"
                            };

                            moment.user_list_2.forEach((organizer) => {
                                userSnoozeOff = true;
                                if (organizer.snoozed) {
                                    if (organizer.snoozed.state === 'on' || (organizer.snoozed.state === 'timed' && new Date().getTime() < new Date(organizer.snoozed.expiredAt).getTime())) {
                                        userSnoozeOff = false;
                                    }
                                }
                                if (userSnoozeOff && organizer.pushNotifySystemMessages === 'instant') {
                                    push.send(organizer.deviceTokens, data, function (err, result) {
                                        if (err) {
                                            console.log(err);
                                        } else {
                                            console.log(result[0].message);
                                            UserData.removeDeviceTokens(organizer, result[0].message);
                                        }
                                    });
                                }
                            });
                            // notify Calvin when someone joins an Activity, only if Calvin is not already notified as an admin
                            if (moment.user_list_2.find((c) => c._id !== '596d4fbfdd53ac40156db4d5')) {
                                let owner = await User.findOne({_id: '596d4fbfdd53ac40156db4d5'}).select('deviceTokens appName unreadBadgeCount pushNotifySystemMessages snoozed');
                                userSnoozeOff = true;
                                if (owner.snoozed) {
                                    if (owner.snoozed.state === 'on' || (owner.snoozed.state === 'timed' && new Date().getTime() < new Date(owner.snoozed.expiredAt).getTime())) {
                                        userSnoozeOff = false;
                                    }
                                }
                                if (userSnoozeOff && owner.pushNotifySystemMessages === 'instant') {
                                    push.send(owner.deviceTokens, data, function (err, result) {
                                        if (err) {
                                            console.log(err);
                                        } else {
                                            console.log(result[0].message);
                                            UserData.removeDeviceTokens(owner, result[0].message);
                                        }
                                    });
                                }
                            }
                        }
                    } else {
                        result = 'permission denied';
                    }
                });
                await Promise.all(promises);
                if (req.body.calendarId){
                    await Calendar.updateOne({_id: req.body.calendarId}, {$addToSet: { users: { $each: userObjectIds } }});
                }
                res.json(result);
            } else if (req.body.operation === 'remove from lists') { //remove users from lists
                let result = 'success';
                const promises = req.body.user_lists.map( async (user_list) => {
                    if (await ResourceController.checkMomentPermission(req.body.momentId, user_list, userObjectIds, 'remove from list', req.user, req.query.token)) {
                        let updateObject = {};
                        updateObject[user_list] = userObjectIds; // { 'user_list_3': [ ObjectId("..."), ObjectId("...")... ] }
                        moment = await Moment.findOneAndUpdate({_id: req.body.momentId}, {$pullAll: updateObject}); // intentionally not use {new: true} to keep the old record for processing the rest of the request
                        const promises_2 = userObjectIds.map( async (userId) => {
                            let organizerOnly = false;
                            let leaderOnly = false;
                            let participantOnly = !moment.user_list_2.includes(userId) && !moment.user_list_3.includes(userId);
                            let organizerNotLeader = false;
                            // removing from the organizer chat
                            if (user_list === 'user_list_2' && moment.user_list_2.includes(userId) && moment.conversation_2) {
                                await Conversation.updateOne({ _id : moment.conversation_2 },
                                    { $pull: {  participants : userId,
                                                userBadges : { _id : userId },
                                                pushNotifications : { _id : userId }}});
                                await Conversation.updateOne({_id: moment.conversation_2 }, // update the updatedAt timestamp
                                    { $set: {updatedAt: new Date()}} );
                                organizerOnly = !moment.user_list_1.includes(userId) && !moment.user_list_3.includes(userId);
                                organizerNotLeader = !moment.user_list_3.includes(userId);
                            }
                            // removing from the leader chat
                            if (moment.conversation_3 && ((user_list === 'user_list_3' && moment.user_list_3.includes(userId)) || organizerNotLeader)) {
                                await Conversation.updateOne({ _id : moment.conversation_3 },
                                    { $pull: {  participants : userId,
                                                userBadges : { _id : userId },
                                                pushNotifications : { _id : userId }}});
                                await Conversation.updateOne({_id: moment.conversation_3 }, // update the updatedAt timestamp
                                    { $set: {updatedAt: new Date()}} );
                                leaderOnly = !moment.user_list_1.includes(userId) && !moment.user_list_2.includes(userId);
                            }
                            // removing from the participant chat
                            if ((user_list === 'user_list_1' && participantOnly) || organizerOnly || leaderOnly) {
                                await Conversation.updateOne({ _id : moment.conversation },
                                    { $pull: {  participants : userId,
                                            userBadges : { _id : userId },
                                            pushNotifications : { _id : userId }}});
                                await Conversation.updateOne({_id: moment.conversation }, // update the updatedAt timestamp
                                    { $set: {updatedAt: new Date()}} );
                            }
                        });
                        await Promise.all(promises_2);
                        // in a relationship, process all schedules' content calendar items (either add or remove users from the calendars)
                        await exports.touchSchedulesContentCalendarItems(moment._id, req.body.operation, userObjectIds);
                    } else {
                        result = 'permission denied';
                    }
                });
                await Promise.all(promises);
                res.json(result);
            } else if (req.body.operation === 'add to calendar') { //add users to lists
                if (req.body.calendarId){
                    const promises = userObjectIds.map( async (userObjectId) => {
                        await Calendar.updateOne({_id: req.body.calendarId}, {$addToSet: { users: userObjectId }});
                    });
                    await Promise.all(promises);
                }
                res.json("success");
            } else if (req.body.operation === 'remove from calendar') { //add users to lists
                if (req.body.calendarId){
                    await Calendar.updateOne({ _id: req.body.calendarId }, { $pullAll: { users : userObjectIds } });
                }
                res.json("success");
            } else {
                res.json("wrong operation");
            }
        } else {
            res.json("no user provided");
            console.log("no user provided");
        }
    } catch (err){
        next({topic: 'System Error', title: 'updateMomentUserLists', err: err});
    }
};

/** 
 * @desc add or remove users in the Activity's schedules' content calendar items
 * @memberof moment
 * @param momentId: The Relationship (type: ObjectId) which hosts the Schedules
 * @param operation: Operation (type: String)
 * @param userObjectIds: an array of Users (type: [ObjectId])
 * @returns {Void}
 *
 * updated by Calvin Ho 1/13/20
 */

exports.touchSchedulesContentCalendarItems = async (momentId, operation, userObjectIds) => {
    const schedules = await Calendar.find({ parent_moments: momentId });
    const promises = schedules.map( async (schedule) => {
        if (operation === 'add to lists and calendar') {
            await Calendar.updateMany({ schedule: schedule._id }, { $addToSet: { users: { $each: userObjectIds } }});
        } else if (operation === 'remove from lists') {
            await Calendar.updateMany({ schedule: schedule._id }, { $pullAll: { users: userObjectIds } });
        }
    });
    await Promise.all(promises);
};

/** 
 * @desc Submit various types of responses
 * @memberof moment
 * @example User's Response to Old Restvo Features (response class id = null)
 * @param req
 * @param req.body.moment:
 * @param res
 * @param next
 *
 * @returns {Promise<void>}
 *
 * @example User's Response to an Activity or Content Calendar's Interactables (response class id = null)
 *
 * @param req
 * @param req.body.moment: the Activity (type: ObjectId)
 * @param req.body.relationship (optional): the relationship context (type: ObjectId)
 * @param req.body.calendar (optional): the calendar context (type: ObjectId)
 * @param res
 * @param next
 *
 * @returns {Promise<void>}
 *
 * @example  the user's multiple choice responses to an Activity's list of questions
 *
 * @param req
 * @param req.body.moment: the Activity (type: ObjectId)
 * @param res
 * @param next
 *
 * @returns {Promise<void>}
 *
 * @example  Parent/Child Relationship Record for Onboarding Processes (response class id = null)
 *
 * @param req
 * @param req.body.class: null
 * @param req.body.dependentMomentId: the parent Onboarding process (type: ObjectId)
 * @param res
 * @param next
 * @returns {String} 'success'
 *
 * @example  an Onboarding Process choices of a parent onboarding process's question. Only responding to one question is allowed per record
 *
 * @param req
 * @param req.body.class: response class (type: Number)
 * @param req.body.dependentMomentId: the Onboarding process (type: ObjectId)
 * @param res
 * @param next
 * @returns {String} 'success'
 *
 * @example Activity's Matching Config (50000) Settings (response class id = 50000)
 * an User Matching setting that defines which Onboarding Processes, which question in it, and the logical query operator it uses when matching users' responses
 *
 * @param req
 * @param req.body.class: response class (type: Number)
 * @param req.body.dependentMomentId: the Onboarding process/question of interest (type: ObjectId)
 * @param res
 * @param next
 *
 * @returns {Promise<void>}
 */

exports.submitResponse = async (req, res, next) => {
    try {
        if (req.body.class && req.body.dependentMomentId) { // 1.3.6 (30)+. Added the class field in the body payload
            await Response.deleteMany({
                "array_number.0": req.body.class,
                dependent_moment: req.body.dependentMomentId
            });
            if (req.body.responses) {
                const promises = req.body.responses.map( async (response) => {
                    await Response.create(response);
                });
                await Promise.all(promises);
            }
            res.json("success");
        } else if (!req.body.class && req.body.dependentMomentId) { // obsolete 1.3.6 (30)+
            response = await Response.findOneAndUpdate({
                moment: req.body.moment,
                dependent_moment: req.body.dependentMomentId
            }, {
                $set: req.body
            }, {
                upsert: true,
                new: true
            });
            // clean up old responses
            responses = await Response.deleteMany({
                _id: { $ne: response._id },
                dependent_moment: req.body.dependentMomentId
            });
            res.json(response);
        } else { // this is for user submitting the responses to MC and other interactables
            let query = {
                moment: req.body.moment,
                user: req.user._id
            };
            // it filters based on relationship context, if provided. see models/response.js for definition
            if (req.body.relationship) {
                query.relationship = req.body.relationship;
            } else {
                query.relationship = { $exists: false };
            }
            // it filters based on content calendar context, if provided. see models/response.js for definition
            if (req.body.calendar) {
                query.calendar = req.body.calendar;
            } else {
                query.calendar = { $exists: false };
            }
            req.body.user = req.user._id; // ensure author is set to the current user
            delete req.body._id; // the _id is not needed since the doc is already queried
            if (req.query.version === '1') {
                const response = await Response.findOneAndUpdate(query, { $set: req.body }, {
                    upsert: true,
                    new: true
                });
                if (response && response._id) {
                    res.json({ status: 'success', _id: response._id });
                } else {
                    res.json({ status: 'failed' });
                }
            } else {
                const response = await Response.findOneAndUpdate(query, { $set: req.body }, {
                    upsert: true,
                    new: true
                })
                    .populate({
                        path: 'user',
                        select: 'first_name last_name avatar'
                    });
                res.json(response);
            }
        }
    } catch (err){
        next({topic: 'System Error', title: 'submitResponse', err: err});
    }
};

exports.deleteResponse = async (req, res, next) => {
    try {
        await Response.deleteMany({ _id: req.params.responseId });
        res.json('success');
    } catch (err){
        next({topic: 'System Error', title: 'deleteResponse', err: err});
    }
};

/** 
 * @desc Load Responses by Activity Id
 * @memberof moment
 * @param req
 * @param req.params.momentId: Activity of interest (type: ObjectId)
 * @param res
 * @param next
 * @returns {Array} an array of responses
 * updated by Calvin Ho on 2/12/20
 */
exports.findResponsesByMomentId = async (req, res, next) => {
    try {
        // first check for organizer's access and if containing public interactable (30000 - 39999)
        const moment = await Moment.findById(req.params.momentId)
            .populate('resource')
            .select('resource matrix_number categories'); // load the organizer's list

        let hasOrganizerLeaderAccess = false;
        let hasParticipantAccess = false;
        // check if it is a Content. If so, use the Relationship Object ID to check for Community or Program level admin access and Relationship level leader's access
        if (req.query.relationshipId) {
            hasOrganizerLeaderAccess = await ResourceController.checkMomentPermission(req.query.relationshipId, 'user_list_3', null, null, req.user, null);//moment.user_list_2.indexOf(req.user._id) > -1;
            hasParticipantAccess = await ResourceController.checkMomentPermission(req.query.relationshipId, 'user_list_1', null, null, req.user, null);//moment.user_list_2.indexOf(req.user._id) > -1;
        } else { // for onboarding and other Activity responses, check for organizer's access
            hasOrganizerLeaderAccess = await ResourceController.checkMomentPermission(req.params.momentId, 'user_list_2', null, null, req.user, null);//moment.user_list_2.indexOf(req.user._id) > -1;
            hasParticipantAccess = await ResourceController.checkMomentPermission(req.params.momentId, 'user_list_1', null, null, req.user, null);//moment.user_list_2.indexOf(req.user._id) > -1;
        }

        let hasPublicOrCollaborativeInteractable = false;
        if (moment && moment.resource && moment.resource.matrix_number && moment.resource.matrix_number.length) {
            hasPublicOrCollaborativeInteractable = moment.resource.matrix_number[0].find((c) => (c >= 10210 && c <= 10220) || (c >= 30000 && c <= 39999)); // moment that has a schedule or a poll
            moment.resource.matrix_number[0].forEach((item, componentIndex) => {
                if (item === 40010) {
                    hasPublicOrCollaborativeInteractable = (moment.matrix_number[componentIndex].length > 1 && moment.matrix_number[componentIndex][1]) || hasPublicOrCollaborativeInteractable; // true or keep the same value
                }
            });
        }
        // build the Mongodb query
        let query = { moment: req.params.momentId };
        // if it is not a private Note, and it has parent or current organizer's or leader's permission, or has public or collaborative interactable
        if (hasOrganizerLeaderAccess || (hasParticipantAccess && hasPublicOrCollaborativeInteractable)) {
            query.user = { $exists: true };
        } else { // only user's own response is returned
            query.user = req.user._id;
        }
        // filter if it is in the context of a relationship
        if (req.query.relationshipId) {
            query.relationship = req.query.relationshipId;
        }
        // filter if it is in the context of a content calendar
        if (req.query.calendarId) {
            query.calendar = req.query.calendarId;
        }
        const responses = await Response.find(query)
            .populate({
                path: 'user',
                select: 'first_name last_name avatar'
            })
            .sort('updatedAt');
        if (!req.query.version) {
            return res.status(200).json(responses);
        } else if (req.query.version === '1') {
            return res.status(200).json({ responses: responses, hasOrganizerLeaderAccess: hasOrganizerLeaderAccess });
        }
    } catch (err) {
        next({topic: 'Mongodb Error', title: 'findResponsesByMomentId', err: err});
    }
};

/** 
 * @desc Load Responses by an array of Activity Ids
 * @memberof moment
 * @param req
 * @param req.params.momentId: Activity of interest (type: ObjectId)
 * @param res
 * @param next
 * @returns {Array} an array of responses
 * updated by Calvin Ho on 2/12/20
 */
exports.findResponsesByMomentIds = async (req, res, next) => {
    try {
        let allResponses = [];
        async.each(req.body.array, function (momentId, callback) {
            Response.find({
                moment: momentId,
                user: { $exists: true }
            }).exec(function (err, responses) {
                if (err) {
                    return next({topic: 'Mongodb Error', title: 'findResponsesByMomentIds', err: err});
                }
                for (response of responses){
                    allResponses.push(response);
                }
                callback();
            });
        }, (err) => {
            if (err) res.send(err);
            return res.json(allResponses);
        });
    } catch (err){
        next({topic: 'System Error', title: 'findResponsesByMomentIds', err: err});
    }
};

/** 
 * @desc send Push Notification messages about Calendar Reminders
 * @memberof moment
 * @param io: socket io object
 * @param calendar: the calendar object
 * @param title: title of the Push message
 * @param body: the body of the Push message
 * @returns {Void}
 */
exports.sendRefreshNotification = async (io, calendar, title, body) => {
    try {
        let moment = calendar.moment;
        let data = {
            title: title,
            body: body,
            custom: {
                momentId: calendar.relationship ? calendar.relationship._id : moment._id, // if it is a content calendar item, open the relationship page. Otherwise, open the Moment page
                featureType: moment.resource.field,
                notId: moment._id,
                BigTextStyle: true,
                page: 'Moment',
                createAt: new Date(),
                silence: false
            },
            badge: 0,
            topic: ""
        };
        // this is for refreshing the Poll when it is due. TODO: might not be needed anymore
        socketData = {
            action: 'refresh moment',
            momentId: moment._id,
            featureType: moment.resource.field,
            title: data.title,
            body: data.body,
        };
        io.of('/').to(moment.conversation).emit('refresh status', moment.conversation, socketData);

        calendar.users.forEach(function (user) {
            //Determine if we need to send a push notification
            let userSnoozeOn;

            if (user.snoozed) {
                if (user.snoozed.state === 'on' || (user.snoozed.state === 'timed' && new Date().getTime() < new Date(user.snoozed.expiredAt).getTime())) {
                    userSnoozeOn = true;
                }
            }
            if (userSnoozeOn || (user.pushNotifySystemMessages !== 'instant')) {
                console.log("Push Notification disabled!");
                data.custom.silence = true;
                delete data.title;
                delete data.body;
            }
            data.topic = user.appName;
            data.badge = user.unreadBadgeCount;

            push.send(user.deviceTokens, data, function (err, result) {
                if (err) {
                    console.log(err);
                } else {
                    console.log(result[0].message);
                    UserData.removeDeviceTokens(user, result[0].message);
                }
            });
        });

        await Calendar.updateOne({_id: calendar._id, //set reminderSent to true
            "options.reminders": {
                $elemMatch: {
                    remindAt: {
                        $gte: new Date(new Date().getTime() - 30 * 60 * 1000), //time after 30 min ago to safeguard against accidental server downtime
                        $lte: new Date() //time earlier than now
                    },
                    reminderSent: false
                }
            }},
            {
                $set: {"options.reminders.$.reminderSent" : true}
            });
    } catch (err) {
        SystemlogController.logFunctionError({topic: 'Mongodb Error', title: 'sendRefreshNotification', err: err});
    }
};

/** Obsolete. For future reference only
 * @func loadPublicActivities
 * @memberof moment
 * @param req
 * @param res
 * @param next
 * @returns {Promise<void>}
 */
exports.loadPublicActivities = async (req, res, next) => { // api for querying live activities
    try {
        const itemsPerPage = 10;
        const pageNum = req.query.page || 1;
        if (req.query.lat > 85) req.query.lat = 85;
        if (req.query.lat < -85) req.query.lat = -85;
        if (req.query.lng > 180) req.query.lng = 180;
        if (req.query.lng < -180) req.query.lng = -180;
        let query = {
            kind: {$in: req.query.type.split(' ')},
            metadata: {$regex: req.query.keyword || "", $options: 'i'},
        };
        if (req.query.lat && req.query.lng) {
            query["location.geo"] = {
                $nearSphere: {
                    $geometry: {
                        type: "Point", coordinates: [req.query.lng, req.query.lat]
                    }, $maxDistance: req.query.radius * 1609.34 // in meters
                }
            };
        }
        if (req.query.hasOwnProperty('time') && req.query.time === '0') { // query live activities
            query.startDate = {
                $lte: new Date()
            };
            query.endDate = {
                $gte: new Date()
            };
        } else if (req.query.hasOwnProperty('time') && Math.abs(req.query.time) > 0) { // query upcoming or past activities, elapsed time in seconds
            query.startDate = {
                $gte: new Date(new Date().getTime() + req.query.time*1000)
            };
            query.endDate = {
                $gte: new Date(new Date().getTime() + req.query.time*1000)
            };
        } else { // if not queried, show all activities except expired activities
            query.endDate = {
                $gte: new Date()
            };
        }
        results = await GeoSpatial.find(query)
            .populate({
                path: 'moment',
                populate: [{
                    path: 'calendar'
                }, {
                    path: 'resource'
                }, {
                    path: 'categories'
                }, {
                    path: 'array_community',
                    select: 'name'
                }, {
                    path: 'author',
                    select: 'first_name last_name avatar'
                }]
            })
            .skip(itemsPerPage * (pageNum - 1))
            .limit(itemsPerPage);
        res.json(results);
    } catch (err) {
        next({topic: 'System Error', title: 'loadPublicActivities', err: err});
    }
};