/*
 * 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 "FIRInstanceID.h"

#import <FirebaseCore/FIRAppInternal.h>
#import <FirebaseCore/FIRComponent.h>
#import <FirebaseCore/FIRComponentContainer.h>
#import <FirebaseCore/FIRLibrary.h>
#import <FirebaseCore/FIROptions.h>
#import <GoogleUtilities/GULAppEnvironmentUtil.h>
#import <GoogleUtilities/GULUserDefaults.h>
#import "FIRInstanceID+Private.h"
#import "FIRInstanceIDAuthService.h"
#import "FIRInstanceIDCheckinPreferences.h"
#import "FIRInstanceIDCombinedHandler.h"
#import "FIRInstanceIDConstants.h"
#import "FIRInstanceIDDefines.h"
#import "FIRInstanceIDKeyPairStore.h"
#import "FIRInstanceIDLogger.h"
#import "FIRInstanceIDStore.h"
#import "FIRInstanceIDTokenInfo.h"
#import "FIRInstanceIDTokenManager.h"
#import "FIRInstanceIDUtilities.h"
#import "FIRInstanceIDVersionUtilities.h"
#import "NSError+FIRInstanceID.h"

// Public constants
NSString *const kFIRInstanceIDScopeFirebaseMessaging = @"fcm";

#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
const NSNotificationName kFIRInstanceIDTokenRefreshNotification =
    @"com.firebase.iid.notif.refresh-token";
#else
NSString *const kFIRInstanceIDTokenRefreshNotification = @"com.firebase.iid.notif.refresh-token";
#endif  // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0

NSString *const kFIRInstanceIDInvalidNilHandlerError = @"Invalid nil handler.";

// Private constants
int64_t const kMaxRetryIntervalForDefaultTokenInSeconds = 20 * 60;  // 20 minutes
int64_t const kMinRetryIntervalForDefaultTokenInSeconds = 10;       // 10 seconds
// we retry only a max 5 times.
// TODO(chliangGoogle): If we still fail we should listen for the network change notification
// since GCM would have started Reachability. We only start retrying after we see a configuration
// change.
NSInteger const kMaxRetryCountForDefaultToken = 5;

#if TARGET_OS_IOS || TARGET_OS_TV
static NSString *const kEntitlementsAPSEnvironmentKey = @"Entitlements.aps-environment";
#else
static NSString *const kEntitlementsAPSEnvironmentKey = @"com.apple.developer.aps-environment";
#endif
static NSString *const kEntitlementsKeyForMac = @"Entitlements";
static NSString *const kAPSEnvironmentDevelopmentValue = @"development";
/// FIRMessaging selector that returns the current FIRMessaging auto init
/// enabled flag.
static NSString *const kFIRInstanceIDFCMSelectorAutoInitEnabled =
    @"isAutoInitEnabledWithUserDefaults:";

static NSString *const kFIRInstanceIDAPNSTokenType = @"APNSTokenType";
static NSString *const kFIRIIDAppReadyToConfigureSDKNotification =
    @"FIRAppReadyToConfigureSDKNotification";
static NSString *const kFIRIIDAppNameKey = @"FIRAppNameKey";
static NSString *const kFIRIIDErrorDomain = @"com.firebase.instanceid";
static NSString *const kFIRIIDServiceInstanceID = @"InstanceID";

typedef void (^FIRInstanceIDKeyPairHandler)(FIRInstanceIDKeyPair *keyPair, NSError *error);

/**
 *  The APNS token type for the app. If the token type is set to `UNKNOWN`
 *  InstanceID will implicitly try to figure out what the actual token type
 *  is from the provisioning profile.
 *  This must match FIRMessagingAPNSTokenType in FIRMessaging.h
 */
typedef NS_ENUM(NSInteger, FIRInstanceIDAPNSTokenType) {
  /// Unknown token type.
  FIRInstanceIDAPNSTokenTypeUnknown,
  /// Sandbox token type.
  FIRInstanceIDAPNSTokenTypeSandbox,
  /// Production token type.
  FIRInstanceIDAPNSTokenTypeProd,
} NS_SWIFT_NAME(InstanceIDAPNSTokenType);

@interface FIRInstanceIDResult ()
@property(nonatomic, readwrite, copy) NSString *instanceID;
@property(nonatomic, readwrite, copy) NSString *token;
@end

@interface FIRInstanceID ()

// FIRApp configuration objects.
@property(nonatomic, readwrite, copy) NSString *fcmSenderID;
@property(nonatomic, readwrite, copy) NSString *firebaseAppID;

// Raw APNS token data
@property(nonatomic, readwrite, strong) NSData *apnsTokenData;

@property(nonatomic, readwrite) FIRInstanceIDAPNSTokenType apnsTokenType;
// String-based, internal representation of APNS token
@property(nonatomic, readwrite, copy) NSString *APNSTupleString;
// Token fetched from the server automatically for the default app.
@property(nonatomic, readwrite, copy) NSString *defaultFCMToken;

@property(nonatomic, readwrite, strong) FIRInstanceIDTokenManager *tokenManager;
@property(nonatomic, readwrite, strong) FIRInstanceIDKeyPairStore *keyPairStore;

// backoff and retry for default token
@property(nonatomic, readwrite, assign) NSInteger retryCountForDefaultToken;
@property(atomic, strong, nullable)
    FIRInstanceIDCombinedHandler<NSString *> *defaultTokenFetchHandler;

@end

// InstanceID doesn't provide any functionality to other components,
// so it provides a private, empty protocol that it conforms to and use it for registration.

@protocol FIRInstanceIDInstanceProvider
@end

@interface FIRInstanceID () <FIRInstanceIDInstanceProvider, FIRLibrary>
@end

@implementation FIRInstanceIDResult
- (id)copyWithZone:(NSZone *)zone {
  FIRInstanceIDResult *result = [[[self class] allocWithZone:zone] init];
  result.instanceID = self.instanceID;
  result.token = self.token;
  return result;
}
@end

@implementation FIRInstanceID

// File static to support InstanceID tests that call [FIRInstanceID instanceID] after
// [FIRInstanceID instanceIDForTests].
static FIRInstanceID *gInstanceID;

+ (instancetype)instanceID {
  // If the static instance was created, return it. This should only be set in tests and we should
  // eventually use proper dependency injection for a better test structure.
  if (gInstanceID != nil) {
    return gInstanceID;
  }
  FIRApp *defaultApp = [FIRApp defaultApp];  // Missing configure will be logged here.
  FIRInstanceID *instanceID =
      (FIRInstanceID *)FIR_COMPONENT(FIRInstanceIDInstanceProvider, defaultApp.container);
  return instanceID;
}

- (instancetype)initPrivately {
  self = [super init];
  if (self != nil) {
    // Use automatic detection of sandbox, unless otherwise set by developer
    _apnsTokenType = FIRInstanceIDAPNSTokenTypeUnknown;
  }
  return self;
}

+ (FIRInstanceID *)instanceIDForTests {
  gInstanceID = [[FIRInstanceID alloc] initPrivately];
  [gInstanceID start];
  return gInstanceID;
}

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

#pragma mark - Tokens

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

  NSString *cachedToken = [self cachedTokenIfAvailable];

  if (cachedToken) {
    return cachedToken;
  } else {
    // If we've never had a cached default token, we should fetch one because unrelatedly,
    // this request will help us determine whether the locally-generated Instance ID keypair is not
    // unique, and therefore generate a new one.
    [self defaultTokenWithHandler:nil];
    return nil;
  }
}

- (void)instanceIDWithHandler:(FIRInstanceIDResultHandler)handler {
  FIRInstanceID_WEAKIFY(self);
  [self getIDWithHandler:^(NSString *identity, NSError *error) {
    FIRInstanceID_STRONGIFY(self);
    // This is in main queue already
    if (error) {
      if (handler) {
        handler(nil, error);
      }
      return;
    }
    FIRInstanceIDResult *result = [[FIRInstanceIDResult alloc] init];
    result.instanceID = identity;
    NSString *cachedToken = [self cachedTokenIfAvailable];
    if (cachedToken) {
      if (handler) {
        result.token = cachedToken;
        handler(result, nil);
      }
      // If no handler, simply return since client has generated iid and token.
      return;
    }
    [self defaultTokenWithHandler:^(NSString *_Nullable token, NSError *_Nullable error) {
      if (handler) {
        if (error) {
          handler(nil, error);
          return;
        }
        result.token = token;
        handler(result, nil);
      }
    }];
  }];
}

- (NSString *)cachedTokenIfAvailable {
  FIRInstanceIDTokenInfo *cachedTokenInfo =
      [self.tokenManager cachedTokenInfoWithAuthorizedEntity:self.fcmSenderID
                                                       scope:kFIRInstanceIDDefaultTokenScope];
  return cachedTokenInfo.token;
}

- (void)setDefaultFCMToken:(NSString *)defaultFCMToken {
  if (_defaultFCMToken && defaultFCMToken && [defaultFCMToken isEqualToString:_defaultFCMToken]) {
    return;
  }

  _defaultFCMToken = defaultFCMToken;

  // Sending this notification out will ensure that FIRMessaging has the updated
  // default FCM token.
  NSNotification *internalDefaultTokenNotification =
      [NSNotification notificationWithName:kFIRInstanceIDDefaultGCMTokenNotification
                                    object:_defaultFCMToken];
  [[NSNotificationQueue defaultQueue] enqueueNotification:internalDefaultTokenNotification
                                             postingStyle:NSPostASAP];
}

- (void)tokenWithAuthorizedEntity:(NSString *)authorizedEntity
                            scope:(NSString *)scope
                          options:(NSDictionary *)options
                          handler:(FIRInstanceIDTokenHandler)handler {
  if (!handler) {
    FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeInstanceID000,
                             kFIRInstanceIDInvalidNilHandlerError);
    return;
  }

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

  NSString *APNSKey = kFIRInstanceIDTokenOptionsAPNSKey;
  NSString *serverTypeKey = kFIRInstanceIDTokenOptionsAPNSIsSandboxKey;

  if (tokenOptions[APNSKey] != nil && tokenOptions[serverTypeKey] == 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[serverTypeKey] = @([self isSandboxApp]);
  }

  // comparing enums to ints directly throws a warning
  FIRInstanceIDErrorCode noError = INT_MAX;
  FIRInstanceIDErrorCode errorCode = noError;
  if (FIRInstanceIDIsValidGCMScope(scope) && !tokenOptions[APNSKey]) {
    errorCode = kFIRInstanceIDErrorCodeMissingAPNSToken;
  } else if (FIRInstanceIDIsValidGCMScope(scope) &&
             ![tokenOptions[APNSKey] isKindOfClass:[NSData class]]) {
    errorCode = kFIRInstanceIDErrorCodeInvalidRequest;
  } else if (![authorizedEntity length]) {
    errorCode = kFIRInstanceIDErrorCodeInvalidAuthorizedEntity;
  } else if (![scope length]) {
    errorCode = kFIRInstanceIDErrorCodeInvalidScope;
  } else if (!self.keyPairStore) {
    errorCode = kFIRInstanceIDErrorCodeInvalidStart;
  }

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

  if (errorCode != noError) {
    newHandler(nil, [NSError errorWithFIRInstanceIDErrorCode:errorCode]);
    return;
  }

  // TODO(chliangGoogle): Add some validation logic that the APNs token data and sandbox value are
  // supplied in the valid format (NSData and BOOL, respectively).

  // Add internal options
  if (self.firebaseAppID) {
    tokenOptions[kFIRInstanceIDTokenOptionsFirebaseAppIDKey] = self.firebaseAppID;
  }

  FIRInstanceID_WEAKIFY(self);
  FIRInstanceIDAuthService *authService = self.tokenManager.authService;
  [authService
      fetchCheckinInfoWithHandler:^(FIRInstanceIDCheckinPreferences *preferences, NSError *error) {
        FIRInstanceID_STRONGIFY(self);
        if (error) {
          newHandler(nil, error);
          return;
        }

        // Only use the token in the cache if the APNSInfo matches what the request's options has.
        // It's possible for the request to be with a newer APNs device token, which should be
        // honored.
        FIRInstanceIDTokenInfo *cachedTokenInfo =
            [self.tokenManager cachedTokenInfoWithAuthorizedEntity:authorizedEntity scope:scope];
        if (cachedTokenInfo) {
          // Ensure that the cached token matches APNs data before returning it.
          FIRInstanceIDAPNSInfo *optionsAPNSInfo =
              [[FIRInstanceIDAPNSInfo alloc] initWithTokenOptionsDictionary:tokenOptions];
          // If either the APNs info is missing in both, or if they are an exact match, then we can
          // use this cached token.
          if ((!cachedTokenInfo.APNSInfo && !optionsAPNSInfo) ||
              [cachedTokenInfo.APNSInfo isEqualToAPNSInfo:optionsAPNSInfo]) {
            newHandler(cachedTokenInfo.token, nil);
            return;
          }
        }

        FIRInstanceID_WEAKIFY(self);
        [self asyncLoadKeyPairWithHandler:^(FIRInstanceIDKeyPair *keyPair, NSError *error) {
          FIRInstanceID_STRONGIFY(self);

          if (error) {
            NSError *newError =
                [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidKeyPair];
            newHandler(nil, newError);

          } else {
            [self.tokenManager fetchNewTokenWithAuthorizedEntity:[authorizedEntity copy]
                                                           scope:[scope copy]
                                                         keyPair:keyPair
                                                         options:tokenOptions
                                                         handler:newHandler];
          }
        }];
      }];
}

- (void)deleteTokenWithAuthorizedEntity:(NSString *)authorizedEntity
                                  scope:(NSString *)scope
                                handler:(FIRInstanceIDDeleteTokenHandler)handler {
  if (!handler) {
    FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeInstanceID001,
                             kFIRInstanceIDInvalidNilHandlerError);
  }

  // comparing enums to ints directly throws a warning
  FIRInstanceIDErrorCode noError = INT_MAX;
  FIRInstanceIDErrorCode errorCode = noError;

  if (![authorizedEntity length]) {
    errorCode = kFIRInstanceIDErrorCodeInvalidAuthorizedEntity;
  } else if (![scope length]) {
    errorCode = kFIRInstanceIDErrorCodeInvalidScope;
  } else if (!self.keyPairStore) {
    errorCode = kFIRInstanceIDErrorCodeInvalidStart;
  }

  FIRInstanceIDDeleteTokenHandler newHandler = ^(NSError *error) {
    // If a default token is deleted successfully, reset the defaultFCMToken too.
    if (!error && [authorizedEntity isEqualToString:self.fcmSenderID] &&
        [scope isEqualToString:kFIRInstanceIDDefaultTokenScope]) {
      self.defaultFCMToken = nil;
    }
    dispatch_async(dispatch_get_main_queue(), ^{
      handler(error);
    });
  };

  if (errorCode != noError) {
    newHandler([NSError errorWithFIRInstanceIDErrorCode:errorCode]);
    return;
  }

  FIRInstanceID_WEAKIFY(self);
  FIRInstanceIDAuthService *authService = self.tokenManager.authService;
  [authService
      fetchCheckinInfoWithHandler:^(FIRInstanceIDCheckinPreferences *preferences, NSError *error) {
        FIRInstanceID_STRONGIFY(self);
        if (error) {
          newHandler(error);
          return;
        }

        FIRInstanceID_WEAKIFY(self);
        [self asyncLoadKeyPairWithHandler:^(FIRInstanceIDKeyPair *keyPair, NSError *error) {
          FIRInstanceID_STRONGIFY(self);
          if (error) {
            NSError *newError =
                [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidKeyPair];
            newHandler(newError);

          } else {
            [self.tokenManager deleteTokenWithAuthorizedEntity:authorizedEntity
                                                         scope:scope
                                                       keyPair:keyPair
                                                       handler:newHandler];
          }
        }];
      }];
}

- (void)asyncLoadKeyPairWithHandler:(FIRInstanceIDKeyPairHandler)handler {
  FIRInstanceID_WEAKIFY(self);
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    FIRInstanceID_STRONGIFY(self);

    NSError *error = nil;
    FIRInstanceIDKeyPair *keyPair = [self.keyPairStore loadKeyPairWithError:&error];
    dispatch_async(dispatch_get_main_queue(), ^{
      if (error) {
        FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeInstanceID002,
                                 @"Failed to retreieve keyPair %@", error);
        if (handler) {
          handler(nil, error);
        }
      } else if (!keyPair && !error) {
        if (handler) {
          handler(nil,
                  [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidKeyPair]);
        }
      } else {
        if (handler) {
          handler(keyPair, nil);
        }
      }
    });
  });
}

#pragma mark - Identity

- (void)getIDWithHandler:(FIRInstanceIDHandler)handler {
  if (!handler) {
    FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeInstanceID003,
                             kFIRInstanceIDInvalidNilHandlerError);
    return;
  }

  void (^callHandlerOnMainThread)(NSString *, NSError *) = ^(NSString *identity, NSError *error) {
    dispatch_async(dispatch_get_main_queue(), ^{
      handler(identity, error);
    });
  };

  if (!self.keyPairStore) {
    NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidStart];
    callHandlerOnMainThread(nil, error);
    return;
  }

  FIRInstanceID_WEAKIFY(self);
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    FIRInstanceID_STRONGIFY(self);
    NSError *error;
    NSString *appIdentity = [self.keyPairStore appIdentityWithError:&error];
    // When getID is explicitly called, trigger getToken to make sure token always exists.
    // This is to avoid ID conflict (ID is not checked for conflict until we generate a token)
    if (appIdentity) {
      [self token];
    }
    callHandlerOnMainThread(appIdentity, error);
  });
}

- (void)deleteIDWithHandler:(FIRInstanceIDDeleteHandler)handler {
  if (!handler) {
    FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeInstanceID004,
                             kFIRInstanceIDInvalidNilHandlerError);
    return;
  }

  void (^callHandlerOnMainThread)(NSError *) = ^(NSError *error) {
    if ([NSThread isMainThread]) {
      handler(error);
      return;
    }
    dispatch_async(dispatch_get_main_queue(), ^{
      handler(error);
    });
  };

  if (!self.keyPairStore) {
    FIRInstanceIDErrorCode error = kFIRInstanceIDErrorCodeInvalidStart;
    callHandlerOnMainThread([NSError errorWithFIRInstanceIDErrorCode:error]);
    return;
  }

  FIRInstanceID_WEAKIFY(self);
  void (^deleteTokensHandler)(NSError *) = ^void(NSError *error) {
    FIRInstanceID_STRONGIFY(self);
    if (error) {
      callHandlerOnMainThread(error);
      return;
    }
    [self deleteIdentityWithHandler:^(NSError *error) {
      callHandlerOnMainThread(error);
    }];
  };

  [self asyncLoadKeyPairWithHandler:^(FIRInstanceIDKeyPair *keyPair, NSError *error) {
    FIRInstanceID_STRONGIFY(self);
    if (error) {
      NSError *newError =
          [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidKeyPair];
      callHandlerOnMainThread(newError);
    } else {
      [self.tokenManager deleteAllTokensWithKeyPair:keyPair handler:deleteTokensHandler];
    }
  }];
}

- (void)notifyIdentityReset {
  [self deleteIdentityWithHandler:nil];
}

// Delete all the local cache checkin, IID and token.
- (void)deleteIdentityWithHandler:(FIRInstanceIDDeleteHandler)handler {
  // Delete tokens.
  [self.tokenManager deleteAllTokensLocallyWithHandler:^(NSError *deleteTokenError) {
    // Reset FCM token.
    self.defaultFCMToken = nil;
    if (deleteTokenError) {
      if (handler) {
        handler(deleteTokenError);
      }
      return;
    }

    // Delete Instance ID.
    [self.keyPairStore
        deleteSavedKeyPairWithSubtype:kFIRInstanceIDKeyPairSubType
                              handler:^(NSError *error) {
                                NSError *deletePlistError;
                                [self.keyPairStore
                                    removeKeyPairCreationTimePlistWithError:&deletePlistError];
                                if (error || deletePlistError) {
                                  if (handler) {
                                    // Prefer to use the delete Instance ID error.
                                    error = [NSError
                                        errorWithFIRInstanceIDErrorCode:
                                            kFIRInstanceIDErrorCodeUnknown
                                                               userInfo:@{
                                                                 NSUnderlyingErrorKey : error
                                                                     ? error
                                                                     : deletePlistError
                                                               }];
                                    handler(error);
                                  }
                                  return;
                                }
                                // Delete checkin.
                                [self.tokenManager.authService
                                    resetCheckinWithHandler:^(NSError *error) {
                                      if (error) {
                                        if (handler) {
                                          handler(error);
                                        }
                                        return;
                                      }
                                      // Only request new token if FCM auto initialization is
                                      // enabled.
                                      if ([self isFCMAutoInitEnabled]) {
                                        // Deletion succeeds! Requesting new checkin, IID and token.
                                        // TODO(chliangGoogle) see if dispatch_after is necessary
                                        dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
                                                                     (int64_t)(0.5 * NSEC_PER_SEC)),
                                                       dispatch_get_main_queue(), ^{
                                                         [self defaultTokenWithHandler:nil];
                                                       });
                                      }
                                      if (handler) {
                                        handler(nil);
                                      }
                                    }];
                              }];
  }];
}

#pragma mark - Checkin

- (BOOL)tryToLoadValidCheckinInfo {
  FIRInstanceIDCheckinPreferences *checkinPreferences =
      [self.tokenManager.authService checkinPreferences];
  return [checkinPreferences hasValidCheckinInfo];
}

- (NSString *)deviceAuthID {
  return [self.tokenManager.authService checkinPreferences].deviceID;
}

- (NSString *)secretToken {
  return [self.tokenManager.authService checkinPreferences].secretToken;
}

- (NSString *)versionInfo {
  return [self.tokenManager.authService checkinPreferences].versionInfo;
}

#pragma mark - Config

+ (void)load {
  [FIRApp registerInternalLibrary:(Class<FIRLibrary>)self
                         withName:@"fire-iid"
                      withVersion:FIRInstanceIDCurrentLibraryVersion()];
}

+ (nonnull NSArray<FIRComponent *> *)componentsToRegister {
  FIRComponentCreationBlock creationBlock =
      ^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
    // InstanceID only works with the default app.
    if (!container.app.isDefaultApp) {
      // Only configure for the default FIRApp.
      FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeFIRApp002,
                               @"Firebase Instance ID only works with the default app.");
      return nil;
    }

    // Ensure it's cached so it returns the same instance every time instanceID is called.
    *isCacheable = YES;
    FIRInstanceID *instanceID = [[FIRInstanceID alloc] initPrivately];
    [instanceID start];
    [instanceID configureInstanceIDWithOptions:container.app.options];
    return instanceID;
  };
  FIRComponent *instanceIDProvider =
      [FIRComponent componentWithProtocol:@protocol(FIRInstanceIDInstanceProvider)
                      instantiationTiming:FIRInstantiationTimingEagerInDefaultApp
                             dependencies:@[]
                            creationBlock:creationBlock];
  return @[ instanceIDProvider ];
}

- (void)configureInstanceIDWithOptions:(FIROptions *)options {
  NSString *GCMSenderID = options.GCMSenderID;
  if (!GCMSenderID.length) {
    FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeFIRApp000,
                             @"Firebase not set up correctly, nil or empty senderID.");
    [NSException raise:kFIRIIDErrorDomain
                format:@"Could not configure Firebase InstanceID. GCMSenderID must not be nil or "
                       @"empty."];
  }

  self.fcmSenderID = GCMSenderID;
  self.firebaseAppID = options.googleAppID;

  // FCM generates a FCM token during app start for sending push notification to device.
  // This is not needed for app extension.
  if (![GULAppEnvironmentUtil isAppExtension]) {
    [self didCompleteConfigure];
  }
}

// This is used to start any operations when we receive FirebaseSDK setup notification
// from FIRCore.
- (void)didCompleteConfigure {
  NSString *cachedToken = [self cachedTokenIfAvailable];
  // When there is a cached token, do the token refresh.
  if (cachedToken) {
    // Clean up expired tokens by checking the token refresh policy.
    NSError *error;
    NSString *cachedIID = [self.keyPairStore appIdentityWithError:&error];
    if ([self.tokenManager checkTokenRefreshPolicyWithIID:cachedIID]) {
      // Default token is expired, fetch default token from server.
      [self defaultTokenWithHandler:nil];
    }
    // Notify FCM with the default token.
    self.defaultFCMToken = [self token];
  } else if ([self isFCMAutoInitEnabled]) {
    // When there is no cached token, must check auto init is enabled.
    // If it's disabled, don't initiate token generation/refresh.
    // If no cache token and auto init is enabled, fetch a token from server.
    [self defaultTokenWithHandler:nil];
    // Notify FCM with the default token.
    self.defaultFCMToken = [self token];
  }
  // ONLY checkin when auto data collection is turned on.
  if ([self isFCMAutoInitEnabled]) {
    [self.tokenManager.authService scheduleCheckin:YES];
  }
}

- (BOOL)isFCMAutoInitEnabled {
  Class messagingClass = NSClassFromString(kFIRInstanceIDFCMSDKClassString);
  // Firebase Messaging is not installed, auto init should be disabled since it's for FCM.
  if (!messagingClass) {
    return NO;
  }

  // Messaging doesn't have the class method, auto init should be enabled since FCM exists.
  SEL autoInitSelector = NSSelectorFromString(kFIRInstanceIDFCMSelectorAutoInitEnabled);
  if (![messagingClass respondsToSelector:autoInitSelector]) {
    return YES;
  }

  // Get the autoInitEnabled class method.
  IMP isAutoInitEnabledIMP = [messagingClass methodForSelector:autoInitSelector];
  BOOL(*isAutoInitEnabled)
  (Class, SEL, GULUserDefaults *) = (BOOL(*)(id, SEL, GULUserDefaults *))isAutoInitEnabledIMP;

  // Check FCM's isAutoInitEnabled property.
  return isAutoInitEnabled(messagingClass, autoInitSelector,
                           [GULUserDefaults standardUserDefaults]);
}

// Actually makes InstanceID instantiate both the IID and Token-related subsystems.
- (void)start {
  if (![FIRInstanceIDStore hasSubDirectory:kFIRInstanceIDSubDirectoryName]) {
    [FIRInstanceIDStore createSubDirectory:kFIRInstanceIDSubDirectoryName];
  }

  [self setupTokenManager];
  [self setupKeyPairManager];
  [self setupNotificationListeners];
}

// Creates the token manager, which is used for fetching, caching, and retrieving tokens.
- (void)setupTokenManager {
  self.tokenManager = [[FIRInstanceIDTokenManager alloc] init];
}

// Creates a key pair manager, which stores the public/private keys needed to generate an
// application instance ID.
- (void)setupKeyPairManager {
  self.keyPairStore = [[FIRInstanceIDKeyPairStore alloc] init];
  if ([self.keyPairStore invalidateKeyPairsIfNeeded]) {
    // Reset tokens right away when keypair is deleted, otherwise async call can make first query
    // of token happens before reset old tokens during app start.
    // TODO(chliangGoogle): Delete all tokens on server too, using
    // deleteAllTokensWithKeyPair:handler:. This requires actually retrieving the invalid keypair
    // from Keychain, which is something that the key pair store does not currently do.
    [self.tokenManager deleteAllTokensLocallyWithHandler:nil];
  }
}

- (void)setupNotificationListeners {
  // To prevent double notifications remove observer from all events during setup.
  NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
  [center removeObserver:self];
  [center addObserver:self
             selector:@selector(notifyIdentityReset)
                 name:kFIRInstanceIDIdentityInvalidatedNotification
               object:nil];
  [center addObserver:self
             selector:@selector(notifyAPNSTokenIsSet:)
                 name:kFIRInstanceIDAPNSTokenNotification
               object:nil];
}

#pragma mark - Private Helpers
/// Maximum retry count to fetch the default token.
+ (int64_t)maxRetryCountForDefaultToken {
  return kMaxRetryCountForDefaultToken;
}

/// Minimum interval in seconds between retries to fetch the default token.
+ (int64_t)minIntervalForDefaultTokenRetry {
  return kMinRetryIntervalForDefaultTokenInSeconds;
}

/// Maximum retry interval between retries to fetch default token.
+ (int64_t)maxRetryIntervalForDefaultTokenInSeconds {
  return kMaxRetryIntervalForDefaultTokenInSeconds;
}

- (NSInteger)retryIntervalToFetchDefaultToken {
  if (self.retryCountForDefaultToken >= [[self class] maxRetryCountForDefaultToken]) {
    return (NSInteger)[[self class] maxRetryIntervalForDefaultTokenInSeconds];
  }
  // exponential backoff with a fixed initial retry time
  // 11s, 22s, 44s, 88s ...
  int64_t minInterval = [[self class] minIntervalForDefaultTokenRetry];
  return (NSInteger)MIN(
      (1 << self.retryCountForDefaultToken) + minInterval * self.retryCountForDefaultToken,
      kMaxRetryIntervalForDefaultTokenInSeconds);
}

- (void)defaultTokenWithHandler:(nullable FIRInstanceIDTokenHandler)aHandler {
  [self defaultTokenWithRetry:NO handler:aHandler];
}

/**
 * @param retry Indicates if the method is called to perform a retry after a failed attempt.
 * If `YES`, then actual token request will be performed even if `self.defaultTokenFetchHandler !=
 * nil`
 */
- (void)defaultTokenWithRetry:(BOOL)retry handler:(nullable FIRInstanceIDTokenHandler)aHandler {
  BOOL shouldPerformRequest = retry || self.defaultTokenFetchHandler == nil;

  if (!self.defaultTokenFetchHandler) {
    self.defaultTokenFetchHandler = [[FIRInstanceIDCombinedHandler<NSString *> alloc] init];
  }

  if (aHandler) {
    [self.defaultTokenFetchHandler addHandler:aHandler];
  }

  if (!shouldPerformRequest) {
    return;
  }

  NSDictionary *instanceIDOptions = @{};
  BOOL hasFirebaseMessaging = NSClassFromString(kFIRInstanceIDFCMSDKClassString) != nil;
  if (hasFirebaseMessaging && self.apnsTokenData) {
    BOOL isSandboxApp = (self.apnsTokenType == FIRInstanceIDAPNSTokenTypeSandbox);
    if (self.apnsTokenType == FIRInstanceIDAPNSTokenTypeUnknown) {
      isSandboxApp = [self isSandboxApp];
    }
    instanceIDOptions = @{
      kFIRInstanceIDTokenOptionsAPNSKey : self.apnsTokenData,
      kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @(isSandboxApp),
    };
  }

  FIRInstanceID_WEAKIFY(self);
  FIRInstanceIDTokenHandler newHandler = ^void(NSString *token, NSError *error) {
    FIRInstanceID_STRONGIFY(self);

    if (error) {
      FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeInstanceID009,
                               @"Failed to fetch default token %@", error);

      // This notification can be sent multiple times since we can't guarantee success at any point
      // of time.
      NSNotification *tokenFetchFailNotification =
          [NSNotification notificationWithName:kFIRInstanceIDDefaultGCMTokenFailNotification
                                        object:[error copy]];
      [[NSNotificationQueue defaultQueue] enqueueNotification:tokenFetchFailNotification
                                                 postingStyle:NSPostASAP];

      self.retryCountForDefaultToken = (NSInteger)MIN(self.retryCountForDefaultToken + 1,
                                                      [[self class] maxRetryCountForDefaultToken]);

      // Do not retry beyond the maximum limit.
      if (self.retryCountForDefaultToken < [[self class] maxRetryCountForDefaultToken]) {
        NSInteger retryInterval = [self retryIntervalToFetchDefaultToken];
        [self retryGetDefaultTokenAfter:retryInterval];
      } else {
        FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeInstanceID007,
                                 @"Failed to retrieve the default FCM token after %ld retries",
                                 (long)self.retryCountForDefaultToken);
        [self performDefaultTokenHandlerWithToken:nil error:error];
      }
    } else {
      // If somebody updated IID with APNS token while our initial request did not have it
      // set we need to update it on the server.
      NSData *deviceTokenInRequest = instanceIDOptions[kFIRInstanceIDTokenOptionsAPNSKey];
      BOOL isSandboxInRequest =
          [instanceIDOptions[kFIRInstanceIDTokenOptionsAPNSIsSandboxKey] boolValue];
      // Note that APNSTupleStringInRequest will be nil if deviceTokenInRequest is nil
      NSString *APNSTupleStringInRequest = FIRInstanceIDAPNSTupleStringForTokenAndServerType(
          deviceTokenInRequest, isSandboxInRequest);
      // If the APNs value either remained nil, or was the same non-nil value, the APNs value
      // did not change.
      BOOL APNSRemainedSameDuringFetch =
          (self.APNSTupleString == nil && APNSTupleStringInRequest == nil) ||
          ([self.APNSTupleString isEqualToString:APNSTupleStringInRequest]);
      if (!APNSRemainedSameDuringFetch && hasFirebaseMessaging) {
        // APNs value did change mid-fetch, so the token should be re-fetched with the current APNs
        // value.
        [self retryGetDefaultTokenAfter:0];
        FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeRefetchingTokenForAPNS,
                                 @"Received APNS token while fetching default token. "
                                 @"Refetching default token.");
        // Do not notify and handle completion handler since this is a retry.
        // Simply return.
        return;
      } else {
        FIRInstanceIDLoggerInfo(kFIRInstanceIDMessageCodeInstanceID010,
                                @"Successfully fetched default token.");
      }
      // Post the required notifications if somebody is waiting.
      FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeInstanceID008, @"Got default token %@",
                               token);
      NSString *previousFCMToken = self.defaultFCMToken;
      self.defaultFCMToken = token;

      // Only notify of token refresh if we have a new valid token that's different than before
      if (self.defaultFCMToken.length && ![self.defaultFCMToken isEqualToString:previousFCMToken]) {
        NSNotification *tokenRefreshNotification =
            [NSNotification notificationWithName:kFIRInstanceIDTokenRefreshNotification
                                          object:[self.defaultFCMToken copy]];
        [[NSNotificationQueue defaultQueue] enqueueNotification:tokenRefreshNotification
                                                   postingStyle:NSPostASAP];
      }

      [self performDefaultTokenHandlerWithToken:token error:nil];
    }
  };

  [self tokenWithAuthorizedEntity:self.fcmSenderID
                            scope:kFIRInstanceIDDefaultTokenScope
                          options:instanceIDOptions
                          handler:newHandler];
}

/**
 *
 */
- (void)performDefaultTokenHandlerWithToken:(NSString *)token error:(NSError *)error {
  if (!self.defaultTokenFetchHandler) {
    return;
  }

  [self.defaultTokenFetchHandler combinedHandler](token, error);
  self.defaultTokenFetchHandler = nil;
}

- (void)retryGetDefaultTokenAfter:(NSTimeInterval)retryInterval {
  FIRInstanceID_WEAKIFY(self);
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(retryInterval * NSEC_PER_SEC)),
                 dispatch_get_main_queue(), ^{
                   FIRInstanceID_STRONGIFY(self);
                   // Pass nil: no new handlers to be added, currently existing handlers
                   // will be called
                   [self defaultTokenWithRetry:YES handler:nil];
                 });
}

#pragma mark - APNS Token
// This should only be triggered from FCM.
- (void)notifyAPNSTokenIsSet:(NSNotification *)notification {
  NSData *token = notification.object;
  if (!token || ![token isKindOfClass:[NSData class]]) {
    FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeInternal002, @"Invalid APNS token type %@",
                             NSStringFromClass([notification.object class]));
    return;
  }
  NSInteger type = [notification.userInfo[kFIRInstanceIDAPNSTokenType] integerValue];

  // The APNS token is being added, or has changed (rare)
  if ([self.apnsTokenData isEqualToData:token]) {
    FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeInstanceID011,
                             @"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
  self.apnsTokenType = type;
  BOOL isSandboxApp = (type == FIRInstanceIDAPNSTokenTypeSandbox);
  if (self.apnsTokenType == FIRInstanceIDAPNSTokenTypeUnknown) {
    isSandboxApp = [self isSandboxApp];
  }
  self.apnsTokenData = [token copy];
  self.APNSTupleString = FIRInstanceIDAPNSTupleStringForTokenAndServerType(token, isSandboxApp);

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

  // Re-fetch any invalidated tokens automatically, this time with the current APNs token, so that
  // they are up-to-date.
  if (invalidatedTokens.count > 0) {
    FIRInstanceID_WEAKIFY(self);
    [self asyncLoadKeyPairWithHandler:^(FIRInstanceIDKeyPair *keyPair, NSError *error) {
      FIRInstanceID_STRONGIFY(self);
      if (self == nil) {
        FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeInstanceID017,
                                 @"Instance ID shut down during token reset. Aborting");
        return;
      }
      if (self.apnsTokenData == nil) {
        FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeInstanceID018,
                                 @"apnsTokenData was set to nil during token reset. Aborting");
        return;
      }

      NSMutableDictionary *tokenOptions = [@{
        kFIRInstanceIDTokenOptionsAPNSKey : self.apnsTokenData,
        kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @(isSandboxApp)
      } mutableCopy];
      if (self.firebaseAppID) {
        tokenOptions[kFIRInstanceIDTokenOptionsFirebaseAppIDKey] = self.firebaseAppID;
      }

      for (FIRInstanceIDTokenInfo *tokenInfo in invalidatedTokens) {
        if ([tokenInfo.token isEqualToString:self.defaultFCMToken]) {
          // We will perform a special fetch for the default FCM token, so that the delegate methods
          // are called. For all others, we will do an internal re-fetch.
          [self defaultTokenWithHandler:nil];
        } else {
          [self.tokenManager fetchNewTokenWithAuthorizedEntity:tokenInfo.authorizedEntity
                                                         scope:tokenInfo.scope
                                                       keyPair:keyPair
                                                       options:tokenOptions
                                                       handler:^(NSString *_Nullable token,
                                                                 NSError *_Nullable error){

                                                       }];
        }
      }
    }];
  }
}

- (BOOL)isSandboxApp {
  static BOOL isSandboxApp = YES;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    isSandboxApp = ![self isProductionApp];
  });
  return isSandboxApp;
}

- (BOOL)isProductionApp {
  const BOOL defaultAppTypeProd = YES;

  NSError *error = nil;
  if ([GULAppEnvironmentUtil isSimulator]) {
    [self logAPNSConfigurationError:@"Running InstanceID on a simulator doesn't have APNS. "
                                    @"Use prod profile by default."];
    return defaultAppTypeProd;
  }

  if ([GULAppEnvironmentUtil isFromAppStore]) {
    // Apps distributed via AppStore or TestFlight use the Production APNS certificates.
    return defaultAppTypeProd;
  }
#if TARGET_OS_IOS || TARGET_OS_TV
  NSString *path = [[[NSBundle mainBundle] bundlePath]
      stringByAppendingPathComponent:@"embedded.mobileprovision"];
#elif TARGET_OS_OSX
  NSString *path = [[[[NSBundle mainBundle] resourcePath] stringByDeletingLastPathComponent]
      stringByAppendingPathComponent:@"embedded.provisionprofile"];
#endif

  if ([GULAppEnvironmentUtil isAppStoreReceiptSandbox] && !path.length) {
    // Distributed via TestFlight
    return defaultAppTypeProd;
  }

  NSMutableData *profileData = [NSMutableData dataWithContentsOfFile:path options:0 error:&error];

  if (!profileData.length || error) {
    NSString *errorString =
        [NSString stringWithFormat:@"Error while reading embedded mobileprovision %@", error];
    [self logAPNSConfigurationError:errorString];
    return defaultAppTypeProd;
  }

  // The "embedded.mobileprovision" sometimes contains characters with value 0, which signals the
  // end of a c-string and halts the ASCII parser, or with value > 127, which violates strict 7-bit
  // ASCII. Replace any 0s or invalid characters in the input.
  uint8_t *profileBytes = (uint8_t *)profileData.bytes;
  for (int i = 0; i < profileData.length; i++) {
    uint8_t currentByte = profileBytes[i];
    if (!currentByte || currentByte > 127) {
      profileBytes[i] = '.';
    }
  }

  NSString *embeddedProfile = [[NSString alloc] initWithBytesNoCopy:profileBytes
                                                             length:profileData.length
                                                           encoding:NSASCIIStringEncoding
                                                       freeWhenDone:NO];

  if (error || !embeddedProfile.length) {
    NSString *errorString =
        [NSString stringWithFormat:@"Error while reading embedded mobileprovision %@", error];
    [self logAPNSConfigurationError:errorString];
    return defaultAppTypeProd;
  }

  NSScanner *scanner = [NSScanner scannerWithString:embeddedProfile];
  NSString *plistContents;
  if ([scanner scanUpToString:@"<plist" intoString:nil]) {
    if ([scanner scanUpToString:@"</plist>" intoString:&plistContents]) {
      plistContents = [plistContents stringByAppendingString:@"</plist>"];
    }
  }

  if (!plistContents.length) {
    return defaultAppTypeProd;
  }

  NSData *data = [plistContents dataUsingEncoding:NSUTF8StringEncoding];
  if (!data.length) {
    [self logAPNSConfigurationError:@"Couldn't read plist fetched from embedded mobileprovision"];
    return defaultAppTypeProd;
  }

  NSError *plistMapError;
  id plistData = [NSPropertyListSerialization propertyListWithData:data
                                                           options:NSPropertyListImmutable
                                                            format:nil
                                                             error:&plistMapError];
  if (plistMapError || ![plistData isKindOfClass:[NSDictionary class]]) {
    NSString *errorString =
        [NSString stringWithFormat:@"Error while converting assumed plist to dict %@",
                                   plistMapError.localizedDescription];
    [self logAPNSConfigurationError:errorString];
    return defaultAppTypeProd;
  }
  NSDictionary *plistMap = (NSDictionary *)plistData;

  if ([plistMap valueForKeyPath:@"ProvisionedDevices"]) {
    FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeInstanceID012,
                             @"Provisioning profile has specifically provisioned devices, "
                             @"most likely a Dev profile.");
  }

#if TARGET_OS_IOS || TARGET_OS_TV
  NSString *apsEnvironment = [plistMap valueForKeyPath:kEntitlementsAPSEnvironmentKey];
#elif TARGET_OS_OSX
  NSDictionary *entitlements = [plistMap valueForKey:kEntitlementsKeyForMac];
  NSString *apsEnvironment = [entitlements valueForKey:kEntitlementsAPSEnvironmentKey];
#endif

  NSString *debugString __unused =
      [NSString stringWithFormat:@"APNS Environment in profile: %@", apsEnvironment];
  FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeInstanceID013, @"%@", debugString);

  // No aps-environment in the profile.
  if (!apsEnvironment.length) {
    [self logAPNSConfigurationError:@"No aps-environment set. If testing on a device APNS is not "
                                    @"correctly configured. Please recheck your provisioning "
                                    @"profiles. If testing on a simulator this is fine since APNS "
                                    @"doesn't work on the simulator."];
    return defaultAppTypeProd;
  }

  if ([apsEnvironment isEqualToString:kAPSEnvironmentDevelopmentValue]) {
    return NO;
  }

  return defaultAppTypeProd;
}

/// Log error messages only when Messaging exists in the pod.
- (void)logAPNSConfigurationError:(NSString *)errorString {
  BOOL hasFirebaseMessaging = NSClassFromString(kFIRInstanceIDFCMSDKClassString) != nil;
  if (hasFirebaseMessaging) {
    FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeInstanceID014, @"%@", errorString);
  } else {
    FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeInstanceID015, @"%@", errorString);
  }
}

@end
