import _ from 'lodash';
import { ClosedGroup, getMessageQueue } from '..';
import { ConversationTypeEnum } from '../../models/types';
import { addKeyPairToCacheAndDBIfNeeded } from '../../receiver/closedGroups';
import { ECKeyPair } from '../../receiver/keypairs';
import { openConversationWithMessages } from '../../state/ducks/conversations';
import { updateConfirmModal } from '../../state/ducks/modalDialog';
import { getSwarmPollingInstance } from '../apis/snode_api';
import { SnodeNamespaces } from '../apis/snode_api/namespaces';
import { generateClosedGroupPublicKey, generateCurve25519KeyPairWithoutPrefix } from '../crypto';
import {
  ClosedGroupNewMessage,
  ClosedGroupNewMessageParams,
} from '../messages/outgoing/controlMessage/group/ClosedGroupNewMessage';
import { PubKey } from '../types';
import { UserUtils } from '../utils';
import { forceSyncConfigurationNowIfNeeded } from '../utils/sync/syncUtils';
import { getConversationController } from './ConversationController';

export async function createClosedGroup(groupName: string, members: Array<string>, isV3: boolean) {
  const setOfMembers = new Set(members);

  if (isV3) {
    throw new Error('groupv3 is not supported yet');
  }

  const us = UserUtils.getOurPubKeyStrFromCache();

  const groupPublicKey = await generateClosedGroupPublicKey();

  const encryptionKeyPair = await generateCurve25519KeyPairWithoutPrefix();
  if (!encryptionKeyPair) {
    throw new Error('Could not create encryption keypair for new closed group');
  }

  // Create the group
  const convo = await getConversationController().getOrCreateAndWait(
    groupPublicKey,
    ConversationTypeEnum.GROUP
  );
  convo.set('lastJoinedTimestamp', Date.now());
  await convo.setIsApproved(true, false);

  // Ensure the current user is a member
  setOfMembers.add(us);
  const listOfMembers = [...setOfMembers];
  const admins = [us];

  const existingExpirationType = 'unknown';
  const existingExpireTimer = 0;

  const groupDetails: ClosedGroup.GroupInfo = {
    id: groupPublicKey,
    name: groupName,
    members: listOfMembers,
    admins,
    activeAt: Date.now(),
    // TODO This is only applicable for old closed groups - will be removed in future
    expirationType: existingExpirationType,
    expireTimer: existingExpireTimer,
  };

  // we don't want the initial "AAA and You joined the group" anymore

  // be sure to call this before sending the message.
  // the sending pipeline needs to know from GroupUtils when a message is for a medium group
  await ClosedGroup.updateOrCreateClosedGroup(groupDetails);
  await convo.commit();
  convo.updateLastMessage();

  // Send a closed group update message to all members individually.
  // Note: we do not make those messages expire
  const allInvitesSent = await sendToGroupMembers(
    listOfMembers,
    groupPublicKey,
    groupName,
    admins,
    encryptionKeyPair
  );

  if (allInvitesSent) {
    const newHexKeypair = encryptionKeyPair.toHexKeyPair();
    await addKeyPairToCacheAndDBIfNeeded(groupPublicKey, newHexKeypair);
    // Subscribe to this group id
    getSwarmPollingInstance().addGroupId(new PubKey(groupPublicKey));
  }
  // commit again as now the keypair is saved and can be added to the libsession wrapper UserGroup
  await convo.commit();

  await forceSyncConfigurationNowIfNeeded();

  await openConversationWithMessages({ conversationKey: groupPublicKey, messageId: null });
}

function getMessageArgs(group_name: string, names: Array<string>) {
  const name = names[0];

  switch (names.length) {
    case 1:
      return {
        token: 'groupInviteFailedUser',
        args: {
          group_name,
          name,
        },
      } as const;
    case 2:
      return {
        token: 'groupInviteFailedTwo',
        args: {
          group_name,
          name,
          other_name: names[1],
        },
      } as const;
    default:
      return {
        token: 'groupInviteFailedMultiple',
        args: {
          group_name,
          name,
          count: names.length - 1,
        },
      } as const;
  }
}

/**
 * Sends a group invite message to each member of the group.
 * @returns Array of promises for group invite messages sent to group members.
 */
async function sendToGroupMembers(
  listOfMembers: Array<string>,
  groupPublicKey: string,
  groupName: string,
  admins: Array<string>,
  encryptionKeyPair: ECKeyPair,
  isRetry: boolean = false
): Promise<any> {
  const promises = createInvitePromises(
    listOfMembers,
    groupPublicKey,
    groupName,
    admins,
    encryptionKeyPair
  );
  window?.log?.info(`Sending invites for group ${groupPublicKey} to ${listOfMembers}`);
  // evaluating if all invites sent, if failed give the option to retry failed invites via modal dialog
  const inviteResults = await Promise.all(promises);
  const allInvitesSent = _.every(inviteResults, inviteResult => inviteResult !== false);

  if (allInvitesSent) {
    if (isRetry) {
      window.inboxStore?.dispatch(
        updateConfirmModal({
          title: window.i18n('groupInviteSuccessful'),
          i18nMessage: { token: 'groupInviteSuccessful' },
          hideCancel: true,
          onClickClose: () => {
            window.inboxStore?.dispatch(updateConfirmModal(null));
          },
        })
      );
    }
    return allInvitesSent;
  }
  // Confirmation dialog that recursively calls sendToGroupMembers on resolve
  const membersToResend: Array<string> = new Array<string>();
  inviteResults.forEach((result, index) => {
    const member = listOfMembers[index];
    // group invite must always contain the admin member.
    if (result !== true || admins.includes(member)) {
      membersToResend.push(member);
    }
  });
  const namesOfMembersToResend = membersToResend.map(
    m =>
      getConversationController().get(m)?.getNicknameOrRealUsernameOrPlaceholder() ||
      window.i18n('unknown')
  );

  if (membersToResend.length < 1) {
    throw new Error('Some invites failed, we should have found members to resend');
  }

  window.inboxStore?.dispatch(
    updateConfirmModal({
      title: window.i18n('groupError'),
      i18nMessage: getMessageArgs(groupName, namesOfMembersToResend),
      okText: window.i18n('resend'),
      onClickOk: async () => {
        if (membersToResend.length > 0) {
          const isRetrySend = true;
          await sendToGroupMembers(
            membersToResend,
            groupPublicKey,
            groupName,
            admins,
            encryptionKeyPair,
            isRetrySend
          );
        }
      },
      onClickClose: () => {
        window.inboxStore?.dispatch(updateConfirmModal(null));
      },
    })
  );

  return allInvitesSent;
}

function createInvitePromises(
  listOfMembers: Array<string>,
  groupPublicKey: string,
  groupName: string,
  admins: Array<string>,
  encryptionKeyPair: ECKeyPair
) {
  return listOfMembers.map(async m => {
    const messageParams: ClosedGroupNewMessageParams = {
      groupId: groupPublicKey,
      name: groupName,
      members: listOfMembers,
      admins,
      keypair: encryptionKeyPair,
      timestamp: Date.now(),
      expirationType: null, // we keep that one **not** expiring
      expireTimer: 0,
    };
    const message = new ClosedGroupNewMessage(messageParams);
    return getMessageQueue().sendToPubKeyNonDurably({
      pubkey: PubKey.cast(m),
      message,
      namespace: SnodeNamespaces.UserMessages,
    });
  });
}
