// 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 "Interop/Analytics/Public/FIRAnalyticsInterop.h"

#import "Crashlytics/Crashlytics/Components/FIRCLSApplication.h"
#import "Crashlytics/Crashlytics/Controllers/FIRCLSAnalyticsManager.h"
#import "Crashlytics/Crashlytics/Controllers/FIRCLSManagerData.h"
#import "Crashlytics/Crashlytics/Controllers/FIRCLSReportUploader_Private.h"
#import "Crashlytics/Crashlytics/DataCollection/FIRCLSDataCollectionToken.h"
#import "Crashlytics/Crashlytics/Helpers/FIRCLSDefines.h"
#import "Crashlytics/Crashlytics/Models/FIRCLSFileManager.h"
#import "Crashlytics/Crashlytics/Models/FIRCLSInstallIdentifierModel.h"
#import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h"
#import "Crashlytics/Crashlytics/Models/FIRCLSSettings.h"
#import "Crashlytics/Crashlytics/Models/FIRCLSSymbolResolver.h"
#import "Crashlytics/Crashlytics/Models/Record/FIRCLSReportAdapter.h"
#import "Crashlytics/Crashlytics/Operations/Reports/FIRCLSProcessReportOperation.h"

#include "Crashlytics/Crashlytics/Helpers/FIRCLSUtility.h"

#import "Crashlytics/Shared/FIRCLSConstants.h"
#import "Crashlytics/Shared/FIRCLSNetworking/FIRCLSURLBuilder.h"

#import <GoogleDataTransport/GoogleDataTransport.h>

@interface FIRCLSReportUploader () {
  id<FIRAnalyticsInterop> _analytics;
}

@property(nonatomic, strong) GDTCORTransport *googleTransport;
@property(nonatomic, strong) FIRCLSInstallIdentifierModel *installIDModel;

@property(nonatomic, readonly) NSString *googleAppID;

@end

@implementation FIRCLSReportUploader

- (instancetype)initWithManagerData:(FIRCLSManagerData *)managerData {
  self = [super init];
  if (!self) {
    return nil;
  }

  _operationQueue = managerData.operationQueue;
  _googleAppID = managerData.googleAppID;
  _googleTransport = managerData.googleTransport;
  _installIDModel = managerData.installIDModel;
  _fileManager = managerData.fileManager;
  _analytics = managerData.analytics;

  return self;
}

#pragma mark - Packaging and Submission

/*
 * For a crash report, this is the initial code path for uploading. A report
 * will not repeat this code path after it's happened because this code path
 * will move the report from the "active" folder into "processing" and then
 * "prepared". Once in prepared, the report can be re-uploaded any number of times
 * with uploadPackagedReportAtPath in the case of an upload failure.
 */
- (void)prepareAndSubmitReport:(FIRCLSInternalReport *)report
           dataCollectionToken:(FIRCLSDataCollectionToken *)dataCollectionToken
                      asUrgent:(BOOL)urgent
                withProcessing:(BOOL)shouldProcess {
  if (![dataCollectionToken isValid]) {
    FIRCLSErrorLog(@"Data collection disabled and report will not be submitted");
    return;
  }

  // This activity is still relevant using GoogleDataTransport because the on-device
  // symbolication operation may be computationally intensive.
  FIRCLSApplicationActivity(
      FIRCLSApplicationActivityDefault, @"Crashlytics Crash Report Processing", ^{
        // Check to see if the FID has rotated before we construct the payload
        // so that the payload has an updated value.
        //
        // If we're in urgent mode, this will be running on the main thread. Since
        // the FIID callback is run on the main thread, this call can deadlock in
        // urgent mode. Since urgent mode happens when the app is in a crash loop,
        // we can safely assume users aren't rotating their FIID, so this can be skipped.
        if (!urgent) {
          [self.installIDModel regenerateInstallIDIfNeededWithBlock:^(
                                   NSString *_Nonnull newFIID, NSString *_Nonnull authToken) {
            self.fiid = [newFIID copy];
            self.authToken = [authToken copy];
          }];
        } else {
          FIRCLSWarningLog(
              @"Crashlytics skipped rotating the Install ID during urgent mode because it is run "
              @"on the main thread, which can't succeed. This can happen if the app crashed the "
              @"last run and Crashlytics is uploading urgently.");
        }

        // Run on-device symbolication before packaging if we should process
        if (shouldProcess) {
          if (![self.fileManager moveItemAtPath:report.path
                                    toDirectory:self.fileManager.processingPath]) {
            FIRCLSErrorLog(@"Unable to move report for processing");
            return;
          }

          // adjust the report's path, and process it
          [report setPath:[self.fileManager.processingPath
                              stringByAppendingPathComponent:report.directoryName]];

          FIRCLSSymbolResolver *resolver = [[FIRCLSSymbolResolver alloc] init];

          FIRCLSProcessReportOperation *processOperation =
              [[FIRCLSProcessReportOperation alloc] initWithReport:report resolver:resolver];

          [processOperation start];
        }

        // With the new report endpoint, the report is deleted once it is written to GDT
        // Check if the report has a crash file before the report is moved or deleted
        BOOL isCrash = report.isCrash;

        // For the new endpoint, just move the .clsrecords from "processing" -> "prepared".
        // In the old endpoint this was for packaging the report as a multipartmime file,
        // so this can probably be removed for GoogleDataTransport.
        if (![self.fileManager moveItemAtPath:report.path
                                  toDirectory:self.fileManager.preparedPath]) {
          FIRCLSErrorLog(@"Unable to move report to prepared");
          return;
        }

        NSString *packagedPath = [self.fileManager.preparedPath
            stringByAppendingPathComponent:report.path.lastPathComponent];

        FIRCLSInfoLog(@"[Firebase/Crashlytics] Packaged report with id '%@' for submission",
                      report.identifier);

        [self uploadPackagedReportAtPath:packagedPath
                     dataCollectionToken:dataCollectionToken
                                asUrgent:urgent];

        // We don't check for success here for 2 reasons:
        //   1) If we can't upload a crash for whatever reason, but we can upload analytics
        //      it's better for the customer to get accurate Crash Free Users.
        //   2) In the past we did try to check for success, but it was a useless check because
        //      sendDataEvent is async (unless we're sending urgently).
        if (isCrash) {
          [FIRCLSAnalyticsManager logCrashWithTimeStamp:report.crashedOnDate.timeIntervalSince1970
                                            toAnalytics:self->_analytics];
        }
      });

  return;
}

/*
 * This code path can be repeated any number of times for a prepared crash report if
 * the report is failing to upload.
 *
 * Therefore, side effects (like logging to Analytics) should not go in this method or
 * else they will re-trigger when failures happen.
 *
 * When a crash report fails to upload, it will stay in the "prepared" folder. Upon next
 * run of the app, the ReportManager will attempt to re-upload prepared reports using this
 * method.
 */
- (void)uploadPackagedReportAtPath:(NSString *)path
               dataCollectionToken:(FIRCLSDataCollectionToken *)dataCollectionToken
                          asUrgent:(BOOL)urgent {
  FIRCLSDebugLog(@"Submitting report %@", urgent ? @"urgently" : @"async");

  if (![dataCollectionToken isValid]) {
    FIRCLSErrorLog(@"A report upload was requested with an invalid data collection token.");
    return;
  }

  FIRCLSReportAdapter *adapter = [[FIRCLSReportAdapter alloc] initWithPath:path
                                                               googleAppId:self.googleAppID
                                                            installIDModel:self.installIDModel
                                                                      fiid:self.fiid
                                                                 authToken:self.authToken];

  GDTCOREvent *event = [self.googleTransport eventForTransport];
  event.dataObject = adapter;
  event.qosTier = GDTCOREventQoSFast;  // Bypass batching, send immediately

  dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

  [self.googleTransport
      sendDataEvent:event
         onComplete:^(BOOL wasWritten, NSError *error) {
           if (!wasWritten) {
             FIRCLSErrorLog(
                 @"Failed to send crash report due to failure writing GoogleDataTransport event");
             dispatch_semaphore_signal(semaphore);
             return;
           }

           if (error) {
             FIRCLSErrorLog(@"Failed to send crash report due to GoogleDataTransport error: %@",
                            error.localizedDescription);
             dispatch_semaphore_signal(semaphore);
             return;
           }

           FIRCLSInfoLog(@"Completed report submission with id: %@", path.lastPathComponent);

           if (urgent) {
             dispatch_semaphore_signal(semaphore);
           }

           [self cleanUpSubmittedReportAtPath:path];
         }];

  if (urgent) {
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
  }
}

- (BOOL)cleanUpSubmittedReportAtPath:(NSString *)path {
  if (![[self fileManager] removeItemAtPath:path]) {
    FIRCLSErrorLog(@"Unable to remove packaged submission");
    return NO;
  }

  return YES;
}

@end
