/*
 * Copyright 2019 Google
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenManager.h"

#import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h"
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
#import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingAuthKeychain.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingAuthService.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinPreferences.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinStore.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenDeleteOperation.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenFetchOperation.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenInfo.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenOperation.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenStore.h"

@interface FIRMessagingTokenManager () {
  FIRMessagingTokenStore *_tokenStore;
  NSString *_defaultFCMToken;
}

@property(nonatomic, readwrite, strong) FIRMessagingCheckinStore *checkinStore;
@property(nonatomic, readwrite, strong) FIRMessagingAuthService *authService;
@property(nonatomic, readonly, strong) NSOperationQueue *tokenOperations;

@property(nonatomic, readwrite, strong) FIRMessagingAPNSInfo *currentAPNSInfo;
@property(nonatomic, readwrite) FIRInstallations *installations;

@property(readonly) id<FIRHeartbeatLoggerProtocol> heartbeatLogger;

@end

@implementation FIRMessagingTokenManager

- (instancetype)initWithHeartbeatLogger:(id<FIRHeartbeatLoggerProtocol>)heartbeatLogger {
  self = [super init];
  if (self) {
    _tokenStore = [[FIRMessagingTokenStore alloc] init];
    _authService = [[FIRMessagingAuthService alloc] init];
    [self resetCredentialsIfNeeded];
    [self configureTokenOperations];
    _installations = [FIRInstallations installations];
    _heartbeatLogger = heartbeatLogger;
  }
  return self;
}

- (void)dealloc {
  [self stopAllTokenOperations];
}

- (NSString *)tokenAndRequestIfNotExist {
  if (!self.fcmSenderID.length) {
    return nil;
  }

  if (_defaultFCMToken.length) {
    return _defaultFCMToken;
  }

  FIRMessagingTokenInfo *cachedTokenInfo =
      [self cachedTokenInfoWithAuthorizedEntity:self.fcmSenderID
                                          scope:kFIRMessagingDefaultTokenScope];
  NSString *cachedToken = cachedTokenInfo.token;

  if (cachedToken) {
    return cachedToken;
  } else {
    [self tokenWithAuthorizedEntity:self.fcmSenderID
                              scope:kFIRMessagingDefaultTokenScope
                            options:[self tokenOptions]
                            handler:^(NSString *_Nullable FCMToken, NSError *_Nullable error){

                            }];
    return nil;
  }
}

- (NSString *)defaultFCMToken {
  return _defaultFCMToken;
}

- (void)postTokenRefreshNotificationWithDefaultFCMToken:(NSString *)defaultFCMToken {
  // Should always trigger the token refresh notification when the delegate method is called
  // No need to check if the token has changed, it's handled in the notification receiver.
  NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
  [center postNotificationName:kFIRMessagingRegistrationTokenRefreshNotification
                        object:defaultFCMToken];
}

- (void)saveDefaultTokenInfoInKeychain:(NSString *)defaultFcmToken {
  if ([self hasTokenChangedFromOldToken:_defaultFCMToken toNewToken:defaultFcmToken]) {
    _defaultFCMToken = [defaultFcmToken copy];
    FIRMessagingTokenInfo *tokenInfo =
        [[FIRMessagingTokenInfo alloc] initWithAuthorizedEntity:_fcmSenderID
                                                          scope:kFIRMessagingDefaultTokenScope
                                                          token:defaultFcmToken
                                                     appVersion:FIRMessagingCurrentAppVersion()
                                                  firebaseAppID:_firebaseAppID];
    tokenInfo.APNSInfo =
        [[FIRMessagingAPNSInfo alloc] initWithTokenOptionsDictionary:[self tokenOptions]];

    [self->_tokenStore saveTokenInfoInCache:tokenInfo];
  }
}

- (BOOL)hasTokenChangedFromOldToken:(NSString *)oldToken toNewToken:(NSString *)newToken {
  return oldToken.length != newToken.length ||
         (oldToken.length && newToken.length && ![oldToken isEqualToString:newToken]);
}

- (NSDictionary *)tokenOptions {
  NSDictionary *instanceIDOptions = @{};
  NSData *apnsTokenData = self.currentAPNSInfo.deviceToken;
  if (apnsTokenData) {
    instanceIDOptions = @{
      kFIRMessagingTokenOptionsAPNSKey : apnsTokenData,
      kFIRMessagingTokenOptionsAPNSIsSandboxKey : @(self.currentAPNSInfo.isSandbox),
    };
  }

  return instanceIDOptions;
}

- (NSString *)deviceAuthID {
  return [_authService checkinPreferences].deviceID;
}

- (NSString *)secretToken {
  return [_authService checkinPreferences].secretToken;
}

- (NSString *)versionInfo {
  return [_authService checkinPreferences].versionInfo;
}

- (void)configureTokenOperations {
  _tokenOperations = [[NSOperationQueue alloc] init];
  _tokenOperations.name = @"com.google.iid-token-operations";
  // For now, restrict the operations to be serial, because in some cases (like if the
  // authorized entity and scope are the same), order matters.
  // If we have to deal with several different token requests simultaneously, it would be a good
  // idea to add some better intelligence around this (performing unrelated token operations
  // simultaneously, etc.).
  _tokenOperations.maxConcurrentOperationCount = 1;
  if ([_tokenOperations respondsToSelector:@selector(qualityOfService)]) {
    _tokenOperations.qualityOfService = NSOperationQualityOfServiceUtility;
  }
}

- (void)tokenWithAuthorizedEntity:(NSString *)authorizedEntity
                            scope:(NSString *)scope
                          options:(NSDictionary *)options
                          handler:(FIRMessagingFCMTokenFetchCompletion)handler {
  if (!handler) {
    FIRMessagingLoggerError(kFIRMessagingMessageCodeInstanceID000, @"Invalid nil handler");
    return;
  }

  // Add internal options
  NSMutableDictionary *tokenOptions = [NSMutableDictionary dictionary];
  if (options.count) {
    [tokenOptions addEntriesFromDictionary:options];
  }

  // ensure we have an APNS Token
  if (tokenOptions[kFIRMessagingTokenOptionsAPNSKey] == nil) {
    // we don't have an APNS token. Don't fetch or return a FCM Token
    FIRMessagingLoggerWarn(kFIRMessagingMessageCodeAPNSTokenNotAvailableDuringTokenFetch,
                           @"Declining request for FCM Token since no APNS Token specified");
    dispatch_async(dispatch_get_main_queue(), ^{
      NSError *missingAPNSTokenError =
          [NSError messagingErrorWithCode:kFIRMessagingErrorCodeMissingDeviceToken
                            failureReason:@"No APNS token specified before fetching FCM Token"];
      handler(nil, missingAPNSTokenError);
    });
    return;
  }

#if TARGET_OS_SIMULATOR
  if (tokenOptions[kFIRMessagingTokenOptionsAPNSKey] != nil) {
    // If APNS token is available on iOS Simulator, we must use the sandbox profile
    // https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes
    tokenOptions[kFIRMessagingTokenOptionsAPNSIsSandboxKey] = @(YES);
  }
#endif  // TARGET_OS_SIMULATOR

  if (tokenOptions[kFIRMessagingTokenOptionsAPNSKey] != nil &&
      tokenOptions[kFIRMessagingTokenOptionsAPNSIsSandboxKey] == nil) {
    // APNS key was given, but server type is missing. Supply the server type with automatic
    // checking. This can happen when the token is requested from FCM, which does not include a
    // server type during its request.
    tokenOptions[kFIRMessagingTokenOptionsAPNSIsSandboxKey] = @(FIRMessagingIsSandboxApp());
  }
  if (self.firebaseAppID) {
    tokenOptions[kFIRMessagingTokenOptionsFirebaseAppIDKey] = self.firebaseAppID;
  }

  // comparing enums to ints directly throws a warning
  FIRMessagingErrorCode noError = INT_MAX;
  FIRMessagingErrorCode errorCode = noError;
  if (![authorizedEntity length]) {
    errorCode = kFIRMessagingErrorCodeMissingAuthorizedEntity;
  } else if (![scope length]) {
    errorCode = kFIRMessagingErrorCodeMissingScope;
  } else if (!self.installations) {
    errorCode = kFIRMessagingErrorCodeMissingFid;
  }

  FIRMessagingFCMTokenFetchCompletion newHandler = ^(NSString *token, NSError *error) {
    dispatch_async(dispatch_get_main_queue(), ^{
      handler(token, error);
    });
  };

  if (errorCode != noError) {
    newHandler(
        nil,
        [NSError messagingErrorWithCode:errorCode
                          failureReason:@"Failed to send token request, missing critical info."]);
    return;
  }

  FIRMessaging_WEAKIFY(self);
  [_authService fetchCheckinInfoWithHandler:^(FIRMessagingCheckinPreferences *preferences,
                                              NSError *error) {
    FIRMessaging_STRONGIFY(self);
    if (error) {
      newHandler(nil, error);
      return;
    }

    if (!self) {
      NSError *derefErr =
          [NSError messagingErrorWithCode:kFIRMessagingErrorCodeInternal
                            failureReason:@"Unable to fetch token. Lost Reference to TokenManager"];
      handler(nil, derefErr);
      return;
    }

    FIRMessaging_WEAKIFY(self);
    [self->_installations
        installationIDWithCompletion:^(NSString *_Nullable identifier, NSError *_Nullable error) {
          FIRMessaging_STRONGIFY(self);

          if (error) {
            newHandler(nil, error);
          } else {
            FIRMessagingTokenInfo *cachedTokenInfo =
                [self cachedTokenInfoWithAuthorizedEntity:authorizedEntity scope:scope];
            FIRMessagingAPNSInfo *optionsAPNSInfo =
                [[FIRMessagingAPNSInfo alloc] initWithTokenOptionsDictionary:tokenOptions];
            // Check if APNS Info is changed
            if ((!cachedTokenInfo.APNSInfo && !optionsAPNSInfo) ||
                [cachedTokenInfo.APNSInfo isEqualToAPNSInfo:optionsAPNSInfo]) {
              // check if token is fresh
              if ([cachedTokenInfo isFreshWithIID:identifier]) {
                newHandler(cachedTokenInfo.token, nil);
                return;
              }
            }
            [self fetchNewTokenWithAuthorizedEntity:[authorizedEntity copy]
                                              scope:[scope copy]
                                         instanceID:identifier
                                            options:tokenOptions
                                            handler:newHandler];
          }
        }];
  }];
}

- (void)fetchNewTokenWithAuthorizedEntity:(NSString *)authorizedEntity
                                    scope:(NSString *)scope
                               instanceID:(NSString *)instanceID
                                  options:(NSDictionary *)options
                                  handler:(FIRMessagingFCMTokenFetchCompletion)handler {
  FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenManager000,
                          @"Fetch new token for authorizedEntity: %@, scope: %@", authorizedEntity,
                          scope);
  FIRMessagingTokenFetchOperation *operation =
      [self createFetchOperationWithAuthorizedEntity:authorizedEntity
                                               scope:scope
                                             options:options
                                          instanceID:instanceID];
  FIRMessaging_WEAKIFY(self);
  FIRMessagingTokenOperationCompletion completion = ^(FIRMessagingTokenOperationResult result,
                                                      NSString *_Nullable token,
                                                      NSError *_Nullable error) {
    FIRMessaging_STRONGIFY(self);
    if (error) {
      handler(nil, error);
      return;
    }

    if (!self) {
      NSError *lostRefError = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeInternal
                                                failureReason:@"Lost Reference to TokenManager"];
      handler(nil, lostRefError);
      return;
    }

    if ([self isDefaultTokenWithAuthorizedEntity:authorizedEntity scope:scope]) {
      [self postTokenRefreshNotificationWithDefaultFCMToken:token];
    }
    NSString *firebaseAppID = options[kFIRMessagingTokenOptionsFirebaseAppIDKey];
    FIRMessagingTokenInfo *tokenInfo =
        [[FIRMessagingTokenInfo alloc] initWithAuthorizedEntity:authorizedEntity
                                                          scope:scope
                                                          token:token
                                                     appVersion:FIRMessagingCurrentAppVersion()
                                                  firebaseAppID:firebaseAppID];
    tokenInfo.APNSInfo = [[FIRMessagingAPNSInfo alloc] initWithTokenOptionsDictionary:options];

    [self->_tokenStore
        saveTokenInfo:tokenInfo
              handler:^(NSError *error) {
                if (!error) {
                  // Do not send the token back in case the save was unsuccessful. Since with
                  // the new asynchronous fetch mechanism this can lead to infinite loops, for
                  // example, we will return a valid token even though we weren't able to store
                  // it in our cache. The first token will lead to a onTokenRefresh callback
                  // wherein the user again calls `getToken` but since we weren't able to save
                  // it we won't hit the cache but hit the server again leading to an infinite
                  // loop.
                  FIRMessagingLoggerDebug(
                      kFIRMessagingMessageCodeTokenManager001,
                      @"Token fetch successful, token: %@, authorizedEntity: %@, scope:%@", token,
                      authorizedEntity, scope);

                  if (handler) {
                    handler(token, nil);
                  }
                } else {
                  if (handler) {
                    handler(nil, error);
                  }
                }
              }];
  };
  // Add completion handler, and ensure it's called on the main queue
  [operation addCompletionHandler:^(FIRMessagingTokenOperationResult result,
                                    NSString *_Nullable token, NSError *_Nullable error) {
    dispatch_async(dispatch_get_main_queue(), ^{
      completion(result, token, error);
    });
  }];
  [self.tokenOperations addOperation:operation];
}

- (FIRMessagingTokenInfo *)cachedTokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity
                                                         scope:(NSString *)scope {
  FIRMessagingTokenInfo *tokenInfo = [_tokenStore tokenInfoWithAuthorizedEntity:authorizedEntity
                                                                          scope:scope];
  return tokenInfo;
}

- (BOOL)isDefaultTokenWithAuthorizedEntity:(NSString *)authorizedEntity scope:(NSString *)scope {
  if (_fcmSenderID.length != authorizedEntity.length) {
    return NO;
  }
  if (![_fcmSenderID isEqualToString:authorizedEntity]) {
    return NO;
  }
  return [scope isEqualToString:kFIRMessagingDefaultTokenScope];
}

- (void)deleteTokenWithAuthorizedEntity:(NSString *)authorizedEntity
                                  scope:(NSString *)scope
                             instanceID:(NSString *)instanceID
                                handler:(FIRMessagingDeleteFCMTokenCompletion)handler {
  if ([_tokenStore tokenInfoWithAuthorizedEntity:authorizedEntity scope:scope]) {
    [_tokenStore removeTokenWithAuthorizedEntity:authorizedEntity scope:scope];
  }
  // Does not matter if we cannot find it in the cache. Still make an effort to unregister
  // from the server.
  FIRMessagingCheckinPreferences *checkinPreferences = self.authService.checkinPreferences;
  FIRMessagingTokenDeleteOperation *operation =
      [self createDeleteOperationWithAuthorizedEntity:authorizedEntity
                                                scope:scope
                                   checkinPreferences:checkinPreferences
                                           instanceID:instanceID
                                               action:FIRMessagingTokenActionDeleteToken];

  if (handler) {
    [operation addCompletionHandler:^(FIRMessagingTokenOperationResult result,
                                      NSString *_Nullable token, NSError *_Nullable error) {
      if ([self isDefaultTokenWithAuthorizedEntity:authorizedEntity scope:scope]) {
        [self postTokenRefreshNotificationWithDefaultFCMToken:nil];
      }
      dispatch_async(dispatch_get_main_queue(), ^{
        handler(error);
      });
    }];
  }
  [self.tokenOperations addOperation:operation];
}

- (void)deleteAllTokensWithHandler:(void (^)(NSError *))handler {
  FIRMessaging_WEAKIFY(self);

  [self.installations
      installationIDWithCompletion:^(NSString *_Nullable identifier, NSError *_Nullable error) {
        FIRMessaging_STRONGIFY(self);
        if (error) {
          if (handler) {
            dispatch_async(dispatch_get_main_queue(), ^{
              handler(error);
            });
          }
          return;
        }
        // delete all tokens
        FIRMessagingCheckinPreferences *checkinPreferences = self.authService.checkinPreferences;
        if (!checkinPreferences) {
          // The checkin is already deleted. No need to trigger the token delete operation as client
          // no longer has the checkin information for server to delete.
          dispatch_async(dispatch_get_main_queue(), ^{
            handler(nil);
          });
          return;
        }
        FIRMessagingTokenDeleteOperation *operation = [self
            createDeleteOperationWithAuthorizedEntity:kFIRMessagingKeychainWildcardIdentifier
                                                scope:kFIRMessagingKeychainWildcardIdentifier
                                   checkinPreferences:checkinPreferences
                                           instanceID:identifier
                                               action:FIRMessagingTokenActionDeleteTokenAndIID];
        if (handler) {
          [operation addCompletionHandler:^(FIRMessagingTokenOperationResult result,
                                            NSString *_Nullable token, NSError *_Nullable error) {
            self->_defaultFCMToken = nil;
            dispatch_async(dispatch_get_main_queue(), ^{
              handler(error);
            });
          }];
        }
        [self.tokenOperations addOperation:operation];
      }];
}

- (void)deleteAllTokensLocallyWithHandler:(void (^)(NSError *error))handler {
  [_tokenStore removeAllTokensWithHandler:handler];
}

- (void)stopAllTokenOperations {
  [self.authService stopCheckinRequest];
  [self.tokenOperations cancelAllOperations];
}

- (void)deleteWithHandler:(void (^)(NSError *))handler {
  FIRMessaging_WEAKIFY(self);
  [self deleteAllTokensWithHandler:^(NSError *_Nullable error) {
    FIRMessaging_STRONGIFY(self);
    if (error) {
      handler(error);
      return;
    }

    if (!self) {
      NSError *lostRefError =
          [NSError messagingErrorWithCode:kFIRMessagingErrorCodeInternal
                            failureReason:@"Cannot delete token. Lost reference to TokenManager"];
      handler(lostRefError);
      return;
    }

    [self deleteAllTokensLocallyWithHandler:^(NSError *localError) {
      [self postTokenRefreshNotificationWithDefaultFCMToken:nil];
      self->_defaultFCMToken = nil;
      if (localError) {
        handler(localError);
        return;
      }
      [self.authService resetCheckinWithHandler:^(NSError *_Nonnull authError) {
        handler(authError);
      }];
    }];
  }];
}

#pragma mark - CheckinStore

/**
 *  Reset the keychain preferences if the app had been deleted earlier and then reinstalled.
 *  Keychain preferences are not cleared in the above scenario so explicitly clear them.
 *
 *  In case of an iCloud backup and restore the Keychain preferences should already be empty
 *  since the Keychain items are marked with `*BackupThisDeviceOnly`.
 */
- (void)resetCredentialsIfNeeded {
  BOOL checkinPlistExists = [_authService hasCheckinPlist];
  // Checkin info existed in backup excluded plist. Should not be a fresh install.
  if (checkinPlistExists) {
    return;
  }
  // Keychain can still exist even if app is uninstalled.
  FIRMessagingCheckinPreferences *oldCheckinPreferences = _authService.checkinPreferences;

  if (!oldCheckinPreferences) {
    FIRMessagingLoggerDebug(kFIRMessagingMessageCodeStore009,
                            @"App reset detected but no valid checkin auth preferences found."
                            @" Will not delete server token registrations.");
    return;
  }
  [_authService resetCheckinWithHandler:^(NSError *_Nonnull error) {
    if (!error) {
      FIRMessagingLoggerDebug(
          kFIRMessagingMessageCodeStore002,
          @"Removed cached checkin preferences from Keychain because this is a fresh install.");
    } else {
      FIRMessagingLoggerError(
          kFIRMessagingMessageCodeStore003,
          @"Couldn't remove cached checkin preferences for a fresh install. Error: %@", error);
    }

    if (oldCheckinPreferences.deviceID.length && oldCheckinPreferences.secretToken.length) {
      FIRMessagingLoggerDebug(kFIRMessagingMessageCodeStore006,
                              @"Resetting old checkin and deleting server token registrations.");
      // We don't really need to delete old FCM tokens created via IID auth tokens since
      // those tokens are already hashed by APNS token as the has so creating a new
      // token should automatically delete the old-token.
      [self didDeleteFCMScopedTokensForCheckin:oldCheckinPreferences];
    }
  }];
}

- (void)didDeleteFCMScopedTokensForCheckin:(FIRMessagingCheckinPreferences *)checkin {
  // Make a best effort try to delete the old client related state on the FCM server. This is
  // required to delete old pubusb registrations which weren't cleared when the app was deleted.
  //
  // This is only a one time effort. If this call fails the client would still receive duplicate
  // pubsub notifications if he is again subscribed to the same topic.
  //
  // The client state should be cleared on the server for the provided checkin preferences.
  FIRMessagingTokenDeleteOperation *operation =
      [self createDeleteOperationWithAuthorizedEntity:nil
                                                scope:nil
                                   checkinPreferences:checkin
                                           instanceID:nil
                                               action:FIRMessagingTokenActionDeleteToken];
  [operation addCompletionHandler:^(FIRMessagingTokenOperationResult result,
                                    NSString *_Nullable token, NSError *_Nullable error) {
    if (error) {
      FIRMessagingMessageCode code =
          kFIRMessagingMessageCodeTokenManagerErrorDeletingFCMTokensOnAppReset;
      FIRMessagingLoggerDebug(code, @"Failed to delete GCM server registrations on app reset.");
    } else {
      FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenManagerDeletedFCMTokensOnAppReset,
                              @"Successfully deleted GCM server registrations on app reset");
    }
  }];

  [self.tokenOperations addOperation:operation];
}

#pragma mark - Unit Testing Stub Helpers
// We really have this method so that we can more easily stub it out for unit testing
- (FIRMessagingTokenFetchOperation *)
    createFetchOperationWithAuthorizedEntity:(NSString *)authorizedEntity
                                       scope:(NSString *)scope
                                     options:(NSDictionary<NSString *, NSString *> *)options
                                  instanceID:(NSString *)instanceID {
  FIRMessagingCheckinPreferences *checkinPreferences = self.authService.checkinPreferences;
  FIRMessagingTokenFetchOperation *operation =
      [[FIRMessagingTokenFetchOperation alloc] initWithAuthorizedEntity:authorizedEntity
                                                                  scope:scope
                                                                options:options
                                                     checkinPreferences:checkinPreferences
                                                             instanceID:instanceID
                                                        heartbeatLogger:self.heartbeatLogger];
  return operation;
}

// We really have this method so that we can more easily stub it out for unit testing
- (FIRMessagingTokenDeleteOperation *)
    createDeleteOperationWithAuthorizedEntity:(NSString *)authorizedEntity
                                        scope:(NSString *)scope
                           checkinPreferences:(FIRMessagingCheckinPreferences *)checkinPreferences
                                   instanceID:(NSString *)instanceID
                                       action:(FIRMessagingTokenAction)action {
  FIRMessagingTokenDeleteOperation *operation =
      [[FIRMessagingTokenDeleteOperation alloc] initWithAuthorizedEntity:authorizedEntity
                                                                   scope:scope
                                                      checkinPreferences:checkinPreferences
                                                              instanceID:instanceID
                                                                  action:action
                                                         heartbeatLogger:self.heartbeatLogger];
  return operation;
}

#pragma mark - Invalidating Cached Tokens
- (BOOL)checkTokenRefreshPolicyWithIID:(NSString *)IID {
  // We know at least one cached token exists.
  BOOL shouldFetchDefaultToken = NO;
  NSArray<FIRMessagingTokenInfo *> *tokenInfos = [_tokenStore cachedTokenInfos];

  NSMutableArray<FIRMessagingTokenInfo *> *tokenInfosToDelete =
      [NSMutableArray arrayWithCapacity:tokenInfos.count];
  for (FIRMessagingTokenInfo *tokenInfo in tokenInfos) {
    if ([tokenInfo isFreshWithIID:IID]) {
      // Token is fresh and in right format, do nothing
      continue;
    }
    if ([tokenInfo isDefaultToken]) {
      // Default token is expired, do not mark for deletion. Fetch directly from server to
      // replace the current one.
      shouldFetchDefaultToken = YES;
    } else {
      // Non-default token is expired, mark for deletion.
      [tokenInfosToDelete addObject:tokenInfo];
    }
    FIRMessagingLoggerDebug(
        kFIRMessagingMessageCodeTokenManagerInvalidateStaleToken,
        @"Invalidating cached token for %@ (%@) due to token is no longer fresh.",
        tokenInfo.authorizedEntity, tokenInfo.scope);
  }
  for (FIRMessagingTokenInfo *tokenInfoToDelete in tokenInfosToDelete) {
    [_tokenStore removeTokenWithAuthorizedEntity:tokenInfoToDelete.authorizedEntity
                                           scope:tokenInfoToDelete.scope];
  }
  return shouldFetchDefaultToken;
}

- (NSArray<FIRMessagingTokenInfo *> *)updateTokensToAPNSDeviceToken:(NSData *)deviceToken
                                                          isSandbox:(BOOL)isSandbox {
  // Each cached IID token that is missing an APNSInfo, or has an APNSInfo associated should be
  // checked and invalidated if needed.
  FIRMessagingAPNSInfo *APNSInfo = [[FIRMessagingAPNSInfo alloc] initWithDeviceToken:deviceToken
                                                                           isSandbox:isSandbox];
  if ([self.currentAPNSInfo isEqualToAPNSInfo:APNSInfo]) {
    return @[];
  }
  self.currentAPNSInfo = APNSInfo;

  NSArray<FIRMessagingTokenInfo *> *tokenInfos = [_tokenStore cachedTokenInfos];
  NSMutableArray<FIRMessagingTokenInfo *> *tokenInfosToDelete =
      [NSMutableArray arrayWithCapacity:tokenInfos.count];
  for (FIRMessagingTokenInfo *cachedTokenInfo in tokenInfos) {
    // Check if the cached APNSInfo is nil, or if it is an old APNSInfo.
    if (!cachedTokenInfo.APNSInfo ||
        ![cachedTokenInfo.APNSInfo isEqualToAPNSInfo:self.currentAPNSInfo]) {
      // Mark for invalidation.
      [tokenInfosToDelete addObject:cachedTokenInfo];
    }
  }
  for (FIRMessagingTokenInfo *tokenInfoToDelete in tokenInfosToDelete) {
    FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenManagerAPNSChangedTokenInvalidated,
                            @"Invalidating cached token for %@ (%@) due to APNs token change.",
                            tokenInfoToDelete.authorizedEntity, tokenInfoToDelete.scope);
    [_tokenStore removeTokenWithAuthorizedEntity:tokenInfoToDelete.authorizedEntity
                                           scope:tokenInfoToDelete.scope];
  }
  return tokenInfosToDelete;
}

#pragma mark - APNS Token
- (void)setAPNSToken:(NSData *)APNSToken withUserInfo:(NSDictionary *)userInfo {
  if (!APNSToken || ![APNSToken isKindOfClass:[NSData class]]) {
    if ([APNSToken class]) {
      FIRMessagingLoggerDebug(kFIRMessagingMessageCodeInternal002, @"Invalid APNS token type %@",
                              NSStringFromClass([APNSToken class]));
    } else {
      FIRMessagingLoggerDebug(kFIRMessagingMessageCodeInternal002, @"Empty APNS token type");
    }
    return;
  }

  // The APNS token is being added, or has changed (rare)
  if ([self.currentAPNSInfo.deviceToken isEqualToData:APNSToken]) {
    FIRMessagingLoggerDebug(kFIRMessagingMessageCodeInstanceID011,
                            @"Trying to reset APNS token to the same value. Will return");
    return;
  }
  // Use this token type for when we have to automatically fetch tokens in the future
#if TARGET_OS_SIMULATOR
  // If APNS token is available on iOS Simulator, we must use the sandbox profile
  // https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes
  BOOL isSandboxApp = YES;
#else   // TARGET_OS_SIMULATOR
  NSInteger type = [userInfo[kFIRMessagingAPNSTokenType] integerValue];
  BOOL isSandboxApp = (type == FIRMessagingAPNSTokenTypeSandbox);
  if (type == FIRMessagingAPNSTokenTypeUnknown) {
    isSandboxApp = FIRMessagingIsSandboxApp();
  }
#endif  // TARGET_OS_SIMULATOR

  // Pro-actively invalidate the default token, if the APNs change makes it
  // invalid. Previously, we invalidated just before fetching the token.
  NSArray<FIRMessagingTokenInfo *> *invalidatedTokens =
      [self updateTokensToAPNSDeviceToken:APNSToken isSandbox:isSandboxApp];

  self.currentAPNSInfo = [[FIRMessagingAPNSInfo alloc] initWithDeviceToken:[APNSToken copy]
                                                                 isSandbox:isSandboxApp];

  // Re-fetch any invalidated tokens automatically, this time with the current APNs token, so that
  // they are up-to-date. Or this is a fresh install and no apns token stored yet.
  if (invalidatedTokens.count > 0 || [_tokenStore cachedTokenInfos].count == 0) {
    FIRMessaging_WEAKIFY(self);

    [self.installations installationIDWithCompletion:^(NSString *_Nullable identifier,
                                                       NSError *_Nullable error) {
      FIRMessaging_STRONGIFY(self);
      if (self == nil) {
        FIRMessagingLoggerError(kFIRMessagingMessageCodeInstanceID017,
                                @"Instance ID shut down during token reset. Aborting");
        return;
      }
      if (self.currentAPNSInfo == nil) {
        FIRMessagingLoggerError(kFIRMessagingMessageCodeInstanceID018,
                                @"apnsTokenData was set to nil during token reset. Aborting");
        return;
      }

      NSMutableDictionary *tokenOptions = [@{
        kFIRMessagingTokenOptionsAPNSKey : self.currentAPNSInfo.deviceToken,
        kFIRMessagingTokenOptionsAPNSIsSandboxKey : @(isSandboxApp)
      } mutableCopy];
      if (self.firebaseAppID) {
        tokenOptions[kFIRMessagingTokenOptionsFirebaseAppIDKey] = self.firebaseAppID;
      }

      for (FIRMessagingTokenInfo *tokenInfo in invalidatedTokens) {
        [self fetchNewTokenWithAuthorizedEntity:tokenInfo.authorizedEntity
                                          scope:tokenInfo.scope
                                     instanceID:identifier
                                        options:tokenOptions
                                        handler:^(NSString *_Nullable token,
                                                  NSError *_Nullable error){
                                            // Do nothing as callback is not needed and the
                                            // sub-function already handle errors.
                                        }];
      }
      if ([self->_tokenStore cachedTokenInfos].count == 0) {
        [self tokenWithAuthorizedEntity:self.fcmSenderID
                                  scope:kFIRMessagingDefaultTokenScope
                                options:tokenOptions
                                handler:^(NSString *_Nullable FCMToken, NSError *_Nullable error){
                                    // Do nothing as callback is not needed and the sub-function
                                    // already handle errors.
                                }];
      }
    }];
  }
}

#pragma mark - checkin
- (BOOL)hasValidCheckinInfo {
  return self.authService.checkinPreferences.hasValidCheckinInfo;
}

@end
