import { TransactionType } from '../../../../shared/constants/transaction';
import { sumHexes } from '../../../../shared/modules/conversion.utils';
import { getHexGasTotal } from '../../../helpers/utils/confirm-tx.util';

import {
  // event constants
  TRANSACTION_CREATED_EVENT,
  TRANSACTION_SUBMITTED_EVENT,
  TRANSACTION_RESUBMITTED_EVENT,
  TRANSACTION_CONFIRMED_EVENT,
  TRANSACTION_DROPPED_EVENT,
  TRANSACTION_UPDATED_EVENT,
  TRANSACTION_ERRORED_EVENT,
  TRANSACTION_CANCEL_ATTEMPTED_EVENT,
  TRANSACTION_CANCEL_SUCCESS_EVENT,
  // status constants
  SUBMITTED_STATUS,
  CONFIRMED_STATUS,
  DROPPED_STATUS,
} from './transaction-activity-log.constants';

// path constants
const STATUS_PATH = '/status';
const GAS_PRICE_PATH = '/txParams/gasPrice';
const GAS_LIMIT_PATH = '/txParams/gas';
const ESTIMATE_BASE_FEE_PATH = '/estimatedBaseFee';
const BLOCKTIMESTAMP = '/blockTimestamp';

// op constants
const REPLACE_OP = 'replace';

const eventPathsHash = {
  [STATUS_PATH]: true,
  [GAS_PRICE_PATH]: true,
  [GAS_LIMIT_PATH]: true,
  [BLOCKTIMESTAMP]: true,
};

const statusHash = {
  [SUBMITTED_STATUS]: TRANSACTION_SUBMITTED_EVENT,
  [CONFIRMED_STATUS]: TRANSACTION_CONFIRMED_EVENT,
  [DROPPED_STATUS]: TRANSACTION_DROPPED_EVENT,
};

/**
 * @name getActivities
 * @param {object} transaction - txMeta object
 * @param {boolean} isFirstTransaction - True if the transaction is the first created transaction
 * in the list of transactions with the same nonce. If so, we use this transaction to create the
 * transactionCreated activity.
 * @returns {Array}
 */
export function getActivities(transaction, isFirstTransaction = false) {
  const {
    id,
    chainId,
    metamaskNetworkId,
    hash,
    history = [],
    txParams: {
      gas: paramsGasLimit,
      gasPrice: paramsGasPrice,
      maxPriorityFeePerGas: paramsMaxPriorityFeePerGas,
    },
    txReceipt: { status } = {},
    type,
    estimatedBaseFee: paramsEstimatedBaseFee,
  } = transaction;

  const paramsEip1559Price =
    paramsEstimatedBaseFee &&
    paramsMaxPriorityFeePerGas &&
    sumHexes(paramsEstimatedBaseFee, paramsMaxPriorityFeePerGas);
  let cachedGasLimit = '0x0';
  let cachedGasPrice = '0x0';

  const historyActivities = history.reduce((acc, base, index) => {
    // First history item should be transaction creation
    if (index === 0 && !Array.isArray(base) && base.txParams) {
      const {
        time: timestamp,
        estimatedBaseFee,
        txParams: { value, gas = '0x0', gasPrice, maxPriorityFeePerGas } = {},
      } = base;

      const eip1559Price =
        estimatedBaseFee &&
        maxPriorityFeePerGas &&
        sumHexes(estimatedBaseFee, maxPriorityFeePerGas);
      // The cached gas limit and gas price are used to display the gas fee in the activity log. We
      // need to cache these values because the status update history events don't provide us with
      // the latest gas limit and gas price.
      cachedGasLimit = gas;
      cachedGasPrice = eip1559Price || gasPrice || paramsGasPrice || '0x0';

      if (isFirstTransaction) {
        return acc.concat({
          id,
          hash,
          chainId,
          metamaskNetworkId,
          eventKey: TRANSACTION_CREATED_EVENT,
          timestamp,
          value,
        });
      }
      // An entry in the history may be an array of more sub-entries.
    } else if (Array.isArray(base)) {
      const events = [];

      base.forEach((entry) => {
        const { op, path, value, timestamp: entryTimestamp } = entry;
        // Not all sub-entries in a history entry have a timestamp. If the sub-entry does not have a
        // timestamp, the first sub-entry in a history entry should.
        const timestamp = entryTimestamp || (base[0] && base[0].timestamp);

        const isAddBaseFee = path === ESTIMATE_BASE_FEE_PATH && op === 'add';

        if ((path in eventPathsHash && op === REPLACE_OP) || isAddBaseFee) {
          switch (path) {
            case STATUS_PATH: {
              const gasFee =
                cachedGasLimit === '0x0' && cachedGasPrice === '0x0'
                  ? getHexGasTotal({
                      gasLimit: paramsGasLimit,
                      gasPrice: paramsEip1559Price || paramsGasPrice,
                    })
                  : getHexGasTotal({
                      gasLimit: cachedGasLimit,
                      gasPrice: cachedGasPrice,
                    });

              if (value in statusHash) {
                let eventKey = statusHash[value];

                // If the status is 'submitted', we need to determine whether the event is a
                // transaction retry or a cancellation attempt.
                if (value === SUBMITTED_STATUS) {
                  if (type === TransactionType.retry) {
                    eventKey = TRANSACTION_RESUBMITTED_EVENT;
                  } else if (type === TransactionType.cancel) {
                    eventKey = TRANSACTION_CANCEL_ATTEMPTED_EVENT;
                  }
                } else if (value === CONFIRMED_STATUS) {
                  if (type === TransactionType.cancel) {
                    eventKey = TRANSACTION_CANCEL_SUCCESS_EVENT;
                  }
                }
                events.push({
                  id,
                  hash,
                  eventKey,
                  timestamp,
                  chainId,
                  metamaskNetworkId,
                  value: gasFee,
                });
              }
              break;
            }

            // If the gas price or gas limit has been changed, we update the gasFee of the
            // previously submitted event. These events happen when the gas limit and gas price is
            // changed at the confirm screen.
            case GAS_PRICE_PATH:
            case GAS_LIMIT_PATH:
            case ESTIMATE_BASE_FEE_PATH: {
              const lastEvent = events[events.length - 1] || {};
              const { lastEventKey } = lastEvent;

              if (path === GAS_LIMIT_PATH) {
                cachedGasLimit = value;
              } else if (path === GAS_PRICE_PATH) {
                cachedGasPrice = value;
              } else if (path === ESTIMATE_BASE_FEE_PATH) {
                cachedGasPrice = paramsEip1559Price || base?.txParams?.gasPrice;
                lastEvent.value = getHexGasTotal({
                  gasLimit: paramsGasLimit,
                  gasPrice: cachedGasPrice,
                });
              }

              if (
                lastEventKey === TRANSACTION_SUBMITTED_EVENT ||
                lastEventKey === TRANSACTION_RESUBMITTED_EVENT
              ) {
                lastEvent.value = getHexGasTotal({
                  gasLimit: cachedGasLimit,
                  gasPrice: cachedGasPrice,
                });
              }
              break;
            }

            case BLOCKTIMESTAMP: {
              const filteredAcc = acc.find(
                (ac) => ac.eventKey === TRANSACTION_CONFIRMED_EVENT,
              );
              if (filteredAcc !== undefined) {
                filteredAcc.timestamp = new Date(
                  parseInt(entry.value, 16) * 1000,
                ).getTime();
              }
              break;
            }

            default: {
              events.push({
                id,
                hash,
                chainId,
                metamaskNetworkId,
                eventKey: TRANSACTION_UPDATED_EVENT,
                timestamp,
              });
            }
          }
        }
      });

      return acc.concat(events);
    }

    return acc;
  }, []);

  // If txReceipt.status is '0x0', that means that an on-chain error occurred for the transaction,
  // so we add an error entry to the Activity Log.
  return status === '0x0'
    ? historyActivities.concat({
        id,
        hash,
        chainId,
        metamaskNetworkId,
        eventKey: TRANSACTION_ERRORED_EVENT,
      })
    : historyActivities;
}

/**
 * @description Removes "Transaction dropped" activities from a list of sorted activities if one of
 * the transactions has been confirmed. Typically, if multiple transactions have the same nonce,
 * once one transaction is confirmed, the rest are dropped. In this case, we don't want to show
 * multiple "Transaction dropped" activities, and instead want to show a single "Transaction
 * confirmed".
 * @param {Array} activities - List of sorted activities generated from the getActivities function.
 * @returns {Array}
 */
function filterSortedActivities(activities) {
  const filteredActivities = [];
  const hasConfirmedActivity = Boolean(
    activities.find(
      ({ eventKey }) =>
        eventKey === TRANSACTION_CONFIRMED_EVENT ||
        eventKey === TRANSACTION_CANCEL_SUCCESS_EVENT,
    ),
  );
  let addedDroppedActivity = false;

  activities.forEach((activity) => {
    if (activity.eventKey === TRANSACTION_DROPPED_EVENT) {
      if (!hasConfirmedActivity && !addedDroppedActivity) {
        filteredActivities.push(activity);
        addedDroppedActivity = true;
      }
    } else {
      filteredActivities.push(activity);
    }
  });

  return filteredActivities;
}

/**
 * Combines the histories of an array of transactions into a single array.
 *
 * @param {Array} transactions - Array of txMeta transaction objects.
 * @returns {Array}
 */
export function combineTransactionHistories(transactions = []) {
  if (!transactions.length) {
    return [];
  }

  const activities = [];

  transactions.forEach((transaction, index) => {
    // The first transaction should be the transaction with the earliest submittedTime. We show the
    // 'created' and 'submitted' activities here. All subsequent transactions will use 'resubmitted'
    // instead.
    const transactionActivities = getActivities(transaction, index === 0);
    activities.push(...transactionActivities);
  });

  const sortedActivities = activities.sort((a, b) => a.timestamp - b.timestamp);
  return filterSortedActivities(sortedActivities);
}