import {LocalEvent, LocalEventTypes} from '@config/synchroniser';
import {TaggedUser} from '@interfaces/taggedUsers';
import {
  InsertMessage,
  SelectAsset,
  SelectMessage,
  SelectMessageWithAssets,
  SelectReaction,
} from '@modules/chats/db/interfaces';
import {
  ConversationLatest,
  Message,
  MessageEvent,
  MessageStatus,
  ChatUserStatus,
  BlockMessageReplied,
  BlockType,
  HighlightedSpan,
  ChatEventType,
  ChatEventSubtype,
  HighlightedSpanType,
  SearchMessageMatchItem,
  SearchMessageMatchStoreItem,
  GeneratedBy,
} from '@modules/chats/interfaces';
import {ALL_MENTION, LOCAL_INITIAL_VERSION} from '@modules/chats/constants';
import {usersMapper} from '@modules/chats/instances/UsersMapper';
import {persistedUserId} from '@services/localStorage';
import {clamp} from '@shared/utils';

import {mapBlocksFromDB, mapBlocksToDB} from './blocksMapper';
import {
  mapAssetFromDB,
  mapAssetFromService,
  sortAssets,
  sortAssetsFromBlocks,
} from './assetMapper';
import {mapReactionsFromDB} from './reactionMapper';
import {getFullNameForMention, getTextWithHighlights} from '../mentions';
import {isMessageForwarded} from '../blocks';

export const EXCLUSIVE_SUBTYPES = [
  ChatEventSubtype.channelLeft,
  ChatEventSubtype.channelJoined,
  ChatEventSubtype.channelCreated,
  ChatEventSubtype.channelChanged,
  ChatEventSubtype.callInitialized,
  ChatEventSubtype.callEnded,
  ChatEventSubtype.messagePinAdded,
];

export const mapMessageFromDB = ({
  message,
  assets,
  reactions,
}: {
  message: SelectMessage;
  assets?: SelectAsset[];
  reactions?: SelectReaction[];
}): Message => {
  const formattedReactions = reactions
    ? mapReactionsFromDB(reactions)
    : undefined;
  const highlightedSpans = message.highlighted_spans
    ? JSON.parse(message.highlighted_spans)
    : [];
  const blocks = message.blocks ? mapBlocksFromDB(message.blocks) : undefined;
  const forwarded = isMessageForwarded({blocks});

  const deliverStatus = {
    read_at: message.read_at ?? null,
    received_at: message.received_at ?? null,
  };

  const user = usersMapper.get(message.user_id);

  return {
    text: message.text ?? '',
    user: `${message.user_id}`,
    channel: message.conversation_id,
    ts: message.ts,
    latest_event_ts: message.latest_event_ts ?? undefined,
    status: message.status as MessageStatus,
    version: message.version ?? 0,
    blocks: blocks ? sortAssetsFromBlocks(blocks) : undefined,
    height: message.height ?? -1,
    edited: message.edited ?? false,
    hidden: message.hidden ?? false,
    client_msg_id: message.id,
    attachments: assets?.length ? mapAssetFromDB(assets) : [],
    reactions: formattedReactions,
    highlighted_spans: highlightedSpans,
    formatted_text: message.formatted_text ?? message.text ?? '',
    deliver_status: deliverStatus,
    deleted_from_ui: message.deleted_from_ui ?? false,
    forwarded,
    associated_font_scale: message.associated_font_scale ?? undefined,
    username: user
      ? `${user.first_name} ${user.last_name}`
      : message.sender_name,
    sender_profile_picture: user
      ? user.profile_picture ?? undefined
      : message.sender_profile_picture ?? undefined,
    generated_by: message.generated_by
      ? mapGeneratedByFromDB(message.generated_by)
      : undefined,
  };
};

export const mapMessageToDB = (message: Message): InsertMessage => {
  return {
    conversation_id: message.channel,
    user_id: Number(message.user),
    text: message.text,
    id: message.client_msg_id,
    ts: message.ts,
    latest_event_ts: message.latest_event_ts ?? undefined,
    sender_name: message.username,
    sender_profile_picture: message.sender_profile_picture,
    version: message.version,
    status: message.status,
    blocks: message.blocks ? mapBlocksToDB(message.blocks) : null,
    edited: message.edited ?? false,
    hidden: message.hidden ?? false,
    highlighted_spans: JSON.stringify(message.highlighted_spans),
    formatted_text: message.formatted_text ?? message.text ?? '',
    height: message.height,
    read_at: message.deliver_status?.read_at ?? null,
    received_at: message.deliver_status?.received_at ?? null,
    associated_font_scale: message.associated_font_scale ?? undefined,
    deleted_from_ui: message.deleted_from_ui ?? false,
    generated_by: message.generated_by
      ? mapGeneratedByToDB(message.generated_by)
      : null,
  };
};

export const mapLatestToMessage = ({
  message,
  channel,
}: {
  message: ConversationLatest;
  channel: string;
}): Message => {
  const forwarded = isMessageForwarded(message);
  return {
    attachments: message.attachments,
    text: message.text ?? '',
    user: `${message.user}`,
    username: message.username,
    channel,
    ts: message.ts,
    latest_event_ts: undefined,
    status: message.status as MessageStatus,
    version: message.version ?? LOCAL_INITIAL_VERSION,
    blocks: message.blocks,
    height: message.attachments?.length ? 0 : -1,
    edited: message.edited,
    hidden: message.hidden,
    client_msg_id: message.client_msg_id,
    highlighted_spans: message.highlighted_spans,
    formatted_text: message.formatted_text ?? message.text ?? '',
    deliver_status: message.deliver_status,
    deleted_from_ui: message.deleted_from_ui ?? false,
    forwarded,
    associated_font_scale: message.associated_font_scale ?? undefined,
    generated_by: message.generated_by,
  };
};

export const mapLatestMessageFromDB = (
  message: SelectMessageWithAssets,
): ConversationLatest => {
  const highlightedSpans = message.highlighted_spans
    ? JSON.parse(message.highlighted_spans)
    : [];
  return {
    text: message.text || '',
    ts: message.ts,
    user: `${message.user_id}`,
    client_msg_id: message.id,
    status: message.status as MessageStatus,
    version: message.version || 0,
    username: message.sender_name,
    blocks: message.blocks
      ? sortAssetsFromBlocks(mapBlocksFromDB(message.blocks))
      : undefined,
    hidden: message.hidden ?? false,
    highlighted_spans: highlightedSpans,
    formatted_text: message.formatted_text ?? message.text ?? '',
    attachments: message.assets?.length ? mapAssetFromDB(message.assets) : [],
    deliver_status: {
      read_at: message.read_at ?? null,
      received_at: message.received_at ?? null,
    },
    associated_font_scale: message.associated_font_scale ?? undefined,
    deleted_from_ui: message.deleted_from_ui ?? false,
    generated_by: message.generated_by
      ? mapGeneratedByFromDB(message.generated_by)
      : undefined,
  };
};

/**
 * Map a message from the server (from endpoints, socket events) to a message object
 * @param message - The message from the server
 */
export const mapMessageFromServer = async (
  message: MessageEvent,
): Promise<Message> => {
  const highlightedSpans = message.hu_data.highlighted_spans;
  const formattedText = highlightedSpans?.length
    ? await getTextWithHighlights({
        message: message.text,
        highlightedSpans,
      })
    : message.text;
  const forwarded = isMessageForwarded(message);
  return {
    text: message.text,
    user: `${message.hu_data.sender_id}`,
    username: message.username,
    channel: message.channel,
    ts: message.hu_data.message_ts,
    latest_event_ts: message.ts,
    client_msg_id: message.client_msg_id,
    sender_profile_picture: message.hu_data.picture_asset_url,
    version: message.hu_data.version,
    height: message.attachments?.length ? 0 : -1,
    attachments: mapAssetFromService(message.attachments ?? []),
    edited: !!message.edited,
    hidden: !!message.hidden,
    blocks: message.blocks ? sortAssetsFromBlocks(message.blocks) : undefined,
    status:
      persistedUserId === Number(message.hu_data.sender_id)
        ? MessageStatus.Sent
        : undefined,
    reactions: message.reactions,
    highlighted_spans: highlightedSpans,
    formatted_text: formattedText,
    deliver_status: message.hu_data.read_status,
    deleted_from_ui:
      !!message.hidden &&
      message.type === ChatEventType.message &&
      EXCLUSIVE_SUBTYPES.includes(message.subtype!),
    forwarded,
    generated_by: message.hu_data.generated_by,
  };
};

export const mapMessageWithLocalEvents = (
  message: Message,
  localEvents: LocalEvent[],
): Message => {
  const updatedMessage = {
    ...message,
  };

  for (const update of localEvents) {
    switch (update.type) {
      case LocalEventTypes.CHATS2_DELETE_MESSAGE:
        updatedMessage.hidden = true;
        break;
      case LocalEventTypes.CHATS2_EDIT_MESSAGE:
        updatedMessage.text = update.data.text;
        updatedMessage.formatted_text =
          update.data.formatted_text ?? update.data.text;
        updatedMessage.edited = true;
        updatedMessage.highlighted_spans = update.data.highlighted_spans ?? [];
        break;
    }
  }
  return updatedMessage;
};

export const mapMessageToReplyBlock = (
  replyMessage: Message,
): BlockMessageReplied[] => {
  return [
    {
      type: BlockType.MessageReplied,
      text: replyMessage.text,
      message_replied_id: replyMessage.ts,
      assets: sortAssets(replyMessage.attachments || []),
      blocks: replyMessage.blocks
        ? sortAssetsFromBlocks(replyMessage.blocks)
        : [],
      highlighted_spans: replyMessage.highlighted_spans,
      sender_membership: {
        id: '',
        version: 0,
        user: {
          hu_data: {
            member: {
              id: Number(replyMessage.user),
              first_name: replyMessage.username,
              last_name: '',
              employee_internal_id: replyMessage.user,
              picture_asset_url: replyMessage.sender_profile_picture || null,
              status: ChatUserStatus.Active,
              email: null,
              version: 0,
            },
          },
          id: Number(replyMessage.user),
          team_id: '',
          name: replyMessage.username,
        },
      },
    },
  ];
};

export const mapHighlightedSpansToTaggedUsers = (
  highlightedSpans: HighlightedSpan[],
  formattedText: string,
): TaggedUser[] => {
  let searchFromIndex = 0;

  return highlightedSpans.map(span => {
    if (span.type === HighlightedSpanType.AllMention) {
      const mentionWithAt = `@${ALL_MENTION.label}`;
      const start = formattedText.indexOf(mentionWithAt, searchFromIndex);
      if (start !== -1) {
        searchFromIndex = start + mentionWithAt.length;
      }
      return {
        start,
        length: mentionWithAt.length,
        type: {
          user: {
            id: ALL_MENTION.id,
          },
        },
      };
    } else {
      const memberInfo = usersMapper.get(span.user_id);
      const name = getFullNameForMention({user: memberInfo, span});
      const mentionWithAt = `@${name}`;

      const start = formattedText.indexOf(mentionWithAt, searchFromIndex);

      if (start !== -1) {
        searchFromIndex = start + mentionWithAt.length;
      }

      return {
        start,
        length: mentionWithAt.length,
        type: {
          user: {
            id: span.user_id,
          },
        },
      };
    }
  });
};

/**
 * Find the latest query word match position in the message text to determine the view position for search results.
 * @param text
 * @param query
 * @returns The view position of the latest match, clamped between 0 and 1.
 */
function findLastMatch(text: string, query: string): number {
  const words = query.toLowerCase().split(/\s+/);
  const textLower = text.toLowerCase();
  const textLen = text.length;
  let resViewPosition = -1;

  for (const word of words) {
    const index = textLower.lastIndexOf(word);

    if (index !== -1) {
      const currentViewPosition = index / textLen; // 0..1

      // enough to consider the match is the latest
      if (currentViewPosition >= 0.9) {
        return currentViewPosition;
      }

      if (currentViewPosition > resViewPosition) {
        resViewPosition = currentViewPosition;
      }
    }
  }

  if (resViewPosition === -1) {
    return 0.5; // default to the middle if no match is found
  }

  return clamp(resViewPosition, 0, 1);
}

export const mapSearchMessageMatchesFromServer = (
  data: SearchMessageMatchItem[],
  query: string,
): SearchMessageMatchStoreItem[] => {
  return data.map(item => {
    const position = findLastMatch(item.item.text, query);

    return {
      ts: item.item.hu_data.message_ts,
      viewPosition: parseFloat(position.toFixed(1)),
    };
  });
};

export const mapGeneratedByFromDB = (
  generatedBy: string,
): GeneratedBy | undefined => {
  try {
    return JSON.parse(generatedBy) as GeneratedBy;
  } catch {
    //
  }
};

export const mapGeneratedByToDB = (generatedBy: GeneratedBy): string => {
  return JSON.stringify(generatedBy);
};
