import {db, DBTransaction} from '@db/index';
import {
  ChatNotificationType,
  Conversation,
  HuDataMember,
  Message,
} from '@modules/chats/interfaces';
import {
  mapAssetToDB,
  mapConversationFromDB,
  mapConversationPropertiesToDB,
  mapConversationToDB,
  mapHuMemberToDB,
  mapLatestToMessage,
  mapMessageToDB,
} from '@modules/chats/utils';
import {insertUsers} from '@modules/chats/db/repositories/users';
import {insertMessages} from '@modules/chats/db/repositories/messages';
import {saveAssets} from '@modules/chats/db/repositories/assets';
import {
  getAllConversationsFromDB,
  getFullConversationById,
  getLatestByConversationIds,
  insertConversation,
  insertConversationProperties,
  insertConversations,
  updateConversation,
  updateConversationLatest,
  updateConversationLatestInBatch,
} from '@modules/chats/db/repositories/conversations';
import {
  insertMemberships,
  selectGroupMemberships,
} from '@modules/chats/db/repositories/groupMemberships';
import {persistedUserId} from '@services/localStorage';
import {asyncMap} from '@shared/utils';

import {getMessagesWithHeight, saveNewMessages} from './messageService';
import {saveUsers} from './userService';
import {
  InsertConversationProperties,
  InsertGroupMembership,
} from '../interfaces';

export const saveAssetsForMessages = async (
  messages: Message[],
  tx?: DBTransaction,
) => {
  const assetsToInsert = messages
    .filter(message => message.attachments?.length)
    .map(message => ({
      messageId: message.client_msg_id,
      assets: message.attachments!.map(mapAssetToDB),
    }));

  if (!assetsToInsert.length) {
    return;
  }

  await asyncMap(assetsToInsert, ({messageId, assets}) =>
    saveAssets(assets, messageId, tx),
  );
};

export const getFullConversation = async (id: string) => {
  const conversation = await getFullConversationById(id);
  return conversation && persistedUserId
    ? mapConversationFromDB(conversation, persistedUserId)
    : null;
};

export const saveConversations = async (data: Conversation[]) => {
  await insertConversations(data.map(mapConversationToDB));
  await insertConversationProperties(data.map(mapConversationPropertiesToDB));
};

export const saveNewConversation = async (data: Conversation) => {
  await insertConversation(mapConversationToDB(data));
  const newUser = data.other_user;
  await Promise.all([
    insertConversationProperties([mapConversationPropertiesToDB(data)]),
    data.latest &&
      saveNewMessages([
        mapLatestToMessage({
          message: data.latest,
          channel: data.id,
        }),
      ]),
    data.latest && updateConversationLatest(data.id, data.latest.client_msg_id),
    newUser && saveUsers([newUser]),
  ]);
};

export const saveMemberships = async (
  memberships: InsertGroupMembership[],
  tx?: DBTransaction,
) => {
  // Split in chunks of 200
  const chunkSize = 200;
  const chunks: InsertGroupMembership[][] = [];

  for (let i = 0; i < memberships.length; i += chunkSize) {
    chunks.push(memberships.slice(i, i + chunkSize));
  }

  // Process all chunks in parallel
  await Promise.all(chunks.map(chunk => insertMemberships(chunk, tx)));
};

export const getGroupMemberships = (channel: string) => {
  return selectGroupMemberships(channel);
};

export const reduceToLatestMessages = async (messages: Message[]) => {
  const latestFromDB = await getLatestByConversationIds(
    messages.map(msg => msg.channel),
  );
  const latestFromDBMap = new Map(
    latestFromDB.map(conversation => [
      conversation.id,
      conversation.latest_message,
    ]),
  );
  const {latest} = messages.reduce(
    (acc, msg) => {
      const fromDB = latestFromDBMap.get(msg.channel);
      const fromCtx = acc.ctx[msg.channel];
      if (
        (!fromDB && !fromCtx) ||
        (fromDB && fromDB.ts < msg.ts) ||
        (fromCtx && fromCtx.ts < msg.ts)
      ) {
        acc.latest.push(msg);
        acc.ctx[msg.channel] = msg;
      }

      return acc;
    },
    {latest: [], ctx: {}} as {latest: Message[]; ctx: Record<string, Message>},
  );
  return latest;
};

export const updateLatestStatusBatch = async (messages: Message[]) => {
  const updates = messages.map(message => ({
    id: message.channel,
    latestMessageId: message.client_msg_id,
    ts: message.ts,
  }));

  await updateConversationLatestInBatch(updates);
};

export const removeMentionFromConversation = (id: string) => {
  return updateConversation({
    id,
    mentioned: false,
  });
};

export const getAllConversations = async (): Promise<Conversation[]> => {
  if (!persistedUserId) {
    throw new Error('User not found');
  }

  const conversations = await getAllConversationsFromDB(persistedUserId);
  const promises = conversations.map(async conversation => {
    const mappedConversation = mapConversationFromDB(
      conversation,
      persistedUserId!,
    );
    return {
      ...mappedConversation,
      messages: mappedConversation.messages
        ? await getMessagesWithHeight(mappedConversation.messages)
        : [],
    };
  });

  return Promise.all(promises);
};

export const updateConversationProperties = async ({
  conversationId,
  notificationStatus,
  pinnedAt,
  markedUnreadAt,
  version,
}: {
  conversationId: string;
  notificationStatus?: {
    type: string;
    until: Nullable<number>;
  };
  pinnedAt?: Nullable<number>;
  markedUnreadAt?: Nullable<number>;
  version: number;
}) => {
  const newValues: InsertConversationProperties = {
    conversation_id: conversationId,
    version,
  };
  if (notificationStatus !== undefined) {
    newValues.notification_type = notificationStatus.type;
    newValues.muted_until =
      notificationStatus.type === ChatNotificationType.All
        ? null
        : notificationStatus.until;
  }

  if (pinnedAt !== undefined) {
    newValues.pinned_at = pinnedAt;
  }

  if (markedUnreadAt !== undefined) {
    newValues.marked_unread_at = markedUnreadAt;
  }
  return await insertConversationProperties([newValues]);
};

export const processConversationsInTransaction = async ({
  conversations,
  messages,
  users,
}: {
  conversations: Conversation[];
  messages: Message[];
  users: HuDataMember[];
}) => {
  const messagesToDB = messages.map(mapMessageToDB);
  const conversationsToDB = conversations.map(mapConversationToDB);
  const conversationPropertiesToDB = conversations.map(
    mapConversationPropertiesToDB,
  );
  const usersToDB = users.map(mapHuMemberToDB);

  // Single transaction for all DB operations of this batch
  await db.instance.transaction(async tx => {
    await Promise.all([
      insertConversations(conversationsToDB, tx),
      insertConversationProperties(conversationPropertiesToDB, tx),
      usersToDB.length && insertUsers(usersToDB, tx),
      messagesToDB.length && insertMessages(messagesToDB, tx),
    ]);
    await saveAssetsForMessages(messages, tx);
  });
};
