import { type Editor, type JSONContent } from '@tiptap/react';
import axios from 'axios';
import { addDays, addHours, addMinutes, addYears, isBefore } from 'date-fns';
import { decodeTime } from 'ulid';
import { type FileAsset } from '@material-hu/types/attachments';

import { logEvent } from 'src/config/amplitude';
import { isDev, isStaging } from 'src/constants/env';
import { EventName } from 'src/types/amplitude';
import {
  ALPHANUMERIC_REGEX,
  CONVERSATIONS_SERVICE_UNAVAILABLE,
  CONVERSATIONS_SPACING,
  MESSAGE_TYPE,
  mentionContentRegex,
  mentionTagRegex,
  numericOnlyRegex,
  TUTORIAL_VIEWED_KEY,
  USER_ALL,
  urlAndMentionCombinedRegex,
  userMentionRegex,
} from 'src/pages/dashboard/Conversations/constants';
import { getMembers } from 'src/pages/dashboard/Conversations/stores/membersStore';
import {
  type BloackMessageReplied,
  BlockType,
  type Channel,
  type ConversationEventSocket,
  type ConversationMessage,
  type ConversationMessageSocket,
  type ConversationUser,
  type CreateMessageParams,
  EndCallReason,
  Format,
  type HighlightedSpan,
  HighlightedSpanType,
  type HuDataChannel,
  type HuDataMessage,
  type LastMessage,
  type Member,
  type Membership,
  MessageTypeSocket,
  MuteOptions,
  type PendingReadStatus,
  PinOptions,
  type PreviewFileItem,
  type SendAudiosType,
  type SendFileItem,
  type SendFilesList,
  type SendFileType,
  type SendGifBody,
  type SendImagesType,
  type SendVideosType,
  StateMessage,
  StatusUser,
  type SummaryReaction,
  type SystemMessage,
  type SystemMessages,
  type TutorialViewedState,
  type UploadGifBody,
  type UserReactionInfo,
} from 'src/pages/dashboard/Conversations/types';
import { FileTypes, type FormFile, GIPHY } from 'src/types/attachments';
import { type MeUser, type User } from 'src/types/user';
import { splitFileName } from 'src/utils/attachments';
import { sizeToBytes } from 'src/utils/bytes';
import { formatPillsDate, isSameUTCDate } from 'src/utils/date';
import { type LokaliseTFunction } from 'src/utils/i18n';
import { idMaker } from 'src/utils/idMaker';
import { getFullName } from 'src/utils/userUtils';

const idGen = idMaker();

const ULID_REGEX = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/i;

export const isValidUlid = (value?: string): value is string =>
  !!value && ULID_REGEX.test(value);

export const createMessage = ({
  channel,
  text,
  ulid,
  attachments = [],
  audio,
  user,
  messageToReply,
  pictureAssetUrl,
}: CreateMessageParams): ConversationMessage => {
  const blocks = messageToReply
    ? createBlockMessageReplied(messageToReply)
    : undefined;
  const huData: HuDataMessage = {
    picture_asset_url: pictureAssetUrl,
    highlighted_spans: extractHighlightedSpans(text || ''),
    message_ts: ulid,
  };
  const formattedAttachments = audio
    ? buildAudioAttachment(audio)
    : attachments;
  return {
    channel,
    client_msg_id: `${idGen.next().value}-internal`,
    ts: ulid,
    user: user.id.toString(),
    username: getFullName(user),
    type: MessageTypeSocket.MESSAGE,
    state: StateMessage.PENDING,
    text,
    blocks,
    attachments: formattedAttachments,
    reactions: [],
    hu_data: huData,
  };
};

export const createBlockMessageReplied = (message: ConversationMessage) =>
  [
    {
      type: BlockType.MESSAGE_REPLIED,
      message_replied_id: message.hu_data.message_ts,
      text: message.text,
      assets: message.attachments,
      sender_membership: {
        user: {
          id: message.user,
          name: message.username,
          hu_data: {
            member: {
              first_name: message.username.split(' ')[0],
              last_name: message.username.split(' ')[1],
            },
          },
        },
      },
    },
  ] as BloackMessageReplied[];

export const createObjectMessage = (
  socketMessage: ConversationMessageSocket,
): ConversationMessage => JSON.parse(socketMessage.json_event);

export const createObjectLatest = (
  newMsg: ConversationMessage | ConversationEventSocket,
): LastMessage => ({
  client_msg_id: newMsg.client_msg_id,
  type: MessageTypeSocket.MESSAGE,
  subtype: newMsg.subtype,
  ts: newMsg.ts,
  text: newMsg.text || '',
  user: newMsg.user,
  username: newMsg.username,
  attachments: newMsg?.attachments,
  blocks: newMsg?.blocks,
  highlighted_spans: newMsg?.hu_data?.highlighted_spans,
  version: newMsg?.hu_data?.version,
});

export const getDateToShow = (
  t: LokaliseTFunction,
  hasNextPage: boolean,
  actualMessageDate: string,
  previousMessageDate: string,
  user: MeUser | null,
): string | null => {
  const actualMessageDateFormated = decodeTime(actualMessageDate);
  const actualDate = new Date(actualMessageDateFormated);
  if (!previousMessageDate) {
    if (hasNextPage) return null;
    return formatPillsDate(actualDate, user, t);
  }

  const previousMessageDateFormated = decodeTime(previousMessageDate);
  const previousDate = new Date(previousMessageDateFormated);

  if (!isSameUTCDate(actualDate, previousDate)) {
    return formatPillsDate(actualDate, user, t);
  }

  return null;
};

export const isMessageFromDifferentDay = (
  currentMessageTs: string,
  prevMessageTs: string,
): boolean => {
  const currentMessageDate = new Date(decodeTime(currentMessageTs));
  const prevMessageDate = new Date(decodeTime(prevMessageTs));
  return !isSameUTCDate(currentMessageDate, prevMessageDate);
};

/**
 * Checks if the current message is older than the new one.
 * Returns true if the new message is more recent.
 */
export const isNewerMessage = ({
  newTs,
  currentTs,
}: {
  newTs?: string;
  currentTs?: string;
}): boolean => {
  if (!newTs || !currentTs) return false;
  const newDate = new Date(decodeTime(newTs));
  const currentDate = new Date(decodeTime(currentTs));
  return isBefore(currentDate, newDate);
};

/**
 * Finds the index where to insert a message so the list stays ordered by message_ts (oldest first).
 * Uses decodeTime for comparison. If the new message is older than the last, it goes at the end;
 * otherwise walks backwards until finding a message older than the new one and inserts after it.
 */
export const findInsertIndexForMessage = (
  messages: Array<{ hu_data: { message_ts: string } }>,
  newMessage: { hu_data: { message_ts: string } },
): number => {
  if (messages.length === 0) return 0;
  const newTs = decodeTime(newMessage.hu_data.message_ts);
  for (let i = messages.length - 1; i >= 0; i--) {
    if (decodeTime(messages[i].hu_data.message_ts) < newTs) return i + 1;
  }
  return 0;
};

const firstLetterUpper = (word: string) => word.charAt(0).toUpperCase();

export const getChatablesForList = (
  member: ConversationUser,
  oldChatables: {
    character: string;
    chatables: Member[];
  }[],
) => {
  let response = [...oldChatables];
  const newElem = { ...member.hu_data.member };
  if (response.length === 0) {
    response = [
      {
        character: firstLetterUpper(newElem.first_name),
        chatables: [newElem],
      },
    ];
  } else if (
    response[response.length - 1].character ===
    firstLetterUpper(newElem.first_name)
  ) {
    response[response.length - 1].chatables = [
      ...response[response.length - 1].chatables,
      newElem,
    ];
  } else {
    response = [
      ...response,
      {
        character: firstLetterUpper(newElem.first_name),
        chatables: [newElem],
      },
    ];
  }

  return response;
};

/**
 * Compares two IDs to determine if they are equal.
 *
 * @param {string | number} id1 - The first ID to compare.
 * @param {string | number} id2 - The second ID to compare.
 * @returns {boolean} True if the IDs are equal, false otherwise.
 */
export const compareIds = (
  id1?: string | number,
  id2?: string | number,
): boolean => {
  if (!id1 || !id2) return false;
  return Number(id1) === Number(id2);
};

export const formateDataImage = (file: any): SendImagesType => ({
  id: file.id,
  image_url: file.url,
  image_bytes: Math.round(sizeToBytes(file.size)),
  image_height: file.height,
  image_width: file.width,
  mimetype: file.contentType,
  filename: file.name,
  metadata: {
    format: file.type?.toLowerCase(),
  },
});

export const formateDataVideo = (file: any) => ({
  id: file.id,
  key: file.key,
  video_url: file.url,
  size: Math.round(sizeToBytes(file.size)),
  video_html_height: file.height,
  video_html_width: file.width,
  mimetype: file.contentType,
  filename: file.name,
  metadata: {
    format: file.type?.toLowerCase(),
    durationInMs: file.metadata?.durationInMs,
  },
});

export const formateDataFile = (file: any) => ({
  id: file.id,
  url: file.url,
  size: Math.round(sizeToBytes(file.size)),
  mimetype: file.contentType,
  filename: file.name,
  metadata: {
    format: file.type?.toLowerCase(),
  },
});

export const formatDataAudio = (
  file: FileAsset & { duration?: string; isLoading?: boolean },
): SendAudiosType => ({
  id: file.id,
  url: file.url,
  size: Math.round(sizeToBytes(file.size || '0')),
  mimetype: file.contentType || 'audio/webm',
  filename: file.name,
  audio_duration_ms: Number(file.duration),
  metadata: {
    format: Format.AUDIO,
  },
});

export const buildAudioAttachment = (values: {
  blob: Blob;
  duration: string;
}): SendFilesList => [
  {
    isLoading: true,
    id: idGen.next().value,
    filename: 'internal-audio-file',
    url: URL.createObjectURL(values.blob),
    size: values.blob.size,
    audio_duration_ms: Number(values.duration),
    mimetype: 'audio/webm',
    metadata: {
      format: Format.AUDIO,
    },
  },
];

export const gifWidthAndHeightCalculator = (gifSize: {
  width: number;
  height: number;
}) => {
  let width = 0;
  let height = 0;

  const aspectRatio = gifSize.width / gifSize.height;
  if (aspectRatio > 1) {
    width = CONVERSATIONS_SPACING.gifHorizontalWidth;
  }
  if (aspectRatio < 1) {
    width = CONVERSATIONS_SPACING.gifVerticalWidth;
  }
  if (aspectRatio === 1) {
    width = CONVERSATIONS_SPACING.gifSquareWidth;
  }
  height = Math.round(width / aspectRatio);
  return { width, height };
};

export const formateDataGif = (file: UploadGifBody): SendGifBody => {
  return {
    filename: file.name,
    id: file.id,
    image_bytes: 0,
    image_height: file.height,
    image_width: file.width,
    mimetype: 'image/gif',
    metadata: {
      format: Format.GIF,
    },
    external_reference: {
      origin: GIPHY,
      id: file.external_reference.id,
    },
  };
};

export const getMembersNamesOfGroup = (
  members: Member[],
  membersCount: number | undefined,
  loggedUserId: number | undefined,
  t: LokaliseTFunction,
) => {
  if (!membersCount) return '';
  const limitMembers = membersCount < 11;

  const membersNotDeleted = members.filter(elem => !elem.deleted_at);
  const membersWithOutLoggedUser = membersNotDeleted
    .filter(elem => elem.id !== loggedUserId)
    .sort((a, b) => a.first_name.localeCompare(b.first_name))
    .slice(0, 9)
    .map(elem => elem.first_name);

  membersWithOutLoggedUser.push(t('you'));

  const membersToShow = !limitMembers
    ? `${membersWithOutLoggedUser.join(', ')} ...`
    : membersWithOutLoggedUser.join(', ');

  return membersToShow;
};

export const getMembersToShow = (
  showAllMembers: boolean,
  members: Member[],
  userId: number | undefined,
) => {
  let currentUser: Member | null = null;
  const admins: Member[] = [];
  const regularUsers: Member[] = [];

  for (const member of members) {
    if (member.deleted_at) continue;

    if (member.id === userId) {
      currentUser = member;
    } else if (member.is_admin) {
      admins.push(member);
    } else {
      regularUsers.push(member);
    }
  }

  const sortAlphabetically = (a: Member, b: Member) => {
    const aName = `${a.first_name} ${a.last_name}`.toLowerCase();
    const bName = `${b.first_name} ${b.last_name}`.toLowerCase();
    return aName.localeCompare(bName);
  };

  admins.sort(sortAlphabetically);
  regularUsers.sort(sortAlphabetically);

  const sortedMembers = [
    ...(currentUser ? [currentUser] : []),
    ...admins,
    ...regularUsers,
  ];

  return !showAllMembers ? sortedMembers : sortedMembers.slice(0, 4);
};

export const getFulltextJoined = (
  usernames: string[],
  userCount: number,
  isUser: boolean,
  t: LokaliseTFunction,
  username: string,
  userIds: number[],
  userId: number | undefined,
) => {
  if (!isUser && userId && userIds.includes(userId)) {
    return t('chat:system_messages:channel_joined_current_user', {
      emitter: username,
    });
  }

  const usernamesText = usernames.join(', ');
  const startText = isUser
    ? t('chat:system_messages:channel_joined_by_user', {
        user: usernamesText,
      })
    : t('chat:system_messages:channel_joined_by_others', {
        emitter: username,
        users: usernamesText,
      });

  const remaining = userCount - usernames.length;

  const remainingText =
    remaining > 0
      ? ` ${t('general:and')} ${t('chat:system_messages:more_collaborators', { count: remaining })}`
      : '';

  return `${startText} ${remainingText}`;
};

export const getEnabledSendAudio = (hasFiles: boolean, body: string) =>
  !(hasFiles || body.trim());

const getValidFileFormat = (fileType: Format) => {
  if (fileType === Format.IMAGE) {
    return Format.IMAGE;
  }
  return fileType === Format.VIDEO ? Format.VIDEO : Format.FILE;
};

export const createPreviewFiles = (
  fileType: Format,
  files: File[],
): PreviewFileItem[] =>
  files.map(file => ({
    id: `${idGen.next().value}-file`,
    isLoading: true,
    image_url: fileType === Format.IMAGE ? URL.createObjectURL(file) : '',
    video_url: fileType === Format.VIDEO ? URL.createObjectURL(file) : '',
    file: (fileType === Format.FILE || fileType === Format.VIDEO) && file,
    name: file?.name,
    filename: file?.name,
    mimetype: file?.type,
    metadata: {
      format: getValidFileFormat(fileType),
    },
  }));

export const getUnreadMessagesLabel = (
  t: LokaliseTFunction,
  count: number,
): string => {
  if (count > 99) return t('chat:messages.max_unreads');
  return t('chat:messages.unreads', { count });
};

export const deduplicateConversations = (
  conversations: Channel[],
): Channel[] => {
  const map = new Map<string, Channel>();
  for (const conv of conversations) {
    if (!map.has(conv.id)) {
      map.set(conv.id, conv);
    }
  }
  return Array.from(map.values());
};

const getImageText = (attachments: SendFileItem[], t: LokaliseTFunction) =>
  t('chat:files:image', { count: attachments.length });

const getVideoText = (attachments: SendFileItem[], t: LokaliseTFunction) =>
  t('chat:files:video', { count: attachments.length });

type GetDisplayUserParams = {
  isGroupConversation: boolean;
  isUser: boolean;
  username?: string;
  t: LokaliseTFunction;
};
export const getDisplayUser = ({
  isGroupConversation,
  isUser,
  t,
  username,
}: GetDisplayUserParams) => {
  if (!isGroupConversation) return false;
  const userDisplay = isUser ? t('chat:you') : username;
  return `${userDisplay}: `;
};

type GetSystemMessageTextParams = {
  text?: string;
  t: LokaliseTFunction;
  username: string;
  systemMessages: SystemMessages | null;
  isUser?: boolean;
  isSidebarMsg?: boolean;
  attachments?: SendFileItem[];
  userId?: number;
  isKnowBlock?: boolean;
  isDeleted?: boolean;
};

export const getText = ({
  t,
  text = '',
  attachments,
}: {
  t: LokaliseTFunction;
  text?: string;
  attachments?: SendFileItem[];
}): string => {
  const hasAttachments = !!attachments?.length;
  if (hasAttachments) {
    const attachment = attachments[0];
    if (
      attachment.metadata.format === FileTypes.IMAGE.toLocaleLowerCase() ||
      attachment.metadata.format === FileTypes.VIDEO.toLocaleLowerCase()
    ) {
      const images = attachments.filter(att => isImageAttachment(att));
      const videos = attachments.filter(att => isVideoAttachment(att));
      if (images.length === attachments.length) {
        return getImageText(attachments, t);
      }
      if (videos.length === attachments.length) {
        return getVideoText(attachments, t);
      }
      return `${getImageText(images, t)}, ${getVideoText(videos, t)}`;
    }

    if (attachment.metadata.format === FileTypes.GIF.toLocaleLowerCase()) {
      return t('chat:files:gif');
    }

    if (attachment.metadata.format === FileTypes.FILE.toLocaleLowerCase()) {
      const lastFile = attachments[attachments.length - 1];
      return lastFile.filename;
    }

    if (attachment.metadata.format === FileTypes.AUDIO.toLocaleLowerCase()) {
      return t('chat:files:audio');
    }
  }
  return text;
};

export const getSystemMessageText = ({
  text = '',
  t,
  username,
  systemMessages,
  isUser = false,
  isSidebarMsg = false,
  attachments = [],
  userId = undefined,
  isKnowBlock = true,
  isDeleted = false,
}: GetSystemMessageTextParams): string => {
  if (userId === null) {
    return '';
  }

  if (isDeleted) {
    return isUser
      ? t('chat:delete:message_deleted_you')
      : t('chat:delete:message_deleted_user');
  }

  if (
    !systemMessages ||
    systemMessages.length === 0 ||
    isReplyMessage(systemMessages)
  ) {
    if (isSidebarMsg) {
      return getText({ t, text, attachments });
    }

    return text;
  }

  if (!isKnowBlock) return t('chat:messages:block_not_available');

  const systemMessage = systemMessages[0];

  switch (systemMessage.type) {
    case BlockType.CREATED:
      return isUser
        ? t('chat:system_messages:channel_created_by_user')
        : t('chat:system_messages:channel_created_by_others', {
            emitter: username,
          });

    case BlockType.JOINED:
      return getFulltextJoined(
        systemMessage.usernames,
        systemMessage.user_count,
        isUser,
        t,
        username,
        systemMessage.user_ids,
        userId,
      );
    case BlockType.RENAMED:
      return isUser
        ? t('chat:system_messages:channel_renamed_by_user', {
            old: systemMessage.old_name,
            new: systemMessage.new_name,
          })
        : t('chat:system_messages:channel_renamed_by_others', {
            emitter: username,
            old: systemMessage.old_name,
            new: systemMessage.new_name,
          });

    case BlockType.PICTURE_CHANGED:
      return isUser
        ? t('chat:system_messages:channel_picture_changed_by_user')
        : t('chat:system_messages:channel_picture_changed_by_others', {
            emitter: username,
          });
    case BlockType.LEFT: {
      const isSameUser = username === systemMessage.username;
      const isUserLeft = systemMessage.user_id === userId;
      if (isSameUser)
        return t('chat:system_messages:channel_left_by_others', {
          emitter: username,
        });
      if (isUserLeft)
        return t('chat:system_messages:user_deleted_you', {
          emitter: username,
        });
      return isUser
        ? t('chat:system_messages:channel_kick_by_user', {
            user: systemMessage.username,
          })
        : t('chat:system_messages:channel_kick_by_others', {
            emitter: username,
            users: systemMessage.username,
          });
    }
    case BlockType.CALL_INITIALIZED:
      return t('chat:system_messages:call_initialized', {
        emitter: `${systemMessage.member.first_name} ${systemMessage.member.last_name}`,
      });
    case BlockType.CALL_ENDED:
      if (systemMessage.reason === EndCallReason.NO_RESPONSE) {
        return t('chat:system_messages:call_missed_by_others');
      }
      if (systemMessage.reason === EndCallReason.REJECTED) {
        return t('chat:system_messages:call_refused');
      }

      return t('chat:system_messages:call_ended');
    case BlockType.CHANNEL_DESCRIPTION_CHANGED:
      return isUser
        ? t('chat:system_messages:channel_description_changed_by_user')
        : t('chat:system_messages:channel_description_changed_by_others', {
            emitter: username,
          });

    case BlockType.MESSAGE_FORWARDED:
      return getText({ t, text, attachments });
    case BlockType.MESSAGE_PIN_ADDED:
      return isUser
        ? t('chat:system_messages:message_pin_added_by_user')
        : t('chat:system_messages:message_pin_added_by_other', {
            emitter: username,
          });
    default:
      return text || '';
  }
};

/**
 * Returns user information (picture and name) for a conversation.
 *
 * - If it is a self-conversation, returns the logged-in user's info.
 * - If it is a one-to-one conversation, returns the other user's info.
 * - For group conversations, returns nothing.
 *
 * @param {Function} t - Translation function used to translate labels like "YOU".
 * @param {boolean} isSelfConversation - Whether the conversation is with oneself.
 * @param {boolean} isGroupConversation - Whether the conversation is a group conversation.
 * @param {HuDataChannel} huData - Data object containing information about the other user.
 * @param {User} oneSelfUser - Current logged-in user data.
 * @returns {{ picture: string | undefined; name: string } | undefined} Object with user's picture and name, or undefined for group conversations.
 */

export const getDataUserItem = (
  t: LokaliseTFunction,
  isSelfConversation: boolean,
  isGroupConversation: boolean,
  huData: HuDataChannel,
  oneSelfUser: MeUser | null,
): { picture: string | undefined; name: string } | undefined => {
  if (isGroupConversation || !oneSelfUser) return;
  if (isSelfConversation) {
    const picture =
      huData.other_membership?.user.hu_data.member.picture_asset_url;
    return {
      picture,
      name: `${oneSelfUser.firstName} ${oneSelfUser.lastName} (${t('chat:you')})`,
    };
  }
  return {
    picture: huData.other_membership?.user.hu_data.member.picture_asset_url,
    name: `${huData.other_membership?.user.hu_data.member.first_name} ${huData.other_membership?.user.hu_data.member.last_name}`,
  };
};

export const getIsSelfConversation = (
  otherUserId: number | undefined,
  loggedUserId: number | undefined,
  isGroupConversation: boolean,
): boolean =>
  Boolean(
    loggedUserId &&
      otherUserId &&
      otherUserId === loggedUserId &&
      !isGroupConversation,
  );

/**
 * Checks if a block has a recognized type defined in the BlockType enum.
 *
 * @param blocks - An Array of objects with at least a `type` property of type string.
 * @returns `true` if the block's type exists in BlockType, otherwise `false`.
 */
export const isKnownBlockType = (blocks: SystemMessage[]): boolean => {
  if (!blocks || !blocks?.length) return false;
  return blocks.every(block =>
    Object.values(BlockType).includes(block.type as BlockType),
  );
};

export const isReplyMessage = (blocks: SystemMessage[]): boolean => {
  if (!blocks || !blocks?.length) return false;
  return blocks.some(block => block.type === BlockType.MESSAGE_REPLIED);
};

export const isForwardedMessage = (blocks: SystemMessage[]): boolean => {
  if (!blocks || !blocks?.length) return false;
  return blocks.some(block => block.type === BlockType.MESSAGE_FORWARDED);
};

/**
 * Determines the type of message based on attachments and text.
 *
 * @param attachments - A list of files being sent with the message.
 * @param text - The plain text content of the message.
 * @returns The message type, which can be text-only, image, image with text, file, or file with text.
 */
export const getTypeMessage = (
  attachments: SendFileItem[] = [],
  text: string = '',
) => {
  const isEmptyText = text.trim() === '';

  if (attachments.length === 0) {
    return MESSAGE_TYPE.TEXT;
  }

  const isOnlyImages = attachments.every(isImageAttachment);
  const isOnlyVideos = attachments.every(isVideoAttachment);
  const isOnlyFiles = attachments.every(isFileAttachment);
  const isOnlyAudio = attachments.every(isAudioAttachment);
  const isVideoAndImages =
    attachments.some(isVideoAttachment) && attachments.some(isImageAttachment);

  if (isVideoAndImages) {
    return isEmptyText
      ? MESSAGE_TYPE.IMAGES_AND_VIDEO
      : MESSAGE_TYPE.IMAGES_AND_VIDEO_AND_TEXT;
  }

  if (isOnlyImages) {
    return isEmptyText ? MESSAGE_TYPE.IMAGE : MESSAGE_TYPE.IMAGE_AND_TEXT;
  }

  if (isOnlyVideos) {
    return isEmptyText ? MESSAGE_TYPE.VIDEO : MESSAGE_TYPE.VIDEO_AND_TEXT;
  }

  if (isOnlyFiles) {
    return isEmptyText ? MESSAGE_TYPE.FILE : MESSAGE_TYPE.FILE_AND_TEXT;
  }

  if (isOnlyAudio) {
    return MESSAGE_TYPE.VOICE_NOTE;
  }

  return MESSAGE_TYPE.TEXT;
};

/**
 * Determines the type of message based on files.
 *
 * @param files - A list of files being sent with the message.
 * @returns The message type, which can be image, video, file, or text.
 */
export const getTypeMessageFromFiles = (files: FormFile[]) => {
  const isOnlyImages = files.every(file => file.type === FileTypes.IMAGE);
  const isOnlyVideos = files.every(file => file.type === FileTypes.VIDEO);
  const isOnlyFiles = files.every(file => file.type === FileTypes.FILE);
  const hasImages = files.some(file => file.type === FileTypes.IMAGE);
  const hasVideos = files.some(file => file.type === FileTypes.VIDEO);
  const hasFiles = files.some(file => file.type === FileTypes.FILE);
  const isVideoAndImages = hasVideos && hasImages;
  const isImagesAndFiles = hasImages && hasFiles;
  const isVideosAndFiles = hasVideos && hasFiles;
  const isImagesAndVideosAndFiles = hasImages && hasVideos && hasFiles;

  if (isImagesAndVideosAndFiles) {
    return MESSAGE_TYPE.IMAGES_AND_VIDEOS_AND_FILE;
  }
  if (isImagesAndFiles) {
    return MESSAGE_TYPE.IMAGES_AND_FILE;
  }
  if (isVideosAndFiles) {
    return MESSAGE_TYPE.VIDEOS_AND_FILE;
  }
  if (isVideoAndImages) {
    return MESSAGE_TYPE.IMAGES_AND_VIDEO;
  }
  if (isOnlyImages) {
    return MESSAGE_TYPE.IMAGE;
  }

  if (isOnlyVideos) {
    return MESSAGE_TYPE.VIDEO;
  }

  if (isOnlyFiles) {
    return MESSAGE_TYPE.FILE;
  }
};

type GetMessageContentTypeParams = {
  attachments: SendFileItem[];
  text: string;
};

/**
 * Determines the content type of a message based on its attachments and text.
 *
 * @param {Object} params - The parameters object.
 * @param {Array} params.attachments - An array of attachments for the message.
 * @param {string} params.text - The text content of the message.
 * @returns {Object} An object indicating whether the message has only one image, only text, or only text or image.
 */
export const getMessageContentType = ({
  attachments,
  text,
}: GetMessageContentTypeParams) => {
  const hasOnlyOneImage =
    attachments?.length === 1 && isImageAttachment(attachments[0]);
  const hasOnlyText = attachments?.length === 0 && text.length > 0;

  return {
    hasOnlyOneImage,
    hasOnlyText,
    hasOnlyTextOrImage: hasOnlyOneImage || hasOnlyText,
  };
};

/**
 * Calculates the total count of reactions from an array of reactions
 *
 * @param reactions - Array of reactions with count property
 * @returns The total count of all reactions combined
 */
export const getTotalReactionsCount = (
  reactions: SummaryReaction[],
): number => {
  return reactions.reduce((sum, reaction) => sum + reaction.count, 0);
};

/**
 * Builds a map keyed by emoji name → sorted users (logged user first).
 * Computed once per reactions/members change; tab switching is a O(1) lookup.
 */
export const buildReactionUsersMap = (
  reactions: SummaryReaction[],
  loggedUserId: number | undefined,
): Map<string, UserReactionInfo[]> =>
  reactions.reduce((map, reaction) => {
    const users = loggedUserId
      ? [...reaction.users].sort((a, b) => {
          if (a.id === loggedUserId) return -1;
          if (b.id === loggedUserId) return 1;
          return 0;
        })
      : reaction.users;
    map.set(reaction.name, users);
    return map;
  }, new Map<string, UserReactionInfo[]>());

export const formatTimeInPlayAudio = (seconds: number) => {
  const mins = Math.floor(seconds / 60);
  const secs = Math.floor(seconds % 60);
  return `${mins}:${String(secs).padStart(2, '0')}`;
};

export const formatTotalTimeAudio = (timeAudio: number) => {
  if (!timeAudio) return '0:00';
  const totalSeconds = Math.floor(timeAudio / 1000);
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;
  return `${minutes}:${String(seconds).padStart(2, '0')}`;
};

export const getDescriptionToForwardMessage = (
  users: ConversationUser[],
  conversations: Channel[],
) => {
  const userNames = users.map(user => user.name);

  const conversationNames = conversations.map(conv => {
    if (conv.is_channel) {
      return conv.name || '';
    } else {
      return conv.hu_data.other_membership?.user?.name || '';
    }
  });

  return [...userNames, ...conversationNames].filter(Boolean).join(', ');
};

/**
 * Converts Tiptap editor content to backend format, transforming mentions to <@U{id}> format
 *
 * @param editor - The Tiptap editor instance
 * @returns Text with mentions in backend format <@U{id}>
 */
export const formatValueMessage = (editor: Editor): string => {
  if (!editor) return '';

  const json = editor.getJSON();
  let result = '';

  const processNode = (node: JSONContent) => {
    if (node.type === 'text') {
      result += node.text || '';
    } else if (node.type === 'mention') {
      if (node.attrs?.id === USER_ALL.id.toString()) {
        result += USER_ALL.employee_internal_id;
      } else {
        result += `<@U${node.attrs?.id}>`;
      }
    } else if (node.type === 'hardBreak') {
      result += '\n';
    } else if (node.type === 'paragraph') {
      if (node.content) {
        node.content.forEach(processNode);
      }
    } else if (node.content) {
      node.content.forEach(processNode);
    }
  };

  if (json.content) {
    json.content.forEach(processNode);
  }

  return result;
};

export const convertUserToMember = (user: User | MeUser): Member => {
  return {
    id: user.id,
    first_name: user.firstName,
    last_name: user.lastName,
    picture_url: user.profilePicture || '',
    employee_internal_id: user.employeeInternalId,
    status: (user as any).status,
    is_admin: null,
    deleted_at: user.deleted ? Date.now() : 0,
  };
};

/**
 * Checks if a text contains a mention (<@U{numbers}> or <!channel>)
 *
 * @param text - The text to check for mentions
 * @returns True if the text contains a mention, false otherwise
 */
export const getIsMention = (text?: string) => {
  if (!text) return false;

  return (
    userMentionRegex.test(text) || text.includes(USER_ALL.employee_internal_id)
  );
};

/**
 * Extracts highlighted_spans from text containing user mention tags
 * Optimized for performance with early returns and efficient regex processing
 *
 * @param text - Text containing user mentions in format <@U123>
 * @returns Array of highlighted_spans with user mention information
 */
export const extractHighlightedSpans = (text: string): HighlightedSpan[] => {
  // Early return: if no text or no user tags, return immediately
  if (!text || !getIsMention(text)) {
    return [];
  }
  const membersMap = getMembers();
  const highlightedSpans: HighlightedSpan[] = [];

  for (const match of text.matchAll(mentionTagRegex)) {
    const start = match.index ?? 0;
    const end = start + match[0].length;
    const isUserMention = match[1] !== undefined;
    const member = membersMap[match[1]];
    const fullName = getFullName({
      firstName: member?.first_name,
      lastName: member?.last_name,
    });
    highlightedSpans.push({
      type: isUserMention
        ? HighlightedSpanType.USER_MENTION
        : HighlightedSpanType.CHANNEL_MENTION,
      user_id: isUserMention ? parseInt(match[1], 10) : USER_ALL.id,
      start,
      end,
      full_name: fullName,
    });
  }

  return highlightedSpans;
};

/**
 * If the text contains mentions (<@Uid> or <!channel>), replaces each with @name.
 * User mentions use full_name from API; channel mention (<!channel>) uses 'all'.
 *
 * @param text - Text that may contain mentions
 * @param highlightedSpans - Spans with start, end, type and full_name from API
 * @returns Text with mentions replaced by name or all
 */
export const replaceMentionIdsWithFullName = (
  text: string,
  highlightedSpans: HighlightedSpan[] = [],
): string => {
  if (!text || !getIsMention(text)) return text;
  if (!highlightedSpans.length) return text;
  const sortedSpans = [...highlightedSpans].sort((a, b) => a.start - b.start);
  let lastEnd = 0;
  const parts: string[] = [];
  for (const span of sortedSpans) {
    const name =
      span.full_name ??
      (span.type === HighlightedSpanType.CHANNEL_MENTION
        ? USER_ALL.last_name
        : text.slice(span.start, span.end));
    parts.push(text.slice(lastEnd, span.start), `@${name}`);
    lastEnd = span.end;
  }
  parts.push(text.slice(lastEnd));
  return parts.join('');
};

/**
 * Converts backend message format to TipTap JSON format for editing
 * Transforms <@U{id}> mentions to TipTap mention nodes
 *
 * @param messageText - The original message text with <@U{id}> format
 * @param members - Record of members keyed by user ID
 * @param mentionSpans - Array of mention spans with positions and user IDs
 * @param t - Translation function
 * @returns TipTap JSON content with mention nodes
 */

type ParamsConvertMessageToTipTapFormat = {
  messageText: string;
  mentionSpans?: HighlightedSpan[];
  t: LokaliseTFunction;
};
export const convertMessageToTipTapFormat = ({
  messageText,
  mentionSpans = [],
  t,
}: ParamsConvertMessageToTipTapFormat): JSONContent => {
  if (!messageText.length) {
    return {
      type: 'doc',
      content: [{ type: 'paragraph', content: [] }],
    };
  }

  const content: JSONContent[] = [];
  const spans = [...mentionSpans].sort((a, b) => a.start - b.start);

  let globalIndex = 0;
  let spanIndex = 0;

  while (globalIndex < messageText.length) {
    const nextSpan = spans[spanIndex];
    const spanStart = nextSpan ? nextSpan.start : Number.POSITIVE_INFINITY;

    const nextLineBreakIndex = messageText.indexOf('\n', globalIndex);
    const nextNewline =
      nextLineBreakIndex === -1 ? Number.POSITIVE_INFINITY : nextLineBreakIndex;

    const nextStop = Math.min(spanStart, nextNewline);

    if (nextStop > globalIndex) {
      const chunk = messageText.substring(globalIndex, nextStop);
      if (chunk) {
        content.push({ type: 'text', text: chunk });
      }
      globalIndex = nextStop;
      continue;
    }

    if (nextNewline === globalIndex) {
      if (globalIndex < messageText.length - 1) {
        content.push({ type: 'hardBreak' });
      }
      globalIndex += 1;
      continue;
    }

    if (spanStart === globalIndex) {
      const span = nextSpan;

      if (span.type === HighlightedSpanType.CHANNEL_MENTION) {
        content.push({
          type: 'mention',
          attrs: {
            id: USER_ALL.id.toString(),
            label: USER_ALL.last_name,
          },
        });
      } else if (span.full_name) {
        content.push({
          type: 'mention',
          attrs: {
            id: span.user_id.toString(),
            label: span.full_name,
          },
        });
      } else {
        content.push({
          type: 'text',
          text: t('chat:messages.mention_not_available'),
        });
      }

      globalIndex = span.end;
      spanIndex += 1;
      continue;
    }

    content.push({ type: 'text', text: messageText[globalIndex] });
    globalIndex += 1;
  }

  return {
    type: 'doc',
    content: [{ type: 'paragraph', content }],
  };
};

/**
 * Formats a user or member to a User object
 *
 * @param user - The user or member to format
 * @param isExternalUsers - Whether the user is an external user
 * @returns The formatted User object
 */
export const formatUser = (
  user: any,
  isExternalUsers: boolean,
  isGroupPath: boolean,
): Partial<User> => {
  const formatExternalUserAsUser = (member: Member): Partial<User> => ({
    id: member.id,
    firstName: member.first_name,
    lastName: member.last_name,
    profilePicture: member.picture_url,
  });

  if (isExternalUsers) {
    return formatExternalUserAsUser(user as Member);
  }
  if (isGroupPath) {
    return user.user as User;
  }
  return user as User;
};

/**
 * Separates text into parts identifying URLs and user mentions in <@U123> format
 *
 * @param text - The original text to process
 * @returns Array of objects with part content and type (text, url, mention)
 */
export const separateTextParts = (text: string) => {
  if (!text) return [];

  const parts: Array<{
    content: string;
    type: 'text' | 'url' | 'mention';
    userId?: number;
    isAll?: boolean;
  }> = [];

  let lastIndex = 0;

  for (const match of text.matchAll(urlAndMentionCombinedRegex)) {
    const matchIndex = match.index ?? 0;

    // Add text before the match if there is any
    if (matchIndex > lastIndex) {
      const textBefore = text.substring(lastIndex, matchIndex);
      if (textBefore) {
        parts.push({
          content: textBefore,
          type: 'text',
        });
      }
    }

    const matchedText = match[0];

    // Check if it's a URL
    if (matchedText.startsWith('http') || matchedText.startsWith('www.')) {
      parts.push({
        content: matchedText,
        type: 'url',
      });
    } else if (matchedText.startsWith('<@U')) {
      const userIdMatch = matchedText.match(mentionContentRegex);
      const userIdString = userIdMatch ? userIdMatch[1] : undefined;
      const userId =
        userIdString && numericOnlyRegex.test(userIdString)
          ? parseInt(userIdString, 10)
          : undefined;

      if (!userId) {
        parts.push({
          content: matchedText,
          type: 'text',
        });
        lastIndex = matchIndex + matchedText.length;
        continue;
      }

      const mentionPart: {
        content: string;
        type: 'mention';
        userId: number;
      } = {
        content: matchedText,
        type: 'mention',
        userId,
      };

      parts.push(mentionPart);
    } else if (matchedText.startsWith(USER_ALL.employee_internal_id)) {
      parts.push({
        content: USER_ALL.last_name,
        type: 'mention',
        userId: USER_ALL.id,
        isAll: true,
      });
    }

    lastIndex = matchIndex + matchedText.length;
  }

  // Add remaining text after the last match
  if (lastIndex < text.length) {
    const remainingText = text.substring(lastIndex);
    if (remainingText) {
      parts.push({
        content: remainingText,
        type: 'text',
      });
    }
  }

  return parts;
};

export const getUserFullNameFromId = (
  userId: number,
  highlightedSpans: HighlightedSpan[],
): string => {
  const highlightedSpan = highlightedSpans.find(
    span => span.user_id === userId,
  );

  if (highlightedSpan?.full_name) return highlightedSpan.full_name;

  const member = getMembers()[userId.toString()];

  logEvent(EventName.CHATS_2_MENTION_USER_NOT_IN_HIGHLIGHTED_SPANS, {
    userId,
    highlightedSpan: highlightedSpan ?? null,
    member: member ?? null,
  });
  if (member)
    return getFullName({
      firstName: member.first_name,
      lastName: member.last_name,
    });

  return '';
};

type GetOtherUserStatusParams = {
  isGroupConversation: boolean;
  conversation: Channel;
};

export const getOtherUserStatus = ({
  isGroupConversation,
  conversation,
}: GetOtherUserStatusParams) => {
  if (isGroupConversation || !conversation)
    return { isDeleted: false, isInactive: false, otherMember: undefined };
  const otherUser = conversation?.hu_data?.other_membership?.user;
  const otherMember = otherUser?.hu_data.member;
  const isDeleted = otherUser?.deleted || false;
  const isInactive = otherMember?.status === StatusUser.INACTIVE || false;
  return { isDeleted, isInactive, otherMember };
};

export const sortMembersAlphabetically = (members: Member[]) => {
  return members.sort((a, b) => {
    const fullNameA = `${a.first_name} ${a.last_name}`.toLowerCase().trim();
    const fullNameB = `${b.first_name} ${b.last_name}`.toLowerCase().trim();
    return fullNameA.localeCompare(fullNameB);
  });
};

/**
 * Detects the position of an input element in the viewport
 *
 * @param id - The ID of the input element to detect
 * @returns The position of the input element in the viewport
 */
export const detectInputPosition = (id: string): number | null => {
  const inputElement = document.getElementById(id);
  if (!inputElement) return null;

  const rect = inputElement.getBoundingClientRect();
  const inputBottom = window.innerHeight - rect.top;
  return inputBottom;
};

/**
 * Extracts the default message from the URL search parameters
 *
 * @param searchParams - The URL search parameters to extract the default message from
 * @returns The default message or an empty string if not found
 */
export const getDefaultMessage = (searchParams: URLSearchParams) => {
  const meParam = searchParams.get('me');
  if (!meParam) return '';

  try {
    const decoded = decodeURIComponent(meParam);
    const trimmed = decoded.trim();
    return trimmed;
  } catch {
    return '';
  }
};

export const getCorrectIconType = (attachments: SendFilesList) => {
  const images = attachments.filter(att => isImageAttachment(att));
  const videos = attachments.filter(att => isVideoAttachment(att));
  if (images.length && videos.length) {
    return Format.IMAGE;
  }
  return attachments[0].metadata.format;
};
/**
 * Returns the message state based on the message's read status and user's id
 *
 * @param readAt - The read timestamp of the message
 * @param receivedAt - The received timestamp of the message
 * @param messageState - The state of the message, if it is provided, it will be returned instead of the calculated state
 * @param isSelfConversation - Whether the message is from the logged user
 * @returns The message state
 */
export const getMessageState = ({
  readAt,
  receivedAt,
  messageState,
  isSelfConversation,
  isErrorSendMessage,
}: {
  readAt?: number;
  receivedAt?: number;
  messageState?: StateMessage;
  isSelfConversation: boolean;
  isErrorSendMessage?: boolean;
}) => {
  if (messageState) return messageState;
  if (isSelfConversation) return StateMessage.READ;
  if (isErrorSendMessage) return StateMessage.PENDING;

  const isMessageRead = !!readAt && readAt > 0;
  const isMessageReceived = !!receivedAt && receivedAt > 0;

  if (!isMessageRead && isMessageReceived) return StateMessage.RECEIVED;
  if (isMessageRead) return StateMessage.READ;

  return StateMessage.DELIVERED;
};

const getOptions = (type: 'mute' | 'pin') => {
  return type === 'mute' ? MuteOptions : PinOptions;
};

export const buildOptions = (t: LokaliseTFunction, type: 'mute' | 'pin') => [
  ...Object.keys(getOptions(type)).reduce(
    (acc, value) => {
      if ((isDev || isStaging) && value === '1m') return acc;
      acc.push({
        label: t(`${type}.dialog.options.${value.toLowerCase()}`),
        value: value.toLowerCase(),
      });
      return acc;
    },
    [] as { label: string; value: string }[],
  ),
];

export const getTimeFromOption = (time: string): number => {
  const now = Date.now();
  switch (time) {
    case '1m':
      return addMinutes(now, 1).getTime();
    case '8h':
      return addHours(now, 8).getTime();
    case '24h':
      return addHours(now, 24).getTime();
    case '1w':
      return addDays(now, 7).getTime();
    case '30d':
      return addDays(now, 30).getTime();
    default:
      return addYears(now, 500).getTime();
  }
};

/**
 * Trims leading and trailing spaces from a string and replaces internal spaces with plus signs
 *
 * @param text - The text to process
 * @returns The processed text with spaces replaced by plus signs
 */
export const replaceSpacesWithPlus = (text: string): string => {
  return text.trim().replace(/\s+/g, '+');
};

export const generateFilename = (filename: string | null): string => {
  if (!filename) return '';
  const { name } = splitFileName(filename);
  return replaceSpacesWithPlus(name);
};
/**
 * Check if the received version of the conversation is the most recent version
 *
 * @param receivedVersion - The received version of the conversation
 * @param currentVersion - The current version of the conversation
 * @returns True if the received version is the most recent version, false otherwise
 */
export const requireUpdate = (
  receivedVersion?: number,
  currentVersion?: number,
) => {
  if (!receivedVersion || !currentVersion) return true;
  return receivedVersion > currentVersion;
};

/**
 * Validates if the search query is valid for searching messages.
 * - Must contain at least one letter or number
 * - Cannot be only whitespace
 * - Cannot contain only special characters
 */
export const isValidSearchQuery = (query: string): boolean => {
  if (!query) return false;

  const trimmedQuery = query.trim();
  if (!trimmedQuery) return false;

  // Check if query contains at least one alphanumeric character (letter or number)
  return ALPHANUMERIC_REGEX.test(trimmedQuery);
};
export const flatMapPages = <T, R>(
  pages: T[] | undefined,
  fn: (p: T) => R[] | undefined,
): R[] => (pages ?? []).flatMap(p => fn(p) ?? []);

export const getMembersFromMemberships = (
  memberships: Membership[],
): Member[] =>
  memberships.reduce<Member[]>((members, membership) => {
    const member = membership.user?.hu_data?.member;
    if (!member) return members;
    members.push({
      ...member,
      is_admin: membership.is_admin || false,
      deleted_at: membership.deleted_at || 0,
    });
    return members;
  }, []);

const escapeRegex = (value: string): string =>
  value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

export const splitTextBySearchQuery = (text: string, query: string) => {
  let result: { parts: string[] };

  if (query?.length) {
    const escapedQuery = escapeRegex(query);
    // Expand each match to its full non-whitespace token
    const pattern = `(\\S*${escapedQuery}\\S*)`;
    result = { parts: text.split(new RegExp(pattern, 'gi')) };
  } else {
    result = {
      parts: [text],
    };
  }

  return result;
};
/**
 * Gets the tutorial viewed state from localStorage
 * Returns a hashmap where keys are user IDs and values are booleans
 *
 * @returns The tutorial viewed state object
 */
const getTutorialViewedState = (): TutorialViewedState => {
  try {
    const stored = localStorage.getItem(TUTORIAL_VIEWED_KEY);
    return stored ? JSON.parse(stored) : {};
  } catch {
    return {};
  }
};

/**
 * Checks if a specific user has already viewed the tutorial
 *
 * @param userId - The user ID to check
 * @returns True if the user has viewed the tutorial, false otherwise
 */
export const hasUserViewedTutorial = (userId: number | undefined): boolean => {
  if (!userId) return false;
  const state = getTutorialViewedState();
  return state[userId.toString()] === true;
};

/**
 * Marks that a specific user has viewed the tutorial
 * This preserves the viewed state for other users
 *
 * @param userId - The user ID to mark as having viewed the tutorial
 */
export const markTutorialAsViewed = (userId: number | undefined): void => {
  if (!userId) return;
  const state = getTutorialViewedState();
  state[userId.toString()] = true;
  localStorage.setItem(TUTORIAL_VIEWED_KEY, JSON.stringify(state));
};

export const getScaledDimensions = (
  maxHeight: number,
  maxWidth: number,
  videoWidth: number = 1,
  videoHeight: number = 1,
) => {
  const scale = Math.min(1, maxHeight / videoHeight, maxWidth / videoWidth);

  return {
    width: videoWidth * scale,
    height: videoHeight * scale,
  };
};

export const isConversationsMaintenanceHttpError = (
  error: unknown,
): boolean => {
  if (!axios.isAxiosError(error)) {
    return false;
  }
  const status = error.response?.status;
  return status != null && status === CONVERSATIONS_SERVICE_UNAVAILABLE;
};

// --- Attachment type guards ---

export const isVideoAttachment = (
  item: SendFileItem,
): item is SendVideosType & { isNotSecure?: boolean } =>
  item.metadata.format === Format.VIDEO;

export const isImageAttachment = (
  attachment: SendFileItem,
): attachment is SendImagesType => {
  return attachment.metadata.format === Format.IMAGE;
};

export const isGifAttachment = (
  attachment: SendFileItem,
): attachment is SendGifBody => {
  return attachment.metadata.format === Format.GIF;
};

export const isFileAttachment = (
  attachment: SendFileItem,
): attachment is SendFileType => {
  return attachment.metadata.format === Format.FILE;
};

export const isAudioAttachment = (
  attachment: SendFileItem,
): attachment is SendAudiosType => {
  return attachment.metadata.format === Format.AUDIO;
};

export const getMediaUrl = (attachment: SendFileItem) => {
  if (isImageAttachment(attachment)) {
    return attachment.image_url;
  }
  if (isVideoAttachment(attachment)) {
    return attachment.local_video_url || attachment.video_url;
  }
  return '';
};

export const makePreviewItem = (
  id: string,
  file: File,
  format: Format,
): PreviewFileItem => ({
  id,
  isLoading: true,
  image_url: format === Format.IMAGE ? URL.createObjectURL(file) : '',
  video_url: format === Format.VIDEO ? URL.createObjectURL(file) : '',
  file: format === Format.FILE ? file : false,
  name: file.name,
  filename: file.name,
  mimetype: file.type,
  metadata: { format },
});

export const formatCompletedItem = (
  fileAsset: FileAsset,
  preview: PreviewFileItem,
): SendFileItem => {
  const format = preview.metadata.format;
  const objectUrl = preview.image_url || preview.video_url || '';

  if (format === Format.IMAGE) {
    return {
      isNotSecure: true,
      ...formateDataImage(fileAsset),
      local_image_url: objectUrl,
    };
  }
  if (format === Format.VIDEO) {
    return {
      isNotSecure: true,
      ...formateDataVideo(fileAsset),
      local_video_url: objectUrl,
    };
  }
  return formateDataFile(fileAsset);
};

export const upsertPendingReadStatus = (
  pendingReadStatus: Record<string, PendingReadStatus>,
  channel: string,
  next: PendingReadStatus,
) => {
  const current = pendingReadStatus[channel];
  if (
    !current ||
    isNewerMessage({
      newTs: next.toMessageTs,
      currentTs: current.toMessageTs,
    })
  ) {
    pendingReadStatus[channel] = next;
  }
};
