import BigNumber from 'bignumber.js';
import log from 'loglevel';
import { CHAIN_IDS } from '../constants/network';
import {
  GAS_API_BASE_URL,
  GAS_DEV_API_BASE_URL,
  SWAPS_API_V2_BASE_URL,
  SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
  SWAPS_CLIENT_ID,
  SWAPS_DEV_API_V2_BASE_URL,
  SWAPS_WRAPPED_TOKENS_ADDRESSES,
} from '../constants/swaps';
import { SECOND } from '../constants/time';
import { isValidHexAddress } from '../modules/hexstring-utils';
import { isEqualCaseInsensitive } from '../modules/string-utils';
import { addHexPrefix } from '../../app/scripts/lib/util';
import { decimalToHex } from '../modules/conversion.utils';
import fetchWithCache from './fetch-with-cache';

const TEST_CHAIN_IDS = [CHAIN_IDS.GOERLI, CHAIN_IDS.LOCALHOST];

const clientIdHeader = { 'X-Client-Id': SWAPS_CLIENT_ID };

export const validHex = (string) => Boolean(string?.match(/^0x[a-f0-9]+$/u));
export const truthyString = (string) => Boolean(string?.length);
export const truthyDigitString = (string) =>
  truthyString(string) && Boolean(string.match(/^\d+$/u));

export function validateData(validators, object, urlUsed, logError = true) {
  return validators.every(({ property, type, validator }) => {
    const types = type.split('|');

    const valid =
      types.some((_type) => typeof object[property] === _type) &&
      (!validator || validator(object[property]));
    if (!valid && logError) {
      log.error(
        `response to GET ${urlUsed} invalid for property ${property}; value was:`,
        object[property],
        '| type was: ',
        typeof object[property],
      );
    }
    return valid;
  });
}

export const QUOTE_VALIDATORS = [
  {
    property: 'trade',
    type: 'object',
    validator: (trade) =>
      trade &&
      validHex(trade.data) &&
      isValidHexAddress(trade.to, { allowNonPrefixed: false }) &&
      isValidHexAddress(trade.from, { allowNonPrefixed: false }) &&
      truthyString(trade.value),
  },
  {
    property: 'approvalNeeded',
    type: 'object',
    validator: (approvalTx) =>
      approvalTx === null ||
      (approvalTx &&
        validHex(approvalTx.data) &&
        isValidHexAddress(approvalTx.to, { allowNonPrefixed: false }) &&
        isValidHexAddress(approvalTx.from, { allowNonPrefixed: false })),
  },
  {
    property: 'sourceAmount',
    type: 'string',
    validator: truthyDigitString,
  },
  {
    property: 'destinationAmount',
    type: 'string',
    validator: truthyDigitString,
  },
  {
    property: 'sourceToken',
    type: 'string',
    validator: (input) => isValidHexAddress(input, { allowNonPrefixed: false }),
  },
  {
    property: 'destinationToken',
    type: 'string',
    validator: (input) => isValidHexAddress(input, { allowNonPrefixed: false }),
  },
  {
    property: 'aggregator',
    type: 'string',
    validator: truthyString,
  },
  {
    property: 'aggType',
    type: 'string',
    validator: truthyString,
  },
  {
    property: 'error',
    type: 'object',
    validator: (error) => error === null || typeof error === 'object',
  },
  {
    property: 'averageGas',
    type: 'number',
  },
  {
    property: 'maxGas',
    type: 'number',
  },
  {
    property: 'gasEstimate',
    type: 'number|undefined',
    validator: (gasEstimate) => gasEstimate === undefined || gasEstimate > 0,
  },
  {
    property: 'fee',
    type: 'number',
  },
];

/**
 * @param {string} type - Type of an API call, e.g. "tokens"
 * @param {string} chainId
 * @returns string
 */
const getBaseUrlForNewSwapsApi = (type, chainId) => {
  const useDevApis = process.env.SWAPS_USE_DEV_APIS;
  const v2ApiBaseUrl = useDevApis
    ? SWAPS_DEV_API_V2_BASE_URL
    : SWAPS_API_V2_BASE_URL;
  const gasApiBaseUrl = useDevApis ? GAS_DEV_API_BASE_URL : GAS_API_BASE_URL;
  const noNetworkSpecificTypes = ['refreshTime']; // These types don't need network info in the URL.
  if (noNetworkSpecificTypes.includes(type)) {
    return v2ApiBaseUrl;
  }
  const chainIdDecimal = chainId && parseInt(chainId, 16);
  const gasApiTypes = ['gasPrices'];
  if (gasApiTypes.includes(type)) {
    return `${gasApiBaseUrl}/networks/${chainIdDecimal}`; // Gas calculations are in its own repo.
  }
  return `${v2ApiBaseUrl}/networks/${chainIdDecimal}`;
};

export const getBaseApi = function (type, chainId) {
  const _chainId = TEST_CHAIN_IDS.includes(chainId)
    ? CHAIN_IDS.MAINNET
    : chainId;
  const baseUrl = getBaseUrlForNewSwapsApi(type, _chainId);
  if (!baseUrl) {
    throw new Error(`Swaps API calls are disabled for chainId: ${_chainId}`);
  }
  switch (type) {
    case 'trade':
      return `${baseUrl}/trades?`;
    case 'tokens':
      return `${baseUrl}/tokens`;
    case 'token':
      return `${baseUrl}/token`;
    case 'topAssets':
      return `${baseUrl}/topAssets`;
    case 'aggregatorMetadata':
      return `${baseUrl}/aggregatorMetadata`;
    case 'gasPrices':
      return `${baseUrl}/gasPrices`;
    case 'network':
      return baseUrl;
    default:
      throw new Error('getBaseApi requires an api call type');
  }
};

export function calcTokenValue(value, decimals) {
  const multiplier = Math.pow(10, Number(decimals || 0));
  return new BigNumber(String(value)).times(multiplier);
}

export const shouldEnableDirectWrapping = (
  chainId,
  sourceToken,
  destinationToken,
) => {
  if (!sourceToken || !destinationToken) {
    return false;
  }
  const wrappedToken = SWAPS_WRAPPED_TOKENS_ADDRESSES[chainId];
  const nativeToken = SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId]?.address;
  return (
    (isEqualCaseInsensitive(sourceToken, wrappedToken) &&
      isEqualCaseInsensitive(destinationToken, nativeToken)) ||
    (isEqualCaseInsensitive(sourceToken, nativeToken) &&
      isEqualCaseInsensitive(destinationToken, wrappedToken))
  );
};

/**
 * Given and object where all values are strings, returns the same object with all values
 * now prefixed with '0x'
 *
 * @param obj
 */
export function addHexPrefixToObjectValues(obj) {
  return Object.keys(obj).reduce((newObj, key) => {
    return { ...newObj, [key]: addHexPrefix(obj[key]) };
  }, {});
}

/**
 * Given the standard set of information about a transaction, returns a transaction properly formatted for
 * publishing via JSON RPC and web3
 *
 * @param {object} options
 * @param {boolean} [options.sendToken] - Indicates whether or not the transaciton is a token transaction
 * @param {string} options.data - A hex string containing the data to include in the transaction
 * @param {string} options.to - A hex address of the tx recipient address
 * @param options.amount
 * @param {string} options.from - A hex address of the tx sender address
 * @param {string} options.gas - A hex representation of the gas value for the transaction
 * @param {string} options.gasPrice - A hex representation of the gas price for the transaction
 * @returns {object} An object ready for submission to the blockchain, with all values appropriately hex prefixed
 */
export function constructTxParams({
  sendToken,
  data,
  to,
  amount,
  from,
  gas,
  gasPrice,
}) {
  const txParams = {
    data,
    from,
    value: '0',
    gas,
    gasPrice,
  };

  if (!sendToken) {
    txParams.value = amount;
    txParams.to = to;
  }
  return addHexPrefixToObjectValues(txParams);
}

export async function fetchTradesInfo(
  {
    slippage,
    sourceToken,
    sourceDecimals,
    destinationToken,
    value,
    fromAddress,
    exchangeList,
  },
  { chainId },
) {
  const urlParams = {
    destinationToken,
    sourceToken,
    sourceAmount: calcTokenValue(value, sourceDecimals).toString(10),
    slippage,
    timeout: SECOND * 10,
    walletAddress: fromAddress,
  };

  if (exchangeList) {
    urlParams.exchangeList = exchangeList;
  }
  if (shouldEnableDirectWrapping(chainId, sourceToken, destinationToken)) {
    urlParams.enableDirectWrapping = true;
  }

  const queryString = new URLSearchParams(urlParams).toString();
  const tradeURL = `${getBaseApi('trade', chainId)}${queryString}`;
  const tradesResponse = await fetchWithCache(
    tradeURL,
    { method: 'GET', headers: clientIdHeader },
    { cacheRefreshTime: 0, timeout: SECOND * 15 },
  );
  const newQuotes = tradesResponse.reduce((aggIdTradeMap, quote) => {
    if (
      quote.trade &&
      !quote.error &&
      validateData(QUOTE_VALIDATORS, quote, tradeURL)
    ) {
      const constructedTrade = constructTxParams({
        to: quote.trade.to,
        from: quote.trade.from,
        data: quote.trade.data,
        amount: decimalToHex(quote.trade.value),
        gas: decimalToHex(quote.maxGas),
      });

      let { approvalNeeded } = quote;

      if (approvalNeeded) {
        approvalNeeded = constructTxParams({
          ...approvalNeeded,
        });
      }

      return {
        ...aggIdTradeMap,
        [quote.aggregator]: {
          ...quote,
          slippage,
          trade: constructedTrade,
          approvalNeeded,
        },
      };
    }
    return aggIdTradeMap;
  }, {});

  return newQuotes;
}