/*
Copyright 2024 New Vector Ltd.
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import { type Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
import {
    type IEventRelation,
    type MatrixEvent,
    type ISendEventResponse,
    type MatrixClient,
    THREAD_RELATION_TYPE,
} from "matrix-js-sdk/src/matrix";
import { type RoomMessageEventContent } from "matrix-js-sdk/src/types";

import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
import SettingsStore from "../../../../../settings/SettingsStore";
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../../../sendTimePerformanceMetrics";
import { doMaybeLocalRoomAction } from "../../../../../utils/local-room";
import { CHAT_EFFECTS } from "../../../../../effects";
import { containsEmoji } from "../../../../../effects/utils";
import { type IRoomState } from "../../../../structures/RoomView";
import dis from "../../../../../dispatcher/dispatcher";
import { createRedactEventDialog } from "../../../dialogs/ConfirmRedactDialog";
import { endEditing, cancelPreviousPendingEdit } from "./editing";
import type EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
import { createMessageContent, EMOTE_PREFIX } from "./createMessageContent";
import { isContentModified } from "./isContentModified";
import { CommandCategories, getCommand } from "../../../../../slash-commands/SlashCommands";
import { runSlashCommand, shouldSendAnyway } from "../../../../../editor/commands";
import { Action } from "../../../../../dispatcher/actions";
import { addReplyToMessageContent } from "../../../../../utils/Reply";
import { attachRelation } from "../../../../../utils/messages";

export interface SendMessageParams {
    mxClient: MatrixClient;
    relation?: IEventRelation;
    replyToEvent?: MatrixEvent;
    roomContext: Pick<IRoomState, "timelineRenderingType" | "room">;
}

export async function sendMessage(
    message: string,
    isHTML: boolean,
    { roomContext, mxClient, ...params }: SendMessageParams,
): Promise<ISendEventResponse | undefined> {
    const { relation, replyToEvent } = params;
    const { room } = roomContext;
    const roomId = room?.roomId;

    if (!roomId) {
        return;
    }

    const posthogEvent: ComposerEvent = {
        eventName: "Composer",
        isEditing: false,
        messageType: "Text",
        isReply: Boolean(replyToEvent),
        // TODO thread
        inThread: relation?.rel_type === THREAD_RELATION_TYPE.name,
    };

    // TODO thread
    /*if (posthogEvent.inThread) {
        const threadRoot = room.findEventById(relation?.event_id);
        posthogEvent.startsThread = threadRoot?.getThread()?.events.length === 1;
    }*/
    PosthogAnalytics.instance.trackEvent<ComposerEvent>(posthogEvent);

    let content: RoomMessageEventContent | null = null;

    // Slash command handling here approximates what can be found in SendMessageComposer.sendMessage()
    // but note that the /me and // special cases are handled by the call to createMessageContent
    if (message.startsWith("/") && !message.startsWith("//") && !message.startsWith(EMOTE_PREFIX)) {
        const { cmd, args } = getCommand(roomId, message);
        if (cmd) {
            const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation?.event_id : null;
            let commandSuccessful: boolean;
            [content, commandSuccessful] = await runSlashCommand(mxClient, cmd, args, roomId, threadId ?? null);

            if (!commandSuccessful) {
                return; // errored
            }

            if (
                content &&
                (cmd.category === CommandCategories.messages || cmd.category === CommandCategories.effects)
            ) {
                attachRelation(content, relation);
                if (replyToEvent) {
                    addReplyToMessageContent(content, replyToEvent);
                }
            } else {
                // instead of setting shouldSend to false as in SendMessageComposer, just return
                return;
            }
        } else {
            const sendAnyway = await shouldSendAnyway(message);
            // re-focus the composer after QuestionDialog is closed
            dis.dispatch({
                action: Action.FocusAComposer,
                context: roomContext.timelineRenderingType,
            });
            // if !sendAnyway bail to let the user edit the composer and try again
            if (!sendAnyway) return;
        }
    }

    // if content is null, we haven't done any slash command processing, so generate some content
    content ??= await createMessageContent(message, isHTML, params);

    // TODO replace emotion end of message ?

    // TODO quick reaction

    // don't bother sending an empty message
    if (!content.body.trim()) {
        return;
    }

    if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
        decorateStartSendingTime(content);
    }

    const threadId = relation?.event_id && relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null;

    const prom = doMaybeLocalRoomAction(
        roomId,
        (actualRoomId: string) => mxClient.sendMessage(actualRoomId, threadId, content!),
        mxClient,
    );

    if (replyToEvent) {
        // Clear reply_to_event as we put the message into the queue
        // if the send fails, retry will handle resending.
        dis.dispatch({
            action: "reply_to_event",
            event: null,
            context: roomContext.timelineRenderingType,
        });
    }

    dis.dispatch({ action: "message_sent" });
    CHAT_EFFECTS.forEach((effect) => {
        if (content && containsEmoji(content, effect.emojis)) {
            // For initial threads launch, chat effects are disabled
            // see #19731
            const isNotThread = relation?.rel_type !== THREAD_RELATION_TYPE.name;
            if (isNotThread) {
                dis.dispatch({ action: `effects.${effect.command}` });
            }
        }
    });
    if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
        prom.then((resp) => {
            sendRoundTripMetric(mxClient, roomId, resp.event_id);
        });
    }

    // TODO save history
    // TODO save local state

    //if (shouldSend && SettingsStore.getValue("scrollToBottomOnMessageSent")) {
    if (SettingsStore.getValue("scrollToBottomOnMessageSent")) {
        dis.dispatch({
            action: "scroll_to_bottom",
            timelineRenderingType: roomContext.timelineRenderingType,
        });
    }

    return prom;
}

interface EditMessageParams {
    mxClient: MatrixClient;
    roomContext: Pick<IRoomState, "timelineRenderingType">;
    editorStateTransfer: EditorStateTransfer;
}

export async function editMessage(
    html: string,
    { roomContext, mxClient, editorStateTransfer }: EditMessageParams,
): Promise<ISendEventResponse | undefined> {
    const editedEvent = editorStateTransfer.getEvent();

    PosthogAnalytics.instance.trackEvent<ComposerEvent>({
        eventName: "Composer",
        isEditing: true,
        messageType: "Text",
        inThread: Boolean(editedEvent?.getThread()),
        isReply: Boolean(editedEvent.replyEventId),
    });

    // TODO emoji
    // Replace emoticon at the end of the message
    /*    if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
        const caret = this.editorRef.current?.getCaret();
        const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
        this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
    }*/
    const editContent = await createMessageContent(html, true, { editedEvent });
    const newContent = editContent["m.new_content"]!;

    const shouldSend = true;

    if (newContent?.body === "") {
        cancelPreviousPendingEdit(mxClient, editorStateTransfer);
        createRedactEventDialog({
            mxEvent: editedEvent,
            onCloseDialog: () => {
                endEditing(roomContext);
            },
        });
        return;
    }

    let response: Promise<ISendEventResponse> | undefined;

    const roomId = editedEvent.getRoomId();

    // If content is modified then send an updated event into the room
    if (isContentModified(newContent, editorStateTransfer) && roomId) {
        // TODO Slash Commands

        if (shouldSend) {
            cancelPreviousPendingEdit(mxClient, editorStateTransfer);

            const event = editorStateTransfer.getEvent();
            const threadId = event.threadRootId || null;

            response = mxClient.sendMessage(roomId, threadId, editContent);
            dis.dispatch({ action: "message_sent" });
        }
    }

    endEditing(roomContext);
    return response;
}
