import { isNil } from 'lodash';
import {
  ConversationNotificationSettingType,
  READ_MESSAGE_STATE,
} from '../models/conversationAttributes';
import { CallManager, SyncUtils, ToastUtils, UserUtils } from '../session/utils';

import { SessionButtonColor } from '../components/basic/SessionButton';
import { getCallMediaPermissionsSettings } from '../components/settings/SessionSettings';
import { Data } from '../data/data';
import { SettingsKey } from '../data/settings-key';
import { ConversationTypeEnum } from '../models/types';
import { uploadFileToFsWithOnionV4 } from '../session/apis/file_server_api/FileServerApi';
import { OpenGroupUtils } from '../session/apis/open_group_api/utils';
import { GetNetworkTime } from '../session/apis/snode_api/getNetworkTime';
import { getConversationController } from '../session/conversations';
import { getSodiumRenderer } from '../session/crypto';
import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager';
import { DisappearingMessageConversationModeType } from '../session/disappearing_messages/types';
import { perfEnd, perfStart } from '../session/utils/Performance';
import { fromHexToArray, toHex } from '../session/utils/String';
import { ConfigurationSync } from '../session/utils/job_runners/jobs/ConfigurationSyncJob';
import { SessionUtilContact } from '../session/utils/libsession/libsession_utils_contacts';
import { forceSyncConfigurationNowIfNeeded } from '../session/utils/sync/syncUtils';
import {
  conversationReset,
  quoteMessage,
  resetConversationExternal,
} from '../state/ducks/conversations';
import {
  changeNickNameModal,
  updateAddModeratorsModal,
  updateBanOrUnbanUserModal,
  updateBlockOrUnblockModal,
  updateConfirmModal,
  updateGroupMembersModal,
  updateGroupNameModal,
  updateInviteContactModal,
  updateRemoveModeratorsModal,
} from '../state/ducks/modalDialog';
import { MIME } from '../types';
import { IMAGE_JPEG } from '../types/MIME';
import { processNewAttachment } from '../types/MessageAttachment';
import { urlToBlob } from '../types/attachments/VisualAttachment';
import { encryptProfile } from '../util/crypto/profileEncrypter';
import { ReleasedFeatures } from '../util/releaseFeature';
import { Storage, setLastProfileUpdateTimestamp } from '../util/storage';
import { UserGroupsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface';
import { ConversationInteractionStatus, ConversationInteractionType } from './types';
import { BlockedNumberController } from '../util';

export async function copyPublicKeyByConvoId(convoId: string) {
  if (OpenGroupUtils.isOpenGroupV2(convoId)) {
    const fromWrapper = await UserGroupsWrapperActions.getCommunityByFullUrl(convoId);

    if (!fromWrapper) {
      window.log.warn('opengroup to copy was not found in the UserGroupsWrapper');
      return;
    }

    if (fromWrapper.fullUrlWithPubkey) {
      window.clipboard.writeText(fromWrapper.fullUrlWithPubkey);
      ToastUtils.pushCopiedToClipBoard();
    }
  } else {
    window.clipboard.writeText(convoId);
  }
}

export async function blockConvoById(conversationId: string) {
  window.inboxStore?.dispatch(
    updateBlockOrUnblockModal({
      action: 'block',
      pubkeys: [conversationId],
    })
  );
}

export async function unblockConvoById(conversationId: string) {
  window.inboxStore?.dispatch(
    updateBlockOrUnblockModal({
      action: 'unblock',
      pubkeys: [conversationId],
    })
  );
}

/**
 * marks the conversation's approval fields, sends messageRequestResponse, syncs to linked devices
 */
export const approveConvoAndSendResponse = async (conversationId: string) => {
  const convoToApprove = getConversationController().get(conversationId);

  if (!convoToApprove) {
    window?.log?.info('Conversation is already approved.');
    return;
  }

  await convoToApprove.setIsApproved(true, false);

  await convoToApprove.commit();
  await convoToApprove.sendMessageRequestResponse();
};

export async function declineConversationWithoutConfirm({
  blockContact,
  conversationId,
  currentlySelectedConvo,
  syncToDevices,
}: {
  conversationId: string;
  currentlySelectedConvo: string | undefined;
  syncToDevices: boolean;
  blockContact: boolean; // if set to false, the contact will just be set to not approved
}) {
  const conversationToDecline = getConversationController().get(conversationId);

  if (!conversationToDecline || !conversationToDecline.isPrivate()) {
    window?.log?.info('No conversation to decline.');
    return;
  }

  // Note: do not set the active_at undefined as this would make that conversation not synced with the libsession wrapper
  await conversationToDecline.setIsApproved(false, false);
  await conversationToDecline.setDidApproveMe(false, false);
  // this will update the value in the wrapper if needed but not remove the entry if we want it gone. The remove is done below with removeContactFromWrapper
  await conversationToDecline.commit();
  if (blockContact) {
    await BlockedNumberController.block(conversationId);
  }
  // when removing a message request, without blocking it, we actually have no need to store the conversation in the wrapper. So just remove the entry

  if (
    conversationToDecline.isPrivate() &&
    !SessionUtilContact.isContactToStoreInWrapper(conversationToDecline)
  ) {
    await SessionUtilContact.removeContactFromWrapper(conversationToDecline.id);
  }

  if (syncToDevices) {
    await forceSyncConfigurationNowIfNeeded();
  }
  if (currentlySelectedConvo && currentlySelectedConvo === conversationId) {
    window?.inboxStore?.dispatch(resetConversationExternal());
  }
}

export const declineConversationWithConfirm = ({
  conversationId,
  syncToDevices,
  blockContact,
  currentlySelectedConvo,
}: {
  conversationId: string;
  currentlySelectedConvo: string | undefined;
  syncToDevices: boolean;
  blockContact: boolean; // if set to false, the contact will just be set to not approved
}) => {
  const convoName =
    getConversationController().get(conversationId)?.getNicknameOrRealUsernameOrPlaceholder() ||
    window.i18n('unknown');
  window?.inboxStore?.dispatch(
    updateConfirmModal({
      okText: blockContact ? window.i18n('block') : window.i18n('delete'),
      cancelText: window.i18n('cancel'),
      title: blockContact ? window.i18n('block') : window.i18n('delete'),
      i18nMessage: blockContact
        ? { token: 'blockDescription', args: { name: convoName } }
        : { token: 'messageRequestsDelete' },
      onClickOk: async () => {
        await declineConversationWithoutConfirm({
          conversationId,
          currentlySelectedConvo,
          blockContact,
          syncToDevices,
        });
      },
      onClickCancel: () => {
        window?.inboxStore?.dispatch(updateConfirmModal(null));
      },
      onClickClose: () => {
        window?.inboxStore?.dispatch(updateConfirmModal(null));
      },
      okTheme: SessionButtonColor.Danger,
      closeTheme: SessionButtonColor.Primary,
    })
  );
};

export async function showUpdateGroupNameByConvoId(conversationId: string) {
  const conversation = getConversationController().get(conversationId);
  if (conversation.isClosedGroup()) {
    // make sure all the members' convo exists so we can add or remove them
    await Promise.all(
      conversation
        .get('members')
        .map(m => getConversationController().getOrCreateAndWait(m, ConversationTypeEnum.PRIVATE))
    );
  }
  window.inboxStore?.dispatch(updateGroupNameModal({ conversationId }));
}

export async function showUpdateGroupMembersByConvoId(conversationId: string) {
  const conversation = getConversationController().get(conversationId);
  if (conversation.isClosedGroup()) {
    // make sure all the members' convo exists so we can add or remove them
    await Promise.all(
      conversation
        .get('members')
        .map(m => getConversationController().getOrCreateAndWait(m, ConversationTypeEnum.PRIVATE))
    );
  }
  window.inboxStore?.dispatch(updateGroupMembersModal({ conversationId }));
}

export function showLeavePrivateConversationbyConvoId(conversationId: string) {
  const conversation = getConversationController().get(conversationId);
  const isMe = conversation.isMe();

  if (!conversation.isPrivate()) {
    throw new Error('showLeavePrivateConversationDialog() called with a non private convo.');
  }

  const onClickClose = () => {
    window?.inboxStore?.dispatch(updateConfirmModal(null));
  };

  const onClickOk = async () => {
    try {
      await updateConversationInteractionState({
        conversationId,
        type: isMe ? ConversationInteractionType.Hide : ConversationInteractionType.Leave,
        status: ConversationInteractionStatus.Start,
      });
      onClickClose();
      await getConversationController().delete1o1(conversationId, {
        fromSyncMessage: false,
        justHidePrivate: true,
        keepMessages: isMe,
      });
      await clearConversationInteractionState({ conversationId });
    } catch (err) {
      window.log.warn(`showLeavePrivateConversationbyConvoId error: ${err}`);
      await saveConversationInteractionErrorAsMessage({
        conversationId,
        interactionType: isMe
          ? ConversationInteractionType.Hide
          : ConversationInteractionType.Leave,
      });
    }
  };

  window?.inboxStore?.dispatch(
    updateConfirmModal({
      title: isMe ? window.i18n('noteToSelfHide') : window.i18n('conversationsDelete'),
      i18nMessage: isMe
        ? { token: 'noteToSelfHideDescription' }
        : {
            token: 'conversationsDeleteDescription',
            args: {
              name: conversation.getNicknameOrRealUsernameOrPlaceholder(),
            },
          },
      onClickOk,
      okText: isMe ? window.i18n('hide') : window.i18n('delete'),
      okTheme: SessionButtonColor.Danger,
      onClickClose,
      conversationId,
    })
  );
}

async function leaveGroupOrCommunityByConvoId(
  conversationId: string,
  isPublic: boolean,
  forceDeleteLocal: boolean,
  onClickClose?: () => void
) {
  try {
    if (onClickClose) {
      onClickClose();
    }

    if (isPublic) {
      await getConversationController().deleteCommunity(conversationId, {
        fromSyncMessage: false,
      });
      return;
    }
    // for groups, we have a "leaving..." state that we don't need for communities.
    // that's because communities can be left always, whereas for groups we need to send a leave message (and so have some encryption keypairs)
    await updateConversationInteractionState({
      conversationId,
      type: ConversationInteractionType.Leave,
      status: ConversationInteractionStatus.Start,
    });
    await getConversationController().deleteClosedGroup(conversationId, {
      fromSyncMessage: false,
      sendLeaveMessage: true,
      forceDeleteLocal,
    });
    await clearConversationInteractionState({ conversationId });
  } catch (err) {
    window.log.warn(`showLeaveGroupByConvoId error: ${err}`);
    await saveConversationInteractionErrorAsMessage({
      conversationId,
      interactionType: ConversationInteractionType.Leave,
    });
  }
}

export async function showLeaveGroupByConvoId(conversationId: string, name: string | undefined) {
  const conversation = getConversationController().get(conversationId);

  if (!conversation.isGroup()) {
    throw new Error('showLeaveGroupDialog() called with a non group convo.');
  }

  const isClosedGroup = conversation.isClosedGroup() || false;
  const isPublic = conversation.isPublic() || false;
  const admins = conversation.get('groupAdmins') || [];
  const isAdmin = admins.includes(UserUtils.getOurPubKeyStrFromCache());
  const showOnlyGroupAdminWarning = isClosedGroup && isAdmin && admins.length === 1;
  const lastMessageInteractionType = conversation.get('lastMessageInteractionType');
  const lastMessageInteractionStatus = conversation.get('lastMessageInteractionStatus');

  if (
    !isPublic &&
    lastMessageInteractionType === ConversationInteractionType.Leave &&
    lastMessageInteractionStatus === ConversationInteractionStatus.Error
  ) {
    await leaveGroupOrCommunityByConvoId(conversationId, isPublic, true);
    return;
  }

  // if this is a community, or we legacy group are not admin, we can just show a confirmation dialog

  const onClickClose = () => {
    window?.inboxStore?.dispatch(updateConfirmModal(null));
  };

  const onClickOk = async () => {
    await leaveGroupOrCommunityByConvoId(conversationId, isPublic, false, onClickClose);
  };

  if (showOnlyGroupAdminWarning) {
    // NOTE For legacy closed groups
    window?.inboxStore?.dispatch(
      updateConfirmModal({
        title: window.i18n('groupLeave'),
        i18nMessage: {
          token: 'groupDeleteDescription',
          args: { group_name: name ?? '' },
        },
        onClickOk,
        okText: window.i18n('leave'),
        okTheme: SessionButtonColor.Danger,
        onClickClose,
        conversationId,
      })
    );
    // TODO Only to be used after the closed group rebuild
    // const onClickOkLastAdmin = () => {
    //   /* TODO */
    // };
    // const onClickCloseLastAdmin = () => {
    //   /* TODO */
    // };
    // window?.inboxStore?.dispatch(
    //   updateConfirmModal({
    //     title: window.i18n('groupLeave'),
    //     message: window.i18n('leaveGroupConfirmationOnlyAdmin', {name: name ?? ''}),
    //     messageSub: window.i18n('leaveGroupConfirmationOnlyAdminWarning'),
    //     onClickOk: onClickOkLastAdmin,
    //     okText: window.i18n('addModerator'),
    //     cancelText: window.i18n('leave'),
    //     onClickCancel: onClickCloseLastAdmin,
    //     closeTheme: SessionButtonColor.Danger,
    //     onClickClose,
    //     showExitIcon: true,
    //     headerReverse: true,
    //     conversationId,
    //   })
    // );
  } else if (isPublic || (isClosedGroup && !isAdmin)) {
    window?.inboxStore?.dispatch(
      updateConfirmModal({
        title: isPublic ? window.i18n('communityLeave') : window.i18n('groupLeave'),
        i18nMessage: { token: 'groupLeaveDescription', args: { group_name: name ?? '' } },
        onClickOk,
        okText: window.i18n('leave'),
        okTheme: SessionButtonColor.Danger,
        onClickClose,
        conversationId,
      })
    );
  }
}

export function showInviteContactByConvoId(conversationId: string) {
  window.inboxStore?.dispatch(updateInviteContactModal({ conversationId }));
}

export function showAddModeratorsByConvoId(conversationId: string) {
  window.inboxStore?.dispatch(updateAddModeratorsModal({ conversationId }));
}

export function showRemoveModeratorsByConvoId(conversationId: string) {
  window.inboxStore?.dispatch(updateRemoveModeratorsModal({ conversationId }));
}

export function showBanUserByConvoId(conversationId: string, pubkey?: string) {
  window.inboxStore?.dispatch(
    updateBanOrUnbanUserModal({ banType: 'ban', conversationId, pubkey })
  );
}

export function showUnbanUserByConvoId(conversationId: string, pubkey?: string) {
  window.inboxStore?.dispatch(
    updateBanOrUnbanUserModal({ banType: 'unban', conversationId, pubkey })
  );
}

export async function markAllReadByConvoId(conversationId: string) {
  const conversation = getConversationController().get(conversationId);
  perfStart(`markAllReadByConvoId-${conversationId}`);

  await conversation?.markAllAsRead();

  perfEnd(`markAllReadByConvoId-${conversationId}`, 'markAllReadByConvoId');
}

export async function setNotificationForConvoId(
  conversationId: string,
  selected: ConversationNotificationSettingType
) {
  const conversation = getConversationController().get(conversationId);

  const existingSettings = conversation.get('triggerNotificationsFor');
  if (existingSettings !== selected) {
    conversation.set({ triggerNotificationsFor: selected });
    await conversation.commit();
  }
}

export async function clearNickNameByConvoId(conversationId: string) {
  const conversation = getConversationController().get(conversationId);
  await conversation.setNickname(null, true);
}

export function showChangeNickNameByConvoId(conversationId: string) {
  window.inboxStore?.dispatch(changeNickNameModal({ conversationId }));
}

export async function deleteAllMessagesByConvoIdNoConfirmation(conversationId: string) {
  const conversation = getConversationController().get(conversationId);
  await Data.removeAllMessagesInConversation(conversationId);

  // destroy message keeps the active timestamp set so the
  // conversation still appears on the conversation list but is empty
  conversation.set({
    lastMessage: null,
    lastMessageInteractionType: null,
    lastMessageInteractionStatus: null,
  });

  await conversation.commit();
  window.inboxStore?.dispatch(conversationReset(conversationId));
}

export function deleteAllMessagesByConvoIdWithConfirmation(conversationId: string) {
  const onClickClose = () => {
    window?.inboxStore?.dispatch(updateConfirmModal(null));
  };

  const onClickOk = async () => {
    await deleteAllMessagesByConvoIdNoConfirmation(conversationId);
    onClickClose();
  };

  window?.inboxStore?.dispatch(
    updateConfirmModal({
      title: window.i18n('deleteMessage', { count: 2 }), // count of 2 to get the plural "Messages Deleted"
      i18nMessage: { token: 'deleteAfterGroupPR3DeleteMessagesConfirmation' },
      onClickOk,
      okTheme: SessionButtonColor.Danger,
      onClickClose,
    })
  );
}

export async function setDisappearingMessagesByConvoId(
  conversationId: string,
  expirationMode: DisappearingMessageConversationModeType,
  seconds?: number
) {
  const conversation = getConversationController().get(conversationId);

  const canSetDisappearing = !conversation.isOutgoingRequest() && !conversation.isIncomingRequest();

  if (!canSetDisappearing) {
    return;
  }

  if (!expirationMode || expirationMode === 'off' || !seconds || seconds <= 0) {
    await conversation.updateExpireTimer({
      providedDisappearingMode: 'off',
      providedExpireTimer: 0,
      fromSync: false,
      fromCurrentDevice: true,
      fromConfigMessage: false,
    });
  } else {
    await conversation.updateExpireTimer({
      providedDisappearingMode: expirationMode,
      providedExpireTimer: seconds,
      fromSync: false,
      fromCurrentDevice: true,
      fromConfigMessage: false,
    });
  }
}

/**
 * This function can be used for reupload our avatar to the fileserver or upload a new avatar.
 *
 * If this is a reupload, the old profileKey is used, otherwise a new one is generated
 */
export async function uploadOurAvatar(newAvatarDecrypted?: ArrayBuffer) {
  const ourConvo = getConversationController().get(UserUtils.getOurPubKeyStrFromCache());
  if (!ourConvo) {
    window.log.warn('ourConvo not found... This is not a valid case');
    return null;
  }

  let profileKey: Uint8Array | null;
  let decryptedAvatarData;
  if (newAvatarDecrypted) {
    // Encrypt with a new key every time
    profileKey = (await getSodiumRenderer()).randombytes_buf(32);
    decryptedAvatarData = newAvatarDecrypted;
  } else {
    // this is a reupload. no need to generate a new profileKey
    const ourConvoProfileKey =
      getConversationController().get(UserUtils.getOurPubKeyStrFromCache())?.get('profileKey') ||
      null;

    profileKey = ourConvoProfileKey ? fromHexToArray(ourConvoProfileKey) : null;
    if (!profileKey) {
      window.log.info('our profileKey not found. Not reuploading our avatar');
      return null;
    }
    const currentAttachmentPath = ourConvo.getAvatarPath();

    if (!currentAttachmentPath) {
      window.log.warn('No attachment currently set for our convo.. Nothing to do.');
      return null;
    }

    const decryptedAvatarUrl = await getDecryptedMediaUrl(currentAttachmentPath, IMAGE_JPEG, true);

    if (!decryptedAvatarUrl) {
      window.log.warn('Could not decrypt avatar stored locally..');
      return null;
    }
    const blob = await urlToBlob(decryptedAvatarUrl);

    decryptedAvatarData = await blob.arrayBuffer();
  }

  if (!decryptedAvatarData?.byteLength) {
    window.log.warn('Could not read content of avatar ...');
    return null;
  }

  const encryptedData = await encryptProfile(decryptedAvatarData, profileKey);

  const avatarPointer = await uploadFileToFsWithOnionV4(encryptedData);
  if (!avatarPointer) {
    window.log.warn('failed to upload avatar to fileserver');
    return null;
  }
  const { fileUrl, fileId } = avatarPointer;

  ourConvo.set('avatarPointer', fileUrl);

  // this encrypts and save the new avatar and returns a new attachment path
  const upgraded = await processNewAttachment({
    isRaw: true,
    data: decryptedAvatarData,
    contentType: MIME.IMAGE_UNKNOWN, // contentType is mostly used to generate previews and screenshot. We do not care for those in this case.
  });
  // Replace our temporary image with the attachment pointer from the server:
  ourConvo.set('avatarInProfile', undefined);
  const displayName = ourConvo.get('displayNameInProfile');

  // write the profileKey even if it did not change
  ourConvo.set({ profileKey: toHex(profileKey) });
  // Replace our temporary image with the attachment pointer from the server:
  // this commits already
  await ourConvo.setSessionProfile({
    avatarPath: upgraded.path,
    displayName,
    avatarImageId: fileId,
  });
  const newTimestampReupload = Date.now();
  await Storage.put(SettingsKey.lastAvatarUploadTimestamp, newTimestampReupload);

  if (newAvatarDecrypted) {
    await setLastProfileUpdateTimestamp(Date.now());
    await ConfigurationSync.queueNewJobIfNeeded();
    const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased();

    if (!userConfigLibsession) {
      await SyncUtils.forceSyncConfigurationNowIfNeeded(true);
    }
  } else {
    window.log.info(
      `Reuploading avatar finished at ${newTimestampReupload}, newAttachmentPointer ${fileUrl}`
    );
  }
  return {
    avatarPointer: ourConvo.get('avatarPointer'),
    profileKey: ourConvo.get('profileKey'),
  };
}

/**
 * This function can be used for clearing our avatar.
 */
export async function clearOurAvatar(commit: boolean = true) {
  const ourConvo = getConversationController().get(UserUtils.getOurPubKeyStrFromCache());
  if (!ourConvo) {
    window.log.warn('ourConvo not found... This is not a valid case');
    return;
  }

  // return early if no change are needed at all
  if (
    isNil(ourConvo.get('avatarPointer')) &&
    isNil(ourConvo.get('avatarInProfile')) &&
    isNil(ourConvo.get('profileKey'))
  ) {
    return;
  }

  ourConvo.set('avatarPointer', undefined);
  ourConvo.set('avatarInProfile', undefined);
  ourConvo.set('profileKey', undefined);

  await setLastProfileUpdateTimestamp(Date.now());

  if (commit) {
    await ourConvo.commit();
    await SyncUtils.forceSyncConfigurationNowIfNeeded(true);
  }
}

export async function replyToMessage(messageId: string) {
  const quotedMessageModel = await Data.getMessageById(messageId);
  if (!quotedMessageModel) {
    window.log.warn('Failed to find message to reply to');
    return false;
  }
  const conversationModel = getConversationController().getOrThrow(
    quotedMessageModel.get('conversationId')
  );

  const quotedMessageProps = await conversationModel.makeQuote(quotedMessageModel);

  if (quotedMessageProps) {
    window.inboxStore?.dispatch(quoteMessage(quotedMessageProps));
  } else {
    window.inboxStore?.dispatch(quoteMessage(undefined));
  }

  return true;
}

export async function resendMessage(messageId: string) {
  const foundMessageModel = await Data.getMessageById(messageId);

  if (!foundMessageModel) {
    window.log.warn('Failed to find message to resend');
    return false;
  }

  await foundMessageModel.retrySend();
  return true;
}

/**
 * Check if what is pasted is a URL and prompt confirmation for a setting change
 * @param e paste event
 */
export async function showLinkSharingConfirmationModalDialog(e: any) {
  const pastedText = e.clipboardData.getData('text');
  if (isURL(pastedText) && !window.getSettingValue(SettingsKey.settingsLinkPreview, false)) {
    const alreadyDisplayedPopup =
      (await Data.getItemById(SettingsKey.hasLinkPreviewPopupBeenDisplayed))?.value || false;
    if (!alreadyDisplayedPopup) {
      window.inboxStore?.dispatch(
        updateConfirmModal({
          title: window.i18n('linkPreviewsEnable'),
          i18nMessage: { token: 'linkPreviewsFirstDescription' },
          okTheme: SessionButtonColor.Danger,
          onClickOk: async () => {
            await window.setSettingValue(SettingsKey.settingsLinkPreview, true);
          },
          onClickClose: async () => {
            await Storage.put(SettingsKey.hasLinkPreviewPopupBeenDisplayed, true);
          },
          okText: window.i18n('enable'),
        })
      );
    }
  }
}

/**
 *
 * @param str String to evaluate
 * @returns boolean if the string is true or false
 */
function isURL(str: string) {
  const urlRegex =
    '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$';
  const url = new RegExp(urlRegex, 'i');
  return str.length < 2083 && url.test(str);
}

export async function callRecipient(pubkey: string, canCall: boolean) {
  const convo = getConversationController().get(pubkey);

  if (!canCall) {
    ToastUtils.pushUnableToCall();
    return;
  }

  if (!getCallMediaPermissionsSettings()) {
    ToastUtils.pushVideoCallPermissionNeeded();
    return;
  }

  if (convo && convo.isPrivate() && !convo.isMe()) {
    await CallManager.USER_callRecipient(convo.id);
  }
}

/**
 * Updates the interaction state for a conversation. Remember to run clearConversationInteractionState() when the interaction is complete and we don't want to show it in the UI anymore.
 * @param conversationId id of the converation we want to interact with
 * @param type the type of conversation interaciton we are doing
 * @param status the status of that interaction
 */
export async function updateConversationInteractionState({
  conversationId,
  type,
  status,
}: {
  conversationId: string;
  type: ConversationInteractionType;
  status: ConversationInteractionStatus;
}) {
  const convo = getConversationController().get(conversationId);
  if (
    convo &&
    (type !== convo.get('lastMessageInteractionType') ||
      status !== convo.get('lastMessageInteractionStatus'))
  ) {
    convo.set('lastMessageInteractionType', type);
    convo.set('lastMessageInteractionStatus', status);

    await convo.commit();
    window.log.debug(
      `updateConversationInteractionState for ${conversationId} to ${type} ${status}`
    );
  }
}

/**
 * Clears the interaction state for a conversation. We would use this when we don't need to show anything in the UI once an action is complete.
 * @param conversationId id of the conversation whose interaction we want to clear
 */
export async function clearConversationInteractionState({
  conversationId,
}: {
  conversationId: string;
}) {
  const convo = getConversationController().get(conversationId);
  if (
    convo &&
    (convo.get('lastMessageInteractionType') || convo.get('lastMessageInteractionStatus'))
  ) {
    convo.set('lastMessageInteractionType', undefined);
    convo.set('lastMessageInteractionStatus', undefined);

    await convo.commit();
    window.log.debug(`clearConversationInteractionState for ${conversationId}`);
  }
}

async function saveConversationInteractionErrorAsMessage({
  conversationId,
  interactionType,
}: {
  conversationId: string;
  interactionType: ConversationInteractionType;
}) {
  const conversation = getConversationController().get(conversationId);
  if (!conversation) {
    return;
  }

  const interactionStatus = ConversationInteractionStatus.Error;

  await updateConversationInteractionState({
    conversationId,
    type: interactionType,
    status: interactionStatus,
  });

  // NOTE at this time we don't have visible control messages in communities
  if (conversation.isPublic()) {
    return;
  }

  // Add an error message to the database so we can view it in the message history
  await conversation?.addSingleIncomingMessage({
    source: GetNetworkTime.getNowWithNetworkOffset().toString(),
    sent_at: Date.now(),
    interactionNotification: {
      interactionType,
      interactionStatus,
    },
    unread: READ_MESSAGE_STATE.read,
    expireTimer: 0,
  });

  conversation.updateLastMessage();
}
