/* jslint node: true */
'use strict';

//  ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController;
const messageArea = require('./message_area.js');
const MessageAreaConfTempSwitcher =
    require('./mod_mixins.js').MessageAreaConfTempSwitcher;
const Errors = require('./enig_error.js').Errors;
const Message = require('./message.js');
const UserProps = require('./user_property.js');

//  deps
const async = require('async');
const _ = require('lodash');
const moment = require('moment');

/*
    Available itemFormat/focusItemFormat members for |msgList|

    msgNum          : Message number
    to              : To username/handle
    from            : From username/handle
    subj            : Subject
    ts              : Message mod timestamp (format with config.dateTimeFormat)
    newIndicator    : New mark/indicator (config.newIndicator)
*/
exports.moduleInfo = {
    name: 'Message List',
    desc: 'Module for listing/browsing available messages',
    author: 'NuSkooler',
};

const FormIds = {
    allViews: 0,
    delPrompt: 1,
};

const MciViewIds = {
    allViews: {
        msgList: 1, //  VM1 - see above
        delPromptXy: 2, //  %XY2, e.g: delete confirmation
        customRangeStart: 10, //  Everything |msgList| has plus { msgNumSelected, msgNumTotal }
    },

    delPrompt: {
        prompt: 1,
    },
};

exports.getModule = class MessageListModule extends (
    MessageAreaConfTempSwitcher(MenuModule)
) {
    constructor(options) {
        super(options);

        //  :TODO: consider this pattern in base MenuModule - clean up code all over
        this.config = Object.assign(
            {},
            _.get(options, 'menuConfig.config'),
            options.extraArgs
        );

        this.lastMessageReachedExit = _.get(
            options,
            'lastMenuResult.lastMessageReached',
            false
        );

        this.menuMethods = {
            selectMessage: (formData, extraArgs, cb) => {
                if (!Array.isArray(this.config?.messageList)) {
                    this.client.log.error(
                        { formData },
                        'No message list is available to select from!'
                    );
                    return cb(null);
                }

                if (MciViewIds.allViews.msgList === formData.submitId) {
                    //  'messageIndex' or older deprecated 'message' member
                    this.initialFocusIndex = _.get(
                        formData,
                        'value.messageIndex',
                        formData.value.message
                    );

                    const modOpts = {
                        extraArgs: {
                            messageAreaTag: this.getSelectedAreaTag(
                                this.initialFocusIndex
                            ),
                            messageList: this.config.messageList,
                            messageIndex: this.initialFocusIndex,
                            lastMessageNextExit: true,
                        },
                    };

                    if (_.isBoolean(this.config.noUpdateLastReadId)) {
                        modOpts.extraArgs.noUpdateLastReadId =
                            this.config.noUpdateLastReadId;
                    }

                    //
                    //  Provide a serializer so we don't dump *huge* bits of information to the log
                    //  due to the size of |messageList|. See https://github.com/trentm/node-bunyan/issues/189
                    //
                    const self = this;
                    modOpts.extraArgs.toJSON = function () {
                        const logMsgList =
                            self.config.messageList.length <= 4
                                ? self.config.messageList
                                : self.config.messageList
                                      .slice(0, 2)
                                      .concat(self.config.messageList.slice(-2));

                        return {
                            //  note |this| is scope of toJSON()!
                            messageAreaTag: this.messageAreaTag,
                            apprevMessageList: logMsgList,
                            messageCount: this.messageList.length,
                            messageIndex: this.messageIndex,
                        };
                    };

                    return this.gotoMenu(
                        this.config.menuViewPost || 'messageAreaViewPost',
                        modOpts,
                        cb
                    );
                } else {
                    return cb(null);
                }
            },
            fullExit: (formData, extraArgs, cb) => {
                this.menuResult = { fullExit: true };
                return this.prevMenu(cb);
            },
            deleteSelected: (formData, extraArgs, cb) => {
                if (MciViewIds.allViews.msgList != formData.submitId) {
                    return cb(null);
                }

                //  newer 'messageIndex' or older deprecated value
                const messageIndex = _.get(
                    formData,
                    'value.messageIndex',
                    formData.value.message
                );
                return this.promptDeleteMessageConfirm(messageIndex, cb);
            },
            deleteMessageYes: (formData, extraArgs, cb) => {
                const msgListView = this.viewControllers.allViews.getView(
                    MciViewIds.allViews.msgList
                );
                this.enableMessageListIndexUpdates(msgListView);
                if (this.selectedMessageForDelete) {
                    this.selectedMessageForDelete.deleteMessage(this.client.user, err => {
                        if (err) {
                            this.client.log.error(
                                `Failed to delete message: ${this.selectedMessageForDelete.messageUuid}`
                            );
                        } else {
                            this.client.log.info(
                                `User deleted message: ${this.selectedMessageForDelete.messageUuid}`
                            );
                            this.config.messageList.splice(
                                msgListView.focusedItemIndex,
                                1
                            );
                            this.updateMessageNumbersAfterDelete(
                                msgListView.focusedItemIndex
                            );
                            msgListView.setItems(this.config.messageList);
                        }
                        this.selectedMessageForDelete = null;
                        msgListView.redraw();
                        this.populateCustomLabelsForSelected(
                            msgListView.focusedItemIndex
                        );
                        return cb(null);
                    });
                } else {
                    return cb(null);
                }
            },
            deleteMessageNo: (formData, extraArgs, cb) => {
                const msgListView = this.viewControllers.allViews.getView(
                    MciViewIds.allViews.msgList
                );
                this.enableMessageListIndexUpdates(msgListView);
                return cb(null);
            },
            markAllRead: (formData, extraArgs, cb) => {
                if (this.config.noUpdateLastReadId) {
                    return cb(null);
                }

                return this.markAllMessagesAsRead(cb);
            },
        };
    }

    getSelectedAreaTag(listIndex) {
        return this.config.messageList[listIndex].areaTag || this.config.messageAreaTag;
    }

    enter() {
        if (this.lastMessageReachedExit) {
            return this.prevMenu();
        }

        super.enter();

        //
        //  Config can specify |messageAreaTag| else it comes from
        //  the user's current area. If |messageList| is supplied,
        //  each item is expected to contain |areaTag|, so we use that
        //  instead in those cases.
        //
        if (!Array.isArray(this.config.messageList)) {
            if (this.config.messageAreaTag) {
                this.tempMessageConfAndAreaSwitch(this.config.messageAreaTag);
            } else {
                this.config.messageAreaTag =
                    this.client.user.properties[UserProps.MessageAreaTag];
            }
        }
    }

    leave() {
        this.tempMessageConfAndAreaRestore();
        super.leave();
    }

    populateCustomLabelsForSelected(selectedIndex) {
        const formatObj = Object.assign(
            {
                msgNumSelected: selectedIndex + 1,
                msgNumTotal: this.config.messageList.length,
            },
            this.config.messageList[selectedIndex] //  plus, all the selected message props
        );
        return this.updateCustomViewTextsWithFilter(
            'allViews',
            MciViewIds.allViews.customRangeStart,
            formatObj
        );
    }

    mciReady(mciData, cb) {
        super.mciReady(mciData, err => {
            if (err) {
                return cb(err);
            }

            const self = this;
            const vc = (self.viewControllers.allViews = new ViewController({
                client: self.client,
            }));
            let configProvidedMessageList = false;

            async.series(
                [
                    function loadFromConfig(callback) {
                        const loadOpts = {
                            callingMenu: self,
                            mciMap: mciData.menu,
                        };

                        return vc.loadFromMenuConfig(loadOpts, callback);
                    },
                    function fetchMessagesInArea(callback) {
                        //
                        //  Config can supply messages else we'll need to populate the list now
                        //
                        if (_.isArray(self.config.messageList)) {
                            configProvidedMessageList = true;
                            return callback(
                                0 === self.config.messageList.length
                                    ? new Error('No messages in area')
                                    : null
                            );
                        }

                        messageArea.getMessageListForArea(
                            self.client,
                            self.config.messageAreaTag,
                            function msgs(err, msgList) {
                                if (!msgList || 0 === msgList.length) {
                                    return callback(new Error('No messages in area'));
                                }

                                self.config.messageList = msgList;
                                return callback(err);
                            }
                        );
                    },
                    function getLastReadMessageId(callback) {
                        //  messageList entries can contain |isNew| if they want to be considered new
                        if (configProvidedMessageList) {
                            self.lastReadId = 0;
                            return callback(null);
                        }

                        messageArea.getMessageAreaLastReadId(
                            self.client.user.userId,
                            self.config.messageAreaTag,
                            function lastRead(err, lastReadId) {
                                self.lastReadId = lastReadId || 0;
                                return callback(null); //  ignore any errors, e.g. missing value
                            }
                        );
                    },
                    function updateMessageListObjects(callback) {
                        const dateTimeFormat =
                            self.menuConfig.config.dateTimeFormat ||
                            self.client.currentTheme.helpers.getDateTimeFormat();
                        const newIndicator = self.menuConfig.config.newIndicator || '*';
                        const regIndicator = ' '.repeat(newIndicator.length); //  fill with space to avoid draw issues

                        let msgNum = 1;
                        self.config.messageList.forEach((listItem, index) => {
                            listItem.msgNum = msgNum++;
                            try {
                                listItem.ts = moment(listItem.modTimestamp).format(
                                    dateTimeFormat
                                );
                            } catch (e) {
                                self.client.log.warn(
                                    `Error parsing "${listItem.modTimestamp}"; expected timestamp: ${e.message}`
                                );
                                listItem.ts = moment().format(dateTimeFormat);
                            }
                            const isNew = _.isBoolean(listItem.isNew)
                                ? listItem.isNew
                                : listItem.messageId > self.lastReadId;
                            listItem.newIndicator = isNew ? newIndicator : regIndicator;

                            if (
                                _.isUndefined(self.initialFocusIndex) &&
                                listItem.messageId > self.lastReadId
                            ) {
                                self.initialFocusIndex = index;
                            }

                            listItem.text = `${listItem.msgNum} - ${listItem.subject} from ${listItem.fromUserName}`; //  default text
                        });
                        return callback(null);
                    },
                    function populateAndDrawViews(callback) {
                        const msgListView = vc.getView(MciViewIds.allViews.msgList);
                        msgListView.setItems(self.config.messageList);
                        self.enableMessageListIndexUpdates(msgListView);

                        if (self.initialFocusIndex > 0) {
                            //  note: causes redraw()
                            msgListView.setFocusItemIndex(self.initialFocusIndex);
                        } else {
                            msgListView.redraw();
                        }

                        self.populateCustomLabelsForSelected(self.initialFocusIndex || 0);
                        return callback(null);
                    },
                ],
                err => {
                    if (err) {
                        self.client.log.error(
                            { error: err.message },
                            'Error loading message list'
                        );
                    }
                    return cb(err);
                }
            );
        });
    }

    getSaveState() {
        return { initialFocusIndex: this.initialFocusIndex };
    }

    restoreSavedState(savedState) {
        if (savedState) {
            this.initialFocusIndex = savedState.initialFocusIndex;
        }
    }

    getMenuResult() {
        return this.menuResult;
    }

    enableMessageListIndexUpdates(msgListView) {
        msgListView.on('index update', idx => this.populateCustomLabelsForSelected(idx));
    }

    markAllMessagesAsRead(cb) {
        if (!this.config.messageList || this.config.messageList.length === 0) {
            return cb(null); //  nothing to do.
        }

        //
        //  Generally we'll have a message list for a specific area,
        //  but this is not always the case. For a given area, we need
        //  to find the highest message ID in the list to set a
        //  last read pointer.
        //
        const areaHighestIds = {};
        this.config.messageList.forEach(msg => {
            const highestId = areaHighestIds[msg.areaTag];
            if (highestId) {
                if (msg.messageId > highestId) {
                    areaHighestIds[msg.areaTag] = msg.messageId;
                }
            } else {
                areaHighestIds[msg.areaTag] = msg.messageId;
            }
        });

        const regIndicator = ' '.repeat(
            (this.menuConfig.config.newIndicator || '*').length
        );
        async.forEachOf(
            areaHighestIds,
            (highestId, areaTag, nextArea) => {
                messageArea.updateMessageAreaLastReadId(
                    this.client.user.userId,
                    areaTag,
                    highestId,
                    err => {
                        if (err) {
                            this.client.log.warn(
                                { error: err.message },
                                'Failed marking area as read'
                            );
                        } else {
                            //  update newIndicator on messages
                            this.config.messageList.forEach(msg => {
                                if (areaTag === msg.areaTag) {
                                    msg.newIndicator = regIndicator;
                                }
                            });
                            const msgListView = this.viewControllers.allViews.getView(
                                MciViewIds.allViews.msgList
                            );
                            msgListView.setItems(this.config.messageList);
                            msgListView.redraw();
                            this.client.log.info(
                                { highestId, areaTag },
                                'User marked area as read'
                            );
                        }
                        return nextArea(null); //  always continue
                    }
                );
            },
            () => {
                return cb(null);
            }
        );
    }

    updateMessageNumbersAfterDelete(startIndex) {
        //  all index -= 1 from this point on.
        for (let i = startIndex; i < this.config.messageList.length; ++i) {
            const msgItem = this.config.messageList[i];
            msgItem.msgNum -= 1;
            msgItem.text = `${msgItem.msgNum} - ${msgItem.subject} from ${msgItem.fromUserName}`; //  default text
        }
    }

    promptDeleteMessageConfirm(messageIndex, cb) {
        const messageInfo = this.config.messageList[messageIndex];
        if (!_.isObject(messageInfo)) {
            return cb(Errors.Invalid(`Invalid message index: ${messageIndex}`));
        }

        //  :TODO: create static userHasDeleteRights() that takes id || uuid that doesn't require full msg load
        this.selectedMessageForDelete = new Message();
        this.selectedMessageForDelete.load({ uuid: messageInfo.messageUuid }, err => {
            if (err) {
                this.selectedMessageForDelete = null;
                return cb(err);
            }

            if (!this.selectedMessageForDelete.userHasDeleteRights(this.client.user)) {
                this.selectedMessageForDelete = null;
                return cb(
                    Errors.AccessDenied(
                        'User does not have rights to delete this message'
                    )
                );
            }

            //  user has rights to delete -- prompt/confirm then proceed
            return this.promptConfirmDelete(cb);
        });
    }

    promptConfirmDelete(cb) {
        const promptXyView = this.viewControllers.allViews.getView(
            MciViewIds.allViews.delPromptXy
        );
        if (!promptXyView) {
            return cb(
                Errors.MissingMci(
                    `Missing prompt XY${MciViewIds.allViews.delPromptXy} MCI`
                )
            );
        }

        const promptOpts = {
            clearAtSubmit: true,
        };
        if (promptXyView.dimens.width) {
            promptOpts.clearWidth = promptXyView.dimens.width;
        }

        return this.promptForInput(
            {
                formName: 'delPrompt',
                formId: FormIds.delPrompt,
                promptName:
                    this.config.deleteMessageFromListPrompt ||
                    'deleteMessageFromListPrompt',
                prevFormName: 'allViews',
                position: promptXyView.position,
            },
            promptOpts,
            err => {
                return cb(err);
            }
        );
    }
};
