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

import memoizee from 'memoizee';
import lodash from 'lodash';
import type { ConversationModel } from '../models/conversations.preload.ts';
import type { ConversationType } from '../state/ducks/conversations.preload.ts';
import type { ConversationAttributesType } from '../model-types.d.ts';
import type { GroupNameCollisionsWithIdsByTitle } from './groupMemberNameCollisions.std.ts';
import { StorySendMode } from '../types/Stories.std.ts';
import { areWeAdmin } from './areWeAdmin.preload.ts';
import { buildGroupLink } from '../groups.preload.ts';
import { canAddNewMembers } from './canAddNewMembers.preload.ts';
import { canBeAnnouncementGroup } from './canBeAnnouncementGroup.dom.ts';
import { canChangeTimer } from './canChangeTimer.preload.ts';
import { canEditGroupInfo } from './canEditGroupInfo.preload.ts';
import { dropNull } from './dropNull.std.ts';
import { getAboutText } from './getAboutText.dom.ts';
import {
  getAvatarHash,
  getLocalAvatarUrl,
  getLocalProfileAvatarUrl,
  getRawAvatarPath,
  hasAvatar,
} from './avatarUtils.preload.ts';
import { getAvatarData } from './getAvatarData.dom.ts';
import { getConversationMembers } from './getConversationMembers.dom.ts';
import { getCustomColorData, migrateColor } from './migrateColor.node.ts';
import { getDraftPreview } from './getDraftPreview.preload.ts';
import { getLastMessage } from './getLastMessage.preload.ts';
import {
  getNumber,
  getProfileName,
  getTitle,
  getTitleNoDefault,
  canHaveUsername,
  renderNumber,
} from './getTitle.preload.ts';
import { hasDraft } from './hasDraft.std.ts';
import { isAciString } from './isAciString.std.ts';
import { isBlocked } from './isBlocked.preload.ts';
import { isConversationAccepted } from './isConversationAccepted.preload.ts';
import {
  isDirectConversation,
  isGroupV1,
  isGroupV2,
  isMe,
} from './whatTypeOfConversation.dom.ts';
import {
  areWePending,
  getBannedMemberships,
  getMembersCount,
  getMemberships,
  getPendingApprovalMemberships,
  getPendingMemberships,
  isMemberAwaitingApproval,
} from './groupMembershipUtils.preload.ts';
import { isNotNil } from './isNotNil.std.ts';
import { getIdentifierHash } from '../Crypto.node.ts';
import { getAvatarPlaceholderGradient } from '../utils/getAvatarPlaceholderGradient.std.ts';
import { itemStorage } from '../textsecure/Storage.preload.ts';

const { sortBy } = lodash;

const EMPTY_ARRAY: Readonly<[]> = [];
const EMPTY_GROUP_COLLISIONS: GroupNameCollisionsWithIdsByTitle = {};

const getCollator = memoizee((): Intl.Collator => {
  return new Intl.Collator(undefined, { sensitivity: 'base' });
});

function sortConversationTitles(
  left: ConversationAttributesType,
  right: ConversationAttributesType
) {
  return getCollator().compare(getTitle(left), getTitle(right));
}

// Note: this should never be called directly. Use conversation.format() instead, which
//   maintains a cache, and protects against reentrant calls.
// Note: When writing code inside this function, do not call .format() on a conversation
//   unless you are sure that it's not this very same conversation.
// Note: If you start relying on an attribute that is in
//   `ATTRIBUTES_THAT_DONT_INVALIDATE_PROPS_CACHE`, remove it from that list.
export function getConversation(model: ConversationModel): ConversationType {
  const { attributes } = model;
  const typingValues = sortBy(
    Object.values(model.contactTypingTimers || {}),
    'timestamp'
  );
  const typingContactIdTimestamps = Object.fromEntries(
    typingValues.map(({ senderId, timestamp }) => [senderId, timestamp])
  );

  const ourAci = itemStorage.user.getAci();

  const identifierHash = getIdentifierHash({
    aci: isAciString(attributes.serviceId) ? attributes.serviceId : undefined,
    e164: attributes.e164,
    pni: attributes.pni,
    groupId: attributes.groupId,
  });

  const color = migrateColor(attributes.color, {
    aci: isAciString(attributes.serviceId) ? attributes.serviceId : undefined,
    e164: attributes.e164,
    pni: attributes.pni,
    groupId: attributes.groupId,
  });

  const avatarPlaceholderGradient =
    hasAvatar(attributes) && identifierHash != null
      ? getAvatarPlaceholderGradient(identifierHash)
      : undefined;

  const { draftTimestamp, draftEditMessage, timestamp } = attributes;
  const draftPreview = getDraftPreview(attributes);
  const draftText = dropNull(attributes.draft);
  const shouldShowDraft = Boolean(
    hasDraft(attributes) && draftTimestamp && draftTimestamp >= (timestamp || 0)
  );
  const inboxPosition = attributes.inbox_position;
  const ourConversationId =
    window.ConversationController.getOurConversationId();

  let groupVersion: undefined | 1 | 2;
  if (isGroupV1(attributes)) {
    groupVersion = 1;
  } else if (isGroupV2(attributes)) {
    groupVersion = 2;
  }

  const sortedGroupMembers = isGroupV2(attributes)
    ? getConversationMembers(attributes)
        .sort((left, right) => sortConversationTitles(left, right))
        .map(member => window.ConversationController.get(member.id)?.format())
        .filter(isNotNil)
    : undefined;

  const { customColor, customColorId } = getCustomColorData(attributes);

  const isItMe = isMe(attributes);

  // TODO: DESKTOP-720
  return {
    id: attributes.id,
    serviceId: attributes.serviceId,
    pni: attributes.pni,
    e164: attributes.e164,

    // We had previously stored `null` instead of `undefined` in some cases. We should
    //   be able to remove this `dropNull` once usernames have gone to production.
    username: canHaveUsername(attributes, ourConversationId)
      ? dropNull(attributes.username)
      : undefined,

    about: getAboutText(attributes),
    aboutText: attributes.about,
    aboutEmoji: attributes.aboutEmoji,
    acceptedMessageRequest: isConversationAccepted(attributes),
    // oxlint-disable-next-line typescript/no-non-null-assertion
    activeAt: attributes.active_at!,
    areWePending: areWePending(attributes),
    areWePendingApproval: Boolean(
      ourConversationId &&
      ourAci &&
      isMemberAwaitingApproval(attributes, ourAci)
    ),
    areWeAdmin: areWeAdmin(attributes),
    avatarPlaceholderGradient,
    avatars: getAvatarData(attributes),
    badges: attributes.badges ?? EMPTY_ARRAY,
    canChangeTimer: canChangeTimer(attributes),
    canEditGroupInfo: canEditGroupInfo(attributes),
    canAddNewMembers: canAddNewMembers(attributes),
    avatarUrl: getLocalAvatarUrl(attributes),
    rawAvatarPath: getRawAvatarPath(attributes),
    avatarHash: getAvatarHash(attributes),
    profileAvatarUrl: getLocalProfileAvatarUrl(attributes),
    hasAvatar: hasAvatar(attributes),
    color,
    conversationColor: attributes.conversationColor,
    customColor,
    customColorId,
    discoveredUnregisteredAt: attributes.discoveredUnregisteredAt,
    draftBodyRanges: attributes.draftBodyRanges,
    draftPreview,
    draftText,
    draftTimestamp: dropNull(draftTimestamp),
    draftEditMessage,
    familyName: attributes.nicknameFamilyName ?? attributes.profileFamilyName,
    firstName: attributes.nicknameGivenName ?? attributes.profileName,
    groupDescription: attributes.description,
    groupVersion,
    groupId: attributes.groupId,
    groupLink: buildGroupLink(attributes),
    hideStory: Boolean(attributes.hideStory),
    inboxPosition,
    isArchived: attributes.isArchived,
    isBlocked: isBlocked(attributes),
    reportingToken: attributes.reportingToken,
    removalStage: attributes.removalStage,
    isMe: isItMe,
    isGroupV1AndDisabled: isGroupV1(attributes),
    isPinned: attributes.isPinned,
    isUntrusted: model.isUntrusted(),
    isVerified: model.isVerified(),
    isFetchingUUID: model.isFetchingUUID,
    lastMessage: getLastMessage(attributes),
    lastMessageReceivedAt: attributes.lastMessageReceivedAt,
    lastMessageReceivedAtMs: attributes.lastMessageReceivedAtMs,
    lastUpdated: dropNull(timestamp),
    left: Boolean(attributes.left),
    markedUnread: attributes.markedUnread,
    membersCount: getMembersCount(attributes),
    memberships: getMemberships(attributes),
    membersV2: attributes.membersV2,
    messagesDeleted: Boolean(attributes.messagesDeleted),
    hasMessages: (attributes.messageCount ?? 0) > 0,
    pendingMemberships: getPendingMemberships(attributes),
    pendingApprovalMemberships: getPendingApprovalMemberships(attributes),
    bannedMemberships: getBannedMemberships(attributes),
    profileKey: attributes.profileKey,
    accessControlAddFromInviteLink: attributes.accessControl?.addFromInviteLink,
    accessControlAttributes: attributes.accessControl?.attributes,
    accessControlMembers: attributes.accessControl?.members,
    accessControlMemberLabel: attributes.accessControl?.memberLabel,
    announcementsOnly: Boolean(attributes.announcementsOnly),
    announcementsOnlyReady: canBeAnnouncementGroup(attributes),
    expireTimer: attributes.expireTimer,
    muteExpiresAt: attributes.muteExpiresAt,
    dontNotifyForMentionsIfMuted: attributes.dontNotifyForMentionsIfMuted,
    nicknameFamilyName: dropNull(attributes.nicknameFamilyName),
    nicknameGivenName: dropNull(attributes.nicknameGivenName),
    note: dropNull(attributes.note),
    name: attributes.name,
    systemGivenName: attributes.systemGivenName,
    systemFamilyName: attributes.systemFamilyName,
    systemNickname: attributes.systemNickname,
    phoneNumber:
      isItMe && attributes.e164
        ? renderNumber(attributes.e164)
        : getNumber(attributes),
    profileName: getProfileName(attributes),
    profileSharing: attributes.profileSharing,
    profileLastUpdatedAt: attributes.profileLastUpdatedAt,
    capabilities: attributes.capabilities,
    sharingPhoneNumber: attributes.sharingPhoneNumber,
    publicParams: attributes.publicParams,
    secretParams: attributes.secretParams,
    shouldShowDraft,
    sortedGroupMembers,
    terminated: Boolean(attributes.terminated),
    timestamp: dropNull(timestamp),
    title: getTitle(attributes),
    titleNoDefault: getTitleNoDefault(attributes),
    titleNoNickname: getTitle(attributes, { ignoreNickname: true }),
    titleShortNoDefault: getTitle(attributes, { isShort: true }),
    typingContactIdTimestamps,
    searchableTitle: isMe(attributes)
      ? window.SignalContext.i18n('icu:noteToSelf')
      : getTitle(attributes),
    unreadCount: attributes.unreadCount || 0,
    unreadMentionsCount: attributes.unreadMentionsCount || 0,
    ...(isDirectConversation(attributes)
      ? {
          type: 'direct' as const,
        }
      : {
          type: 'group' as const,
          acknowledgedGroupNameCollisions:
            attributes.acknowledgedGroupNameCollisions ||
            EMPTY_GROUP_COLLISIONS,
          storySendMode: attributes.storySendMode ?? StorySendMode.IfActive,
        }),
    voiceNotePlaybackRate: attributes.voiceNotePlaybackRate,
  };
}
