/** @odoo-module **/

import { registerModel } from '@mail/model/model_core';
import { attr, many, one } from '@mail/model/model_field';
import { clear, insert } from '@mail/model/model_field_command';
import { addLink, htmlToTextContentInline, parseAndTransform } from '@mail/js/utils';

import { session } from '@web/session';

import { getLangDatetimeFormat, str_to_datetime } from 'web.time';

const { markup } = owl;

registerModel({
    name: 'Message',
    modelMethods: {
        /**
         * @param {Object} data
         * @return {Object}
         */
        convertData(data) {
            const data2 = {};
            data2.attachments = data.attachment_ids;
            if ('author' in data) {
                data2.author = data.author;
            }
            if ('body' in data) {
                data2.body = data.body;
            }
            if ('date' in data && data.date) {
                data2.date = moment(str_to_datetime(data.date));
            }
            if ('email_from' in data) {
                data2.email_from = data.email_from;
            }
            if ('guestAuthor' in data) {
                data2.guestAuthor = data.guestAuthor;
            }
            if ('history_partner_ids' in data && this.messaging.currentPartner) {
                data2.isHistory = data.history_partner_ids.includes(this.messaging.currentPartner.id);
            }
            if ('id' in data) {
                data2.id = data.id;
            }
            if ('is_discussion' in data) {
                data2.is_discussion = data.is_discussion;
            }
            if ('is_note' in data) {
                data2.is_note = data.is_note;
            }
            if ('is_notification' in data) {
                data2.is_notification = data.is_notification;
            }
            data2.linkPreviews = data.linkPreviews;
            if ('messageReactionGroups' in data) {
                data2.messageReactionGroups = data.messageReactionGroups;
            }
            if ('message_type' in data) {
                data2.message_type = data.message_type;
            }
            if ('model' in data && 'res_id' in data && data.model && data.res_id) {
                const originThreadData = {
                    id: data.res_id,
                    model: data.model,
                };
                if ('record_name' in data && data.record_name) {
                    originThreadData.name = data.record_name;
                }
                if ('res_model_name' in data && data.res_model_name) {
                    originThreadData.model_name = data.res_model_name;
                }
                if ('module_icon' in data) {
                    originThreadData.moduleIcon = data.module_icon;
                }
                data2.originThread = originThreadData;
            }
            if ('needaction_partner_ids' in data && this.messaging.currentPartner) {
                data2.isNeedaction = data.needaction_partner_ids.includes(this.messaging.currentPartner.id);
            }
            if ('notifications' in data) {
                data2.notifications = insert(data.notifications.map(notificationData =>
                    this.messaging.models['Notification'].convertData(notificationData)
                ));
            }
            if ('parentMessage' in data) {
                if (!data.parentMessage) {
                    data2.parentMessage = clear();
                } else {
                    data2.parentMessage = this.convertData(data.parentMessage);
                }
            }
            if ('recipients' in data) {
                data2.recipients = data.recipients;
            }
            if ('starred_partner_ids' in data && this.messaging.currentPartner) {
                data2.isStarred = data.starred_partner_ids.includes(this.messaging.currentPartner.id);
            }
            if ('subject' in data) {
                data2.subject = data.subject;
            }
            if ('subtype_description' in data) {
                data2.subtype_description = data.subtype_description;
            }
            if ('subtype_id' in data) {
                data2.subtype_id = data.subtype_id;
            }
            if ('trackingValues' in data) {
                data2.trackingValues = data.trackingValues;
            }

            return data2;
        },
        /**
         * Mark all messages of current user with given domain as read.
         *
         * @param {Array[]} domain
         */
        async markAllAsRead(domain) {
            await this.messaging.rpc({
                model: 'mail.message',
                method: 'mark_all_as_read',
                kwargs: { domain },
            }, { shadow: true });
        },
        /**
         * Mark provided messages as read. Messages that have been marked as
         * read are acknowledged by server with response as bus.
         * notification of following format:
         *
         * [[dbname, 'res.partner', partnerId], { type: 'mark_as_read' }]
         *
         * @see MessagingNotificationHandler:_handleNotificationPartnerMarkAsRead()
         *
         * @param {Message[]} messages
         */
        async markAsRead(messages) {
            await this.messaging.rpc({
                model: 'mail.message',
                method: 'set_message_done',
                args: [messages.map(message => message.id)]
            });
        },
        /**
         * Performs the given `route` RPC to fetch messages.
         *
         * @param {string} route
         * @param {Object} params
         * @returns {Message[]}
         */
        async performRpcMessageFetch(route, params) {
            const messagesData = await this.messaging.rpc({ route, params }, { shadow: true });
            if (!this.messaging) {
                return;
            }
            const messages = this.messaging.models['Message'].insert(messagesData.map(
                messageData => this.messaging.models['Message'].convertData(messageData)
            ));
            // compute seen indicators (if applicable)
            for (const message of messages) {
                for (const thread of message.threads) {
                    if (!thread.channel || thread.channel.channel_type === 'channel') {
                        // disabled on non-channel threads and
                        // on `channel` channels for performance reasons
                        continue;
                    }
                    this.messaging.models['MessageSeenIndicator'].insert({
                        thread,
                        message,
                    });
                }
            }
            return messages;
        },
        /**
         * Unstar all starred messages of current user.
         */
        async unstarAll() {
            await this.messaging.rpc({
                model: 'mail.message',
                method: 'unstar_all',
            });
        },
    },
    recordMethods: {
        /**
         * Adds the given reaction on this message.
         *
         * @param {string} content
         */
        async addReaction(content) {
            const messageData = await this.messaging.rpc({
                route: '/mail/message/add_reaction',
                params: { content, message_id: this.id },
            });
            if (!this.exists()) {
                return;
            }
            this.update(messageData);
        },
        /**
         * Mark this message as read, so that it no longer appears in current
         * partner Inbox.
         */
        async markAsRead() {
            await this.messaging.rpc({
                model: 'mail.message',
                method: 'set_message_done',
                args: [[this.id]]
            });
        },
        /**
         * Opens the view that allows to resend the message in case of failure.
         */
        openResendAction() {
            this.env.services.action.doAction(
                'mail.mail_resend_message_action',
                {
                    additionalContext: {
                        mail_message_to_resend: this.id,
                    },
                }
            );
        },
        /**
         * Removes the given reaction from this message.
         *
         * @param {string} content
         */
        async removeReaction(content) {
            const messageData = await this.messaging.rpc({
                route: '/mail/message/remove_reaction',
                params: { content, message_id: this.id },
            });
            if (!this.exists()) {
                return;
            }
            this.update(messageData);
        },
        /**
         * Toggle the starred status of the provided message.
         */
        async toggleStar() {
            await this.messaging.rpc({
                model: 'mail.message',
                method: 'toggle_message_starred',
                args: [[this.id]]
            });
        },
        /**
         * Updates the message's content.
         *
         * @param {Object} param0
         * @param {string} param0.body the new body of the message
         * @param {number[]} param0.attachment_ids
         * @param {string[]} param0.attachment_tokens
         */
        async updateContent({ body, attachment_ids, attachment_tokens }) {
            const messageData = await this.messaging.rpc({
                route: '/mail/message/update_content',
                params: {
                    body,
                    attachment_ids,
                    attachment_tokens,
                    message_id: this.id,
                },
            });
            if (!this.messaging) {
                return;
            }
            this.messaging.models['Message'].insert(messageData);
        },
    },
    fields: {
        authorName: attr({
            compute() {
                if (this.author) {
                    return this.author.nameOrDisplayName;
                }
                if (this.guestAuthor) {
                    return this.guestAuthor.name;
                }
                if (this.email_from) {
                    return this.email_from;
                }
                return this.env._t("Anonymous");
            },
        }),
        attachments: many('Attachment', {
            inverse: 'messages',
        }),
        author: one('Partner'),
        avatarUrl: attr({
            compute() {
                if (this.author && (!this.originThread || this.originThread.model !== 'mail.channel')) {
                    // TODO FIXME for public user this might not be accessible. task-2223236
                    // we should probably use the correspondig attachment id + access token
                    // or create a dedicated route to get message image, checking the access right of the message
                    return this.author.avatarUrl;
                } else if (this.author && this.originThread && this.originThread.model === 'mail.channel') {
                    return `/mail/channel/${this.originThread.id}/partner/${this.author.id}/avatar_128`;
                } else if (this.guestAuthor && (!this.originThread || this.originThread.model !== 'mail.channel')) {
                    return this.guestAuthor.avatarUrl;
                } else if (this.guestAuthor && this.originThread && this.originThread.model === 'mail.channel') {
                    return `/mail/channel/${this.originThread.id}/guest/${this.guestAuthor.id}/avatar_128?unique=${this.guestAuthor.name}`;
                } else if (this.message_type === 'email') {
                    return '/mail/static/src/img/email_icon.png';
                }
                return '/mail/static/src/img/smiley/avatar.jpg';
            },
        }),
        /**
         * This value is meant to be returned by the server
         * (and has been sanitized before stored into db).
         * Do not use this value in a 't-raw' if the message has been created
         * directly from user input and not from server data as it's not escaped.
         */
        body: attr({
            default: "",
        }),
        /**
         * Whether this message can be deleted.
         */
        canBeDeleted: attr({
            compute() {
                if (!session.is_admin && !this.isCurrentUserOrGuestAuthor) {
                    return false;
                }
                if (!this.originThread) {
                    return false;
                }
                if (this.trackingValues.length > 0) {
                    return false;
                }
                if (this.message_type !== 'comment') {
                    return false;
                }
                if (this.originThread.model === 'mail.channel') {
                    return true;
                }
                return this.is_note;
            },
        }),
        /**
         * Whether this message can be starred/unstarred.
         */
        canStarBeToggled: attr({
            compute() {
                return !this.messaging.isCurrentUserGuest && !this.isTemporary && !this.isTransient;
            },
        }),
        /**
         * Determines the date of the message as a moment object.
         */
        date: attr(),
        /**
         * States the date of this message as a string (either a relative period
         * in the near past or an actual date for older dates).
         */
        dateDay: attr({
            compute() {
                if (!this.date) {
                    // Without a date, we assume that it's a today message. This is
                    // mainly done to avoid flicker inside the UI.
                    return this.env._t("Today");
                }
                const date = this.date.format('YYYY-MM-DD');
                if (date === moment().format('YYYY-MM-DD')) {
                    return this.env._t("Today");
                } else if (
                    date === moment()
                        .subtract(1, 'days')
                        .format('YYYY-MM-DD')
                ) {
                    return this.env._t("Yesterday");
                }
                return this.date.format('LL');
            },
        }),
        /**
         * The date time of the message at current user locale time.
         */
        datetime: attr({
            compute() {
                if (!this.date) {
                    return clear();
                }
                return this.date.format(getLangDatetimeFormat());
            },
        }),
        email_from: attr(),
        failureNotifications: many('Notification', {
            compute() {
                return this.notifications.filter(notifications => notifications.isFailure);
            },
        }),
        guestAuthor: one('Guest', {
            inverse: 'authoredMessages',
        }),
        /**
         * States whether the message has some attachments.
         */
        hasAttachments: attr({
            compute() {
                return this.attachments.length > 0;
            },
        }),
        /**
         * Determines whether the message has a reaction icon.
         */
        hasReactionIcon: attr({
            compute() {
                return !this.isTemporary && !this.isTransient;
            },
        }),
        id: attr({
            identifying: true,
        }),
        isCurrentUserOrGuestAuthor: attr({
            compute() {
                return !!(
                    this.author &&
                    this.messaging.currentPartner &&
                    this.messaging.currentPartner === this.author
                ) || !!(
                    this.guestAuthor &&
                    this.messaging.currentGuest &&
                    this.messaging.currentGuest === this.guestAuthor
                );
            },
            default: false,
        }),
        /**
         * States if the body field is empty, regardless of editor default
         * html content. To determine if a message is fully empty, use
         * `isEmpty`.
         */
        isBodyEmpty: attr({
            compute() {
                return (
                    !this.body ||
                    [
                        '',
                        '<p></p>',
                        '<p><br></p>',
                        '<p><br/></p>',
                    ].includes(this.body.replace(/\s/g, ''))
                );
            },
        }),
        /**
         * States whether `body` and `subtype_description` contain similar
         * values.
         *
         * This is necessary to avoid displaying both of them together when they
         * contain duplicate information. This will especially happen with
         * messages that are posted automatically at the creation of a record
         * (messages that serve as tracking messages). They do have hard-coded
         * "record created" body while being assigned a subtype with a
         * description that states the same information.
         *
         * Fixing newer messages is possible by not assigning them a duplicate
         * body content, but the check here is still necessary to handle
         * existing messages.
         *
         * Limitations:
         * - A translated subtype description might not match a non-translatable
         *   body created by a user with a different language.
         * - Their content might be mostly but not exactly the same.
         */
        isBodyEqualSubtypeDescription: attr({
            compute() {
                if (!this.body || !this.subtype_description) {
                    return false;
                }
                const inlineBody = htmlToTextContentInline(this.body);
                return inlineBody.toLowerCase() === this.subtype_description.toLowerCase();
            },
            default: false,
        }),
        isDiscussionOrNotification: attr({
            compute() {
                if (this.is_discussion || this.is_notification || this.message_type === "auto_comment") {
                    return true;
                }
                return clear();
            },
            default: false,
        }),
        /**
         * Determine whether the message has to be considered empty or not.
         *
         * An empty message has no text, no attachment and no tracking value.
         */
        isEmpty: attr({
            /**
             * The method does not attempt to cover all possible cases of empty
             * messages, but mostly those that happen with a standard flow. Indeed
             * it is preferable to be defensive and show an empty message sometimes
             * instead of hiding a non-empty message.
             *
             * The main use case for when a message should become empty is for a
             * message posted with only an attachment (no body) and then the
             * attachment is deleted.
             *
             * The main use case for being defensive with the check is when
             * receiving a message that has no textual content but has other
             * meaningful HTML tags (eg. just an <img/>).
             */
            compute() {
                return (
                    this.isBodyEmpty &&
                    !this.hasAttachments &&
                    this.trackingValues.length === 0 &&
                    !this.subtype_description
                );
            },
        }),
        /**
         * States whether `originThread.name` and `subject` contain similar
         * values except it contains the extra prefix at the start
         * of the subject.
         *
         * This is necessary to avoid displaying the subject, if
         * the subject is same as threadname.
         */
        isSubjectSimilarToOriginThreadName: attr({
            compute() {
                if (
                    !this.subject ||
                    !this.originThread ||
                    !this.originThread.name
                ) {
                    return false;
                }
                const threadName = this.originThread.name.toLowerCase().trim();
                const prefixList = ['re:', 'fw:', 'fwd:'];
                let cleanedSubject = this.subject.toLowerCase();
                let wasSubjectCleaned = true;
                while (wasSubjectCleaned) {
                    wasSubjectCleaned = false;
                    if (threadName === cleanedSubject) {
                        return true;
                    }
                    for (const prefix of prefixList) {
                        if (cleanedSubject.startsWith(prefix)) {
                            cleanedSubject = cleanedSubject.replace(prefix, '').trim();
                            wasSubjectCleaned = true;
                            break;
                        }
                    }
                }
                return false;
            },
        }),
        isTemporary: attr({
            default: false,
        }),
        isTransient: attr({
            default: false,
        }),
        is_discussion: attr({
            default: false,
        }),
        /**
         * Determine whether the message was a needaction. Useful to make it
         * present in history mailbox.
         */
        isHistory: attr({
            default: false,
        }),
        /**
         * Determine whether the message is needaction. Useful to make it
         * present in inbox mailbox and messaging menu.
         */
        isNeedaction: attr({
            default: false,
        }),
        is_note: attr({
            default: false,
        }),
        is_notification: attr({
            default: false,
        }),
        /**
         * Determine whether the current partner is mentioned.
         */
        isCurrentPartnerMentioned: attr({
            compute() {
                return this.recipients.includes(this.messaging.currentPartner);
            },
            default: false,
        }),
        /**
         * Determine whether the message is highlighted.
         */
        isHighlighted: attr({
            compute() {
                return (
                    this.isCurrentPartnerMentioned &&
                    this.originThread &&
                    this.originThread.model === 'mail.channel'
                );
            },
        }),
        /**
         * Determine whether the message is starred. Useful to make it present
         * in starred mailbox.
         */
        isStarred: attr({
            default: false,
        }),
        /**
         * Last tracking value of the message.
         */
        lastTrackingValue: one('TrackingValue', {
            compute() {
                const {
                    length: l,
                    [l - 1]: lastTrackingValue,
                } = this.trackingValues;
                if (lastTrackingValue) {
                    return lastTrackingValue;
                }
                return clear();
            },
        }),
        linkPreviews: many('LinkPreview', {
            inverse: 'message',
        }),
        /**
         * Groups of reactions per content allowing to know the number of
         * reactions for each.
         */
        messageReactionGroups: many('MessageReactionGroup', {
            inverse: 'message',
        }),
        messageTypeText: attr({
            compute() {
                if (this.message_type === 'notification') {
                    return this.env._t("System notification");
                }
                if (this.message_type === "auto_comment") {
                    return this.env._t("Automated message");
                }
                if (!this.is_discussion && !this.is_notification) {
                    return this.env._t("Note");
                }
                return this.env._t("Message");
            },
        }),
        message_type: attr(),
        notificationMessageViews: many('NotificationMessageView', {
            inverse: 'message',
            isCausal: true,
        }),
        /**
         * States the views that are displaying this message.
         */
        messageViews: many('MessageView', {
            inverse: 'message',
            isCausal: true,
        }),
        messageListViewItems: many('MessageListViewItem', {
            inverse: 'message',
        }),
        notifications: many('Notification', {
            inverse: 'message',
            isCausal: true,
        }),
        /**
         * Origin thread of this message (if any).
         */
        originThread: one('Thread', {
            inverse: 'messagesAsOriginThread',
        }),
        /**
         * States the message that this message replies to (if any). Only makes
         * sense on channels. Other types of threads might have a parent message
         * (parent_id in python) that should be ignored for the purpose of this
         * feature.
         */
        parentMessage: one('Message'),
        /**
         * This value is meant to be based on field body which is
         * returned by the server (and has been sanitized before stored into db).
         * Do not use this value in a 't-raw' if the message has been created
         * directly from user input and not from server data as it's not escaped.
         */
        prettyBody: attr({
            /**
             * This value is meant to be based on field body which is
             * returned by the server (and has been sanitized before stored into db).
             * Do not use this value in a 't-raw' if the message has been created
             * directly from user input and not from server data as it's not escaped.
             */
            compute() {
                if (!this.body) {
                    // body null in db, body will be false instead of empty string
                    return clear();
                }
                // add anchor tags to urls
                return parseAndTransform(this.body, addLink);
            },
            default: "",
        }),
        prettyBodyAsMarkup: attr({
            compute() {
                return markup(this.prettyBody);
            },
        }),
        recipients: many('Partner'),
        shortTime: attr({
            compute() {
                if (!this.date) {
                    return clear();
                }
                return this.date.format('hh:mm');
            },
        }),
        subject: attr(),
        subtype_description: attr(),
        subtype_id: attr(),
        /**
         * All threads that this message is linked to. This field is read-only.
         */
        threads: many('Thread', {
            compute() {
                const threads = [];
                if (this.isHistory && this.messaging.history) {
                    threads.push(this.messaging.history.thread);
                }
                if (this.isNeedaction && this.messaging.inbox) {
                    threads.push(this.messaging.inbox.thread);
                }
                if (this.isStarred && this.messaging.starred) {
                    threads.push(this.messaging.starred.thread);
                }
                if (this.originThread) {
                    threads.push(this.originThread);
                }
                return threads;
            },
            inverse: 'messages',
        }),
        trackingValues: many('TrackingValue', {
            inverse: 'messageOwner',
            isCausal: true,
            sort: [['smaller-first', 'id']],
        }),
    },
});
