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

public import LibSignalClient

// MARK: - Get recipients

extension TSOutgoingMessage {
    @objc
    open func recipientState(for address: SignalServiceAddress) -> TSOutgoingMessageRecipientState? {
        return recipientAddressStates?[address]
    }

    /// All recipients of this message.
    @objc
    public func recipientAddresses() -> [SignalServiceAddress] {
        return filterRecipientAddresses { _ in
            return true
        }
    }

    /// All recipients of this message to whom we are currently trying to send.
    @objc
    public func sendingRecipientAddresses() -> [SignalServiceAddress] {
        return filterRecipientAddresses { state in
            switch state.status {
            case .sending, .pending: return true
            case .skipped, .failed, .sent, .delivered, .read, .viewed: return false
            }
        }
    }

    /// All recipients of this message to whom it has been sent, including those
    /// for whom it has been delivered, read, or viewed.
    ///
    /// - Note: we only learn "read" status if read receipts are enabled.
    @objc
    public func sentRecipientAddresses() -> [SignalServiceAddress] {
        return filterRecipientAddresses { state in
            switch state.status {
            case .sent, .delivered, .read, .viewed: return true
            case .skipped, .sending, .pending, .failed: return false
            }
        }
    }

    /// All recipients of this message to whom it has been sent and delivered,
    /// including those for whom it has been read or viewed.
    ///
    /// - Note: we only learn "read" status if read receipts are enabled.
    @objc
    public func deliveredRecipientAddresses() -> [SignalServiceAddress] {
        return filterRecipientAddresses { state in
            switch state.status {
            case .delivered, .read, .viewed: return true
            case .skipped, .sending, .sent, .pending, .failed: return false
            }
        }
    }

    /// All recipients of this message to whom it has been sent, delivered, and
    /// read, including those for whom it has been viewed.
    ///
    /// - Note: we only learn "read" status if read receipts are enabled.
    @objc
    open func readRecipientAddresses() -> [SignalServiceAddress] {
        return filterRecipientAddresses { state in
            switch state.status {
            case .read, .viewed: return true
            case .skipped, .sending, .sent, .delivered, .pending, .failed: return false
            }
        }
    }

    /// All recipients of this message to whom it has been sent, delivered, and
    /// viewed.
    @objc
    public func viewedRecipientAddresses() -> [SignalServiceAddress] {
        return filterRecipientAddresses { state in
            switch state.status {
            case .viewed: return true
            case .skipped, .sending, .sent, .delivered, .read, .pending, .failed: return false
            }
        }
    }

    @objc
    public func failedRecipientAddresses(errorCode: Int) -> [SignalServiceAddress] {
        return filterRecipientAddresses { state in
            return state.status == .failed && state.errorCode == errorCode
        }
    }

    private func filterRecipientAddresses(
        predicate: (TSOutgoingMessageRecipientState) -> Bool,
    ) -> [SignalServiceAddress] {
        guard let recipientAddressStates else { return [] }

        return recipientAddressStates.filter { _, state in
            predicate(state)
        }.map { $0.key }
    }

    // MARK: - Update recipients

    public func updateWithRecipientAddressStates(
        _ recipientAddressStates: [SignalServiceAddress: TSOutgoingMessageRecipientState]?,
        tx: DBWriteTransaction,
    ) {
        anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in
            outgoingMessage.recipientAddressStates = recipientAddressStates
        }
    }

    /// Records a successful send to a one recipient.
    func updateWithSentRecipients(
        _ serviceIds: some Sequence<ServiceId>,
        wasSentByUD: Bool,
        tx: DBWriteTransaction,
    ) {
        anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in
            for serviceId in serviceIds {
                let address = SignalServiceAddress(serviceId)
                guard let recipientState = outgoingMessage.recipientAddressStates?[address] else {
                    owsFailDebug("Missing recipient state for recipient: \(address)!")
                    continue
                }

                recipientState.updateStatusIfPossible(.sent)
                recipientState.wasSentByUD = wasSentByUD
            }
        }
    }

    /// Records a skipped send to multiple recipients.
    func updateWithSkippedRecipients(
        _ addresses: some Sequence<SignalServiceAddress>,
        tx: DBWriteTransaction,
    ) {
        anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in
            for address in addresses {
                guard let recipientState = outgoingMessage.recipientAddressStates?[address] else {
                    owsFailDebug("Missing recipient state for recipient: \(address)!")
                    continue
                }
                recipientState.updateStatusIfPossible(.skipped)
            }
        }
    }

    /// Updates recipients based on information from a linked device outgoing
    /// message transcript.
    ///
    /// - Parameter isSentUpdate:
    /// If false, treats this as message creation, overwriting all existing
    /// recipient state. Otherwise, treats this as a sent update, only adding or
    /// updating recipients but never removing.
    func updateRecipientsFromNonLocalDevice(
        _ nonLocalRecipientStates: [SignalServiceAddress: TSOutgoingMessageRecipientState],
        isSentUpdate: Bool,
        transaction tx: DBWriteTransaction,
    ) {
        anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in
            let localRecipientStates = outgoingMessage.recipientAddressStates ?? [:]

            if nonLocalRecipientStates.count > 0 {
                /// If we have specific recipient info from the transcript,
                /// build new recipient states wholesale.
                if isSentUpdate {
                    /// If this is a "sent update", make sure that:
                    ///
                    /// 1) We never remove any recipients. We end up with the
                    /// union of the existing and new recipients.
                    ///
                    /// 2) We never downgrade the recipient state for any
                    /// recipients. Prefer existing recipient state; "sent
                    /// updates" only add new recipients in the "sent" state.
                    var nonLocalRecipientStates = nonLocalRecipientStates
                    for (recipientAddress, recipientState) in localRecipientStates {
                        nonLocalRecipientStates[recipientAddress] = recipientState
                    }

                    outgoingMessage.recipientAddressStates = nonLocalRecipientStates
                } else {
                    outgoingMessage.recipientAddressStates = nonLocalRecipientStates
                }
            } else {
                /// Otherwise, mark any "sending" recipients as "sent".
                for recipientState in localRecipientStates.values {
                    guard recipientState.status == .sending else {
                        continue
                    }

                    recipientState.updateStatusIfPossible(.sent)
                }
            }

            if !isSentUpdate {
                outgoingMessage.wasNotCreatedLocally = true
            }
        }
    }

    /// Records failed sends to the given recipients.
    func updateWithFailedRecipients(
        _ recipientErrors: some Sequence<(serviceId: ServiceId, error: Error)>,
        tx: DBWriteTransaction,
    ) {
        let fatalErrors = recipientErrors.filter { !MessageSender.isRetryableError($0.error) }
        let retryableErrors = recipientErrors.filter { MessageSender.isRetryableError($0.error) }

        if fatalErrors.isEmpty {
            Logger.warn("Couldn't send \(self.timestamp), but all errors are retryable: \(retryableErrors)")
        } else {
            Logger.warn("Couldn't send \(self.timestamp): \(fatalErrors); retryable errors: \(retryableErrors)")
        }

        self.anyUpdateOutgoingMessage(transaction: tx) {
            for (serviceId, error) in recipientErrors {
                guard let recipientState = $0.recipientAddressStates?[SignalServiceAddress(serviceId)] else {
                    owsFailDebug("Missing recipient state for \(serviceId)")
                    continue
                }
                if MessageSender.isRetryableError(error), recipientState.status == .sending {
                    // For retryable errors, we can just set the error code and leave the
                    // state set as Sending
                } else if error is SpamChallengeRequiredError || error is SpamChallengeResolvedError {
                    recipientState.updateStatusIfPossible(.pending)
                } else {
                    recipientState.updateStatusIfPossible(.failed)
                }

                if recipientState.canHaveErrorCode {
                    if error is UntrustedIdentityError {
                        recipientState.errorCode = OWSErrorCode.untrustedIdentity.rawValue
                    } else {
                        recipientState.errorCode = OWSErrorCode.genericFailure.rawValue
                    }
                }
            }
        }
    }

    /// Mark all "sending" recipients as "failed".
    ///
    /// This should be called on app launch.
    @objc
    public func updateWithAllSendingRecipientsMarkedAsFailed(
        error: (any Error)? = nil,
        transaction tx: DBWriteTransaction,
    ) {
        anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in
            if error is AppExpiredError {
                // TODO: Don't store rasterized strings in the database.
                outgoingMessage.mostRecentFailureText = OWSLocalizedString(
                    "ERROR_SENDING_EXPIRED",
                    comment: "Error indicating a send failure due to an expired application.",
                )
            } else if error is NotRegisteredError {
                // TODO: Don't store rasterized strings in the database.
                let tsAccountManager = DependenciesBridge.shared.tsAccountManager
                switch tsAccountManager.registrationState(tx: tx).isPrimaryDevice {
                case .some(true), .none:
                    outgoingMessage.mostRecentFailureText = OWSLocalizedString(
                        "ERROR_SENDING_DEREGISTERED",
                        comment: "Error indicating a send failure due to a deregistered application.",
                    )
                case .some(false):
                    outgoingMessage.mostRecentFailureText = OWSLocalizedString(
                        "ERROR_SENDING_DELINKED",
                        comment: "Error indicating a send failure due to a delinked application.",
                    )
                }
            } else if let error {
                outgoingMessage.mostRecentFailureText = error.userErrorDescription
            }

            guard let recipientAddressStates = outgoingMessage.recipientAddressStates else {
                return
            }

            for recipientState in recipientAddressStates.values {
                if recipientState.status == .sending {
                    recipientState.updateStatusIfPossible(.failed)
                }
            }
        }
    }

    /// Mark all "failed" recipients as "sending".
    ///
    /// This should be called when we start a message send.
    func updateAllUnsentRecipientsAsSending(
        transaction tx: DBWriteTransaction,
    ) {
        anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in
            guard let recipientAddressStates = outgoingMessage.recipientAddressStates else {
                return
            }

            for recipientState in recipientAddressStates.values {
                if recipientState.status == .failed {
                    recipientState.updateStatusIfPossible(.sending)
                }
            }
        }
    }

    /// Called when a message successfully sends.
    /// Subclasses that need to know when a message send succeeds can override this.
    @objc
    public func updateWithSendSuccess(tx: DBWriteTransaction) { }

    // MARK: -

    static func isEligibleToStartExpireTimer(recipientStates: [TSOutgoingMessageRecipientState]) -> Bool {
        let messageState = Self.messageStateForRecipientStates(recipientStates)
        return isEligibleToStartExpireTimer(messageState: messageState)
    }

    // This method will be called after every insert and update, so it needs
    // to be cheap.
    @objc
    static func isEligibleToStartExpireTimer(messageState: TSOutgoingMessageState) -> Bool {
        switch messageState {
        case .sent, .sent_OBSOLETE, .delivered_OBSOLETE:
            // If _all_ recipients have been sent (not necessarily received or viewed)
            // we should start the expire timer.
            return true
        case .pending, .sending, .failed:
            // If _any_ recipient is pending or failed, don't start the timer.
            return false
        }
    }

    @objc
    public var isStorySend: Bool { isGroupStoryReply }

    @objc
    func _buildPlaintextData(inThread thread: TSThread, tx: DBWriteTransaction) throws -> Data {
        guard let contentBuilder = self.contentBuilder(thread: thread, transaction: tx) else {
            throw OWSAssertionError("couldn't build protobuf")
        }
        if let pniSignatureMessage = self.buildPniSignatureMessageIfNeeded(tx: tx) {
            contentBuilder.setPniSignatureMessage(pniSignatureMessage)
        }
        return try contentBuilder.buildSerializedData()
    }

    @objc
    func _dataMessageBuilder(thread: TSThread, tx: DBReadTransaction) -> SSKProtoDataMessageBuilder? {
        let builder = SSKProtoDataMessage.builder()
        builder.setTimestamp(self.timestamp)

        var requiredProtocolVersion = SSKProtoDataMessageProtocolVersion.initial.rawValue

        if self.isViewOnceMessage {
            builder.setIsViewOnce(true)
            requiredProtocolVersion = max(requiredProtocolVersion, SSKProtoDataMessageProtocolVersion.viewOnceVideo.rawValue)
        }

        let trimmedBody = self.body?.trimToUtf8ByteCount(OWSMediaUtils.kOversizeTextMessageSizeThresholdBytes)
        // It was historically possible to end up with a message in the database that
        // exceeds this threshold, and therefore possible to hit this assert (by forwarding
        // an older message). But it is good for us to know when this happens.
        owsAssertDebug(self.body?.utf8.count == trimmedBody?.utf8.count)

        if self.isPoll {
            if let pollCreateProto = self.buildPollProto(tx: tx) {
                builder.setPollCreate(pollCreateProto)
            } else {
                owsFailDebug("Could not build poll protobuf")
            }
            requiredProtocolVersion = max(requiredProtocolVersion, SSKProtoDataMessageProtocolVersion.polls.rawValue)
        } else {
            if let trimmedBody {
                builder.setBody(trimmedBody)
                let bodyRanges = self.bodyRanges?.toProtoBodyRanges(bodyLength: trimmedBody.utf16.count) ?? []
                if !bodyRanges.isEmpty {
                    builder.setBodyRanges(bodyRanges)
                    requiredProtocolVersion = max(requiredProtocolVersion, SSKProtoDataMessageProtocolVersion.mentions.rawValue)
                }
            }
        }

        // Story Context
        if let storyTimestamp, let storyAuthorAci {
            if let storyReactionEmoji {
                let reactionBuilder = SSKProtoDataMessageReaction.builder(emoji: storyReactionEmoji, timestamp: storyTimestamp.uint64Value)
                reactionBuilder.setTargetAuthorAciBinary(storyAuthorAci.serviceIdBinary)

                do {
                    builder.setReaction(try reactionBuilder.build())
                    requiredProtocolVersion = max(requiredProtocolVersion, SSKProtoDataMessageProtocolVersion.reactions.rawValue)
                } catch {
                    owsFailDebug("Could not build story reaction protobuf: \(error)")
                }
            }

            let storyContextBuilder = SSKProtoDataMessageStoryContext.builder()
            storyContextBuilder.setAuthorAciBinary(storyAuthorAci.serviceIdBinary)
            storyContextBuilder.setSentTimestamp(storyTimestamp.uint64Value)

            builder.setStoryContext(storyContextBuilder.buildInfallibly())
        }

        builder.setExpireTimer(self.expiresInSeconds)
        if let expireTimerVersion {
            owsAssertDebug(expireTimerVersion.uint32Value >= 1)
            builder.setExpireTimerVersion(expireTimerVersion.uint32Value)
        }

        // Group Messages
        if let thread = thread as? TSGroupThread {
            switch thread.groupModel.groupsVersion {
            case .V1:
                Logger.error("[GV1] Cannot build data message for V1 group!")
                return nil
            case .V2:
                break
            }
            let result = self.addGroupsV2ToDataMessageBuilder(builder, groupThread: thread, tx: tx)
            switch result {
            case .error:
                return nil
            case .addedWithoutGroupAvatar:
                break
            }
        }

        // Message Attachments

        // Only inserted messages should have attachments, and if they are saveable
        // they should be inserted by now.
        if self.shouldBeSaved {
            if grdbId != nil {
                let attachments = buildProtosForBodyAttachments(tx: tx)
                builder.setAttachments(attachments)
            } else {
                owsFailDebug("Saved message uninserted at proto build time!")
            }
        }

        // Quoted Reply
        if let quotedMessage {
            do {
                let quoteProto = try self.buildQuoteProto(quote: quotedMessage, tx: tx)
                builder.setQuote(quoteProto)
                if !quoteProto.bodyRanges.isEmpty {
                    requiredProtocolVersion = max(requiredProtocolVersion, SSKProtoDataMessageProtocolVersion.mentions.rawValue)
                }
            } catch {
                owsFailDebug("Could not build quote protobuf: \(error)")
            }
        }

        // Contact Share
        if let contactShare {
            do {
                let contactProto = try self.buildContactShareProto(contactShare, tx: tx)
                builder.addContact(contactProto)
            } catch {
                owsFailDebug("Could not build contact share protobuf: \(error)")
            }
        }

        // Link Preview
        if let linkPreview {
            do {
                let previewProto = try self.buildLinkPreviewProto(linkPreview: linkPreview, tx: tx)
                builder.addPreview(previewProto)
            } catch {
                owsFailDebug("Could not build link preview protobuf: \(error)")
            }
        }

        // Sticker
        if let messageSticker {
            do {
                let stickerProto = try self.buildStickerProto(sticker: messageSticker, tx: tx)
                builder.setSticker(stickerProto)
            } catch {
                owsFailDebug("Could not build sticker protobuf: \(error)")
            }
        }

        // Gift badge
        if let giftBadge {
            let giftBadgeBuilder = SSKProtoDataMessageGiftBadge.builder()
            if let redemptionCredential = giftBadge.redemptionCredential {
                giftBadgeBuilder.setReceiptCredentialPresentation(redemptionCredential)
            }
            builder.setGiftBadge(giftBadgeBuilder.buildInfallibly())
        }

        builder.setRequiredProtocolVersion(UInt32(requiredProtocolVersion))
        return builder
    }

    @objc
    func _buildSyncTranscriptMessage(localThread: TSContactThread, tx: DBWriteTransaction) throws -> OutgoingSyncMessage {
        owsAssertDebug(self.shouldSyncTranscript())

        guard let messageThread = self.thread(tx: tx) else {
            throw OWSAssertionError("missing thread for sent message")
        }

        return OutgoingSentMessageTranscript(
            localThread: localThread,
            messageThread: messageThread,
            message: self,
            isRecipientUpdate: self.hasSyncedTranscript,
            tx: tx,
        )
    }

    private func buildPniSignatureMessageIfNeeded(tx: DBReadTransaction) -> SSKProtoPniSignatureMessage? {
        guard recipientAddressStates?.count == 1 else {
            // This is probably a group message, nothing to be alarmed about.
            return nil
        }
        guard let recipientServiceId = recipientAddressStates!.keys.first!.serviceId else {
            return nil
        }
        let identityManager = DependenciesBridge.shared.identityManager
        guard identityManager.shouldSharePhoneNumber(with: recipientServiceId, tx: tx) else {
            // No PNI signature needed.
            return nil
        }
        guard let pni = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx)?.pni else {
            owsFailDebug("missing PNI")
            return nil
        }
        guard let pniIdentityKeyPair = identityManager.identityKeyPair(for: .pni, tx: tx) else {
            owsFailDebug("missing PNI identity key")
            return nil
        }
        guard let aciIdentityKeyPair = identityManager.identityKeyPair(for: .aci, tx: tx) else {
            owsFailDebug("missing ACI identity key")
            return nil
        }

        let signature = pniIdentityKeyPair.identityKeyPair.signAlternateIdentity(
            aciIdentityKeyPair.identityKeyPair.identityKey,
        )

        let builder = SSKProtoPniSignatureMessage.builder()
        builder.setPni(pni.rawUUID.data)
        builder.setSignature(signature)
        return builder.buildInfallibly()
    }

    private func addGroupsV2ToDataMessageBuilder(
        _ builder: SSKProtoDataMessageBuilder,
        groupThread: TSGroupThread,
        tx: DBReadTransaction,
    ) -> OutgoingGroupProtoResult {
        guard let groupModel = groupThread.groupModel as? TSGroupModelV2 else {
            owsFailDebug("Invalid group model.")
            return .error
        }

        do {
            let groupContextV2 = try GroupsV2Protos.buildGroupContextProto(
                groupModel: groupModel,
                groupChangeProtoData: self.changeActionsProtoData,
            )
            builder.setGroupV2(groupContextV2)
            return .addedWithoutGroupAvatar
        } catch {
            owsFailDebug("Error: \(error)")
            return .error
        }
    }

    private func maybeClearShouldSharePhoneNumber(
        for recipientAddress: SignalServiceAddress,
        recipientDeviceId deviceId: DeviceId,
        transaction: DBWriteTransaction,
    ) {
        guard let aci = recipientAddress.serviceId as? Aci else {
            // We can't be sharing our phone number b/c there's no ACI.
            return
        }

        guard recipientAddressStates?[recipientAddress]?.wasSentByUD == true else {
            // Can't be sure the message was actually decrypted by the recipient,
            // because the server sends delivery receipts for non-sealed-sender messages.
            return
        }

        let identityManager = DependenciesBridge.shared.identityManager
        guard identityManager.shouldSharePhoneNumber(with: aci, tx: transaction) else {
            // Not currently sharing anyway!
            return
        }

        let messageSendLog = SSKEnvironment.shared.messageSendLogRef
        let messagePayload = messageSendLog.fetchPayload(
            recipientAci: aci,
            recipientDeviceId: deviceId,
            timestamp: timestamp,
            tx: transaction,
        )
        guard let messagePayload, let payloadId = messagePayload.payloadId else {
            // Can't check whether this message included a PNI signature.
            return
        }

        let deviceIdsPendingDelivery = messageSendLog.deviceIdsPendingDelivery(
            for: payloadId,
            recipientAci: aci,
            tx: transaction,
        )
        guard let deviceIdsPendingDelivery, deviceIdsPendingDelivery == [deviceId] else {
            // Other devices still need the PniSignature.
            return
        }

        guard
            let content = try? SSKProtoContent(serializedData: messagePayload.plaintextContent),
            let messagePniData = content.pniSignatureMessage?.pni
        else {
            // No PNI signature in the message.
            return
        }

        guard let currentPni = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: transaction)?.pni else {
            owsFailDebug("missing local PNI")
            return
        }

        if messagePniData == currentPni.rawUUID.data {
            identityManager.clearShouldSharePhoneNumber(with: aci, tx: transaction)
        }
    }

    // MARK: - Attachments

    private func buildProtosForBodyAttachments(tx: DBReadTransaction) -> [SSKProtoAttachmentPointer] {
        let referencedAttachments = sqliteRowId.map { sqliteRowId in
            return DependenciesBridge.shared.attachmentStore.fetchReferencedAttachments(
                owners: [
                    .messageOversizeText(messageRowId: sqliteRowId),
                    .messageBodyAttachment(messageRowId: sqliteRowId),
                ],
                tx: tx,
            )
        } ?? []

        return referencedAttachments.compactMap { referencedAttachment in
            guard let attachmentProto = referencedAttachment.asProtoForSending() else {
                owsFailDebug("Failed to generate proto for body attachment!")
                return nil
            }

            return attachmentProto
        }
    }

    private func buildLinkPreviewProto(
        linkPreview: OWSLinkPreview,
        tx: DBReadTransaction,
    ) throws -> SSKProtoPreview {
        return try DependenciesBridge.shared.linkPreviewManager.buildProtoForSending(
            linkPreview,
            parentMessage: self,
            tx: tx,
        )
    }

    private func buildContactShareProto(
        _ contact: OWSContact,
        tx: DBReadTransaction,
    ) throws -> SSKProtoDataMessageContact {
        return try DependenciesBridge.shared.contactShareManager.buildProtoForSending(
            from: contact,
            parentMessage: self,
            tx: tx,
        )
    }

    private func buildStickerProto(
        sticker: MessageSticker,
        tx: DBReadTransaction,
    ) throws -> SSKProtoDataMessageSticker {
        return try DependenciesBridge.shared.messageStickerManager.buildProtoForSending(
            sticker,
            parentMessage: self,
            tx: tx,
        )
    }

    private func buildQuoteProto(
        quote: TSQuotedMessage,
        tx: DBReadTransaction,
    ) throws -> SSKProtoDataMessageQuote {
        return try DependenciesBridge.shared.quotedReplyManager.buildProtoForSending(
            quote,
            outgoingMessage: self,
            tx: tx,
        )
    }

    // MARK: - Polls

    private func buildPollProto(tx: DBReadTransaction) -> SSKProtoDataMessagePollCreate? {
        do {
            return try DependenciesBridge.shared.pollMessageManager.buildProtoForSending(
                parentMessage: self,
                tx: tx,
            )
        } catch {
            return nil
        }
    }

    // MARK: - Receipts

    public func update(
        withDeliveredRecipient recipientAddress: SignalServiceAddress,
        deviceId: DeviceId,
        deliveryTimestamp timestamp: UInt64,
        context: DeliveryReceiptContext,
        tx: DBWriteTransaction,
    ) {
        handleReceipt(
            from: recipientAddress,
            deviceId: deviceId,
            receiptType: .delivered,
            receiptTimestamp: timestamp,
            tryToClearPhoneNumberSharing: true,
            context: context,
            tx: tx,
        )
    }

    public func update(
        withReadRecipient recipientAddress: SignalServiceAddress,
        deviceId: DeviceId,
        readTimestamp timestamp: UInt64,
        tx: DBWriteTransaction,
    ) {
        handleReceipt(
            from: recipientAddress,
            deviceId: deviceId,
            receiptType: .read,
            receiptTimestamp: timestamp,
            context: PassthroughDeliveryReceiptContext(),
            tx: tx,
        )
    }

    public func update(
        withViewedRecipient recipientAddress: SignalServiceAddress,
        deviceId: DeviceId,
        viewedTimestamp timestamp: UInt64,
        tx: DBWriteTransaction,
    ) {
        handleReceipt(
            from: recipientAddress,
            deviceId: deviceId,
            receiptType: .viewed,
            receiptTimestamp: timestamp,
            context: PassthroughDeliveryReceiptContext(),
            tx: tx,
        )
    }

    private enum IncomingReceiptType {
        case delivered
        case read
        case viewed

        var asRecipientStatus: OWSOutgoingMessageRecipientStatus {
            switch self {
            case .delivered: return .delivered
            case .read: return .read
            case .viewed: return .viewed
            }
        }
    }

    private func handleReceipt(
        from recipientAddress: SignalServiceAddress,
        deviceId: DeviceId,
        receiptType: IncomingReceiptType,
        receiptTimestamp: UInt64,
        tryToClearPhoneNumberSharing: Bool = false,
        context: any DeliveryReceiptContext,
        tx: DBWriteTransaction,
    ) {
        owsAssertDebug(recipientAddress.isValid)

        // Ignore receipts for messages that have been deleted. They are no longer
        // relevant to this message.
        if wasRemotelyDeleted {
            return
        }

        // Note that this relies on the Message Send Log, so we have to execute it first.
        if tryToClearPhoneNumberSharing {
            maybeClearShouldSharePhoneNumber(for: recipientAddress, recipientDeviceId: deviceId, transaction: tx)
        }

        // This is only necessary for delivery receipts, but while we're here with
        // an open write transaction, we check it for other receipts as well.
        clearMessageSendLogEntry(forRecipient: recipientAddress, deviceId: deviceId, tx: tx)

        let recipientStateMerger = RecipientStateMerger(
            recipientDatabaseTable: DependenciesBridge.shared.recipientDatabaseTable,
            signalServiceAddressCache: SSKEnvironment.shared.signalServiceAddressCacheRef,
        )
        context.addUpdate(
            message: self,
            transaction: tx,
            update: { message in
                guard
                    let recipientState: TSOutgoingMessageRecipientState = {
                        if let existingMatch = message.recipientAddressStates?[recipientAddress] {
                            return existingMatch
                        }
                        if let normalizedAddress = recipientStateMerger.normalizedAddressIfNeeded(for: recipientAddress, tx: tx) {
                            // If we get a receipt from a PNI, then normalizing PNIs -> ACIs won't fix
                            // it, but normalizing the address from a PNI to an ACI might fix it.
                            return message.recipientAddressStates?[normalizedAddress]
                        } else {
                            // If we get a receipt from an ACI, then we might have the PNI stored, and
                            // we need to migrate it to the ACI before we'll be able to find it.
                            recipientStateMerger.normalize(&message.recipientAddressStates, tx: tx)
                            return message.recipientAddressStates?[recipientAddress]
                        }
                    }()
                else {
                    owsFailDebug("Missing recipient state for \(recipientAddress)")
                    return
                }

                recipientState.updateStatusIfPossible(
                    receiptType.asRecipientStatus,
                    statusTimestamp: receiptTimestamp,
                )
            },
        )
    }

    // MARK: - Sender Key + Message Send Log

    /// A collection of message unique IDs related to the outgoing message
    ///
    /// Used to help prune the Message Send Log. For example, a properly annotated outgoing reaction
    /// message will automatically be deleted from the Message Send Log when the reacted message is
    /// deleted.
    ///
    /// Subclasses should override to include any interactionIds their specific subclass relates to. Subclasses
    /// *probably* want to return a union with the results of their parent class' implementation
    @objc
    var relatedUniqueIds: Set<String> {
        Set([self.uniqueId])
    }

    /// Returns a content hint appropriate for representing this content
    ///
    /// If a message is sent with sealed sender, this will be included inside the envelope. A recipient who's
    /// able to decrypt the envelope, but unable to decrypt the inner content can use this to infer how to
    /// handle recovery based on the user-visibility of the content and likelihood of recovery.
    ///
    /// See: SealedSenderContentHint
    @objc
    var contentHint: SealedSenderContentHint {
        .resendable
    }

    /// Returns a groupId relevant to the message. This is included in the envelope, outside the content encryption.
    ///
    /// Usually, this will be the groupId of the target thread. However, there's a special case here where message resend
    /// responses will inherit the groupId of the original message. This probably shouldn't be overridden by anything except
    /// OWSOutgoingMessageResendResponse
    @objc
    func envelopeGroupIdWithTransaction(_ transaction: DBReadTransaction) -> Data? {
        (thread(tx: transaction) as? TSGroupThread)?.groupId
    }

    /// Indicates whether or not this message's proto should be saved into the MessageSendLog
    ///
    /// Anything high volume or time-dependent (typing indicators, calls, etc.) should set this false.
    /// A non-resendable content hint does not necessarily mean this should be false set false (though
    /// it is a good indicator)
    @objc
    var shouldRecordSendLog: Bool { true }

    /// Used in MessageSender to signal how a message should be encrypted before sending
    /// Currently only overridden by OutgoingResendRequest (this is asserted in the MessageSender implementation)
    @objc
    var encryptionStyle: EncryptionStyle { .whisper }

    func clearMessageSendLogEntry(forRecipient address: SignalServiceAddress, deviceId: DeviceId, tx: DBWriteTransaction) {
        // MSL entries will only exist for addresses with ACIs
        guard let aci = address.serviceId as? Aci else {
            return
        }
        let messageSendLog = SSKEnvironment.shared.messageSendLogRef
        messageSendLog.recordSuccessfulDelivery(
            message: self,
            recipientAci: aci,
            recipientDeviceId: deviceId,
            tx: tx,
        )
    }

    @objc
    func markMessageSendLogEntryCompleteIfNeeded(tx: DBWriteTransaction) {
        guard sendingRecipientAddresses().isEmpty else {
            return
        }
        let messageSendLog = SSKEnvironment.shared.messageSendLogRef
        messageSendLog.sendComplete(message: self, tx: tx)
    }
}
