/*
 * Copyright 2018 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 <UIKit/UIKit.h>
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"

#import "FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.h"
#import "FirebaseInAppMessaging/Sources/Private/Analytics/FIRIAMClearcutUploader.h"
#import "FirebaseInAppMessaging/Sources/Private/Util/FIRIAMTimeFetcher.h"

#import "FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutHttpRequestSender.h"
#import "FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutLogStorage.h"

// a macro for turning a millisecond value into seconds
#define MILLS_TO_SECONDS(x) (((long)x) / 1000)

@implementation FIRIAMClearcutStrategy
- (instancetype)initWithMinWaitTimeInMills:(NSInteger)minWaitTimeInMills
                        maxWaitTimeInMills:(NSInteger)maxWaitTimeInMills
                 failureBackoffTimeInMills:(NSInteger)failureBackoffTimeInMills
                             batchSendSize:(NSInteger)batchSendSize {
  if (self = [super init]) {
    _minimalWaitTimeInMills = minWaitTimeInMills;
    _maximumWaitTimeInMills = maxWaitTimeInMills;
    _failureBackoffTimeInMills = failureBackoffTimeInMills;
    _batchSendSize = batchSendSize;
  }
  return self;
}

- (NSString *)description {
  return [NSString stringWithFormat:@"min wait time in seconds:%ld;max wait time in seconds:%ld;"
                                     "failure backoff time in seconds:%ld;batch send size:%d",
                                    MILLS_TO_SECONDS(self.minimalWaitTimeInMills),
                                    MILLS_TO_SECONDS(self.maximumWaitTimeInMills),
                                    MILLS_TO_SECONDS(self.failureBackoffTimeInMills),
                                    (int)self.batchSendSize];
}
@end

@interface FIRIAMClearcutUploader () {
  dispatch_queue_t _queue;
  BOOL _nextSendScheduled;
}

@property(readwrite, nonatomic) FIRIAMClearcutHttpRequestSender *requestSender;
@property(nonatomic, assign) int64_t nextValidSendTimeInMills;

@property(nonatomic, readonly) id<FIRIAMTimeFetcher> timeFetcher;
@property(nonatomic, readonly) FIRIAMClearcutLogStorage *logStorage;

@property(nonatomic, readonly) FIRIAMClearcutStrategy *strategy;
@property(nonatomic, readonly) GULUserDefaults *userDefaults;
@end

static NSString *FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills =
    @"firebase-iam-next-clearcut-upload-timestamp-in-mills";

/**
 * The high level behavior in this implementation is like this
 *  1 New records always pushed into FIRIAMClearcutLogStorage first.
 *  2 Upload log records in batches.
 *  3 If prior upload was successful, next upload would wait for the time parsed out of the
 *      clearcut response body.
 *  4 If prior upload failed, next upload attempt would wait for failureBackoffTimeInMills defined
 *      in strategy
 *  5 When app
 */

@implementation FIRIAMClearcutUploader

- (instancetype)initWithRequestSender:(FIRIAMClearcutHttpRequestSender *)requestSender
                          timeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher
                           logStorage:(FIRIAMClearcutLogStorage *)logStorage
                        usingStrategy:(FIRIAMClearcutStrategy *)strategy
                    usingUserDefaults:(nullable GULUserDefaults *)userDefaults {
  if (self = [super init]) {
    _nextSendScheduled = NO;
    _timeFetcher = timeFetcher;
    _requestSender = requestSender;
    _logStorage = logStorage;
    _strategy = strategy;
    _queue = dispatch_queue_create("com.google.firebase.inappmessaging.clearcut_upload", NULL);
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(scheduleNextSendFromForeground:)
                                                 name:UIApplicationWillEnterForegroundNotification
                                               object:nil];
    if (@available(iOS 13.0, tvOS 13.0, *)) {
      [[NSNotificationCenter defaultCenter] addObserver:self
                                               selector:@selector(scheduleNextSendFromForeground:)
                                                   name:UISceneWillEnterForegroundNotification
                                                 object:nil];
    }
    _userDefaults = userDefaults ? userDefaults : [GULUserDefaults standardUserDefaults];
    // it would be 0 if it does not exist, which is equivalent to saying that
    // you can send now
    _nextValidSendTimeInMills = (int64_t)[_userDefaults
        doubleForKey:FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills];

    NSArray<FIRIAMClearcutLogRecord *> *availableLogs =
        [logStorage popStillValidRecordsForUpTo:strategy.batchSendSize];
    if (availableLogs.count) {
      [self scheduleNextSend];
    }

    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260001",
                @"FIRIAMClearcutUploader created with strategy as %@", self.strategy);
  }
  return self;
}

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

- (void)scheduleNextSendFromForeground:(NSNotification *)notification {
  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260010",
              @"App foregrounded, FIRIAMClearcutUploader will seed next send");
  [self scheduleNextSend];
}

- (void)addNewLogRecord:(FIRIAMClearcutLogRecord *)record {
  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260002",
              @"New log record sent to clearcut uploader");

  [self.logStorage pushRecords:@[ record ]];
  [self scheduleNextSend];
}

- (void)attemptUploading {
  NSArray<FIRIAMClearcutLogRecord *> *availableLogs =
      [self.logStorage popStillValidRecordsForUpTo:self.strategy.batchSendSize];

  if (availableLogs.count > 0) {
    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260011", @"Deliver %d clearcut records",
                (int)availableLogs.count);
    [self.requestSender
        sendClearcutHttpRequestForLogs:availableLogs
                        withCompletion:^(BOOL success, BOOL shouldRetryLogs,
                                         int64_t waitTimeInMills) {
                          if (success) {
                            FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260003",
                                        @"Delivering %d clearcut records was successful",
                                        (int)availableLogs.count);
                            // make sure the effective wait time is between two bounds
                            // defined in strategy
                            waitTimeInMills =
                                MAX(self.strategy.minimalWaitTimeInMills, waitTimeInMills);

                            waitTimeInMills =
                                MIN(waitTimeInMills, self.strategy.maximumWaitTimeInMills);
                          } else {
                            // failed to deliver
                            FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260004",
                                        @"Failed to attempt the delivery of %d clearcut "
                                        @"records and should-retry for them is %@",
                                        (int)availableLogs.count, shouldRetryLogs ? @"YES" : @"NO");
                            if (shouldRetryLogs) {
                              /**
                               * Note that there is a chance that the app crashes before we can
                               * call pushRecords: on the logStorage below which means we lost
                               * these log records permanently. This is a trade-off between handling
                               * duplicate records on server side vs taking the risk of losing
                               * data. This implementation picks the latter.
                               */
                              FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260007",
                                          @"Push failed log records back to storage");
                              [self.logStorage pushRecords:availableLogs];
                            }

                            waitTimeInMills = (int64_t)self.strategy.failureBackoffTimeInMills;
                          }

                          FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260005",
                                      @"Wait for at least %ld seconds before next upload attempt",
                                      MILLS_TO_SECONDS(waitTimeInMills));

                          self.nextValidSendTimeInMills =
                              (int64_t)[self.timeFetcher currentTimestampInSeconds] * 1000 +
                              waitTimeInMills;

                          // persisted so that it can be recovered next time the app runs
                          [self.userDefaults
                              setDouble:(double)self.nextValidSendTimeInMills
                                 forKey:
                                     FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills];

                          @synchronized(self) {
                            self->_nextSendScheduled = NO;
                          }
                          [self scheduleNextSend];
                        }];

  } else {
    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260007", @"No clearcut records to be uploaded");
    @synchronized(self) {
      _nextSendScheduled = NO;
    }
  }
}

- (void)scheduleNextSend {
  @synchronized(self) {
    if (_nextSendScheduled) {
      return;
    }
  }

  int64_t delayTimeInMills =
      self.nextValidSendTimeInMills - (int64_t)[self.timeFetcher currentTimestampInSeconds] * 1000;

  if (delayTimeInMills <= 0) {
    delayTimeInMills = 0;  // no need to delay since we can send now
  }

  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260006",
              @"Next upload attempt scheduled in %d seconds", (int)delayTimeInMills / 1000);

  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delayTimeInMills * (int64_t)NSEC_PER_MSEC),
                 _queue, ^{
                   [self attemptUploading];
                 });
  @synchronized(self) {
    _nextSendScheduled = YES;
  }
}

@end

#endif  // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION
