/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

#import <Foundation/Foundation.h>

#import <FBControlCore/FBControlCore.h>
#import <FBDeviceControl/FBDeviceControl.h>
#import <FBSimulatorControl/FBSimulatorControl.h>
#import <XCTestBootstrap/XCTestBootstrap.h>

#import "FBIDBCompanionServer.h"
#import "FBIDBConfiguration.h"
#import "FBIDBError.h"
#import "FBIDBLogger.h"
#import "FBIDBPortsConfiguration.h"
#import "FBiOSTargetProvider.h"
#import "FBiOSTargetStateChangeNotifier.h"
#import "FBiOSTargetDescription.h"
#import "FBIDBStorageManager.h"
#import "FBIDBCommandExecutor.h"

#import "idb-Swift.h"

const char *kUsageHelpMessage = "\
Usage: \n \
  Modes of operation, only one of these may be specified:\n\
    --udid UDID|mac|only       Launches a companion server for the specified UDID, 'mac' for a mac companion, or 'only' to run a companion for the only simulator/device available.\n\
    --boot UDID                Boots the simulator with the specified UDID.\n\
    --reboot UDID              Reboots the target with the specified UDID.\n\
    --shutdown UDID            Shuts down the target with the specified UDID.\n\
    --erase UDID               Erases the target with the specified UDID.\n\
    --clean UDID               Performs a soft reset to the specified UDID.\n\
    --delete UDID|all          Deletes the simulator with the specified UDID, or 'all' to delete all simulators in the set.\n\
    --create VALUE             Creates a simulator using the VALUE argument like \"iPhone X,iOS 12.4\"\n\
    --clone UDID               Clones a simulator by a given UDID\n\
    --clone-destination-set    A path to the destination device set in a clone operation, --device-set-path specifies the source simulator.\n\
    --recover ecid:ECID        Causes the targeted device ECID to enter recovery mode\n\
    --unrecover ecid:ECID      Causes the targeted device ECID to exit recovery mode\n\
    --activate ecid:ECID       Causes the device to activate\n\
    --notify PATH|stdout       Launches a companion notifier which will stream availability updates to the specified path, or stdout.\n\
    --forward UDID:PORT        Forwards the remote socket for the specified UDID to the specified remote PORT. Input and output is relayed via stdin/stdout\n\
    --list 1                   Lists all available devices and simulators in the current context. If Xcode is not correctly installed, only devices will be listed.\n\
    --version                  Writes companion version information to stdout.\n\
    --help                     Show this help message and exit.\n\
\n\
  Options:\n\
    --grpc-port PORT           Port to start the grpc companion server on (default: 10882).\n\
    --tls-cert-path PATH       If specified exposed GRPC server will be listening on a TLS enabled socket.\n\
    --grpc-domain-sock PATH    Unix Domain Socket path to start the companion server on, will superceed TCP binding via --grpc-port.\n\
    --debug-port PORT          Port to connect debugger on (default: 10881).\n\
    --log-file-path PATH       Path to write a log file to e.g ./output.log (default: logs to stdErr).\n\
    --log-level info|debug     The log level to use, 'debug' for a higher level of debugging 'info' for a lower level of logging (default 'debug').\n\
    --device-set-path PATH     Path to a custom Simulator device set.\n\
    --only FILTER_OPTION       If provided, will limit interaction to a subset of all available targets\n\
    --headless VALUE           If VALUE is a true value, the Simulator boot's lifecycle will be tied to the lifecycle of this invocation.\n\
    --verify-booted VALUE      If VALUE is a true value, will verify that the Simulator is in a known-booted state before --boot completes. Default is true.\n\
    --terminate-offline VALUE  Terminate if the target goes offline, otherwise the companion will stay alive.\n\
\n\
 Filter Options:\n\
    simulator                  Limit interactions to Simulators only.\n\
    device                     Limit interactions to Devices only.\n\
    ecid:ECID                  Limit interactions to a specific Device ECID\n\
";

static void WriteJSONToStdOut(id json)
{
  NSData *jsonOutput = [NSJSONSerialization dataWithJSONObject:json options:0 error:nil];
  NSMutableData *readyOutput = [NSMutableData dataWithData:jsonOutput];
  [readyOutput appendData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]];
  write(STDOUT_FILENO, readyOutput.bytes, readyOutput.length);
  fflush(stdout);
}

static void WriteTargetToStdOut(id<FBiOSTargetInfo> target)
{
  WriteJSONToStdOut([[FBiOSTargetDescription alloc] initWithTarget:target].asJSON);
}

static FBFuture<FBSimulatorSet *> *SimulatorSetWithPath(NSString *deviceSetPath, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  // Give a more meaningful message if we can't load the frameworks.
  NSError *error = nil;
  if(![FBSimulatorControlFrameworkLoader.essentialFrameworks loadPrivateFrameworks:logger error:&error]) {
    return [FBFuture futureWithError:error];
  }
  FBSimulatorControlConfiguration *configuration = [FBSimulatorControlConfiguration configurationWithDeviceSetPath:deviceSetPath logger:logger reporter:reporter];
  FBSimulatorControl *control = [FBSimulatorControl withConfiguration:configuration error:&error];
  if (!control) {
    return [FBFuture futureWithError:error];
  }
  return [FBFuture futureWithResult:control.set];
}

static FBFuture<FBSimulatorSet *> *SimulatorSet(NSUserDefaults *userDefaults, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  NSString *deviceSetPath = [userDefaults stringForKey:@"-device-set-path"];
  return SimulatorSetWithPath(deviceSetPath, logger, reporter);
}

static FBFuture<FBDeviceSet *> *DeviceSet(id<FBControlCoreLogger> logger, NSString *ecidFilter)
{
  return [[FBFuture
    onQueue:dispatch_get_main_queue() resolveValue:^ FBDeviceSet * (NSError **error) {
      // Give a more meaningful message if we can't load the frameworks.
      if(![FBDeviceControlFrameworkLoader.new loadPrivateFrameworks:logger error:error]) {
        return nil;
      }
      FBDeviceSet *deviceSet = [FBDeviceSet setWithLogger:logger delegate:nil ecidFilter:ecidFilter error:error];
      if (!deviceSet) {
        return nil;
      }
      return deviceSet;
    }]
    delay:0.2]; // This is needed to give the Restorable Devices time to populate.
}

static FBFuture<NSArray<id<FBiOSTargetSet>> *> *DefaultTargetSets(NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  NSString *only = [userDefaults stringForKey:@"-only"];
  if (only) {
    if ([only.lowercaseString containsString:@"simulator"]) {
      [logger log:@"'--only' set for Simulators"];
      return [FBFuture futureWithFutures:@[SimulatorSet(userDefaults, logger, reporter)]];
    }
    if ([only.lowercaseString containsString:@"device"]) {
      [logger log:@"'--only' set for Devices"];
      return [FBFuture futureWithFutures:@[DeviceSet(logger, nil)]];
    }
    if ([only.lowercaseString hasPrefix:@"ecid:"]) {
      NSString *ecid = [only.lowercaseString stringByReplacingOccurrencesOfString:@"ecid:" withString:@""];
      [logger logFormat:@"ECID filter of %@", ecid];
      return [FBFuture futureWithFutures:@[DeviceSet(logger, ecid)]];
    }
    return [[FBIDBError
      describeFormat:@"%@ is not a valid argument for '--only'", only]
      failFuture];
  }
  if (!xcodeAvailable) {
    [logger log:@"Xcode is not available, only Devices will be provided"];
    return [FBFuture futureWithFutures:@[DeviceSet(logger, nil)]];
  }
  [logger log:@"Providing targets across Simulator and Device sets."];
  return [FBFuture futureWithFutures:@[
    SimulatorSet(userDefaults, logger, reporter),
    DeviceSet(logger, nil),
  ]];
}

static FBFuture<id<FBiOSTarget>> *TargetForUDID(NSString *udid, NSUserDefaults *userDefaults, BOOL xcodeAvailable, BOOL warmUp, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  return [DefaultTargetSets(userDefaults, xcodeAvailable, logger, reporter)
    onQueue:dispatch_get_main_queue() fmap:^(NSArray<id<FBiOSTargetSet>> *targetSets) {
      return [FBiOSTargetProvider targetWithUDID:udid targetSets:targetSets warmUp:warmUp logger:logger];
    }];
}

static FBFuture<FBDevice *> *DeviceForECID(NSString *ecid, id<FBControlCoreLogger> logger)
{
  return [DeviceSet(logger, [ecid stringByReplacingOccurrencesOfString:@"ecid:" withString:@""])
    onQueue:dispatch_get_main_queue() fmap:^ FBFuture<NSNull *> * (FBDeviceSet *deviceSet) {
      NSArray<FBDevice *> *devices = deviceSet.allDevices;
      if (devices.count == 0) {
        return [[FBIDBError
          describeFormat:@"No devices %@ matching %@", [FBCollectionInformation oneLineDescriptionFromArray:devices], ecid]
          failFuture];
      }
      return [FBFuture futureWithResult:devices.firstObject];
    }];
}

static FBFuture<FBSimulator *> *SimulatorFuture(NSString *udid, NSUserDefaults *userDefaults, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  return [[SimulatorSet(userDefaults, logger, reporter)
    onQueue:dispatch_get_main_queue() fmap:^(FBSimulatorSet *simulatorSet) {
      return [FBiOSTargetProvider targetWithUDID:udid targetSets:@[simulatorSet] warmUp:NO logger:logger];
    }]
    onQueue:dispatch_get_main_queue() fmap:^(id<FBiOSTarget> target) {
      id<FBSimulatorLifecycleCommands> commands = (id<FBSimulatorLifecycleCommands>) target;
      if (![commands conformsToProtocol:@protocol(FBSimulatorLifecycleCommands)]) {
        return [[FBIDBError
          describeFormat:@"%@ does not support Simulator Lifecycle commands", commands]
          failFuture];
      }
      return [FBFuture futureWithResult:commands];
    }];
}

static FBFuture<NSNull *> *TargetOfflineFuture(id<FBiOSTarget> target, id<FBControlCoreLogger> logger)
{
  return [[target
    resolveLeavesState:FBiOSTargetStateBooted]
    onQueue:target.workQueue doOnResolved:^(id _){
      [target.logger log:@"Target is no longer booted, companion going offline"];
    }];
}

static FBFuture<FBFuture<NSNull *> *> *BootFuture(NSString *udid, NSUserDefaults *userDefaults, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  BOOL headless = [userDefaults boolForKey:@"-headless"];
  BOOL verifyBooted = [userDefaults objectForKey:@"-verify-booted"] == nil ? YES : [userDefaults boolForKey:@"-verify-booted"];
  return [[SimulatorFuture(udid, userDefaults, logger, reporter)
    onQueue:dispatch_get_main_queue() fmap:^(FBSimulator *simulator) {
      // Boot the simulator with the options provided.
      FBSimulatorBootOptions options = FBSimulatorBootConfiguration.defaultConfiguration.options;
      if (headless) {
        [logger logFormat:@"Booting %@ headlessly", udid];
        options = options | FBSimulatorBootOptionsTieToProcessLifecycle;
      } else {
        [logger logFormat:@"Booting %@ normally", udid];
        options = options & ~FBSimulatorBootOptionsTieToProcessLifecycle;
      }
      if (verifyBooted) {
        [logger logFormat:@"Booting %@ with verification", udid];
        options = options | FBSimulatorBootOptionsVerifyUsable;
      } else {
        [logger logFormat:@"Booting %@ without verification", udid];
        options = options & ~FBSimulatorBootOptionsVerifyUsable;
      }
      FBSimulatorBootConfiguration *config = [[FBSimulatorBootConfiguration alloc] initWithOptions:options environment:@{}];
      return [[simulator boot:config] mapReplace:simulator];
    }]
    onQueue:dispatch_get_main_queue() map:^ FBFuture<NSNull *> * (FBSimulator *simulator) {
      // Write the boot success to stdout
      WriteTargetToStdOut(simulator);
      // In a headless boot:
      // - We need to keep this process running until it's otherwise shutdown. When the sim is shutdown this process will die.
      // - If this process is manually killed then the simulator will die
      // For a regular boot the sim will outlive this process.
      if (!headless) {
        return FBFuture.empty;
      }
      // Whilst we can rely on this process being killed shutting the simulator, this is asynchronous.
      // This means that we should attempt to handle cancellation gracefully.
      // In this case we should attempt to shutdown in response to cancellation.
      // This means if this future is cancelled and waited-for before the process exits we will return it in a "Shutdown" state.
      return [TargetOfflineFuture(simulator, logger)
        onQueue:dispatch_get_main_queue() respondToCancellation:^{
          return [simulator shutdown];
        }];
    }];
}

static FBFuture<NSNull *> *ShutdownFuture(NSString *udid, NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  return [TargetForUDID(udid, userDefaults, xcodeAvailable, NO, logger, reporter)
    onQueue:dispatch_get_main_queue() fmap:^ FBFuture<NSNull *> * (id<FBiOSTarget> target) {
      id<FBPowerCommands> commands = (id<FBPowerCommands>) target;
      if (![commands conformsToProtocol:@protocol(FBPowerCommands)]) {
        return [[FBIDBError
          describeFormat:@"Cannot shutdown %@, does not support shutting down", target]
          failFuture];
      }
      return [commands shutdown];
    }];
}

static FBFuture<NSNull *> *RebootFuture(NSString *udid, NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  return [TargetForUDID(udid, userDefaults, xcodeAvailable, NO, logger, reporter)
    onQueue:dispatch_get_main_queue() fmap:^ FBFuture<NSNull *> * (id<FBiOSTarget> target) {
      id<FBPowerCommands> commands = (id<FBPowerCommands>) target;
      if (![commands conformsToProtocol:@protocol(FBPowerCommands)]) {
        return [[FBIDBError
          describeFormat:@"Cannot shutdown %@, does not support rebooting", target]
          failFuture];
      }
      return [commands reboot];
    }];
}

static FBFuture<NSNull *> *EraseFuture(NSString *udid, NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  return [TargetForUDID(udid, userDefaults, xcodeAvailable, NO, logger, reporter)
    onQueue:dispatch_get_main_queue() fmap:^ FBFuture<NSNull *> * (id<FBiOSTarget> target) {
      id<FBEraseCommands> commands = (id<FBEraseCommands>) target;
      if (![commands conformsToProtocol:@protocol(FBEraseCommands)]) {
        return [[FBIDBError
          describeFormat:@"Cannot erase %@, does not support erasing", target]
          failFuture];
      }
      return [commands erase];
    }];
}

static FBFuture<NSNull *> *DeleteFuture(NSString *udidOrAll, NSUserDefaults *userDefaults, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  return [[SimulatorSet(userDefaults, logger, reporter)
    onQueue:dispatch_get_main_queue() fmap:^ FBFuture * (FBSimulatorSet *set) {
      if ([udidOrAll.lowercaseString isEqualToString:@"all"]) {
        return [set deleteAll];
      }
      FBSimulator *simulator = [set simulatorWithUDID:udidOrAll];
      if (!simulator) {
        return [[FBIDBError
          describeFormat:@"Could not find a simulator with udid %@", udidOrAll]
          failFuture];
      }
      return [set delete:simulator];
    }]
    mapReplace:NSNull.null];
}

static FBFuture<NSNull *> *ListFuture(NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  return [DefaultTargetSets(userDefaults, xcodeAvailable, logger, reporter)
    onQueue:dispatch_get_main_queue() map:^ NSNull * (NSArray<id<FBiOSTargetSet>> *targetSets) {
      NSUInteger reportedCount = 0;
      for (id<FBiOSTargetSet> targetSet in targetSets) {
        for (id<FBiOSTargetInfo> targetInfo in targetSet.allTargetInfos) {
          WriteTargetToStdOut(targetInfo);
          reportedCount++;
        }
      }
      [logger logFormat:@"Reported %lu targets to stdout", reportedCount];
      return NSNull.null;
    }];
}

static FBFuture<NSNull *> *CreateFuture(NSString *create, NSUserDefaults *userDefaults, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  return [[SimulatorSet(userDefaults, logger, reporter)
    onQueue:dispatch_get_main_queue() fmap:^ FBFuture<FBSimulator *> * (FBSimulatorSet *set) {
      NSArray<NSString *> *parameters = [create componentsSeparatedByString:@","];
      FBSimulatorConfiguration *config = [FBSimulatorConfiguration defaultConfiguration];
      if (parameters.count > 0) {
        config = [config withDeviceModel:parameters[0]];
      }
      if (parameters.count > 1) {
        config = [config withOSNamed:parameters[1]];
      }
      return [set createSimulatorWithConfiguration:config];
    }]
    onQueue:dispatch_get_main_queue() map:^(FBSimulator *simulator) {
      WriteTargetToStdOut(simulator);
      return NSNull.null;
    }];
}

static FBFuture<NSNull *> *CloneFuture(NSString *udid, NSUserDefaults *userDefaults, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  NSString *destinationSet = [userDefaults stringForKey:@"-clone-destination-set"];
  return [[[FBFuture
    futureWithFutures:@[
      SimulatorFuture(udid, userDefaults, logger, reporter),
      SimulatorSetWithPath(destinationSet, logger, reporter),
    ]]
    onQueue:dispatch_get_main_queue() fmap:^ FBFuture<FBSimulator *> * (NSArray<id> *tuple) {
      FBSimulator *base = tuple[0];
      FBSimulatorSet *destination = tuple[1];
      return [base.set cloneSimulator:base toDeviceSet:destination];
    }]
    onQueue:dispatch_get_main_queue() map:^(FBSimulator *cloned) {
      WriteTargetToStdOut(cloned);
      return NSNull.null;
    }];
}

static FBFuture<NSNull *> *EnterRecoveryFuture(NSString *ecid, id<FBControlCoreLogger> logger)
{
  return [DeviceForECID(ecid, logger)
    onQueue:dispatch_get_main_queue() fmap:^ FBFuture<NSNull *> * (FBDevice *device) {
      return [device enterRecovery];
    }];
}
static FBFuture<NSNull *> *ExitRecoveryFuture(NSString *ecid, id<FBControlCoreLogger> logger)
{
  return [DeviceForECID(ecid, logger)
    onQueue:dispatch_get_main_queue() fmap:^ FBFuture<NSNull *> * (FBDevice *device) {
      return [device exitRecovery];
    }];
}

static FBFuture<NSNull *> *ActivateFuture(NSString *ecid, id<FBControlCoreLogger> logger)
{
  return [DeviceForECID(ecid, logger)
    onQueue:dispatch_get_main_queue() fmap:^ FBFuture<NSNull *> * (FBDevice *device) {
      return [device activate];
    }];
}

static FBFuture<NSNull *> *CleanFuture(NSString *udid, NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  return [TargetForUDID(udid, userDefaults, xcodeAvailable, YES, logger, reporter)
    onQueue:dispatch_get_main_queue() fmap:^FBFuture<NSNull *> *(id<FBiOSTarget> target) {
      NSError *error = nil;
      FBIDBStorageManager *storageManager = [FBIDBStorageManager managerForTarget:target logger:logger error:&error];
      if (!storageManager) {
        return [FBFuture futureWithError:error];
      }
      FBIDBCommandExecutor *commandExecutor = [FBIDBCommandExecutor
        commandExecutorForTarget:target
        storageManager:storageManager
        temporaryDirectory:[FBTemporaryDirectory temporaryDirectoryWithLogger:logger]
        ports:[FBIDBPortsConfiguration portsWithArguments:userDefaults]
        logger:logger];
      return [commandExecutor clean];
    }];
}

static FBFuture<FBFuture<NSNull *> *> *CompanionServerFuture(NSString *udid, NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  BOOL terminateOffline = [userDefaults boolForKey:@"-terminate-offline"];

  return [TargetForUDID(udid, userDefaults, xcodeAvailable, YES, logger, reporter)
    onQueue:dispatch_get_main_queue() fmap:^(id<FBiOSTarget> target) {
      [reporter addMetadata:@{
        @"udid": udid,
        @"target_type": FBiOSTargetTypeStringFromTargetType(target.targetType).lowercaseString,
      }];
      [reporter report:[FBEventReporterSubject subjectForEvent:@"launched"]];
      // Start up the companion
      FBIDBPortsConfiguration *ports = [FBIDBPortsConfiguration portsWithArguments:userDefaults];
      BOOL withSwiftServer = ports.grpcSwiftPort != 0;

      FBTemporaryDirectory *temporaryDirectory = [FBTemporaryDirectory temporaryDirectoryWithLogger:logger];
      NSError *error = nil;

      FBIDBStorageManager *storageManager = [FBIDBStorageManager managerForTarget:target logger:logger error:&error];
      if (!storageManager) {
        return [FBFuture futureWithError:error];
      }

      // Command Executor
      FBIDBCommandExecutor *commandExecutor = [FBIDBCommandExecutor
                                               commandExecutorForTarget:target
                                               storageManager:storageManager
                                               temporaryDirectory:temporaryDirectory
                                               ports:ports
                                               logger:logger];

      FBIDBCommandExecutor *cppCommandExecutor = [FBLoggingWrapper wrap:commandExecutor simplifiedNaming:YES eventReporter:reporter logger:logger];

      FBIDBCommandExecutor *swiftCommandExecutor = [FBLoggingWrapper wrap:commandExecutor simplifiedNaming:YES eventReporter:FBIDBConfiguration.swiftEventReporter logger:logger];

      FBIDBCompanionServer *server = [FBIDBCompanionServer companionForTarget:target commandExecutor: cppCommandExecutor ports:ports eventReporter:reporter logger:logger error:&error];
      if (!server) {
        return [FBFuture futureWithError:error];
      }

      GRPCSwiftServer *swiftServer = nil;

      if (withSwiftServer) {
        swiftServer = [[GRPCSwiftServer alloc] initWithTarget:target
                                              commandExecutor:swiftCommandExecutor
                                                     reporter:reporter
                                                       logger:logger
                                                        ports:ports
                                                        error:&error];
      }

      if (error) {
        return [FBFuture futureWithError:error];
      }

      return [[[server start] onQueue:target.workQueue fmap:^FBFuture * _Nonnull(NSDictionary<NSString *,id> * _Nonnull cppServerDescrption) {
        if (withSwiftServer) {
          return [[swiftServer start] onQueue:target.workQueue map: ^(NSDictionary<NSString *,id> * _Nonnull swiftServerDescription) {
            NSMutableDictionary<NSString *,id> * resultDictionary = [cppServerDescrption mutableCopy];
            [resultDictionary addEntriesFromDictionary:swiftServerDescription];
            return resultDictionary;
          }];
        }
        return [FBFuture futureWithResult: cppServerDescrption];
      }] onQueue:target.workQueue map:^ FBFuture * (NSDictionary<NSString *, id> *serverDescription) {
        WriteJSONToStdOut(serverDescription);
        NSMutableArray<FBFuture<NSNull *> *> *futures = [[NSMutableArray alloc] initWithArray: @[server.completed]];
        if (swiftServer) {
          [futures addObject: swiftServer.completed];
        }

        if (terminateOffline) {
          [logger.info logFormat:@"Companion will terminate when target goes offline"];
          [futures addObject:TargetOfflineFuture(target, logger)];
        } else {
          [logger.info logFormat:@"Companion will stay alive if target goes offline"];
        }

        FBFuture<NSNull *> *completed = [FBFuture race: futures];
        return [completed
          onQueue:target.workQueue chain:^(FBFuture *future) {
            [temporaryDirectory cleanOnExit];
            return future;
          }];
      }];

    }];
}

static FBFuture<FBFuture<NSNull *> *> *NotiferFuture(NSString *notify, NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  return [[[DefaultTargetSets(userDefaults, xcodeAvailable, logger, reporter)
    onQueue:dispatch_get_main_queue() fmap:^(NSArray<id<FBiOSTargetSet>> *targetSets) {
      if ([notify isEqualToString:@"stdout"]) {
        return [FBiOSTargetStateChangeNotifier notifierToStdOutWithTargetSets:targetSets logger:logger];
      }
      return [FBiOSTargetStateChangeNotifier notifierToFilePath:notify withTargetSets:targetSets logger:logger];
    }]
    onQueue:dispatch_get_main_queue() fmap:^(FBiOSTargetStateChangeNotifier *notifier) {
      [logger logFormat:@"Starting Notifier %@", notifier];
      return [[notifier startNotifier] mapReplace:notifier];
    }]
    onQueue:dispatch_get_main_queue() map:^(FBiOSTargetStateChangeNotifier *notifier) {
      [logger logFormat:@"Started Notifier %@", notifier];
      return [notifier.notifierDone
        onQueue:dispatch_get_main_queue() respondToCancellation:^{
          [logger logFormat:@"Stopping Notifier %@", notifier];
          return FBFuture.empty;
        }];
    }];
}

static FBFuture<NSNull *> *ForwardFuture(NSString *forward, NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter)
{
  NSArray<NSString *> *components = [forward componentsSeparatedByString:@":"];
  if (components.count != 2) {
    return [[FBIDBError
      describeFormat:@"%@ should be of the form UDID:PORT", forward]
      failFuture];
  }
  NSString *udid = components[0];
  int remotePort = [components[1] intValue];

  return [TargetForUDID(udid, userDefaults, xcodeAvailable, NO, logger, reporter)
    onQueue:dispatch_get_main_queue() fmap:^ FBFuture<NSNull *> * (id<FBiOSTarget> target) {
      id<FBSocketForwardingCommands> commands = (id<FBSocketForwardingCommands>) target;
      if (![commands conformsToProtocol:@protocol(FBSocketForwardingCommands)]) {
        return [[FBIDBError
          describeFormat:@"%@ does not conform to FBSocketForwardingCommands", target]
          failFuture];
      }
      return [commands drainLocalFileInput:STDIN_FILENO localFileOutput:STDOUT_FILENO remotePort:remotePort];
    }];
}

static FBFuture<FBFuture<NSNull *> *> *GetCompanionCompletedFuture(NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger) {
  NSString *boot = [userDefaults stringForKey:@"-boot"];
  NSString *reboot = [userDefaults stringForKey:@"-reboot"];
  NSString *clone = [userDefaults stringForKey:@"-clone"];
  NSString *create = [userDefaults stringForKey:@"-create"];
  NSString *delete = [userDefaults stringForKey:@"-delete"];
  NSString *erase = [userDefaults stringForKey:@"-erase"];
  NSString *list = [userDefaults stringForKey:@"-list"];
  NSString *notify = [userDefaults stringForKey:@"-notify"];
  NSString *recover = [userDefaults stringForKey:@"-recover"];
  NSString *shutdown = [userDefaults stringForKey:@"-shutdown"];
  NSString *udid = [userDefaults stringForKey:@"-udid"];
  NSString *unrecover = [userDefaults stringForKey:@"-unrecover"];
  NSString *activate = [userDefaults stringForKey:@"-activate"];
  NSString *clean = [userDefaults stringForKey:@"-clean"];
  NSString *forward = [userDefaults stringForKey:@"-forward"];

  id<FBEventReporter> reporter = FBIDBConfiguration.eventReporter;
  if (udid) {
    return CompanionServerFuture(udid, userDefaults, xcodeAvailable, logger, reporter);
  } else if (list) {
    [logger.info log:@"Listing"];
    return [FBFuture futureWithResult:ListFuture(userDefaults, xcodeAvailable, logger, reporter)];
  } else if (notify) {
    [logger.info logFormat:@"Notifying %@", notify];
    return NotiferFuture(notify, userDefaults, xcodeAvailable, logger, reporter);
  } else if (boot) {
    [logger logFormat:@"Booting %@", boot];
    return BootFuture(boot, userDefaults, logger, reporter);
  } else if (shutdown) {
    [logger.info logFormat:@"Shutting down %@", shutdown];
    return [FBFuture futureWithResult:ShutdownFuture(shutdown, userDefaults, xcodeAvailable, logger, reporter)];
  } else if (reboot) {
    [logger.info logFormat:@"Rebooting %@", reboot];
    return [FBFuture futureWithResult:RebootFuture(reboot, userDefaults, xcodeAvailable, logger, reporter)];
  } else if (erase) {
    [logger.info logFormat:@"Erasing %@", erase];
    return [FBFuture futureWithResult:EraseFuture(erase, userDefaults, xcodeAvailable, logger, reporter)];
  } else if (delete) {
    [logger.info logFormat:@"Deleting %@", delete];
    return [FBFuture futureWithResult:DeleteFuture(delete, userDefaults, logger, reporter)];
  } else if (create) {
    [logger.info logFormat:@"Creating %@", create];
    return [FBFuture futureWithResult:CreateFuture(create, userDefaults, logger, reporter)];
  } else if (clone) {
    [logger.info logFormat:@"Cloning %@", clone];
    return [FBFuture futureWithResult:CloneFuture(clone, userDefaults, logger, reporter)];
  } else if (recover) {
    [logger.info logFormat:@"Putting %@ into recovery", recover];
    return [FBFuture futureWithResult:EnterRecoveryFuture(recover, logger)];
  } else if (unrecover) {
    [logger.info logFormat:@"Removing %@ from recovery", recover];
    return [FBFuture futureWithResult:ExitRecoveryFuture(unrecover, logger)];
  } else if (activate) {
    [logger.info logFormat:@"Activating %@", activate];
    return [FBFuture futureWithResult:ActivateFuture(activate, logger)];
  } else if (clean) {
    [logger.info logFormat:@"Cleaning %@", clean];
    return [FBFuture futureWithResult:CleanFuture(clean, userDefaults, xcodeAvailable, logger, reporter)];
  } else if (forward) {
    [logger.info logFormat:@"Forwarding %@", forward];
    return [FBFuture futureWithResult:ForwardFuture(forward, userDefaults, xcodeAvailable, logger, reporter)];
  }
  return [[FBIDBError
    describeFormat:@"You must specify at least one 'Mode of operation'\n\n%s", kUsageHelpMessage]
    failFuture];
}

static FBFuture<NSNumber *> *signalHandlerFuture(uintptr_t signalCode, NSString *exitMessage, id<FBControlCoreLogger> logger)
{
  dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
  FBMutableFuture<NSNumber *> *future = FBMutableFuture.future;
  dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, signalCode, 0, dispatch_get_main_queue());
  dispatch_source_set_event_handler(source, ^{
    [logger.error log:exitMessage];
    [future resolveWithResult:@(signalCode)];
  });
  dispatch_resume(source);
  struct sigaction action = {{0}};
  action.sa_handler = SIG_IGN;
  sigaction((int)signalCode, &action, NULL);
  return [future
    onQueue:queue notifyOfCompletion:^(FBFuture *_) {
      dispatch_cancel(source);
    }];
}

static NSString *EnvDescription()
{
  return [FBCollectionInformation oneLineDescriptionFromDictionary:FBControlCoreGlobalConfiguration.safeSubprocessEnvironment];
}

static NSString *ArchName()
{
#if TARGET_CPU_ARM64
  return @"arm64";
#elif TARGET_CPU_X86_64
  return @"x86_64";
#else
  return @"not supported");
#endif
}

static void logStartupInfo(FBIDBLogger *logger)
{
    [logger.info logFormat:@"IDB Companion Built at %s %s", __DATE__, __TIME__];
    [logger.info logFormat:@"IDB Companion architecture %@", ArchName()];
    [logger.info logFormat:@"Invoked with args=%@ env=%@", [FBCollectionInformation oneLineDescriptionFromArray:NSProcessInfo.processInfo.arguments], EnvDescription()];
}

int main(int argc, const char *argv[]) {
  @autoreleasepool
  {
    NSArray<NSString *> *arguments = NSProcessInfo.processInfo.arguments;
    if ([arguments containsObject:@"--help"]) {
      fprintf(stderr, "%s", kUsageHelpMessage);
      return 1;
    }
    if ([arguments containsObject:@"--version"]) {
      WriteJSONToStdOut(@{@"build_time": @(__TIME__), @"build_date": @(__DATE__)});
      return 0;
    }

    NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults;
    FBIDBLogger *logger = [FBIDBLogger loggerWithUserDefaults:userDefaults];
    logStartupInfo(logger);

    NSError *error = nil;

    // Check that xcode-select returns a valid path, throw a big
    // warning if not
    BOOL xcodeAvailable = [FBXcodeDirectory.xcodeSelectDeveloperDirectory await:&error] != nil;
    if (!xcodeAvailable) {
      [logger.error logFormat:@"Xcode is not available, idb will not be able to use Simulators: %@", error];
      error = nil;
    }

    FBFuture<NSNumber *> *signalled = [FBFuture race:@[
      signalHandlerFuture(SIGINT, @"Signalled: SIGINT", logger),
      signalHandlerFuture(SIGTERM, @"Signalled: SIGTERM", logger),
    ]];
    FBFuture<NSNull *> *companionCompleted = [GetCompanionCompletedFuture(userDefaults, xcodeAvailable, logger) await:&error];
    if (!companionCompleted) {
      [logger.error log:error.localizedDescription];
      return 1;
    }

    FBFuture<NSNull *> *completed = [FBFuture race:@[
      companionCompleted,
      signalled,
    ]];
    if (completed.error) {
      [logger.error log:completed.error.localizedDescription];
      return 1;
    }
    id result = [completed await:&error];
    if (!result) {
      [logger.error log:error.localizedDescription];
      return 1;
    }
    if (companionCompleted.state == FBFutureStateCancelled) {
      [logger logFormat:@"Responding to termination of idb with signo %@", result];
      FBFuture<NSNull *> *cancellation = [companionCompleted cancel];
      result = [cancellation await:&error];
      if (!result) {
        [logger.error log:error.localizedDescription];
        return 1;
      }
    }
  }
  return 0;
}
