import { MouseEvent, MouseEventHandler, useCallback, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Editor } from 'slate';
import { ReactEditor } from 'slate-react';
import { EventType, IContent, Room } from 'matrix-js-sdk';
import type { ReactionEventContent } from 'matrix-js-sdk/lib/types';

import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useOpenUserRoomProfile } from '../../../state/hooks/userRoomProfile';
import { useSpaceOptionally } from '../../../hooks/useSpace';
import { createMentionElement, moveCursor } from '../../../components/editor';
import {
  getEditedEvent,
  getEventReactions,
  getMemberDisplayName,
  getReactionContent,
} from '../../../utils/room';
import { eventWithShortcode, factoryEventSentBy, getMxIdLocalPart } from '../../../utils/matrix';
import { IReplyDraft } from '../../../state/room/roomInputDrafts';
import { getChannelsThreadPath } from '../../../pages/pathUtils';

export type UseMessageInteractionHandlersOptions = {
  room: Room;
  // Slate editor that owns this hook's caller composer (main timeline →
  // channel/DM/legacy editor, drawer → thread composer editor). Mention
  // insertion + reply-draft focus target this instance.
  editor: Editor;
  // True when the composer associated with `editor` is currently
  // unmounted/hidden. Main timeline passes `threadDrawerOpen` (drawer
  // open hides main composer); drawer passes `false`. When suspended,
  // the username-click mention insert no-ops to avoid writing into a
  // hidden Slate instance the user can't see.
  composerSuspended: boolean;
  // Reply-draft setter targeting the right composer scope. Caller
  // subscribes via `useSetAtom(roomIdToReplyDraftAtomFamily(...))` —
  // main timeline scopes to `[roomId, 'main']`, drawer scopes to
  // `[roomId, rootId]`.
  setReplyDraft: (draft: IReplyDraft | undefined) => void;
  // Caller-specific scroll-to-event for reply-chip clicks. Main timeline
  // scrolls via virtual paginator + setFocusItem; drawer scrolls via DOM
  // scrollIntoView on the matching `data-message-id` node.
  onOpenEvent: (eventId: string) => void;
  // Channels-mode flags that gate the «Reply in Thread» navigate branch
  // in `handleReplyClick`. Drawer hides the ThreadPlus affordance via
  // `hideThreadReplyAffordance` so the branch is dead from drawer in
  // practice; the hook stays generic for symmetry.
  channelsMode: boolean;
  isBridged: boolean;
  spaceIdOrAlias?: string;
  roomIdOrAlias?: string;
};

export type MessageInteractionHandlers = {
  editId: string | undefined;
  handleEdit: (editEvtId?: string) => void;
  handleOpenReply: MouseEventHandler;
  handleUserClick: MouseEventHandler<HTMLButtonElement>;
  handleUsernameClick: MouseEventHandler<HTMLButtonElement>;
  handleReplyClick: (ev: MouseEvent<HTMLButtonElement>, startThread?: boolean) => void;
  handleReactionToggle: (targetEventId: string, key: string, shortcode?: string) => void;
};

// Wiring layer for `<Message>` event-handler props, shared between
// `RoomTimeline` (main column) and `ThreadDrawer` (thread column). Each
// hook instance owns its own `editId` — editing in the drawer doesn't
// flip a row in the main timeline into edit mode and vice versa, which
// matches the user's mental model of two independent conversations.
export function useMessageInteractionHandlers({
  room,
  editor,
  composerSuspended,
  setReplyDraft,
  onOpenEvent,
  channelsMode,
  isBridged,
  spaceIdOrAlias,
  roomIdOrAlias,
}: UseMessageInteractionHandlersOptions): MessageInteractionHandlers {
  const mx = useMatrixClient();
  const navigate = useNavigate();
  const space = useSpaceOptionally();
  const openUserRoomProfile = useOpenUserRoomProfile();

  const [editId, setEditId] = useState<string>();

  const handleOpenReply: MouseEventHandler = useCallback(
    (evt) => {
      const targetId = evt.currentTarget.getAttribute('data-event-id');
      if (!targetId) return;
      onOpenEvent(targetId);
    },
    [onOpenEvent]
  );

  const handleUserClick: MouseEventHandler<HTMLButtonElement> = useCallback(
    (evt) => {
      evt.preventDefault();
      evt.stopPropagation();
      const userId = evt.currentTarget.getAttribute('data-user-id');
      if (!userId) {
        // eslint-disable-next-line no-console
        console.warn('Button should have "data-user-id" attribute!');
        return;
      }
      openUserRoomProfile(
        room.roomId,
        space?.roomId,
        userId,
        evt.currentTarget.getBoundingClientRect()
      );
    },
    [room, space, openUserRoomProfile]
  );

  const handleUsernameClick: MouseEventHandler<HTMLButtonElement> = useCallback(
    (evt) => {
      evt.preventDefault();
      // Suspended composer: editor is unmounted or hidden, so inserting
      // a mention would write into a Slate instance the user can't see
      // and the focus call would no-op silently. RoomTimeline passes
      // `threadDrawerOpen` for this — drawer always passes `false`.
      if (composerSuspended) return;
      const userId = evt.currentTarget.getAttribute('data-user-id');
      if (!userId) {
        // eslint-disable-next-line no-console
        console.warn('Button should have "data-user-id" attribute!');
        return;
      }
      const name = getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
      editor.insertNode(
        createMentionElement(
          userId,
          name.startsWith('@') ? name : `@${name}`,
          userId === mx.getUserId()
        )
      );
      ReactEditor.focus(editor);
      moveCursor(editor);
    },
    [mx, room, editor, composerSuspended]
  );

  const handleReplyClick = useCallback(
    (evt: MouseEvent<HTMLButtonElement>, startThread = false) => {
      const replyId = evt.currentTarget.getAttribute('data-event-id');
      if (!replyId) {
        // eslint-disable-next-line no-console
        console.warn('Button should have "data-event-id" attribute!');
        return;
      }
      const replyEvt = room.findEventById(replyId);
      if (!replyEvt) return;
      // Channels: «Reply in Thread» opens the right-side drawer instead
      // of stuffing a thread relation into the channel composer's reply
      // draft. Bridged rooms (Telegram puppets etc.) have no thread
      // semantic on the bridge side — fall back to the legacy
      // m.in_reply_to draft path so messages still go through.
      if (startThread && channelsMode && !isBridged && spaceIdOrAlias && roomIdOrAlias) {
        // useParams returns the encoded URL value; getChannelsThreadPath
        // re-encodes via generatePath so we decode once first to avoid
        // double-encoding (matches `routeParent.ts` decode pattern).
        let decodedSpace: string;
        let decodedRoom: string;
        try {
          decodedSpace = decodeURIComponent(spaceIdOrAlias);
          decodedRoom = decodeURIComponent(roomIdOrAlias);
        } catch {
          decodedSpace = spaceIdOrAlias;
          decodedRoom = roomIdOrAlias;
        }
        navigate(getChannelsThreadPath(decodedSpace, decodedRoom, replyId));
        return;
      }
      const editedReply = getEditedEvent(replyId, replyEvt, room.getUnfilteredTimelineSet());
      const content: IContent = editedReply?.getContent()['m.new_content'] ?? replyEvt.getContent();
      const { body, formatted_body: formattedBody } = content;
      const { 'm.relates_to': relation } = startThread
        ? { 'm.relates_to': { rel_type: 'm.thread', event_id: replyId } }
        : replyEvt.getWireContent();
      const senderId = replyEvt.getSender();
      if (senderId && typeof body === 'string') {
        setReplyDraft({
          userId: senderId,
          eventId: replyId,
          body,
          formattedBody,
          relation,
        });
        setTimeout(() => ReactEditor.focus(editor), 100);
      }
    },
    [room, setReplyDraft, editor, channelsMode, isBridged, navigate, spaceIdOrAlias, roomIdOrAlias]
  );

  const handleReactionToggle = useCallback(
    (targetEventId: string, key: string, shortcode?: string) => {
      const relations = getEventReactions(room.getUnfilteredTimelineSet(), targetEventId);
      const allReactions = relations?.getSortedAnnotationsByKey() ?? [];
      const [, reactionsSet] = allReactions.find(([k]) => k === key) ?? [];
      const reactions = reactionsSet ? Array.from(reactionsSet) : [];
      const myUserId = mx.getUserId();
      const myReaction = myUserId ? reactions.find(factoryEventSentBy(myUserId)) : undefined;

      if (myReaction && !!myReaction?.isRelation()) {
        const reactionId = myReaction.getId();
        if (reactionId) mx.redactEvent(room.roomId, reactionId);
        return;
      }
      const rShortcode =
        shortcode ||
        (reactions.find(eventWithShortcode)?.getContent().shortcode as string | undefined);
      mx.sendEvent(
        room.roomId,
        EventType.Reaction,
        getReactionContent(targetEventId, key, rShortcode) as ReactionEventContent
      );
    },
    [mx, room]
  );

  const handleEdit = useCallback(
    (editEvtId?: string) => {
      if (editEvtId) {
        setEditId(editEvtId);
        return;
      }
      setEditId(undefined);
      ReactEditor.focus(editor);
    },
    [editor]
  );

  return {
    editId,
    handleEdit,
    handleOpenReply,
    handleUserClick,
    handleUsernameClick,
    handleReplyClick,
    handleReactionToggle,
  };
}
