// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:meta/meta.dart';
import 'package:process/process.dart';

import '../artifacts.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/version.dart';
import '../build_info.dart';
import '../cache.dart';
import '../convert.dart';
import '../device.dart';
import '../globals.dart' as globals;
import '../ios/devices.dart';
import '../ios/ios_deploy.dart';
import '../ios/iproxy.dart';
import '../ios/mac.dart';
import '../reporting/reporting.dart';
import 'xcode.dart';

class XCDeviceEventNotification {
  XCDeviceEventNotification(
    this.eventType,
    this.eventInterface,
    this.deviceIdentifier,
  );

  final XCDeviceEvent eventType;
  final XCDeviceEventInterface eventInterface;
  final String deviceIdentifier;
}

enum XCDeviceEvent {
  attach,
  detach,
}

enum XCDeviceEventInterface {
  usb(name: 'usb', connectionInterface: DeviceConnectionInterface.attached),
  wifi(name: 'wifi', connectionInterface: DeviceConnectionInterface.wireless);

  const XCDeviceEventInterface({
    required this.name,
    required this.connectionInterface,
  });

  final String name;
  final DeviceConnectionInterface connectionInterface;
}

/// A utility class for interacting with Xcode xcdevice command line tools.
class XCDevice {
  XCDevice({
    required Artifacts artifacts,
    required Cache cache,
    required ProcessManager processManager,
    required Logger logger,
    required Xcode xcode,
    required Platform platform,
    required IProxy iproxy,
  }) : _processUtils = ProcessUtils(logger: logger, processManager: processManager),
      _logger = logger,
      _iMobileDevice = IMobileDevice(
        artifacts: artifacts,
        cache: cache,
        logger: logger,
        processManager: processManager,
      ),
      _iosDeploy = IOSDeploy(
        artifacts: artifacts,
        cache: cache,
        logger: logger,
        platform: platform,
        processManager: processManager,
      ),
      _iProxy = iproxy,
      _xcode = xcode {

    _setupDeviceIdentifierByEventStream();
  }

  void dispose() {
    _usbDeviceObserveProcess?.kill();
    _wifiDeviceObserveProcess?.kill();
    _usbDeviceWaitProcess?.kill();
    _wifiDeviceWaitProcess?.kill();
  }

  final ProcessUtils _processUtils;
  final Logger _logger;
  final IMobileDevice _iMobileDevice;
  final IOSDeploy _iosDeploy;
  final Xcode _xcode;
  final IProxy _iProxy;

  List<Object>? _cachedListResults;

  Process? _usbDeviceObserveProcess;
  Process? _wifiDeviceObserveProcess;
  StreamController<XCDeviceEventNotification>? _observeStreamController;

  @visibleForTesting
  StreamController<XCDeviceEventNotification>? waitStreamController;

  Process? _usbDeviceWaitProcess;
  Process? _wifiDeviceWaitProcess;

  void _setupDeviceIdentifierByEventStream() {
    // _observeStreamController Should always be available for listeners
    // in case polling needs to be stopped and restarted.
    _observeStreamController = StreamController<XCDeviceEventNotification>.broadcast(
      onListen: _startObservingTetheredIOSDevices,
      onCancel: _stopObservingTetheredIOSDevices,
    );
  }

  bool get isInstalled => _xcode.isInstalledAndMeetsVersionCheck;

  Future<List<Object>?> _getAllDevices({
    bool useCache = false,
    required Duration timeout,
  }) async {
    if (!isInstalled) {
      _logger.printTrace("Xcode not found. Run 'flutter doctor' for more information.");
      return null;
    }
    if (useCache && _cachedListResults != null) {
      return _cachedListResults;
    }
    try {
      // USB-tethered devices should be found quickly. 1 second timeout is faster than the default.
      final RunResult result = await _processUtils.run(
        <String>[
          ..._xcode.xcrunCommand(),
          'xcdevice',
          'list',
          '--timeout',
          timeout.inSeconds.toString(),
        ],
        throwOnError: true,
      );
      if (result.exitCode == 0) {
        final String listOutput = result.stdout;
        try {
          final List<Object> listResults = (json.decode(result.stdout) as List<Object?>).whereType<Object>().toList();
          _cachedListResults = listResults;
          return listResults;
        } on FormatException {
          // xcdevice logs errors and crashes to stdout.
          _logger.printError('xcdevice returned non-JSON response: $listOutput');
          return null;
        }
      }
      _logger.printTrace('xcdevice returned an error:\n${result.stderr}');
    } on ProcessException catch (exception) {
      _logger.printTrace('Process exception running xcdevice list:\n$exception');
    } on ArgumentError catch (exception) {
      _logger.printTrace('Argument exception running xcdevice list:\n$exception');
    }

    return null;
  }

  /// Observe identifiers (UDIDs) of devices as they attach and detach.
  ///
  /// Each attach and detach event contains information on the event type,
  /// the event interface, and the device identifer.
  Stream<XCDeviceEventNotification>? observedDeviceEvents() {
    if (!isInstalled) {
      _logger.printTrace("Xcode not found. Run 'flutter doctor' for more information.");
      return null;
    }
    return _observeStreamController?.stream;
  }

  // Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
  // Attach: 00008027-00192736010F802E
  // Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
  final RegExp _observationIdentifierPattern = RegExp(r'^(\w*): ([\w-]*)$');

  Future<void> _startObservingTetheredIOSDevices() async {
    try {
      if (_usbDeviceObserveProcess != null || _wifiDeviceObserveProcess != null) {
        throw Exception('xcdevice observe restart failed');
      }

      _usbDeviceObserveProcess = await _startObserveProcess(
        XCDeviceEventInterface.usb,
      );

      _wifiDeviceObserveProcess = await _startObserveProcess(
        XCDeviceEventInterface.wifi,
      );

      final Future<void> usbProcessExited = _usbDeviceObserveProcess!.exitCode.then((int status) {
        _logger.printTrace('xcdevice observe --usb exited with code $exitCode');
        // Kill other process in case only one was killed.
        _wifiDeviceObserveProcess?.kill();
      });

      final Future<void> wifiProcessExited = _wifiDeviceObserveProcess!.exitCode.then((int status) {
        _logger.printTrace('xcdevice observe --wifi exited with code $exitCode');
        // Kill other process in case only one was killed.
        _usbDeviceObserveProcess?.kill();
      });

      unawaited(Future.wait(<Future<void>>[
        usbProcessExited,
        wifiProcessExited,
      ]).whenComplete(() async {
        if (_observeStreamController?.hasListener ?? false) {
          // Tell listeners the process died.
          await _observeStreamController?.close();
        }
        _usbDeviceObserveProcess = null;
        _wifiDeviceObserveProcess = null;

        // Reopen it so new listeners can resume polling.
        _setupDeviceIdentifierByEventStream();
      }));
    } on ProcessException catch (exception, stackTrace) {
      _observeStreamController?.addError(exception, stackTrace);
    } on ArgumentError catch (exception, stackTrace) {
      _observeStreamController?.addError(exception, stackTrace);
    }
  }

  Future<Process> _startObserveProcess(XCDeviceEventInterface eventInterface) {
    // Run in interactive mode (via script) to convince
    // xcdevice it has a terminal attached in order to redirect stdout.
    return _streamXCDeviceEventCommand(
      <String>[
        'script',
        '-t',
        '0',
        '/dev/null',
        ..._xcode.xcrunCommand(),
        'xcdevice',
        'observe',
        '--${eventInterface.name}',
      ],
      prefix: 'xcdevice observe --${eventInterface.name}: ',
      mapFunction: (String line) {
        final XCDeviceEventNotification? event = _processXCDeviceStdOut(
          line,
          eventInterface,
        );
        if (event != null) {
          _observeStreamController?.add(event);
        }
        return line;
      },
    );
  }

  /// Starts the command and streams stdout/stderr from the child process to
  /// this process' stdout/stderr.
  ///
  /// If [mapFunction] is present, all lines are forwarded to [mapFunction] for
  /// further processing.
  Future<Process> _streamXCDeviceEventCommand(
    List<String> cmd, {
    String prefix = '',
    StringConverter? mapFunction,
  }) async {
    final Process process = await _processUtils.start(cmd);

    final StreamSubscription<String> stdoutSubscription = process.stdout
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
      .listen((String line) {
        String? mappedLine = line;
        if (mapFunction != null) {
          mappedLine = mapFunction(line);
        }
        if (mappedLine != null) {
          final String message = '$prefix$mappedLine';
          _logger.printTrace(message);
        }
      });
    final StreamSubscription<String> stderrSubscription = process.stderr
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
      .listen((String line) {
        String? mappedLine = line;
        if (mapFunction != null) {
          mappedLine = mapFunction(line);
        }
        if (mappedLine != null) {
          _logger.printError('$prefix$mappedLine', wrap: false);
        }
      });

    unawaited(process.exitCode.whenComplete(() {
      stdoutSubscription.cancel();
      stderrSubscription.cancel();
    }));

    return process;
  }

  void _stopObservingTetheredIOSDevices() {
    _usbDeviceObserveProcess?.kill();
    _wifiDeviceObserveProcess?.kill();
  }

  XCDeviceEventNotification? _processXCDeviceStdOut(
    String line,
    XCDeviceEventInterface eventInterface,
  ) {
    // xcdevice observe example output of UDIDs:
    //
    // Listening for all devices, on both interfaces.
    // Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
    // Attach: 00008027-00192736010F802E
    // Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
    // Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
    final RegExpMatch? match = _observationIdentifierPattern.firstMatch(line);
    if (match != null && match.groupCount == 2) {
      final String verb = match.group(1)!.toLowerCase();
      final String identifier = match.group(2)!;
      if (verb.startsWith('attach')) {
        return XCDeviceEventNotification(
          XCDeviceEvent.attach,
          eventInterface,
          identifier,
        );
      } else if (verb.startsWith('detach')) {
        return XCDeviceEventNotification(
          XCDeviceEvent.detach,
          eventInterface,
          identifier,
        );
      }
    }
    return null;
  }

  /// Wait for a connect event for a specific device. Must use device's exact UDID.
  ///
  /// To cancel this process, call [cancelWaitForDeviceToConnect].
  Future<XCDeviceEventNotification?> waitForDeviceToConnect(
    String deviceId,
  ) async {
    try {
      if (_usbDeviceWaitProcess != null || _wifiDeviceWaitProcess != null) {
        throw Exception('xcdevice wait restart failed');
      }

      waitStreamController = StreamController<XCDeviceEventNotification>();

      _usbDeviceWaitProcess = await _startWaitProcess(
        deviceId,
        XCDeviceEventInterface.usb,
      );

      _wifiDeviceWaitProcess = await _startWaitProcess(
        deviceId,
        XCDeviceEventInterface.wifi,
      );

      final Future<void> usbProcessExited = _usbDeviceWaitProcess!.exitCode.then((int status) {
        _logger.printTrace('xcdevice wait --usb exited with code $exitCode');
        // Kill other process in case only one was killed.
        _wifiDeviceWaitProcess?.kill();
      });

      final Future<void> wifiProcessExited = _wifiDeviceWaitProcess!.exitCode.then((int status) {
        _logger.printTrace('xcdevice wait --wifi exited with code $exitCode');
        // Kill other process in case only one was killed.
        _usbDeviceWaitProcess?.kill();
      });

      final Future<void> allProcessesExited = Future.wait(
          <Future<void>>[
            usbProcessExited,
            wifiProcessExited,
          ]).whenComplete(() async {
        _usbDeviceWaitProcess = null;
        _wifiDeviceWaitProcess = null;
        await waitStreamController?.close();
      });

      return await Future.any(
        <Future<XCDeviceEventNotification?>>[
          allProcessesExited.then((_) => null),
          waitStreamController!.stream.first.whenComplete(() async {
            cancelWaitForDeviceToConnect();
          }),
        ],
      );
    } on ProcessException catch (exception, stackTrace) {
      _logger.printTrace('Process exception running xcdevice wait:\n$exception\n$stackTrace');
    } on ArgumentError catch (exception, stackTrace) {
      _logger.printTrace('Process exception running xcdevice wait:\n$exception\n$stackTrace');
    } on StateError {
      _logger.printTrace('Stream broke before first was found');
      return null;
    }
    return null;
  }

  Future<Process> _startWaitProcess(String deviceId, XCDeviceEventInterface eventInterface) {
    // Run in interactive mode (via script) to convince
    // xcdevice it has a terminal attached in order to redirect stdout.
    return _streamXCDeviceEventCommand(
      <String>[
        'script',
        '-t',
        '0',
        '/dev/null',
        ..._xcode.xcrunCommand(),
        'xcdevice',
        'wait',
        '--${eventInterface.name}',
        deviceId,
      ],
      prefix: 'xcdevice wait --${eventInterface.name}: ',
      mapFunction: (String line) {
        final XCDeviceEventNotification? event = _processXCDeviceStdOut(
          line,
          eventInterface,
        );
        if (event != null && event.eventType == XCDeviceEvent.attach) {
          waitStreamController?.add(event);
        }
        return line;
      },
    );
  }

  void cancelWaitForDeviceToConnect() {
    _usbDeviceWaitProcess?.kill();
    _wifiDeviceWaitProcess?.kill();
  }

  /// A list of [IOSDevice]s. This list includes connected devices and
  /// disconnected wireless devices.
  ///
  /// Sometimes devices may have incorrect connection information
  /// (`isConnected`, `connectionInterface`) if it timed out before it could get the
  /// information. Wireless devices can take longer to get the correct
  /// information.
  ///
  /// [timeout] defaults to 2 seconds.
  Future<List<IOSDevice>> getAvailableIOSDevices({ Duration? timeout }) async {
    final List<Object>? allAvailableDevices = await _getAllDevices(timeout: timeout ?? const Duration(seconds: 2));

    if (allAvailableDevices == null) {
      return const <IOSDevice>[];
    }

    // [
    //  {
    //    "simulator" : true,
    //    "operatingSystemVersion" : "13.3 (17K446)",
    //    "available" : true,
    //    "platform" : "com.apple.platform.appletvsimulator",
    //    "modelCode" : "AppleTV5,3",
    //    "identifier" : "CBB5E1ED-2172-446E-B4E7-F2B5823DBBA6",
    //    "architecture" : "x86_64",
    //    "modelName" : "Apple TV",
    //    "name" : "Apple TV"
    //  },
    //  {
    //    "simulator" : false,
    //    "operatingSystemVersion" : "13.3 (17C54)",
    //    "interface" : "usb",
    //    "available" : true,
    //    "platform" : "com.apple.platform.iphoneos",
    //    "modelCode" : "iPhone8,1",
    //    "identifier" : "d83d5bc53967baa0ee18626ba87b6254b2ab5418",
    //    "architecture" : "arm64",
    //    "modelName" : "iPhone 6s",
    //    "name" : "iPhone"
    //  },
    //  {
    //    "simulator" : true,
    //    "operatingSystemVersion" : "6.1.1 (17S445)",
    //    "available" : true,
    //    "platform" : "com.apple.platform.watchsimulator",
    //    "modelCode" : "Watch5,4",
    //    "identifier" : "2D74FB11-88A0-44D0-B81E-C0C142B1C94A",
    //    "architecture" : "i386",
    //    "modelName" : "Apple Watch Series 5 - 44mm",
    //    "name" : "Apple Watch Series 5 - 44mm"
    //  },
    // ...

    final Map<String, IOSDevice> deviceMap = <String, IOSDevice>{};
    for (final Object device in allAvailableDevices) {
      if (device is Map<String, Object?>) {
        // Only include iPhone, iPad, iPod, or other iOS devices.
        if (!_isIPhoneOSDevice(device)) {
          continue;
        }
        final String? identifier = device['identifier'] as String?;
        final String? name = device['name'] as String?;
        if (identifier == null || name == null) {
          continue;
        }
        bool devModeEnabled = true;
        bool isConnected = true;
        final Map<String, Object?>? errorProperties = _errorProperties(device);
        if (errorProperties != null) {
          final String? errorMessage = _parseErrorMessage(errorProperties);
          if (errorMessage != null) {
            if (errorMessage.contains('not paired')) {
              UsageEvent('device', 'ios-trust-failure', flutterUsage: globals.flutterUsage).send();
            }
            _logger.printTrace(errorMessage);
          }

          final int? code = _errorCode(errorProperties);

          // Temporary error -10: iPhone is busy: Preparing debugger support for iPhone.
          // Sometimes the app launch will fail on these devices until Xcode is done setting up the device.
          // Other times this is a false positive and the app will successfully launch despite the error.
          if (code != -10) {
            isConnected = false;
          }

          if (code == 6) {
            devModeEnabled = false;
          }
        }

        String? sdkVersionString = _sdkVersion(device);

        if (sdkVersionString != null) {
          final String? buildVersion = _buildVersion(device);
          if (buildVersion != null) {
            sdkVersionString = '$sdkVersionString $buildVersion';
          }
        }

        // Duplicate entries started appearing in Xcode 15, possibly due to
        // Xcode's new device connectivity stack.
        // If a duplicate entry is found in `xcdevice list`, don't overwrite
        // existing entry when the existing entry indicates the device is
        // connected and the current entry indicates the device is not connected.
        // Don't overwrite if current entry's sdkVersion is null.
        // Don't overwrite if both entries indicate the device is not
        // connected and the existing entry has a higher sdkVersion.
        if (deviceMap.containsKey(identifier)) {
          final IOSDevice deviceInMap = deviceMap[identifier]!;
          if ((deviceInMap.isConnected && !isConnected) || sdkVersionString == null) {
            continue;
          }

          final Version? sdkVersion = Version.parse(sdkVersionString);
          if (!deviceInMap.isConnected &&
              !isConnected &&
              sdkVersion != null &&
              deviceInMap.sdkVersion != null &&
              deviceInMap.sdkVersion!.compareTo(sdkVersion) > 0) {
            continue;
          }
        }

        deviceMap[identifier] = IOSDevice(
          identifier,
          name: name,
          cpuArchitecture: _cpuArchitecture(device),
          connectionInterface: _interfaceType(device),
          isConnected: isConnected,
          sdkVersion: sdkVersionString,
          iProxy: _iProxy,
          fileSystem: globals.fs,
          logger: _logger,
          iosDeploy: _iosDeploy,
          iMobileDevice: _iMobileDevice,
          platform: globals.platform,
          devModeEnabled: devModeEnabled,
        );
      }
    }
    return deviceMap.values.toList();
  }

  /// Despite the name, com.apple.platform.iphoneos includes iPhone, iPads, and all iOS devices.
  /// Excludes simulators.
  static bool _isIPhoneOSDevice(Map<String, Object?> deviceProperties) {
    final Object? platform = deviceProperties['platform'];
    if (platform is String) {
      return platform == 'com.apple.platform.iphoneos';
    }
    return false;
  }

  static Map<String, Object?>? _errorProperties(Map<String, Object?> deviceProperties) {
    final Object? error = deviceProperties['error'];
    return error is Map<String, Object?> ? error : null;
  }

  static int? _errorCode(Map<String, Object?>? errorProperties) {
    if (errorProperties == null) {
      return null;
    }
    final Object? code = errorProperties['code'];
    return code is int ? code : null;
  }

  static DeviceConnectionInterface _interfaceType(Map<String, Object?> deviceProperties) {
    // Interface can be "usb" or "network". It can also be missing
    // (e.g. simulators do not have an interface property).
    // If the interface is "network", use `DeviceConnectionInterface.wireless`,
    // otherwise use `DeviceConnectionInterface.attached.
    final Object? interface = deviceProperties['interface'];
    if (interface is String && interface.toLowerCase() == 'network') {
      return DeviceConnectionInterface.wireless;
    }
    return DeviceConnectionInterface.attached;
  }

  static String? _sdkVersion(Map<String, Object?> deviceProperties) {
    final Object? operatingSystemVersion = deviceProperties['operatingSystemVersion'];
    if (operatingSystemVersion is String) {
      // Parse out the OS version, ignore the build number in parentheses.
      // "13.3 (17C54)"
      final RegExp operatingSystemRegex = RegExp(r'(.*) \(.*\)$');
      if (operatingSystemRegex.hasMatch(operatingSystemVersion.trim())) {
        return operatingSystemRegex.firstMatch(operatingSystemVersion.trim())?.group(1);
      }
      return operatingSystemVersion;
    }
    return null;
  }

  static String? _buildVersion(Map<String, Object?> deviceProperties) {
    final Object? operatingSystemVersion = deviceProperties['operatingSystemVersion'];
    if (operatingSystemVersion is String) {
      // Parse out the build version, for example 17C54 from "13.3 (17C54)".
      final RegExp buildVersionRegex = RegExp(r'\(.*\)$');
      return buildVersionRegex.firstMatch(operatingSystemVersion)?.group(0)?.replaceAll(RegExp('[()]'), '');
    }
    return null;
  }

  DarwinArch _cpuArchitecture(Map<String, Object?> deviceProperties) {
    DarwinArch? cpuArchitecture;
    final Object? architecture = deviceProperties['architecture'];
    if (architecture is String) {
      try {
        cpuArchitecture = getIOSArchForName(architecture);
      } on Exception {
        // Fallback to default iOS architecture. Future-proof against a
        // theoretical version of Xcode that changes this string to something
        // slightly different like "ARM64", or armv7 variations like
        // armv7s and armv7f.
        if (architecture.startsWith('armv7')) {
          cpuArchitecture = DarwinArch.armv7;
        } else {
          cpuArchitecture = DarwinArch.arm64;
        }
        _logger.printWarning(
          'Unknown architecture $architecture, defaulting to '
          '${cpuArchitecture.name}',
        );
      }
    }
    return cpuArchitecture ?? DarwinArch.arm64;
  }

  /// Error message parsed from xcdevice. null if no error.
  static String? _parseErrorMessage(Map<String, Object?>? errorProperties) {
    //  {
    //    "simulator" : false,
    //    "operatingSystemVersion" : "13.3 (17C54)",
    //    "interface" : "usb",
    //    "available" : false,
    //    "platform" : "com.apple.platform.iphoneos",
    //    "modelCode" : "iPhone8,1",
    //    "identifier" : "98206e7a4afd4aedaff06e687594e089dede3c44",
    //    "architecture" : "arm64",
    //    "modelName" : "iPhone 6s",
    //    "name" : "iPhone",
    //    "error" : {
    //      "code" : -9,
    //      "failureReason" : "",
    //      "underlyingErrors" : [
    //        {
    //          "code" : 5,
    //          "failureReason" : "allowsSecureServices: 1. isConnected: 0. Platform: <DVTPlatform:0x7f804ce32880:'com.apple.platform.iphoneos':<DVTFilePath:0x7f804ce32800:'\/Users\/magder\/Applications\/Xcode_11-3-1.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform'>>. DTDKDeviceIdentifierIsIDID: 0",
    //          "description" : "📱<DVTiOSDevice (0x7f801f190450), iPhone, iPhone, 13.3 (17C54), d83d5bc53967baa0ee18626ba87b6254b2ab5418> -- Failed _shouldMakeReadyForDevelopment check even though device is not locked by passcode.",
    //          "recoverySuggestion" : "",
    //          "domain" : "com.apple.platform.iphoneos"
    //        }
    //      ],
    //      "description" : "iPhone is not paired with your computer.",
    //      "recoverySuggestion" : "To use iPhone with Xcode, unlock it and choose to trust this computer when prompted.",
    //      "domain" : "com.apple.platform.iphoneos"
    //    }
    //  },
    //  {
    //    "simulator" : false,
    //    "operatingSystemVersion" : "13.3 (17C54)",
    //    "interface" : "usb",
    //    "available" : false,
    //    "platform" : "com.apple.platform.iphoneos",
    //    "modelCode" : "iPhone8,1",
    //    "identifier" : "d83d5bc53967baa0ee18626ba87b6254b2ab5418",
    //    "architecture" : "arm64",
    //    "modelName" : "iPhone 6s",
    //    "name" : "iPhone",
    //    "error" : {
    //      "code" : -9,
    //      "failureReason" : "",
    //      "description" : "iPhone is not paired with your computer.",
    //      "domain" : "com.apple.platform.iphoneos"
    //    }
    //  }
    // ...

    if (errorProperties == null) {
      return null;
    }

    final StringBuffer errorMessage = StringBuffer('Error: ');

    final Object? description = errorProperties['description'];
    if (description is String) {
      errorMessage.write(description);
      if (!description.endsWith('.')) {
        errorMessage.write('.');
      }
    } else {
      errorMessage.write('Xcode pairing error.');
    }

    final Object? recoverySuggestion = errorProperties['recoverySuggestion'];
    if (recoverySuggestion is String) {
      errorMessage.write(' $recoverySuggestion');
    }

    final int? code = _errorCode(errorProperties);
    if (code != null) {
      errorMessage.write(' (code $code)');
    }

    return errorMessage.toString();
  }

  /// List of all devices reporting errors.
  Future<List<String>> getDiagnostics() async {
    final List<Object>? allAvailableDevices = await _getAllDevices(
      useCache: true,
      timeout: const Duration(seconds: 2)
    );

    if (allAvailableDevices == null) {
      return const <String>[];
    }

    final List<String> diagnostics = <String>[];
    for (final Object deviceProperties in allAvailableDevices) {
      if (deviceProperties is! Map<String, Object?>) {
        continue;
      }
      final Map<String, Object?>? errorProperties = _errorProperties(deviceProperties);
      final String? errorMessage = _parseErrorMessage(errorProperties);
      if (errorMessage != null) {
        final int? code = _errorCode(errorProperties);
        // Error -13: iPhone is not connected. Xcode will continue when iPhone is connected.
        // This error is confusing since the device is not connected and maybe has not been connected
        // for a long time. Avoid showing it.
        if (code == -13 && errorMessage.contains('not connected')) {
          continue;
        }

        diagnostics.add(errorMessage);
      }
    }
    return diagnostics;
  }
}
