/* @odoo-module */

import { patch } from "@web/core/utils/patch";
import { MockServer } from "@web/../tests/helpers/mock_server";

import { parseEmail } from "@mail/js/utils";
import { serializeDateTime, today } from "@web/core/l10n/dates";

patch(MockServer.prototype, {
    async _performRPC(route, args) {
        if (args.method === "message_subscribe") {
            const ids = args.args[0];
            const partner_ids = args.args[1] || args.kwargs.partner_ids;
            const subtype_ids = args.args[2] || args.kwargs.subtype_ids;
            return this._mockMailThreadMessageSubscribe(args.model, ids, partner_ids, subtype_ids);
        }
        if (args.method === "message_unsubscribe") {
            const ids = args.args[0];
            const partner_ids = args.args[1] || args.kwargs.partner_ids;
            return this._mockMailThreadMessageUnsubscribe(args.model, ids, partner_ids);
        }
        if (args.method === "message_get_followers") {
            const ids = args.args[0];
            const after = args.args[1] || args.kwargs.after;
            const limit = args.args[2] || args.kwargs.limit;
            return this._mockMailThreadMessageGetFollowers(args.model, ids, after, limit);
        }
        if (args.method === "message_post") {
            const id = args.args[0];
            const kwargs = args.kwargs;
            const context = kwargs.context;
            delete kwargs.context;
            return this._mockMailThreadMessagePost(args.model, [id], kwargs, context);
        }
        if (args.method === "notify_cancel_by_type") {
            return this._mockMailThreadNotifyCancelByType(
                args.model,
                args.kwargs.notification_type
            );
        }
        return super._performRPC(route, args);
    },
    /**
     * Simulates `_message_compute_author` on `mail.thread`.
     *
     * @private
     * @param {string} model
     * @param {integer[]} ids
     * @param {Object} [context={}]
     * @returns {Array}
     */
    _MockMailThread_MessageComputeAuthor(model, ids, author_id, email_from, context = {}) {
        if (author_id === undefined) {
            // For simplicity partner is not guessed from email_from here, but
            // that would be the first step on the server.
            const author = this.getRecords(
                "res.partner",
                [["id", "=", this.pyEnv.currentUser.partner_id]],
                {
                    active_test: false,
                }
            )[0];
            author_id = author.id;
            email_from = `${author.display_name} <${author.email}>`;
        }

        if (email_from === undefined) {
            if (author_id) {
                const author = this.getRecords("res.partner", [["id", "=", author_id]], {
                    active_test: false,
                })[0];
                email_from = `${author.display_name} <${author.email}>`;
            }
        }

        if (!email_from) {
            throw Error("Unable to log message due to missing author email.");
        }

        return [author_id, email_from];
    },
    /**
     * @param {string} model
     * @param {integer[]} ids
     *
     * @returns {Map<integer:string>}
     * Simulate `_message_compute_subject` on `mail.thread`
     */
    mockMailThread_MessageComputeSubject(model, ids) {
        const records = this.getRecords(model, [["id", "in", ids]]);
        return new Map(records.map((record) => [record.id, record.name || ""]));
    },
    /**
     * Simulates `_get_customer_information` on `mail.thread`.
     *
     * @private
     * @param {string} model
     * @param {integer[]} ids
     * @returns {Object}
     */
    _mockMailThread_GetCustomerInformation(model, ids) {
        return {};
    },
    /**
     * Simulates `_message_add_suggested_recipient` on `mail.thread`.
     *
     * @private
     * @param {string} model
     * @param {integer[]} ids
     * @param {Object} result
     * @param {Object} [param3={}]
     * @param {string} [param3.email]
     * @param {integer} [param3.partner]
     * @param {string} [param3.reason]
     * @returns {Object}
     */
    _mockMailThread_MessageAddSuggestedRecipient(
        model,
        ids,
        result,
        { email, partner, lang, reason = "" } = {}
    ) {
        const record = this.getRecords(model, [["id", "in", ids]])[0];
        if (email !== undefined && partner === undefined) {
            const partnerInfo = parseEmail(email);
            partner = this.getRecords("res.partner", [["email", "=", partnerInfo[1]]])[0];
        }
        if (partner) {
            result[record.id].push([partner.id, partner.display_name, lang, reason, {}]);
        } else {
            const partnerCreateValues = this._mockMailThread_GetCustomerInformation(model, ids);
            result[record.id].push([false, email, reason, lang, partnerCreateValues]);
        }

        return result;
    },
    /**
     * Simulates `message_get_followers` on `mail.thread`.
     *
     * @private
     * @param {string} model
     * @param {integer[]} ids
     * @param {integer} [after]
     * @param {integer} [limit=100]
     * @returns {Object[]}
     */
    _mockMailThreadMessageGetFollowers(model, ids, after, limit = 100, kwargs = {}) {
        const domain = [
            ["res_id", "=", ids[0]],
            ["res_model", "=", model],
        ];
        if (after) {
            domain.push(["id", ">", after]);
        }
        if (kwargs.filter_recipients) {
            domain.push(["partner_id", "!=", this.pyEnv.currentPartnerId]);
        }
        const followers = this.getRecords("mail.followers", domain).sort(
            (f1, f2) => (f1.id < f2.id ? -1 : 1) // sorted from lowest ID to highest ID (i.e. from oldest to youngest)
        );
        followers.length = Math.min(followers.length, limit);
        return this._mockMailFollowers_FormatForChatter(followers.map((follower) => follower.id));
    },
    /**
     * Simulates `_message_get_suggested_recipients` on `mail.thread`.
     *
     * @private
     * @param {string} model
     * @param {integer[]} ids
     * @returns {Object}
     */
    _mockMailThread_MessageGetSuggestedRecipients(model, ids) {
        if (model === "res.fake") {
            return this._mockResFake_MessageGetSuggestedRecipients(model, ids);
        }
        const result = ids.reduce((result, id) => (result[id] = []), {});
        const records = this.getRecords(model, [["id", "in", ids]]);
        for (const record in records) {
            if (record.user_id) {
                const user = this.getRecords("res.users", [["id", "=", record.user_id]]);
                if (user.partner_id) {
                    const reason = this.models[model].fields["user_id"].string;
                    const partner = this.getRecords("res.partner", [["id", "=", user.partner_id]]);
                    this._mockMailThread_MessageAddSuggestedRecipient(model, ids, result, {
                        email: partner.email,
                        partner,
                        reason,
                    });
                }
            }
        }
        return result;
    },
    /**
     * Simulates `message_post` on `mail.thread`.
     *
     * @private
     * @param {string} model
     * @param {integer[]} ids
     * @param {Object} kwargs
     * @param {Object} [context]
     * @returns {Object}
     */
    _mockMailThreadMessagePost(model, ids, kwargs, context) {
        const id = ids[0]; // ensure_one
        if (kwargs.partner_emails) {
            kwargs.partner_ids = kwargs.partner_ids || [];
            for (const email of kwargs.partner_emails) {
                const partner = this.getRecords("res.partner", [["email", "=", email]]);
                if (partner.length !== 0) {
                    kwargs.partner_ids.push(partner[0].id);
                } else {
                    const partner_id = this.pyEnv["res.partner"].create(
                        Object.assign({ email }, kwargs.partner_additional_values[email] || {})
                    );
                    kwargs.partner_ids.push(partner_id);
                }
            }
        }
        delete kwargs.partner_emails;
        delete kwargs.partner_additional_values;
        if (context?.["mail_post_autofollow"] && kwargs["partner_ids"].length > 0) {
            this._mockMailThreadMessageSubscribe(model, ids, kwargs["partner_ids"]);
        }
        if (kwargs.attachment_ids) {
            const attachments = this.getRecords("ir.attachment", [
                ["id", "in", kwargs.attachment_ids],
                ["res_model", "=", "mail.compose.message"],
                ["res_id", "=", 0],
            ]);
            const attachmentIds = attachments.map((attachment) => attachment.id);
            this.pyEnv["ir.attachment"].write(attachmentIds, {
                res_id: id,
                res_model: model,
            });
            kwargs.attachment_ids = attachmentIds.map((attachmentId) => [4, attachmentId]);
        }
        const subtype_xmlid = kwargs.subtype_xmlid || "mail.mt_note";
        let author_id;
        let email_from;
        const author_guest_id = this.pyEnv.currentUser?._is_public()
            ? this._mockMailGuest__getGuestFromContext()?.id
            : undefined;
        if (!author_guest_id) {
            [author_id, email_from] = this._MockMailThread_MessageComputeAuthor(
                model,
                ids,
                kwargs.author_id,
                kwargs.email_from,
                context
            );
        }

        const values = Object.assign({}, kwargs, {
            author_id,
            author_guest_id,
            email_from,
            is_discussion: subtype_xmlid === "mail.mt_comment",
            is_note: subtype_xmlid === "mail.mt_note",
            model,
            res_id: id,
        });
        delete values.subtype_xmlid;
        const messageId = this.pyEnv["mail.message"].create(values);
        for (const partnerId of kwargs.partner_ids || []) {
            this.pyEnv["mail.notification"].create({
                mail_message_id: messageId,
                notification_type: "inbox",
                res_partner_id: partnerId,
            });
        }
        this._mockMailThread_NotifyThread(model, ids, messageId, context?.temporary_id);
        return Object.assign(this._mockMailMessageMessageFormat([messageId])[0], {
            temporary_id: context?.temporary_id,
        });
    },
    /**
     * Simulates `message_subscribe` on `mail.thread`.
     *
     * @private
     * @param {string} model not in server method but necessary for thread mock
     * @param {integer[]} ids
     * @param {integer[]} partner_ids
     * @param {integer[]} [subtype_ids]
     * @returns {boolean}
     */
    _mockMailThreadMessageSubscribe(model, ids, partner_ids, subtype_ids) {
        for (const id of ids) {
            for (const partner_id of partner_ids) {
                let followerId = this.pyEnv["mail.followers"].search([
                    ["partner_id", "=", partner_id],
                ])[0];
                if (!followerId) {
                    if (!subtype_ids || subtype_ids.length === 0) {
                        subtype_ids = this.pyEnv["mail.message.subtype"].search([
                            ["default", "=", true],
                            "|",
                            ["res_model", "=", model],
                            ["res_model", "=", false],
                        ]);
                    }
                    followerId = this.pyEnv["mail.followers"].create({
                        is_active: true,
                        partner_id,
                        res_id: id,
                        res_model: model,
                        subtype_ids: subtype_ids,
                    });
                }
                this.pyEnv[model].write(ids, {
                    message_follower_ids: [[4, followerId]],
                });
                this.pyEnv["res.partner"].write([partner_id], {
                    message_follower_ids: [[4, followerId]],
                });
            }
        }
    },
    /**
     * Simulates `_notify_thread` on `mail.thread`.
     * Simplified version that sends notification to author and channel.
     *
     * @private
     * @param {string} model not in server method but necessary for thread mock
     * @param {integer[]} ids
     * @param {integer} messageId
     * @returns {boolean}
     */
    _mockMailThread_NotifyThread(model, ids, messageId, temporary_id) {
        const message = this.getRecords("mail.message", [["id", "=", messageId]])[0];
        const messageFormat = this._mockMailMessageMessageFormat([messageId])[0];
        const notifications = [];
        if (model === "discuss.channel") {
            const channels = this.getRecords("discuss.channel", [["id", "=", message.res_id]]);
            for (const channel of channels) {
                const now = serializeDateTime(today());
                notifications.push([
                    [channel, "members"],
                    "mail.record/insert",
                    {
                        Thread: {
                            id: channel.id,
                            is_pinned: true,
                            model: "discuss.channel",
                        },
                    },
                ]);
                notifications.push([
                    channel,
                    "discuss.channel/last_interest_dt_changed",
                    {
                        id: channel.id,
                        last_interest_dt: now,
                    },
                ]);
                notifications.push([
                    channel,
                    "discuss.channel/new_message",
                    {
                        id: channel.id,
                        message: Object.assign(messageFormat, { temporary_id }),
                    },
                ]);
                if (message.author_id === this.pyEnv.currentPartnerId) {
                    this._mockDiscussChannel_ChannelSeen(ids, message.id);
                }
            }
        }
        this.pyEnv["bus.bus"]._sendmany(notifications);
    },
    /**
     * Simulates `message_unsubscribe` on `mail.thread`.
     *
     * @private
     * @param {string} model not in server method but necessary for thread mock
     * @param {integer[]} ids
     * @param {integer[]} partner_ids
     * @returns {boolean|undefined}
     */
    _mockMailThreadMessageUnsubscribe(model, ids, partner_ids) {
        if (!partner_ids) {
            return true;
        }
        const followers = this.getRecords("mail.followers", [
            ["res_model", "=", model],
            ["res_id", "in", ids],
            ["partner_id", "in", partner_ids || []],
        ]);
        this.pyEnv["mail.followers"].unlink(followers.map((follower) => follower.id));
    },
    /**
     * Simulates `_message_track` on `mail.thread`
     */
    _mockMailThread_MessageTrack(
        modelName,
        trackedFieldNames,
        initialTrackedFieldValuesByRecordId
    ) {
        const trackFieldNamesToField = this.mockFieldsGet(modelName, trackedFieldNames);
        const tracking = {};
        const records = this.models[modelName].records;
        for (const record of records) {
            tracking[record.id] = this._mockMailBaseModel__MailTrack(
                modelName,
                trackFieldNamesToField,
                initialTrackedFieldValuesByRecordId[record.id],
                record
            );
        }
        for (const record of records) {
            const { trackingValueIds, changedFieldNames } = tracking[record.id] || {};
            if (!changedFieldNames || !changedFieldNames.length) {
                continue;
            }
            const changedFieldsInitialValues = {};
            const initialFieldValues = initialTrackedFieldValuesByRecordId[record.id];
            for (const fname in changedFieldNames) {
                changedFieldsInitialValues[fname] = initialFieldValues[fname];
            }
            const subtype = this._mockMailThread_TrackSubtype(changedFieldsInitialValues);
            this._mockMailThreadMessagePost(modelName, [record.id], {
                subtype_id: subtype.id,
                tracking_value_ids: trackingValueIds,
            });
        }
        return tracking;
    },
    /**
     * Simulates `_track_finalize` on `mail.thread`
     */
    _mockMailThread_TrackFinalize(model, initialTrackedFieldValuesByRecordId) {
        this._mockMailThread_MessageTrack(
            model,
            this._mockMailThread_TrackGetFields(model),
            initialTrackedFieldValuesByRecordId
        );
    },
    /**
     * Simulates `_track_get_fields` on `mail.thread`
     */
    _mockMailThread_TrackGetFields(model) {
        return Object.entries(this.models[model].fields).reduce((prev, next) => {
            if (next[1].tracking) {
                prev.push(next[0]);
            }
            return prev;
        }, []);
    },
    /**
     * Simulates `_track_prepare` on `mail.thread`
     */
    _mockMailThread_TrackPrepare(model) {
        const trackedFieldNames = this._mockMailThread_TrackGetFields(model);
        if (!trackedFieldNames.length) {
            return;
        }
        const initialTrackedFieldValuesByRecordId = {};
        for (const record of this.models[model].records) {
            const values = {};
            initialTrackedFieldValuesByRecordId[record.id] = values;
            for (const fname of trackedFieldNames) {
                values[fname] = record[fname];
            }
        }
        return initialTrackedFieldValuesByRecordId;
    },
    /**
     * Simulates `_track_subtype` on `mail.thread`
     */
    _mockMailThread_TrackSubtype(initialFieldValuesByRecordId) {
        return false;
    },
    /**
     * Simulate the `notify_cancel_by_type` on `mail.thread` .
     * Note that this method is overridden by snailmail module but not simulated here.
     */
    _mockMailThreadNotifyCancelByType(model, notificationType) {
        // Query matching notifications
        const notifications = this.getRecords("mail.notification", [
            ["notification_type", "=", notificationType],
            ["notification_status", "in", ["bounce", "exception"]],
        ]).filter((notification) => {
            const message = this.getRecords("mail.message", [
                ["id", "=", notification.mail_message_id],
            ])[0];
            return message.model === model && message.author_id === this.pyEnv.currentPartnerId;
        });
        // Update notification status
        this.pyEnv["mail.notification"].write(
            notifications.map((notification) => notification.id),
            { notification_status: "canceled" }
        );
        // Send bus notifications to update status of notifications in the web client
        this.pyEnv["bus.bus"]._sendone(
            this.pyEnv.currentPartner,
            "mail.message/notification_update",
            {
                elements: this._mockMailMessage_MessageNotificationFormat(
                    notifications.map((notification) => notification.mail_message_id)
                ),
            }
        );
    },
});
