/*
 * Copyright 2026 Element Creations Ltd.
 *
 * 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 React, { type MouseEvent } from "react";
import { MatrixEventEvent, MsgType, type MatrixEvent } from "matrix-js-sdk/src/matrix";
import {
    BaseViewModel,
    LINKIFIED_DATA_ATTRIBUTE,
    TextualBodyViewBodyWrapperKind,
    TextualBodyViewKind,
    type TextualBodyViewModel as TextualBodyViewModelInterface,
    type TextualBodyViewSnapshot,
} from "@element-hq/web-shared-components";

import { formatDate } from "../../../../../DateUtils";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher";
import { _t } from "../../../../../languageHandler";
import { IntegrationManagers } from "../../../../../integrations/IntegrationManagers";
import { tryTransformPermalinkToLocalHref } from "../../../../../utils/permalinks/Permalinks";
import { Action } from "../../../../../dispatcher/actions";
import QuestionDialog from "../../../../../components/views/dialogs/QuestionDialog";
import MessageEditHistoryDialog from "../../../../../components/views/dialogs/MessageEditHistoryDialog";
import { type TimelineRenderingType } from "../../../../../contexts/RoomContext";

const CAPTION_MESSAGE_TYPES = new Set<MsgType>([MsgType.Image, MsgType.File, MsgType.Audio, MsgType.Video]);

export interface TextualBodyViewModelProps {
    /**
     * Optional DOM id forwarded to the root textual body element.
     */
    id?: string;
    /**
     * Matrix event used to derive the body kind, sender label, starter link, edit state, and moderation state.
     */
    mxEvent: MatrixEvent;
    /**
     * Optional URL that wraps the body when the message should behave as a highlighted link target.
     */
    highlightLink?: string;
    /**
     * Event id of the replacement event, when this message has been edited.
     */
    replacingEventId?: string;
    /**
     * Whether the user is viewing a hidden message while moderation is pending.
     */
    isSeeingThroughMessageHiddenForModeration?: boolean;
    /**
     * Timeline context used when dispatching actions from textual body interactions.
     */
    timelineRenderingType: TimelineRenderingType;
}

export class TextualBodyViewModel
    extends BaseViewModel<TextualBodyViewSnapshot, TextualBodyViewModelProps>
    implements TextualBodyViewModelInterface
{
    private watchedEvent?: MatrixEvent;

    private static readonly getKind = (mxEvent: MatrixEvent): TextualBodyViewKind => {
        const msgtype = mxEvent.getContent().msgtype as MsgType | undefined;

        if (msgtype === MsgType.Notice) {
            return TextualBodyViewKind.NOTICE;
        }

        if (msgtype === MsgType.Emote) {
            return TextualBodyViewKind.EMOTE;
        }

        if (msgtype && CAPTION_MESSAGE_TYPES.has(msgtype)) {
            return TextualBodyViewKind.CAPTION;
        }

        return TextualBodyViewKind.TEXT;
    };

    private static readonly getStarterLink = (mxEvent: MatrixEvent): string | undefined => {
        const starterLink = mxEvent.getContent().data?.["org.matrix.neb.starter_link"];

        return typeof starterLink === "string" ? starterLink : undefined;
    };

    private static readonly computeBodyWrapperSnapshot = (
        props: TextualBodyViewModelProps,
    ): Pick<TextualBodyViewSnapshot, "bodyWrapper" | "bodyLinkHref" | "bodyActionAriaLabel"> => {
        if (props.highlightLink) {
            return {
                bodyWrapper: TextualBodyViewBodyWrapperKind.LINK,
                bodyLinkHref: props.highlightLink,
                bodyActionAriaLabel: undefined,
            };
        }

        if (TextualBodyViewModel.getStarterLink(props.mxEvent)) {
            return {
                bodyWrapper: TextualBodyViewBodyWrapperKind.ACTION,
                bodyLinkHref: undefined,
                bodyActionAriaLabel: undefined,
            };
        }

        return {
            bodyWrapper: TextualBodyViewBodyWrapperKind.NONE,
            bodyLinkHref: undefined,
            bodyActionAriaLabel: undefined,
        };
    };

    private static readonly computeEditedMarkerSnapshot = (
        props: TextualBodyViewModelProps,
    ): Pick<
        TextualBodyViewSnapshot,
        | "showEditedMarker"
        | "editedMarkerText"
        | "editedMarkerAriaLabel"
        | "editedMarkerTooltip"
        | "editedMarkerCaption"
    > => {
        const replacingEventId = props.replacingEventId ?? props.mxEvent.replacingEventId();
        if (!replacingEventId) {
            return {
                showEditedMarker: false,
                editedMarkerText: undefined,
                editedMarkerAriaLabel: undefined,
                editedMarkerTooltip: undefined,
                editedMarkerCaption: undefined,
            };
        }

        const replacingDate = props.mxEvent.replacingEventDate();
        const date = replacingDate ? formatDate(replacingDate) : undefined;

        return {
            showEditedMarker: true,
            editedMarkerText: `(${_t("common|edited")})`,
            editedMarkerAriaLabel: _t("timeline|edits|tooltip_label", { date }),
            editedMarkerTooltip: _t("timeline|edits|tooltip_title", { date }),
            editedMarkerCaption: _t("timeline|edits|tooltip_sub"),
        };
    };

    private static readonly computePendingModerationSnapshot = (
        props: TextualBodyViewModelProps,
    ): Pick<TextualBodyViewSnapshot, "showPendingModerationMarker" | "pendingModerationText"> => {
        if (!props.isSeeingThroughMessageHiddenForModeration) {
            return {
                showPendingModerationMarker: false,
                pendingModerationText: undefined,
            };
        }

        const visibility = props.mxEvent.messageVisibility();
        if (visibility.visible) {
            throw new Error("TextualBodyViewModel should only render pending moderation for hidden messages");
        }

        const text = visibility.reason
            ? _t("timeline|pending_moderation_reason", { reason: visibility.reason })
            : _t("timeline|pending_moderation");

        return {
            showPendingModerationMarker: true,
            pendingModerationText: `(${text})`,
        };
    };

    private static readonly computeEventSnapshot = (
        props: TextualBodyViewModelProps,
    ): Pick<
        TextualBodyViewSnapshot,
        | "kind"
        | "bodyWrapper"
        | "bodyLinkHref"
        | "bodyActionAriaLabel"
        | "showEditedMarker"
        | "editedMarkerText"
        | "editedMarkerTooltip"
        | "editedMarkerCaption"
        | "showPendingModerationMarker"
        | "pendingModerationText"
        | "emoteSenderName"
    > => ({
        kind: TextualBodyViewModel.getKind(props.mxEvent),
        emoteSenderName: props.mxEvent.sender?.name ?? props.mxEvent.getSender(),
        ...TextualBodyViewModel.computeBodyWrapperSnapshot(props),
        ...TextualBodyViewModel.computeEditedMarkerSnapshot(props),
        ...TextualBodyViewModel.computePendingModerationSnapshot(props),
    });

    private static readonly computeSnapshot = (props: TextualBodyViewModelProps): TextualBodyViewSnapshot => ({
        id: props.id,
        ...TextualBodyViewModel.computeEventSnapshot(props),
    });

    public constructor(props: TextualBodyViewModelProps) {
        super(props, TextualBodyViewModel.computeSnapshot(props));
        this.watchEvent(props.mxEvent);
        this.disposables.track(this.unwatchEvent);
    }

    private readonly onEventContentChanged = (): void => {
        this.snapshot.merge(TextualBodyViewModel.computeEventSnapshot(this.props));
    };

    private watchEvent(mxEvent: MatrixEvent): void {
        if (this.watchedEvent === mxEvent) return;
        this.unwatchEvent();
        this.watchedEvent = mxEvent;
        this.watchedEvent.on(MatrixEventEvent.Replaced, this.onEventContentChanged);
    }

    private readonly unwatchEvent = (): void => {
        this.watchedEvent?.off(MatrixEventEvent.Replaced, this.onEventContentChanged);
        this.watchedEvent = undefined;
    };

    public setId(id: string | undefined): void {
        this.props = {
            ...this.props,
            id,
        };

        this.snapshot.merge({ id });
    }

    public setEvent(mxEvent: MatrixEvent): void {
        this.props = {
            ...this.props,
            mxEvent,
        };
        this.watchEvent(mxEvent);

        this.snapshot.merge(TextualBodyViewModel.computeEventSnapshot(this.props));
    }

    public setHighlightLink(highlightLink: string | undefined): void {
        this.props = {
            ...this.props,
            highlightLink,
        };

        this.snapshot.merge(TextualBodyViewModel.computeBodyWrapperSnapshot(this.props));
    }

    public setReplacingEventId(replacingEventId: string | undefined): void {
        this.props = {
            ...this.props,
            replacingEventId,
        };

        this.snapshot.merge(TextualBodyViewModel.computeEditedMarkerSnapshot(this.props));
    }

    public setIsSeeingThroughMessageHiddenForModeration(
        isSeeingThroughMessageHiddenForModeration: boolean | undefined,
    ): void {
        this.props = {
            ...this.props,
            isSeeingThroughMessageHiddenForModeration,
        };

        this.snapshot.merge(TextualBodyViewModel.computePendingModerationSnapshot(this.props));
    }

    public setTimelineRenderingType(timelineRenderingType: TimelineRenderingType): void {
        this.props = {
            ...this.props,
            timelineRenderingType,
        };
    }

    public onRootClick = (event: MouseEvent<HTMLDivElement>): void => {
        let target: HTMLLinkElement | null = event.target as HTMLLinkElement;

        if (target.dataset?.[LINKIFIED_DATA_ATTRIBUTE]) {
            return;
        }

        if (target.nodeName !== "A") {
            target = target.closest<HTMLLinkElement>("a");
        }

        if (!target) {
            return;
        }

        const localHref = tryTransformPermalinkToLocalHref(target.href);
        if (localHref !== target.href) {
            event.preventDefault();
            window.location.hash = localHref;
        }
    };

    public onBodyActionClick = (event: MouseEvent<HTMLButtonElement>): void => {
        event.preventDefault();

        const starterLink = TextualBodyViewModel.getStarterLink(this.props.mxEvent);
        if (!starterLink) {
            return;
        }

        const managers = IntegrationManagers.sharedInstance();
        if (!managers.hasManager()) {
            managers.openNoManagerDialog();
            return;
        }

        const integrationManager = managers.getPrimaryManager();
        const scalarClient = integrationManager?.getScalarClient();
        scalarClient?.connect().then(() => {
            const completeUrl = scalarClient.getStarterLink(starterLink);
            const integrationsUrl = integrationManager!.uiUrl;
            const { finished } = Modal.createDialog(QuestionDialog, {
                title: _t("timeline|scalar_starter_link|dialog_title"),
                description: <div>{_t("timeline|scalar_starter_link|dialog_description", { integrationsUrl })}</div>,
                button: _t("action|continue"),
            });

            finished.then(([confirmed]) => {
                if (!confirmed) {
                    return;
                }

                const width = Math.min(window.screen.width, 1024);
                const height = Math.min(window.screen.height, 800);
                const left = (window.screen.width - width) / 2;
                const top = (window.screen.height - height) / 2;
                const features = `height=${height}, width=${width}, top=${top}, left=${left},`;
                const wnd = window.open(completeUrl, "_blank", features)!;
                wnd.opener = null;
            });
        });
    };

    public onEditedMarkerClick = (): void => {
        Modal.createDialog(MessageEditHistoryDialog, { mxEvent: this.props.mxEvent });
    };

    public onEmoteSenderClick = (): void => {
        dis.dispatch({
            action: Action.ComposerInsert,
            userId: this.props.mxEvent.getSender(),
            timelineRenderingType: this.props.timelineRenderingType,
        });
    };
}
