import pify from 'pify';
import log from 'loglevel';
import { capitalize, isEqual } from 'lodash';
import getBuyEthUrl from '../../app/scripts/lib/buy-eth-url';
import {
  fetchLocale,
  loadRelativeTimeFormatLocaleData,
} from '../helpers/utils/i18n-helper';
import { getMethodDataAsync } from '../helpers/utils/transactions.util';
import { getSymbolAndDecimals } from '../helpers/utils/token-util';
import switchDirection from '../helpers/utils/switch-direction';
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../shared/constants/app';
import { hasUnconfirmedTransactions } from '../helpers/utils/confirm-tx.util';
import txHelper from '../helpers/utils/tx-helper';
import { getEnvironmentType, addHexPrefix } from '../../app/scripts/lib/util';
import {
  getMetaMaskAccounts,
  getPermittedAccountsForCurrentTab,
  getSelectedAddress,
} from '../selectors';
import { computeEstimatedGasLimit, resetSendState } from '../ducks/send';
import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account';
import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask';
import { LISTED_CONTRACT_ADDRESSES } from '../../shared/constants/tokens';
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
import * as actionConstants from './actionConstants';

let background = null;
let promisifiedBackground = null;
export function _setBackgroundConnection(backgroundConnection) {
  background = backgroundConnection;
  promisifiedBackground = pify(background);
}

export function goHome() {
  return {
    type: actionConstants.GO_HOME,
  };
}
// async actions

export function tryUnlockMetamask(password) {
  return (dispatch) => {
    dispatch(showLoadingIndication());
    dispatch(unlockInProgress());
    log.debug(`background.submitPassword`);

    return new Promise((resolve, reject) => {
      background.submitPassword(password, (error) => {
        if (error) {
          reject(error);
          return;
        }

        resolve();
      });
    })
      .then(() => {
        dispatch(unlockSucceeded());
        return forceUpdateMetamaskState(dispatch);
      })
      .then(() => {
        return new Promise((resolve, reject) => {
          background.verifySeedPhrase((err) => {
            if (err) {
              dispatch(displayWarning(err.message));
              reject(err);
              return;
            }

            resolve();
          });
        });
      })
      .then(() => {
        dispatch(hideLoadingIndication());
      })
      .catch((err) => {
        dispatch(unlockFailed(err.message));
        dispatch(hideLoadingIndication());
        return Promise.reject(err);
      });
  };
}

export function createNewVaultAndRestore(password, seed) {
  return (dispatch) => {
    dispatch(showLoadingIndication());
    log.debug(`background.createNewVaultAndRestore`);
    let vault;
    return new Promise((resolve, reject) => {
      background.createNewVaultAndRestore(password, seed, (err, _vault) => {
        if (err) {
          reject(err);
          return;
        }
        vault = _vault;
        resolve();
      });
    })
      .then(() => dispatch(unMarkPasswordForgotten()))
      .then(() => {
        dispatch(showAccountsPage());
        dispatch(hideLoadingIndication());
        return vault;
      })
      .catch((err) => {
        dispatch(displayWarning(err.message));
        dispatch(hideLoadingIndication());
        return Promise.reject(err);
      });
  };
}

export function createNewVaultAndGetSeedPhrase(password) {
  return async (dispatch) => {
    dispatch(showLoadingIndication());

    try {
      await createNewVault(password);
      const seedWords = await verifySeedPhrase();
      return seedWords;
    } catch (error) {
      dispatch(displayWarning(error.message));
      throw new Error(error.message);
    } finally {
      dispatch(hideLoadingIndication());
    }
  };
}

export function unlockAndGetSeedPhrase(password) {
  return async (dispatch) => {
    dispatch(showLoadingIndication());

    try {
      await submitPassword(password);
      const seedWords = await verifySeedPhrase();
      await forceUpdateMetamaskState(dispatch);
      return seedWords;
    } catch (error) {
      dispatch(displayWarning(error.message));
      throw new Error(error.message);
    } finally {
      dispatch(hideLoadingIndication());
    }
  };
}

export function submitPassword(password) {
  return new Promise((resolve, reject) => {
    background.submitPassword(password, (error) => {
      if (error) {
        reject(error);
        return;
      }

      resolve();
    });
  });
}

export function createNewVault(password) {
  return new Promise((resolve, reject) => {
    background.createNewVaultAndKeychain(password, (error) => {
      if (error) {
        reject(error);
        return;
      }

      resolve(true);
    });
  });
}

export function verifyPassword(password) {
  return new Promise((resolve, reject) => {
    background.verifyPassword(password, (error) => {
      if (error) {
        reject(error);
        return;
      }

      resolve(true);
    });
  });
}

export function verifySeedPhrase() {
  return new Promise((resolve, reject) => {
    background.verifySeedPhrase((error, seedWords) => {
      if (error) {
        reject(error);
        return;
      }

      resolve(seedWords);
    });
  });
}

export function requestRevealSeedWords(password) {
  return async (dispatch) => {
    dispatch(showLoadingIndication());
    log.debug(`background.verifyPassword`);

    try {
      await verifyPassword(password);
      const seedWords = await verifySeedPhrase();
      return seedWords;
    } catch (error) {
      dispatch(displayWarning(error.message));
      throw new Error(error.message);
    } finally {
      dispatch(hideLoadingIndication());
    }
  };
}

export function tryReverseResolveAddress(address) {
  return () => {
    return new Promise((resolve) => {
      background.tryReverseResolveAddress(address, (err) => {
        if (err) {
          log.error(err);
        }
        resolve();
      });
    });
  };
}

export function fetchInfoToSync() {
  return (dispatch) => {
    log.debug(`background.fetchInfoToSync`);
    return new Promise((resolve, reject) => {
      background.fetchInfoToSync((err, result) => {
        if (err) {
          dispatch(displayWarning(err.message));
          reject(err);
          return;
        }
        resolve(result);
      });
    });
  };
}

export function resetAccount() {
  return (dispatch) => {
    dispatch(showLoadingIndication());

    return new Promise((resolve, reject) => {
      background.resetAccount((err, account) => {
        dispatch(hideLoadingIndication());
        if (err) {
          dispatch(displayWarning(err.message));
          reject(err);
          return;
        }

        log.info(`Transaction history reset for ${account}`);
        dispatch(showAccountsPage());
        resolve(account);
      });
    });
  };
}

export function removeAccount(address) {
  return async (dispatch) => {
    dispatch(showLoadingIndication());

    try {
      await new Promise((resolve, reject) => {
        background.removeAccount(address, (error, account) => {
          if (error) {
            reject(error);
            return;
          }
          resolve(account);
        });
      });
      await forceUpdateMetamaskState(dispatch);
    } catch (error) {
      dispatch(displayWarning(error.message));
      throw error;
    } finally {
      dispatch(hideLoadingIndication());
    }

    log.info(`Account removed: ${address}`);
    dispatch(showAccountsPage());
  };
}

export function importNewAccount(strategy, args) {
  return async (dispatch) => {
    let newState;
    dispatch(
      showLoadingIndication('This may take a while, please be patient.'),
    );
    try {
      log.debug(`background.importAccountWithStrategy`);
      await promisifiedBackground.importAccountWithStrategy(strategy, args);
      log.debug(`background.getState`);
      newState = await promisifiedBackground.getState();
    } catch (err) {
      dispatch(displayWarning(err.message));
      throw err;
    } finally {
      dispatch(hideLoadingIndication());
    }

    dispatch(updateMetamaskState(newState));
    if (newState.selectedAddress) {
      dispatch({
        type: actionConstants.SHOW_ACCOUNT_DETAIL,
        value: newState.selectedAddress,
      });
    }
    return newState;
  };
}

export function addNewAccount() {
  log.debug(`background.addNewAccount`);
  return async (dispatch, getState) => {
    const oldIdentities = getState().metamask.identities;
    dispatch(showLoadingIndication());

    let newIdentities;
    try {
      const { identities } = await promisifiedBackground.addNewAccount();
      newIdentities = identities;
    } catch (error) {
      dispatch(displayWarning(error.message));
      throw error;
    } finally {
      dispatch(hideLoadingIndication());
    }

    const newAccountAddress = Object.keys(newIdentities).find(
      (address) => !oldIdentities[address],
    );
    await forceUpdateMetamaskState(dispatch);
    return newAccountAddress;
  };
}

export function checkHardwareStatus(deviceName, hdPath) {
  log.debug(`background.checkHardwareStatus`, deviceName, hdPath);
  return async (dispatch) => {
    dispatch(showLoadingIndication());

    let unlocked;
    try {
      unlocked = await promisifiedBackground.checkHardwareStatus(
        deviceName,
        hdPath,
      );
    } catch (error) {
      log.error(error);
      dispatch(displayWarning(error.message));
      throw error;
    } finally {
      dispatch(hideLoadingIndication());
    }

    await forceUpdateMetamaskState(dispatch);
    return unlocked;
  };
}

export function forgetDevice(deviceName) {
  log.debug(`background.forgetDevice`, deviceName);
  return async (dispatch) => {
    dispatch(showLoadingIndication());
    try {
      await promisifiedBackground.forgetDevice(deviceName);
    } catch (error) {
      log.error(error);
      dispatch(displayWarning(error.message));
      throw error;
    } finally {
      dispatch(hideLoadingIndication());
    }

    await forceUpdateMetamaskState(dispatch);
  };
}

export function connectHardware(deviceName, page, hdPath) {
  log.debug(`background.connectHardware`, deviceName, page, hdPath);
  return async (dispatch) => {
    dispatch(
      showLoadingIndication(`Looking for your ${capitalize(deviceName)}...`),
    );

    let accounts;
    try {
      accounts = await promisifiedBackground.connectHardware(
        deviceName,
        page,
        hdPath,
      );
    } catch (error) {
      log.error(error);
      dispatch(displayWarning(error.message));
      throw error;
    } finally {
      dispatch(hideLoadingIndication());
    }

    await forceUpdateMetamaskState(dispatch);
    return accounts;
  };
}

export function unlockHardwareWalletAccounts(
  indexes,
  deviceName,
  hdPath,
  hdPathDescription,
) {
  log.debug(
    `background.unlockHardwareWalletAccount`,
    indexes,
    deviceName,
    hdPath,
    hdPathDescription,
  );
  return async (dispatch) => {
    dispatch(showLoadingIndication());

    for (const index of indexes) {
      try {
        await promisifiedBackground.unlockHardwareWalletAccount(
          index,
          deviceName,
          hdPath,
          hdPathDescription,
        );
      } catch (e) {
        log.error(e);
        dispatch(displayWarning(e.message));
        dispatch(hideLoadingIndication());
        throw e;
      }
    }

    dispatch(hideLoadingIndication());
    return undefined;
  };
}

export function showQrScanner() {
  return (dispatch) => {
    dispatch(
      showModal({
        name: 'QR_SCANNER',
      }),
    );
  };
}

export function setCurrentCurrency(currencyCode) {
  return async (dispatch) => {
    dispatch(showLoadingIndication());
    log.debug(`background.setCurrentCurrency`);
    try {
      await promisifiedBackground.setCurrentCurrency(currencyCode);
      await forceUpdateMetamaskState(dispatch);
    } catch (error) {
      log.error(error);
      dispatch(displayWarning(error.message));
      return;
    } finally {
      dispatch(hideLoadingIndication());
    }
  };
}

export function signMsg(msgData) {
  log.debug('action - signMsg');
  return async (dispatch) => {
    dispatch(showLoadingIndication());
    log.debug(`actions calling background.signMessage`);
    let newState;
    try {
      newState = await promisifiedBackground.signMessage(msgData);
    } catch (error) {
      log.error(error);
      dispatch(displayWarning(error.message));
      throw error;
    } finally {
      dispatch(hideLoadingIndication());
    }

    dispatch(updateMetamaskState(newState));
    dispatch(completedTx(msgData.metamaskId));
    dispatch(closeCurrentNotificationWindow());
    return msgData;
  };
}

export function signPersonalMsg(msgData) {
  log.debug('action - signPersonalMsg');
  return async (dispatch) => {
    dispatch(showLoadingIndication());
    log.debug(`actions calling background.signPersonalMessage`);

    let newState;
    try {
      newState = await promisifiedBackground.signPersonalMessage(msgData);
    } catch (error) {
      log.error(error);
      dispatch(displayWarning(error.message));
      throw error;
    } finally {
      dispatch(hideLoadingIndication());
    }

    dispatch(updateMetamaskState(newState));
    dispatch(completedTx(msgData.metamaskId));
    dispatch(closeCurrentNotificationWindow());
    return msgData;
  };
}

export function decryptMsgInline(decryptedMsgData) {
  log.debug('action - decryptMsgInline');
  return async (dispatch) => {
    log.debug(`actions calling background.decryptMessageInline`);

    let newState;
    try {
      newState = await promisifiedBackground.decryptMessageInline(
        decryptedMsgData,
      );
    } catch (error) {
      log.error(error);
      dispatch(displayWarning(error.message));
      throw error;
    }

    dispatch(updateMetamaskState(newState));
    return newState.unapprovedDecryptMsgs[decryptedMsgData.metamaskId];
  };
}

export function decryptMsg(decryptedMsgData) {
  log.debug('action - decryptMsg');
  return async (dispatch) => {
    dispatch(showLoadingIndication());
    log.debug(`actions calling background.decryptMessage`);

    let newState;
    try {
      newState = await promisifiedBackground.decryptMessage(decryptedMsgData);
    } catch (error) {
      log.error(error);
      dispatch(displayWarning(error.message));
      throw error;
    } finally {
      dispatch(hideLoadingIndication());
    }

    dispatch(updateMetamaskState(newState));
    dispatch(completedTx(decryptedMsgData.metamaskId));
    dispatch(closeCurrentNotificationWindow());
    return decryptedMsgData;
  };
}

export function encryptionPublicKeyMsg(msgData) {
  log.debug('action - encryptionPublicKeyMsg');
  return async (dispatch) => {
    dispatch(showLoadingIndication());
    log.debug(`actions calling background.encryptionPublicKey`);

    let newState;
    try {
      newState = await promisifiedBackground.encryptionPublicKey(msgData);
    } catch (error) {
      log.error(error);
      dispatch(displayWarning(error.message));
      throw error;
    } finally {
      dispatch(hideLoadingIndication());
    }

    dispatch(updateMetamaskState(newState));
    dispatch(completedTx(msgData.metamaskId));
    dispatch(closeCurrentNotificationWindow());
    return msgData;
  };
}

export function signTypedMsg(msgData) {
  log.debug('action - signTypedMsg');
  return async (dispatch) => {
    dispatch(showLoadingIndication());
    log.debug(`actions calling background.signTypedMessage`);

    let newState;
    try {
      newState = await promisifiedBackground.signTypedMessage(msgData);
    } catch (error) {
      log.error(error);
      dispatch(displayWarning(error.message));
      throw error;
    } finally {
      dispatch(hideLoadingIndication());
    }

    dispatch(updateMetamaskState(newState));
    dispatch(completedTx(msgData.metamaskId));
    dispatch(closeCurrentNotificationWindow());
    return msgData;
  };
}

export function updateCustomNonce(value) {
  return {
    type: actionConstants.UPDATE_CUSTOM_NONCE,
    value,
  };
}

const updateMetamaskStateFromBackground = () => {
  log.debug(`background.getState`);

  return new Promise((resolve, reject) => {
    background.getState((error, newState) => {
      if (error) {
        reject(error);
        return;
      }

      resolve(newState);
    });
  });
};

export function updateTransaction(txData, dontShowLoadingIndicator) {
  return async (dispatch) => {
    !dontShowLoadingIndicator && dispatch(showLoadingIndication());

    try {
      await promisifiedBackground.updateTransaction(txData);
    } catch (error) {
      dispatch(updateTransactionParams(txData.id, txData.txParams));
      dispatch(hideLoadingIndication());
      dispatch(txError(error));
      dispatch(goHome());
      log.error(error.message);
      throw error;
    }

    try {
      dispatch(updateTransactionParams(txData.id, txData.txParams));
      const newState = await updateMetamaskStateFromBackground();
      dispatch(updateMetamaskState(newState));
      dispatch(showConfTxPage({ id: txData.id }));
      return txData;
    } finally {
      dispatch(hideLoadingIndication());
    }
  };
}

export function addUnapprovedTransaction(txParams, origin) {
  log.debug('background.addUnapprovedTransaction');

  return () => {
    return new Promise((resolve, reject) => {
      background.addUnapprovedTransaction(txParams, origin, (err, txMeta) => {
        if (err) {
          reject(err);
          return;
        }
        resolve(txMeta);
      });
    });
  };
}

export function updateAndApproveTx(txData, dontShowLoadingIndicator) {
  return (dispatch) => {
    !dontShowLoadingIndicator && dispatch(showLoadingIndication());
    return new Promise((resolve, reject) => {
      background.updateAndApproveTransaction(txData, (err) => {
        dispatch(updateTransactionParams(txData.id, txData.txParams));
        dispatch(resetSendState());

        if (err) {
          dispatch(txError(err));
          dispatch(goHome());
          log.error(err.message);
          reject(err);
          return;
        }

        resolve(txData);
      });
    })
      .then(() => updateMetamaskStateFromBackground())
      .then((newState) => dispatch(updateMetamaskState(newState)))
      .then(() => {
        dispatch(resetSendState());
        dispatch(completedTx(txData.id));
        dispatch(hideLoadingIndication());
        dispatch(updateCustomNonce(''));
        dispatch(closeCurrentNotificationWindow());

        return txData;
      })
      .catch((err) => {
        dispatch(hideLoadingIndication());
        return Promise.reject(err);
      });
  };
}

export function completedTx(id) {
  return (dispatch, getState) => {
    const state = getState();
    const {
      unapprovedTxs,
      unapprovedMsgs,
      unapprovedPersonalMsgs,
      unapprovedTypedMessages,
      network,
      provider: { chainId },
    } = state.metamask;
    const unconfirmedActions = txHelper(
      unapprovedTxs,
      unapprovedMsgs,
      unapprovedPersonalMsgs,
      unapprovedTypedMessages,
      network,
      chainId,
    );
    const otherUnconfirmedActions = unconfirmedActions.filter(
      (tx) => tx.id !== id,
    );
    dispatch({
      type: actionConstants.COMPLETED_TX,
      value: {
        id,
        unconfirmedActionsCount: otherUnconfirmedActions.length,
      },
    });
  };
}

export function updateTransactionParams(id, txParams) {
  return {
    type: actionConstants.UPDATE_TRANSACTION_PARAMS,
    id,
    value: txParams,
  };
}

export function txError(err) {
  return {
    type: actionConstants.TRANSACTION_ERROR,
    message: err.message,
  };
}

export function cancelMsg(msgData) {
  return async (dispatch) => {
    dispatch(showLoadingIndication());

    let newState;
    try {
      newState = await promisifiedBackground.cancelMessage(msgData.id);
    } finally {
      dispatch(hideLoadingIndication());
    }

    dispatch(updateMetamaskState(newState));
    dispatch(completedTx(msgData.id));
    dispatch(closeCurrentNotificationWindow());
    return msgData;
  };
}

export function cancelPersonalMsg(msgData) {
  return async (dispatch) => {
    dispatch(showLoadingIndication());

    let newState;
    try {
      newState = await promisifiedBackground.cancelPersonalMessage(msgData.id);
    } finally {
      dispatch(hideLoadingIndication());
    }

    dispatch(updateMetamaskState(newState));
    dispatch(completedTx(msgData.id));
    dispatch(closeCurrentNotificationWindow());
    return msgData;
  };
}

export function cancelDecryptMsg(msgData) {
  return async (dispatch) => {
    dispatch(showLoadingIndication());

    let newState;
    try {
      newState = await promisifiedBackground.cancelDecryptMessage(msgData.id);
    } finally {
      dispatch(hideLoadingIndication());
    }

    dispatch(updateMetamaskState(newState));
    dispatch(completedTx(msgData.id));
    dispatch(closeCurrentNotificationWindow());
    return msgData;
  };
}

export function cancelEncryptionPublicKeyMsg(msgData) {
  return async (dispatch) => {
    dispatch(showLoadingIndication());

    let newState;
    try {
      newState = await promisifiedBackground.cancelEncryptionPublicKey(
        msgData.id,
      );
    } finally {
      dispatch(hideLoadingIndication());
    }

    dispatch(updateMetamaskState(newState));
    dispatch(completedTx(msgData.id));
    dispatch(closeCurrentNotificationWindow());
    return msgData;
  };
}

export function cancelTypedMsg(msgData) {
  return async (dispatch) => {
    dispatch(showLoadingIndication());

    let newState;
    try {
      newState = await promisifiedBackground.cancelTypedMessage(msgData.id);
    } finally {
      dispatch(hideLoadingIndication());
    }

    dispatch(updateMetamaskState(newState));
    dispatch(completedTx(msgData.id));
    dispatch(closeCurrentNotificationWindow());
    return msgData;
  };
}

export function cancelTx(txData, _showLoadingIndication = true) {
  return (dispatch) => {
    _showLoadingIndication && dispatch(showLoadingIndication());
    return new Promise((resolve, reject) => {
      background.cancelTransaction(txData.id, (error) => {
        if (error) {
          reject(error);
          return;
        }

        resolve();
      });
    })
      .then(() => updateMetamaskStateFromBackground())
      .then((newState) => dispatch(updateMetamaskState(newState)))
      .then(() => {
        dispatch(resetSendState());
        dispatch(completedTx(txData.id));
        dispatch(hideLoadingIndication());
        dispatch(closeCurrentNotificationWindow());

        return txData;
      })
      .catch((error) => {
        dispatch(hideLoadingIndication());
        throw error;
      });
  };
}

/**
 * Cancels all of the given transactions
 * @param {Array<object>} txDataList - a list of tx data objects
 * @returns {function(*): Promise<void>}
 */
export function cancelTxs(txDataList) {
  return async (dispatch) => {
    dispatch(showLoadingIndication());

    try {
      const txIds = txDataList.map(({ id }) => id);
      const cancellations = txIds.map(
        (id) =>
          new Promise((resolve, reject) => {
            background.cancelTransaction(id, (err) => {
              if (err) {
                reject(err);
                return;
              }

              resolve();
            });
          }),
      );

      await Promise.all(cancellations);

      const newState = await updateMetamaskStateFromBackground();
      dispatch(updateMetamaskState(newState));
      dispatch(resetSendState());

      txIds.forEach((id) => {
        dispatch(completedTx(id));
      });
    } finally {
      if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) {
        global.platform.closeCurrentWindow();
      } else {
        dispatch(hideLoadingIndication());
      }
    }
  };
}

export function markPasswordForgotten() {
  return async (dispatch) => {
    try {
      await new Promise((resolve, reject) => {
        return background.markPasswordForgotten((error) => {
          if (error) {
            reject(error);
            return;
          }
          resolve();
        });
      });
    } finally {
      // TODO: handle errors
      dispatch(hideLoadingIndication());
      dispatch(forgotPassword());
      await forceUpdateMetamaskState(dispatch);
    }
  };
}

export function unMarkPasswordForgotten() {
  return (dispatch) => {
    return new Promise((resolve) => {
      background.unMarkPasswordForgotten(() => {
        dispatch(forgotPassword(false));
        resolve();
      });
    }).then(() => forceUpdateMetamaskState(dispatch));
  };
}

export function forgotPassword(forgotPasswordState = true) {
  return {
    type: actionConstants.FORGOT_PASSWORD,
    value: forgotPasswordState,
  };
}

export function closeWelcomeScreen() {
  return {
    type: actionConstants.CLOSE_WELCOME_SCREEN,
  };
}

//
// unlock screen
//

export function unlockInProgress() {
  return {
    type: actionConstants.UNLOCK_IN_PROGRESS,
  };
}

export function unlockFailed(message) {
  return {
    type: actionConstants.UNLOCK_FAILED,
    value: message,
  };
}

export function unlockSucceeded(message) {
  return {
    type: actionConstants.UNLOCK_SUCCEEDED,
    value: message,
  };
}

export function updateMetamaskState(newState) {
  return (dispatch, getState) => {
    const { metamask: currentState } = getState();

    const { currentLocale, selectedAddress, provider } = currentState;
    const {
      currentLocale: newLocale,
      selectedAddress: newSelectedAddress,
      provider: newProvider,
    } = newState;

    if (currentLocale && newLocale && currentLocale !== newLocale) {
      dispatch(updateCurrentLocale(newLocale));
    }

    if (selectedAddress !== newSelectedAddress) {
      dispatch({ type: actionConstants.SELECTED_ADDRESS_CHANGED });
    }

    const newAddressBook = newState.addressBook?.[newProvider?.chainId] ?? {};
    const oldAddressBook = currentState.addressBook?.[provider?.chainId] ?? {};
    const newAccounts = getMetaMaskAccounts({ metamask: newState });
    const oldAccounts = getMetaMaskAccounts({ metamask: currentState });
    const newSelectedAccount = newAccounts[newSelectedAddress];
    const oldSelectedAccount = newAccounts[selectedAddress];
    // dispatch an ACCOUNT_CHANGED for any account whose balance or other
    // properties changed in this update
    Object.entries(oldAccounts).forEach(([address, oldAccount]) => {
      if (!isEqual(oldAccount, newAccounts[address])) {
        dispatch({
          type: actionConstants.ACCOUNT_CHANGED,
          payload: { account: newAccounts[address] },
        });
      }
    });
    // Also emit an event for the selected account changing, either due to a
    // property update or if the entire account changes.
    if (isEqual(oldSelectedAccount, newSelectedAccount) === false) {
      dispatch({
        type: actionConstants.SELECTED_ACCOUNT_CHANGED,
        payload: { account: newSelectedAccount },
      });
    }
    // We need to keep track of changing address book entries
    if (isEqual(oldAddressBook, newAddressBook) === false) {
      dispatch({
        type: actionConstants.ADDRESS_BOOK_UPDATED,
        payload: { addressBook: newAddressBook },
      });
    }

    if (provider.chainId !== newProvider.chainId) {
      dispatch({
        type: actionConstants.CHAIN_CHANGED,
        payload: newProvider.chainId,
      });
    }
    dispatch({
      type: actionConstants.UPDATE_METAMASK_STATE,
      value: newState,
    });
  };
}

const backgroundSetLocked = () => {
  return new Promise((resolve, reject) => {
    background.setLocked((error) => {
      if (error) {
        reject(error);
        return;
      }
      resolve();
    });
  });
};

export function lockMetamask() {
  log.debug(`background.setLocked`);

  return (dispatch) => {
    dispatch(showLoadingIndication());

    return backgroundSetLocked()
      .then(() => updateMetamaskStateFromBackground())
      .catch((error) => {
        dispatch(displayWarning(error.message));
        return Promise.reject(error);
      })
      .then((newState) => {
        dispatch(updateMetamaskState(newState));
        dispatch(hideLoadingIndication());
        dispatch({ type: actionConstants.LOCK_METAMASK });
      })
      .catch(() => {
        dispatch(hideLoadingIndication());
        dispatch({ type: actionConstants.LOCK_METAMASK });
      });
  };
}

async function _setSelectedAddress(dispatch, address) {
  log.debug(`background.setSelectedAddress`);
  const tokens = await promisifiedBackground.setSelectedAddress(address);
  dispatch(updateTokens(tokens));
}

export function setSelectedAddress(address) {
  return async (dispatch) => {
    dispatch(showLoadingIndication());
    log.debug(`background.setSelectedAddress`);
    try {
      await _setSelectedAddress(dispatch, address);
    } catch (error) {
      dispatch(displayWarning(error.message));
      return;
    } finally {
      dispatch(hideLoadingIndication());
    }
  };
}

export function showAccountDetail(address) {
  return async (dispatch, getState) => {
    dispatch(showLoadingIndication());
    log.debug(`background.setSelectedAddress`);

    const state = getState();
    const unconnectedAccountAccountAlertIsEnabled = getUnconnectedAccountAlertEnabledness(
      state,
    );
    const activeTabOrigin = state.activeTab.origin;
    const selectedAddress = getSelectedAddress(state);
    const permittedAccountsForCurrentTab = getPermittedAccountsForCurrentTab(
      state,
    );
    const currentTabIsConnectedToPreviousAddress =
      Boolean(activeTabOrigin) &&
      permittedAccountsForCurrentTab.includes(selectedAddress);
    const currentTabIsConnectedToNextAddress =
      Boolean(activeTabOrigin) &&
      permittedAccountsForCurrentTab.includes(address);
    const switchingToUnconnectedAddress =
      currentTabIsConnectedToPreviousAddress &&
      !currentTabIsConnectedToNextAddress;

    try {
      await _setSelectedAddress(dispatch, address);
      await forceUpdateMetamaskState(dispatch);
    } catch (error) {
      dispatch(displayWarning(error.message));
      return;
    } finally {
      dispatch(hideLoadingIndication());
    }

    dispatch({
      type: actionConstants.SHOW_ACCOUNT_DETAIL,
      value: address,
    });
    if (
      unconnectedAccountAccountAlertIsEnabled &&
      switchingToUnconnectedAddress
    ) {
      dispatch(switchedToUnconnectedAccount());
      await setUnconnectedAccountAlertShown(activeTabOrigin);
    }
  };
}

export function addPermittedAccount(origin, address) {
  return async (dispatch) => {
    await new Promise((resolve, reject) => {
      background.addPermittedAccount(origin, address, (error) => {
        if (error) {
          reject(error);
          return;
        }
        resolve();
      });
    });
    await forceUpdateMetamaskState(dispatch);
  };
}

export function removePermittedAccount(origin, address) {
  return async (dispatch) => {
    await new Promise((resolve, reject) => {
      background.removePermittedAccount(origin, address, (error) => {
        if (error) {
          reject(error);
          return;
        }
        resolve();
      });
    });
    await forceUpdateMetamaskState(dispatch);
  };
}

export function showAccountsPage() {
  return {
    type: actionConstants.SHOW_ACCOUNTS_PAGE,
  };
}

export function showConfTxPage({ id } = {}) {
  return {
    type: actionConstants.SHOW_CONF_TX_PAGE,
    id,
  };
}

export function addToken(
  address,
  symbol,
  decimals,
  image,
  dontShowLoadingIndicator,
) {
  return (dispatch) => {
    if (!address) {
      throw new Error('MetaMask - Cannot add token without address');
    }
    if (!dontShowLoadingIndicator) {
      dispatch(showLoadingIndication());
    }
    return new Promise((resolve, reject) => {
      background.addToken(address, symbol, decimals, image, (err, tokens) => {
        dispatch(hideLoadingIndication());
        if (err) {
          dispatch(displayWarning(err.message));
          reject(err);
          return;
        }
        dispatch(updateTokens(tokens));
        resolve(tokens);
      });
    });
  };
}

export function removeToken(address) {
  return (dispatch) => {
    dispatch(showLoadingIndication());
    return new Promise((resolve, reject) => {
      background.removeToken(address, (err, tokens) => {
        dispatch(hideLoadingIndication());
        if (err) {
          dispatch(displayWarning(err.message));
          reject(err);
          return;
        }
        dispatch(updateTokens(tokens));
        resolve(tokens);
      });
    });
  };
}

export function addTokens(tokens) {
  return (dispatch) => {
    if (Array.isArray(tokens)) {
      return Promise.all(
        tokens.map(({ address, symbol, decimals }) =>
          dispatch(addToken(address, symbol, decimals)),
        ),
      );
    }
    return Promise.all(
      Object.entries(tokens).map(([_, { address, symbol, decimals }]) =>
        dispatch(addToken(address, symbol, decimals)),
      ),
    );
  };
}

export function removeSuggestedTokens() {
  return (dispatch) => {
    dispatch(showLoadingIndication());
    return new Promise((resolve) => {
      background.removeSuggestedTokens((err, suggestedTokens) => {
        dispatch(hideLoadingIndication());
        if (err) {
          dispatch(displayWarning(err.message));
        }
        dispatch(clearPendingTokens());
        if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) {
          global.platform.closeCurrentWindow();
          return;
        }
        resolve(suggestedTokens);
      });
    })
      .then(() => updateMetamaskStateFromBackground())
      .then((suggestedTokens) =>
        dispatch(updateMetamaskState({ ...suggestedTokens })),
      );
  };
}

export function addKnownMethodData(fourBytePrefix, methodData) {
  return () => {
    background.addKnownMethodData(fourBytePrefix, methodData);
  };
}

export function updateTokens(newTokens) {
  return {
    type: actionConstants.UPDATE_TOKENS,
    newTokens,
  };
}

export function clearPendingTokens() {
  return {
    type: actionConstants.CLEAR_PENDING_TOKENS,
  };
}

export function createCancelTransaction(txId, customGasPrice, customGasLimit) {
  log.debug('background.cancelTransaction');
  let newTxId;

  return (dispatch) => {
    return new Promise((resolve, reject) => {
      background.createCancelTransaction(
        txId,
        customGasPrice,
        customGasLimit,
        (err, newState) => {
          if (err) {
            dispatch(displayWarning(err.message));
            reject(err);
            return;
          }

          const { currentNetworkTxList } = newState;
          const { id } = currentNetworkTxList[currentNetworkTxList.length - 1];
          newTxId = id;
          resolve(newState);
        },
      );
    })
      .then((newState) => dispatch(updateMetamaskState(newState)))
      .then(() => newTxId);
  };
}

export function createSpeedUpTransaction(txId, customGasPrice, customGasLimit) {
  log.debug('background.createSpeedUpTransaction');
  let newTx;

  return (dispatch) => {
    return new Promise((resolve, reject) => {
      background.createSpeedUpTransaction(
        txId,
        customGasPrice,
        customGasLimit,
        (err, newState) => {
          if (err) {
            dispatch(displayWarning(err.message));
            reject(err);
            return;
          }

          const { currentNetworkTxList } = newState;
          newTx = currentNetworkTxList[currentNetworkTxList.length - 1];
          resolve(newState);
        },
      );
    })
      .then((newState) => dispatch(updateMetamaskState(newState)))
      .then(() => newTx);
  };
}

export function createRetryTransaction(txId, customGasPrice, customGasLimit) {
  log.debug('background.createRetryTransaction');
  let newTx;

  return (dispatch) => {
    return new Promise((resolve, reject) => {
      background.createSpeedUpTransaction(
        txId,
        customGasPrice,
        customGasLimit,
        (err, newState) => {
          if (err) {
            dispatch(displayWarning(err.message));
            reject(err);
            return;
          }

          const { currentNetworkTxList } = newState;
          newTx = currentNetworkTxList[currentNetworkTxList.length - 1];
          resolve(newState);
        },
      );
    })
      .then((newState) => dispatch(updateMetamaskState(newState)))
      .then(() => newTx);
  };
}

//
// config
//

export function setProviderType(type) {
  return async (dispatch) => {
    log.debug(`background.setProviderType`, type);

    try {
      await promisifiedBackground.setProviderType(type);
    } catch (error) {
      log.error(error);
      dispatch(displayWarning('Had a problem changing networks!'));
      return;
    }
    dispatch(updateProviderType(type));
  };
}

export function updateProviderType(type) {
  return {
    type: actionConstants.SET_PROVIDER_TYPE,
    value: type,
  };
}

export function updateAndSetCustomRpc(
  newRpc,
  chainId,
  ticker = 'ETH',
  nickname,
  rpcPrefs,
) {
  return async (dispatch) => {
    log.debug(
      `background.updateAndSetCustomRpc: ${newRpc} ${chainId} ${ticker} ${nickname}`,
    );

    try {
      await promisifiedBackground.updateAndSetCustomRpc(
        newRpc,
        chainId,
        ticker,
        nickname || newRpc,
        rpcPrefs,
      );
    } catch (error) {
      log.error(error);
      dispatch(displayWarning('Had a problem changing networks!'));
      return;
    }

    dispatch({
      type: actionConstants.SET_RPC_TARGET,
      value: newRpc,
    });
  };
}

export function editRpc(
  oldRpc,
  newRpc,
  chainId,
  ticker = 'ETH',
  nickname,
  rpcPrefs,
) {
  return async (dispatch) => {
    log.debug(`background.delRpcTarget: ${oldRpc}`);
    try {
      promisifiedBackground.delCustomRpc(oldRpc);
    } catch (error) {
      log.error(error);
      dispatch(displayWarning('Had a problem removing network!'));
      return;
    }

    try {
      await promisifiedBackground.updateAndSetCustomRpc(
        newRpc,
        chainId,
        ticker,
        nickname || newRpc,
        rpcPrefs,
      );
    } catch (error) {
      log.error(error);
      dispatch(displayWarning('Had a problem changing networks!'));
      return;
    }

    dispatch({
      type: actionConstants.SET_RPC_TARGET,
      value: newRpc,
    });
  };
}

export function setRpcTarget(newRpc, chainId, ticker = 'ETH', nickname) {
  return async (dispatch) => {
    log.debug(
      `background.setRpcTarget: ${newRpc} ${chainId} ${ticker} ${nickname}`,
    );

    try {
      await promisifiedBackground.setCustomRpc(
        newRpc,
        chainId,
        ticker,
        nickname || newRpc,
      );
    } catch (error) {
      log.error(error);
      dispatch(displayWarning('Had a problem changing networks!'));
    }
  };
}

export function rollbackToPreviousProvider() {
  return async (dispatch) => {
    try {
      await promisifiedBackground.rollbackToPreviousProvider();
    } catch (error) {
      log.error(error);
      dispatch(displayWarning('Had a problem changing networks!'));
    }
  };
}

export function delRpcTarget(oldRpc) {
  return (dispatch) => {
    log.debug(`background.delRpcTarget: ${oldRpc}`);
    return new Promise((resolve, reject) => {
      background.delCustomRpc(oldRpc, (err) => {
        if (err) {
          log.error(err);
          dispatch(displayWarning('Had a problem removing network!'));
          reject(err);
          return;
        }
        resolve();
      });
    });
  };
}

// Calls the addressBookController to add a new address.
export function addToAddressBook(recipient, nickname = '', memo = '') {
  log.debug(`background.addToAddressBook`);

  return async (dispatch, getState) => {
    const { chainId } = getState().metamask.provider;

    let set;
    try {
      set = await promisifiedBackground.setAddressBook(
        toChecksumHexAddress(recipient),
        nickname,
        chainId,
        memo,
      );
    } catch (error) {
      log.error(error);
      dispatch(displayWarning('Address book failed to update'));
      throw error;
    }
    if (!set) {
      dispatch(displayWarning('Address book failed to update'));
    }
  };
}

/**
 * @description Calls the addressBookController to remove an existing address.
 * @param {string} addressToRemove - Address of the entry to remove from the address book
 */
export function removeFromAddressBook(chainId, addressToRemove) {
  log.debug(`background.removeFromAddressBook`);

  return async () => {
    await promisifiedBackground.removeFromAddressBook(
      chainId,
      toChecksumHexAddress(addressToRemove),
    );
  };
}

export function showNetworkDropdown() {
  return {
    type: actionConstants.NETWORK_DROPDOWN_OPEN,
  };
}

export function hideNetworkDropdown() {
  return {
    type: actionConstants.NETWORK_DROPDOWN_CLOSE,
  };
}

export function showModal(payload) {
  return {
    type: actionConstants.MODAL_OPEN,
    payload,
  };
}

export function hideModal(payload) {
  return {
    type: actionConstants.MODAL_CLOSE,
    payload,
  };
}

export function closeCurrentNotificationWindow() {
  return (_, getState) => {
    if (
      getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION &&
      !hasUnconfirmedTransactions(getState())
    ) {
      global.platform.closeCurrentWindow();
    }
  };
}

export function showSidebar({ transitionName, type, props }) {
  return {
    type: actionConstants.SIDEBAR_OPEN,
    value: {
      transitionName,
      type,
      props,
    },
  };
}

export function hideSidebar() {
  return {
    type: actionConstants.SIDEBAR_CLOSE,
  };
}

export function showAlert(msg) {
  return {
    type: actionConstants.ALERT_OPEN,
    value: msg,
  };
}

export function hideAlert() {
  return {
    type: actionConstants.ALERT_CLOSE,
  };
}

/**
 * This action will receive two types of values via qrCodeData
 * an object with the following structure {type, values}
 * or null (used to clear the previous value)
 */
export function qrCodeDetected(qrCodeData) {
  return async (dispatch) => {
    await dispatch({
      type: actionConstants.QR_CODE_DETECTED,
      value: qrCodeData,
    });

    // If on the send page, the send slice will listen for the QR_CODE_DETECTED
    // action and update its state. Address changes need to recompute gasLimit
    // so we fire this method so that the send page gasLimit can be recomputed
    dispatch(computeEstimatedGasLimit());
  };
}

export function showLoadingIndication(message) {
  return {
    type: actionConstants.SHOW_LOADING,
    value: message,
  };
}

export function setHardwareWalletDefaultHdPath({ device, path }) {
  return {
    type: actionConstants.SET_HARDWARE_WALLET_DEFAULT_HD_PATH,
    value: { device, path },
  };
}

export function hideLoadingIndication() {
  return {
    type: actionConstants.HIDE_LOADING,
  };
}

export function displayWarning(text) {
  return {
    type: actionConstants.DISPLAY_WARNING,
    value: text,
  };
}

export function hideWarning() {
  return {
    type: actionConstants.HIDE_WARNING,
  };
}

export function exportAccount(password, address) {
  return function (dispatch) {
    dispatch(showLoadingIndication());

    log.debug(`background.verifyPassword`);
    return new Promise((resolve, reject) => {
      background.verifyPassword(password, function (err) {
        if (err) {
          log.error('Error in verifying password.');
          dispatch(hideLoadingIndication());
          dispatch(displayWarning('Incorrect Password.'));
          reject(err);
          return;
        }
        log.debug(`background.exportAccount`);
        background.exportAccount(address, function (err2, result) {
          dispatch(hideLoadingIndication());

          if (err2) {
            log.error(err2);
            dispatch(displayWarning('Had a problem exporting the account.'));
            reject(err2);
            return;
          }

          dispatch(showPrivateKey(result));
          resolve(result);
        });
      });
    });
  };
}

export function exportAccounts(password, addresses) {
  return function (dispatch) {
    log.debug(`background.verifyPassword`);
    return new Promise((resolve, reject) => {
      background.verifyPassword(password, function (err) {
        if (err) {
          log.error('Error in submitting password.');
          reject(err);
          return;
        }
        log.debug(`background.exportAccounts`);
        const accountPromises = addresses.map(
          (address) =>
            new Promise((resolve2, reject2) =>
              background.exportAccount(address, function (err2, result) {
                if (err2) {
                  log.error(err2);
                  dispatch(
                    displayWarning('Had a problem exporting the account.'),
                  );
                  reject2(err2);
                  return;
                }
                resolve2(result);
              }),
            ),
        );
        resolve(Promise.all(accountPromises));
      });
    });
  };
}

export function showPrivateKey(key) {
  return {
    type: actionConstants.SHOW_PRIVATE_KEY,
    value: key,
  };
}

export function setAccountLabel(account, label) {
  return (dispatch) => {
    dispatch(showLoadingIndication());
    log.debug(`background.setAccountLabel`);

    return new Promise((resolve, reject) => {
      background.setAccountLabel(account, label, (err) => {
        dispatch(hideLoadingIndication());

        if (err) {
          dispatch(displayWarning(err.message));
          reject(err);
          return;
        }

        dispatch({
          type: actionConstants.SET_ACCOUNT_LABEL,
          value: { account, label },
        });
        resolve(account);
      });
    });
  };
}

export function clearAccountDetails() {
  return {
    type: actionConstants.CLEAR_ACCOUNT_DETAILS,
  };
}

export function showSendTokenPage() {
  return {
    type: actionConstants.SHOW_SEND_TOKEN_PAGE,
  };
}

export function buyEth(opts) {
  return async (dispatch) => {
    const url = await getBuyEthUrl(opts);
    global.platform.openTab({ url });
    dispatch({
      type: actionConstants.BUY_ETH,
    });
  };
}

export function setFeatureFlag(feature, activated, notificationType) {
  return (dispatch) => {
    dispatch(showLoadingIndication());
    return new Promise((resolve, reject) => {
      background.setFeatureFlag(
        feature,
        activated,
        (err, updatedFeatureFlags) => {
          dispatch(hideLoadingIndication());
          if (err) {
            dispatch(displayWarning(err.message));
            reject(err);
            return;
          }
          dispatch(updateFeatureFlags(updatedFeatureFlags));
          notificationType && dispatch(showModal({ name: notificationType }));
          resolve(updatedFeatureFlags);
        },
      );
    });
  };
}

export function updateFeatureFlags(updatedFeatureFlags) {
  return {
    type: actionConstants.UPDATE_FEATURE_FLAGS,
    value: updatedFeatureFlags,
  };
}

export function setPreference(preference, value) {
  return (dispatch) => {
    dispatch(showLoadingIndication());
    return new Promise((resolve, reject) => {
      background.setPreference(preference, value, (err, updatedPreferences) => {
        dispatch(hideLoadingIndication());

        if (err) {
          dispatch(displayWarning(err.message));
          reject(err);
          return;
        }

        dispatch(updatePreferences(updatedPreferences));
        resolve(updatedPreferences);
      });
    });
  };
}

export function updatePreferences(value) {
  return {
    type: actionConstants.UPDATE_PREFERENCES,
    value,
  };
}

export function setDefaultHomeActiveTabName(value) {
  return async (dispatch) => {
    await promisifiedBackground.setDefaultHomeActiveTabName(value);
    await forceUpdateMetamaskState(dispatch);
  };
}

export function setUseNativeCurrencyAsPrimaryCurrencyPreference(value) {
  return setPreference('useNativeCurrencyAsPrimaryCurrency', value);
}

export function setHideZeroBalanceTokens(value) {
  return setPreference('hideZeroBalanceTokens', value);
}

export function setShowFiatConversionOnTestnetsPreference(value) {
  return setPreference('showFiatInTestnets', value);
}

export function setAutoLockTimeLimit(value) {
  return setPreference('autoLockTimeLimit', value);
}

export function setCompletedOnboarding() {
  return async (dispatch) => {
    dispatch(showLoadingIndication());

    try {
      await promisifiedBackground.completeOnboarding();
      dispatch(completeOnboarding());
    } catch (err) {
      dispatch(displayWarning(err.message));
      throw err;
    } finally {
      dispatch(hideLoadingIndication());
    }
  };
}

export function completeOnboarding() {
  return {
    type: actionConstants.COMPLETE_ONBOARDING,
  };
}

export function setMouseUserState(isMouseUser) {
  return {
    type: actionConstants.SET_MOUSE_USER_STATE,
    value: isMouseUser,
  };
}

export async function forceUpdateMetamaskState(dispatch) {
  log.debug(`background.getState`);

  let newState;
  try {
    newState = await promisifiedBackground.getState();
  } catch (error) {
    dispatch(displayWarning(error.message));
    throw error;
  }

  dispatch(updateMetamaskState(newState));
  return newState;
}

export function toggleAccountMenu() {
  return {
    type: actionConstants.TOGGLE_ACCOUNT_MENU,
  };
}

export function setParticipateInMetaMetrics(val) {
  return (dispatch) => {
    log.debug(`background.setParticipateInMetaMetrics`);
    return new Promise((resolve, reject) => {
      background.setParticipateInMetaMetrics(val, (err, metaMetricsId) => {
        log.debug(err);
        if (err) {
          dispatch(displayWarning(err.message));
          reject(err);
          return;
        }

        dispatch({
          type: actionConstants.SET_PARTICIPATE_IN_METAMETRICS,
          value: val,
        });
        resolve([val, metaMetricsId]);
      });
    });
  };
}

export function setUseBlockie(val) {
  return (dispatch) => {
    dispatch(showLoadingIndication());
    log.debug(`background.setUseBlockie`);
    background.setUseBlockie(val, (err) => {
      dispatch(hideLoadingIndication());
      if (err) {
        dispatch(displayWarning(err.message));
      }
    });
    dispatch({
      type: actionConstants.SET_USE_BLOCKIE,
      value: val,
    });
  };
}

export function setUseNonceField(val) {
  return (dispatch) => {
    dispatch(showLoadingIndication());
    log.debug(`background.setUseNonceField`);
    background.setUseNonceField(val, (err) => {
      dispatch(hideLoadingIndication());
      if (err) {
        dispatch(displayWarning(err.message));
      }
    });
    dispatch({
      type: actionConstants.SET_USE_NONCEFIELD,
      value: val,
    });
  };
}

export function setUsePhishDetect(val) {
  return (dispatch) => {
    dispatch(showLoadingIndication());
    log.debug(`background.setUsePhishDetect`);
    background.setUsePhishDetect(val, (err) => {
      dispatch(hideLoadingIndication());
      if (err) {
        dispatch(displayWarning(err.message));
      }
    });
  };
}

export function setIpfsGateway(val) {
  return (dispatch) => {
    dispatch(showLoadingIndication());
    log.debug(`background.setIpfsGateway`);
    background.setIpfsGateway(val, (err) => {
      dispatch(hideLoadingIndication());
      if (err) {
        dispatch(displayWarning(err.message));
      } else {
        dispatch({
          type: actionConstants.SET_IPFS_GATEWAY,
          value: val,
        });
      }
    });
  };
}

export function updateCurrentLocale(key) {
  return async (dispatch) => {
    dispatch(showLoadingIndication());

    try {
      await loadRelativeTimeFormatLocaleData(key);
      const localeMessages = await fetchLocale(key);
      const textDirection = await promisifiedBackground.setCurrentLocale(key);
      await switchDirection(textDirection);
      dispatch(setCurrentLocale(key, localeMessages));
    } catch (error) {
      dispatch(displayWarning(error.message));
      return;
    } finally {
      dispatch(hideLoadingIndication());
    }
  };
}

export function setCurrentLocale(locale, messages) {
  return {
    type: actionConstants.SET_CURRENT_LOCALE,
    value: {
      locale,
      messages,
    },
  };
}

export function setPendingTokens(pendingTokens) {
  const { customToken = {}, selectedTokens = {} } = pendingTokens;
  const { address, symbol, decimals } = customToken;
  const tokens =
    address && symbol && decimals >= 0 <= 36
      ? {
          ...selectedTokens,
          [address]: {
            ...customToken,
            isCustom: true,
          },
        }
      : selectedTokens;

  Object.keys(tokens).forEach((tokenAddress) => {
    tokens[tokenAddress].unlisted = !LISTED_CONTRACT_ADDRESSES.includes(
      tokenAddress.toLowerCase(),
    );
  });

  return {
    type: actionConstants.SET_PENDING_TOKENS,
    payload: tokens,
  };
}

// Swaps

export function setSwapsLiveness(swapsFeatureIsLive) {
  return async (dispatch) => {
    await promisifiedBackground.setSwapsLiveness(swapsFeatureIsLive);
    await forceUpdateMetamaskState(dispatch);
  };
}

export function fetchAndSetQuotes(fetchParams, fetchParamsMetaData) {
  return async (dispatch) => {
    const [
      quotes,
      selectedAggId,
    ] = await promisifiedBackground.fetchAndSetQuotes(
      fetchParams,
      fetchParamsMetaData,
    );
    await forceUpdateMetamaskState(dispatch);
    return [quotes, selectedAggId];
  };
}

export function setSelectedQuoteAggId(aggId) {
  return async (dispatch) => {
    await promisifiedBackground.setSelectedQuoteAggId(aggId);
    await forceUpdateMetamaskState(dispatch);
  };
}

export function setSwapsTokens(tokens) {
  return async (dispatch) => {
    await promisifiedBackground.setSwapsTokens(tokens);
    await forceUpdateMetamaskState(dispatch);
  };
}

export function resetBackgroundSwapsState() {
  return async (dispatch) => {
    const id = await promisifiedBackground.resetSwapsState();
    await forceUpdateMetamaskState(dispatch);
    return id;
  };
}

export function setCustomApproveTxData(data) {
  return async (dispatch) => {
    await promisifiedBackground.setCustomApproveTxData(data);
    await forceUpdateMetamaskState(dispatch);
  };
}

export function setSwapsTxGasPrice(gasPrice) {
  return async (dispatch) => {
    await promisifiedBackground.setSwapsTxGasPrice(gasPrice);
    await forceUpdateMetamaskState(dispatch);
  };
}

export function setSwapsTxGasLimit(gasLimit) {
  return async (dispatch) => {
    await promisifiedBackground.setSwapsTxGasLimit(gasLimit, true);
    await forceUpdateMetamaskState(dispatch);
  };
}

export function customSwapsGasParamsUpdated(gasLimit, gasPrice) {
  return async (dispatch) => {
    await promisifiedBackground.setSwapsTxGasPrice(gasPrice);
    await promisifiedBackground.setSwapsTxGasLimit(gasLimit, true);
    await forceUpdateMetamaskState(dispatch);
  };
}

export function setTradeTxId(tradeTxId) {
  return async (dispatch) => {
    await promisifiedBackground.setTradeTxId(tradeTxId);
    await forceUpdateMetamaskState(dispatch);
  };
}

export function setApproveTxId(approveTxId) {
  return async (dispatch) => {
    await promisifiedBackground.setApproveTxId(approveTxId);
    await forceUpdateMetamaskState(dispatch);
  };
}

export function safeRefetchQuotes() {
  return async (dispatch) => {
    await promisifiedBackground.safeRefetchQuotes();
    await forceUpdateMetamaskState(dispatch);
  };
}

export function stopPollingForQuotes() {
  return async (dispatch) => {
    await promisifiedBackground.stopPollingForQuotes();
    await forceUpdateMetamaskState(dispatch);
  };
}

export function setBackgroundSwapRouteState(routeState) {
  return async (dispatch) => {
    await promisifiedBackground.setBackgroundSwapRouteState(routeState);
    await forceUpdateMetamaskState(dispatch);
  };
}

export function resetSwapsPostFetchState() {
  return async (dispatch) => {
    await promisifiedBackground.resetPostFetchState();
    await forceUpdateMetamaskState(dispatch);
  };
}

export function setSwapsErrorKey(errorKey) {
  return async (dispatch) => {
    await promisifiedBackground.setSwapsErrorKey(errorKey);
    await forceUpdateMetamaskState(dispatch);
  };
}

export function setInitialGasEstimate(initialAggId) {
  return async (dispatch) => {
    await promisifiedBackground.setInitialGasEstimate(initialAggId);
    await forceUpdateMetamaskState(dispatch);
  };
}

// Permissions

export function requestAccountsPermissionWithId(origin) {
  return async (dispatch) => {
    const id = await promisifiedBackground.requestAccountsPermissionWithId(
      origin,
    );
    await forceUpdateMetamaskState(dispatch);
    return id;
  };
}

/**
 * Approves the permissions request.
 * @param {Object} request - The permissions request to approve
 * @param {string[]} accounts - The accounts to expose, if any.
 */
export function approvePermissionsRequest(request, accounts) {
  return (dispatch) => {
    background.approvePermissionsRequest(request, accounts, (err) => {
      if (err) {
        dispatch(displayWarning(err.message));
      }
    });
  };
}

/**
 * Rejects the permissions request with the given ID.
 * @param {string} requestId - The id of the request to be rejected
 */
export function rejectPermissionsRequest(requestId) {
  return (dispatch) => {
    return new Promise((resolve, reject) => {
      background.rejectPermissionsRequest(requestId, (err) => {
        if (err) {
          dispatch(displayWarning(err.message));
          reject(err);
          return;
        }
        forceUpdateMetamaskState(dispatch).then(resolve).catch(reject);
      });
    });
  };
}

/**
 * Clears the given permissions for the given origin.
 */
export function removePermissionsFor(domains) {
  return (dispatch) => {
    background.removePermissionsFor(domains, (err) => {
      if (err) {
        dispatch(displayWarning(err.message));
      }
    });
  };
}

/**
 * Clears all permissions for all domains.
 */
export function clearPermissions() {
  return (dispatch) => {
    background.clearPermissions((err) => {
      if (err) {
        dispatch(displayWarning(err.message));
      }
    });
  };
}

// Pending Approvals

/**
 * Resolves a pending approval and closes the current notification window if no
 * further approvals are pending after the background state updates.
 * @param {string} id - The pending approval id
 * @param {any} [value] - The value required to confirm a pending approval
 */
export function resolvePendingApproval(id, value) {
  return async (dispatch) => {
    await promisifiedBackground.resolvePendingApproval(id, value);
    // Before closing the current window, check if any additional confirmations
    // are added as a result of this confirmation being accepted
    const { pendingApprovals } = await forceUpdateMetamaskState(dispatch);
    if (Object.values(pendingApprovals).length === 0) {
      dispatch(closeCurrentNotificationWindow());
    }
  };
}

/**
 * Rejects a pending approval and closes the current notification window if no
 * further approvals are pending after the background state updates.
 * @param {string} id - The pending approval id
 * @param {Error} [error] - The error to throw when rejecting the approval
 */
export function rejectPendingApproval(id, error) {
  return async (dispatch) => {
    await promisifiedBackground.rejectPendingApproval(id, error);
    // Before closing the current window, check if any additional confirmations
    // are added as a result of this confirmation being rejected
    const { pendingApprovals } = await forceUpdateMetamaskState(dispatch);
    if (Object.values(pendingApprovals).length === 0) {
      dispatch(closeCurrentNotificationWindow());
    }
  };
}

export function setFirstTimeFlowType(type) {
  return (dispatch) => {
    log.debug(`background.setFirstTimeFlowType`);
    background.setFirstTimeFlowType(type, (err) => {
      if (err) {
        dispatch(displayWarning(err.message));
      }
    });
    dispatch({
      type: actionConstants.SET_FIRST_TIME_FLOW_TYPE,
      value: type,
    });
  };
}

export function setSelectedSettingsRpcUrl(newRpcUrl) {
  return {
    type: actionConstants.SET_SELECTED_SETTINGS_RPC_URL,
    value: newRpcUrl,
  };
}

export function setNetworksTabAddMode(isInAddMode) {
  return {
    type: actionConstants.SET_NETWORKS_TAB_ADD_MODE,
    value: isInAddMode,
  };
}

export function setLastActiveTime() {
  return (dispatch) => {
    background.setLastActiveTime((err) => {
      if (err) {
        dispatch(displayWarning(err.message));
      }
    });
  };
}

export function setDismissSeedBackUpReminder(value) {
  return async (dispatch) => {
    dispatch(showLoadingIndication());
    await promisifiedBackground.setDismissSeedBackUpReminder(value);
    dispatch(hideLoadingIndication());
  };
}

export function setConnectedStatusPopoverHasBeenShown() {
  return () => {
    background.setConnectedStatusPopoverHasBeenShown((err) => {
      if (err) {
        throw new Error(err.message);
      }
    });
  };
}

export function setRecoveryPhraseReminderHasBeenShown() {
  return () => {
    background.setRecoveryPhraseReminderHasBeenShown((err) => {
      if (err) {
        throw new Error(err.message);
      }
    });
  };
}

export function setRecoveryPhraseReminderLastShown(lastShown) {
  return () => {
    background.setRecoveryPhraseReminderLastShown(lastShown, (err) => {
      if (err) {
        throw new Error(err.message);
      }
    });
  };
}

export function loadingMethodDataStarted() {
  return {
    type: actionConstants.LOADING_METHOD_DATA_STARTED,
  };
}

export function loadingMethodDataFinished() {
  return {
    type: actionConstants.LOADING_METHOD_DATA_FINISHED,
  };
}

export function getContractMethodData(data = '') {
  return (dispatch, getState) => {
    const prefixedData = addHexPrefix(data);
    const fourBytePrefix = prefixedData.slice(0, 10);
    const { knownMethodData } = getState().metamask;

    if (
      (knownMethodData &&
        knownMethodData[fourBytePrefix] &&
        Object.keys(knownMethodData[fourBytePrefix]).length !== 0) ||
      fourBytePrefix === '0x'
    ) {
      return Promise.resolve(knownMethodData[fourBytePrefix]);
    }

    dispatch(loadingMethodDataStarted());
    log.debug(`loadingMethodData`);

    return getMethodDataAsync(fourBytePrefix).then(({ name, params }) => {
      dispatch(loadingMethodDataFinished());
      background.addKnownMethodData(fourBytePrefix, { name, params }, (err) => {
        if (err) {
          dispatch(displayWarning(err.message));
        }
      });
      return { name, params };
    });
  };
}

export function loadingTokenParamsStarted() {
  return {
    type: actionConstants.LOADING_TOKEN_PARAMS_STARTED,
  };
}

export function loadingTokenParamsFinished() {
  return {
    type: actionConstants.LOADING_TOKEN_PARAMS_FINISHED,
  };
}

export function getTokenParams(tokenAddress) {
  return (dispatch, getState) => {
    const existingTokens = getState().metamask.tokens;
    const existingToken = existingTokens.find(
      ({ address }) => tokenAddress === address,
    );

    if (existingToken) {
      return Promise.resolve({
        symbol: existingToken.symbol,
        decimals: existingToken.decimals,
      });
    }

    dispatch(loadingTokenParamsStarted());
    log.debug(`loadingTokenParams`);

    return getSymbolAndDecimals(tokenAddress).then(({ symbol, decimals }) => {
      dispatch(addToken(tokenAddress, symbol, Number(decimals)));
      dispatch(loadingTokenParamsFinished());
    });
  };
}

export function setSeedPhraseBackedUp(seedPhraseBackupState) {
  return (dispatch) => {
    log.debug(`background.setSeedPhraseBackedUp`);
    return new Promise((resolve, reject) => {
      background.setSeedPhraseBackedUp(seedPhraseBackupState, (err) => {
        if (err) {
          dispatch(displayWarning(err.message));
          reject(err);
          return;
        }
        forceUpdateMetamaskState(dispatch).then(resolve).catch(reject);
      });
    });
  };
}

export function initializeThreeBox() {
  return (dispatch) => {
    return new Promise((resolve, reject) => {
      background.initializeThreeBox((err) => {
        if (err) {
          dispatch(displayWarning(err.message));
          reject(err);
          return;
        }
        resolve();
      });
    });
  };
}

export function setShowRestorePromptToFalse() {
  return (dispatch) => {
    return new Promise((resolve, reject) => {
      background.setShowRestorePromptToFalse((err) => {
        if (err) {
          dispatch(displayWarning(err.message));
          reject(err);
          return;
        }
        resolve();
      });
    });
  };
}

export function turnThreeBoxSyncingOn() {
  return (dispatch) => {
    return new Promise((resolve, reject) => {
      background.turnThreeBoxSyncingOn((err) => {
        if (err) {
          dispatch(displayWarning(err.message));
          reject(err);
          return;
        }
        resolve();
      });
    });
  };
}

export function restoreFromThreeBox(accountAddress) {
  return (dispatch) => {
    return new Promise((resolve, reject) => {
      background.restoreFromThreeBox(accountAddress, (err) => {
        if (err) {
          dispatch(displayWarning(err.message));
          reject(err);
          return;
        }
        resolve();
      });
    });
  };
}

export function getThreeBoxLastUpdated() {
  return (dispatch) => {
    return new Promise((resolve, reject) => {
      background.getThreeBoxLastUpdated((err, lastUpdated) => {
        if (err) {
          dispatch(displayWarning(err.message));
          reject(err);
          return;
        }
        resolve(lastUpdated);
      });
    });
  };
}

export function setThreeBoxSyncingPermission(threeBoxSyncingAllowed) {
  return (dispatch) => {
    return new Promise((resolve, reject) => {
      background.setThreeBoxSyncingPermission(threeBoxSyncingAllowed, (err) => {
        if (err) {
          dispatch(displayWarning(err.message));
          reject(err);
          return;
        }
        resolve();
      });
    });
  };
}

export function turnThreeBoxSyncingOnAndInitialize() {
  return async (dispatch) => {
    await dispatch(setThreeBoxSyncingPermission(true));
    await dispatch(turnThreeBoxSyncingOn());
    await dispatch(initializeThreeBox(true));
  };
}

export function setNextNonce(nextNonce) {
  return {
    type: actionConstants.SET_NEXT_NONCE,
    value: nextNonce,
  };
}

export function getNextNonce() {
  return (dispatch, getState) => {
    const address = getState().metamask.selectedAddress;
    return new Promise((resolve, reject) => {
      background.getNextNonce(address, (err, nextNonce) => {
        if (err) {
          dispatch(displayWarning(err.message));
          reject(err);
          return;
        }
        dispatch(setNextNonce(nextNonce));
        resolve(nextNonce);
      });
    });
  };
}

export function setRequestAccountTabIds(requestAccountTabIds) {
  return {
    type: actionConstants.SET_REQUEST_ACCOUNT_TABS,
    value: requestAccountTabIds,
  };
}

export function getRequestAccountTabIds() {
  return async (dispatch) => {
    const requestAccountTabIds = await promisifiedBackground.getRequestAccountTabIds();
    dispatch(setRequestAccountTabIds(requestAccountTabIds));
  };
}

export function setOpenMetamaskTabsIDs(openMetaMaskTabIDs) {
  return {
    type: actionConstants.SET_OPEN_METAMASK_TAB_IDS,
    value: openMetaMaskTabIDs,
  };
}

export function getOpenMetamaskTabsIds() {
  return async (dispatch) => {
    const openMetaMaskTabIDs = await promisifiedBackground.getOpenMetamaskTabsIds();
    dispatch(setOpenMetamaskTabsIDs(openMetaMaskTabIDs));
  };
}

export function setCurrentWindowTab(currentWindowTab) {
  return {
    type: actionConstants.SET_CURRENT_WINDOW_TAB,
    value: currentWindowTab,
  };
}

export function getCurrentWindowTab() {
  return async (dispatch) => {
    const currentWindowTab = await global.platform.currentTab();
    dispatch(setCurrentWindowTab(currentWindowTab));
  };
}

export function setLedgerLivePreference(value) {
  return async (dispatch) => {
    dispatch(showLoadingIndication());
    await promisifiedBackground.setLedgerLivePreference(value);
    dispatch(hideLoadingIndication());
  };
}

// Wrappers around promisifedBackground
/**
 * The "actions" below are not actions nor action creators. They cannot use
 * dispatch nor should they be dispatched when used. Instead they can be
 * called directly. These wrappers will be moved into their location at some
 * point in the future.
 */

export function estimateGas(params) {
  return promisifiedBackground.estimateGas(params);
}

export async function updateTokenType(tokenAddress) {
  let token = {};
  try {
    token = await promisifiedBackground.updateTokenType(tokenAddress);
  } catch (error) {
    log.error(error);
  }
  return token;
}

// MetaMetrics
/**
 * @typedef {import('../../shared/constants/metametrics').MetaMetricsEventPayload} MetaMetricsEventPayload
 * @typedef {import('../../shared/constants/metametrics').MetaMetricsEventOptions} MetaMetricsEventOptions
 * @typedef {import('../../shared/constants/metametrics').MetaMetricsPagePayload} MetaMetricsPagePayload
 * @typedef {import('../../shared/constants/metametrics').MetaMetricsPageOptions} MetaMetricsPageOptions
 */

/**
 * @param {MetaMetricsEventPayload} payload - details of the event to track
 * @param {MetaMetricsEventOptions} options - options for routing/handling of event
 * @returns {Promise<void>}
 */
export function trackMetaMetricsEvent(payload, options) {
  return promisifiedBackground.trackMetaMetricsEvent(payload, options);
}

/**
 * @param {MetaMetricsPagePayload} payload - details of the page viewed
 * @param {MetaMetricsPageOptions} options - options for handling the page view
 * @returns {void}
 */
export function trackMetaMetricsPage(payload, options) {
  return promisifiedBackground.trackMetaMetricsPage(payload, options);
}

export function updateViewedNotifications(notificationIdViewedStatusMap) {
  return promisifiedBackground.updateViewedNotifications(
    notificationIdViewedStatusMap,
  );
}

export async function setAlertEnabledness(alertId, enabledness) {
  await promisifiedBackground.setAlertEnabledness(alertId, enabledness);
}

export async function setUnconnectedAccountAlertShown(origin) {
  await promisifiedBackground.setUnconnectedAccountAlertShown(origin);
}

export async function setWeb3ShimUsageAlertDismissed(origin) {
  await promisifiedBackground.setWeb3ShimUsageAlertDismissed(origin);
}