import {StackActions} from '@react-navigation/native';
import {chatMembershipsHandler} from '@config/synchroniser';
import {EventsSocketsType} from '@config/constants';
import {trackCustomHumandError} from '@hooks/useGeneralError';
import {ListenerExtraData} from '@interfaces/sockets';
import {
  AdminAddedEvent,
  AdminAddedEventSocket,
  AdminRemovedEvent,
  AdminRemovedEventSocket,
  ChannelChangedEvent,
  ChannelChangedEventSocket,
  ChannelCreatedEvent,
  ChannelJoinedEvent,
  ChannelLeftEvent,
  ConversationMarkedEvent,
  ImCreatedEvent,
  JoinedChatEvent,
  LeftChatEvent,
  Message,
  NewChannelEvent,
  NewMessageEvent,
  NewMessageSocketEvent,
  ReadChatEvent,
  MessageEditedEvent,
  ReactionEvent,
  MessageDeletedEvent,
  HighlightedSpanType,
  AssetChangedEvent,
  MarkedReceivedEvent,
  MarkedReadEventSocket,
  MarkedReadEvent,
  ChannelInfoChangedEvent,
  PinAddedEvent,
  PinRemovedEvent,
  JsonEventSocket,
  MessageReadByAllEvent,
} from '@modules/chats/interfaces';
import {getConversationInfo, getUserInfo} from '@modules/chats/services';
import {getMessageById, updateConversation} from '@modules/chats/db';
import {CHATS_ERROR} from '@modules/chats/constants';
import {useGroups} from '@modules/chats/store/zustand/useGroups';
import {
  ConversationsUpdaterService,
  MembershipUpdaterService,
  UsersUpdaterService,
  ReactionsUpdaterService,
  UnreadsUpdaterService,
  AssetsUpdaterService,
  MessagesUpdaterService,
  OrchestratorUpdaterService,
  PinnedMessagesUpdaterService,
} from '@modules/chats/instances/updaters';
import {FileMessagesLogger} from '@modules/chats/instances/FileMessagesLogger';
import {navigationRef} from '@navigation/navigator';
import {persistedUserId} from '@services/localStorage';
import {logAmplitudeEvent, sentryLogs} from '@shared/utils';
import {AMPLITUDE_EVENTS} from '@shared/constants';
import {pendingNotificationsService} from '@shared/notifications/NotificationManager/PendingNotificationsService';

import {
  mapAssetFromDB,
  mapAssetFromService,
  mapImEventToConversation,
  mapMembershipFromServer,
  mapMessageFromServer,
} from './mappers';
import {
  logConversationInfoDBValidationIssues,
  logNewMessageEventDBValidationIssues,
  logReactionEventDBValidationIssues,
  logAssetChangedEventDBValidationIssues,
  logMembershipDBValidationIssues,
  logUserDBValidationIssues,
} from './validators';
import {visibleChatStore} from '../store';
import {conversationMarker} from '../instances/ConversationMarker';

/**
 * Set of message ids that have been processed.
 * This is used to avoid processing the same message multiple times between push notifications and socket events.
 */
const messageProcessed = new Map<
  string,
  Pick<Message, 'client_msg_id' | 'version'>
>();

type MessageSource = 'socket' | 'push' | 'events';

type WithSource<T> = T & {_source?: MessageSource};

const getMessageSource = (data: object): MessageSource => {
  if ('_source' in data) {
    return data._source as MessageSource;
  }
  return 'socket';
};
export const processMessage = (client_msg_id: string, version: number) => {
  messageProcessed.set(client_msg_id, {client_msg_id, version});
};

export const wasMessageProcessed = (client_msg_id: string, version: number) => {
  const processedMessage = messageProcessed.get(client_msg_id);
  return !!processedMessage && processedMessage.version >= version;
};

const hasJsonEvent = (data: object): data is {json_event: string} => {
  return 'json_event' in data && typeof data.json_event === 'string';
};

const parseEvent = <T>(data: object, type?: EventsSocketsType) => {
  try {
    const eventFormat: T = hasJsonEvent(data)
      ? (JSON.parse(data.json_event) as T)
      : (data as T);
    return eventFormat;
  } catch (error) {
    sentryLogs.error({
      message: 'Failed to parse event',
      module: 'CHATS',
      component: 'socketEvents',
      data: {
        type,
        error: error instanceof Error ? error.message : String(error),
      },
    });

    return null;
  }
};

export const handleNewMessage = async (
  data: WithSource<NewMessageSocketEvent | NewMessageEvent>,
  extra?: ListenerExtraData,
) => {
  const source = getMessageSource(data);

  const eventFormat = parseEvent<NewMessageEvent>(data, extra?.type);

  if (!eventFormat) {
    /**
     *  If the parsing failed, we need to remove this event from pending notifications,
     *  check if "data.ts" exists to avoid errors
     */
    if (data.ts) {
      pendingNotificationsService.remove(data.ts);
    }
    return;
  }

  try {
    logNewMessageEventDBValidationIssues(eventFormat, source);
    const wasProcessed = wasMessageProcessed(
      eventFormat.client_msg_id,
      eventFormat.hu_data.version,
    );

    if (wasProcessed && source !== 'events') {
      return;
    }

    FileMessagesLogger.logMessageTs(
      eventFormat?.hu_data?.message_ts ?? data.ts,
    );

    const message: Message = await mapMessageFromServer(eventFormat);

    processMessage(message.client_msg_id, message.version);

    const isCurrentUserMessage = persistedUserId === Number(message.user);
    const isCurrentConversation =
      visibleChatStore.getState().id === eventFormat.channel;
    const shouldMarkAsRead = isCurrentConversation;
    // Increment unread count for socket/push events, but not for channel created events (this subtype won't increment unread count)
    const shouldIncrementUnread =
      !isCurrentConversation &&
      !isCurrentUserMessage &&
      source !== 'events' &&
      !!eventFormat.hu_data.count_unread;
    const [conversation, {isFromDB, isFromServer}] =
      await getConversationInfoWithLocalState(eventFormat.channel);
    if (isFromServer) {
      logConversationInfoDBValidationIssues(conversation, eventFormat);
    }

    if (!isCurrentUserMessage) {
      conversationMarker.markMessageAsReceived(
        eventFormat.channel,
        eventFormat.hu_data.message_ts,
      );
    }

    const hasMentionInSpans = message.highlighted_spans?.some(
      span =>
        (span.type === HighlightedSpanType.AllMention &&
          !isCurrentUserMessage) ||
        (span.type === HighlightedSpanType.Mention &&
          span.user_id === persistedUserId),
    );

    const mentioned = hasMentionInSpans || conversation.mentioned;

    let latest = conversation.latest;

    if (!latest?.ts || message.ts > latest.ts) {
      latest = message;
    }

    await ConversationsUpdaterService.saveNewConversation({
      ...conversation,
      ...(isFromDB &&
        conversation.latest?.ts !== latest?.ts && {
          version: conversation.version + 1,
        }),
      latest,
      mentioned,
    });
    if (isFromDB) {
      updateConversation({
        id: message.channel,
        latestMessageId: message.client_msg_id,
        mentioned,
      });
    }
    if (shouldMarkAsRead) {
      conversationMarker.markConversationAsRead(
        eventFormat.channel,
        eventFormat.hu_data.message_ts,
      );
    }

    if (shouldIncrementUnread) {
      UnreadsUpdaterService.incrementUnread(
        eventFormat.channel,
        eventFormat.hu_data.message_ts,
      );
    }

    await OrchestratorUpdaterService.processMessage(message);

    if (isFromServer && conversation.other_user) {
      UsersUpdaterService.saveUsers([conversation.other_user]);
    }
  } catch (error) {
    // Remove from processed set so the event can be retried via pendingEvents or next sync
    messageProcessed.delete(eventFormat.client_msg_id);
    sentryLogs.error({
      message: 'handleNewMessage failed — event will be retried',
      module: 'CHATS',
      component: 'socketEvents:handleNewMessage',
      data: {
        client_msg_id: eventFormat.client_msg_id,
        channel: eventFormat.channel,
        source,
        error: error instanceof Error ? error.message : String(error),
      },
    });
    trackCustomHumandError(error, CHATS_ERROR);
    // Re-throw so EventsProcessor fires onError instead of onSuccess.
    // Without this, processEventsStore marks the event as processed even though
    // the message was never saved, and future syncs will skip it permanently.
    throw error;
  } finally {
    pendingNotificationsService.remove(eventFormat.ts);
  }
};

export const handleReadChat = async (
  data: ReadChatEvent | ConversationMarkedEvent,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<ConversationMarkedEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }

  if (Number(eventFormat.user) === persistedUserId) {
    const lastRead = eventFormat.hu_data?.membership?.last_read;
    const eventVersion = eventFormat.hu_data?.membership?.version;
    const currentUnreads = UnreadsUpdaterService.get(eventFormat.channel);

    if (!lastRead) {
      return;
    }

    // Skip if the event's membership version is older than the one we already
    // have. Server bumps version monotonically for every read-state mutation,
    // so a lower value means this event is stale (e.g. arrived out of order).
    if (
      eventVersion !== undefined &&
      currentUnreads &&
      eventVersion < currentUnreads.version
    ) {
      sentryLogs.info({
        message:
          'handleReadChat — skipped stale channel_marked event (version)',
        module: 'CHATS',
        component: 'handleReadChat',
        data: {
          channel: eventFormat.channel,
          eventVersion,
          currentVersion: currentUnreads.version,
        },
      });
      return;
    }

    // Fallback ts-based stale check for clients that pre-date version tracking
    // or when version is missing on the payload.
    if (
      currentUnreads?.latest_message_ts &&
      lastRead < currentUnreads.latest_message_ts
    ) {
      sentryLogs.info({
        message: 'handleReadChat — skipped stale channel_marked event',
        module: 'CHATS',
        component: 'handleReadChat',
        data: {
          channel: eventFormat.channel,
          eventLastRead: lastRead,
          currentLatestMessageTs: currentUnreads.latest_message_ts,
        },
      });
      return;
    }

    await OrchestratorUpdaterService.markConversationAsRead(
      eventFormat.channel,
      lastRead,
      eventVersion,
    );
  }
};

export const handleJoinedChat = async (
  data: JoinedChatEvent | ChannelJoinedEvent,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<ChannelJoinedEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }

  const [conversation, {isFromServer}] =
    await getConversationInfoWithLocalState(eventFormat.channel);

  if (isFromServer) {
    logConversationInfoDBValidationIssues(conversation, eventFormat);
    await ConversationsUpdaterService.saveNewConversation(conversation);
  }

  /**
   * Conversation could exist cause of the created channel event
   */
  const currentUserWasAdded =
    eventFormat.metadata.event_payload.memberships.find(
      membership => membership.user.id === String(persistedUserId),
    );

  if (currentUserWasAdded) {
    logAmplitudeEvent(AMPLITUDE_EVENTS.CHATS_2_CONVERSATION_JOINED, {
      conversationID: eventFormat.channel,
      triggeredBy: Number(eventFormat.user),
    });

    //Start fetching users by batches
    chatMembershipsHandler.addToQueue(eventFormat.channel, 'high');
  } else {
    eventFormat.metadata.event_payload.memberships.forEach(membership =>
      logMembershipDBValidationIssues(
        membership,
        eventFormat.channel,
        eventFormat.type,
      ),
    );

    const memberships = eventFormat.metadata.event_payload.memberships.map(
      mapMembershipFromServer,
    );

    memberships.forEach(m =>
      logUserDBValidationIssues(m.user, 'SocketEvents: handleJoinedChat'),
    );

    await OrchestratorUpdaterService.saveUsersAndMemberships(
      memberships,
      eventFormat.channel,
    );
  }
};

export const handleLeftChat = async (
  data: LeftChatEvent | ChannelLeftEvent,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<ChannelLeftEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }

  const membership = eventFormat.metadata.event_payload.membership;
  const currentUserWasDeleted =
    membership.user.hu_data.member.id === persistedUserId;

  if (currentUserWasDeleted) {
    const isInConversation =
      visibleChatStore.getState().id === eventFormat.channel;
    const triggeredBy = Number(eventFormat.user);

    if (isInConversation) {
      navigationRef.dispatch(StackActions.popToTop());
    }

    await ConversationsUpdaterService.leaveGroup({
      channelId: eventFormat.channel,
      triggeredBy: triggeredBy,
    });

    chatMembershipsHandler.cancelFetch(eventFormat.channel);
  } else {
    await MembershipUpdaterService.deleteUserGroupMembership(
      Number(membership.user.id),
      eventFormat.channel,
    );
  }
};

/**
 * Handler for new group channel created
 */
export const handleNewChannel = async (
  data: NewChannelEvent | ChannelCreatedEvent,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<ChannelCreatedEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }

  const [conversation, {isFromServer}] =
    await getConversationInfoWithLocalState(eventFormat.channel);

  if (isFromServer) {
    logConversationInfoDBValidationIssues(conversation, eventFormat);
    await ConversationsUpdaterService.saveNewConversation(conversation);
  }

  chatMembershipsHandler.addToQueue(eventFormat.channel, 'high');
};

/**
 * Handler for new 1:1 chat created
 */
export const handleNewChat = async (
  data: ImCreatedEvent,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<ImCreatedEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }

  const foundUserId = eventFormat.metadata.event_payload.users.find(
    id => id !== persistedUserId,
  );

  // Validate found user ID before conversion to prevent NaN
  if (foundUserId === undefined || foundUserId === null) {
    // Could not find other user ID in chat creation event
    return;
  }

  const otherUserId = Number(foundUserId);

  // Additional check if conversion resulted in NaN
  if (isNaN(otherUserId)) {
    // Invalid user ID in chat creation event
    return;
  }

  const [userInfo] = await getUserInfo({
    user: otherUserId,
  });

  await ConversationsUpdaterService.saveNewConversation(
    mapImEventToConversation(eventFormat, userInfo, persistedUserId),
  );
};

export const handleAdminAdded = async (
  data: AdminAddedEventSocket | AdminAddedEvent,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<AdminAddedEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }
  const membership = eventFormat.metadata.event_payload.membership;
  logMembershipDBValidationIssues(
    membership,
    eventFormat.channel,
    eventFormat.type,
  );
  await MembershipUpdaterService.saveGroupMemberships({
    channelId: eventFormat.channel,
    memberships: [
      {
        conversation_id: eventFormat.channel,
        is_admin: membership.is_admin ?? true,
        version: membership.version,
        user_id: Number(membership.user.id),
      },
    ],
  });
};

export const handleAdminRemoved = async (
  data: AdminRemovedEventSocket | AdminRemovedEvent,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<AdminRemovedEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }
  const membership = eventFormat.metadata.event_payload.membership;
  logMembershipDBValidationIssues(
    membership,
    eventFormat.channel,
    eventFormat.type,
  );
  await MembershipUpdaterService.saveGroupMemberships({
    channelId: eventFormat.channel,
    memberships: [
      {
        conversation_id: eventFormat.channel,
        is_admin: membership.is_admin || false,
        version: membership.version,
        user_id: Number(membership.user.id),
      },
    ],
  });
};

export const handleChannelChanged = async (
  data: ChannelChangedEventSocket | ChannelChangedEvent,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<ChannelChangedEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }
  const [conversation] = await getConversationInfo({
    channel: eventFormat.channel,
    fromServer: true,
  });
  await ConversationsUpdaterService.saveNewConversation(conversation);
};

export const handleMessageUpdated = async (
  data: MessageEditedEvent,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<MessageEditedEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }

  const persistedMessage = await getMessageById(eventFormat.client_msg_id);

  if (!persistedMessage) {
    return;
  }

  const [conversation, {isFromServer}] =
    await getConversationInfoWithLocalState(eventFormat.channel);

  if (isFromServer) {
    logConversationInfoDBValidationIssues(conversation, eventFormat);
  }

  const mentioned = eventFormat.hu_data.highlighted_spans?.some(
    span =>
      span.type === HighlightedSpanType.Mention &&
      span.user_id === persistedUserId,
  );
  await ConversationsUpdaterService.saveNewConversation({
    ...conversation,
    mentioned,
  });

  const message = await mapMessageFromServer(eventFormat);
  await OrchestratorUpdaterService.processMessage(message);
};

export const handleReaction = async (
  data: ReactionEvent,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<ReactionEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }

  logReactionEventDBValidationIssues(eventFormat);

  await ReactionsUpdaterService.process([
    {
      channel: eventFormat.channel,
      client_msg_id: eventFormat.client_msg_id,
      ts: eventFormat.hu_data.message_ts,
      reactions: eventFormat.reactions || [],
    },
  ]);
};

export const handleMessageDeleted = async (
  data: MessageDeletedEvent,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<MessageDeletedEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }

  const persistedMessage = await getMessageById(eventFormat.client_msg_id);

  if (!persistedMessage) {
    return;
  }

  const message = await mapMessageFromServer(eventFormat);
  message.attachments = mapAssetFromDB(persistedMessage.assets);
  await OrchestratorUpdaterService.processMessage(message);
};

export const handleAssetChanged = (
  data: JsonEventSocket | AssetChangedEvent,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<AssetChangedEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }

  logAssetChangedEventDBValidationIssues(eventFormat);

  const newAttachments = mapAssetFromService(eventFormat.attachments);
  AssetsUpdaterService.updateAssets({
    assets: newAttachments,
    messageId: eventFormat.client_msg_id,
    channelId: eventFormat.channel,
    hidden: eventFormat.hidden || false,
  });
};

export const handleMarkedReceived = async (
  data: JsonEventSocket | MarkedReceivedEvent,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<MarkedReceivedEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }

  const {from_message_ts, to_message_ts, received_at} =
    eventFormat.metadata.event_payload;

  await MessagesUpdaterService.updateDeliverStatus({
    fromMessageTs: from_message_ts,
    toMessageTs: to_message_ts,
    channelId: eventFormat.channel,
    deliverStatus: {
      received_at: received_at.toString(),
    },
  });
};

export const handleMarkedRead = async (
  data: MarkedReadEventSocket | MarkedReadEvent,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<MarkedReadEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }

  const {from_message_ts, to_message_ts, read_at} =
    eventFormat.metadata.event_payload;

  await MessagesUpdaterService.updateDeliverStatus({
    fromMessageTs: from_message_ts,
    toMessageTs: to_message_ts,
    channelId: eventFormat.channel,
    deliverStatus: {
      read_at: read_at.toString(),
    },
  });
};

export const handleChannelInfoChanged = async (
  data: ChannelInfoChangedEvent | {json_event: string},
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<ChannelInfoChangedEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }
  const payload = eventFormat.metadata.event_payload;

  if (!payload) {
    return;
  }

  await ConversationsUpdaterService.updateProperties(eventFormat.channel, {
    version: payload.version,
    notification_status: {
      notification_type: payload.notification_type,
      until: payload.until || null,
    },
    pinned_at: payload.pinned_at || null,
    marked_unread_at: payload.marked_unread_at || null,
  });
};

export const handlePinAdded = async (
  data: PinAddedEvent | JsonEventSocket,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<PinAddedEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }

  const message = await mapMessageFromServer(eventFormat);
  PinnedMessagesUpdaterService.addFromSocket(eventFormat.channel, message);
};

export const handlePinRemoved = (
  data: PinRemovedEvent | JsonEventSocket,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<PinRemovedEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }

  PinnedMessagesUpdaterService.removeFromSocket(
    eventFormat.channel,
    eventFormat.hu_data.message_ts,
  );
};

export const handleMessageReadByAll = async (
  data: MessageReadByAllEvent | JsonEventSocket,
  extra?: ListenerExtraData,
) => {
  const eventFormat = parseEvent<MessageReadByAllEvent>(data, extra?.type);
  if (!eventFormat) {
    return;
  }

  const {message_ts} = eventFormat.metadata.event_payload;

  await MessagesUpdaterService.updateDeliverStatus({
    fromMessageTs: message_ts,
    toMessageTs: message_ts,
    channelId: eventFormat.channel,
    deliverStatus: {
      read_at: eventFormat.ts,
    },
  });
};

// Skip DB query if there's a local state
const getConversationInfoWithLocalState = (
  channel: string,
): ReturnType<typeof getConversationInfo> => {
  const groupInfo = useGroups.getState().groups[channel]?.groupInfo;
  if (groupInfo) {
    return Promise.resolve([
      groupInfo,
      {
        isFromDB: true,
        isFromServer: false,
      },
    ]);
  }
  return getConversationInfo({
    channel,
  });
};
