// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */

import type { PublicKey } from '@signalapp/libsignal-client';
import { z } from 'zod';

import type { SignalService as Proto } from '../protobuf/index.std.js';
import {
  type ServiceIdString,
  type AciString,
  isPniString,
} from '../types/ServiceId.std.js';
import type { StoryDistributionIdString } from '../types/StoryDistributionId.std.js';
import type { StoryMessageRecipientsType } from '../types/Stories.std.js';
import type {
  ProcessedEnvelope,
  ProcessedDataMessage,
  ProcessedSent,
  ProcessedAttachment,
} from './Types.d.ts';
import type {
  CallEventDetails,
  CallLogEventDetails,
} from '../types/CallDisposition.std.js';
import type { CallLinkUpdateSyncType } from '../types/CallLink.std.js';
import { isAciString } from '../util/isAciString.std.js';

export class EmptyEvent extends Event {
  constructor() {
    super('empty');
  }
}

export type TypingEventData = Readonly<{
  typingMessage: Proto.TypingMessage;
  timestamp: number;
  started: boolean;
  stopped: boolean;
  groupId?: string;
  groupV2Id?: string;
}>;

export type TypingEventConfig = {
  sender?: string;
  senderAci?: AciString;
  senderDevice: number;
  typing: TypingEventData;
};

export class TypingEvent extends Event {
  public readonly sender?: string;

  public readonly senderAci?: AciString;

  public readonly senderDevice: number;

  public readonly typing: TypingEventData;

  constructor({ sender, senderAci, senderDevice, typing }: TypingEventConfig) {
    super('typing');

    this.sender = sender;
    this.senderAci = senderAci;
    this.senderDevice = senderDevice;
    this.typing = typing;
  }
}

export class ErrorEvent extends Event {
  constructor(public readonly error: Error) {
    super('error');
  }
}

export class ContactSyncEvent extends Event {
  constructor(
    public readonly contactAttachment: ProcessedAttachment,
    public readonly complete: boolean,
    public readonly receivedAtCounter: number,
    public readonly sentAt: number
  ) {
    super('contactSync');
  }
}

// Emitted right before we do full decrypt on a message, but after Sealed Sender unseal
export class EnvelopeUnsealedEvent extends Event {
  constructor(public readonly envelope: ProcessedEnvelope) {
    super('envelopeUnsealed');
  }
}

export class EnvelopeQueuedEvent extends Event {
  constructor(public readonly envelope: ProcessedEnvelope) {
    super('envelopeQueued');
  }
}

//
// Confirmable events below
//

export type ConfirmCallback = () => void;

export class ConfirmableEvent extends Event {
  constructor(
    type: string,
    public readonly confirm: ConfirmCallback
  ) {
    super(type);
  }
}

export type DeliveryEventData = Readonly<{
  timestamp: number;
  source?: string;
  sourceServiceId?: ServiceIdString;
  sourceDevice?: number;
  wasSentEncrypted: boolean;
}>;

export class DeliveryEvent extends ConfirmableEvent {
  constructor(
    public readonly deliveryReceipts: ReadonlyArray<DeliveryEventData>,
    public readonly envelopeId: string,
    public readonly envelopeTimestamp: number,
    confirm: ConfirmCallback
  ) {
    super('delivery', confirm);
  }
}

export type SuccessfulDecryptEventData = Readonly<{
  senderDevice: number;
  senderAci: AciString;
  timestamp: number;
}>;

export class SuccessfulDecryptEvent extends Event {
  constructor(public readonly data: SuccessfulDecryptEventData) {
    super('successful-decrypt');
  }
}

export type DecryptionErrorEventData = Readonly<{
  cipherTextBytes: Uint8Array | undefined;
  cipherTextType: number | undefined;
  contentHint: number | undefined;
  groupId: string | undefined;
  receivedAtCounter: number;
  receivedAtDate: number;
  senderDevice: number;
  senderAci: AciString;
  timestamp: number;
}>;

export class DecryptionErrorEvent extends ConfirmableEvent {
  constructor(
    public readonly decryptionError: DecryptionErrorEventData,
    confirm: ConfirmCallback
  ) {
    super('decryption-error', confirm);
  }
}

export type InvalidPlaintextEventData = Readonly<{
  senderDevice: number;
  senderAci: AciString;
  timestamp: number;
}>;

export class InvalidPlaintextEvent extends Event {
  constructor(public readonly data: InvalidPlaintextEventData) {
    super('invalid-plaintext');
  }
}

export type RetryRequestEventData = Readonly<{
  groupId?: string;
  ratchetKey?: PublicKey;
  requesterAci: AciString;
  requesterDevice: number;
  senderDevice: number;
  sentAt: number;
}>;

export class RetryRequestEvent extends ConfirmableEvent {
  constructor(
    public readonly retryRequest: RetryRequestEventData,
    confirm: ConfirmCallback
  ) {
    super('retry-request', confirm);
  }
}

export type SentEventData = Readonly<{
  envelopeId: string;
  destinationE164?: string;
  destinationServiceId?: ServiceIdString;
  timestamp: number;
  serverTimestamp: number;
  device: number | undefined;
  unidentifiedStatus: ProcessedSent['unidentifiedStatus'];
  message: ProcessedDataMessage;
  isRecipientUpdate: boolean;
  receivedAtCounter: number;
  receivedAtDate: number;
  expirationStartTimestamp?: number;
  storyDistributionListId?: StoryDistributionIdString;
}>;

export class SentEvent extends ConfirmableEvent {
  constructor(
    public readonly data: SentEventData,
    confirm: ConfirmCallback
  ) {
    super('sent', confirm);
  }
}

export type ProfileKeyUpdateData = Readonly<{
  source?: string;
  sourceAci?: AciString;
  profileKey: string;
}>;

export class ProfileKeyUpdateEvent extends ConfirmableEvent {
  constructor(
    public readonly data: ProfileKeyUpdateData,
    public readonly reason: string,
    confirm: ConfirmCallback
  ) {
    super('profileKeyUpdate', confirm);
  }
}

export type MessageEventData = Readonly<{
  envelopeId: string;
  source?: string;
  sourceAci: AciString;
  sourceDevice?: number;
  destinationServiceId: ServiceIdString;
  timestamp: number;
  serverGuid: string;
  serverTimestamp: number;
  unidentifiedDeliveryReceived: boolean;
  message: ProcessedDataMessage;
  receivedAtCounter: number;
  receivedAtDate: number;
}>;

export class MessageEvent extends ConfirmableEvent {
  constructor(
    public readonly data: MessageEventData,
    confirm: ConfirmCallback
  ) {
    super('message', confirm);
  }
}

export type ReadOrViewEventData = Readonly<{
  timestamp: number;
  source?: string;
  sourceServiceId?: ServiceIdString;
  sourceDevice?: number;
  wasSentEncrypted: true;
}>;

export class ReadEvent extends ConfirmableEvent {
  constructor(
    public readonly receipts: ReadonlyArray<ReadOrViewEventData>,
    public readonly envelopeId: string,
    public readonly envelopeTimestamp: number,
    confirm: ConfirmCallback
  ) {
    super('read', confirm);
  }
}

export class ViewEvent extends ConfirmableEvent {
  constructor(
    public readonly receipts: ReadonlyArray<ReadOrViewEventData>,
    public readonly envelopeId: string,
    public readonly envelopeTimestamp: number,
    confirm: ConfirmCallback
  ) {
    super('view', confirm);
  }
}

export class ConfigurationEvent extends ConfirmableEvent {
  constructor(
    public readonly configuration: Proto.SyncMessage.Configuration,
    confirm: ConfirmCallback
  ) {
    super('configuration', confirm);
  }
}

export type ViewOnceOpenSyncOptions = {
  sourceAci?: AciString;
  timestamp?: number;
  envelopeTimestamp: number;
};

export class ViewOnceOpenSyncEvent extends ConfirmableEvent {
  public readonly sourceAci?: AciString;

  public readonly envelopeTimestamp: number;
  public readonly timestamp?: number;

  constructor(
    { sourceAci, timestamp, envelopeTimestamp }: ViewOnceOpenSyncOptions,
    confirm: ConfirmCallback
  ) {
    super('viewOnceOpenSync', confirm);

    this.sourceAci = sourceAci;
    this.timestamp = timestamp;
    this.envelopeTimestamp = envelopeTimestamp;
  }
}

export type MessageRequestResponseOptions = {
  envelopeId: string;
  threadE164?: string;
  threadAci?: AciString;
  messageRequestResponseType: Proto.SyncMessage.MessageRequestResponse.Type;
  groupId?: string;
  groupV2Id?: string;
  receivedAtCounter: number;
  receivedAtMs: number;
  sentAt: number;
};

export class MessageRequestResponseEvent extends ConfirmableEvent {
  public readonly threadAci?: AciString;

  public readonly messageRequestResponseType?: MessageRequestResponseOptions['messageRequestResponseType'];

  public readonly groupId?: string;

  public readonly groupV2Id?: string;

  public readonly envelopeId?: string;

  public readonly receivedAtMs: number;

  public readonly receivedAtCounter: number;

  public readonly sentAt: number;

  constructor(
    {
      envelopeId,
      threadAci,
      messageRequestResponseType,
      groupId,
      groupV2Id,
      receivedAtMs,
      receivedAtCounter,
      sentAt,
    }: MessageRequestResponseOptions,
    confirm: ConfirmCallback
  ) {
    super('messageRequestResponse', confirm);

    this.envelopeId = envelopeId;
    this.threadAci = threadAci;
    this.messageRequestResponseType = messageRequestResponseType;
    this.groupId = groupId;
    this.groupV2Id = groupV2Id;
    this.receivedAtMs = receivedAtMs;
    this.receivedAtCounter = receivedAtCounter;
    this.sentAt = sentAt;
  }
}

export class FetchLatestEvent extends ConfirmableEvent {
  constructor(
    public readonly eventType: Proto.SyncMessage.FetchLatest['type'],
    confirm: ConfirmCallback
  ) {
    super('fetchLatest', confirm);
  }
}

export type KeysEventData = Readonly<{
  masterKey: Uint8Array | undefined;
  accountEntropyPool: string | undefined;
  mediaRootBackupKey: Uint8Array | undefined;
}>;

export class KeysEvent extends ConfirmableEvent {
  public readonly masterKey: Uint8Array | undefined;
  public readonly accountEntropyPool: string | undefined;
  public readonly mediaRootBackupKey: Uint8Array | undefined;

  constructor(
    { masterKey, accountEntropyPool, mediaRootBackupKey }: KeysEventData,
    confirm: ConfirmCallback
  ) {
    super('keys', confirm);

    this.masterKey = masterKey;
    this.accountEntropyPool = accountEntropyPool;
    this.mediaRootBackupKey = mediaRootBackupKey;
  }
}

export type StickerPackEventData = Readonly<{
  id?: string;
  key?: string;
  isInstall: boolean;
  isRemove: boolean;
}>;

export class StickerPackEvent extends ConfirmableEvent {
  constructor(
    public readonly stickerPacks: ReadonlyArray<StickerPackEventData>,
    confirm: ConfirmCallback
  ) {
    super('sticker-pack', confirm);
  }
}

export type ReadSyncEventData = Readonly<{
  envelopeId: string;
  timestamp?: number;
  envelopeTimestamp: number;
  sender?: string;
  senderAci?: AciString;
}>;

export class ReadSyncEvent extends ConfirmableEvent {
  constructor(
    public readonly reads: ReadonlyArray<ReadSyncEventData>,
    public readonly envelopeId: string,
    public readonly envelopeTimestamp: number,
    confirm: ConfirmCallback
  ) {
    super('readSync', confirm);
  }
}

export type ViewSyncEventData = Readonly<{
  timestamp?: number;
  senderE164?: string;
  senderAci?: AciString;
}>;

export class ViewSyncEvent extends ConfirmableEvent {
  constructor(
    public readonly views: ReadonlyArray<ViewSyncEventData>,
    public readonly envelopeId: string,
    public readonly envelopeTimestamp: number,
    confirm: ConfirmCallback
  ) {
    super('viewSync', confirm);
  }
}

export type CallEventSyncEventData = Readonly<{
  callEventDetails: CallEventDetails;
  receivedAtCounter: number;
  receivedAtMS: number;
}>;

export class CallEventSyncEvent extends ConfirmableEvent {
  constructor(
    public readonly callEvent: CallEventSyncEventData,
    confirm: ConfirmCallback
  ) {
    super('callEventSync', confirm);
  }
}

export type CallLinkUpdateSyncEventData = Readonly<{
  type: CallLinkUpdateSyncType;
  rootKey: Uint8Array | undefined;
  adminKey: Uint8Array | undefined;
}>;

export class CallLinkUpdateSyncEvent extends ConfirmableEvent {
  constructor(
    public readonly callLinkUpdate: CallLinkUpdateSyncEventData,
    confirm: ConfirmCallback
  ) {
    super('callLinkUpdateSync', confirm);
  }
}

export class DeviceNameChangeSyncEvent extends ConfirmableEvent {
  constructor(confirm: ConfirmCallback) {
    super('deviceNameChangeSync', confirm);
  }
}

const addressableMessageSchema = z.union([
  z.object({
    type: z.literal('aci').readonly(),
    authorAci: z.string().refine(isAciString),
    sentAt: z.number(),
  }),
  z.object({
    type: z.literal('e164').readonly(),
    authorE164: z.string(),
    sentAt: z.number(),
  }),
  z.object({
    type: z.literal('pni').readonly(),
    authorPni: z.string().refine(isPniString),
    sentAt: z.number(),
  }),
]);

export type AddressableMessage = z.infer<typeof addressableMessageSchema>;

const conversationIdentifierSchema = z.union([
  z.object({
    type: z.literal('aci').readonly(),
    aci: z.string().refine(isAciString),
  }),
  z.object({
    type: z.literal('e164').readonly(),
    e164: z.string(),
  }),
  z.object({
    type: z.literal('group').readonly(),
    groupId: z.string(),
  }),
  z.object({
    type: z.literal('pni').readonly(),
    pni: z.string().refine(isPniString),
  }),
]);

export type ConversationIdentifier = z.infer<
  typeof conversationIdentifierSchema
>;

export const deleteMessageSchema = z.object({
  type: z.literal('delete-message').readonly(),
  conversation: conversationIdentifierSchema,
  message: addressableMessageSchema,
  timestamp: z.number(),
});
export type DeleteMessageSyncTarget = z.infer<typeof deleteMessageSchema>;
export const deleteConversationSchema = z.object({
  type: z.literal('delete-conversation').readonly(),
  conversation: conversationIdentifierSchema,
  mostRecentMessages: z.array(addressableMessageSchema),
  mostRecentNonExpiringMessages: z.array(addressableMessageSchema).optional(),
  isFullDelete: z.boolean(),
  timestamp: z.number(),
});
export const deleteLocalConversationSchema = z.object({
  type: z.literal('delete-local-conversation').readonly(),
  conversation: conversationIdentifierSchema,
  timestamp: z.number(),
});
export const deleteAttachmentSchema = z.object({
  type: z.literal('delete-single-attachment').readonly(),
  conversation: conversationIdentifierSchema,
  message: addressableMessageSchema,
  clientUuid: z.string().optional(),
  fallbackDigest: z.string().optional(),
  fallbackPlaintextHash: z.string().optional(),
  timestamp: z.number(),
});
export const deleteForMeSyncTargetSchema = z.union([
  deleteMessageSchema,
  deleteConversationSchema,
  deleteLocalConversationSchema,
  deleteAttachmentSchema,
]);

export type DeleteForMeSyncTarget = z.infer<typeof deleteForMeSyncTargetSchema>;

export type DeleteForMeSyncEventData = ReadonlyArray<DeleteForMeSyncTarget>;

export class DeleteForMeSyncEvent extends ConfirmableEvent {
  constructor(
    public readonly deleteForMeSync: DeleteForMeSyncEventData,
    public readonly timestamp: number,
    public readonly envelopeId: string,
    confirm: ConfirmCallback
  ) {
    super('deleteForMeSync', confirm);
  }
}

export type AttachmentBackfillAttachmentType = Readonly<
  | {
      attachment: ProcessedAttachment;
    }
  | {
      status: Proto.SyncMessage.AttachmentBackfillResponse.AttachmentData.Status;
    }
>;

export type AttachmentBackfillResponseSyncEventData = Readonly<
  {
    targetMessage: AddressableMessage;
    targetConversation: ConversationIdentifier;
  } & (
    | {
        error: Proto.SyncMessage.AttachmentBackfillResponse.Error;
      }
    | {
        attachments: ReadonlyArray<AttachmentBackfillAttachmentType>;
        longText: AttachmentBackfillAttachmentType | undefined;
      }
  )
>;

export class AttachmentBackfillResponseSyncEvent extends ConfirmableEvent {
  constructor(
    public readonly response: AttachmentBackfillResponseSyncEventData,
    public readonly timestamp: number,
    public readonly envelopeId: string,
    confirm: ConfirmCallback
  ) {
    super('attachmentBackfillResponseSync', confirm);
  }
}

export type CallLogEventSyncEventData = Readonly<{
  callLogEventDetails: CallLogEventDetails;
  receivedAtCounter: number;
}>;

export class CallLogEventSyncEvent extends ConfirmableEvent {
  constructor(
    public readonly data: CallLogEventSyncEventData,
    confirm: ConfirmCallback
  ) {
    super('callLogEventSync', confirm);
  }
}

export type StoryRecipientUpdateData = Readonly<{
  destinationServiceId: ServiceIdString;
  storyMessageRecipients: StoryMessageRecipientsType;
  timestamp: number;
}>;

export class StoryRecipientUpdateEvent extends ConfirmableEvent {
  constructor(
    public readonly data: StoryRecipientUpdateData,
    confirm: ConfirmCallback
  ) {
    super('storyRecipientUpdate', confirm);
  }
}
