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

import createDebug from 'debug';
import { v4 as generateUuid } from 'uuid';
import type {
  Group,
  PrimaryDevice,
  Server,
  StorageStateRecord,
} from '@signalapp/mock-server';
import { StorageState, Proto } from '@signalapp/mock-server';
import path from 'node:path';
import fs from 'node:fs/promises';
import lodash from 'lodash';
import { CallLinkRootKey } from '@signalapp/ringrtc';
import { App } from '../playwright.node.ts';
import { Bootstrap } from '../bootstrap.node.ts';
import type { BootstrapOptions } from '../bootstrap.node.ts';
import { MY_STORY_ID } from '../../types/Stories.std.ts';
import { uuidToBytes } from '../../util/uuidToBytes.std.ts';
import { artAddStickersRoute } from '../../util/signalRoutes.std.ts';
import { getRoomIdFromRootKey } from '../../util/callLinksRingrtc.node.ts';

const { range } = lodash;

export const debug = createDebug('mock:test:storage');

export { App, Bootstrap };

const GROUP_SIZE = 8;

const IdentifierType = Proto.ManifestRecord.Identifier.Type;

export type InitStorageResultType = Readonly<{
  bootstrap: Bootstrap;
  app: App;
  group: Group;
  members: ReadonlyArray<PrimaryDevice>;
}>;

//
// This function creates an initial storage service state that includes:
//
// - All contacts from contact sync (first contact pinned)
// - A pinned group with GROUP_SIZE members (from the contacts)
// - Account with e164 and profileKey
//
// In addition to above, this function will queue one incoming message in the
// group, and one for the first contact (so that both will appear in the left
// pane).
export async function initStorage(
  options?: BootstrapOptions
): Promise<InitStorageResultType> {
  // Creates primary device, contacts
  const bootstrap = new Bootstrap(options);

  await bootstrap.init();

  try {
    // Populate storage service
    const { contacts, phone } = bootstrap;

    const [firstContact] = contacts as [PrimaryDevice];

    const members = [...contacts].slice(0, GROUP_SIZE);

    const group = await phone.createGroup({
      title: 'Mock Group',
      members: [phone, ...members],
    });

    let state = StorageState.getEmpty();

    state = state.updateAccount({
      profileKey: phone.profileKey.serialize(),
      givenName: phone.profileName,
    });

    state = state
      .addGroup(group, {
        whitelisted: true,
      })
      .pinGroup(group);

    for (const contact of contacts) {
      state = state.addContact(contact, {
        identityState: Proto.ContactRecord.IdentityState.VERIFIED,
        whitelisted: true,

        identityKey: contact.publicKey.serialize(),
        profileKey: contact.profileKey.serialize(),
        givenName: contact.profileName,
      });
    }

    state = state.pin(firstContact);

    state = state.addRecord({
      type: IdentifierType.STORY_DISTRIBUTION_LIST,
      record: {
        storyDistributionList: {
          allowsReplies: true,
          identifier: uuidToBytes(MY_STORY_ID),
          isBlockList: true,
          name: MY_STORY_ID,
          deletedAtTimestamp: null,
          recipientServiceIdsBinary: null,
        },
      },
    });

    state = state.addRecord({
      type: IdentifierType.CHAT_FOLDER,
      record: {
        chatFolder: {
          id: uuidToBytes(generateUuid()),
          name: null,
          position: 0,
          showOnlyUnread: false,
          showMutedChats: true,
          includeAllIndividualChats: true,
          includeAllGroupChats: true,
          folderType: Proto.ChatFolderRecord.FolderType.ALL,
          includedRecipients: null,
          excludedRecipients: null,
          deletedAtTimestampMs: null,
        },
      },
    });

    await phone.setStorageState(state);

    // Link new device
    const app = await bootstrap.link();

    const { desktop } = bootstrap;

    // Send a message to the group and the first contact
    const contactSend = contacts[0]?.sendText(desktop, 'hello from contact', {
      timestamp: bootstrap.getTimestamp(),
      sealed: true,
    });

    const groupSend = members[0]?.sendText(desktop, 'hello in group', {
      timestamp: bootstrap.getTimestamp(),
      sealed: true,
      group,
    });

    await Promise.all([contactSend, groupSend]);

    return { bootstrap, app, group, members };
  } catch (error) {
    await bootstrap.saveLogs();
    throw error;
  }
}

export const FIXTURES = path.join(__dirname, '..', '..', '..', 'fixtures');

export const EMPTY = new Uint8Array(0);

export type StickerPackType = Readonly<{
  id: Buffer<ArrayBuffer>;
  key: Buffer<ArrayBuffer>;
  stickerCount: number;
}>;

export const STICKER_PACKS = [
  {
    id: Buffer.from('c40ed069cdc2b91eccfccf25e6bcddfc', 'hex'),
    key: Buffer.from(
      'cefadd6e81c128680aead1711eb5c92c10f63bdfbc78528a4519ba682de396e4',
      'hex'
    ),
    stickerCount: 1,
  },
  {
    id: Buffer.from('ae8fedafda4768fd3384d4b3b9db963d', 'hex'),
    key: Buffer.from(
      '53f4aa8b95e1c2e75afab2328fe67eb6d7affbcd4f50cd4da89dfc325dbc73ca',
      'hex'
    ),
    stickerCount: 1,
  },
] as const satisfies ReadonlyArray<StickerPackType>;

export function getStickerPackLink(pack: StickerPackType): string {
  return artAddStickersRoute
    .toWebUrl({
      packId: pack.id.toString('hex'),
      packKey: pack.key.toString('hex'),
    })
    .toString();
}

type StickerRecord = StorageStateRecord<{
  stickerPack: Proto.StickerPackRecord.Params;
}>;

export function getStickerPackRecordPredicate(
  pack: StickerPackType
): (record: StorageStateRecord) => record is StickerRecord {
  return (stateRecord: StorageStateRecord): stateRecord is StickerRecord => {
    const { type, record } = stateRecord;
    if (record.stickerPack == null) {
      return false;
    }
    if (type !== IdentifierType.STICKER_PACK) {
      return false;
    }
    return pack.id.equals(record.stickerPack.packId ?? EMPTY);
  };
}

export async function storeStickerPacks(
  server: Server,
  stickerPacks: ReadonlyArray<StickerPackType>
): Promise<void> {
  await Promise.all(
    stickerPacks.map(async ({ id, stickerCount }) => {
      const hexId = id.toString('hex');

      await server.storeStickerPack({
        id,
        manifest: await fs.readFile(
          path.join(FIXTURES, `stickerpack-${hexId}.bin`)
        ),
        stickers: await Promise.all(
          range(0, stickerCount).map(async index =>
            fs.readFile(
              path.join(FIXTURES, `stickerpack-${hexId}-${index}.bin`)
            )
          )
        ),
      });
    })
  );
}

type CallLinkRecord = StorageStateRecord<{
  callLink: Proto.CallLinkRecord.Params;
}>;

export function getCallLinkRecordPredicate(
  roomId: string
): (record: StorageStateRecord) => record is CallLinkRecord {
  return (stateRecord: StorageStateRecord): stateRecord is CallLinkRecord => {
    const { type, record } = stateRecord;
    if (record.callLink == null) {
      return false;
    }

    const rootKeyBytes = record.callLink.rootKey;
    if (type !== IdentifierType.CALL_LINK || rootKeyBytes == null) {
      return false;
    }
    const recordRootKey = CallLinkRootKey.fromBytes(Buffer.from(rootKeyBytes));
    const recordRoomId = getRoomIdFromRootKey(recordRootKey);
    return roomId === recordRoomId;
  };
}

type ChatFolderRecord = StorageStateRecord<{
  chatFolder: Proto.ChatFolderRecord.Params;
}>;

export function getChatFolderRecordPredicate(
  folderType: keyof typeof Proto.ChatFolderRecord.FolderType,
  name: string,
  deleted: boolean
): (record: StorageStateRecord) => record is ChatFolderRecord {
  return (stateRecord): stateRecord is ChatFolderRecord => {
    const { type, record } = stateRecord;
    if (record.chatFolder == null) {
      return false;
    }

    const { chatFolder } = record;
    if (type !== IdentifierType.CHAT_FOLDER || chatFolder == null) {
      return false;
    }

    const deletedAtTimestampMs = chatFolder.deletedAtTimestampMs ?? 0n;
    const isDeleted = deletedAtTimestampMs > 0n;

    return (
      chatFolder.folderType === Proto.ChatFolderRecord.FolderType[folderType] &&
      chatFolder.name === name &&
      isDeleted === deleted
    );
  };
}
