// @flow
import React from 'react';
import Button from 'component/button';
import { FormField, Form } from 'component/common/form';
import { Lbryio } from 'lbryinc';
import Card from 'component/common/card';
import LbcSymbol from 'component/common/lbc-symbol';
import Spinner from 'component/spinner';
import Nag from 'component/common/nag';
import CopyableText from 'component/copyableText';
import Icon from 'component/common/icon';
import QRCode from 'component/common/qr-code';
import usePersistedState from 'effects/use-persisted-state';
import * as ICONS from 'constants/icons';
import * as MODALS from 'constants/modal_types';
import * as PAGES from 'constants/pages';
import { clipboard } from 'electron';
import I18nMessage from 'component/i18nMessage';
import { Redirect, useHistory } from 'react-router';

const ENABLE_ALTERNATIVE_COINS = true;

const BTC_SATOSHIS = 100000000;
const LBC_MAX = 21000000;
const LBC_MIN = 1;

const IS_DEV = process.env.NODE_ENV !== 'production';
const DEBOUNCE_BTC_CHANGE_MS = 400;

const INTERNAL_APIS_DOWN = 'internal_apis_down';
const BTC_API_STATUS_PENDING = 'NEW'; // Started swap, waiting for coin.
const BTC_API_STATUS_CONFIRMING = 'PENDING'; // Coin receiving, waiting confirmation.
const BTC_API_STATUS_PROCESSING = 'COMPLETED'; // Coin confirmed. Sending LBC.
const BTC_API_STATUS_UNRESOLVED = 'UNRESOLVED'; // Underpaid, overpaid, etc.
const BTC_API_STATUS_EXPIRED = 'EXPIRED'; // Charge expired (60 minutes).
const BTC_API_STATUS_ERROR = 'Error';

const ACTION_MAIN = 'action_main';
const ACTION_STATUS_PENDING = 'action_pending';
const ACTION_STATUS_CONFIRMING = 'action_confirming';
const ACTION_STATUS_PROCESSING = 'action_processing';
const ACTION_STATUS_SUCCESS = 'action_success';
const ACTION_PAST_SWAPS = 'action_past_swaps';

const NAG_API_STATUS_PENDING = 'Waiting to receive your crypto.';
const NAG_API_STATUS_CONFIRMING = 'Confirming transaction.';
const NAG_API_STATUS_PROCESSING = 'Crypto received. Sending your Credits.';
const NAG_API_STATUS_SUCCESS = 'Credits sent. You should see it in your wallet.';
const NAG_API_STATUS_ERROR = 'An error occurred on the previous swap.';
const NAG_SWAP_CALL_FAILED = 'Failed to initiate swap.';
// const NAG_STATUS_CALL_FAILED = 'Failed to query swap status.';
const NAG_SERVER_DOWN = 'The system is currently down. Come back later.';
const NAG_RATE_CALL_FAILED = 'Unable to obtain exchange rate. Try again later.';
const NAG_EXPIRED = 'Swap expired.';

type Props = {
  receiveAddress: string,
  coinSwaps: Array<CoinSwapInfo>,
  isAuthenticated: boolean,
  doToast: ({ message: string }) => void,
  addCoinSwap: (CoinSwapInfo) => void,
  getNewAddress: () => void,
  checkAddressIsMine: (string) => void,
  openModal: (string, {}) => void,
  queryCoinSwapStatus: (string) => void,
};

function WalletSwap(props: Props) {
  const {
    receiveAddress,
    doToast,
    coinSwaps,
    isAuthenticated,
    addCoinSwap,
    getNewAddress,
    checkAddressIsMine,
    openModal,
    queryCoinSwapStatus,
  } = props;

  const [btc, setBtc] = React.useState(0);
  const [lbcError, setLbcError] = React.useState();
  const [lbc, setLbc] = usePersistedState('swap-desired-lbc', LBC_MIN);
  const [action, setAction] = React.useState(ACTION_MAIN);
  const [nag, setNag] = React.useState(null);
  const [showQr, setShowQr] = React.useState(false);
  const [isFetchingRate, setIsFetchingRate] = React.useState(false);
  const [isSwapping, setIsSwapping] = React.useState(false);
  const [isRefreshingStatus, setIsRefreshingStatus] = React.useState(false);
  const { location } = useHistory();
  const [swap, setSwap] = React.useState({});
  const [coin, setCoin] = React.useState('bitcoin');
  const [lastStatusQuery, setLastStatusQuery] = React.useState();
  const { goBack } = useHistory();

  function formatCoinAmountString(amount) {
    return amount === 0 ? '---' : amount.toLocaleString(undefined, { minimumFractionDigits: 8 });
  }

  function returnToMainAction() {
    setIsSwapping(false);
    setAction(ACTION_MAIN);
    setSwap(null);
  }

  function removeCoinSwap(chargeCode) {
    openModal(MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS, {
      chargeCode: chargeCode,
    });
  }

  // Ensure 'receiveAddress' is populated
  React.useEffect(() => {
    if (!receiveAddress) {
      getNewAddress();
    } else {
      checkAddressIsMine(receiveAddress);
    }
  }, [receiveAddress, getNewAddress, checkAddressIsMine]);

  // Get 'btc/rate' and calculate required BTC.
  React.useEffect(() => {
    if (isNaN(lbc) || lbc === 0) {
      setBtc(0);
      return;
    }

    setIsFetchingRate(true);

    const timer = setTimeout(() => {
      Lbryio.call('btc', 'rate', { satoshi: BTC_SATOSHIS })
        .then((rate) => {
          setIsFetchingRate(false);
          setBtc((lbc * Math.round(BTC_SATOSHIS * rate)) / BTC_SATOSHIS);
        })
        .catch(() => {
          setIsFetchingRate(false);
          setBtc(0);
          setNag({ msg: NAG_RATE_CALL_FAILED, type: 'error' });
        });
    }, DEBOUNCE_BTC_CHANGE_MS);

    return () => clearTimeout(timer);
  }, [lbc]);

  // Resolve 'swap' with the latest info from 'coinSwaps'
  React.useEffect(() => {
    const swapInfo = swap && coinSwaps.find((x) => x.chargeCode === swap.chargeCode);
    if (!swapInfo) {
      return;
    }

    const jsonSwap = JSON.stringify(swap);
    const jsonSwapInfo = JSON.stringify(swapInfo);
    if (jsonSwap !== jsonSwapInfo) {
      setSwap({ ...swapInfo });
    }

    if (!swapInfo.status) {
      return;
    }

    switch (swapInfo.status.status) {
      case BTC_API_STATUS_PENDING:
        setAction(ACTION_STATUS_PENDING);
        setNag({ msg: NAG_API_STATUS_PENDING, type: 'helpful' });
        break;
      case BTC_API_STATUS_CONFIRMING:
        setAction(ACTION_STATUS_CONFIRMING);
        setNag({ msg: NAG_API_STATUS_CONFIRMING, type: 'helpful' });
        break;
      case BTC_API_STATUS_PROCESSING:
        if (swapInfo.status.lbcTxid) {
          setAction(ACTION_STATUS_SUCCESS);
          setNag({ msg: NAG_API_STATUS_SUCCESS, type: 'helpful' });
          setIsSwapping(false);
        } else {
          setAction(ACTION_STATUS_PROCESSING);
          setNag({ msg: NAG_API_STATUS_PROCESSING, type: 'helpful' });
        }
        break;
      case BTC_API_STATUS_ERROR:
        setNag({ msg: NAG_API_STATUS_ERROR, type: 'error' });
        break;
      case INTERNAL_APIS_DOWN:
        setNag({ msg: NAG_SERVER_DOWN, type: 'error' });
        break;
      case BTC_API_STATUS_EXPIRED:
        setNag({ msg: NAG_EXPIRED, type: 'error' });
        if (action === ACTION_PAST_SWAPS) {
          setAction(ACTION_STATUS_PENDING);
        }
        break;
      case BTC_API_STATUS_UNRESOLVED:
        setNag({
          msg: __(
            'Received amount did not match order code %chargeCode%. Contact hello@lbry.com to resolve the payment.',
            { chargeCode: swapInfo.chargeCode }
          ),
          type: 'error',
        });
        if (action === ACTION_PAST_SWAPS) {
          setAction(ACTION_STATUS_PENDING);
        }
        break;
      default:
        setNag({ msg: swapInfo.status.status, type: 'error' });
        break;
    }
  }, [swap, coinSwaps]);

  // Validate entered LBC
  React.useEffect(() => {
    let msg;
    if (lbc < LBC_MIN) {
      msg = __('The amount needs to be higher');
    } else if (lbc > LBC_MAX) {
      msg = __('The amount is too high');
    }
    setLbcError(msg);
  }, [lbc]);

  // 'Refresh' button feedback
  React.useEffect(() => {
    let timer;
    if (isRefreshingStatus) {
      timer = setTimeout(() => {
        setIsRefreshingStatus(false);
      }, 1000);
    }

    return () => clearTimeout(timer);
  }, [isRefreshingStatus]);

  function getCoinAddress(coin) {
    if (swap && swap.sendAddresses) {
      return swap.sendAddresses[coin];
    }
    return '';
  }

  function getCoinSendAmountStr(coin) {
    if (swap && swap.sendAmounts && swap.sendAmounts[coin]) {
      return `${swap.sendAmounts[coin].amount} ${swap.sendAmounts[coin].currency}`;
    }
    return '';
  }

  function currencyToCoin(currency) {
    const MAP = {
      DAI: 'dai',
      USDC: 'usdc',
      BTC: 'bitcoin',
      ETH: 'ethereum',
      LTC: 'litecoin',
      BCH: 'bitcoincash',
    };
    return MAP[currency] || 'bitcoin';
  }

  function getSentAmountStr(swapInfo) {
    if (swapInfo && swapInfo.status) {
      const currency = swapInfo.status.receiptCurrency;
      const coin = currencyToCoin(currency);
      return getCoinSendAmountStr(coin);
    }
    return '';
  }

  function getCoinLabel(coin) {
    const COIN_LABEL = {
      dai: 'Dai',
      usdc: 'USD Coin',
      bitcoin: 'Bitcoin',
      ethereum: 'Ethereum',
      litecoin: 'Litecoin',
      bitcoincash: 'Bitcoin Cash',
    };

    return COIN_LABEL[coin] || coin;
  }

  function getLbcAmountStrForSwap(swap) {
    if (swap && swap.lbcAmount) {
      return formatCoinAmountString(swap.lbcAmount);
    }
    return '---';
  }

  function handleStartSwap() {
    setIsSwapping(true);
    setSwap(null);
    setNag(null);

    Lbryio.call('btc', 'swap', {
      lbc_satoshi_requested: parseInt(lbc * BTC_SATOSHIS + 0.5),
      btc_satoshi_provided: parseInt(btc * BTC_SATOSHIS + 0.5),
      pay_to_wallet_address: receiveAddress,
    })
      .then((response) => {
        const btcAmount = response.Charge.data.pricing['bitcoin'].amount;
        const rate = response.Exchange.rate;

        const timeline = response.Charge.data.timeline;
        const lastTimeline = timeline[timeline.length - 1];

        const newSwap = {
          chargeCode: response.Exchange.charge_code,
          coins: Object.keys(response.Charge.data.addresses),
          sendAddresses: response.Charge.data.addresses,
          sendAmounts: response.Charge.data.pricing,
          lbcAmount: (btcAmount * BTC_SATOSHIS) / rate,
          status: {
            status: lastTimeline.status,
            receiptCurrency: lastTimeline.payment.value.currency,
            receiptTxid: lastTimeline.payment.transaction_id,
            lbcTxid: response.Exchange.lbc_txid || '',
          },
        };

        setSwap({ ...newSwap });
        addCoinSwap({ ...newSwap });
      })
      .catch((err) => {
        const translateError = (err) => {
          // TODO: https://github.com/lbryio/lbry.go/issues/87
          // Translate error codes instead of strings when it is available.
          if (err === 'users are currently limited to 4 transactions per month') {
            return __('Users are currently limited to 4 completed swaps per month or 5 pending swaps.');
          }
          return err;
        };
        setNag({ msg: err === INTERNAL_APIS_DOWN ? NAG_SWAP_CALL_FAILED : translateError(err.message), type: 'error' });
        returnToMainAction();
      });
  }

  function handleViewPastSwaps() {
    setAction(ACTION_PAST_SWAPS);
    setNag(null);
    setIsRefreshingStatus(true);

    const now = Date.now();
    if (!lastStatusQuery || now - lastStatusQuery > 30000) {
      // There is a '200/minute' limit in the commerce API. If the history is
      // long, or if the user goes trigger-happy, the limit could be reached
      // easily. Statuses don't change often, so just limit it to every 30s.
      setLastStatusQuery(now);
      coinSwaps.forEach((x) => {
        queryCoinSwapStatus(x.chargeCode);
      });
    }
  }

  function getShortStatusStr(coinSwap: CoinSwapInfo) {
    const swapInfo = coinSwaps.find((x) => x.chargeCode === coinSwap.chargeCode);
    if (!swapInfo || !swapInfo.status) {
      return '---';
    }

    let msg;
    switch (swapInfo.status.status) {
      case BTC_API_STATUS_PENDING:
        msg = __('Waiting');
        break;
      case BTC_API_STATUS_CONFIRMING:
        msg = __('Confirming');
        break;
      case BTC_API_STATUS_PROCESSING:
        if (swapInfo.status.lbcTxid) {
          msg = __('Credits sent');
        } else {
          msg = __('Sending Credits');
        }
        break;
      case BTC_API_STATUS_ERROR:
        msg = __('Failed');
        break;
      case BTC_API_STATUS_EXPIRED:
        msg = __('Expired');
        break;
      case BTC_API_STATUS_UNRESOLVED:
        msg = __('Unresolved');
        break;
      default:
        msg = swapInfo.status.status;
        // if (IS_DEV) throw new Error('Unhandled "status": ' + status.Status);
        break;
    }
    return msg;
  }

  function getViewTransactionElement(swap, isSend) {
    if (!swap || !swap.status) {
      return '';
    }

    const explorerUrl = (coin, txid) => {
      // It's unclear whether we can link to sites like blockchain.com.
      // Don't do it for now.
      return '';
    };

    if (isSend) {
      const sendTxId = swap.status.receiptTxid;
      const url = explorerUrl(swap.status.receiptCurrency, sendTxId);
      return sendTxId ? (
        <>
          {url && <Button button="link" href={url} label={__('View transaction')} />}
          {!url && (
            <Button
              button="link"
              label={__('Copy transaction ID')}
              title={sendTxId}
              onClick={() => {
                clipboard.writeText(sendTxId);
                doToast({
                  message: __('Transaction ID copied.'),
                });
              }}
            />
          )}
        </>
      ) : null;
    } else {
      const lbcTxId = swap.status.lbcTxid;
      return lbcTxId ? (
        <Button button="link" href={`https://explorer.lbry.com/tx/${lbcTxId}`} label={__('View transaction')} />
      ) : null;
    }
  }

  function getCloseButton() {
    return (
      <>
        <Button autoFocus button="primary" label={__('Close')} onClick={() => goBack()} />
        <Icon
          className="icon--help"
          icon={ICONS.HELP}
          tooltip
          size={16}
          customTooltipText={__(
            'This page can be closed while the transactions are in progress.\nYou can view the status later from:\n • Wallet » Swap » View Past Swaps'
          )}
        />
      </>
    );
  }

  function getGap() {
    return <div className="confirm__value" />; // better way?
  }

  function getActionElement() {
    switch (action) {
      case ACTION_MAIN:
        return actionMain;

      case ACTION_STATUS_PENDING:
        return actionPending;

      case ACTION_STATUS_CONFIRMING:
        return actionConfirmingSend;

      case ACTION_STATUS_PROCESSING: // fall-through
      case ACTION_STATUS_SUCCESS:
        return actionProcessingAndSuccess;

      case ACTION_PAST_SWAPS:
        return actionPastSwaps;

      default:
        if (IS_DEV) throw new Error('Unhandled action: ' + action);
        return actionMain;
    }
  }

  const actionMain = (
    <>
      <div className="section section--padded card--inline confirm__wrapper">
        <div className="section">
          <FormField
            autoFocus
            label={
              <I18nMessage
                tokens={{
                  lbc: <LbcSymbol size={22} />,
                }}
              >
                Enter desired %lbc%
              </I18nMessage>
            }
            type="number"
            name="lbc"
            className="form-field--price-amount--auto"
            affixClass="form-field--fix-no-height"
            max={LBC_MAX}
            min={LBC_MIN}
            step={1 / BTC_SATOSHIS}
            placeholder="12.34"
            value={lbc}
            error={lbcError}
            disabled={isSwapping}
            onChange={(event) => setLbc(parseFloat(event.target.value))}
          />
          {getGap()}
          <div className="confirm__label">{__('Estimated BTC price')}</div>
          <div className="confirm__value">
            {formatCoinAmountString(btc)} {btc === 0 ? '' : 'BTC'}
            {isFetchingRate && <Spinner type="small" />}
          </div>
        </div>
      </div>
      <div className="section__actions">
        <Button
          autoFocus
          onClick={handleStartSwap}
          button="primary"
          disabled={isSwapping || isNaN(btc) || btc === 0 || lbc === 0 || lbcError}
          label={isSwapping ? __('Processing...') : __('Start Swap')}
        />
        {!isSwapping && coinSwaps.length !== 0 && (
          <Button button="link" label={__('View Past Swaps')} onClick={handleViewPastSwaps} />
        )}
        {isSwapping && <Spinner type="small" />}
      </div>
    </>
  );

  const actionPending = (
    <>
      <div className="section section--padded card--inline confirm__wrapper">
        <div className="section">
          {swap && swap.coins && ENABLE_ALTERNATIVE_COINS && (
            <>
              <FormField
                type="select"
                name="select_coin"
                value={coin}
                label={__('Alternative coins')}
                onChange={(e) => setCoin(e.target.value)}
              >
                {swap.coins.map((x) => (
                  <option key={x} value={x}>
                    {getCoinLabel(x)}
                  </option>
                ))}
              </FormField>
              {getGap()}
            </>
          )}
          <div className="confirm__label">{__('Send')}</div>
          <CopyableText
            primaryButton
            copyable={getCoinSendAmountStr(coin)}
            snackMessage={__('Amount copied.')}
            onCopy={(inputElem) => {
              const inputStr = inputElem.value;
              const selectEndIndex = inputStr.lastIndexOf(' ');
              if (selectEndIndex > -1 && inputStr.substring(0, selectEndIndex).match(/[\d.]/)) {
                inputElem.setSelectionRange(0, selectEndIndex, 'forward');
              }
            }}
          />
          <div className="help">{__('Use the copy button to ensure the EXACT amount is sent!')}</div>
          {getGap()}
          <div className="confirm__label">{__('To')}</div>
          <CopyableText primaryButton copyable={getCoinAddress(coin)} snackMessage={__('Address copied.')} />
          <div className="confirm__value--subitem">
            <Button
              button="link"
              label={showQr ? __('Hide QR code') : __('Show QR code')}
              onClick={() => setShowQr(!showQr)}
            />
            {showQr && getCoinAddress(coin) && <QRCode value={getCoinAddress(coin)} />}
          </div>
          {getGap()}
          <div className="confirm__label">{__('Receive')}</div>
          <div className="confirm__value">{<LbcSymbol postfix={getLbcAmountStrForSwap(swap)} size={22} />}</div>
        </div>
      </div>
      <div className="section__actions">{getCloseButton()}</div>
    </>
  );

  const actionConfirmingSend = (
    <>
      <div className="section section--padded card--inline confirm__wrapper">
        <div className="section">
          <div className="confirm__label">{__('Confirming')}</div>
          <div className="confirm__value confirm__value--no-gap">{getSentAmountStr(swap)}</div>
          <div className="confirm__value--subitem">{getViewTransactionElement(swap, true)}</div>
        </div>
      </div>
      <div className="section__actions">{getCloseButton()}</div>
    </>
  );

  const actionProcessingAndSuccess = (
    <>
      <div className="section section--padded card--inline confirm__wrapper">
        <div className="section">
          <div className="confirm__label">{__('Sent')}</div>
          <div className="confirm__value confirm__value--no-gap">{getSentAmountStr(swap)}</div>
          <div className="confirm__value--subitem">{getViewTransactionElement(swap, true)}</div>
          {getGap()}
          <div className="confirm__label">{action === ACTION_STATUS_SUCCESS ? __('Received') : __('Receiving')}</div>
          <div className="confirm__value confirm__value--no-gap">
            {<LbcSymbol postfix={getLbcAmountStrForSwap(swap)} size={22} />}
          </div>
          {action === ACTION_STATUS_SUCCESS && (
            <div className="confirm__value--subitem">{getViewTransactionElement(swap, false)}</div>
          )}
        </div>
      </div>
      <div className="section__actions">{getCloseButton()}</div>
    </>
  );

  const actionPastSwaps = (
    <>
      <div className="section section--padded card--inline confirm__wrapper">
        <div className="section">
          <div className="table__wrapper">
            <table className="table table--btc-swap">
              <thead>
                <tr>
                  <th>{__('Code')}</th>
                  <th>{__('Status')}</th>
                  <th />
                </tr>
              </thead>
              <tbody>
                {coinSwaps.length === 0 && (
                  <tr>
                    <td>{'---'}</td>
                  </tr>
                )}
                {coinSwaps.length !== 0 &&
                  coinSwaps.map((x) => {
                    return (
                      <tr key={x.chargeCode}>
                        <td>
                          <Button
                            button="link"
                            className="button--hash-id"
                            title={x.chargeCode}
                            label={x.chargeCode}
                            onClick={() => {
                              setSwap({ ...x });
                            }}
                          />
                        </td>
                        <td>{isRefreshingStatus ? '...' : getShortStatusStr(x)}</td>
                        <td>
                          <Button
                            button="link"
                            icon={ICONS.REMOVE}
                            title={__('Remove swap')}
                            onClick={() => removeCoinSwap(x.chargeCode)}
                          />
                        </td>
                      </tr>
                    );
                  })}
              </tbody>
            </table>
          </div>
        </div>
      </div>
      <div className="section__actions">
        <Button
          autoFocus
          button="primary"
          label={__('Go Back')}
          onClick={() => {
            returnToMainAction();
            setNag(null);
          }}
        />
        {coinSwaps.length !== 0 && !isRefreshingStatus && (
          <Button button="link" label={__('Refresh')} onClick={handleViewPastSwaps} />
        )}
        {isRefreshingStatus && <Spinner type="small" />}
      </div>
    </>
  );

  if (!isAuthenticated) {
    return <Redirect to={`/$/${PAGES.AUTH_SIGNIN}?redirect=${location.pathname}`} />;
  }

  return (
    <Form onSubmit={handleStartSwap}>
      <Card
        title={<I18nMessage tokens={{ lbc: <LbcSymbol size={22} /> }}>Swap Crypto for %lbc%</I18nMessage>}
        subtitle={__(
          'Send crypto to the address provided and you will be sent an equivalent amount of Credits. You can pay with BCH, LTC, ETH, USDC or DAI after starting the swap.'
        )}
        actions={getActionElement()}
        nag={nag ? <Nag relative type={nag.type} message={__(nag.msg)} /> : null}
      />
    </Form>
  );
}

export default WalletSwap;
