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

#import "TSOutgoingMessage.h"
#import "AppReadiness.h"
#import "OWSOutgoingSyncMessage.h"
#import "ProtoUtils.h"
#import "TSAttachmentStream.h"
#import "TSContactThread.h"
#import "TSGroupThread.h"
#import "TSQuotedMessage.h"
#import <SignalCoreKit/NSDate+OWS.h>
#import <SignalCoreKit/NSString+OWS.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>

NS_ASSUME_NONNULL_BEGIN

const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024;

NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRecipientAll";

NSString *NSStringForOutgoingMessageState(TSOutgoingMessageState value)
{
    switch (value) {
        case TSOutgoingMessageStateSending:
            return @"TSOutgoingMessageStateSending";
        case TSOutgoingMessageStateFailed:
            return @"TSOutgoingMessageStateFailed";
        case TSOutgoingMessageStateSent_OBSOLETE:
            return @"TSOutgoingMessageStateSent_OBSOLETE";
        case TSOutgoingMessageStateDelivered_OBSOLETE:
            return @"TSOutgoingMessageStateDelivered_OBSOLETE";
        case TSOutgoingMessageStateSent:
            return @"TSOutgoingMessageStateSent";
        case TSOutgoingMessageStatePending:
            return @"TSOutgoingMessageStatePending";
    }
}

NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientState value)
{
    switch (value) {
        case OWSOutgoingMessageRecipientStateFailed:
            return @"OWSOutgoingMessageRecipientStateFailed";
        case OWSOutgoingMessageRecipientStateSending:
            return @"OWSOutgoingMessageRecipientStateSending";
        case OWSOutgoingMessageRecipientStateSkipped:
            return @"OWSOutgoingMessageRecipientStateSkipped";
        case OWSOutgoingMessageRecipientStateSent:
            return @"OWSOutgoingMessageRecipientStateSent";
        case OWSOutgoingMessageRecipientStatePending:
            return @"OWSOutgoingMessageRecipientStatePending";
    }
}

#pragma mark -

@interface TSMessage (Private)

- (void)removeAllAttachmentsWithTransaction:(SDSAnyWriteTransaction *)transaction;

@end

#pragma mark -

@interface TSOutgoingMessageRecipientState ()

@end

#pragma mark -

@implementation TSOutgoingMessageRecipientState

@end

#pragma mark -

NSUInteger const TSOutgoingMessageSchemaVersion = 1;

@interface TSOutgoingMessage ()

@property (atomic) BOOL hasSyncedTranscript;
@property (atomic, nullable) NSString *customMessage;
@property (atomic, nullable) NSString *mostRecentFailureText;
@property (atomic) BOOL wasNotCreatedLocally;
@property (atomic) TSGroupMetaMessage groupMetaMessage;
@property (nonatomic, readonly) NSUInteger outgoingMessageSchemaVersion;

@property (nonatomic, readonly) TSOutgoingMessageState legacyMessageState;
@property (nonatomic, readonly) BOOL legacyWasDelivered;
@property (nonatomic, readonly) BOOL hasLegacyMessageState;

// This property is only intended to be used by GRDB queries.
@property (nonatomic, readonly) TSOutgoingMessageState storedMessageState;

@end

#pragma mark -

@implementation TSOutgoingMessage

// --- CODE GENERATION MARKER

// This snippet is generated by /Scripts/sds_codegen/sds_generate.py. Do not manually edit it, instead run
// `sds_codegen.sh`.

// clang-format off

- (instancetype)initWithGrdbId:(int64_t)grdbId
                      uniqueId:(NSString *)uniqueId
             receivedAtTimestamp:(uint64_t)receivedAtTimestamp
                          sortId:(uint64_t)sortId
                       timestamp:(uint64_t)timestamp
                  uniqueThreadId:(NSString *)uniqueThreadId
                   attachmentIds:(NSArray<NSString *> *)attachmentIds
                            body:(nullable NSString *)body
                      bodyRanges:(nullable MessageBodyRanges *)bodyRanges
                    contactShare:(nullable OWSContact *)contactShare
                       editState:(TSEditState)editState
                 expireStartedAt:(uint64_t)expireStartedAt
                       expiresAt:(uint64_t)expiresAt
                expiresInSeconds:(unsigned int)expiresInSeconds
                       giftBadge:(nullable OWSGiftBadge *)giftBadge
               isGroupStoryReply:(BOOL)isGroupStoryReply
              isViewOnceComplete:(BOOL)isViewOnceComplete
               isViewOnceMessage:(BOOL)isViewOnceMessage
                     linkPreview:(nullable OWSLinkPreview *)linkPreview
                  messageSticker:(nullable MessageSticker *)messageSticker
                   quotedMessage:(nullable TSQuotedMessage *)quotedMessage
    storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer
           storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString
              storyReactionEmoji:(nullable NSString *)storyReactionEmoji
                  storyTimestamp:(nullable NSNumber *)storyTimestamp
              wasRemotelyDeleted:(BOOL)wasRemotelyDeleted
                   customMessage:(nullable NSString *)customMessage
                groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage
           hasLegacyMessageState:(BOOL)hasLegacyMessageState
             hasSyncedTranscript:(BOOL)hasSyncedTranscript
                  isVoiceMessage:(BOOL)isVoiceMessage
              legacyMessageState:(TSOutgoingMessageState)legacyMessageState
              legacyWasDelivered:(BOOL)legacyWasDelivered
           mostRecentFailureText:(nullable NSString *)mostRecentFailureText
          recipientAddressStates:(nullable NSDictionary<SignalServiceAddress *,TSOutgoingMessageRecipientState *> *)recipientAddressStates
              storedMessageState:(TSOutgoingMessageState)storedMessageState
            wasNotCreatedLocally:(BOOL)wasNotCreatedLocally
{
    self = [super initWithGrdbId:grdbId
                        uniqueId:uniqueId
               receivedAtTimestamp:receivedAtTimestamp
                            sortId:sortId
                         timestamp:timestamp
                    uniqueThreadId:uniqueThreadId
                     attachmentIds:attachmentIds
                              body:body
                        bodyRanges:bodyRanges
                      contactShare:contactShare
                         editState:editState
                   expireStartedAt:expireStartedAt
                         expiresAt:expiresAt
                  expiresInSeconds:expiresInSeconds
                         giftBadge:giftBadge
                 isGroupStoryReply:isGroupStoryReply
                isViewOnceComplete:isViewOnceComplete
                 isViewOnceMessage:isViewOnceMessage
                       linkPreview:linkPreview
                    messageSticker:messageSticker
                     quotedMessage:quotedMessage
      storedShouldStartExpireTimer:storedShouldStartExpireTimer
             storyAuthorUuidString:storyAuthorUuidString
                storyReactionEmoji:storyReactionEmoji
                    storyTimestamp:storyTimestamp
                wasRemotelyDeleted:wasRemotelyDeleted];

    if (!self) {
        return self;
    }

    _customMessage = customMessage;
    _groupMetaMessage = groupMetaMessage;
    _hasLegacyMessageState = hasLegacyMessageState;
    _hasSyncedTranscript = hasSyncedTranscript;
    _isVoiceMessage = isVoiceMessage;
    _legacyMessageState = legacyMessageState;
    _legacyWasDelivered = legacyWasDelivered;
    _mostRecentFailureText = mostRecentFailureText;
    _recipientAddressStates = recipientAddressStates;
    _storedMessageState = storedMessageState;
    _wasNotCreatedLocally = wasNotCreatedLocally;

    return self;
}

// clang-format on

// --- CODE GENERATION MARKER

- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];

    if (self) {
#ifndef TESTABLE_BUILD
        OWSAssertDebug(self.outgoingMessageSchemaVersion >= 1);
#endif

        _outgoingMessageSchemaVersion = TSOutgoingMessageSchemaVersion;
    }


    return self;
}

+ (instancetype)outgoingMessageInThread:(TSThread *)thread messageBody:(nullable NSString *)body
{
    return [self outgoingMessageInThread:thread messageBody:body expiresInSeconds:0];
}

+ (instancetype)outgoingMessageInThread:(TSThread *)thread
                            messageBody:(nullable NSString *)body
                       expiresInSeconds:(uint32_t)expiresInSeconds
{
    TSOutgoingMessageBuilder *builder = [TSOutgoingMessageBuilder outgoingMessageBuilderWithThread:thread];
    builder.messageBody = body;
    builder.expiresInSeconds = expiresInSeconds;
    return [builder buildWithSneakyTransaction];
}

- (instancetype)initOutgoingMessageWithBuilder:(TSOutgoingMessageBuilder *)outgoingMessageBuilder
                                   transaction:(SDSAnyReadTransaction *)transaction
{
    self = [super initMessageWithBuilder:outgoingMessageBuilder];
    if (!self) {
        return self;
    }

    _hasSyncedTranscript = NO;

    TSThread *thread = outgoingMessageBuilder.thread;
    TSGroupMetaMessage groupMetaMessage = outgoingMessageBuilder.groupMetaMessage;

    if ([thread isKindOfClass:TSGroupThread.class]) {
        // Unless specified, we assume group messages are "Delivery" i.e. normal messages.
        if (groupMetaMessage == TSGroupMetaMessageUnspecified) {
            _groupMetaMessage = TSGroupMetaMessageDeliver;
        } else {
            _groupMetaMessage = groupMetaMessage;
        }
    } else {
        OWSAssertDebug(groupMetaMessage == TSGroupMetaMessageUnspecified);
        // Specifying a group meta message only makes sense for Group threads
        _groupMetaMessage = TSGroupMetaMessageUnspecified;
    }

    _isVoiceMessage = outgoingMessageBuilder.isVoiceMessage;

    // New outgoing messages should immediately determine their
    // recipient list from current thread state.
    NSMutableSet<SignalServiceAddress *> *recipientAddresses = [NSMutableSet new];
    if ([self isKindOfClass:[OWSOutgoingSyncMessage class]]) {
        // Sync messages should only be sent to linked devices.
        SignalServiceAddress *localAddress = [TSAccountManagerObjcBridge localAciAddressWith:transaction];
        OWSAssertDebug(localAddress);
        [recipientAddresses addObject:localAddress];
    } else {
        // Most messages should only be sent to the current members of the group.
        [recipientAddresses addObjectsFromArray:[thread recipientAddressesWithTransaction:transaction]];
        // Some messages (eg certain call messages) go to a subset of the group.
        if (outgoingMessageBuilder.explicitRecipients != nil) {
            NSMutableSet<SignalServiceAddress *> *explicitRecipientAddresses = [[NSMutableSet alloc] init];
            for (AciObjC *recipientAci in outgoingMessageBuilder.explicitRecipients) {
                [explicitRecipientAddresses
                    addObject:[[SignalServiceAddress alloc] initWithServiceIdObjC:recipientAci]];
            }
            [recipientAddresses intersectSet:explicitRecipientAddresses];
        }
        // Group updates should also be sent to pending members of the group.
        if (outgoingMessageBuilder.additionalRecipients) {
            [recipientAddresses addObjectsFromArray:outgoingMessageBuilder.additionalRecipients];
        }
    }

    NSMutableDictionary<SignalServiceAddress *, TSOutgoingMessageRecipientState *> *recipientAddressStates =
        [NSMutableDictionary new];
    for (SignalServiceAddress *recipientAddress in recipientAddresses) {
        if (!recipientAddress.isValid) {
            OWSFailDebug(@"Ignoring invalid address.");
            continue;
        }
        TSOutgoingMessageRecipientState *recipientState = [TSOutgoingMessageRecipientState new];
        recipientState.state = [outgoingMessageBuilder.skippedRecipients containsObject:recipientAddress]
            ? OWSOutgoingMessageRecipientStateSkipped
            : OWSOutgoingMessageRecipientStateSending;
        recipientAddressStates[recipientAddress] = recipientState;
    }
    self.recipientAddressStates = [recipientAddressStates copy];
    _outgoingMessageSchemaVersion = TSOutgoingMessageSchemaVersion;

    _changeActionsProtoData = outgoingMessageBuilder.changeActionsProtoData;

    return self;
}

#pragma mark -

- (TSOutgoingMessageState)messageState
{
    TSOutgoingMessageState newMessageState =
        [TSOutgoingMessage messageStateForRecipientStates:self.recipientAddressStates.allValues];
    if (self.hasLegacyMessageState) {
        if (newMessageState == TSOutgoingMessageStateSent || self.legacyMessageState == TSOutgoingMessageStateSent) {
            return TSOutgoingMessageStateSent;
        }
    }
    return newMessageState;
}

- (BOOL)wasDeliveredToAnyRecipient
{
    if (self.deliveredRecipientAddresses.count > 0) {
        return YES;
    }
    return (self.hasLegacyMessageState && self.legacyWasDelivered && self.messageState == TSOutgoingMessageStateSent);
}

- (BOOL)wasSentToAnyRecipient
{
    if (self.sentRecipientAddresses.count > 0) {
        return YES;
    }
    return (self.hasLegacyMessageState && self.messageState == TSOutgoingMessageStateSent);
}

+ (TSOutgoingMessageState)messageStateForRecipientStates:(NSArray<TSOutgoingMessageRecipientState *> *)recipientStates
{
    OWSAssertDebug(recipientStates);

    // If there are any "sending" recipients, consider this message "sending".
    // If there are any "pending" recipients, consider this message "pending".
    BOOL hasFailed = NO;
    for (TSOutgoingMessageRecipientState *recipientState in recipientStates) {
        if (recipientState.state == OWSOutgoingMessageRecipientStateSending) {
            return TSOutgoingMessageStateSending;
        } else if (recipientState.state == OWSOutgoingMessageRecipientStatePending) {
            return TSOutgoingMessageStatePending;
        } else if (recipientState.state == OWSOutgoingMessageRecipientStateFailed) {
            hasFailed = YES;
        }
    }

    // If there are any "failed" recipients, consider this message "failed".
    if (hasFailed) {
        return TSOutgoingMessageStateFailed;
    }

    // Otherwise, consider the message "sent".
    //
    // NOTE: This includes messages with no recipients.
    return TSOutgoingMessageStateSent;
}

- (BOOL)shouldBeSaved
{
    if (!super.shouldBeSaved) {
        return NO;
    }
    if (self.groupMetaMessage == TSGroupMetaMessageDeliver || self.groupMetaMessage == TSGroupMetaMessageUnspecified) {
        return YES;
    }

    // There's no need to save this message, since it's not displayed to the user.
    //
    // Should we find a need to save this in the future, we need to exclude any non-serializable properties.
    return NO;
}

- (void)anyWillInsertWithTransaction:(SDSAnyWriteTransaction *)transaction
{
    [super anyWillInsertWithTransaction:transaction];

    _storedMessageState = self.messageState;
}

- (void)anyDidInsertWithTransaction:(SDSAnyWriteTransaction *)transaction
{
    [super anyDidInsertWithTransaction:transaction];
    [self markMessageSendLogEntryCompleteIfNeededWithTx:transaction];
}

- (void)anyWillUpdateWithTransaction:(SDSAnyWriteTransaction *)transaction
{
    [super anyWillUpdateWithTransaction:transaction];

    _storedMessageState = self.messageState;
}

- (void)anyDidUpdateWithTransaction:(SDSAnyWriteTransaction *)transaction
{
    [super anyDidUpdateWithTransaction:transaction];
    [self markMessageSendLogEntryCompleteIfNeededWithTx:transaction];
}

// This method will be called after every insert and update, so it needs
// to be cheap.
- (BOOL)shouldStartExpireTimer
{
    if (self.hasPerConversationExpirationStarted) {
        // Expiration already started.
        return YES;
    } else if (!self.hasPerConversationExpiration) {
        return NO;
    } else if (!super.shouldStartExpireTimer) {
        return NO;
    }

    // It's not clear if we should wait until _all_ recipients have reached "sent or later"
    // (which could never occur if one group member is unregistered) or only wait until
    // the first recipient has reached "sent or later" (which could cause partially delivered
    // messages to expire).  For now, we'll do the latter.
    //
    // TODO: Revisit this decision.
    return self.messageState == TSOutgoingMessageStateSent;
}

- (BOOL)isOnline
{
    return NO;
}

- (BOOL)isUrgent
{
    return YES;
}

- (OWSInteractionType)interactionType
{
    return OWSInteractionType_OutgoingMessage;
}

// MCR: Check what calls this method and if it needs to be changed.
- (NSArray<SignalServiceAddress *> *)recipientAddresses
{
    return self.recipientAddressStates.allKeys;
}

- (NSArray<SignalServiceAddress *> *)sendingRecipientAddresses
{
    NSMutableArray<SignalServiceAddress *> *result = [NSMutableArray new];
    for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) {
        TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress];
        if (recipientState.state == OWSOutgoingMessageRecipientStateSending
            || recipientState.state == OWSOutgoingMessageRecipientStatePending) {
            [result addObject:recipientAddress];
        }
    }
    return result;
}

- (NSArray<SignalServiceAddress *> *)sentRecipientAddresses
{
    NSMutableArray<SignalServiceAddress *> *result = [NSMutableArray new];
    for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) {
        TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress];
        if (recipientState.state == OWSOutgoingMessageRecipientStateSent) {
            [result addObject:recipientAddress];
        }
    }
    return result;
}

- (NSArray<SignalServiceAddress *> *)deliveredRecipientAddresses
{
    NSMutableArray<SignalServiceAddress *> *result = [NSMutableArray new];
    for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) {
        TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress];
        if (recipientState.deliveryTimestamp != nil) {
            [result addObject:recipientAddress];
        }
    }
    return result;
}

- (NSArray<SignalServiceAddress *> *)readRecipientAddresses
{
    NSMutableArray<SignalServiceAddress *> *result = [NSMutableArray new];
    for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) {
        TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress];
        if (recipientState.readTimestamp != nil) {
            [result addObject:recipientAddress];
        }
    }
    return result;
}

- (NSArray<SignalServiceAddress *> *)viewedRecipientAddresses
{
    NSMutableArray<SignalServiceAddress *> *result = [NSMutableArray new];
    for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) {
        TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress];
        if (recipientState.viewedTimestamp != nil) {
            [result addObject:recipientAddress];
        }
    }
    return result;
}

- (NSUInteger)sentRecipientsCount
{
    return [self.recipientAddressStates
                .allValues filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
                                                           TSOutgoingMessageRecipientState *recipientState,
                                                           NSDictionary<NSString *, id> *_Nullable bindings) {
        return recipientState.state == OWSOutgoingMessageRecipientStateSent;
    }]].count;
}

// MCR: Check what calls this and if it needs to be changed.
- (nullable TSOutgoingMessageRecipientState *)recipientStateForAddress:(SignalServiceAddress *)address
{
    OWSAssertDebug(address.isValid);

    TSOutgoingMessageRecipientState *_Nullable result = self.recipientAddressStates[address];
    OWSAssertDebug(result);
    return [result copy];
}

#pragma mark - Update With... Methods

- (void)updateWithSendingError:(NSError *)error transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(error);
    [self
        anyUpdateOutgoingMessageWithTransaction:transaction
                                          block:^(TSOutgoingMessage *message) {
                                              // Mark any "sending" recipients as "failed."
                                              for (TSOutgoingMessageRecipientState *recipientState in message
                                                       .recipientAddressStates.allValues) {
                                                  if (recipientState.state == OWSOutgoingMessageRecipientStateSending) {
                                                      recipientState.state = OWSOutgoingMessageRecipientStateFailed;
                                                  }
                                              }
                                              [message setMostRecentFailureText:error.userErrorDescription];
                                          }];
}

- (void)updateWithAllSendingRecipientsMarkedAsFailedWithTransaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(transaction);

    [self
        anyUpdateOutgoingMessageWithTransaction:transaction
                                          block:^(TSOutgoingMessage *message) {
                                              // Mark any "sending" recipients as "failed."
                                              for (TSOutgoingMessageRecipientState *recipientState in message
                                                       .recipientAddressStates.allValues) {
                                                  if (recipientState.state == OWSOutgoingMessageRecipientStateSending) {
                                                      recipientState.state = OWSOutgoingMessageRecipientStateFailed;
                                                  }
                                              }
                                          }];
}

- (BOOL)hasFailedRecipients
{
    for (TSOutgoingMessageRecipientState *recipientState in self.recipientAddressStates.allValues) {
        if (recipientState.state == OWSOutgoingMessageRecipientStateFailed) {
            return YES;
        }
    }
    return NO;
}

- (void)updateAllUnsentRecipientsAsSendingWithTransaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(transaction);

    [self
        anyUpdateOutgoingMessageWithTransaction:transaction
                                          block:^(TSOutgoingMessage *message) {
                                              // Mark any "failed" recipients as "sending."
                                              for (TSOutgoingMessageRecipientState *recipientState in message
                                                       .recipientAddressStates.allValues) {
                                                  if (recipientState.state == OWSOutgoingMessageRecipientStateFailed) {
                                                      recipientState.state = OWSOutgoingMessageRecipientStateSending;
                                                  }
                                              }
                                          }];
}

- (void)updateWithHasSyncedTranscript:(BOOL)hasSyncedTranscript transaction:(SDSAnyWriteTransaction *)transaction
{
    [self anyUpdateOutgoingMessageWithTransaction:transaction
                                            block:^(TSOutgoingMessage *message) {
                                                [message setHasSyncedTranscript:hasSyncedTranscript];
                                            }];
}

- (void)updateWithSentRecipient:(ServiceIdObjC *)serviceId
                    wasSentByUD:(BOOL)wasSentByUD
                    transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(serviceId);
    OWSAssertDebug(transaction);

    SignalServiceAddress *recipientAddress = [[SignalServiceAddress alloc] initWithServiceIdObjC:serviceId];
    [self updateWithSentRecipientAddress:recipientAddress wasSentByUD:wasSentByUD transaction:transaction];
}

- (void)updateWithSentRecipientAddress:(SignalServiceAddress *)recipientAddress
                           wasSentByUD:(BOOL)wasSentByUD
                           transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(recipientAddress);
    OWSAssertDebug(transaction);
    [self anyUpdateOutgoingMessageWithTransaction:transaction
                                            block:^(TSOutgoingMessage *message) {
                                                TSOutgoingMessageRecipientState *_Nullable recipientState
                                                    = message.recipientAddressStates[recipientAddress];
                                                if (!recipientState) {
                                                    OWSFailDebug(
                                                        @"Missing recipient state for recipient: %@", recipientAddress);
                                                    return;
                                                }
                                                recipientState.state = OWSOutgoingMessageRecipientStateSent;
                                                recipientState.wasSentByUD = wasSentByUD;
                                                recipientState.errorCode = nil;
                                            }];
}

- (void)updateWithSkippedRecipient:(SignalServiceAddress *)recipientAddress
                       transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(recipientAddress.isValid);
    OWSAssertDebug(transaction);

    [self anyUpdateOutgoingMessageWithTransaction:transaction
                                            block:^(TSOutgoingMessage *message) {
                                                TSOutgoingMessageRecipientState *_Nullable recipientState
                                                    = message.recipientAddressStates[recipientAddress];
                                                if (!recipientState) {
                                                    OWSFailDebug(
                                                        @"Missing recipient state for recipient: %@", recipientAddress);
                                                    return;
                                                }
                                                recipientState.state = OWSOutgoingMessageRecipientStateSkipped;
                                            }];
}

- (void)updateWithFailedRecipient:(SignalServiceAddress *)recipientAddress
                            error:(NSError *)error
                      transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(recipientAddress.isValid);
    OWSAssertDebug(transaction);

    OWSLogWarn(@"Send to recipient failed, address: %@, timestamp: %llu, error: %@ (isRetryable: %d)",
        recipientAddress,
        self.timestamp,
        error,
        error.isRetryable);

    [self
        anyUpdateOutgoingMessageWithTransaction:transaction
                                          block:^(TSOutgoingMessage *message) {
                                              TSOutgoingMessageRecipientState *_Nullable recipientState
                                                  = message.recipientAddressStates[recipientAddress];
                                              if (!recipientState) {
                                                  OWSFailDebug(
                                                      @"Missing recipient state for recipient: %@", recipientAddress);
                                                  return;
                                              }

                                              if (error.isRetryable
                                                  && recipientState.state == OWSOutgoingMessageRecipientStateSending) {
                                                  // For retryable errors, we can just set the error code and leave the
                                                  // state set as Sending
                                              } else if ([SpamChallengeRequiredError isSpamChallengeRequiredError:error]
                                                  || [SpamChallengeResolvedError isSpamChallengeResolvedError:error]) {
                                                  recipientState.state = OWSOutgoingMessageRecipientStatePending;
                                              } else {
                                                  recipientState.state = OWSOutgoingMessageRecipientStateFailed;
                                              }
                                              recipientState.errorCode = @(error.code);
                                          }];
}

- (void)updateRecipientsFromNonLocalDevice:
            (NSDictionary<SignalServiceAddress *, TSOutgoingMessageRecipientState *> *)recipientStates
                              isSentUpdate:(BOOL)isSentUpdate
                               transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(transaction);

    [self anyUpdateOutgoingMessageWithTransaction:transaction
                                            block:^(TSOutgoingMessage *message) {
                                                if (recipientStates.count > 0) {
                                                    // If we have specific recipient info from the transcript,
                                                    // build a new recipient state map.


                                                    if (isSentUpdate) {
                                                        // If this is a "sent update", make sure that:
                                                        //
                                                        // a) "Sent updates" should never remove any recipients.  We end
                                                        // up with the
                                                        //    union of the existing and new recipients.
                                                        // b) "Sent updates" should never downgrade the "recipient
                                                        // state" for any
                                                        //    recipients.  Prefer existing "recipient state"; "sent
                                                        //    updates" only add new recipients at the "sent" state.
                                                        //
                                                        // Therefore we retain all existing entries in the recipient
                                                        // state map.
                                                        NSMutableDictionary<SignalServiceAddress *,
                                                            TSOutgoingMessageRecipientState *> *recipientAddressStates
                                                            = recipientStates.mutableCopy;
                                                        [recipientAddressStates
                                                            addEntriesFromDictionary:self.recipientAddressStates];
                                                        [message setRecipientAddressStates:recipientAddressStates];
                                                    } else {
                                                        [message setRecipientAddressStates:recipientStates];
                                                    }
                                                } else {
                                                    // Otherwise assume this is a legacy message before UD was
                                                    // introduced, and mark any "sending" recipient as "sent".  Note
                                                    // that this will apply to non-legacy messages with no recipients.
                                                    for (TSOutgoingMessageRecipientState *recipientState in message
                                                             .recipientAddressStates.allValues) {
                                                        if (recipientState.state
                                                            == OWSOutgoingMessageRecipientStateSending) {
                                                            recipientState.state = OWSOutgoingMessageRecipientStateSent;
                                                        }
                                                    }
                                                }

                                                if (!isSentUpdate) {
                                                    [message setWasNotCreatedLocally:YES];
                                                }
                                            }];
}

- (void)updateWithRecipientAddressStates:
            (nullable NSDictionary<SignalServiceAddress *, TSOutgoingMessageRecipientState *> *)recipientAddressStates
                             transaction:(SDSAnyWriteTransaction *)transaction
{
    [self anyUpdateOutgoingMessageWithTransaction:transaction
                                            block:^(TSOutgoingMessage *message) {
                                                message.recipientAddressStates = [recipientAddressStates copy];
                                            }];
}

#ifdef TESTABLE_BUILD
- (void)updateWithFakeMessageState:(TSOutgoingMessageState)messageState
                       transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(transaction);
    [self anyUpdateOutgoingMessageWithTransaction:transaction
                                            block:^(TSOutgoingMessage *message) {
                                                for (TSOutgoingMessageRecipientState *recipientState in message
                                                         .recipientAddressStates.allValues) {
                                                    switch (messageState) {
                                                        case TSOutgoingMessageStateSending:
                                                            recipientState.state
                                                                = OWSOutgoingMessageRecipientStateSending;
                                                            break;
                                                        case TSOutgoingMessageStateFailed:
                                                            recipientState.state
                                                                = OWSOutgoingMessageRecipientStateFailed;
                                                            break;
                                                        case TSOutgoingMessageStateSent:
                                                            recipientState.state = OWSOutgoingMessageRecipientStateSent;
                                                            break;
                                                        case TSOutgoingMessageStatePending:
                                                            recipientState.state
                                                                = OWSOutgoingMessageRecipientStatePending;
                                                            break;
                                                        default:
                                                            OWSFailDebug(@"unexpected message state.");
                                                            break;
                                                    }
                                                }
                                            }];
}
#endif

#pragma mark -

- (nullable SSKProtoDataMessageBuilder *)dataMessageBuilderWithThread:(TSThread *)thread
                                                          transaction:(SDSAnyReadTransaction *)transaction
{
    OWSAssertDebug(thread);

    SSKProtoDataMessageBuilder *builder = [SSKProtoDataMessage builder];
    [builder setTimestamp:self.timestamp];

    NSUInteger requiredProtocolVersion = SSKProtoDataMessageProtocolVersionInitial;

    if (self.isViewOnceMessage) {
        [builder setIsViewOnce:YES];
        requiredProtocolVersion = SSKProtoDataMessageProtocolVersionViewOnceVideo;
    }

    NSString *body = self.body;
    NSString *trimmedBody = [body trimToUtf8ByteCount:(NSInteger)kOversizeTextMessageSizeThreshold];
    OWSAssertDebug(body.length == trimmedBody.length);
    [builder setBody:trimmedBody];

    NSArray<SSKProtoBodyRange *> *bodyRanges =
        [self.bodyRanges toProtoBodyRangesWithBodyLength:(NSInteger)self.body.length];
    if (bodyRanges.count > 0) {
        [builder setBodyRanges:bodyRanges];

        if (requiredProtocolVersion < SSKProtoDataMessageProtocolVersionMentions) {
            requiredProtocolVersion = SSKProtoDataMessageProtocolVersionMentions;
        }
    }

    // Story Context
    if (self.storyTimestamp && self.storyAuthorUuidString) {
        if (self.storyReactionEmoji) {
            SSKProtoDataMessageReactionBuilder *reactionBuilder =
                [SSKProtoDataMessageReaction builderWithEmoji:self.storyReactionEmoji
                                                    timestamp:self.storyTimestamp.unsignedLongLongValue];
            // ACI TODO: Use `serviceIdString` to populate this value.
            [reactionBuilder setTargetAuthorAci:self.storyAuthorUuidString];

            NSError *error;
            SSKProtoDataMessageReaction *_Nullable reaction = [reactionBuilder buildAndReturnError:&error];
            if (error || !reaction) {
                OWSFailDebug(@"Could not build story reaction protobuf: %@.", error);
            } else {
                [builder setReaction:reaction];

                if (requiredProtocolVersion < SSKProtoDataMessageProtocolVersionReactions) {
                    requiredProtocolVersion = SSKProtoDataMessageProtocolVersionReactions;
                }
            }
        }

        SSKProtoDataMessageStoryContextBuilder *storyContextBuilder = [SSKProtoDataMessageStoryContext builder];
        // ACI TODO: Use `serviceIdString` to populate this value.
        [storyContextBuilder setAuthorAci:self.storyAuthorUuidString];
        [storyContextBuilder setSentTimestamp:self.storyTimestamp.unsignedLongLongValue];

        [builder setStoryContext:[storyContextBuilder buildInfallibly]];
    }

    [builder setExpireTimer:self.expiresInSeconds];

    // Group Messages
    if ([thread isKindOfClass:[TSGroupThread class]]) {
        TSGroupThread *groupThread = (TSGroupThread *)thread;
        OutgoingGroupProtoResult result;
        switch (groupThread.groupModel.groupsVersion) {
            case GroupsVersionV1:
                OWSLogError(@"[GV1] Cannot build data message for V1 group!");
                result = OutgoingGroupProtoResult_Error;
                break;
            case GroupsVersionV2:
                result = [self addGroupsV2ToDataMessageBuilder:builder groupThread:groupThread tx:transaction];
                break;
        }
        switch (result) {
            case OutgoingGroupProtoResult_Error:
                return nil;
            case OutgoingGroupProtoResult_AddedWithoutGroupAvatar:
                break;
        }
    }

    // Message Attachments
    NSError *bodyError;
    NSArray<SSKProtoAttachmentPointer *> *attachments = [self buildProtosForBodyAttachmentsWithTx:transaction
                                                                                            error:&bodyError];
    if (bodyError) {
        OWSFailDebug(@"Could not build body attachments");
    } else {
        [builder setAttachments:attachments];
    }

    // Quoted Reply
    if (self.quotedMessage) {
        NSError *error;
        SSKProtoDataMessageQuote *_Nullable quoteProto = [self buildQuoteProtoWithQuote:self.quotedMessage
                                                                                     tx:transaction
                                                                                  error:&error];
        if (error || !quoteProto) {
            OWSFailDebug(@"Could not build quote protobuf: %@.", error);
        } else {
            [builder setQuote:quoteProto];

            if (quoteProto.bodyRanges.count > 0) {
                if (requiredProtocolVersion < SSKProtoDataMessageProtocolVersionMentions) {
                    requiredProtocolVersion = SSKProtoDataMessageProtocolVersionMentions;
                }
            }
        }
    }

    // Contact Share
    if (self.contactShare) {
        NSError *error;
        SSKProtoDataMessageContact *_Nullable contactProto = [self buildContactShareProto:self.contactShare
                                                                                       tx:transaction
                                                                                    error:&error];
        if (error || !contactProto) {
            OWSFailDebug(@"Could not build contact share protobuf: %@.", error);
        } else {
            [builder addContact:contactProto];
        }
    }

    // Link Preview
    if (self.linkPreview) {
        NSError *error;
        SSKProtoPreview *_Nullable previewProto = [self buildLinkPreviewProtoWithLinkPreview:self.linkPreview
                                                                                          tx:transaction
                                                                                       error:&error];
        if (error || !previewProto) {
            OWSFailDebug(@"Could not build link preview protobuf: %@.", error);
        } else {
            [builder addPreview:previewProto];
        }
    }

    // Sticker
    if (self.messageSticker) {
        NSError *error;
        SSKProtoDataMessageSticker *_Nullable stickerProto = [self buildStickerProtoWithSticker:self.messageSticker
                                                                                             tx:transaction
                                                                                          error:&error];
        if (error || !stickerProto) {
            OWSFailDebug(@"Could not build sticker protobuf: %@.", error);
        } else {
            [builder setSticker:stickerProto];
        }
    }

    // Gift badge
    if (self.giftBadge) {
        SSKProtoDataMessageGiftBadgeBuilder *giftBadgeBuilder = [SSKProtoDataMessageGiftBadge builder];
        [giftBadgeBuilder setReceiptCredentialPresentation:self.giftBadge.redemptionCredential];
        [builder setGiftBadge:[giftBadgeBuilder buildInfallibly]];
    }

    [builder setRequiredProtocolVersion:(uint32_t)requiredProtocolVersion];
    return builder;
}


// recipientId is nil when building "sent" sync messages for messages sent to groups.
- (nullable SSKProtoDataMessage *)buildDataMessage:(TSThread *)thread transaction:(SDSAnyReadTransaction *)transaction
{
    OWSAssertDebug(thread);
    OWSAssertDebug([thread.uniqueId isEqualToString:self.uniqueThreadId]);
    SSKProtoDataMessageBuilder *_Nullable builder = [self dataMessageBuilderWithThread:thread transaction:transaction];
    if (!builder) {
        OWSFailDebug(@"could not build protobuf.");
        return nil;
    }

    [ProtoUtils addLocalProfileKeyIfNecessary:thread dataMessageBuilder:builder transaction:transaction];

    NSError *error;
    SSKProtoDataMessage *_Nullable dataProto = [builder buildAndReturnError:&error];
    if (error || !dataProto) {
        OWSFailDebug(@"could not build protobuf: %@", error);
        return nil;
    }
    return dataProto;
}

- (nullable SSKProtoContentBuilder *)contentBuilderWithThread:(TSThread *)thread
                                                  transaction:(SDSAnyReadTransaction *)transaction
{
    SSKProtoDataMessage *_Nullable dataMessage = [self buildDataMessage:thread transaction:transaction];
    if (!dataMessage) {
        return nil;
    }

    SSKProtoContentBuilder *contentBuilder = [SSKProtoContent builder];
    [contentBuilder setDataMessage:dataMessage];
    return contentBuilder;
}

- (nullable NSData *)buildPlainTextData:(TSThread *)thread transaction:(SDSAnyWriteTransaction *)transaction
{
    SSKProtoContentBuilder *_Nullable contentBuilder = [self contentBuilderWithThread:thread transaction:transaction];
    if (!contentBuilder) {
        OWSFailDebug(@"could not build protobuf.");
        return nil;
    }

    [contentBuilder setPniSignatureMessage:[self buildPniSignatureMessageIfNeededWithTransaction:transaction]];

    NSError *error;
    NSData *_Nullable contentData = [contentBuilder buildSerializedDataAndReturnError:&error];
    if (error || !contentData) {
        OWSFailDebug(@"could not serialize protobuf: %@", error);
        return nil;
    }
    return contentData;
}

- (BOOL)shouldSyncTranscript
{
    return YES;
}

- (nullable OWSOutgoingSyncMessage *)buildTranscriptSyncMessageWithLocalThread:(TSThread *)localThread
                                                                   transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(self.shouldSyncTranscript);

    TSThread *messageThread = [self threadWithTx:transaction];
    if (messageThread == nil) {
        return nil;
    }

    return [[OWSOutgoingSentMessageTranscript alloc] initWithLocalThread:localThread
                                                           messageThread:messageThread
                                                         outgoingMessage:self
                                                       isRecipientUpdate:self.hasSyncedTranscript
                                                             transaction:transaction];
}

- (NSString *)statusDescription
{
    NSMutableString *result = [NSMutableString new];
    [result appendFormat:@"[status: %@\n", NSStringForOutgoingMessageState(self.messageState)];
    for (SignalServiceAddress *address in self.recipientAddressStates) {
        TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[address];
        [result appendFormat:@", %@: %@\n", address, NSStringForOutgoingMessageRecipientState(recipientState.state)];
    }
    [result appendString:@"]"];
    return [result copy];
}

@end

NS_ASSUME_NONNULL_END
