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

import os from 'os';
import path from 'path';
import electron from 'electron';
import {getInstance as getLogger} from '../fb-stubs/Logger';
import {Store, State as ReduxState, MiddlewareAPI} from '../reducers';
import {DeviceExport} from '../devices/BaseDevice';
import {State as PluginStatesState} from '../reducers/pluginStates';
import {State as PluginsState} from '../reducers/plugins';
import {PluginNotification} from '../reducers/notifications';
import Client, {ClientExport, ClientQuery} from '../Client';
import {pluginKey} from '../reducers/pluginStates';
import {
  FlipperDevicePlugin,
  callClient,
  supportsMethod,
  PluginDefinition,
  DevicePluginMap,
  ClientPluginMap,
  isSandyPlugin,
} from '../plugin';
import {default as BaseDevice} from '../devices/BaseDevice';
import {default as ArchivedDevice} from '../devices/ArchivedDevice';
import fs from 'fs';
import {v4 as uuidv4} from 'uuid';
import {remote, OpenDialogOptions} from 'electron';
import {readCurrentRevision} from './packageMetadata';
import {tryCatchReportPlatformFailures} from './metrics';
import {promisify} from 'util';
import promiseTimeout from './promiseTimeout';
import {Idler} from './Idler';
import {setStaticView} from '../reducers/connections';
import {
  resetSupportFormV2State,
  SupportFormRequestDetailsState,
} from '../reducers/supportForm';
import {setSelectPluginsToExportActiveSheet} from '../reducers/application';
import {deconstructClientId, deconstructPluginKey} from '../utils/clientUtils';
import {performance} from 'perf_hooks';
import {processMessageQueue} from './messageQueue';
import {getPluginTitle} from './pluginUtils';
import {capture} from './screenshot';
import {uploadFlipperMedia} from '../fb-stubs/user';

export const IMPORT_FLIPPER_TRACE_EVENT = 'import-flipper-trace';
export const EXPORT_FLIPPER_TRACE_EVENT = 'export-flipper-trace';
export const EXPORT_FLIPPER_TRACE_TIME_SERIALIZATION_EVENT = `${EXPORT_FLIPPER_TRACE_EVENT}:serialization`;

// maps clientId -> pluginId -> persistence key -> state
export type SandyPluginStates = Record<
  string,
  Record<string, Record<string, any>>
>;

export type PluginStatesExportState = {
  [pluginKey: string]: string;
};

export type ExportType = {
  fileVersion: string;
  flipperReleaseRevision: string | undefined;
  clients: Array<ClientExport>;
  device: DeviceExport | null;
  deviceScreenshot: string | null;
  store: {
    pluginStates: PluginStatesExportState;
    activeNotifications: Array<PluginNotification>;
  };
  pluginStates2: SandyPluginStates;
  supportRequestDetails?: SupportFormRequestDetailsState;
};

type ProcessPluginStatesOptions = {
  clients: Array<ClientExport>;
  serial: string;
  allPluginStates: PluginStatesState;
  devicePlugins: Map<string, typeof FlipperDevicePlugin>;
  selectedPlugins: Array<string>;
  statusUpdate?: (msg: string) => void;
};

type ProcessNotificationStatesOptions = {
  clients: Array<ClientExport>;
  serial: string;
  allActiveNotifications: Array<PluginNotification>;
  devicePlugins: Map<string, typeof FlipperDevicePlugin>;
  statusUpdate?: (msg: string) => void;
};

type SerializePluginStatesOptions = {
  pluginStates: PluginStatesState;
};

type PluginsToProcess = {
  pluginKey: string;
  pluginId: string;
  pluginName: string;
  pluginClass: PluginDefinition;
  client: Client;
}[];

type AddSaltToDeviceSerialOptions = {
  salt: string;
  device: BaseDevice;
  deviceScreenshot: string | null;
  clients: Array<ClientExport>;
  pluginStates: PluginStatesExportState;
  pluginStates2: SandyPluginStates;
  pluginNotification: Array<PluginNotification>;
  selectedPlugins: Array<string>;
  statusUpdate?: (msg: string) => void;
};

export function displayFetchMetadataErrors(
  fetchMetaDataErrors: {
    [plugin: string]: Error;
  } | null,
): {title: string; errorArray: Array<Error>} {
  const errors = fetchMetaDataErrors ? Object.values(fetchMetaDataErrors) : [];
  const pluginsWithFetchMetadataErrors = fetchMetaDataErrors
    ? Object.keys(fetchMetaDataErrors)
    : [];
  const title =
    fetchMetaDataErrors && pluginsWithFetchMetadataErrors.length > 0
      ? `Export was successfull, but plugin${
          pluginsWithFetchMetadataErrors.length > 1 ? 's' : ''
        } ${pluginsWithFetchMetadataErrors.join(
          ', ',
        )} might be ignored because of the following errors.`
      : '';
  return {title, errorArray: errors};
}

export function processClients(
  clients: Array<ClientExport>,
  serial: string,
  statusUpdate?: (msg: string) => void,
): Array<ClientExport> {
  statusUpdate &&
    statusUpdate(`Filtering Clients for the device id ${serial}...`);
  const filteredClients = clients.filter(
    (client) => client.query.device_id === serial,
  );
  return filteredClients;
}

export function processPluginStates(
  options: ProcessPluginStatesOptions,
): PluginStatesState {
  const {
    clients,
    serial,
    allPluginStates,
    devicePlugins,
    selectedPlugins,
    statusUpdate,
  } = options;

  let pluginStates: PluginStatesState = {};
  statusUpdate &&
    statusUpdate('Filtering the plugin states for the filtered Clients...');
  for (const key in allPluginStates) {
    const plugin = deconstructPluginKey(key);

    const pluginName = plugin.pluginName;
    if (
      pluginName &&
      selectedPlugins.length > 0 &&
      !selectedPlugins.includes(pluginName)
    ) {
      continue;
    }
    if (plugin.type === 'client') {
      if (!clients.some((c) => c.id.includes(plugin.client))) {
        continue;
      }
    }
    if (plugin.type === 'device') {
      if (
        !pluginName ||
        !devicePlugins.has(pluginName) ||
        serial !== plugin.client
      ) {
        continue;
      }
    }
    pluginStates = {...pluginStates, [key]: allPluginStates[key]};
  }
  return pluginStates;
}

export function processNotificationStates(
  options: ProcessNotificationStatesOptions,
): Array<PluginNotification> {
  const {
    clients,
    serial,
    allActiveNotifications,
    devicePlugins,
    statusUpdate,
  } = options;
  statusUpdate &&
    statusUpdate('Filtering the notifications for the filtered Clients...');
  const activeNotifications = allActiveNotifications.filter((notif) => {
    const filteredClients = clients.filter((client) =>
      notif.client ? client.id.includes(notif.client) : false,
    );
    return (
      filteredClients.length > 0 ||
      (devicePlugins.has(notif.pluginId) && serial === notif.client)
    ); // There need not be any client for device Plugins
  });
  return activeNotifications;
}

const serializePluginStates = async (
  pluginStates: PluginStatesState,
  clientPlugins: ClientPluginMap,
  devicePlugins: DevicePluginMap,
  statusUpdate?: (msg: string) => void,
  idler?: Idler,
): Promise<PluginStatesExportState> => {
  const pluginsMap = new Map<string, PluginDefinition>([
    ...clientPlugins.entries(),
    ...devicePlugins.entries(),
  ]);
  const pluginExportState: PluginStatesExportState = {};
  for (const key in pluginStates) {
    const pluginName = deconstructPluginKey(key).pluginName;
    statusUpdate && statusUpdate(`Serialising ${pluginName}...`);
    const serializationMarker = `${EXPORT_FLIPPER_TRACE_EVENT}:serialization-per-plugin`;
    performance.mark(serializationMarker);
    const pluginClass = pluginName ? pluginsMap.get(pluginName) : null;
    if (isSandyPlugin(pluginClass)) {
      continue; // Those are already processed by `exportSandyPluginStates`
    } else if (pluginClass) {
      pluginExportState[key] = await pluginClass.serializePersistedState(
        pluginStates[key],
        statusUpdate,
        idler,
        pluginName,
      );
      getLogger().trackTimeSince(serializationMarker, serializationMarker, {
        plugin: pluginName,
      });
    }
  }
  return pluginExportState;
};

function exportSandyPluginStates(
  pluginsToProcess: PluginsToProcess,
): SandyPluginStates {
  const res: SandyPluginStates = {};
  pluginsToProcess.forEach(({pluginId, client, pluginClass}) => {
    if (isSandyPlugin(pluginClass) && client.sandyPluginStates.has(pluginId)) {
      if (!res[client.id]) {
        res[client.id] = {};
      }
      res[client.id][pluginId] = client.sandyPluginStates
        .get(pluginId)!
        .exportState();
    }
  });
  return res;
}

const deserializePluginStates = (
  pluginStatesExportState: PluginStatesExportState,
  clientPlugins: ClientPluginMap,
  devicePlugins: DevicePluginMap,
): PluginStatesState => {
  const pluginsMap = new Map<string, PluginDefinition>([
    ...clientPlugins.entries(),
    ...devicePlugins.entries(),
  ]);
  const pluginsState: PluginStatesState = {};
  for (const key in pluginStatesExportState) {
    const pluginName = deconstructPluginKey(key).pluginName;
    if (!pluginName || !pluginsMap.get(pluginName)) {
      continue;
    }
    const pluginClass = pluginsMap.get(pluginName);
    if (isSandyPlugin(pluginClass)) {
      pluginsState[key] = pluginStatesExportState[key];
    } else if (pluginClass) {
      pluginsState[key] = pluginClass.deserializePersistedState(
        pluginStatesExportState[key],
      );
    }
  }
  return pluginsState;
};

function replaceSerialsInKeys<T extends Record<string, any>>(
  collection: T,
  baseSerial: string,
  newSerial: string,
): T {
  const result: Record<string, any> = {};
  for (const key in collection) {
    if (!key.includes(baseSerial)) {
      throw new Error(
        `Error while exporting, plugin state (${key}) does not have ${baseSerial} in its key`,
      );
    }
    result[key.replace(baseSerial, newSerial)] = collection[key];
  }
  return result as T;
}

async function addSaltToDeviceSerial({
  salt,
  device,
  deviceScreenshot,
  clients,
  pluginStates,
  pluginNotification,
  statusUpdate,
  selectedPlugins,
  pluginStates2,
}: AddSaltToDeviceSerialOptions): Promise<ExportType> {
  const {serial} = device;
  const newSerial = salt + '-' + serial;
  const newDevice = new ArchivedDevice({
    serial: newSerial,
    deviceType: device.deviceType,
    title: device.title,
    os: device.os,
    logEntries: selectedPlugins.includes('DeviceLogs')
      ? device.getLogs(
          new Date(new Date().getTime() - 1000 * 60 * 10), // Last 10 mins of logs
        )
      : [],
    screenshotHandle: deviceScreenshot,
  });
  statusUpdate &&
    statusUpdate('Adding salt to the selected device id in the client data...');
  const updatedClients = clients.map((client: ClientExport) => {
    return {
      ...client,
      id: client.id.replace(serial, newSerial),
      query: {...client.query, device_id: newSerial},
    };
  });

  statusUpdate &&
    statusUpdate(
      'Adding salt to the selected device id in the plugin states...',
    );
  const updatedPluginStates = replaceSerialsInKeys(
    pluginStates,
    serial,
    newSerial,
  );
  const updatedPluginStates2 = replaceSerialsInKeys(
    pluginStates2,
    serial,
    newSerial,
  );

  statusUpdate &&
    statusUpdate(
      'Adding salt to the selected device id in the notification data...',
    );
  const updatedPluginNotifications = pluginNotification.map((notif) => {
    if (!notif.client || !notif.client.includes(serial)) {
      throw new Error(
        `Error while exporting, plugin state (${notif.pluginId}) does not have ${serial} in it`,
      );
    }
    return {...notif, client: notif.client.replace(serial, newSerial)};
  });
  const revision: string | undefined = await readCurrentRevision();
  return {
    fileVersion: remote.app.getVersion(),
    flipperReleaseRevision: revision,
    clients: updatedClients,
    device: newDevice.toJSON(),
    deviceScreenshot: deviceScreenshot,
    store: {
      pluginStates: updatedPluginStates,
      activeNotifications: updatedPluginNotifications,
    },
    pluginStates2: updatedPluginStates2,
  };
}

type ProcessStoreOptions = {
  activeNotifications: Array<PluginNotification>;
  device: BaseDevice | null;
  pluginStates: PluginStatesState;
  pluginStates2: SandyPluginStates;
  clients: Array<ClientExport>;
  devicePlugins: DevicePluginMap;
  clientPlugins: ClientPluginMap;
  salt: string;
  selectedPlugins: Array<string>;
  statusUpdate?: (msg: string) => void;
};

export async function processStore(
  {
    activeNotifications,
    device,
    pluginStates,
    pluginStates2,
    clients,
    devicePlugins,
    clientPlugins,
    salt,
    selectedPlugins,
    statusUpdate,
  }: ProcessStoreOptions,
  idler?: Idler,
): Promise<ExportType> {
  if (device) {
    const {serial} = device;
    statusUpdate && statusUpdate('Capturing screenshot...');
    const deviceScreenshot = await capture(device).catch((e) => {
      console.warn('Failed to capture device screenshot when exporting. ' + e);
      return null;
    });
    const processedClients = processClients(clients, serial, statusUpdate);
    const processedPluginStates = processPluginStates({
      clients: processedClients,
      serial,
      allPluginStates: pluginStates,
      devicePlugins,
      selectedPlugins,
      statusUpdate,
    });
    const processedActiveNotifications = processNotificationStates({
      clients: processedClients,
      serial,
      allActiveNotifications: activeNotifications,
      devicePlugins,
      statusUpdate,
    });

    const exportPluginState = await serializePluginStates(
      processedPluginStates,
      clientPlugins,
      devicePlugins,
      statusUpdate,
      idler,
    );

    statusUpdate && statusUpdate('Uploading screenshot...');
    const deviceScreenshotLink =
      deviceScreenshot &&
      (await uploadFlipperMedia(deviceScreenshot, 'Image').catch((e) => {
        console.warn('Failed to upload device screenshot when exporting. ' + e);
        return null;
      }));
    // Adding salt to the device id, so that the device_id in the device list is unique.
    const exportFlipperData = await addSaltToDeviceSerial({
      salt,
      device,
      deviceScreenshot: deviceScreenshotLink,
      clients: processedClients,
      pluginStates: exportPluginState,
      pluginNotification: processedActiveNotifications,
      statusUpdate,
      selectedPlugins,
      pluginStates2,
    });

    return exportFlipperData;
  }
  throw new Error('Selected device is null, please select a device');
}

export async function fetchMetadata(
  pluginsToProcess: PluginsToProcess,
  pluginStates: PluginStatesState,
  state: ReduxState,
  statusUpdate?: (msg: string) => void,
  idler?: Idler,
): Promise<{
  pluginStates: PluginStatesState;
  errors: {[plugin: string]: Error} | null;
}> {
  const newPluginState = {...pluginStates};
  let errorObject: {[plugin: string]: Error} | null = null;

  for (const {
    pluginName,
    pluginId,
    pluginClass,
    client,
    pluginKey,
  } of pluginsToProcess) {
    const exportState = pluginClass ? pluginClass.exportPersistedState : null;
    if (exportState) {
      const fetchMetaDataMarker = `${EXPORT_FLIPPER_TRACE_EVENT}:fetch-meta-data-per-plugin`;
      performance.mark(fetchMetaDataMarker);
      try {
        statusUpdate &&
          statusUpdate(`Fetching metadata for plugin ${pluginName}...`);
        const data = await promiseTimeout(
          240000, // Fetching MobileConfig data takes ~ 3 mins, thus keeping timeout at 4 mins.
          exportState(
            callClient(client, pluginId),
            newPluginState[pluginKey],
            state,
            idler,
            statusUpdate,
            supportsMethod(client, pluginId),
          ),
          `Timed out while collecting data for ${pluginName}`,
        );
        if (!data) {
          throw new Error(
            `Metadata returned by the ${pluginName} is undefined`,
          );
        }
        getLogger().trackTimeSince(fetchMetaDataMarker, fetchMetaDataMarker, {
          pluginId,
        });
        newPluginState[pluginKey] = data;
      } catch (e) {
        if (!errorObject) {
          errorObject = {};
        }
        errorObject[pluginName] = e;
        getLogger().trackTimeSince(fetchMetaDataMarker, fetchMetaDataMarker, {
          pluginId,
          error: e,
        });
        continue;
      }
    }
  }

  return {pluginStates: newPluginState, errors: errorObject};
}

async function processQueues(
  store: MiddlewareAPI,
  pluginsToProcess: PluginsToProcess,
  statusUpdate?: (msg: string) => void,
  idler?: Idler,
) {
  for (const {
    pluginName,
    pluginId,
    pluginKey,
    pluginClass,
    client,
  } of pluginsToProcess) {
    if (isSandyPlugin(pluginClass) || pluginClass.persistedStateReducer) {
      client.flushMessageBuffer();
      const processQueueMarker = `${EXPORT_FLIPPER_TRACE_EVENT}:process-queue-per-plugin`;
      performance.mark(processQueueMarker);
      const plugin = isSandyPlugin(pluginClass)
        ? client.sandyPluginStates.get(pluginId)
        : pluginClass;
      if (!plugin) continue;
      await processMessageQueue(
        plugin,
        pluginKey,
        store,
        ({current, total}) => {
          statusUpdate?.(
            `Processing event ${current} / ${total} (${Math.round(
              (current / total) * 100,
            )}%) for plugin ${pluginName}`,
          );
        },
        idler,
      );

      getLogger().trackTimeSince(processQueueMarker, processQueueMarker, {
        pluginId,
      });
    }
  }
}

export function determinePluginsToProcess(
  clients: Array<Client>,
  selectedDevice: null | BaseDevice,
  plugins: PluginsState,
): PluginsToProcess {
  const pluginsToProcess: PluginsToProcess = [];
  const selectedPlugins = plugins.selectedPlugins;

  for (const client of clients) {
    if (
      !selectedDevice ||
      selectedDevice.isArchived ||
      client.query.device_id !== selectedDevice.serial
    ) {
      continue;
    }
    const selectedFilteredPlugins = client
      ? selectedPlugins.length > 0
        ? client.plugins.filter((plugin) => selectedPlugins.includes(plugin))
        : client.plugins
      : [];
    for (const plugin of selectedFilteredPlugins) {
      if (!client.plugins.includes(plugin)) {
        // Ignore clients which doesn't support the selected plugins.
        continue;
      }
      const pluginClass =
        plugins.clientPlugins.get(plugin) || plugins.devicePlugins.get(plugin);
      if (pluginClass) {
        const key = pluginKey(client.id, plugin);
        pluginsToProcess.push({
          pluginKey: key,
          client,
          pluginId: plugin,
          pluginName: getPluginTitle(pluginClass),
          pluginClass,
        });
      }
    }
  }
  return pluginsToProcess;
}

async function getStoreExport(
  store: MiddlewareAPI,
  statusUpdate?: (msg: string) => void,
  idler?: Idler,
): Promise<{
  exportData: ExportType;
  fetchMetaDataErrors: {[plugin: string]: Error} | null;
}> {
  const state = store.getState();
  const {clients, selectedApp, selectedDevice} = state.connections;
  const pluginsToProcess = determinePluginsToProcess(
    clients,
    selectedDevice,
    state.plugins,
  );

  statusUpdate?.('Preparing to process data queues for plugins...');
  await processQueues(store, pluginsToProcess, statusUpdate, idler);

  statusUpdate && statusUpdate('Preparing to fetch metadata from client...');
  const fetchMetaDataMarker = `${EXPORT_FLIPPER_TRACE_EVENT}:fetch-meta-data`;
  performance.mark(fetchMetaDataMarker);

  const client = clients.find((client) => client.id === selectedApp);
  const metadata = await fetchMetadata(
    pluginsToProcess,
    state.pluginStates,
    state,
    statusUpdate,
    idler,
  );
  const newPluginState = metadata.pluginStates;

  // TODO: support async export like fetchMetaData T68683476
  // TODO: support device plugins T68738317
  const pluginStates2 = pluginsToProcess
    ? exportSandyPluginStates(pluginsToProcess)
    : {};

  getLogger().trackTimeSince(fetchMetaDataMarker, fetchMetaDataMarker, {
    plugins: state.plugins.selectedPlugins,
  });
  const {errors} = metadata;

  const {activeNotifications} = state.notifications;
  const {devicePlugins, clientPlugins} = state.plugins;
  const exportData = await processStore(
    {
      activeNotifications,
      device: selectedDevice,
      pluginStates: newPluginState,
      pluginStates2,
      clients: client ? [client.toJSON()] : [],
      devicePlugins,
      clientPlugins,
      salt: uuidv4(),
      selectedPlugins: state.plugins.selectedPlugins,
      statusUpdate,
    },
    idler,
  );
  return {exportData, fetchMetaDataErrors: errors};
}

export async function exportStore(
  store: MiddlewareAPI,
  includeSupportDetails?: boolean,
  idler?: Idler,
  statusUpdate?: (msg: string) => void,
): Promise<{
  serializedString: string;
  fetchMetaDataErrors: {
    [plugin: string]: Error;
  } | null;
  exportStoreData: ExportType;
}> {
  getLogger().track('usage', EXPORT_FLIPPER_TRACE_EVENT);
  performance.mark(EXPORT_FLIPPER_TRACE_TIME_SERIALIZATION_EVENT);
  statusUpdate && statusUpdate('Preparing to export Flipper data...');
  const state = store.getState();
  const {exportData, fetchMetaDataErrors} = await getStoreExport(
    store,
    statusUpdate,
    idler,
  );
  if (includeSupportDetails) {
    exportData.supportRequestDetails = {
      ...state.supportForm?.supportFormV2,
      appName:
        state.connections.selectedApp == null
          ? ''
          : deconstructClientId(state.connections.selectedApp).app,
    };
  }

  statusUpdate && statusUpdate('Serializing Flipper data...');
  const serializedString = JSON.stringify(exportData);
  if (serializedString.length <= 0) {
    throw new Error('Serialize function returned empty string');
  }
  getLogger().trackTimeSince(
    EXPORT_FLIPPER_TRACE_TIME_SERIALIZATION_EVENT,
    EXPORT_FLIPPER_TRACE_TIME_SERIALIZATION_EVENT,
    {
      plugins: state.plugins.selectedPlugins,
    },
  );
  return {serializedString, fetchMetaDataErrors, exportStoreData: exportData};
}

export const exportStoreToFile = (
  exportFilePath: string,
  store: MiddlewareAPI,
  includeSupportDetails: boolean,
  idler?: Idler,
  statusUpdate?: (msg: string) => void,
): Promise<{
  fetchMetaDataErrors: {
    [plugin: string]: Error;
  } | null;
}> => {
  return exportStore(store, includeSupportDetails, idler, statusUpdate).then(
    ({serializedString, fetchMetaDataErrors}) => {
      return promisify(fs.writeFile)(exportFilePath, serializedString).then(
        () => {
          store.dispatch(resetSupportFormV2State());
          return {fetchMetaDataErrors};
        },
      );
    },
  );
};

export function importDataToStore(source: string, data: string, store: Store) {
  getLogger().track('usage', IMPORT_FLIPPER_TRACE_EVENT);
  const json: ExportType = JSON.parse(data);
  const {device, clients, supportRequestDetails, deviceScreenshot} = json;
  if (device == null) {
    return;
  }
  const {serial, deviceType, title, os, logs} = device;

  const archivedDevice = new ArchivedDevice({
    serial,
    deviceType,
    title,
    os,
    logEntries: logs
      ? logs.map((l) => {
          return {...l, date: new Date(l.date)};
        })
      : [],
    screenshotHandle: deviceScreenshot,
    source,
    supportRequestDetails,
  });
  const devices = store.getState().connections.devices;
  const matchedDevices = devices.filter(
    (availableDevice) => availableDevice.serial === serial,
  );
  if (matchedDevices.length > 0) {
    store.dispatch({
      type: 'SELECT_DEVICE',
      payload: matchedDevices[0],
    });
    return;
  }
  archivedDevice.loadDevicePlugins(store.getState().plugins.devicePlugins);
  store.dispatch({
    type: 'REGISTER_DEVICE',
    payload: archivedDevice,
  });
  store.dispatch({
    type: 'SELECT_DEVICE',
    payload: archivedDevice,
  });

  const {pluginStates} = json.store;
  const processedPluginStates: PluginStatesState = deserializePluginStates(
    pluginStates,
    store.getState().plugins.clientPlugins,
    store.getState().plugins.devicePlugins,
  );
  const keys = Object.keys(processedPluginStates);
  keys.forEach((key) => {
    store.dispatch({
      type: 'SET_PLUGIN_STATE',
      payload: {
        pluginKey: key,
        state: processedPluginStates[key],
      },
    });
  });

  clients.forEach((client: {id: string; query: ClientQuery}) => {
    const sandyPluginStates = json.pluginStates2[client.id] || {};
    const clientPlugins: Array<string> = [
      ...keys
        .filter((key) => {
          const plugin = deconstructPluginKey(key);
          return plugin.type === 'client' && client.id === plugin.client;
        })
        .map((pluginKey) => deconstructPluginKey(pluginKey).pluginName),
      ...Object.keys(sandyPluginStates),
    ];
    store.dispatch({
      type: 'NEW_CLIENT',
      payload: new Client(
        client.id,
        client.query,
        null,
        getLogger(),
        store,
        clientPlugins,
        archivedDevice,
      ).initFromImport(sandyPluginStates),
    });
  });
  if (supportRequestDetails) {
    store.dispatch(
      // Late require to avoid circular dependency issue
      setStaticView(require('../fb-stubs/SupportRequestDetails').default),
    );
  }
}

export const importFileToStore = (file: string, store: Store) => {
  fs.readFile(file, 'utf8', (err, data) => {
    if (err) {
      console.error(err);
      return;
    }
    importDataToStore(file, data, store);
  });
};

export function showOpenDialog(store: Store) {
  const options: OpenDialogOptions = {
    properties: ['openFile'],
    filters: [{extensions: ['flipper', 'json', 'txt'], name: 'Flipper files'}],
  };
  remote.dialog.showOpenDialog(options).then((result) => {
    const filePaths = result.filePaths;
    if (filePaths.length > 0) {
      tryCatchReportPlatformFailures(() => {
        importFileToStore(filePaths[0], store);
      }, `${IMPORT_FLIPPER_TRACE_EVENT}:UI`);
    }
  });
}

export function startFileExport(dispatch: Store['dispatch']) {
  electron.remote.dialog
    .showSaveDialog(
      // @ts-ignore This appears to work but isn't allowed by the types
      null,
      {
        title: 'FlipperExport',
        defaultPath: path.join(os.homedir(), 'FlipperExport.flipper'),
      },
    )
    .then(async (result: electron.SaveDialogReturnValue) => {
      const file = result.filePath;
      if (!file) {
        return;
      }
      dispatch(
        setSelectPluginsToExportActiveSheet({
          type: 'file',
          file: file,
          closeOnFinish: false,
        }),
      );
    });
}

export function startLinkExport(dispatch: Store['dispatch']) {
  dispatch(
    setSelectPluginsToExportActiveSheet({
      type: 'link',
      closeOnFinish: false,
    }),
  );
}
