import {ApiErrors} from '@config/api';
import {
  Message,
  Reaction,
  ReactionInfo,
  ReactionWithUser,
  UserReaction,
} from '@modules/chats/interfaces';
import {
  deleteMessageReactionsBatch,
  deleteReaction,
  insertReactions,
  setReactionsForMessageBatch,
} from '@modules/chats/db';
import {getReactions} from '@modules/chats/services/reactionsServices';
import {getUsersInfo} from '@modules/chats/services/usersServices';
import {
  addMessageReactionsBatch,
  optimisticUpdateReaction,
  addMessageReactions,
} from '@modules/chats/store/zustand/reactions';
import {InsertReaction} from '@modules/chats/db/interfaces';
import {
  mapReactionFromEvent,
  mapReactionToStore,
  mapUserReactionToDB,
} from '@modules/chats/utils/mappers/reactionMapper';
import {Storage} from '@services/mmkvFactory';
import {asyncMap, getUlid, wait} from '@shared/utils';

type ReactionsData = {
  channel: string;
  client_msg_id: string;
  ts: string;
  reactions: Reaction[];
};

type Options = {
  updateReactionsStore?: boolean;
  updateDb?: boolean;
};

type ClientMessageId = string;

type PendingData = {
  clientMsgId: ClientMessageId;
  channel: string;
  reactions: ReactionWithUser[];
};

type RetryItem = Pick<ReactionsData, 'channel' | 'client_msg_id' | 'ts'>;

class ReactionsUpdaterServiceClass {
  private failedReactionsStore = new Storage({
    id: '__REACTIONS_RETRY_PERSISTENT_STORE__',
  });

  // Called on app start to retry any pending requests
  init = () => {
    this.retryPersistedFailures();
  };

  process = async (data: ReactionsData[], options: Options = {}) => {
    // Prepare data: check which reactions need to fetch full list from server
    const {pendingData, dataToRetry} = await this.prepareReactionsData(data);

    // Update reactions store and DB
    await this.updateData(pendingData, options);

    // Retry any failed requests up to 3 times with delay in parallel ( no await )
    this.retryGetFullReactions(dataToRetry, options);
  };

  processWithMessages = (messages: Message[], options?: Options) => {
    const data = messages.map(msg => ({
      channel: msg.channel,
      client_msg_id: msg.client_msg_id,
      ts: msg.ts,
      reactions: msg.reactions || [],
    }));

    return this.process(data, options);
  };

  addMessageReactionsToStore = (
    clientMsgId: string,
    reactions: ReactionInfo,
  ) => {
    addMessageReactions(clientMsgId, reactions);
  };

  async optimisticUpdateReaction(reaction: UserReaction) {
    // Update Summary and list of reactions in the store
    optimisticUpdateReaction(reaction);
    if (reaction.action === 'add') {
      // Add reaction to the DB
      await insertReactions([
        mapUserReactionToDB(reaction, reaction.clientMsgId),
      ]);
    } else if (reaction.action === 'remove') {
      // Remove reaction from the DB
      await deleteReaction(
        reaction.clientMsgId,
        reaction.userId,
        reaction.reactionName,
      );
    }
  }

  // Remove all persistent retry data on logout
  cleanup = () => {
    this.failedReactionsStore.clearAll();
  };

  private prepareReactionsData = async (
    data: ReactionsData[],
  ): Promise<{pendingData: PendingData[]; dataToRetry: RetryItem[]}> => {
    const pendingData: PendingData[] = [];
    const dataToRetry: ReactionsData[] = [];

    await asyncMap(data, async item => {
      // Check if reaction list is not full
      const isFull = item.reactions.every(
        reaction => reaction.count === reaction.users.length,
      );

      if (item.reactions?.length && !isFull) {
        // We need to get the full list of reactions from the server
        try {
          // Save to persistent store to retry later if needed
          this.failedReactionsStore.set(
            item.client_msg_id,
            JSON.stringify({
              channel: item.channel,
              ts: item.ts,
              client_msg_id: item.client_msg_id,
            }),
          );

          const response = await getReactions({
            channel: item.channel,
            ts: item.ts,
            full: true,
          });
          pendingData.push({
            channel: item.channel,
            clientMsgId: item.client_msg_id,
            reactions: response,
          });

          // Remove from retry store on success
          this.failedReactionsStore.remove(item.client_msg_id);
        } catch (error) {
          dataToRetry.push(item);
        }

        return;
      }

      const usersIds = item.reactions
        .flatMap(reaction => reaction.users)
        .map(Number)
        .filter(userId => !isNaN(userId));

      // Get users info, all reactions should have HuDataMember
      const users = await getUsersInfo(usersIds);
      const reactionsFromEvent = mapReactionFromEvent(item, users);

      pendingData.push({
        channel: item.channel,
        clientMsgId: item.client_msg_id,
        reactions: reactionsFromEvent,
      });
    });

    return {pendingData, dataToRetry};
  };

  private updateData = async (data: PendingData[], options: Options = {}) => {
    const {updateDb = true, updateReactionsStore = true} = options || {};

    if (!data.length) {
      return;
    }

    if (updateReactionsStore) {
      addMessageReactionsBatch(
        data.map(r => ({
          clientMsgId: r.clientMsgId,
          reactions: mapReactionToStore({reactionsWithUser: r.reactions}),
        })),
      );
    }

    // Save reactions to the DB
    if (updateDb) {
      const clientMsgIdsToRemove: ClientMessageId[] = [];
      const reactionsToInsert: InsertReaction[] = [];

      for (const reaction of data) {
        if (reaction.reactions.length === 0) {
          clientMsgIdsToRemove.push(reaction.clientMsgId);
        } else {
          const dataToInsert = reaction.reactions.flatMap(item =>
            item.users.map(user => ({
              message_id: reaction.clientMsgId,
              user_id: Number(user.id),
              name: item.name,
              ts: getUlid(),
            })),
          );
          reactionsToInsert.push(...dataToInsert);
        }
      }

      const promises: Promise<unknown>[] = [];

      // Remove all reactions from messages that have empty reactions
      if (clientMsgIdsToRemove.length) {
        promises.push(deleteMessageReactionsBatch(clientMsgIdsToRemove));
      }

      // remove and insert reactions in a batch
      promises.push(setReactionsForMessageBatch(reactionsToInsert));

      await Promise.all(promises);
    }
  };

  // Retry all failed requests up to 3 times with delay in parallel
  private retryGetFullReactions = async (
    data: RetryItem[],
    options: Options = {},
  ) => {
    if (!data.length) {
      return;
    }

    try {
      const pendingData: PendingData[] = [];

      await asyncMap(data, async item => {
        const runTask = async (attempts = 1) => {
          try {
            const response = await getReactions({
              channel: item.channel,
              ts: item.ts,
              full: true,
            });
            pendingData.push({
              channel: item.channel,
              clientMsgId: item.client_msg_id,
              reactions: response,
            });

            // Remove from retry store on success
            this.failedReactionsStore.remove(item.client_msg_id);
          } catch (error) {
            if (attempts < 3) {
              await wait(500 * attempts); //500ms -> 1s -> 1.5s
              await runTask(attempts + 1);
            } else {
              // Remove from retry store if not a network error
              if ((error as Error).message !== ApiErrors.NETWORK_ERROR) {
                this.failedReactionsStore.remove(item.client_msg_id);
              }
            }
          }
        };
        await runTask();
      });

      await this.updateData(pendingData, options);
    } catch {
      //do nothing
    }
  };

  private retryPersistedFailures = () => {
    const keys = this.failedReactionsStore.getAllKeys();
    if (!keys.length) {
      return;
    }
    const dataToRetry: RetryItem[] = [];

    keys.forEach(key => {
      const item = this.failedReactionsStore.getString(key);
      if (item) {
        try {
          const parsed: RetryItem = JSON.parse(item);
          dataToRetry.push(parsed);
        } catch {
          // corrupted data - remove it
          this.failedReactionsStore.remove(key);
        }
      }
    });

    if (!dataToRetry.length) {
      return;
    }

    // Update reactions store and DB
    this.retryGetFullReactions(dataToRetry, {
      updateDb: true,
      updateReactionsStore: true,
    });
  };
}

export const ReactionsUpdaterService = new ReactionsUpdaterServiceClass();
