/*
 * Copyright 2017 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 <TargetConditionals.h>
#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION

#import <GoogleUtilities/GULUserDefaults.h>

#import "FirebaseCore/Extension/FirebaseCoreInternal.h"

#import "FirebaseInAppMessaging/Sources/Analytics/FIRIAMAnalyticsEventLoggerImpl.h"
#import "FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutHttpRequestSender.h"
#import "FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutLogStorage.h"
#import "FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.h"
#import "FirebaseInAppMessaging/Sources/FIRInAppMessagingPrivate.h"
#import "FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.h"
#import "FirebaseInAppMessaging/Sources/Private/Analytics/FIRIAMClearcutLogger.h"
#import "FirebaseInAppMessaging/Sources/Private/Analytics/FIRIAMClearcutUploader.h"
#import "FirebaseInAppMessaging/Sources/Private/Analytics/FIRIAMClientInfoFetcher.h"
#import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMFetchResponseParser.h"
#import "FirebaseInAppMessaging/Sources/Private/Flows/FIRIAMActivityLogger.h"
#import "FirebaseInAppMessaging/Sources/Private/Flows/FIRIAMBookKeeper.h"
#import "FirebaseInAppMessaging/Sources/Private/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.h"
#import "FirebaseInAppMessaging/Sources/Private/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.h"
#import "FirebaseInAppMessaging/Sources/Private/Flows/FIRIAMDisplayExecutor.h"
#import "FirebaseInAppMessaging/Sources/Private/Flows/FIRIAMFetchOnAppForegroundFlow.h"
#import "FirebaseInAppMessaging/Sources/Private/Flows/FIRIAMMessageClientCache.h"
#import "FirebaseInAppMessaging/Sources/Private/Flows/FIRIAMMsgFetcherUsingRestful.h"
#import "FirebaseInAppMessaging/Sources/Private/Runtime/FIRIAMRuntimeManager.h"
#import "FirebaseInAppMessaging/Sources/Private/Runtime/FIRIAMSDKModeManager.h"
#import "FirebaseInAppMessaging/Sources/Public/FirebaseInAppMessaging/FIRInAppMessaging.h"

// A enum indicating 3 different possibilities of a setting about auto data collection.
typedef NS_ENUM(NSInteger, FIRIAMAutoDataCollectionSetting) {
  // This indicates that the config is not explicitly set.
  FIRIAMAutoDataCollectionSettingNone = 0,

  // This indicates that the setting explicitly enables the auto data collection.
  FIRIAMAutoDataCollectionSettingEnabled = 1,

  // This indicates that the setting explicitly disables the auto data collection.
  FIRIAMAutoDataCollectionSettingDisabled = 2,
};

@interface FIRIAMRuntimeManager () <FIRIAMTestingModeListener>
@property(nonatomic, nonnull) FIRIAMMsgFetcherUsingRestful *restfulFetcher;
@property(nonatomic, nonnull) FIRIAMDisplayCheckOnAppForegroundFlow *displayOnAppForegroundFlow;
@property(nonatomic, nonnull) FIRIAMDisplayCheckOnFetchDoneNotificationFlow *displayOnFetchDoneFlow;
@property(nonatomic, nonnull)
    FIRIAMDisplayCheckOnAnalyticEventsFlow *displayOnFIRAnalyticEventsFlow;

@property(nonatomic, nonnull) FIRIAMFetchOnAppForegroundFlow *fetchOnAppForegroundFlow;
@property(nonatomic, nonnull) FIRIAMClientInfoFetcher *clientInfoFetcher;
@property(nonatomic, nonnull) FIRIAMFetchResponseParser *responseParser;
@end

static NSString *const _userDefaultsKeyForFIAMProgrammaticAutoDataCollectionSetting =
    @"firebase-iam-sdk-auto-data-collection";

@implementation FIRIAMRuntimeManager {
  // since we allow the SDK feature to be disabled/enabled at runtime, we need a field to track
  // its state on this
  BOOL _running;
}
+ (FIRIAMRuntimeManager *)getSDKRuntimeInstance {
  static FIRIAMRuntimeManager *managerInstance = nil;
  static dispatch_once_t onceToken;

  dispatch_once(&onceToken, ^{
    managerInstance = [[FIRIAMRuntimeManager alloc] init];
  });

  return managerInstance;
}

// For protocol FIRIAMTestingModeListener.
- (void)testingModeSwitchedOn {
  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180015",
              @"Dynamically switch to the display flow for testing mode instance.");

  [self.displayOnAppForegroundFlow stop];
  [self.displayOnFetchDoneFlow start];
}

- (FIRIAMAutoDataCollectionSetting)FIAMProgrammaticAutoDataCollectionSetting {
  id settingEntry = [[GULUserDefaults standardUserDefaults]
      objectForKey:_userDefaultsKeyForFIAMProgrammaticAutoDataCollectionSetting];

  if (![settingEntry isKindOfClass:[NSNumber class]]) {
    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180014",
                @"No auto data collection enable setting entry detected."
                 "So no FIAM programmatic setting from the app.");
    return FIRIAMAutoDataCollectionSettingNone;
  } else {
    if ([(NSNumber *)settingEntry boolValue]) {
      return FIRIAMAutoDataCollectionSettingEnabled;
    } else {
      return FIRIAMAutoDataCollectionSettingDisabled;
    }
  }
}

// the key for the plist entry to suppress auto start
static NSString *const kFirebaseInAppMessagingAutoDataCollectionKey =
    @"FirebaseInAppMessagingAutomaticDataCollectionEnabled";

- (FIRIAMAutoDataCollectionSetting)FIAMPlistAutoDataCollectionSetting {
  id fiamAutoDataCollectionPlistEntry = [[NSBundle mainBundle]
      objectForInfoDictionaryKey:kFirebaseInAppMessagingAutoDataCollectionKey];

  if ([fiamAutoDataCollectionPlistEntry isKindOfClass:[NSNumber class]]) {
    BOOL fiamDataCollectionEnabledPlistSetting =
        [(NSNumber *)fiamAutoDataCollectionPlistEntry boolValue];

    if (fiamDataCollectionEnabledPlistSetting) {
      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180011",
                  @"Auto data collection is explicitly enabled in FIAM plist entry.");
      return FIRIAMAutoDataCollectionSettingEnabled;
    } else {
      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180012",
                  @"Auto data collection is explicitly disabled in FIAM plist entry.");
      return FIRIAMAutoDataCollectionSettingDisabled;
    }
  } else {
    return FIRIAMAutoDataCollectionSettingNone;
  }
}

// Whether data collection is enabled by FIAM programmatic flag.
- (BOOL)automaticDataCollectionEnabled {
  return
      [self FIAMProgrammaticAutoDataCollectionSetting] != FIRIAMAutoDataCollectionSettingDisabled;
}

// Sets FIAM's programmatic flag for auto data collection.
- (void)setAutomaticDataCollectionEnabled:(BOOL)automaticDataCollectionEnabled {
  if (automaticDataCollectionEnabled) {
    [self resume];
  } else {
    [self pause];
  }
}

- (BOOL)shouldRunSDKFlowsOnStartup {
  // This can be controlled at 3 different levels in descending priority. If a higher-priority
  // setting exists, the lower level settings are ignored.
  //   1. Setting made by the app by setting FIAM SDK's automaticDataCollectionEnabled flag.
  //   2. FIAM specific data collection setting in plist file.
  //   3. Global Firebase auto data collecting setting (carried over by currentSetting property).

  FIRIAMAutoDataCollectionSetting programmaticSetting =
      [self FIAMProgrammaticAutoDataCollectionSetting];

  if (programmaticSetting == FIRIAMAutoDataCollectionSettingEnabled) {
    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180010",
                @"FIAM auto data-collection is explicitly enabled, start SDK flows.");
    return true;
  } else if (programmaticSetting == FIRIAMAutoDataCollectionSettingDisabled) {
    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180013",
                @"FIAM auto data-collection is explicitly disabled, do not start SDK flows.");
    return false;
  } else {
    // No explicit setting from fiam's programmatic setting. Checking next level down.
    FIRIAMAutoDataCollectionSetting fiamPlistDataCollectionSetting =
        [self FIAMPlistAutoDataCollectionSetting];

    if (fiamPlistDataCollectionSetting == FIRIAMAutoDataCollectionSettingNone) {
      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180018",
                  @"No programmatic or plist setting at FIAM level. Fallback to global Firebase "
                   "level setting.");
      return self.currentSetting.isFirebaseAutoDataCollectionEnabled;
    } else {
      return fiamPlistDataCollectionSetting == FIRIAMAutoDataCollectionSettingEnabled;
    }
  }
}

- (void)resume {
  // persist the setting
  [[GULUserDefaults standardUserDefaults]
      setObject:@(YES)
         forKey:_userDefaultsKeyForFIAMProgrammaticAutoDataCollectionSetting];

  @synchronized(self) {
    if (!_running) {
      [self.fetchOnAppForegroundFlow start];
      [self.displayOnAppForegroundFlow start];
      [self.displayOnFIRAnalyticEventsFlow start];
      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180019",
                  @"Start Firebase In-App Messaging flows from inactive.");
      _running = YES;
    } else {
      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM180004",
                    @"Runtime is already active, resume is just a no-op");
    }
  }
}

- (void)pause {
  // persist the setting
  [[GULUserDefaults standardUserDefaults]
      setObject:@(NO)
         forKey:_userDefaultsKeyForFIAMProgrammaticAutoDataCollectionSetting];

  @synchronized(self) {
    if (_running) {
      [self.fetchOnAppForegroundFlow stop];
      [self.displayOnAppForegroundFlow stop];
      [self.displayOnFIRAnalyticEventsFlow stop];
      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180006",
                  @"Shutdown Firebase In-App Messaging flows.");
      _running = NO;
    } else {
      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM180005",
                    @"No runtime active yet, pause is just a no-op");
    }
  }
}

- (void)setShouldSuppressMessageDisplay:(BOOL)shouldSuppress {
  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180003", @"Message display suppress set to %@",
              @(shouldSuppress));
  self.displayExecutor.suppressMessageDisplay = shouldSuppress;
}

- (void)startRuntimeWithSDKSettings:(FIRIAMSDKSettings *)settings {
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{
    [self internalStartRuntimeWithSDKSettings:settings];
  });
}

- (void)internalStartRuntimeWithSDKSettings:(FIRIAMSDKSettings *)settings {
  if (_running) {
    // Runtime has been started previously. Stop all the flows first.
    [self.fetchOnAppForegroundFlow stop];
    [self.displayOnAppForegroundFlow stop];
    [self.displayOnFIRAnalyticEventsFlow stop];
  }

  self.currentSetting = settings;

  FIRIAMTimerWithNSDate *timeFetcher = [[FIRIAMTimerWithNSDate alloc] init];
  NSTimeInterval start = [timeFetcher currentTimestampInSeconds];

  self.activityLogger =
      [[FIRIAMActivityLogger alloc] initWithMaxCountBeforeReduce:settings.loggerMaxCountBeforeReduce
                                             withSizeAfterReduce:settings.loggerSizeAfterReduce
                                                     verboseMode:settings.loggerInVerboseMode
                                                   loadFromCache:YES];

  self.responseParser = [[FIRIAMFetchResponseParser alloc] initWithTimeFetcher:timeFetcher];

  self.bookKeeper = [[FIRIAMBookKeeperViaUserDefaults alloc]
      initWithUserDefaults:[GULUserDefaults standardUserDefaults]];

  self.messageCache = [[FIRIAMMessageClientCache alloc] initWithBookkeeper:self.bookKeeper
                                                       usingResponseParser:self.responseParser];
  self.fetchResultStorage = [[FIRIAMServerMsgFetchStorage alloc] init];

  self.clientInfoFetcher = [[FIRIAMClientInfoFetcher alloc]
      initWithFirebaseInstallations:[FIRInAppMessaging inAppMessaging].installations];

  self.restfulFetcher =
      [[FIRIAMMsgFetcherUsingRestful alloc] initWithHost:settings.apiServerHost
                                            HTTPProtocol:settings.apiHttpProtocol
                                                 project:settings.firebaseProjectNumber
                                             firebaseApp:settings.firebaseAppId
                                                  APIKey:settings.apiKey
                                            fetchStorage:self.fetchResultStorage
                                       instanceIDFetcher:self.clientInfoFetcher
                                         usingURLSession:nil
                                          responseParser:self.responseParser];

  // start fetch on app foreground flow
  FIRIAMFetchSetting *fetchSetting = [[FIRIAMFetchSetting alloc] init];
  fetchSetting.fetchMinIntervalInMinutes = settings.fetchMinIntervalInMinutes;

  // start render on app foreground flow
  FIRIAMDisplaySetting *appForegroundDisplaysetting = [[FIRIAMDisplaySetting alloc] init];
  appForegroundDisplaysetting.displayMinIntervalInMinutes =
      settings.appFGRenderMinIntervalInMinutes;

  // clearcut log expires after 14 days: give up on attempting to deliver them any more
  NSInteger ctLogExpiresInSeconds = 14 * 24 * 60 * 60;

  FIRIAMClearcutLogStorage *ctLogStorage =
      [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:ctLogExpiresInSeconds
                                                     withTimeFetcher:timeFetcher];

  FIRIAMClearcutHttpRequestSender *clearcutRequestSender = [[FIRIAMClearcutHttpRequestSender alloc]
      initWithClearcutHost:settings.clearcutServerHost
          usingTimeFetcher:timeFetcher
        withOSMajorVersion:[self.clientInfoFetcher getOSMajorVersion]];

  FIRIAMClearcutUploader *ctUploader =
      [[FIRIAMClearcutUploader alloc] initWithRequestSender:clearcutRequestSender
                                                timeFetcher:timeFetcher
                                                 logStorage:ctLogStorage
                                              usingStrategy:settings.clearcutStrategy
                                          usingUserDefaults:nil];

  FIRIAMClearcutLogger *clearcutLogger =
      [[FIRIAMClearcutLogger alloc] initWithFBProjectNumber:settings.firebaseProjectNumber
                                                    fbAppId:settings.firebaseAppId
                                          clientInfoFetcher:self.clientInfoFetcher
                                           usingTimeFetcher:timeFetcher
                                              usingUploader:ctUploader];

  FIRIAMAnalyticsEventLoggerImpl *analyticsEventLogger = [[FIRIAMAnalyticsEventLoggerImpl alloc]
      initWithClearcutLogger:clearcutLogger
            usingTimeFetcher:timeFetcher
           usingUserDefaults:nil
                   analytics:[FIRInAppMessaging inAppMessaging].analytics];

  FIRIAMSDKModeManager *sdkModeManager =
      [[FIRIAMSDKModeManager alloc] initWithUserDefaults:GULUserDefaults.standardUserDefaults
                                     testingModeListener:self];

  FIRIAMActionURLFollower *actionFollower = [FIRIAMActionURLFollower actionURLFollower];

  self.displayExecutor =
      [[FIRIAMDisplayExecutor alloc] initWithInAppMessaging:[FIRInAppMessaging inAppMessaging]
                                                    setting:appForegroundDisplaysetting
                                               messageCache:self.messageCache
                                                timeFetcher:timeFetcher
                                                 bookKeeper:self.bookKeeper
                                          actionURLFollower:actionFollower
                                             activityLogger:self.activityLogger
                                       analyticsEventLogger:analyticsEventLogger];

  self.fetchOnAppForegroundFlow =
      [[FIRIAMFetchOnAppForegroundFlow alloc] initWithSetting:fetchSetting
                                                 messageCache:self.messageCache
                                               messageFetcher:self.restfulFetcher
                                                  timeFetcher:timeFetcher
                                                   bookKeeper:self.bookKeeper
                                               activityLogger:self.activityLogger
                                         analyticsEventLogger:analyticsEventLogger
                                         FIRIAMSDKModeManager:sdkModeManager
                                              displayExecutor:self.displayExecutor];

  // Setting the message display component and suppression. It's needed in case
  // headless SDK is initialized after the these properties are already set on FIRInAppMessaging.
  self.displayExecutor.messageDisplayComponent =
      FIRInAppMessaging.inAppMessaging.messageDisplayComponent;
  self.displayExecutor.suppressMessageDisplay =
      FIRInAppMessaging.inAppMessaging.messageDisplaySuppressed;

  // Both display flows are created on startup. But they would only be turned on (started) based on
  // the sdk mode for the current instance
  self.displayOnFetchDoneFlow = [[FIRIAMDisplayCheckOnFetchDoneNotificationFlow alloc]
      initWithDisplayFlow:self.displayExecutor];
  self.displayOnAppForegroundFlow =
      [[FIRIAMDisplayCheckOnAppForegroundFlow alloc] initWithDisplayFlow:self.displayExecutor];

  self.displayOnFIRAnalyticEventsFlow =
      [[FIRIAMDisplayCheckOnAnalyticEventsFlow alloc] initWithDisplayFlow:self.displayExecutor];

  self.messageCache.analyticsEventDisplayCheckFlow = self.displayOnFIRAnalyticEventsFlow;
  [self.messageCache
      loadMessageDataFromServerFetchStorage:self.fetchResultStorage
                             withCompletion:^(BOOL success) {
                               // start flows regardless whether we can load messages from fetch
                               // storage successfully
                               FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180001",
                                           @"Message loading from fetch storage was done.");

                               if ([self shouldRunSDKFlowsOnStartup]) {
                                 FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180008",
                                             @"Start SDK runtime components.");

                                 [self.clientInfoFetcher
                                     fetchFirebaseInstallationDataWithProjectNumber:
                                         self.currentSetting.firebaseProjectNumber
                                                                     withCompletion:^(
                                                                         NSString *_Nullable FID,
                                                                         NSString
                                                                             *_Nullable FISToken,
                                                                         NSError *_Nullable error) {
                                                                       // Always dump the
                                                                       // installation ID into log
                                                                       // on startup to help
                                                                       // developers to find it for
                                                                       // their app instance.
                                                                       FIRLogDebug(
                                                                           kFIRLoggerInAppMessaging,
                                                                           @"I-IAM180017",
                                                                           @"Starting "
                                                                           @"InAppMessaging "
                                                                           @"runtime "
                                                                           @"with "
                                                                            "Firebase Installation "
                                                                            "ID %@",
                                                                           FID);
                                                                     }];

                                 [self.fetchOnAppForegroundFlow start];
                                 [self.displayOnFIRAnalyticEventsFlow start];

                                 self->_running = YES;

                                 if (sdkModeManager.currentMode == FIRIAMSDKModeTesting) {
                                   FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180007",
                                               @"InAppMessaging testing mode enabled. App "
                                                "foreground messages will be displayed following "
                                                "fetch");
                                   [self.displayOnFetchDoneFlow start];
                                 } else {
                                   FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180020",
                                               @"Start regular display flow for non-testing "
                                                "instance mode");
                                   [self.displayOnAppForegroundFlow start];

                                   // Simulate app going into foreground on startup
                                   [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
                                 }

                                 // One-time triggering of checks for both fetch flow
                                 // upon SDK/app startup.
                                 [self.fetchOnAppForegroundFlow
                                     checkAndFetchForInitialAppLaunch:YES];
                               } else {
                                 FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180009",
                                             @"No FIAM SDK startup due to settings.");
                               }
                             }];

  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180002",
              @"Firebase In-App Messaging SDK version %@ finished startup in %lf seconds "
               "with these settings: %@",
              [self.clientInfoFetcher getIAMSDKVersion],
              (double)([timeFetcher currentTimestampInSeconds] - start), settings);
}
@end

#endif  // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION
