//
//  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//

#import "OWSProfileManager.h"
#import "Environment.h"
#import "OWSUserProfile.h"
#import <PromiseKit/AnyPromise.h>
#import <SessionCoreKit/Cryptography.h>
#import <SessionCoreKit/NSData+OWS.h>
#import <SessionCoreKit/NSDate+OWS.h>
#import <SignalMessaging/SignalMessaging-Swift.h>
#import <SessionServiceKit/AppContext.h>
#import <SessionServiceKit/AppReadiness.h>
#import <SessionServiceKit/MIMETypeUtil.h>
#import <SessionServiceKit/NSData+Image.h>
#import <SessionServiceKit/NSNotificationCenter+OWS.h>
#import <SessionServiceKit/NSString+SSK.h>
#import <SessionServiceKit/OWSBlockingManager.h>
#import <SessionServiceKit/OWSFileSystem.h>
#import <SessionServiceKit/OWSMessageSender.h>
#import <SessionServiceKit/OWSPrimaryStorage+Loki.h>
#import <SessionServiceKit/OWSProfileKeyMessage.h>
#import <SessionServiceKit/OWSRequestBuilder.h>
#import <SessionServiceKit/OWSSignalService.h>
#import <SessionServiceKit/SSKEnvironment.h>
#import <SessionServiceKit/TSAccountManager.h>
#import <SessionServiceKit/TSGroupThread.h>
#import <SessionServiceKit/TSNetworkManager.h>
#import <SessionServiceKit/TSThread.h>
#import <SessionServiceKit/TSYapDatabaseObject.h>
#import <SessionServiceKit/UIImage+OWS.h>
#import <SessionServiceKit/YapDatabaseConnection+OWS.h>
#import <SessionServiceKit/SessionServiceKit-Swift.h>

NS_ASSUME_NONNULL_BEGIN

NSString *const kNSNotificationName_ProfileWhitelistDidChange = @"kNSNotificationName_ProfileWhitelistDidChange";

NSString *const kOWSProfileManager_UserWhitelistCollection = @"kOWSProfileManager_UserWhitelistCollection";
NSString *const kOWSProfileManager_GroupWhitelistCollection = @"kOWSProfileManager_GroupWhitelistCollection";

NSString *const kNSNotificationName_ProfileKeyDidChange = @"kNSNotificationName_ProfileKeyDidChange";

// The max bytes for a user's profile name, encoded in UTF8.
// Before encrypting and submitting we NULL pad the name data to this length.
const NSUInteger kOWSProfileManager_NameDataLength = 26;
const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;

typedef void (^ProfileManagerFailureBlock)(NSError *error);

@interface OWSProfileManager ()

@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;

// This property can be accessed on any thread, while synchronized on self.
@property (atomic, readonly) OWSUserProfile *localUserProfile;

// This property can be accessed on any thread, while synchronized on self.
@property (atomic, readonly) NSCache<NSString *, UIImage *> *profileAvatarImageCache;

// This property can be accessed on any thread, while synchronized on self.
@property (atomic, readonly) NSMutableSet<NSString *> *currentAvatarDownloads;

@end

#pragma mark -

// Access to most state should happen while synchronized on the profile manager.
// Writes should happen off the main thread, wherever possible.
@implementation OWSProfileManager

@synthesize localUserProfile = _localUserProfile;

+ (instancetype)sharedManager
{
    return SSKEnvironment.shared.profileManager;
}

- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage
{
    self = [super init];

    if (!self) {
        return self;
    }

    OWSAssertIsOnMainThread();
    OWSAssertDebug(primaryStorage);

    _dbConnection = primaryStorage.newDatabaseConnection;

    _profileAvatarImageCache = [NSCache new];
    _currentAvatarDownloads = [NSMutableSet new];

    OWSSingletonAssert();

    [AppReadiness runNowOrWhenAppDidBecomeReady:^{
        [self rotateLocalProfileKeyIfNecessary];
    }];

    [self observeNotifications];

    return self;
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)observeNotifications
{
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(applicationDidBecomeActive:)
                                                 name:OWSApplicationDidBecomeActiveNotification
                                               object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(blockListDidChange:)
                                                 name:kNSNotificationName_BlockListDidChange
                                               object:nil];
}

#pragma mark - Dependencies

- (TSAccountManager *)tsAccountManager
{
    return TSAccountManager.sharedInstance;
}

- (AFHTTPSessionManager *)avatarHTTPManager
{
    return [OWSSignalService sharedInstance].CDNSessionManager;
}

- (OWSIdentityManager *)identityManager
{
    return SSKEnvironment.shared.identityManager;
}

- (SSKMessageSenderJobQueue *)messageSenderJobQueue
{
    return SSKEnvironment.shared.messageSenderJobQueue;
}

- (TSNetworkManager *)networkManager
{
    return SSKEnvironment.shared.networkManager;
}

- (OWSBlockingManager *)blockingManager
{
    return SSKEnvironment.shared.blockingManager;
}

- (id<OWSSyncManagerProtocol>)syncManager
{
    return SSKEnvironment.shared.syncManager;
}

- (id<OWSUDManager>)udManager
{
    OWSAssertDebug(SSKEnvironment.shared.udManager);

    return SSKEnvironment.shared.udManager;
}

#pragma mark - User Profile Accessor

- (void)ensureLocalProfileCached
{
    // Since localUserProfile can create a transaction, we want to make sure it's not called for the first
    // time unexpectedly (e.g. in a nested transaction.)
    __unused OWSUserProfile *profile = [self localUserProfile];
}

#pragma mark - Local Profile

- (OWSUserProfile *)localUserProfile
{
    if (_localUserProfile) { return _localUserProfile; }

    __block OWSUserProfile *userProfile;
    [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        userProfile = [self getLocalUserProfileWithTransaction:transaction];
    } error:nil];
    return userProfile;
}

- (OWSUserProfile *)getLocalUserProfileWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
    @synchronized(self)
    {
        if (!_localUserProfile) {
            _localUserProfile = [OWSUserProfile getOrBuildUserProfileForRecipientId:kLocalProfileUniqueId transaction:transaction];
        }
    }

    OWSAssertDebug(_localUserProfile.profileKey);

    return _localUserProfile;
}

- (BOOL)localProfileExists
{
    return [OWSUserProfile localUserProfileExists:self.dbConnection];
}

- (OWSAES256Key *)localProfileKey
{
    OWSAssertDebug(self.localUserProfile.profileKey.keyData.length == kAES256_KeyByteLength);

    return self.localUserProfile.profileKey;
}

- (BOOL)hasLocalProfile
{
    return (self.localProfileName.length > 0 || self.localProfileAvatarImage != nil);
}

- (nullable NSString *)localProfileName
{
    return self.localUserProfile.profileName;
}

- (nullable UIImage *)localProfileAvatarImage
{
    return [self loadProfileAvatarWithFilename:self.localUserProfile.avatarFileName];
}

- (nullable NSData *)localProfileAvatarData
{
    NSString *_Nullable filename = self.localUserProfile.avatarFileName;
    if (filename.length < 1) {
        return nil;
    }
    return [self loadProfileDataWithFilename:filename];
}

- (void)updateLocalProfileName:(nullable NSString *)profileName
                   avatarImage:(nullable UIImage *)avatarImage
                       success:(void (^)(void))successBlockParameter
                       failure:(void (^)(NSError *))failureBlockParameter
                  requiresSync:(BOOL)requiresSync
{
    OWSAssertDebug(successBlockParameter);
    OWSAssertDebug(failureBlockParameter);

    // Ensure that the success and failure blocks are called on the main thread.
    void (^failureBlock)(NSError *) = ^(NSError *error) {
        OWSLogError(@"Updating service with profile failed.");
        
        /*
        // We use a "self-only" contact sync to indicate to desktop
        // that we've changed our profile and that it should do a
        // profile fetch for "self".
        //
        // NOTE: We also inform the desktop in the failure case,
        //       since that _may have_ affected service state.
        if (requiresSync) {
            [[self.syncManager syncLocalContact] retainUntilComplete];
        }
         */

        if (requiresSync) {
            [LKSyncMessagesProtocol syncProfile];
        }

        dispatch_async(dispatch_get_main_queue(), ^{
            failureBlockParameter(error);
        });
    };
    void (^successBlock)(void) = ^{
        OWSLogInfo(@"Successfully updated service with profile.");
        
        /*
        // We use a "self-only" contact sync to indicate to desktop
        // that we've changed our profile and that it should do a
        // profile fetch for "self".
        if (requiresSync) {
            [[self.syncManager syncLocalContact] retainUntilComplete];
        }
         */

        if (requiresSync) {
            [LKSyncMessagesProtocol syncProfile];
        }

        dispatch_async(dispatch_get_main_queue(), ^{
            successBlockParameter();
        });
    };

    // The final steps are to:
    //
    // * Try to update the service.
    // * Update client state on success.
    void (^tryToUpdateService)(NSString *_Nullable, NSString *_Nullable) = ^(
        NSString *_Nullable avatarUrlPath, NSString *_Nullable avatarFileName) {
        [self updateServiceWithProfileName:profileName
                                 avatarUrl:avatarUrlPath
            success:^{
                OWSUserProfile *userProfile = self.localUserProfile;
                OWSAssertDebug(userProfile);

                [userProfile updateWithProfileName:profileName
                                     avatarUrlPath:avatarUrlPath
                                    avatarFileName:avatarFileName
                                      dbConnection:self.dbConnection
                                        completion:^{
                                            if (avatarFileName) {
                                                [self updateProfileAvatarCache:avatarImage filename:avatarFileName];
                                            }

                                            successBlock();
                                        }];
            }
            failure:^(NSError *error) {
                failureBlock(error);
            }];
    };

    OWSUserProfile *userProfile = self.localUserProfile;
    OWSAssertDebug(userProfile);
    
    if (avatarImage) {
        // If we have a new avatar image, we must first:
        //
        // * Encode it to JPEG.
        // * Write it to disk.
        // * Encrypt it
        // * Upload it to asset service
        // * Send asset service info to Signal Service
        OWSLogVerbose(@"Updating local profile on service with new avatar.");
        [self writeAvatarToDisk:avatarImage
            success:^(NSData *data, NSString *fileName) {
                [self uploadAvatarToService:data
                    success:^(NSString *_Nullable avatarUrlPath) {
                        tryToUpdateService(avatarUrlPath, fileName);
                    }
                    failure:^(NSError *error) {
                        failureBlock(error);
                    }];
            }
            failure:^(NSError *error) {
                failureBlock(error);
            }];
    } else if (userProfile.avatarUrlPath) {
        OWSLogVerbose(@"Updating local profile on service with cleared avatar.");
        [self uploadAvatarToService:nil
            success:^(NSString *_Nullable avatarUrlPath) {
                tryToUpdateService(nil, nil);
            }
            failure:^(NSError *error) {
                failureBlock(error);
            }];
    } else {
        OWSLogVerbose(@"Updating local profile on service with no avatar.");
        tryToUpdateService(nil, nil);
    }
}

- (nullable NSString *)profilePictureURL
{
    return self.localUserProfile.avatarUrlPath;
}

- (void)writeAvatarToDisk:(UIImage *)avatar
                  success:(void (^)(NSData *data, NSString *fileName))successBlock
                  failure:(ProfileManagerFailureBlock)failureBlock {
    OWSAssertDebug(avatar);
    OWSAssertDebug(successBlock);
    OWSAssertDebug(failureBlock);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if (avatar) {
            NSData *data = [self processedImageDataForRawAvatar:avatar];
            OWSAssertDebug(data);
            if (data) {
                NSString *fileName = [self generateAvatarFilename];
                NSString *filePath = [OWSUserProfile profileAvatarFilepathWithFilename:fileName];
                BOOL success = [data writeToFile:filePath atomically:YES];
                OWSAssertDebug(success);
                if (success) {
                    return successBlock(data, fileName);
                }
            }
        }
        failureBlock(OWSErrorWithCodeDescription(OWSErrorCodeAvatarWriteFailed, @"Avatar write failed."));
    });
}

- (NSData *)processedImageDataForRawAvatar:(UIImage *)image
{
    NSUInteger kMaxAvatarBytes = 5 * 1000 * 1000;

    if (image.size.width != kOWSProfileManager_MaxAvatarDiameter
        || image.size.height != kOWSProfileManager_MaxAvatarDiameter) {
        // To help ensure the user is being shown the same cropping of their avatar as
        // everyone else will see, we want to be sure that the image was resized before this point.
        OWSFailDebug(@"Avatar image should have been resized before trying to upload");
        image = [image resizedImageToFillPixelSize:CGSizeMake(kOWSProfileManager_MaxAvatarDiameter,
                                                       kOWSProfileManager_MaxAvatarDiameter)];
    }

    NSData *_Nullable data = UIImageJPEGRepresentation(image, 0.95f);
    if (data.length > kMaxAvatarBytes) {
        // Our avatar dimensions are so small that it's incredibly unlikely we wouldn't be able to fit our profile
        // photo. e.g. generating pure noise at our resolution compresses to ~200k.
        OWSFailDebug(@"Suprised to find profile avatar was too large. Was it scaled properly? image: %@", image);
    }

    return data;
}

// If avatarData is nil, we are clearing the avatar.
- (void)uploadAvatarToService:(NSData *_Nullable)avatarData
                      success:(void (^)(NSString *_Nullable avatarUrlPath))successBlock
                      failure:(ProfileManagerFailureBlock)failureBlock {
    OWSAssertDebug(successBlock);
    OWSAssertDebug(failureBlock);
    OWSAssertDebug(avatarData == nil || avatarData.length > 0);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // We always want to encrypt a profile with a new profile key
        // This ensures that other users know that our profile picture was updated
        OWSAES256Key *newProfileKey = [OWSAES256Key generateRandomKey];
        
        if (avatarData) {
            NSData *encryptedAvatarData = [self encryptProfileData:avatarData profileKey:newProfileKey];
            OWSAssertDebug(encryptedAvatarData.length > 0);
            
            [[LKFileServerAPI uploadProfilePicture:encryptedAvatarData]
            .thenOn(dispatch_get_main_queue(), ^(NSString *downloadURL) {
                [self.localUserProfile updateWithProfileKey:newProfileKey dbConnection:self.dbConnection completion:^{
                   successBlock(downloadURL);
                }];
            })
            .catchOn(dispatch_get_main_queue(), ^(id result) {
                // There appears to be a bug in PromiseKit that sometimes causes catchOn
                // to be invoked with the fulfilled promise's value as the error. The below
                // is a quick and dirty workaround.
                if ([result isKindOfClass:NSString.class]) {
                    [self.localUserProfile updateWithProfileKey:newProfileKey dbConnection:self.dbConnection completion:^{
                        successBlock(result);
                    }];
                } else {
                    failureBlock(result);
                }
            }) retainUntilComplete];
        } else {
            // Update our profile key and set the url to nil if avatar data is nil
            [self.localUserProfile updateWithProfileKey:newProfileKey dbConnection:self.dbConnection completion:^{
                successBlock(nil);
            }];
        }
    });
}

- (void)updateServiceWithProfileName:(nullable NSString *)localProfileName
                           avatarUrl:(nullable NSString *)avatarURL
                             success:(void (^)(void))successBlock
                             failure:(ProfileManagerFailureBlock)failureBlock {
    OWSAssertDebug(successBlock);
    OWSAssertDebug(failureBlock);

    __block NSDictionary *publicChats;
    [SSKEnvironment.shared.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
        publicChats = [LKDatabaseUtilities getAllPublicChats:transaction];
    }];
    
    NSSet *servers = [NSSet setWithArray:[publicChats.allValues map:^NSString *(LKPublicChat *publicChat) { return publicChat.server; }]];

    for (NSString *server in servers) {
        [[LKPublicChatAPI setDisplayName:localProfileName on:server] retainUntilComplete];
        [[LKPublicChatAPI setProfilePictureURL:avatarURL usingProfileKey:self.localProfileKey.keyData on:server] retainUntilComplete];
    }
    
    successBlock();
}

- (void)updateServiceWithProfileName:(nullable NSString *)localProfileName avatarURL:(nullable NSString *)avatarURL {
    [self updateServiceWithProfileName:localProfileName avatarUrl:avatarURL success:^{} failure:^(NSError * _Nonnull error) {}];
}

- (void)fetchLocalUsersProfile
{
    OWSAssertIsOnMainThread();

    NSString *_Nullable localNumber = self.tsAccountManager.localNumber;
    if (!localNumber) {
        return;
    }
    [self fetchProfileForRecipientId:localNumber];
}

- (void)fetchProfileForRecipientId:(NSString *)recipientId
{
    OWSAssertIsOnMainThread();

    [ProfileFetcherJob runWithRecipientId:recipientId ignoreThrottling:YES];
}

#pragma mark - Profile Key Rotation

- (nullable NSString *)groupKeyForGroupId:(NSData *)groupId {
    NSString *groupIdKey = [groupId hexadecimalString];
    return groupIdKey;
}

- (nullable NSData *)groupIdForGroupKey:(NSString *)groupKey {
    NSMutableData *groupId = [NSMutableData new];

    if (groupKey.length % 2 != 0) {
        OWSFailDebug(@"Group key has unexpected length: %@ (%lu)", groupKey, (unsigned long)groupKey.length);
        return nil;
    }
    for (NSUInteger i = 0; i + 2 <= groupKey.length; i += 2) {
        NSString *_Nullable byteString = [groupKey substringWithRange:NSMakeRange(i, 2)];
        if (!byteString) {
            OWSFailDebug(@"Couldn't slice group key.");
            return nil;
        }
        unsigned byteValue;
        if (![[NSScanner scannerWithString:byteString] scanHexInt:&byteValue]) {
            OWSFailDebug(@"Couldn't parse hex byte: %@.", byteString);
            return nil;
        }
        if (byteValue > 0xff) {
            OWSFailDebug(@"Invalid hex byte: %@ (%d).", byteString, byteValue);
            return nil;
        }
        uint8_t byte = (uint8_t)(0xff & byteValue);
        [groupId appendBytes:&byte length:1];
    }
    return [groupId copy];
}

- (void)rotateLocalProfileKeyIfNecessary {
    [self
        rotateLocalProfileKeyIfNecessaryWithSuccess:^{
        }
                                            failure:^(NSError *error) {
                                            }];
}

- (void)rotateLocalProfileKeyIfNecessaryWithSuccess:(dispatch_block_t)success
                                            failure:(ProfileManagerFailureBlock)failure {
    OWSAssertDebug(AppReadiness.isAppReady);

    if (!self.tsAccountManager.isRegistered) {
        success();
        return;
    }

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSMutableSet<NSString *> *whitelistedRecipientIds = [NSMutableSet new];
        NSMutableSet<NSData *> *whitelistedGroupIds = [NSMutableSet new];
        [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
            [whitelistedRecipientIds
                addObjectsFromArray:[transaction allKeysInCollection:kOWSProfileManager_UserWhitelistCollection]];

            NSArray<NSString *> *whitelistedGroupKeys =
                [transaction allKeysInCollection:kOWSProfileManager_GroupWhitelistCollection];
            for (NSString *groupKey in whitelistedGroupKeys) {
                NSData *_Nullable groupId = [self groupIdForGroupKey:groupKey];
                if (!groupId) {
                    OWSFailDebug(@"Couldn't parse group key: %@.", groupKey);
                    continue;
                }

                [whitelistedGroupIds addObject:groupId];

                // Note we don't add `group.recipientIds` to the `whitelistedRecipientIds`.
                //
                // Whenever we message a contact, be it in a 1:1 thread or in a group thread,
                // we add them to the contact whitelist, so there's no reason to redundnatly
                // add them here.
                //
                // Furthermore, doing so would cause the following problem:
                // - Alice is in group Book Club
                // - Add Book Club to your profile white list
                // - Message Book Club, which also adds Alice to your profile whitelist.
                // - Block Alice, but not Book Club
                //
                // Now, at this point we'd want to rotate our profile key once, since Alice has
                // it via BookClub.
                //
                // However, after we did. The next time we check if we should rotate our profile
                // key, adding all `group.recipientIds` to `whitelistedRecipientIds` here, would
                // include Alice, and we'd rotate our profile key every time this method is called.
            }
        }];

        NSString *_Nullable localNumber = [TSAccountManager localNumber];
        if (localNumber) {
            [whitelistedRecipientIds removeObject:localNumber];
        } else {
            OWSFailDebug(@"Missing localNumber");
        }

        NSSet<NSString *> *blockedRecipientIds = [NSSet setWithArray:self.blockingManager.blockedPhoneNumbers];
        NSSet<NSData *> *blockedGroupIds = [NSSet setWithArray:self.blockingManager.blockedGroupIds];

        // Find the users and groups which are both a) blocked b) may have our current profile key.
        NSMutableSet<NSString *> *intersectingRecipientIds = [blockedRecipientIds mutableCopy];
        [intersectingRecipientIds intersectSet:whitelistedRecipientIds];
        NSMutableSet<NSData *> *intersectingGroupIds = [blockedGroupIds mutableCopy];
        [intersectingGroupIds intersectSet:whitelistedGroupIds];

        BOOL isProfileKeySharedWithBlocked = (intersectingRecipientIds.count > 0 || intersectingGroupIds.count > 0);
        if (!isProfileKeySharedWithBlocked) {
            // No need to rotate the profile key.
            return success();
        }
        [self rotateProfileKeyWithIntersectingRecipientIds:intersectingRecipientIds
                                      intersectingGroupIds:intersectingGroupIds
                                                   success:success
                                                   failure:failure];
    });
}

- (void)rotateProfileKeyWithIntersectingRecipientIds:(NSSet<NSString *> *)intersectingRecipientIds
                                intersectingGroupIds:(NSSet<NSData *> *)intersectingGroupIds
                                             success:(dispatch_block_t)success
                                             failure:(ProfileManagerFailureBlock)failure {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // Rotate the profile key
        OWSLogInfo(@"Rotating the profile key.");

        // Make copies of the current local profile state.
        OWSUserProfile *localUserProfile = self.localUserProfile;
        NSString *_Nullable oldProfileName = localUserProfile.profileName;
        NSString *_Nullable oldAvatarFileName = localUserProfile.avatarFileName;
        NSData *_Nullable oldAvatarData = [self profileAvatarDataForRecipientId:self.tsAccountManager.localNumber];

        // Rotate the stored profile key.
        AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
            [self.localUserProfile updateWithProfileKey:[OWSAES256Key generateRandomKey]
                                           dbConnection:self.dbConnection
                                             completion:^{
                                                 // The value doesn't matter, we just need any non-NSError value.
                                                 resolve(@(1));
                                             }];
        }];

        // Try to re-upload our profile name, if any.
        //
        // This may fail.
        promise = promise.then(^(id value) {
            if (oldProfileName.length < 1) {
                return [AnyPromise promiseWithValue:@(1)];
            }
            return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
                [self updateServiceWithProfileName:oldProfileName
                 avatarUrl:localUserProfile.avatarUrlPath
                    success:^{
                        OWSLogInfo(@"Update to profile name succeeded.");

                        // The value doesn't matter, we just need any non-NSError value.
                        resolve(@(1));
                    }
                    failure:^(NSError *error) {
                        resolve(error);
                    }];
            }];
        });

        // Try to re-upload our profile avatar, if any.
        //
        // This may fail.
        promise = promise.then(^(id value) {
            if (oldAvatarData.length < 1 || oldAvatarFileName.length < 1) {
                return [AnyPromise promiseWithValue:@(1)];
            }
            return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
                [self uploadAvatarToService:oldAvatarData
                    success:^(NSString *_Nullable avatarUrlPath) {
                        OWSLogInfo(@"Update to profile avatar after profile key rotation succeeded.");
                        // The profile manager deletes the underlying file when updating a profile URL
                        // So we need to copy the underlying file to a new location.
                        NSString *oldPath = [OWSUserProfile profileAvatarFilepathWithFilename:oldAvatarFileName];
                        NSString *newAvatarFilename = [self generateAvatarFilename];
                        NSString *newPath = [OWSUserProfile profileAvatarFilepathWithFilename:newAvatarFilename];
                        NSError *error;
                        [NSFileManager.defaultManager copyItemAtPath:oldPath toPath:newPath error:&error];
                        OWSAssertDebug(!error);

                        [self.localUserProfile updateWithAvatarUrlPath:avatarUrlPath
                                                        avatarFileName:newAvatarFilename
                                                          dbConnection:self.dbConnection
                                                            completion:^{
                                                                // The value doesn't matter, we just need any
                                                                // non-NSError value.
                                                                resolve(@(1));
                                                            }];
                    }
                    failure:^(NSError *error) {
                        OWSLogInfo(@"Update to profile avatar after profile key rotation failed.");
                        resolve(error);
                    }];
            }];
        });

        // Try to re-upload our profile avatar, if any.
        //
        // This may fail.
        promise = promise.then(^(id value) {
            // Remove blocked users and groups from profile whitelist.
            //
            // This will always succeed.
            [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
                [transaction removeObjectsForKeys:intersectingRecipientIds.allObjects
                                     inCollection:kOWSProfileManager_UserWhitelistCollection];
                for (NSData *groupId in intersectingGroupIds) {
                    NSString *groupIdKey = [self groupKeyForGroupId:groupId];
                    [transaction removeObjectForKey:groupIdKey
                                       inCollection:kOWSProfileManager_GroupWhitelistCollection];
                }
            } error:nil];
            return @(1);
        });

        // Update account attributes.
        //
        // This may fail.
        promise = promise.then(^(id value) {
            return [self.tsAccountManager updateAccountAttributes];
        });

        // Fetch local profile.
        promise = promise.then(^(id value) {
            [self fetchLocalUsersProfile];
            
            return @(1);
        });

        // Sync local profile key.
        promise = promise.then(^(id value) {
            return [self.syncManager syncLocalContact];
        });

        promise = promise.then(^(id value) {
            [[NSNotificationCenter defaultCenter] postNotificationNameAsync:kNSNotificationName_ProfileKeyDidChange
                                                                     object:nil
                                                                   userInfo:nil];

            success();
        });
        promise = promise.catch(^(NSError *error) {
            if ([error isKindOfClass:[NSError class]]) {
                failure(error);
            } else {
                failure(OWSErrorMakeAssertionError(@"Profile key rotation failure missing error."));
            }
        });
        [promise retainUntilComplete];
    });
}

#pragma mark - Profile Whitelist

- (void)clearProfileWhitelist
{
    OWSLogWarn(@"Clearing the profile whitelist.");

    [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        [transaction removeAllObjectsInCollection:kOWSProfileManager_UserWhitelistCollection];
        [transaction removeAllObjectsInCollection:kOWSProfileManager_GroupWhitelistCollection];
        OWSAssertDebug(0 == [transaction numberOfKeysInCollection:kOWSProfileManager_UserWhitelistCollection]);
        OWSAssertDebug(0 == [transaction numberOfKeysInCollection:kOWSProfileManager_GroupWhitelistCollection]);
    }];
}

- (void)logProfileWhitelist
{
    [self.dbConnection asyncReadWithBlock:^(YapDatabaseReadTransaction *transaction) {
        OWSLogError(@"kOWSProfileManager_UserWhitelistCollection: %lu",
            (unsigned long)[transaction numberOfKeysInCollection:kOWSProfileManager_UserWhitelistCollection]);
        [transaction enumerateKeysInCollection:kOWSProfileManager_UserWhitelistCollection
                                    usingBlock:^(NSString *_Nonnull key, BOOL *_Nonnull stop) {
                                        OWSLogError(@"\t profile whitelist user: %@", key);
                                    }];
        OWSLogError(@"kOWSProfileManager_GroupWhitelistCollection: %lu",
            (unsigned long)[transaction numberOfKeysInCollection:kOWSProfileManager_GroupWhitelistCollection]);
        [transaction enumerateKeysInCollection:kOWSProfileManager_GroupWhitelistCollection
                                    usingBlock:^(NSString *_Nonnull key, BOOL *_Nonnull stop) {
                                        OWSLogError(@"\t profile whitelist group: %@", key);
                                    }];
    }];
}

- (void)regenerateLocalProfile
{
    OWSUserProfile *userProfile = self.localUserProfile;
    [userProfile clearWithProfileKey:[OWSAES256Key generateRandomKey] dbConnection:self.dbConnection completion:nil];
    [[self.tsAccountManager updateAccountAttributes] retainUntilComplete];
}

- (void)addUserToProfileWhitelist:(NSString *)recipientId
{
    OWSAssertDebug(recipientId.length > 0);

    [self addUsersToProfileWhitelist:@[ recipientId ]];
}

- (void)addUsersToProfileWhitelist:(NSArray<NSString *> *)recipientIds
{
    OWSAssertDebug(recipientIds);

    NSMutableSet<NSString *> *newRecipientIds = [NSMutableSet new];
    [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        for (NSString *recipientId in recipientIds) {
            NSNumber *_Nullable oldValue =
                [transaction objectForKey:recipientId inCollection:kOWSProfileManager_UserWhitelistCollection];
            if (oldValue && oldValue.boolValue) {
                continue;
            }

            // Normally we add all system contacts to the whitelist, but we don't want to do that for
            // blocked contacts.
            if ([self.blockingManager isRecipientIdBlocked:recipientId]) {
                continue;
            }

            [transaction setObject:@(YES) forKey:recipientId inCollection:kOWSProfileManager_UserWhitelistCollection];
            [newRecipientIds addObject:recipientId];
        }
    }
    completion:^{
        for (NSString *recipientId in newRecipientIds) {
            [[NSNotificationCenter defaultCenter]
                postNotificationNameAsync:kNSNotificationName_ProfileWhitelistDidChange
                                   object:nil
                                 userInfo:@{
                                     kNSNotificationKey_ProfileRecipientId : recipientId,
                                 }];
        }
    }];
}

- (BOOL)isUserInProfileWhitelist:(NSString *)recipientId
{
    OWSAssertDebug(recipientId.length > 0);

    if ([self.blockingManager isRecipientIdBlocked:recipientId]) {
        return NO;
    }

    __block BOOL result = NO;
    [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
        NSNumber *_Nullable oldValue =
            [transaction objectForKey:recipientId inCollection:kOWSProfileManager_UserWhitelistCollection];
        result = (oldValue && oldValue.boolValue);
    }];
    return result;
}

- (void)addGroupIdToProfileWhitelist:(NSData *)groupId
{
    OWSAssertDebug(groupId.length > 0);

    NSString *groupIdKey = [self groupKeyForGroupId:groupId];

    __block BOOL didChange = NO;
    [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        NSNumber *_Nullable oldValue =
            [transaction objectForKey:groupIdKey inCollection:kOWSProfileManager_GroupWhitelistCollection];
        if (oldValue && oldValue.boolValue) {
            // Do nothing.
        } else {
            [transaction setObject:@(YES) forKey:groupIdKey inCollection:kOWSProfileManager_GroupWhitelistCollection];
            didChange = YES;
        }
    }
    completion:^{
        if (didChange) {
            [[NSNotificationCenter defaultCenter]
                postNotificationNameAsync:kNSNotificationName_ProfileWhitelistDidChange
                                   object:nil
                                 userInfo:@{
                                     kNSNotificationKey_ProfileGroupId : groupId,
                                 }];
        }
    }];
}

- (void)addThreadToProfileWhitelist:(TSThread *)thread
{
    OWSAssertDebug(thread);

    if (thread.isGroupThread) {
        TSGroupThread *groupThread = (TSGroupThread *)thread;
        NSData *groupId = groupThread.groupModel.groupId;
        [self addGroupIdToProfileWhitelist:groupId];

        // When we add a group to the profile whitelist, we might as well
        // also add all current members to the profile whitelist
        // individually as well just in case delivery of the profile key
        // fails.
        for (NSString *recipientId in groupThread.recipientIdentifiers) {
            [self addUserToProfileWhitelist:recipientId];
        }
    } else {
        NSString *recipientId = thread.contactIdentifier;
        [self addUserToProfileWhitelist:recipientId];
    }
}

- (BOOL)isGroupIdInProfileWhitelist:(NSData *)groupId
{
    OWSAssertDebug(groupId.length > 0);

    if ([self.blockingManager isGroupIdBlocked:groupId]) {
        return NO;
    }

    NSString *groupIdKey = [self groupKeyForGroupId:groupId];

    __block BOOL result = NO;
    [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
        NSNumber *_Nullable oldValue =
            [transaction objectForKey:groupIdKey inCollection:kOWSProfileManager_GroupWhitelistCollection];
        result = (oldValue && oldValue.boolValue);
    }];
    return result;
}

- (BOOL)isThreadInProfileWhitelist:(TSThread *)thread
{
    OWSAssertDebug(thread);

    if (thread.isGroupThread) {
        TSGroupThread *groupThread = (TSGroupThread *)thread;
        NSData *groupId = groupThread.groupModel.groupId;
        return [self isGroupIdInProfileWhitelist:groupId];
    } else {
//        NSString *recipientId = thread.contactIdentifier;
//        return [self isUserInProfileWhitelist:recipientId];
        return true;
    }
}

- (void)setContactRecipientIds:(NSArray<NSString *> *)contactRecipientIds
{
    OWSAssertDebug(contactRecipientIds);

    [self addUsersToProfileWhitelist:contactRecipientIds];
}

#pragma mark - Other User's Profiles

- (void)logUserProfiles
{
    [self.dbConnection asyncReadWithBlock:^(YapDatabaseReadTransaction *transaction) {
        OWSLogError(@"logUserProfiles: %ld", (unsigned long) [transaction numberOfKeysInCollection:OWSUserProfile.collection]);
        [transaction
            enumerateKeysAndObjectsInCollection:OWSUserProfile.collection
                                     usingBlock:^(NSString *_Nonnull key, id _Nonnull object, BOOL *_Nonnull stop) {
                                         OWSAssertDebug([object isKindOfClass:[OWSUserProfile class]]);
                                         OWSUserProfile *userProfile = object;
                                         OWSLogError(@"\t [%@]: has profile key: %d, has avatar URL: %d, has "
                                                     @"avatar file: %d, name: %@",
                                             userProfile.recipientId,
                                             userProfile.profileKey != nil,
                                             userProfile.avatarUrlPath != nil,
                                             userProfile.avatarFileName != nil,
                                             userProfile.profileName);
                                     }];
    }];
}

- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId avatarURL:(nullable NSString *)avatarURL
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        OWSAES256Key *_Nullable profileKey = [OWSAES256Key keyWithData:profileKeyData];
        if (profileKey == nil) {
            OWSFailDebug(@"Failed to make profile key for key data");
            return;
        }
        
        OWSUserProfile *userProfile = [OWSUserProfile getOrBuildUserProfileForRecipientId:recipientId dbConnection:self.dbConnection];
        
        OWSAssertDebug(userProfile);
        if (userProfile.profileKey && [userProfile.profileKey.keyData isEqual:profileKey.keyData]) {
            // Ignore redundant update.
            return;
        }
        
        [userProfile clearWithProfileKey:profileKey
                            dbConnection:self.dbConnection
                              completion:^{
            dispatch_async(dispatch_get_main_queue(), ^{
                [self.udManager setUnidentifiedAccessMode:UnidentifiedAccessModeUnknown
                                              recipientId:recipientId];
                [userProfile updateWithAvatarUrlPath:avatarURL avatarFileName:nil dbConnection:self.dbConnection completion:^{
                    [self downloadAvatarForUserProfile:userProfile];
                }];
            });
        }];
    });
}

- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId
{
    [self setProfileKeyData:profileKeyData forRecipientId:recipientId avatarURL:nil];
}

- (nullable NSData *)profileKeyDataForRecipientId:(NSString *)recipientId
{
    return [self profileKeyForRecipientId:recipientId].keyData;
}

- (nullable OWSAES256Key *)profileKeyForRecipientId:(NSString *)recipientId
{
    OWSAssertDebug(recipientId.length > 0);
    
    // For "local reads", use the local user profile.
    OWSUserProfile *userProfile = ([self.tsAccountManager.localNumber isEqualToString:recipientId]
                                   ? self.localUserProfile
                                   : [OWSUserProfile getOrBuildUserProfileForRecipientId:recipientId dbConnection:self.dbConnection]);
    OWSAssertDebug(userProfile);

    return userProfile.profileKey;
}

- (nullable NSString *)profileNameForRecipientWithID:(NSString *)recipientID
{
    if ([self.tsAccountManager.localNumber isEqualToString:recipientID]) {
        return self.localUserProfile.profileName;
    }

    __block OWSUserProfile *userProfile;

    [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
        userProfile = [OWSUserProfile fetchObjectWithUniqueID:recipientID transaction:transaction];
    }];

    if (userProfile != nil) {
        return userProfile.profileName;
    }

    __block NSString *result;

    [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        result = [self profileNameForRecipientWithID:recipientID transaction:transaction];
    } error:nil];

    return result;
}

- (nullable NSString *)profileNameForRecipientWithID:(NSString *)recipientID transaction:(YapDatabaseReadWriteTransaction *)transaction
{
    OWSAssertDebug(recipientID.length > 0);

    // For "local reads", use the local user profile.
    OWSUserProfile *userProfile = [self.tsAccountManager.localNumber isEqualToString:recipientID]
                                  ? [self getLocalUserProfileWithTransaction:transaction]
                                  : [OWSUserProfile getOrBuildUserProfileForRecipientId:recipientID transaction:transaction];

    return userProfile.profileName;
}

- (nullable UIImage *)profileAvatarForRecipientId:(NSString *)recipientId
{
    OWSAssertDebug(recipientId.length > 0);

    // For "local reads", use the local user profile.
    OWSUserProfile *userProfile = ([self.tsAccountManager.localNumber isEqualToString:recipientId]
                                   ? self.localUserProfile
                                   : [OWSUserProfile getOrBuildUserProfileForRecipientId:recipientId dbConnection:self.dbConnection]);

    if (userProfile.avatarFileName.length > 0) {
        return [self loadProfileAvatarWithFilename:userProfile.avatarFileName];
    }

    if (userProfile.avatarUrlPath.length > 0) {
        [self downloadAvatarForUserProfile:userProfile];
    }

    return nil;
}

- (nullable NSData *)profileAvatarDataForRecipientId:(NSString *)recipientId
{
    OWSAssertDebug(recipientId.length > 0);

    // For "local reads", use the local user profile.
    OWSUserProfile *userProfile = ([self.tsAccountManager.localNumber isEqualToString:recipientId]
                                   ? self.localUserProfile
                                   : [OWSUserProfile getOrBuildUserProfileForRecipientId:recipientId dbConnection:self.dbConnection]);

    if (userProfile.avatarFileName.length > 0) {
        return [self loadProfileDataWithFilename:userProfile.avatarFileName];
    }

    return nil;
}

- (NSString *)generateAvatarFilename
{
    return [[NSUUID UUID].UUIDString stringByAppendingPathExtension:@"jpg"];
}

- (void)downloadAvatarForUserProfile:(OWSUserProfile *)userProfile
{
    OWSAssertDebug(userProfile);

    __block OWSBackgroundTask *backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if (userProfile.avatarUrlPath.length < 1) {
            OWSLogDebug(@"Skipping downloading avatar for %@ because url is not set", userProfile.recipientId);
            return;
        }
        NSString *_Nullable avatarUrlPathAtStart = userProfile.avatarUrlPath;

        if (userProfile.profileKey.keyData.length < 1 || userProfile.avatarUrlPath.length < 1) {
            return;
        }
        
        OWSAES256Key *profileKeyAtStart = userProfile.profileKey;

        NSString *fileName = [self generateAvatarFilename];
        NSString *filePath = [OWSUserProfile profileAvatarFilepathWithFilename:fileName];

        @synchronized(self.currentAvatarDownloads)
        {
            if ([self.currentAvatarDownloads containsObject:userProfile.recipientId]) {
                // Download already in flight; ignore.
                return;
            }
            [self.currentAvatarDownloads addObject:userProfile.recipientId];
        }

        OWSLogVerbose(@"downloading profile avatar: %@", userProfile.uniqueId);

        NSString *tempDirectory = OWSTemporaryDirectory();
        NSString *tempFilePath = [tempDirectory stringByAppendingPathComponent:fileName];

        NSString *profilePictureURL = userProfile.avatarUrlPath;
        NSError *serializationError;
        NSMutableURLRequest *request =
            [self.avatarHTTPManager.requestSerializer requestWithMethod:@"GET"
                                                              URLString:profilePictureURL
                                                             parameters:nil
                                                                  error:&serializationError];
        if (serializationError) {
            OWSFailDebug(@"serializationError: %@", serializationError);
            return;
        }

        NSURLSession* session = [NSURLSession sharedSession];
        NSURLSessionTask* downloadTask = [session downloadTaskWithRequest:request completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
            
            @synchronized(self.currentAvatarDownloads)
            {
                [self.currentAvatarDownloads removeObject:userProfile.recipientId];
            }
            
            if (error) {
                OWSLogError(@"Dowload failed: %@", error);
                return;
            }

            NSFileManager *fileManager = [NSFileManager defaultManager];
            NSURL *tempFileUrl = [NSURL fileURLWithPath:tempFilePath];
            NSError *moveError;
            if (![fileManager moveItemAtURL:location toURL:tempFileUrl error:&moveError]) {
                OWSLogError(@"MoveItemAtURL for avatar failed: %@", moveError);
                return;
            }
            
            NSData *_Nullable encryptedData = (error ? nil : [NSData dataWithContentsOfFile:tempFilePath]);
            NSData *_Nullable decryptedData = [self decryptProfileData:encryptedData profileKey:profileKeyAtStart];
            UIImage *_Nullable image = nil;
            if (decryptedData) {
                BOOL success = [decryptedData writeToFile:filePath atomically:YES];
                if (success) {
                    image = [UIImage imageWithContentsOfFile:filePath];
                }
            }
            
            OWSUserProfile *latestUserProfile = [OWSUserProfile getOrBuildUserProfileForRecipientId:userProfile.recipientId dbConnection:self.dbConnection];

            if (latestUserProfile.profileKey.keyData.length < 1
                || ![latestUserProfile.profileKey isEqual:userProfile.profileKey]) {
                OWSLogWarn(@"Ignoring avatar download for obsolete user profile.");
            } else if (![avatarUrlPathAtStart isEqualToString:latestUserProfile.avatarUrlPath]) {
                OWSLogInfo(@"avatar url has changed during download");
                if (latestUserProfile.avatarUrlPath.length > 0) {
                    [self downloadAvatarForUserProfile:latestUserProfile];
                }
            } else if (error) {
                if ([response isKindOfClass:NSHTTPURLResponse.class]
                    && ((NSHTTPURLResponse *)response).statusCode == 403) {
                    OWSLogInfo(@"no avatar for: %@", userProfile.recipientId);
                } else {
                    OWSLogError(@"avatar download for %@ failed with error: %@", userProfile.recipientId, error);
                }
            } else if (!encryptedData) {
                OWSLogError(@"avatar encrypted data for %@ could not be read.", userProfile.recipientId);
            } else if (!decryptedData) {
                OWSLogError(@"avatar data for %@ could not be decrypted.", userProfile.recipientId);
            } else if (!image) {
                OWSLogError(@"avatar image for %@ could not be loaded with error: %@", userProfile.recipientId, error);
            } else {
                [self updateProfileAvatarCache:image filename:fileName];

                [latestUserProfile updateWithAvatarFileName:fileName dbConnection:self.dbConnection completion:^{
                    [[NSNotificationCenter defaultCenter]
                        postNotificationNameAsync:OWSContactsManagerSignalAccountsDidChangeNotification
                        object:nil];
                }];
            }

            // If we're updating the profile that corresponds to our local number,
            // update the local profile as well.
            if ([self.tsAccountManager.localNumber isEqualToString:userProfile.recipientId]) {
                OWSUserProfile *localUserProfile = self.localUserProfile;
                OWSAssertDebug(localUserProfile);

                [localUserProfile updateWithAvatarFileName:fileName dbConnection:self.dbConnection completion:nil];
                [self updateProfileAvatarCache:image filename:fileName];
            }

            OWSAssertDebug(backgroundTask);
            backgroundTask = nil;
        }];

        [downloadTask resume];
    });
}

- (void)updateProfileForRecipientId:(NSString *)recipientId
               profileNameEncrypted:(nullable NSData *)profileNameEncrypted
                      avatarUrlPath:(nullable NSString *)avatarUrlPath
{
    OWSAssertDebug(recipientId.length > 0);

    OWSLogDebug(@"update profile for: %@ name: %@ avatar: %@", recipientId, profileNameEncrypted, avatarUrlPath);

    // Ensure decryption, etc. off main thread.
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        OWSUserProfile *userProfile =
            [OWSUserProfile getOrBuildUserProfileForRecipientId:recipientId dbConnection:self.dbConnection];

        NSString *_Nullable localNumber = self.tsAccountManager.localNumber;
        // If we're updating the profile that corresponds to our local number,
        // make sure we're using the latest key.
        if (localNumber && [localNumber isEqualToString:recipientId]) {
            [userProfile updateWithProfileKey:self.localUserProfile.profileKey
                                 dbConnection:self.dbConnection
                                   completion:nil];
        }

        if (!userProfile.profileKey) {
            return;
        }

        NSString *_Nullable profileName =
            [self decryptProfileNameData:profileNameEncrypted profileKey:userProfile.profileKey];

        [userProfile updateWithProfileName:profileName
                             avatarUrlPath:avatarUrlPath
                              dbConnection:self.dbConnection
                                completion:nil];

        // If we're updating the profile that corresponds to our local number,
        // update the local profile as well.
        if (localNumber && [localNumber isEqualToString:recipientId]) {
            OWSUserProfile *localUserProfile = self.localUserProfile;
            OWSAssertDebug(localUserProfile);

            [localUserProfile updateWithProfileName:profileName
                                      avatarUrlPath:avatarUrlPath
                                       dbConnection:self.dbConnection
                                         completion:nil];
        }

        // Whenever we change avatarUrlPath, OWSUserProfile clears avatarFileName.
        // So if avatarUrlPath is set and avatarFileName is not set, we should to
        // download this avatar. downloadAvatarForUserProfile will de-bounce
        // downloads.
        if (userProfile.avatarUrlPath.length > 0 && userProfile.avatarFileName.length < 1) {
            [self downloadAvatarForUserProfile:userProfile];
        }
    });
}

- (void)updateProfileForContactWithID:(NSString *)contactID displayName:(NSString *)displayName with:(YapDatabaseReadWriteTransaction *)transaction
{
    OWSUserProfile *userProfile = [OWSUserProfile getOrBuildUserProfileForRecipientId:contactID transaction:transaction];
    [userProfile updateWithProfileName:displayName avatarUrlPath:userProfile.avatarUrlPath avatarFileName:userProfile.avatarFileName transaction:transaction completion:nil];
}

- (void)ensureProfileCachedForContactWithID:(NSString *)contactID with:(YapDatabaseReadWriteTransaction *)transaction
{
    OWSUserProfile *userProfile = [OWSUserProfile getOrBuildUserProfileForRecipientId:contactID transaction:transaction];
    [userProfile saveWithTransaction:transaction];
}

- (BOOL)isNullableDataEqual:(NSData *_Nullable)left toData:(NSData *_Nullable)right
{
    if (left == nil && right == nil) {
        return YES;
    } else if (left == nil || right == nil) {
        return YES;
    } else {
        return [left isEqual:right];
    }
}

- (BOOL)isNullableStringEqual:(NSString *_Nullable)left toString:(NSString *_Nullable)right
{
    if (left == nil && right == nil) {
        return YES;
    } else if (left == nil || right == nil) {
        return YES;
    } else {
        return [left isEqualToString:right];
    }
}

#pragma mark - Profile Encryption

- (nullable NSData *)encryptProfileData:(nullable NSData *)encryptedData profileKey:(OWSAES256Key *)profileKey
{
    OWSAssertDebug(profileKey.keyData.length == kAES256_KeyByteLength);

    if (!encryptedData) {
        return nil;
    }

    return [Cryptography encryptAESGCMWithProfileData:encryptedData key:profileKey];
}

- (nullable NSData *)decryptProfileData:(nullable NSData *)encryptedData profileKey:(OWSAES256Key *)profileKey
{
    OWSAssertDebug(profileKey.keyData.length == kAES256_KeyByteLength);

    if (!encryptedData) {
        return nil;
    }

    return [Cryptography decryptAESGCMWithProfileData:encryptedData key:profileKey];
}

- (nullable NSString *)decryptProfileNameData:(nullable NSData *)encryptedData profileKey:(OWSAES256Key *)profileKey
{
    OWSAssertDebug(profileKey.keyData.length == kAES256_KeyByteLength);

    NSData *_Nullable decryptedData = [self decryptProfileData:encryptedData profileKey:profileKey];
    if (decryptedData.length < 1) {
        return nil;
    }


    // Unpad profile name.
    NSUInteger unpaddedLength = 0;
    const char *bytes = decryptedData.bytes;

    // Work through the bytes until we encounter our first
    // padding byte (our padding scheme is NULL bytes)
    for (NSUInteger i = 0; i < decryptedData.length; i++) {
        if (bytes[i] == 0x00) {
            break;
        }
        unpaddedLength = i + 1;
    }

    NSData *unpaddedData = [decryptedData subdataWithRange:NSMakeRange(0, unpaddedLength)];

    return [[NSString alloc] initWithData:unpaddedData encoding:NSUTF8StringEncoding];
}

- (nullable NSData *)encryptProfileData:(nullable NSData *)data
{
    return [self encryptProfileData:data profileKey:self.localProfileKey];
}

- (BOOL)isProfileNameTooLong:(nullable NSString *)profileName
{
    OWSAssertIsOnMainThread();

    NSData *nameData = [profileName dataUsingEncoding:NSUTF8StringEncoding];
    return nameData.length > kOWSProfileManager_NameDataLength;
}

- (nullable NSData *)encryptProfileNameWithUnpaddedName:(NSString *)name
{
    NSData *nameData = [name dataUsingEncoding:NSUTF8StringEncoding];
    if (nameData.length > kOWSProfileManager_NameDataLength) {
        OWSFailDebug(@"name data is too long with length:%lu", (unsigned long)nameData.length);
        return nil;
    }

    NSUInteger paddingByteCount = kOWSProfileManager_NameDataLength - nameData.length;

    NSMutableData *paddedNameData = [nameData mutableCopy];
    // Since we want all encrypted profile names to be the same length on the server, we use `increaseLengthBy`
    // to pad out any remaining length with 0 bytes.
    [paddedNameData increaseLengthBy:paddingByteCount];
    OWSAssertDebug(paddedNameData.length == kOWSProfileManager_NameDataLength);

    return [self encryptProfileData:[paddedNameData copy] profileKey:self.localProfileKey];
}

#pragma mark - Avatar Disk Cache

- (nullable NSData *)loadProfileDataWithFilename:(NSString *)filename
{
    if (filename.length <= 0) { return nil; };

    NSString *filePath = [OWSUserProfile profileAvatarFilepathWithFilename:filename];
    return [NSData dataWithContentsOfFile:filePath];
}

- (nullable UIImage *)loadProfileAvatarWithFilename:(NSString *)filename
{
    if (filename.length == 0) {
        return nil;
    }

    UIImage *_Nullable image = nil;
    @synchronized(self.profileAvatarImageCache)
    {
        image = [self.profileAvatarImageCache objectForKey:filename];
    }
    if (image) {
        return image;
    }

    NSData *data = [self loadProfileDataWithFilename:filename];
    if (![data ows_isValidImage]) {
        return nil;
    }
    image = [UIImage imageWithData:data];
    [self updateProfileAvatarCache:image filename:filename];
    return image;
}

- (void)updateProfileAvatarCache:(nullable UIImage *)image filename:(NSString *)filename
{
    if (filename.length <= 0) { return; };

    @synchronized(self.profileAvatarImageCache)
    {
        if (image) {
            [self.profileAvatarImageCache setObject:image forKey:filename];
        } else {
            [self.profileAvatarImageCache removeObjectForKey:filename];
        }
    }
}

#pragma mark - User Interface

- (void)presentAddThreadToProfileWhitelist:(TSThread *)thread
                        fromViewController:(UIViewController *)fromViewController
                                   success:(void (^)(void))successHandler
{
    OWSAssertIsOnMainThread();

    UIAlertController *alert =
        [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];

    NSString *shareTitle = NSLocalizedString(@"CONVERSATION_SETTINGS_VIEW_SHARE_PROFILE",
        @"Button to confirm that user wants to share their profile with a user or group.");
    [alert addAction:[UIAlertAction actionWithTitle:shareTitle
                            accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"share_profile")
                                              style:UIAlertActionStyleDefault
                                            handler:^(UIAlertAction *_Nonnull action) {
                                                [self userAddedThreadToProfileWhitelist:thread];
                                                successHandler();
                                            }]];
    [alert addAction:[OWSAlerts cancelAction]];

    [fromViewController presentAlert:alert];
}

- (void)userAddedThreadToProfileWhitelist:(TSThread *)thread
{
    OWSAssertIsOnMainThread();

    BOOL isFeatureEnabled = NO;
    if (!isFeatureEnabled) {
        OWSLogWarn(@"skipping sending profile-key message because the feature is not yet fully available.");
        [OWSProfileManager.sharedManager addThreadToProfileWhitelist:thread];
        return;
    }

    // MJK TODO - should be safe to remove this senderTimestamp
    OWSProfileKeyMessage *message =
        [[OWSProfileKeyMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] inThread:thread];
    [OWSProfileManager.sharedManager addThreadToProfileWhitelist:thread];

    [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
        [self.messageSenderJobQueue addMessage:message transaction:transaction];
    } error:nil];
}

#pragma mark - Notifications

- (void)applicationDidBecomeActive:(NSNotification *)notification
{
    OWSAssertIsOnMainThread();

    // TODO: Sync if necessary.
}

- (void)blockListDidChange:(NSNotification *)notification {
    OWSAssertIsOnMainThread();

    [AppReadiness runNowOrWhenAppDidBecomeReady:^{
        [self rotateLocalProfileKeyIfNecessary];
    }];
}

@end

NS_ASSUME_NONNULL_END
