// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import lodash from 'lodash';

import type { ConversationQueueJobData } from '../jobs/conversationJobQueue.preload.ts';
import type { StoryDataType } from '../state/ducks/stories.preload.ts';
import * as Errors from '../types/errors.std.ts';
import type { StoryMessageRecipientsType } from '../types/Stories.std.ts';
import type { StoryDistributionIdString } from '../types/StoryDistributionId.std.ts';
import type { ServiceIdString } from '../types/ServiceId.std.ts';
import { createLogger } from '../logging/log.std.ts';
import { DAY } from './durations/index.std.ts';
import { StoryRecipientUpdateEvent } from '../textsecure/messageReceiverEvents.std.ts';
import {
  conversationJobQueue,
  conversationQueueJobEnum,
} from '../jobs/conversationJobQueue.preload.ts';
import { onStoryRecipientUpdate } from './onStoryRecipientUpdate.preload.ts';
import { sendDeleteForEveryoneMessage } from './sendDeleteForEveryoneMessage.preload.ts';
import { isGroupV2 } from './whatTypeOfConversation.dom.ts';
import { getMessageById } from '../messages/getMessageById.preload.ts';
import { repeat, zipObject } from './iterables.std.ts';
import { isOlderThan } from './timestamp.std.ts';

const { noop } = lodash;

const log = createLogger('deleteStoryForEveryone');

export async function deleteStoryForEveryone(
  stories: ReadonlyArray<StoryDataType>,
  story: StoryDataType
): Promise<void> {
  if (!story.sendStateByConversationId) {
    return;
  }

  // Group stories are deleted as regular messages.
  const sourceConversation = window.ConversationController.get(
    story.conversationId
  );
  if (sourceConversation && isGroupV2(sourceConversation.attributes)) {
    void sendDeleteForEveryoneMessage(sourceConversation.attributes, {
      id: story.messageId,
      timestamp: story.timestamp,
    });
    return;
  }

  const logId = `deleteStoryForEveryone(${story.messageId})`;
  const message = await getMessageById(story.messageId);
  if (!message) {
    throw new Error('Story not found');
  }

  if (isOlderThan(story.timestamp, DAY)) {
    throw new Error('Cannot send DOE for a story older than one day');
  }

  const conversationIds = new Set(Object.keys(story.sendStateByConversationId));
  const newStoryRecipients = new Map<
    ServiceIdString,
    {
      distributionListIds: Set<StoryDistributionIdString>;
      isAllowedToReply: boolean;
    }
  >();

  const ourConversation =
    window.ConversationController.getOurConversationOrThrow();

  // Remove ourselves from the DOE.
  conversationIds.delete(ourConversation.id);

  // `updatedStoryRecipients` is used to build `storyMessageRecipients` for
  // a sync message. Put all affected destinationServiceIds early on so that if
  // there are no other distribution lists for them - we'd still include an
  // empty list.
  Object.entries(story.sendStateByConversationId).forEach(
    ([recipientId, sendState]) => {
      if (recipientId === ourConversation.id) {
        return;
      }

      const destinationServiceId =
        window.ConversationController.get(recipientId)?.getServiceId();

      if (!destinationServiceId) {
        return;
      }

      newStoryRecipients.set(destinationServiceId, {
        distributionListIds: new Set(),
        isAllowedToReply: sendState.isAllowedToReplyToStory !== false,
      });
    }
  );

  // Find stories that were sent to other distribution lists so that we don't
  // send a DOE request to the members of those lists.
  stories.forEach(item => {
    const { sendStateByConversationId } = item;
    // We only want matching timestamp stories which are stories that were
    // sent to multi distribution lists.
    // We don't want the story we just passed in.
    // Don't need to check for stories that have already been deleted.
    // And only for sent stories, not incoming.
    if (
      item.timestamp !== story.timestamp ||
      item.messageId === story.messageId ||
      item.deletedForEveryone ||
      !sendStateByConversationId
    ) {
      return;
    }

    Object.keys(sendStateByConversationId).forEach(conversationId => {
      if (conversationId === ourConversation.id) {
        return;
      }

      const destinationServiceId =
        window.ConversationController.get(conversationId)?.getServiceId();

      if (!destinationServiceId) {
        return;
      }

      // Remove this conversationId so we don't send the DOE to those that
      // still have access.
      conversationIds.delete(conversationId);

      // Build remaining distribution list ids that the user still has
      // access to.
      if (item.storyDistributionListId === undefined) {
        return;
      }

      // Build complete list of new story recipients (not counting ones that
      // are in the deleted story).
      let recipient = newStoryRecipients.get(destinationServiceId);
      if (!recipient) {
        const isAllowedToReply =
          // oxlint-disable-next-line typescript/no-non-null-assertion
          sendStateByConversationId[conversationId]!.isAllowedToReplyToStory;
        recipient = {
          distributionListIds: new Set(),
          isAllowedToReply: isAllowedToReply !== false,
        };

        newStoryRecipients.set(destinationServiceId, recipient);
      }

      recipient.distributionListIds.add(item.storyDistributionListId);
    });
  });

  const newStoryMessageRecipients: StoryMessageRecipientsType = [];

  newStoryRecipients.forEach((recipientData, destinationServiceId) => {
    newStoryMessageRecipients.push({
      destinationServiceId,
      distributionListIds: Array.from(recipientData.distributionListIds),
      isAllowedToReply: recipientData.isAllowedToReply,
    });
  });

  const destinationServiceId = ourConversation.getCheckedServiceId(
    'deleteStoryForEveryone'
  );

  log.info(`${logId}: sending DOE to ${conversationIds.size} conversations`);

  message.set({
    deletedForEveryoneSendStatus: zipObject(conversationIds, repeat(false)),
  });

  // Send the DOE
  log.info(`${logId}: enqueuing DeleteStoryForEveryone`);

  try {
    const jobData: ConversationQueueJobData = {
      type: conversationQueueJobEnum.enum.DeleteStoryForEveryone,
      conversationId: ourConversation.id,
      storyId: story.messageId,
      targetTimestamp: story.timestamp,
      updatedStoryRecipients: newStoryMessageRecipients,
    };
    await conversationJobQueue.add(jobData, async jobToInsert => {
      log.info(`${logId}: Deleting message with job ${jobToInsert.id}`);

      await window.MessageCache.saveMessage(message.attributes, {
        jobToInsert,
      });
    });
  } catch (error) {
    log.error(
      `${logId}: Failed to queue delete for everyone`,
      Errors.toLogFormat(error)
    );
    throw error;
  }

  log.info(`${logId}: emulating sync message event`);

  // Emulate message for Desktop (this will call deleteForEveryone())
  const ev = new StoryRecipientUpdateEvent(
    {
      destinationServiceId,
      timestamp: story.timestamp,
      storyMessageRecipients: newStoryMessageRecipients.map(recipient => {
        return {
          ...recipient,
          destinationServiceId: recipient.destinationServiceId ?? undefined,
        };
      }),
    },
    noop
  );
  void onStoryRecipientUpdate(ev);
}
