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

import Foundation
import LibSignalClient

// MARK: - Message "isXYZ" properties

private extension TSOutgoingMessage {
    var isTransientSKDM: Bool {
        (self as? OWSOutgoingSenderKeyDistributionMessage)?.isSentOnBehalfOfOnlineMessage ?? false
    }

    var isResendRequest: Bool {
        self is OWSOutgoingResendRequest
    }

    var isSyncMessage: Bool { self is OWSOutgoingSyncMessage }

    var canSendToLocalAddress: Bool {
        return (isSyncMessage ||
                self is OWSOutgoingCallMessage ||
                self is OWSOutgoingResendRequest ||
                self is OWSOutgoingResendResponse)
    }
}

// MARK: - MessageSender

public class MessageSender: Dependencies {

    private var preKeyManager: PreKeyManager { DependenciesBridge.shared.preKeyManager }

    public init() {
        SwiftSingletons.register(self)
    }

    private let pendingTasks = PendingTasks(label: "Message Sends")

    public func pendingSendsPromise() -> Promise<Void> {
        // This promise blocks on all operations already in the queue,
        // but will not block on new operations added after this promise
        // is created. That's intentional to ensure that NotificationService
        // instances complete in a timely way.
        pendingTasks.pendingTasksPromise()
    }

    // MARK: - Creating Signal Protocol Sessions

    private func containsValidSession(for serviceId: ServiceId, deviceId: UInt32, tx: DBReadTransaction) throws -> Bool {
        let sessionStore = DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore
        do {
            guard let session = try sessionStore.loadSession(for: serviceId, deviceId: deviceId, tx: tx) else {
                return false
            }
            return session.hasCurrentState
        } catch {
            switch error {
            case RecipientIdError.mustNotUsePniBecauseAciExists:
                throw error
            default:
                return false
            }
        }
    }

    /// Establishes a session with the recipient if one doesn't already exist.
    private func ensureRecipientHasSession(
        recipientId: AccountId,
        serviceId: ServiceId,
        deviceId: UInt32,
        isOnlineMessage: Bool,
        isTransientSenderKeyDistributionMessage: Bool,
        isStoryMessage: Bool,
        udAccess: OWSUDAccess?
    ) async throws {
        let hasSession = try databaseStorage.read { tx in
            try containsValidSession(for: serviceId, deviceId: deviceId, tx: tx.asV2Read)
        }
        if hasSession {
            return
        }

        let preKeyBundle = try await makePrekeyRequest(
            recipientId: recipientId,
            serviceId: serviceId,
            deviceId: deviceId,
            isOnlineMessage: isOnlineMessage,
            isTransientSenderKeyDistributionMessage: isTransientSenderKeyDistributionMessage,
            isStoryMessage: isStoryMessage,
            udAccess: udAccess
        )

        try await databaseStorage.awaitableWrite { tx in
            try self.createSession(
                for: preKeyBundle,
                recipientId: recipientId,
                serviceId: serviceId,
                deviceId: deviceId,
                transaction: tx
            )
        }
    }

    private func makePrekeyRequest(
        recipientId: AccountId?,
        serviceId: ServiceId,
        deviceId: UInt32,
        isOnlineMessage: Bool,
        isTransientSenderKeyDistributionMessage: Bool,
        isStoryMessage: Bool,
        udAccess: OWSUDAccess?
    ) async throws -> SignalServiceKit.PreKeyBundle {
        Logger.info("serviceId: \(serviceId).\(deviceId)")

        if deviceRecentlyReportedMissing(serviceId: serviceId, deviceId: deviceId) {
            // We don't want to retry prekey requests if we've recently gotten a "404
            // missing device" for the same recipient/device. Fail immediately as
            // though we hit the "404 missing device" error again.
            Logger.info("Skipping prekey request to avoid missing device error.")
            throw MessageSenderError.missingDevice
        }

        // As an optimization, skip the request if an error is likely.
        if let recipientId, willLikelyHaveUntrustedIdentityKeyError(for: recipientId) {
            Logger.info("Skipping prekey request due to untrusted identity.")
            throw UntrustedIdentityError(serviceId: serviceId)
        }

        if let recipientId, willLikelyHaveInvalidKeySignatureError(for: recipientId) {
            Logger.info("Skipping prekey request due to invalid prekey signature.")

            // Check if this error is happening repeatedly for this recipientId.
            // If so, return an InvalidKeySignatureError as a terminal failure.
            throw InvalidKeySignatureError(serviceId: serviceId, isTerminalFailure: true)
        }

        if isOnlineMessage || isTransientSenderKeyDistributionMessage {
            Logger.info("Skipping prekey request for transient message")
            throw MessageSenderNoSessionForTransientMessageError()
        }

        let requestMaker = RequestMaker(
            label: "Prekey Fetch",
            requestFactoryBlock: { (udAccessKeyForRequest: SMKUDAccessKey?) -> TSRequest? in
                return OWSRequestFactory.recipientPreKeyRequest(
                    withServiceId: ServiceIdObjC.wrapValue(serviceId),
                    deviceId: deviceId,
                    udAccessKey: udAccessKeyForRequest
                )
            },
            serviceId: serviceId,
            // Don't use UD for story preKey fetches, we don't have a valid UD auth key
            // TODO: (PreKey Cleanup)
            udAccess: isStoryMessage ? nil : udAccess,
            authedAccount: .implicit(),
            options: []
        )

        do {
            let result = try await requestMaker.makeRequest().awaitable()
            guard let responseObject = result.responseJson as? [String: Any] else {
                throw OWSAssertionError("Prekey fetch missing response object.")
            }
            guard let bundle = SignalServiceKit.PreKeyBundle(from: responseObject, forDeviceNumber: NSNumber(value: deviceId)) else {
                throw OWSAssertionError("Prekey fetch returned an invalid bundle.")
            }
            return bundle
        } catch {
            switch error.httpStatusCode {
            case 404:
                self.reportMissingDeviceError(serviceId: serviceId, deviceId: deviceId)
                throw MessageSenderError.missingDevice
            case 413, 429:
                throw MessageSenderError.prekeyRateLimit
            case 428:
                // SPAM TODO: Only retry messages with -hasRenderableContent
                try await spamChallengeResolver.tryToHandleSilently(
                    bodyData: error.httpResponseData,
                    retryAfter: error.httpRetryAfterDate
                )
                // The resolver has 10s to asynchronously resolve a challenge. If it
                // resolves, great! We'll let MessageSender auto-retry. Otherwise, it'll be
                // marked as "pending".
                throw SpamChallengeResolvedError()
            default:
                throw error
            }
        }
    }

    private func createSession(
        for preKeyBundle: SignalServiceKit.PreKeyBundle,
        recipientId: String,
        serviceId: ServiceId,
        deviceId: UInt32,
        transaction: SDSAnyWriteTransaction
    ) throws {
        assert(!Thread.isMainThread)

        Logger.info("Creating session for \(serviceId), deviceId: \(deviceId)")

        if try containsValidSession(for: serviceId, deviceId: deviceId, tx: transaction.asV2Write) {
            Logger.warn("Session already exists.")
            return
        }

        let bundle: LibSignalClient.PreKeyBundle
        if preKeyBundle.preKeyPublic.isEmpty {
            if preKeyBundle.pqPreKeyPublic.isEmpty {
                Logger.info("Creating prekey bundle with signed prekey (\(preKeyBundle.signedPreKeyId))")
                bundle = try LibSignalClient.PreKeyBundle(
                    registrationId: UInt32(bitPattern: preKeyBundle.registrationId),
                    deviceId: UInt32(bitPattern: preKeyBundle.deviceId),
                    signedPrekeyId: UInt32(bitPattern: preKeyBundle.signedPreKeyId),
                    signedPrekey: try PublicKey(preKeyBundle.signedPreKeyPublic),
                    signedPrekeySignature: preKeyBundle.signedPreKeySignature,
                    identity: try LibSignalClient.IdentityKey(bytes: preKeyBundle.identityKey))
            } else {
                Logger.info("Creating prekey bundle with signed (\(preKeyBundle.signedPreKeyId)) and pq (\(preKeyBundle.pqPreKeyId)) prekey")
                bundle = try LibSignalClient.PreKeyBundle(
                    registrationId: UInt32(bitPattern: preKeyBundle.registrationId),
                    deviceId: UInt32(bitPattern: preKeyBundle.deviceId),
                    signedPrekeyId: UInt32(bitPattern: preKeyBundle.signedPreKeyId),
                    signedPrekey: try PublicKey(preKeyBundle.signedPreKeyPublic),
                    signedPrekeySignature: preKeyBundle.signedPreKeySignature,
                    identity: try LibSignalClient.IdentityKey(bytes: preKeyBundle.identityKey),
                    kyberPrekeyId: UInt32(bitPattern: preKeyBundle.pqPreKeyId),
                    kyberPrekey: try KEMPublicKey(preKeyBundle.pqPreKeyPublic),
                    kyberPrekeySignature: preKeyBundle.pqPreKeySignature
                )
            }
        } else {
            if preKeyBundle.pqPreKeyPublic.isEmpty {
                Logger.info("Creating prekey bundle with signed (\(preKeyBundle.signedPreKeyId)) and one-time (\(preKeyBundle.preKeyId)) prekey")
                bundle = try LibSignalClient.PreKeyBundle(
                    registrationId: UInt32(bitPattern: preKeyBundle.registrationId),
                    deviceId: UInt32(bitPattern: preKeyBundle.deviceId),
                    prekeyId: UInt32(bitPattern: preKeyBundle.preKeyId),
                    prekey: try PublicKey(preKeyBundle.preKeyPublic),
                    signedPrekeyId: UInt32(bitPattern: preKeyBundle.signedPreKeyId),
                    signedPrekey: try PublicKey(preKeyBundle.signedPreKeyPublic),
                    signedPrekeySignature: preKeyBundle.signedPreKeySignature,
                    identity: try LibSignalClient.IdentityKey(bytes: preKeyBundle.identityKey))
            } else {
                Logger.info("Creating prekey bundle with signed (\(preKeyBundle.signedPreKeyId)) and one-time (\(preKeyBundle.preKeyId)) and pq \(preKeyBundle.pqPreKeyId) prekey")
                bundle = try LibSignalClient.PreKeyBundle(
                    registrationId: UInt32(bitPattern: preKeyBundle.registrationId),
                    deviceId: UInt32(bitPattern: preKeyBundle.deviceId),
                    prekeyId: UInt32(bitPattern: preKeyBundle.preKeyId),
                    prekey: try PublicKey(preKeyBundle.preKeyPublic),
                    signedPrekeyId: UInt32(bitPattern: preKeyBundle.signedPreKeyId),
                    signedPrekey: try PublicKey(preKeyBundle.signedPreKeyPublic),
                    signedPrekeySignature: preKeyBundle.signedPreKeySignature,
                    identity: try LibSignalClient.IdentityKey(bytes: preKeyBundle.identityKey),
                    kyberPrekeyId: UInt32(bitPattern: preKeyBundle.pqPreKeyId),
                    kyberPrekey: try KEMPublicKey(preKeyBundle.pqPreKeyPublic),
                    kyberPrekeySignature: preKeyBundle.pqPreKeySignature
                )
            }
        }

        do {
            let identityManager = DependenciesBridge.shared.identityManager
            let protocolAddress = ProtocolAddress(serviceId, deviceId: deviceId)
            try processPreKeyBundle(
                bundle,
                for: protocolAddress,
                sessionStore: DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore,
                identityStore: identityManager.libSignalStore(for: .aci, tx: transaction.asV2Write),
                context: transaction
            )
        } catch SignalError.untrustedIdentity(_) {
            Logger.error("Found untrusted identity for \(serviceId)")
            handleUntrustedIdentityKeyError(
                serviceId: serviceId,
                recipientId: recipientId,
                preKeyBundle: preKeyBundle,
                transaction: transaction
            )
            throw UntrustedIdentityError(serviceId: serviceId)
        } catch SignalError.invalidSignature(_) {
            Logger.error("Invalid key signature for \(serviceId)")

            // Received this error from the server, so this could either be
            // an invalid key due to a broken client, or it may be a random
            // corruption in transit.  Mark having encountered an error for
            // this recipient so later checks can determine if this has happend
            // more than once and fail early.
            // The error thrown here is considered non-terminal which allows
            // the request to be retried.
            hadInvalidKeySignatureError(for: recipientId)
            throw InvalidKeySignatureError(serviceId: serviceId, isTerminalFailure: false)
        }
        owsAssertDebug(try containsValidSession(for: serviceId, deviceId: deviceId, tx: transaction.asV2Write), "Couldn't create session.")
    }

    // MARK: - Untrusted Identities

    private let staleIdentityCache = AtomicDictionary<AccountId, Date>(lock: .init())

    private func handleUntrustedIdentityKeyError(
        serviceId: ServiceId,
        recipientId: AccountId,
        preKeyBundle: SignalServiceKit.PreKeyBundle,
        transaction tx: SDSAnyWriteTransaction
    ) {
        do {
            let identityManager = DependenciesBridge.shared.identityManager
            let newIdentityKey = try IdentityKey(bytes: preKeyBundle.identityKey)
            identityManager.saveIdentityKey(newIdentityKey, for: serviceId, tx: tx.asV2Write)
            staleIdentityCache[recipientId] = Date()
        } catch {
            owsFailDebug("Error: \(error)")
        }
    }

    private func willLikelyHaveUntrustedIdentityKeyError(for recipientId: AccountId) -> Bool {
        assert(!Thread.isMainThread)

        // Prekey rate limits are strict. Therefore, we want to avoid requesting
        // prekey bundles that can't be processed. After a prekey request, we might
        // not be able to process it if the new identity key isn't trusted. We
        // therefore expect all subsequent fetches to fail until that key is
        // trusted, so we don't bother sending them unless the key is trusted.

        guard let mostRecentErrorDate = staleIdentityCache[recipientId] else {
            // We don't have a recent error, so a fetch will probably work.
            return false
        }

        let staleIdentityLifetime = kMinuteInterval * 5
        guard abs(mostRecentErrorDate.timeIntervalSinceNow) < staleIdentityLifetime else {
            // It's been more than five minutes since our last fetch. It's reasonable
            // to try again, even if we don't think it will work. (This helps us
            // discover if there's yet another new identity key.)
            return false
        }

        let identityManager = DependenciesBridge.shared.identityManager
        return databaseStorage.read { tx in
            guard let recipient = SignalRecipient.anyFetch(uniqueId: recipientId, transaction: tx) else {
                return false
            }
            // Otherwise, skip the request if we don't trust the identity.
            let untrustedIdentity = identityManager.untrustedIdentityForSending(
                to: recipient.address,
                untrustedThreshold: nil,
                tx: tx.asV2Read
            )
            return untrustedIdentity != nil
        }
    }

    // MARK: - Invalid Signatures

    private typealias InvalidSignatureCache = [AccountId: InvalidSignatureCacheItem]
    private struct InvalidSignatureCacheItem {
        let lastErrorDate: Date
        let errorCount: UInt32
    }
    private let invalidKeySignatureCache = AtomicValue(InvalidSignatureCache(), lock: .init())

    private func hadInvalidKeySignatureError(for recipientId: AccountId) {
        invalidKeySignatureCache.update { cache in
            var errorCount: UInt32 = 1
            if let mostRecentError = cache[recipientId] {
                errorCount = mostRecentError.errorCount + 1
            }

            cache[recipientId] = InvalidSignatureCacheItem(
                lastErrorDate: Date(),
                errorCount: errorCount
            )
        }
    }

    private func willLikelyHaveInvalidKeySignatureError(for recipientId: AccountId) -> Bool {
        assert(!Thread.isMainThread)

        // Similar to untrusted identity errors, when an invalid signature for a prekey
        // is encountered, it will probably be encountered for a while until the
        // target client rotates prekeys and hopfully fixes the bad signature.
        // To avoid running into prekey rate limits, remember when an error is
        // encountered and slow down sending prekey requests for this recipient.
        //
        // Additionally, there is always a chance of corruption of the prekey
        // bundle during data transmission, which would result in an invalid
        // signature of an otherwise correct bundle. To handle this rare case,
        // don't begin limiting the prekey request until after encounting the
        // second bad signature for a particular recipient.

        guard let mostRecentError = invalidKeySignatureCache.get()[recipientId] else {
            return false
        }

        let staleIdentityLifetime = kMinuteInterval * 5
        guard abs(mostRecentError.lastErrorDate.timeIntervalSinceNow) < staleIdentityLifetime else {

            // Error has expired, remove it to reset the count
            invalidKeySignatureCache.update { cache in
                _ = cache.removeValue(forKey: recipientId)
            }

            return false
        }

        // Let the first error go, only skip starting on the second error
        guard mostRecentError.errorCount > 1 else {
            return false
        }

        return true
    }

    // MARK: - Missing Devices

    private struct CacheKey: Hashable {
        let serviceId: ServiceId
        let deviceId: UInt32
    }

    private let missingDevicesCache = AtomicDictionary<CacheKey, Date>(lock: .init())

    private func reportMissingDeviceError(serviceId: ServiceId, deviceId: UInt32) {
        assert(!Thread.isMainThread)

        guard deviceId == OWSDevice.primaryDeviceId else {
            // For now, only bother ignoring primary devices. HTTP 404s should cause
            // the recipient's device list to be updated, so linked devices shouldn't
            // be a problem.
            return
        }

        let cacheKey = CacheKey(serviceId: serviceId, deviceId: deviceId)
        missingDevicesCache[cacheKey] = Date()
    }

    private func deviceRecentlyReportedMissing(serviceId: ServiceId, deviceId: UInt32) -> Bool {
        assert(!Thread.isMainThread)

        // Prekey rate limits are strict. Therefore, we want to avoid requesting
        // prekey bundles that are missing on the service (404).

        let cacheKey = CacheKey(serviceId: serviceId, deviceId: deviceId)
        let recentlyReportedMissingDate = missingDevicesCache[cacheKey]

        guard let recentlyReportedMissingDate else {
            return false
        }

        // If the "missing device" was recorded more than N minutes ago, try
        // another prekey fetch.  It's conceivable that the recipient has
        // registered (in the primary device case) or linked to the device (in the
        // secondary device case).
        let missingDeviceLifetime = kMinuteInterval * 1
        guard abs(recentlyReportedMissingDate.timeIntervalSinceNow) < missingDeviceLifetime else {
            return false
        }

        return true
    }

    // MARK: - Sending Attachments

    public func sendTransientContactSyncAttachment(
        dataSource: DataSource,
        thread: TSThread
    ) async throws {
        let uploadResult = try await DependenciesBridge.shared.tsResourceUploadManager.uploadTransientAttachment(
            dataSource: dataSource
        )
        let message = databaseStorage.read { tx in
            return OWSSyncContactsMessage(uploadedAttachment: uploadResult, thread: thread, tx: tx)
        }
        let preparedMessage = PreparedOutgoingMessage.preprepared(contactSyncMessage: message)
        let result = await Result { try await sendMessage(preparedMessage) }
        try result.get()
    }

    // MARK: - Constructing Message Sends

    public func sendMessage(_ preparedOutgoingMessage: PreparedOutgoingMessage) async throws {
        let priority: Operation.QueuePriority = await databaseStorage.awaitableWrite { tx in
            preparedOutgoingMessage.updateAllUnsentRecipientsAsSending(tx: tx)
            return preparedOutgoingMessage.sendingQueuePriority(tx: tx)
        }

        Logger.info("Sending \(preparedOutgoingMessage)")

        // We create a PendingTask so we can block on flushing all current message sends.
        let pendingTask = pendingTasks.buildPendingTask(label: "Message Send")
        defer { pendingTask.complete() }

        try await withCheckedThrowingContinuation { continuation in
            let sendMessageOperation = AwaitableAsyncBlockOperation(completionContinuation: continuation) {
                try await preparedOutgoingMessage.send(self.sendPreparedMessage(_:))
            }
            sendMessageOperation.queuePriority = priority

            let uploadOperations = databaseStorage.read { tx in
                preparedOutgoingMessage.attachmentUploadOperations(tx: tx)
            }
            uploadOperations.forEach { uploadOperation in
                sendMessageOperation.addDependency(uploadOperation)
                Upload.uploadQueue.addOperation(uploadOperation)
            }

            sendingQueue(forUniqueThreadId: preparedOutgoingMessage.uniqueThreadId).addOperation(sendMessageOperation)
        }
    }

    private let sendingQueueMap = AtomicValue<[String: OperationQueue]>([:], lock: .init())

    private func sendingQueue(forUniqueThreadId: String) -> OperationQueue {
        let sendingQueueKey = forUniqueThreadId
        return sendingQueueMap.update { sendingQueueMap in
            if let existingQueue = sendingQueueMap[sendingQueueKey] {
                return existingQueue
            }
            let sendingQueue = OperationQueue()
            sendingQueue.qualityOfService = .userInitiated
            sendingQueue.maxConcurrentOperationCount = 1
            sendingQueue.name = "MessageSender-Chat"
            sendingQueueMap[sendingQueueKey] = sendingQueue
            return sendingQueue
        }
    }

    private func waitForPreKeyRotationIfNeeded() async throws {
        while let taskToWaitFor = preKeyRotationTaskIfNeeded() {
            try await taskToWaitFor.value
        }
    }

    private let pendingPreKeyRotation = AtomicValue<Task<Void, Error>?>(nil, lock: .init())

    private func preKeyRotationTaskIfNeeded() -> Task<Void, Error>? {
        return pendingPreKeyRotation.map { existingTask in
            if let existingTask {
                return existingTask
            }
            let shouldRunPreKeyRotation = databaseStorage.read { tx in
                preKeyManager.isAppLockedDueToPreKeyUpdateFailures(tx: tx.asV2Read)
            }
            if shouldRunPreKeyRotation {
                Logger.info("Rotating signed pre-key before sending message.")
                // Retry prekey update every time user tries to send a message while app is
                // disabled due to prekey update failures.
                //
                // Only try to update the signed prekey; updating it is sufficient to
                // re-enable message sending.
                return Task {
                    try await self.preKeyManager.rotateSignedPreKeys().value
                    self.pendingPreKeyRotation.set(nil)
                }
            }
            return nil
        }
    }

    // Mark skipped recipients as such. We may skip because:
    //
    // * A recipient is no longer in the group.
    // * A recipient is blocked.
    // * A recipient is unregistered.
    // * A recipient does not have the required capability.
    private func markSkippedRecipients(
        of message: TSOutgoingMessage,
        sendingRecipients: [ServiceId],
        tx: SDSAnyWriteTransaction
    ) {
        let skippedRecipients = Set(message.sendingRecipientAddresses())
            .subtracting(sendingRecipients.lazy.map { SignalServiceAddress($0) })
        for address in skippedRecipients {
            // Mark this recipient as "skipped".
            message.update(withSkippedRecipient: address, transaction: tx)
        }
    }

    private func unsentRecipients(
        of message: TSOutgoingMessage,
        in thread: TSThread,
        localIdentifiers: LocalIdentifiers,
        tx: SDSAnyReadTransaction
    ) throws -> [SignalServiceAddress] {
        if message.isSyncMessage {
            return [localIdentifiers.aciAddress]
        }

        if let groupThread = thread as? TSGroupThread {
            // Send to the intersection of:
            //
            // * "sending" recipients of the message.
            // * members of the group.
            //
            // I.e. try to send a message IFF:
            //
            // * The recipient was in the group when the message was first tried to be sent.
            // * The recipient is still in the group.
            // * The recipient is in the "sending" state.

            var recipientAddresses = Set<SignalServiceAddress>()

            recipientAddresses.formUnion(message.sendingRecipientAddresses())

            // Only send to members in the latest known group member list.
            // If a member has left the group since this message was enqueued,
            // they should not receive the message.
            let groupMembership = groupThread.groupModel.groupMembership
            var currentValidRecipients = groupMembership.fullMembers

            // ...or latest known list of "additional recipients".
            //
            // This is used to send group update messages for v2 groups to
            // pending members who are not included in .sendingRecipientAddresses().
            if GroupManager.shouldMessageHaveAdditionalRecipients(message, groupThread: groupThread) {
                currentValidRecipients.formUnion(groupMembership.invitedMembers)
            }
            currentValidRecipients.remove(localIdentifiers.aciAddress)
            recipientAddresses.formIntersection(currentValidRecipients)

            let blockedAddresses = blockingManager.blockedAddresses(transaction: tx)
            recipientAddresses.subtract(blockedAddresses)

            return Array(recipientAddresses)
        } else if let contactAddress = (thread as? TSContactThread)?.contactAddress {
            // Treat 1:1 sends to blocked contacts as failures.
            // If we block a user, don't send 1:1 messages to them. The UI
            // should prevent this from occurring, but in some edge cases
            // you might, for example, have a pending outgoing message when
            // you block them.
            if blockingManager.isAddressBlocked(contactAddress, transaction: tx) {
                Logger.info("Skipping 1:1 send to blocked contact: \(contactAddress).")
                throw MessageSenderError.blockedContactRecipient
            } else {
                return [contactAddress]
            }
        } else {
            // Send to the intersection of:
            //
            // * "sending" recipients of the message.
            // * recipients of the thread
            //
            // I.e. try to send a message IFF:
            //
            // * The recipient was part of the thread when the message was first tried to be sent.
            // * The recipient is still part of the thread.
            // * The recipient is in the "sending" state.

            var recipientAddresses = Set(message.sendingRecipientAddresses())

            // Only send to members in the latest known thread recipients list.
            let currentValidThreadRecipients = thread.recipientAddresses(with: tx)

            recipientAddresses.formIntersection(currentValidThreadRecipients)

            let blockedAddresses = blockingManager.blockedAddresses(transaction: tx)
            recipientAddresses.subtract(blockedAddresses)

            if recipientAddresses.contains(localIdentifiers.aciAddress) {
                owsFailDebug("Message send recipients should not include self.")
            }

            return Array(recipientAddresses)
        }
    }

    private static func partitionAddresses(_ addresses: [SignalServiceAddress]) -> ([ServiceId], [E164]) {
        var serviceIds = [ServiceId]()
        var phoneNumbers = [E164]()

        for address in addresses {
            if let serviceId = address.serviceId {
                serviceIds.append(serviceId)
            } else if let phoneNumber = address.e164 {
                phoneNumbers.append(phoneNumber)
            } else {
                owsFailDebug("Recipient has neither ServiceId nor E164.")
            }
        }

        return (serviceIds, phoneNumbers)
    }

    private func lookUpPhoneNumbers(_ phoneNumbers: [E164]) async throws {
        _ = try await contactDiscoveryManager.lookUp(
            phoneNumbers: Set(phoneNumbers.lazy.map { $0.stringValue }),
            mode: .outgoingMessage
        ).awaitable()
    }

    private func areAttachmentsUploadedWithSneakyTransaction(for message: TSOutgoingMessage) -> Bool {
        return databaseStorage.read { tx in
            for attachment in message.allAttachments(transaction: tx) {
                guard attachment.isUploadedToTransitTier else {
                    return false
                }
            }
            return true
        }
    }

    private func sendPreparedMessage(_ message: TSOutgoingMessage) async throws {
        if !areAttachmentsUploadedWithSneakyTransaction(for: message) {
            throw OWSUnretryableMessageSenderError()
        }
        if DependenciesBridge.shared.appExpiry.isExpired {
            throw AppExpiredError()
        }
        if DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered.negated {
            throw AppDeregisteredError()
        }
        if message.shouldBeSaved {
            let latestCopy = databaseStorage.read { tx in
                TSInteraction.anyFetch(uniqueId: message.uniqueId, transaction: tx) as? TSOutgoingMessage
            }
            guard let latestCopy, latestCopy.wasRemotelyDeleted.negated else {
                throw MessageDeletedBeforeSentError()
            }
        }
        if DebugFlags.messageSendsFail.get() {
            throw OWSUnretryableMessageSenderError()
        }
        do {
            try await waitForPreKeyRotationIfNeeded()
            let senderCertificates = try await udManager.ensureSenderCertificates(certificateExpirationPolicy: .permissive).awaitable()
            try await sendPreparedMessage(message, canLookUpPhoneNumbers: true, senderCertificates: senderCertificates)
        } catch {
            if message.wasSentToAnyRecipient {
                // Always ignore the sync error...
                try? await handleMessageSentLocally(message)
            }
            // ...so that we can throw the original error for the caller. (Note that we
            // throw this error even if the sync message is sent successfully.)
            throw error
        }
        try await handleMessageSentLocally(message)
    }

    private enum SendMessageNextAction {
        /// Look up missing phone numbers & then try sending again.
        case lookUpPhoneNumbersAndTryAgain([E164])

        /// Perform the `sendPreparedMessage` step.
        case sendPreparedMessage(
            serializedMessage: SerializedMessage,
            thread: TSThread,
            serviceIds: [ServiceId],
            udAccess: [ServiceId: OWSUDSendingAccess],
            localIdentifiers: LocalIdentifiers
        )
    }

    private func sendPreparedMessage(
        _ message: TSOutgoingMessage,
        canLookUpPhoneNumbers: Bool,
        senderCertificates: SenderCertificates
    ) async throws {
        let nextAction: SendMessageNextAction? = try await databaseStorage.awaitableWrite { tx in
            guard let thread = message.thread(tx: tx) else {
                throw MessageSenderError.threadMissing
            }

            let canSendToThread: Bool = {
                if message is OWSOutgoingReactionMessage {
                    return thread.canSendReactionToThread
                }
                let isChatMessage = (
                    (
                        message.shouldBeSaved
                        && message.insertedMessageHasRenderableContent(rowId: message.sqliteRowId!, tx: tx)
                    )
                    || message is OutgoingGroupCallUpdateMessage
                    || message is OWSOutgoingCallMessage
                )
                return isChatMessage ? thread.canSendChatMessagesToThread() : thread.canSendNonChatMessagesToThread
            }()
            guard canSendToThread else {
                if message.shouldBeSaved {
                    throw OWSAssertionError("Sending to thread blocked.")
                }
                // Pretend to succeed for non-visible messages like read receipts, etc.
                return nil
            }

            let tsAccountManager = DependenciesBridge.shared.tsAccountManager
            guard let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx.asV2Read) else {
                throw OWSAssertionError("Not registered.")
            }

            let proposedAddresses = try self.unsentRecipients(of: message, in: thread, localIdentifiers: localIdentifiers, tx: tx)
            let (serviceIds, phoneNumbersToFetch) = Self.partitionAddresses(proposedAddresses)

            // If we haven't yet tried to look up phone numbers, send an asynchronous
            // request to look up phone numbers, and then try to go through this logic
            // *again* in a new transaction. Things may change for that subsequent
            // attempt, and if there's still missing phone numbers at that point, we'll
            // skip them for this message.
            if canLookUpPhoneNumbers, !phoneNumbersToFetch.isEmpty {
                return .lookUpPhoneNumbersAndTryAgain(phoneNumbersToFetch)
            }

            self.markSkippedRecipients(of: message, sendingRecipients: serviceIds, tx: tx)

            if let contactThread = thread as? TSContactThread {
                // In the "self-send" aka "Note to Self" special case, we only need to send
                // certain kinds of messages. (In particular, regular data messages are
                // sent via their implicit sync message only.)
                if contactThread.contactAddress.isLocalAddress, !message.canSendToLocalAddress {
                    owsAssertDebug(serviceIds.count == 1)
                    Logger.info("Dropping \(type(of: message)) sent to local address (it should be sent by sync message)")
                    // Don't mark self-sent messages as read (or sent) until the sync transcript is sent.
                    return nil
                }
            }

            if serviceIds.isEmpty {
                // All recipients are already sent or can be skipped. NOTE: We might still
                // need to send a sync transcript.
                return nil
            }

            guard let serializedMessage = self.buildAndRecordMessage(message, in: thread, tx: tx) else {
                throw OWSAssertionError("Couldn't build message.")
            }

            let senderCertificate: SenderCertificate = {
                switch self.udManager.phoneNumberSharingMode(tx: tx.asV2Read).orDefault {
                case .everybody:
                    return senderCertificates.defaultCert
                case .nobody:
                    return senderCertificates.uuidOnlyCert
                }
            }()

            let udAccessMap = self.fetchSealedSenderAccess(
                for: serviceIds,
                message: message,
                senderCertificate: senderCertificate,
                localIdentifiers: localIdentifiers,
                tx: tx
            )

            return .sendPreparedMessage(
                serializedMessage: serializedMessage,
                thread: thread,
                serviceIds: serviceIds,
                udAccess: udAccessMap,
                localIdentifiers: localIdentifiers
            )
        }

        switch nextAction {
        case .none:
            return
        case .lookUpPhoneNumbersAndTryAgain(let phoneNumbers):
            try await lookUpPhoneNumbers(phoneNumbers)
            try await sendPreparedMessage(message, canLookUpPhoneNumbers: false, senderCertificates: senderCertificates)
        case .sendPreparedMessage(let serializedMessage, let thread, let serviceIds, let udAccess, let localIdentifiers):
            let allErrors = AtomicArray<(serviceId: ServiceId, error: Error)>(lock: .init())
            do {
                try await sendPreparedMessage(
                    message,
                    serializedMessage: serializedMessage,
                    in: thread,
                    to: serviceIds,
                    udAccess: udAccess,
                    localIdentifiers: localIdentifiers,
                    senderCertificates: senderCertificates,
                    sendErrorBlock: { serviceId, error in
                        allErrors.append((serviceId, error))
                    }
                )
            } catch {
                // We ignore the thrown error & consult `allErrors` instead.
                try await handleSendFailure(message: message, thread: thread, perRecipientErrors: allErrors.get())
            }
        }
    }

    private func sendPreparedMessage(
        _ message: TSOutgoingMessage,
        serializedMessage: SerializedMessage,
        in thread: TSThread,
        to serviceIds: [ServiceId],
        udAccess sendingAccessMap: [ServiceId: OWSUDSendingAccess],
        localIdentifiers: LocalIdentifiers,
        senderCertificates: SenderCertificates,
        sendErrorBlock: @escaping (ServiceId, Error) -> Void
    ) async throws {
        // 3. If we have any participants that support sender key, build a promise
        // for their send.
        let senderKeyStatus = senderKeyStatus(for: thread, intendedRecipients: serviceIds, udAccessMap: sendingAccessMap)

        var senderKeyMessagePromise: Promise<Void>?
        var senderKeyServiceIds: [ServiceId] = senderKeyStatus.allSenderKeyParticipants
        var fanoutServiceIds: [ServiceId] = senderKeyStatus.fanoutParticipants
        if thread.usesSenderKey, senderKeyServiceIds.count >= 2, message.canSendWithSenderKey {
            senderKeyMessagePromise = senderKeyMessageSendPromise(
                message: message,
                plaintextContent: serializedMessage.plaintextData,
                payloadId: serializedMessage.payloadId,
                thread: thread,
                status: senderKeyStatus,
                udAccessMap: sendingAccessMap,
                senderCertificates: senderCertificates,
                localIdentifiers: localIdentifiers,
                sendErrorBlock: sendErrorBlock
            )
        } else {
            senderKeyServiceIds = []
            fanoutServiceIds = serviceIds
            if !message.canSendWithSenderKey {
                Logger.info("Last sender key send attempt failed for message \(message.timestamp). Fanning out")
            }
        }
        owsAssertDebug(fanoutServiceIds.count + senderKeyServiceIds.count == serviceIds.count)

        try await withThrowingTaskGroup(of: Void.self, returning: Void.self) { taskGroup in
            // Perform an "OWSMessageSend" for each non-senderKey recipient.
            for serviceId in fanoutServiceIds {
                let messageSend = OWSMessageSend(
                    message: message,
                    plaintextContent: serializedMessage.plaintextData,
                    plaintextPayloadId: serializedMessage.payloadId,
                    thread: thread,
                    serviceId: serviceId,
                    localIdentifiers: localIdentifiers
                )
                let sealedSenderParameters = sendingAccessMap[serviceId].map {
                    SealedSenderParameters(message: message, udSendingAccess: $0)
                }
                taskGroup.addTask {
                    do {
                        try await self.performMessageSend(messageSend, sealedSenderParameters: sealedSenderParameters)
                    } catch {
                        sendErrorBlock(serviceId, error)
                        throw error
                    }
                }
            }

            // Also wait for the sender key promise.
            if let senderKeyMessagePromise {
                taskGroup.addTask { try await senderKeyMessagePromise.awaitable() }
            }

            // Wait for everything to finish, and *then* throw an arbitrary error b/c
            // the caller doesn't care *what* error is thrown as long as *some* error
            // is thrown when a problem occurs.
            var results = [Result<Void, Error>]()
            while let result = await taskGroup.nextResult() {
                results.append(result)
            }
            for result in results {
                try result.get()
            }
        }
    }

    private func fetchSealedSenderAccess(
        for serviceIds: [ServiceId],
        message: TSOutgoingMessage,
        senderCertificate: SenderCertificate,
        localIdentifiers: LocalIdentifiers,
        tx: SDSAnyReadTransaction
    ) -> [ServiceId: OWSUDSendingAccess] {
        if DebugFlags.disableUD.get() {
            return [:]
        }
        var result = [ServiceId: OWSUDSendingAccess]()
        for serviceId in serviceIds {
            if localIdentifiers.contains(serviceId: serviceId) {
                continue
            }
            let udAccess = (
                message.isStorySend ? udManager.storyUdAccess() : udManager.udAccess(for: serviceId, tx: tx)
            )
            guard let udAccess else {
                continue
            }
            result[serviceId] = OWSUDSendingAccess(udAccess: udAccess, senderCertificate: senderCertificate)
        }
        return result
    }

    private func handleSendFailure(
        message: TSOutgoingMessage,
        thread: TSThread,
        perRecipientErrors allErrors: [(serviceId: ServiceId, error: Error)]
    ) async throws {
        // Some errors should be ignored when sending messages to non 1:1 threads.
        // See discussion on NSError (MessageSender) category.
        let shouldIgnoreError = { (error: Error) -> Bool in
            return !(thread is TSContactThread) && error.shouldBeIgnoredForNonContactThreads
        }

        // Record the individual error for each "failed" recipient.
        await databaseStorage.awaitableWrite { tx in
            for (serviceId, error) in Dictionary(allErrors, uniquingKeysWith: { _, new in new }) {
                if shouldIgnoreError(error) {
                    continue
                }
                message.update(withFailedRecipient: SignalServiceAddress(serviceId), error: error, transaction: tx)
            }
            self.normalizeRecipientStatesIfNeeded(message: message, recipientErrors: allErrors, tx: tx)
        }

        let filteredErrors = allErrors.lazy.map { $0.error }.filter { !shouldIgnoreError($0) }

        // Some errors should never be retried, in order to avoid hitting rate
        // limits, for example.  Unfortunately, since group send retry is
        // all-or-nothing, we need to fail immediately even if some of the other
        // recipients had retryable errors.
        if let fatalError = filteredErrors.first(where: { $0.isFatalError }) {
            throw fatalError
        }

        // If any of the send errors are retryable, we want to retry. Therefore,
        // prefer to propagate a retryable error.
        if let retryableError = filteredErrors.first(where: { $0.isRetryable }) {
            throw retryableError
        }

        // Otherwise, if we have any error at all, propagate it.
        if let anyError = filteredErrors.first {
            throw anyError
        }

        // If we only received errors that we should ignore, consider this send a
        // success, unless the message could not be sent to any recipient.
        if message.sentRecipientsCount() == 0 {
            throw MessageSenderErrorNoValidRecipients()
        }
    }

    private func normalizeRecipientStatesIfNeeded(
        message: TSOutgoingMessage,
        recipientErrors: [(serviceId: ServiceId, error: Error)],
        tx: SDSAnyWriteTransaction
    ) {
        guard recipientErrors.contains(where: {
            switch $0.error {
            case RecipientIdError.mustNotUsePniBecauseAciExists:
                return true
            default:
                return false
            }
        }) else {
            return
        }
        let recipientStateMerger = RecipientStateMerger(
            recipientDatabaseTable: DependenciesBridge.shared.recipientDatabaseTable,
            signalServiceAddressCache: signalServiceAddressCache
        )
        message.anyUpdateOutgoingMessage(transaction: tx) { message in
            recipientStateMerger.normalize(&message.recipientAddressStates, tx: tx.asV2Read)
        }
    }

    /// Sending a reply to a hidden recipient unhides them. But how we
    /// define "reply" is not inclusive of all outgoing messages. We unhide
    /// when the message indicates the user's intent to resume association
    /// with the hidden recipient.
    ///
    /// It is important to be conservative about which messages unhide a
    /// recipient. It is far better to not unhide when should than to
    /// unhide when we should not.
    private func shouldMessageSendUnhideRecipient(_ message: TSOutgoingMessage, tx: SDSAnyReadTransaction) -> Bool {
        if
            message.shouldBeSaved,
            let rowId = message.sqliteRowId,
            // Its a persisted message; check if its renderable
            message.insertedMessageHasRenderableContent(rowId: rowId, tx: tx)
        {
            return true
        }
        if message is OWSOutgoingReactionMessage {
            return true
        }
        if
            let message = message as? OWSOutgoingCallMessage,
            /// OWSOutgoingCallMessages include not only calling
            /// someone (ie, an "offer message"), but also sending
            /// hangup messages, busy messages, and other kinds of
            /// call-related "messages" that do not indicate the
            /// sender's intent to resume association with a recipient.
            message.offerMessage != nil
        {
            return true
        }
        return false
    }

    private func handleMessageSentLocally(_ message: TSOutgoingMessage) async throws {
        await databaseStorage.awaitableWrite { tx in
            if
                let thread = message.thread(tx: tx) as? TSContactThread,
                self.shouldMessageSendUnhideRecipient(message, tx: tx),
                let localAddress = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx.asV2Read)?.aciAddress,
                !localAddress.isEqualToAddress(thread.contactAddress)
            {
                DependenciesBridge.shared.recipientHidingManager.removeHiddenRecipient(
                    thread.contactAddress,
                    wasLocallyInitiated: true,
                    tx: tx.asV2Write
                )
            }
            if message.shouldBeSaved {
                let latestInteraction = TSInteraction.anyFetch(uniqueId: message.uniqueId, transaction: tx)
                guard let latestMessage = latestInteraction as? TSOutgoingMessage else {
                    Logger.warn("Could not update expiration for deleted message.")
                    return
                }
                ViewOnceMessages.completeIfNecessary(message: latestMessage, transaction: tx)
            }
        }

        try await sendSyncTranscriptIfNeeded(for: message)

        // Don't mark self-sent messages as read (or sent) until the sync
        // transcript is sent.
        //
        // NOTE: This only applies to the 'note to self' conversation.
        if message.isSyncMessage {
            return
        }
        let thread = databaseStorage.read { tx in message.thread(tx: tx) }
        guard let contactThread = thread as? TSContactThread, contactThread.contactAddress.isLocalAddress else {
            return
        }
        owsAssertDebug(message.recipientAddresses().count == 1)
        await databaseStorage.awaitableWrite { tx in
            let deviceId = DependenciesBridge.shared.tsAccountManager.storedDeviceId(tx: tx.asV2Read)
            for sendingAddress in message.sendingRecipientAddresses() {
                message.update(
                    withReadRecipient: sendingAddress,
                    deviceId: deviceId,
                    readTimestamp: message.timestamp,
                    tx: tx
                )
                if message.isVoiceMessage || message.isViewOnceMessage {
                    message.update(
                        withViewedRecipient: sendingAddress,
                        deviceId: deviceId,
                        viewedTimestamp: message.timestamp,
                        tx: tx
                    )
                }
            }
        }
    }

    private func sendSyncTranscriptIfNeeded(for message: TSOutgoingMessage) async throws {
        guard message.shouldSyncTranscript() else {
            return
        }
        try await message.sendSyncTranscript()
        await databaseStorage.awaitableWrite { tx in
            message.update(withHasSyncedTranscript: true, transaction: tx)
        }
    }

    // MARK: - Performing Message Sends

    struct SerializedMessage {
        let plaintextData: Data
        let payloadId: Int64?
    }

    func buildAndRecordMessage(
        _ message: TSOutgoingMessage,
        in thread: TSThread,
        tx: SDSAnyWriteTransaction
    ) -> SerializedMessage? {
        guard let plaintextData = message.buildPlainTextData(thread, transaction: tx) else {
            return nil
        }
        let messageSendLog = SSKEnvironment.shared.messageSendLogRef
        let payloadId = messageSendLog.recordPayload(plaintextData, for: message, tx: tx)
        return SerializedMessage(plaintextData: plaintextData, payloadId: payloadId)
    }

    func performMessageSend(
        _ messageSend: OWSMessageSend,
        sealedSenderParameters: SealedSenderParameters?
    ) async throws {
        try await performMessageSendAttempt(
            messageSend,
            sealedSenderParameters: sealedSenderParameters,
            remainingAttempts: 3
        )
    }

    private func retryMessageSend(
        _ messageSend: OWSMessageSend,
        remainingAttempts: Int,
        orThrow error: Error,
        sealedSenderParameters: SealedSenderParameters?
    ) async throws {
        guard remainingAttempts > 1 else {
            throw error
        }
        try await performMessageSendAttempt(
            messageSend,
            sealedSenderParameters: sealedSenderParameters,
            remainingAttempts: remainingAttempts - 1
        )
    }

    private func performMessageSendAttempt(
        _ messageSend: OWSMessageSend,
        sealedSenderParameters: SealedSenderParameters?,
        remainingAttempts: Int
    ) async throws {
        // The caller has access to the error, so they must throw it if no more
        // retries are allowed.
        owsAssert(remainingAttempts > 0)

        let message = messageSend.message
        let serviceId = messageSend.serviceId

        Logger.info("Sending message: \(type(of: message)); timestamp: \(message.timestamp); serviceId: \(serviceId)")

        let deviceMessages: [DeviceMessage]
        do {
            deviceMessages = try await buildDeviceMessages(
                messageSend: messageSend,
                sealedSenderParameters: sealedSenderParameters
            )
        } catch {
            switch error {
            case RequestMakerUDAuthError.udAuthFailure:
                try await retryMessageSend(
                    messageSend,
                    remainingAttempts: remainingAttempts,
                    orThrow: error,
                    sealedSenderParameters: nil  // Retry as an unsealed send.
                )
                return
            default:
                break
            }
            throw error
        }

        if shouldSkipMessageSend(messageSend, deviceMessages: deviceMessages) {
            // This emulates the completion logic of an actual successful send (see below).
            await self.databaseStorage.awaitableWrite { tx in
                message.update(withSkippedRecipient: messageSend.localIdentifiers.aciAddress, transaction: tx)
            }
            return
        }

        for deviceMessage in deviceMessages {
            let hasValidMessageType: Bool = {
                switch deviceMessage.type {
                case .unidentifiedSender:
                    return sealedSenderParameters != nil
                case .ciphertext, .prekeyBundle, .plaintextContent:
                    return sealedSenderParameters == nil
                case .unknown, .keyExchange, .receipt, .senderkeyMessage:
                    return false
                }
            }()
            guard hasValidMessageType else {
                owsFailDebug("Invalid message type: \(deviceMessage.type)")
                throw OWSUnretryableMessageSenderError()
            }
        }

        try await sendDeviceMessages(
            deviceMessages,
            messageSend: messageSend,
            sealedSenderParameters: sealedSenderParameters,
            remainingAttempts: remainingAttempts
        )
    }

    /// We can skip sending sync messages if we know that we have no linked
    /// devices. However, we need to be sure to handle the case where the linked
    /// device list has just changed.
    ///
    /// The linked device list is reflected in two separate pieces of state:
    ///
    /// * OWSDevice's state is updated when you link or unlink a device.
    /// * SignalRecipient's state is updated by 409 "Mismatched devices"
    /// responses from the service.
    ///
    /// If _both_ of these pieces of state agree that there are no linked
    /// devices, then can safely skip sending sync message.
    private func shouldSkipMessageSend(_ messageSend: OWSMessageSend, deviceMessages: [DeviceMessage]) -> Bool {
        guard messageSend.localIdentifiers.contains(serviceId: messageSend.serviceId) else {
            return false
        }
        owsAssertDebug(messageSend.message.canSendToLocalAddress)

        let hasMessageForLinkedDevice = deviceMessages.contains(where: {
            $0.destinationDeviceId != DependenciesBridge.shared.tsAccountManager.storedDeviceIdWithMaybeTransaction
        })

        if hasMessageForLinkedDevice {
            return false
        }

        let mayHaveLinkedDevices = databaseStorage.read { tx in
            DependenciesBridge.shared.deviceManager.mayHaveLinkedDevices(transaction: tx.asV2Read)
        }

        if mayHaveLinkedDevices {
            // We may have just linked a new secondary device which is not yet
            // reflected in the SignalRecipient that corresponds to ourself. Continue
            // sending, where we expect to learn about new devices via a 409 response.
            return false
        }

        return true
    }

    private func buildDeviceMessages(
        messageSend: OWSMessageSend,
        sealedSenderParameters: SealedSenderParameters?
    ) async throws -> [DeviceMessage] {
        let recipientDatabaseTable = DependenciesBridge.shared.recipientDatabaseTable
        let recipient = databaseStorage.read { tx in
            return recipientDatabaseTable.fetchRecipient(serviceId: messageSend.serviceId, transaction: tx.asV2Read)
        }

        // If we think the recipient isn't registered, don't build any device
        // messages. Instead, send an empty message to the server to learn if the
        // account has any devices.
        guard let recipient, recipient.isRegistered else {
            return []
        }

        var recipientDeviceIds = recipient.deviceIds

        if messageSend.localIdentifiers.contains(serviceId: messageSend.serviceId) {
            let localDeviceId = DependenciesBridge.shared.tsAccountManager.storedDeviceIdWithMaybeTransaction
            recipientDeviceIds.removeAll(where: { $0 == localDeviceId })
        }

        var results = [DeviceMessage]()
        for deviceId in recipientDeviceIds {
            let deviceMessage = try await buildDeviceMessage(
                messagePlaintextContent: messageSend.plaintextContent,
                messageEncryptionStyle: messageSend.message.encryptionStyle,
                recipientId: recipient.accountId,
                serviceId: messageSend.serviceId,
                deviceId: deviceId,
                isOnlineMessage: messageSend.message.isOnline,
                isTransientSenderKeyDistributionMessage: messageSend.message.isTransientSKDM,
                isStoryMessage: messageSend.message.isStorySend,
                isResendRequestMessage: messageSend.message.isResendRequest,
                sealedSenderParameters: sealedSenderParameters
            )
            if let deviceMessage {
                results.append(deviceMessage)
            }
        }
        return results
    }

    /// Build a ``DeviceMessage`` for the given parameters describing a message.
    ///
    /// A `nil` return value indicates that the given message could not be built
    /// due to an invalid device ID.
    func buildDeviceMessage(
        messagePlaintextContent: Data,
        messageEncryptionStyle: EncryptionStyle,
        recipientId: AccountId,
        serviceId: ServiceId,
        deviceId: UInt32,
        isOnlineMessage: Bool,
        isTransientSenderKeyDistributionMessage: Bool,
        isStoryMessage: Bool,
        isResendRequestMessage: Bool,
        sealedSenderParameters: SealedSenderParameters?
    ) async throws -> DeviceMessage? {
        AssertNotOnMainThread()

        do {
            try await ensureRecipientHasSession(
                recipientId: recipientId,
                serviceId: serviceId,
                deviceId: deviceId,
                isOnlineMessage: isOnlineMessage,
                isTransientSenderKeyDistributionMessage: isTransientSenderKeyDistributionMessage,
                isStoryMessage: isStoryMessage,
                udAccess: sealedSenderParameters?.udSendingAccess.udAccess
            )
        } catch let error {
            switch error {
            case MessageSenderError.missingDevice:
                // If we have an invalid device exception, remove this device from the
                // recipient and suppress the error.
                await databaseStorage.awaitableWrite { tx in
                    self.updateDevices(
                        serviceId: serviceId,
                        devicesToAdd: [],
                        devicesToRemove: [deviceId],
                        transaction: tx
                    )
                }
                return nil
            case is MessageSenderNoSessionForTransientMessageError:
                // When users re-register, we don't want transient messages (like typing
                // indicators) to cause users to hit the prekey fetch rate limit. So we
                // silently discard these message if there is no pre-existing session for
                // the recipient.
                throw error
            case is UntrustedIdentityError:
                // This *can* happen under normal usage, but it should happen relatively
                // rarely. We expect it to happen whenever Bob reinstalls, and Alice
                // messages Bob before she can pull down his latest identity. If it's
                // happening a lot, we should rethink our profile fetching strategy.
                throw error
            case is InvalidKeySignatureError:
                // This should never happen unless a broken client is uploading invalid
                // keys. The server should now enforce valid signatures on upload,
                // resulting in this become exceedingly rare as time goes by.
                throw error
            case MessageSenderError.prekeyRateLimit:
                throw SignalServiceRateLimitedError()
            case is SpamChallengeRequiredError, is SpamChallengeResolvedError:
                throw error
            case RecipientIdError.mustNotUsePniBecauseAciExists:
                throw error
            case RequestMakerUDAuthError.udAuthFailure:
                throw error
            default:
                owsAssertDebug(error.isNetworkFailureOrTimeout)
                throw OWSRetryableMessageSenderError()
            }
        }

        return try await databaseStorage.awaitableWrite { tx in
            do {
                switch messageEncryptionStyle {
                case .whisper:
                    return try self.encryptMessage(
                        plaintextContent: messagePlaintextContent,
                        serviceId: serviceId,
                        deviceId: deviceId,
                        sealedSenderParameters: sealedSenderParameters,
                        transaction: tx
                    )
                case .plaintext:
                    return try self.wrapPlaintextMessage(
                        plaintextContent: messagePlaintextContent,
                        serviceId: serviceId,
                        deviceId: deviceId,
                        isResendRequestMessage: isResendRequestMessage,
                        sealedSenderParameters: sealedSenderParameters,
                        transaction: tx
                    )
                @unknown default:
                    throw OWSAssertionError("Unrecognized encryption style")
                }
            } catch IdentityManagerError.identityKeyMismatchForOutgoingMessage {
                Logger.warn("Found identity key mismatch on outgoing message to \(serviceId).\(deviceId). Archiving session before retrying...")
                let signalProtocolStoreManager = DependenciesBridge.shared.signalProtocolStoreManager
                let aciSessionStore = signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore
                aciSessionStore.archiveSession(for: serviceId, deviceId: deviceId, tx: tx.asV2Write)
                throw OWSRetryableMessageSenderError()
            } catch {
                Logger.warn("Failed to encrypt message \(error)")
                throw error
            }
        }
    }

    private func sendDeviceMessages(
        _ deviceMessages: [DeviceMessage],
        messageSend: OWSMessageSend,
        sealedSenderParameters: SealedSenderParameters?,
        remainingAttempts: Int
    ) async throws {
        let message: TSOutgoingMessage = messageSend.message

        let requestMaker = RequestMaker(
            label: "Message Send",
            requestFactoryBlock: { (udAccessKey: SMKUDAccessKey?) in
                OWSRequestFactory.submitMessageRequest(
                    withServiceId: ServiceIdObjC.wrapValue(messageSend.serviceId),
                    messages: deviceMessages,
                    timestamp: message.timestamp,
                    udAccessKey: udAccessKey,
                    isOnline: message.isOnline,
                    isUrgent: message.isUrgent,
                    isStory: message.isStorySend
                )
            },
            serviceId: messageSend.serviceId,
            udAccess: sealedSenderParameters?.udSendingAccess.udAccess,
            authedAccount: .implicit(),
            options: []
        )

        do {
            let result = try await requestMaker.makeRequest().awaitable()
            await messageSendDidSucceed(
                messageSend,
                deviceMessages: deviceMessages,
                wasSentByUD: result.wasSentByUD,
                wasSentByWebsocket: result.wasSentByWebsocket
            )
        } catch {
            try await messageSendDidFail(
                messageSend,
                responseError: error,
                sealedSenderParameters: sealedSenderParameters,
                remainingAttempts: remainingAttempts
            )
        }
    }

    private func messageSendDidSucceed(
        _ messageSend: OWSMessageSend,
        deviceMessages: [DeviceMessage],
        wasSentByUD: Bool,
        wasSentByWebsocket: Bool
    ) async {
        let message: TSOutgoingMessage = messageSend.message

        Logger.info("Successfully sent message: \(type(of: message)), serviceId: \(messageSend.serviceId), timestamp: \(message.timestamp), wasSentByUD: \(wasSentByUD), wasSentByWebsocket: \(wasSentByWebsocket)")

        await databaseStorage.awaitableWrite { transaction in
            if deviceMessages.isEmpty, messageSend.localIdentifiers.contains(serviceId: messageSend.serviceId) {
                // Since we know we have no linked devices, we can record that
                // fact to later avoid unnecessary sync message sends unless we
                // later learn of a new linked device.

                Logger.info("Sent a message with no device messages. Recording no linked devices.")

                DependenciesBridge.shared.deviceManager.setMayHaveLinkedDevices(
                    false,
                    transaction: transaction.asV2Write
                )
            }

            deviceMessages.forEach { deviceMessage in
                if let payloadId = messageSend.plaintextPayloadId, let recipientAci = messageSend.serviceId as? Aci {
                    let messageSendLog = SSKEnvironment.shared.messageSendLogRef
                    messageSendLog.recordPendingDelivery(
                        payloadId: payloadId,
                        recipientAci: recipientAci,
                        recipientDeviceId: deviceMessage.destinationDeviceId,
                        message: message,
                        tx: transaction
                    )
                }
            }

            message.update(withSentRecipient: ServiceIdObjC.wrapValue(messageSend.serviceId), wasSentByUD: wasSentByUD, transaction: transaction)

            // If we've just delivered a message to a user, we know they have a valid
            // Signal account. However, if we're sending a story, the server will
            // always tell us the recipient is registered, so we can't use this as an
            // affirmate indication for the existence of an account.
            //
            // This is low trust because we don't actually know for sure the fully
            // qualified address is valid.
            if !message.isStorySend {
                let recipientFetcher = DependenciesBridge.shared.recipientFetcher
                let recipient = recipientFetcher.fetchOrCreate(
                    serviceId: messageSend.serviceId,
                    tx: transaction.asV2Write
                )
                let recipientManager = DependenciesBridge.shared.recipientManager
                recipientManager.markAsRegisteredAndSave(recipient, shouldUpdateStorageService: true, tx: transaction.asV2Write)
            }

            Self.profileManager.didSendOrReceiveMessage(
                serviceId: messageSend.serviceId,
                localIdentifiers: messageSend.localIdentifiers,
                tx: transaction.asV2Write
            )
        }
    }

    private struct MessageSendFailureResponse: Decodable {
        let code: Int?
        let extraDevices: [UInt32]?
        let missingDevices: [UInt32]?
        let staleDevices: [UInt32]?

        static func parse(_ responseData: Data?) -> MessageSendFailureResponse? {
            guard let responseData = responseData else {
                return nil
            }
            do {
                return try JSONDecoder().decode(MessageSendFailureResponse.self, from: responseData)
            } catch {
                owsFailDebug("Error: \(error)")
                return nil
            }
        }
    }

    private func messageSendDidFail(
        _ messageSend: OWSMessageSend,
        responseError: Error,
        sealedSenderParameters: SealedSenderParameters?,
        remainingAttempts: Int
    ) async throws {
        let message: TSOutgoingMessage = messageSend.message

        Logger.warn("\(type(of: message)) to \(messageSend.serviceId), timestamp: \(message.timestamp), error: \(responseError)")

        let httpError: OWSHTTPError?
        switch responseError {
        case RequestMakerUDAuthError.udAuthFailure:
            try await retryMessageSend(
                messageSend,
                remainingAttempts: remainingAttempts,
                orThrow: responseError,
                sealedSenderParameters: nil  // Retry as an unsealed send.
            )
            return
        case let responseError as OWSHTTPError:
            httpError = responseError
        default:
            owsFailDebug("Unexpected error when sending a message.")
            httpError = nil
        }
        switch httpError?.httpStatusCode {
        case 401:
            Logger.warn("Unable to send due to invalid credentials.")
            throw MessageSendUnauthorizedError()
        case 404:
            try await failSendForUnregisteredRecipient(messageSend)
        case 409:
            Logger.warn("Mismatched devices for \(messageSend.serviceId)")

            guard let response = MessageSendFailureResponse.parse(responseError.httpResponseData) else {
                owsFailDebug("Couldn't parse JSON response.")
                throw OWSRetryableMessageSenderError()
            }

            await handleMismatchedDevices(for: messageSend.serviceId, response: response)
        case 410:
            guard let response = MessageSendFailureResponse.parse(responseError.httpResponseData) else {
                owsFailDebug("Couldn't parse JSON response.")
                throw OWSRetryableMessageSenderError()
            }
            await databaseStorage.awaitableWrite { tx in
                self.handleStaleDevices(response.staleDevices, for: messageSend.serviceId, tx: tx.asV2Write)
            }
        case 428:
            // SPAM TODO: Only retry messages with -hasRenderableContent
            Logger.warn("Server requested user complete spam challenge.")
            try await spamChallengeResolver.tryToHandleSilently(
                bodyData: responseError.httpResponseData,
                retryAfter: responseError.httpRetryAfterDate
            )
            // The resolver has 10s to asynchronously resolve a challenge If it
            // resolves, great! We'll let MessageSender auto-retry. Otherwise, it'll be
            // marked as "pending"
        default:
            break
        }

        try await retryMessageSend(
            messageSend,
            remainingAttempts: remainingAttempts,
            orThrow: responseError,
            sealedSenderParameters: sealedSenderParameters
        )
    }

    private func failSendForUnregisteredRecipient(_ messageSend: OWSMessageSend) async throws -> Never {
        let message: TSOutgoingMessage = messageSend.message

        if !message.isSyncMessage {
            await databaseStorage.awaitableWrite { writeTx in
                self.markAsUnregistered(
                    serviceId: messageSend.serviceId,
                    message: message,
                    thread: messageSend.thread,
                    transaction: writeTx
                )
            }
        }

        throw MessageSenderNoSuchSignalRecipientError()
    }

    // MARK: - Unregistered, Missing, & Stale Devices

    func markAsUnregistered(
        serviceId: ServiceId,
        message: TSOutgoingMessage,
        thread: TSThread,
        transaction tx: SDSAnyWriteTransaction
    ) {
        AssertNotOnMainThread()

        if thread.isNonContactThread {
            // Mark as "skipped" group members who no longer have signal accounts.
            message.update(withSkippedRecipient: SignalServiceAddress(serviceId), transaction: tx)
        }

        let recipientDatabaseTable = DependenciesBridge.shared.recipientDatabaseTable
        guard let recipient = recipientDatabaseTable.fetchRecipient(serviceId: serviceId, transaction: tx.asV2Read) else {
            return
        }

        let recipientManager = DependenciesBridge.shared.recipientManager
        recipientManager.markAsUnregisteredAndSave(recipient, unregisteredAt: .now, shouldUpdateStorageService: true, tx: tx.asV2Write)

        let tsAccountManager = DependenciesBridge.shared.tsAccountManager
        guard let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx.asV2Read) else {
            Logger.warn("Can't split recipient because we're not registered.")
            return
        }

        let recipientMerger = DependenciesBridge.shared.recipientMerger
        recipientMerger.splitUnregisteredRecipientIfNeeded(
            localIdentifiers: localIdentifiers,
            unregisteredRecipient: recipient,
            tx: tx.asV2Write
        )
    }

    private func handleMismatchedDevices(for serviceId: ServiceId, response: MessageSendFailureResponse) async {
        await databaseStorage.awaitableWrite { transaction in
            self.updateDevices(
                serviceId: serviceId,
                devicesToAdd: response.missingDevices ?? [],
                devicesToRemove: response.extraDevices ?? [],
                transaction: transaction
            )
        }
    }

    // Called when the server indicates that the devices no longer exist - e.g. when the remote recipient has reinstalled.
    func handleStaleDevices(_ staleDevices: [UInt32]?, for serviceId: ServiceId, tx: DBWriteTransaction) {
        let staleDevices = staleDevices ?? []

        Logger.warn("staleDevices: \(staleDevices) for \(serviceId)")

        guard !staleDevices.isEmpty else {
            return
        }

        let sessionStore = DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore
        for staleDeviceId in staleDevices {
            sessionStore.archiveSession(for: serviceId, deviceId: staleDeviceId, tx: tx)
        }
    }

    func updateDevices(
        serviceId: ServiceId,
        devicesToAdd: [UInt32],
        devicesToRemove: [UInt32],
        transaction: SDSAnyWriteTransaction
    ) {
        AssertNotOnMainThread()
        owsAssertDebug(Set(devicesToAdd).isDisjoint(with: devicesToRemove))

        if !devicesToAdd.isEmpty, SignalServiceAddress(serviceId).isLocalAddress {
            DependenciesBridge.shared.deviceManager.setMayHaveLinkedDevices(
                true,
                transaction: transaction.asV2Write
            )
        }

        let recipientFetcher = DependenciesBridge.shared.recipientFetcher
        let recipient = recipientFetcher.fetchOrCreate(serviceId: serviceId, tx: transaction.asV2Write)
        let recipientManager = DependenciesBridge.shared.recipientManager
        recipientManager.modifyAndSave(
            recipient,
            deviceIdsToAdd: devicesToAdd,
            deviceIdsToRemove: devicesToRemove,
            shouldUpdateStorageService: true,
            tx: transaction.asV2Write
        )

        if !devicesToRemove.isEmpty {
            Logger.info("Archiving sessions for extra devices: \(devicesToRemove)")
            let sessionStore = DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore
            for deviceId in devicesToRemove {
                sessionStore.archiveSession(for: serviceId, deviceId: deviceId, tx: transaction.asV2Write)
            }
        }
    }

    // MARK: - Encryption

    private func encryptMessage(
        plaintextContent plainText: Data,
        serviceId: ServiceId,
        deviceId: UInt32,
        sealedSenderParameters: SealedSenderParameters?,
        transaction: SDSAnyWriteTransaction
    ) throws -> DeviceMessage {
        owsAssertDebug(!Thread.isMainThread)

        guard try containsValidSession(for: serviceId, deviceId: deviceId, tx: transaction.asV2Write) else {
            throw MessageSendEncryptionError(serviceId: serviceId, deviceId: deviceId)
        }

        let paddedPlaintext = plainText.paddedMessageBody

        let serializedMessage: Data
        let messageType: SSKProtoEnvelopeType

        let identityManager = DependenciesBridge.shared.identityManager
        let signalProtocolStore = DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: .aci)
        let protocolAddress = ProtocolAddress(serviceId, deviceId: deviceId)

        if let sealedSenderParameters {
            let secretCipher = try SMKSecretSessionCipher(
                sessionStore: signalProtocolStore.sessionStore,
                preKeyStore: signalProtocolStore.preKeyStore,
                signedPreKeyStore: signalProtocolStore.signedPreKeyStore,
                kyberPreKeyStore: signalProtocolStore.kyberPreKeyStore,
                identityStore: identityManager.libSignalStore(for: .aci, tx: transaction.asV2Write),
                senderKeyStore: Self.senderKeyStore
            )

            serializedMessage = try secretCipher.encryptMessage(
                for: serviceId,
                deviceId: deviceId,
                paddedPlaintext: paddedPlaintext,
                contentHint: sealedSenderParameters.contentHint.signalClientHint,
                groupId: sealedSenderParameters.envelopeGroupId(tx: transaction.asV2Read),
                senderCertificate: sealedSenderParameters.udSendingAccess.senderCertificate,
                protocolContext: transaction
            )

            messageType = .unidentifiedSender

        } else {
            let result = try signalEncrypt(
                message: paddedPlaintext,
                for: protocolAddress,
                sessionStore: signalProtocolStore.sessionStore,
                identityStore: identityManager.libSignalStore(for: .aci, tx: transaction.asV2Write),
                context: transaction
            )

            switch result.messageType {
            case .whisper:
                messageType = .ciphertext
            case .preKey:
                messageType = .prekeyBundle
            case .plaintext:
                messageType = .plaintextContent
            default:
                owsFailDebug("Unrecognized message type")
                messageType = .unknown
            }

            serializedMessage = Data(result.serialize())

            // The message is smaller than the envelope, but if the message
            // is larger than this limit, the envelope will be too.
            if serializedMessage.count > MessageProcessor.largeEnvelopeWarningByteCount {
                owsFailDebug("Unexpectedly large encrypted message.")
            }
        }

        // We had better have a session after encrypting for this recipient!
        let session = try signalProtocolStore.sessionStore.loadSession(
            for: protocolAddress,
            context: transaction
        )!

        return DeviceMessage(
            type: messageType,
            destinationDeviceId: protocolAddress.deviceId,
            destinationRegistrationId: try session.remoteRegistrationId(),
            serializedMessage: serializedMessage
        )
    }

    private func wrapPlaintextMessage(
        plaintextContent rawPlaintext: Data,
        serviceId: ServiceId,
        deviceId: UInt32,
        isResendRequestMessage: Bool,
        sealedSenderParameters: SealedSenderParameters?,
        transaction: SDSAnyWriteTransaction
    ) throws -> DeviceMessage {
        owsAssertDebug(!Thread.isMainThread)

        let identityManager = DependenciesBridge.shared.identityManager
        let protocolAddress = ProtocolAddress(serviceId, deviceId: deviceId)

        // Only resend request messages are allowed to use this codepath.
        guard isResendRequestMessage else {
            throw OWSAssertionError("Unexpected message type")
        }

        let plaintext = try PlaintextContent(bytes: rawPlaintext)

        let serializedMessage: Data
        let messageType: SSKProtoEnvelopeType

        if let sealedSenderParameters {
            let usmc = try UnidentifiedSenderMessageContent(
                CiphertextMessage(plaintext),
                from: sealedSenderParameters.udSendingAccess.senderCertificate,
                contentHint: sealedSenderParameters.contentHint.signalClientHint,
                groupId: sealedSenderParameters.envelopeGroupId(tx: transaction.asV2Read) ?? Data()
            )
            let outerBytes = try sealedSenderEncrypt(
                usmc,
                for: protocolAddress,
                identityStore: identityManager.libSignalStore(for: .aci, tx: transaction.asV2Write),
                context: transaction
            )

            serializedMessage = Data(outerBytes)
            messageType = .unidentifiedSender

        } else {
            serializedMessage = Data(plaintext.serialize())
            messageType = .plaintextContent
        }

        let sessionStore = DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore
        let session = try sessionStore.loadSession(for: protocolAddress, context: transaction)!
        return DeviceMessage(
            type: messageType,
            destinationDeviceId: protocolAddress.deviceId,
            destinationRegistrationId: try session.remoteRegistrationId(),
            serializedMessage: serializedMessage
        )
    }
}
