import { BigNumber } from "bignumber.js";
import { useEffect, useReducer, useCallback, useRef } from "react";
import { log } from "@ledgerhq/logs";
import { getAccountBridge } from ".";
import { getMainAccount } from "../account";
import { delay } from "../promise";
import type { Account, AccountBridge, AccountLike } from "@ledgerhq/types-live";
import type { Transaction, TransactionStatus } from "../generated/types";
import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
import { LiveConfig } from "@ledgerhq/live-config/LiveConfig";

export type State<T extends Transaction = Transaction> = {
  account: AccountLike | null | undefined;
  parentAccount: Account | null | undefined;
  transaction: T | null | undefined;
  status: TransactionStatus;
  statusOnTransaction: T | null | undefined;
  errorAccount: Error | null | undefined;
  errorStatus: Error | null | undefined;
  syncing: boolean;
  synced: boolean;
};

export type Result<T extends Transaction = Transaction> = {
  transaction: T | null | undefined;
  setTransaction: (arg0: T) => void;
  updateTransaction: (updater: (arg0: T) => T) => void;
  account: AccountLike | null | undefined;
  parentAccount: Account | null | undefined;
  setAccount: (arg0: AccountLike, arg1: Account | null | undefined) => void;
  status: TransactionStatus;
  bridgeError: Error | null | undefined;
  bridgePending: boolean;
};

type Actions<T extends Transaction = Transaction> =
  | {
      type: "setAccount";
      account: AccountLike;
      parentAccount: Account | null | undefined;
    }
  | {
      type: "setTransaction";
      transaction: T;
    }
  | {
      type: "updateTransaction";
      updater: (transaction: T) => T;
    }
  | {
      type: "onStatus";
      status: TransactionStatus;
      transaction: T;
    }
  | {
      type: "onStatusError";
      error: Error;
    }
  | {
      type: "setTransaction";
      transaction: T;
    }
  | {
      type: "onStartSync";
    }
  | {
      type: "onSync";
    };

type Reducer<T extends Transaction = Transaction> = (
  state: State<T>,
  action: Actions<T>,
) => State<T>;

const initial: State<Transaction> = {
  account: null,
  parentAccount: null,
  transaction: null,
  status: {
    errors: {},
    warnings: {},
    estimatedFees: new BigNumber(0),
    amount: new BigNumber(0),
    totalSpent: new BigNumber(0),
  },
  statusOnTransaction: null,
  errorAccount: null,
  errorStatus: null,
  syncing: false,
  synced: false,
};

export const shouldSyncBeforeTx = (currency: CryptoCurrency): boolean => {
  const currencyConfig = LiveConfig.getValueByKey(`config_currency_${currency.id}`);
  const sharedConfig = LiveConfig.getValueByKey("config_currency");
  if (currencyConfig && "syncBeforeTx" in currencyConfig) {
    return currencyConfig.syncBeforeTx === true;
  } else {
    return sharedConfig && "syncBeforeTx" in sharedConfig && sharedConfig.syncBeforeTx === true;
  }
};

const makeInit =
  <T extends Transaction = Transaction>(
    optionalInit: (() => Partial<State<T>>) | null | undefined,
  ) =>
  (): State<T> => {
    let s = initial as State<T>;

    if (optionalInit) {
      const patch = optionalInit();
      const { account, parentAccount, transaction } = patch;

      if (account) {
        s = (reducer as Reducer<T>)(s, {
          type: "setAccount",
          account,
          parentAccount,
        });
      }

      if (transaction) {
        s = (reducer as Reducer<T>)(s, {
          type: "setTransaction",
          transaction,
        });
      }
    }

    return s;
  };

const reducer = <T extends Transaction = Transaction>(
  state: State<T>,
  action: Actions<T>,
): State<T> => {
  switch (action.type) {
    case "setAccount": {
      const { account, parentAccount } = action;

      try {
        const mainAccount = getMainAccount(account, parentAccount);
        const bridge = getAccountBridge(account, parentAccount) as AccountBridge<T>;
        const subAccountId = account.type !== "Account" && account.id;
        let t = bridge.createTransaction(mainAccount);
        if (
          // @ts-expect-error transaction.mode is not available on all union types. type guard is required
          state.transaction?.mode &&
          // @ts-expect-error transaction.mode is not available on all union types. type guard is required
          state.transaction.mode !== t.mode
        ) {
          t = bridge.updateTransaction(t, {
            // @ts-expect-error transaction.mode is not available on all union types. type guard is required
            mode: state.transaction.mode,
          });
        }

        if (subAccountId) {
          t = { ...t, subAccountId };
        }

        return {
          ...initial,
          account,
          parentAccount,
          transaction: t,
          syncing: false,
          synced: false,
        } as State<T>;
      } catch (e: any) {
        return {
          ...initial,
          account,
          parentAccount,
          errorAccount: e,
          syncing: false,
          synced: false,
        } as State<T>;
      }
    }

    case "setTransaction":
      if (state.transaction === action.transaction) return state;
      return { ...state, transaction: action.transaction };

    case "updateTransaction": {
      if (!state.transaction) return state;
      const transaction = action.updater(state.transaction);
      if (state.transaction === transaction) return state;
      return { ...state, transaction };
    }

    case "onStatus":
      return {
        ...state,
        errorStatus: null,
        transaction: action.transaction,
        status: action.status,
        statusOnTransaction: action.transaction,
      };

    case "onStatusError":
      if (action.error === state.errorStatus) return state;
      return { ...state, errorStatus: action.error };

    case "onStartSync":
      return { ...state, syncing: true, synced: false };

    case "onSync":
      return { ...state, syncing: false, synced: true };

    default:
      return state;
  }
};

const INITIAL_ERROR_RETRY_DELAY = 1000;
const ERROR_RETRY_DELAY_MULTIPLIER = 1.5;
const DEBOUNCE_STATUS_DELAY = 300;

const useBridgeTransaction = <T extends Transaction = Transaction>(
  optionalInit?: (() => Partial<State<T>>) | null | undefined,
): Result<T> => {
  const [
    {
      account,
      parentAccount,
      transaction,
      status,
      statusOnTransaction,
      syncing,
      synced,
      errorAccount,
      errorStatus,
    },
    dispatch,
  ] = useReducer(reducer as Reducer<T>, undefined, makeInit<T>(optionalInit));
  const setAccount = useCallback(
    (account, parentAccount) =>
      dispatch({
        type: "setAccount",
        account,
        parentAccount,
      }),
    [dispatch],
  );

  const setTransaction = useCallback(
    transaction =>
      dispatch({
        type: "setTransaction",
        transaction,
      }),
    [dispatch],
  );
  const updateTransaction = useCallback(
    updater =>
      dispatch({
        type: "updateTransaction",
        updater,
      }),
    [dispatch],
  );

  const mainAccount = account ? getMainAccount(account, parentAccount) : null;
  const errorDelay = useRef(INITIAL_ERROR_RETRY_DELAY);
  const statusIsPending = useRef(false); // Stores if status already being processed
  const shouldSync = mainAccount && shouldSyncBeforeTx(mainAccount.currency);

  useEffect(() => {
    if (mainAccount === null || synced || syncing) return;

    if (!shouldSync) return; // skip sync if not required by currency config

    dispatch({ type: "onStartSync" });
    const bridge = getAccountBridge(mainAccount, null);
    const sub = bridge.sync(mainAccount, { paginationConfig: {} }).subscribe({
      error: (_: Error) => {
        // we do not block the user in case of error for now but it should be the case
        dispatch({ type: "onSync" });
      },
      complete: () => {
        dispatch({ type: "onSync" });
      },
    });

    return () => {
      sub.unsubscribe();
    };
  }, [mainAccount, synced, syncing, shouldSync]);

  const bridgePending = transaction !== statusOnTransaction;

  // when transaction changes, prepare the transaction
  useEffect(() => {
    let ignore = false;
    let errorTimeout: NodeJS.Timeout | null;
    // If bridge is not pending, transaction change is due to
    // the last onStatus dispatch (prepareTransaction changed original transaction) and must be ignored
    if (!bridgePending && !synced) return;

    if (mainAccount && transaction) {
      // We don't debounce first status refresh, but any subsequent to avoid multiple calls
      // First call is immediate
      const debounce = statusIsPending.current ? delay(DEBOUNCE_STATUS_DELAY) : null;
      statusIsPending.current = true; // consider pending until status is resolved (error or success)

      Promise.resolve(debounce)
        .then(() => getAccountBridge(mainAccount, null))
        .then(async bridge => {
          if (ignore) return;
          const preparedTransaction = await bridge.prepareTransaction(mainAccount, transaction);
          if (ignore) return;
          const status = await bridge.getTransactionStatus(mainAccount, preparedTransaction);
          if (ignore) return;
          return {
            preparedTransaction,
            status,
          };
        })
        .then(
          result => {
            if (ignore || !result) return;
            const { preparedTransaction, status } = result;
            errorDelay.current = INITIAL_ERROR_RETRY_DELAY; // reset delay

            statusIsPending.current = false; // status is now synced with transaction

            dispatch({
              type: "onStatus",
              status,
              transaction: preparedTransaction,
            });
          },
          e => {
            if (ignore) return;
            statusIsPending.current = false;
            dispatch({
              type: "onStatusError",
              error: e,
            });
            log("useBridgeTransaction", "prepareTransaction failed " + String(e));
            // After X seconds of hanging in this error case, we try again
            log("useBridgeTransaction", "retrying prepareTransaction...");
            errorTimeout = setTimeout(() => {
              // $FlowFixMe (mobile)
              errorDelay.current *= ERROR_RETRY_DELAY_MULTIPLIER; // increase delay

              // $FlowFixMe
              const transactionCopy = {
                ...transaction,
              };
              dispatch({
                type: "setTransaction",
                transaction: transactionCopy,
              }); // $FlowFixMe (mobile)
            }, errorDelay.current);
          },
        );
    }

    return () => {
      ignore = true;

      if (errorTimeout) {
        clearTimeout(errorTimeout);
        errorTimeout = null;
      }
    };
  }, [transaction, mainAccount, bridgePending, dispatch, synced]);

  const bridgeError = errorAccount || errorStatus;

  useEffect(() => {
    if (bridgeError && globalOnBridgeError) {
      globalOnBridgeError(bridgeError);
    }
  }, [bridgeError]);

  return {
    transaction,
    setTransaction,
    updateTransaction,
    status,
    account,
    parentAccount,
    setAccount,
    bridgeError,
    bridgePending: bridgePending && (shouldSync ? !synced : true),
  };
};

type GlobalBridgeErrorFn = null | ((error: any) => void);

let globalOnBridgeError: GlobalBridgeErrorFn = null;

// allows to globally set a bridge error catch function in order to log it / report to sentry / ...
export function setGlobalOnBridgeError(f: GlobalBridgeErrorFn): void {
  globalOnBridgeError = f;
}
export function getGlobalOnBridgeError(): GlobalBridgeErrorFn {
  return globalOnBridgeError;
}

export default useBridgeTransaction;
