import * as mfm from "mfm-js";
import { publishNoteStream, publishNoteUpdatesStream } from "../stream.js";
import DeliverManager from "../../remote/activitypub/deliver-manager.js";
import renderNote from "../../remote/activitypub/renderer/note.js";
import { renderActivity } from "../../remote/activitypub/renderer/index.js";
import { extractCustomEmojisFromMfm } from "../../misc/extract-custom-emojis-from-mfm.js";
import { extractHashtags } from "../../misc/extract-hashtags.js";
import { Users, Notes, UserProfiles, Polls, NoteEdits, PollVotes } from "../../models/index.js";
import { In } from "typeorm";
import { genId } from "../../misc/gen-id.js";
import { deliverToRelays } from "../relay.js";
import renderUpdate from "../../remote/activitypub/renderer/update.js";
import { extractMentionedUsers } from "./create.js";
import { normalizeForSearch } from "../../misc/normalize-for-search.js";
import { NoteConverter } from "../../server/api/mastodon/converters/note.js";
export default async function(user, note, data, suppressEvidence = false) {
    if (data.text !== undefined && data.text !== null) {
        data.text = data.text.trim();
    } else {
        data.text = null;
    }
    const fileIds = data.files?.map((file)=>file.id);
    const fileTypes = data.files?.map((file)=>file.type);
    const tokens = mfm.parse(data.text || "").concat(mfm.parse(data.cw || ""));
    const tags = extractHashtags(tokens).filter((tag)=>Array.from(tag || "").length <= 128).splice(0, 32).map(normalizeForSearch);
    const emojis = extractCustomEmojisFromMfm(tokens);
    const mentionUsers = await extractMentionedUsers(user, tokens);
    const mentionUserIds = mentionUsers.map((user)=>user.id);
    const remoteUsers = mentionUsers.filter((user)=>user.host != null);
    const remoteUserIds = remoteUsers.map((user)=>user.id);
    const remoteProfiles = await UserProfiles.findBy({
        userId: In(remoteUserIds)
    });
    const mentionedRemoteUsers = remoteUsers.map((user)=>{
        const profile = remoteProfiles.find((profile)=>profile.userId === user.id);
        return {
            username: user.username,
            host: user.host ?? null,
            uri: user.uri,
            url: profile ? profile.url : undefined
        };
    });
    let publishing = false;
    const update = {};
    if (data.text !== null && data.text !== note.text) {
        update.text = data.text;
    }
    if (data.cw !== note.cw) {
        update.cw = data.cw ?? null;
    }
    if (data.files !== undefined && fileIds.sort().join(",") !== note.fileIds.sort().join(",")) {
        update.fileIds = fileIds;
        update.attachedFileTypes = fileTypes;
    }
    if (tags.sort().join(",") !== note.tags.sort().join(",")) {
        update.tags = tags;
    }
    if (mentionUserIds.sort().join(",") !== note.mentions.sort().join(",")) {
        update.mentions = mentionUserIds;
        update.mentionedRemoteUsers = JSON.stringify(mentionedRemoteUsers);
    }
    if (emojis.sort().join(",") !== note.emojis.sort().join(",")) {
        update.emojis = emojis;
    }
    if (data.poll !== undefined && note.hasPoll !== !!data.poll) {
        update.hasPoll = !!data.poll;
    }
    if (data.poll) {
        const dbPoll = await Polls.findOneBy({
            noteId: note.id
        });
        if (dbPoll == null) {
            await Polls.insert({
                noteId: note.id,
                choices: data.poll?.choices,
                multiple: data.poll?.multiple,
                votes: new Array(data.poll?.choices.length).fill(0),
                expiresAt: data.poll?.expiresAt,
                noteVisibility: note.visibility === "hidden" ? "home" : note.visibility,
                userId: user.id,
                userHost: user.host
            });
            publishing = true;
        } else {
            const choicesChanged = JSON.stringify(dbPoll.choices) !== JSON.stringify(data.poll.choices);
            if (dbPoll.multiple !== data.poll.multiple || dbPoll.expiresAt !== data.poll.expiresAt || dbPoll.noteVisibility !== note.visibility || choicesChanged) {
                await Polls.update({
                    noteId: note.id
                }, {
                    choices: data.poll?.choices,
                    multiple: data.poll?.multiple,
                    votes: choicesChanged ? new Array(data.poll.choices.length).fill(0) : undefined,
                    expiresAt: data.poll?.expiresAt,
                    noteVisibility: note.visibility === "hidden" ? "home" : note.visibility
                });
                // Reset votes
                if (JSON.stringify(dbPoll.choices) !== JSON.stringify(data.poll.choices)) {
                    await PollVotes.delete({
                        noteId: dbPoll.noteId
                    });
                }
                publishing = true;
            }
        }
    }
    if (data.quoteAuthorization !== undefined) {
        update.quoteAuthorization = data.quoteAuthorization;
    }
    if (notEmpty(update)) {
        if (!suppressEvidence) update.updatedAt = new Date();
        await Notes.update(note.id, update);
        if (!suppressEvidence) {
            // Add previous note contents to NoteEdit history
            await NoteEdits.insert({
                id: genId(),
                noteId: note.id,
                text: note.text || undefined,
                cw: note.cw,
                fileIds: note.fileIds,
                updatedAt: update.updatedAt ?? undefined
            });
        }
        publishing = true;
    }
    note = await Notes.findOneByOrFail({
        id: note.id
    });
    if (publishing) {
        NoteConverter.prewarmCache(note);
        // Publish update event for the updated note details
        if (!suppressEvidence) {
            publishNoteStream(note.id, "updated", {
                updatedAt: update.updatedAt
            });
            publishNoteUpdatesStream("updated", note);
        }
        (async ()=>{
            if (note.localOnly) return;
            const noteActivity = await renderNote(note, false);
            if (!suppressEvidence) noteActivity.updated = note.updatedAt?.toISOString();
            const updateActivity = renderUpdate(noteActivity, user);
            updateActivity.to = noteActivity.to;
            updateActivity.cc = noteActivity.cc;
            const activity = renderActivity(updateActivity);
            const dm = new DeliverManager(user, activity);
            // Delivery to remote mentioned users
            for (const u of mentionUsers.filter((u)=>Users.isRemoteUser(u))){
                dm.addDirectRecipe(u);
            }
            // Post is a reply and remote user is the contributor of the original post
            if (note.reply && note.reply.userHost !== null) {
                const u = await Users.findOneBy({
                    id: note.reply.userId
                });
                if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u);
            }
            // Post is a renote and remote user is the contributor of the original post
            if (note.renote && note.renote.userHost !== null) {
                const u = await Users.findOneBy({
                    id: note.renote.userId
                });
                if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u);
            }
            // Deliver to followers for non-direct posts.
            if ([
                "public",
                "home",
                "followers"
            ].includes(note.visibility)) {
                dm.addFollowersRecipe();
            }
            // Deliver to relays for public posts.
            if ([
                "public"
            ].includes(note.visibility)) {
                deliverToRelays(user, activity);
            }
            // GO!
            dm.execute();
        })();
    }
    return note;
}
function notEmpty(partial) {
    return Object.keys(partial).length > 0;
}
