// 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 "Crashlytics/Crashlytics/Models/FIRCLSFileManager.h"

#import "Crashlytics/Crashlytics/Components/FIRCLSApplication.h"
#import "Crashlytics/Crashlytics/Components/FIRCLSCrashedMarkerFile.h"
#import "Crashlytics/Crashlytics/Helpers/FIRCLSLogger.h"
#import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h"

NSString *const FIRCLSCacheDirectoryName = @"com.crashlytics.data";
NSString *const FIRCLSCacheVersion = @"v5";
NSString *const FIRCLSMetricKitDiagnosticPath = @"/MetricKit/Diagnostics/";

@interface FIRCLSFileManager () {
  NSString *_rootPath;
  NSString *_cachesPath;
}
@property(nonatomic) BOOL crashFileMarkerExists;

@end

@implementation FIRCLSFileManager

- (instancetype)init {
  self = [super init];
  if (!self) {
    return nil;
  }

  _underlyingFileManager = [NSFileManager defaultManager];

  NSString *path =
      [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
  _cachesPath = [path copy];
  path = [path stringByAppendingPathComponent:FIRCLSCacheDirectoryName];
  path = [path stringByAppendingPathComponent:[self pathNamespace]];
  _rootPath = [path copy];

  _crashFileMarkerExists = NO;
  return self;
}

#pragma mark - Core API

- (BOOL)fileExistsAtPath:(NSString *)path {
  return [_underlyingFileManager fileExistsAtPath:path];
}

- (BOOL)createFileAtPath:(NSString *)path
                contents:(nullable NSData *)data
              attributes:(nullable NSDictionary<NSFileAttributeKey, id> *)attr {
  return [_underlyingFileManager createFileAtPath:path contents:data attributes:attr];
}

- (BOOL)createDirectoryAtPath:(NSString *)path {
  NSDictionary *attributes;
  NSError *error;

  attributes = @{NSFilePosixPermissions : [NSNumber numberWithShort:0755]};
  error = nil;

  if (![[self underlyingFileManager] createDirectoryAtPath:path
                               withIntermediateDirectories:YES
                                                attributes:attributes
                                                     error:&error]) {
    FIRCLSErrorLog(@"Unable to create directory %@", error);
    return NO;
  }

  return YES;
}

- (BOOL)removeItemAtPath:(NSString *)path {
  NSError *error;

  error = nil;
  if (![[self underlyingFileManager] removeItemAtPath:path error:&error] || !path) {
    FIRCLSErrorLog(@"Failed to remove file %@: %@", path, error);

    return NO;
  }

  return YES;
}

- (BOOL)removeContentsOfDirectoryAtPath:(NSString *)path {
  __block BOOL success = YES;

  // only return true if we were able to remove every item in the directory (or it was empty)

  [self enumerateFilesInDirectory:path
                       usingBlock:^(NSString *filePath, NSString *extension) {
                         success = [self removeItemAtPath:filePath] && success;
                       }];

  return success;
}

- (BOOL)moveItemAtPath:(NSString *)path toDirectory:(NSString *)destDir {
  NSString *destPath;
  NSError *error;

  destPath = [destDir stringByAppendingPathComponent:[path lastPathComponent]];
  error = nil;

  if (!path || !destPath) {
    FIRCLSErrorLog(@"Failed to move file, inputs invalid");

    return NO;
  }

  if (![[self underlyingFileManager] moveItemAtPath:path toPath:destPath error:&error]) {
    FIRCLSErrorLog(@"Failed to move file: %@", error);

    return NO;
  }

  return YES;
}

- (BOOL)didCrashOnPreviousExecution {
  static dispatch_once_t checkCrashFileMarketExistsOnceToken;
  dispatch_once(&checkCrashFileMarketExistsOnceToken, ^{
    NSString *crashedMarkerFileName = [NSString stringWithUTF8String:FIRCLSCrashedMarkerFileName];
    NSString *crashedMarkerFileFullPath =
        [[self rootPath] stringByAppendingPathComponent:crashedMarkerFileName];
    self.crashFileMarkerExists = [self fileExistsAtPath:crashedMarkerFileFullPath];
  });
  return self.crashFileMarkerExists;
}

- (BOOL)metricKitDiagnosticFileExists {
  NSArray *contentsOfMetricKitDirectory = [self
      contentsOfDirectory:[_cachesPath stringByAppendingString:FIRCLSMetricKitDiagnosticPath]];
  return ([contentsOfMetricKitDirectory count] > 0);
}

- (void)createEmptyMetricKitFile:(NSString *)reportPath {
  NSString *metricKitFile =
      [reportPath stringByAppendingPathComponent:FIRCLSMetricKitFatalReportFile];
  [self createFileAtPath:metricKitFile contents:nil attributes:nil];
}

- (void)enumerateFilesInDirectory:(NSString *)directory
                       usingBlock:(void (^)(NSString *filePath, NSString *extension))block {
  for (NSString *path in [[self underlyingFileManager] contentsOfDirectoryAtPath:directory
                                                                           error:nil]) {
    NSString *extension;
    NSString *fullPath;

    // Skip files that start with a dot.  This is important, because if you try to move a .DS_Store
    // file, it will fail if the target directory also has a .DS_Store file in it.  Plus, its
    // wasteful, because we don't care about dot files.
    if ([path hasPrefix:@"."]) {
      continue;
    }

    extension = [path pathExtension];
    fullPath = [directory stringByAppendingPathComponent:path];
    if (block) {
      block(fullPath, extension);
    }
  }
}

- (NSNumber *)fileSizeAtPath:(NSString *)path {
  NSError *error = nil;
  NSDictionary *attrs = [[self underlyingFileManager] attributesOfItemAtPath:path error:&error];

  if (!attrs) {
    FIRCLSErrorLog(@"Unable to read file size: %@", error);
    return nil;
  }

  return [attrs objectForKey:NSFileSize];
}

- (NSArray *)contentsOfDirectory:(NSString *)path {
  NSMutableArray *array = [NSMutableArray array];

  [self enumerateFilesInDirectory:path
                       usingBlock:^(NSString *filePath, NSString *extension) {
                         [array addObject:filePath];
                       }];

  return [array copy];
}

#pragma - Properties
- (NSString *)pathNamespace {
  return FIRCLSApplicationGetBundleIdentifier();
}

- (NSString *)versionedPath {
  return [[self rootPath] stringByAppendingPathComponent:FIRCLSCacheVersion];
}

#pragma - Settings Paths

// This path should be different than the structurePath because the
// settings download operations will delete the settings directory,
// which would delete crash reports if these were the same
- (NSString *)settingsDirectoryPath {
  return [[self versionedPath] stringByAppendingPathComponent:@"settings"];
}

- (NSString *)settingsFilePath {
  return [[self settingsDirectoryPath] stringByAppendingPathComponent:@"settings.json"];
}

- (NSString *)settingsCacheKeyPath {
  return [[self settingsDirectoryPath] stringByAppendingPathComponent:@"cache-key.json"];
}

#pragma - Report Paths
- (NSString *)structurePath {
  return [[self versionedPath] stringByAppendingPathComponent:@"reports"];
}

- (NSString *)activePath {
  return [[self structurePath] stringByAppendingPathComponent:@"active"];
}

- (NSString *)pendingPath {
  return [[self structurePath] stringByAppendingPathComponent:@"pending"];
}

- (NSString *)processingPath {
  return [[self structurePath] stringByAppendingPathComponent:@"processing"];
}

- (NSString *)preparedPath {
  return [[self structurePath] stringByAppendingPathComponent:@"prepared"];
}

- (NSArray *)activePathContents {
  return [self contentsOfDirectory:[self activePath]];
}

- (NSArray *)preparedPathContents {
  return [self contentsOfDirectory:[self preparedPath]];
}

- (NSArray *)processingPathContents {
  return [self contentsOfDirectory:[self processingPath]];
}

#pragma mark - Logic
- (BOOL)createReportDirectories {
  if (![self createDirectoryAtPath:[self activePath]]) {
    return NO;
  }

  if (![self createDirectoryAtPath:[self processingPath]]) {
    return NO;
  }

  if (![self createDirectoryAtPath:[self preparedPath]]) {
    return NO;
  }

  return YES;
}

- (NSString *)setupNewPathForExecutionIdentifier:(NSString *)identifier {
  NSString *path = [[self activePath] stringByAppendingPathComponent:identifier];

  if (![self createDirectoryAtPath:path]) {
    return nil;
  }

  return path;
}

- (BOOL)moveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error {
  return [self.underlyingFileManager moveItemAtPath:srcPath toPath:dstPath error:error];
}

// Wrapper over NSData so the method can be mocked for unit tests
- (NSData *)dataWithContentsOfFile:(NSString *)path {
  return [NSData dataWithContentsOfFile:path];
}

@end
