/* eslint-disable no-console */
import {
  getMainAccount,
  getParentAccount,
  makeEmptyTokenAccount,
} from "@ledgerhq/ledger-wallet-framework/account/index";
import { getCryptoAssetsStore } from "@ledgerhq/cryptoassets/state";
import { decodeSwapPayload } from "@ledgerhq/hw-app-exchange";
import { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets";
import { Account, AccountLike, getCurrencyForAccount, TokenAccount } from "@ledgerhq/types-live";
import {
  createAccountNotFound,
  createCurrencyNotFound,
  createUnknownError,
  deserializeTransaction,
  ServerError,
} from "@ledgerhq/wallet-api-core";
import {
  ExchangeCompleteParams,
  ExchangeCompleteResult,
  ExchangeStartParams,
  ExchangeStartResult,
  ExchangeStartSellParams,
  ExchangeStartSwapParams,
  ExchangeStartFundParams,
  ExchangeSwapParams,
  ExchangeType,
  SwapLiveError,
  SwapResult,
} from "@ledgerhq/wallet-api-exchange-module";
import { customWrapper, RPCHandler } from "@ledgerhq/wallet-api-server";
import { BigNumber } from "bignumber.js";
import { getAccountBridge } from "../../bridge";
import { retrieveSwapPayload } from "../../exchange/swap/api/v5/actions";
import { transactionStrategy } from "../../exchange/swap/transactionStrategies";
import { ExchangeSwap, FeatureFlags } from "../../exchange/swap/types";
import { Exchange } from "../../exchange/types";
import { Transaction } from "../../generated/types";
import {
  getAccountIdFromWalletAccountId,
  getWalletAPITransactionSignFlowInfos,
} from "../converters";
import { AppManifest } from "../types";
import {
  createAccounIdNotFound,
  createWrongSellParams,
  createWrongSwapParams,
  createWrongFundParams,
  ExchangeError,
} from "./error";
import { TrackingAPI } from "./tracking";
import { CompleteExchangeError, getErrorDetails, getSwapStepFromError } from "../../exchange/error";
import { postSwapCancelled } from "../../exchange/swap";
import { DeviceModelId } from "@ledgerhq/types-devices";
import { setBroadcastTransaction } from "../../exchange/swap/setBroadcastTransaction";
import { Transaction as EvmTransaction } from "@ledgerhq/coin-evm/types/index";
import { padHexString } from "@ledgerhq/hw-app-eth";
import { createStepError, StepError, toError } from "./parser";
import { handleErrors } from "./handleSwapErrors";
import get from "lodash/get";
import { SwapError } from "./SwapError";

export { ExchangeType };

type Handlers = {
  "custom.exchange.start": RPCHandler<
    ExchangeStartResult,
    ExchangeStartParams | ExchangeStartSwapParams | ExchangeStartSellParams
  >;
  "custom.exchange.complete": RPCHandler<ExchangeCompleteResult, ExchangeCompleteParams>;
  "custom.exchange.error": RPCHandler<void, SwapLiveError>;
  "custom.isReady": RPCHandler<void, void>;
  "custom.exchange.swap": RPCHandler<SwapResult, ExchangeSwapParams>;
};

export type CompleteExchangeUiRequest = {
  provider: string;
  exchange: Exchange;
  transaction: Transaction;
  binaryPayload: string;
  signature: string;
  feesStrategy: string;
  exchangeType: number;
  swapId?: string;
  amountExpectedTo?: number;
  magnitudeAwareRate?: BigNumber;
  refundAddress?: string;
  payoutAddress?: string;
  sponsored?: boolean;
  isEmbeddedSwap?: boolean;
};
type FundStartParamsUiRequest = {
  exchangeType: "FUND";
  provider: string;
  exchange: Partial<Exchange> | undefined;
};

type SellStartParamsUiRequest = {
  exchangeType: "SELL";
  provider: string;
  exchange: Partial<Exchange> | undefined;
};

type SwapStartParamsUiRequest = {
  exchangeType: "SWAP";
  provider: string;
  exchange: Partial<ExchangeSwap>;
};

type ExchangeStartParamsUiRequest =
  | FundStartParamsUiRequest
  | SellStartParamsUiRequest
  | SwapStartParamsUiRequest;

export type SwapUiRequest = CompleteExchangeUiRequest & {
  provider?: string;
  fromAccountId?: string;
  toAccountId?: string;
  tokenCurrency?: string;
};

type ExchangeUiHooks = {
  "custom.exchange.start": (params: {
    exchangeParams: ExchangeStartParamsUiRequest;
    onSuccess: (nonce: string, device?: ExchangeStartResult["device"]) => void;
    onCancel: (error: Error, device?: ExchangeStartResult["device"]) => void;
  }) => void;
  "custom.exchange.complete": (params: {
    exchangeParams: CompleteExchangeUiRequest;
    onSuccess: (hash: string) => void;
    onCancel: (error: Error) => void;
  }) => void;
  "custom.exchange.error": (params: {
    error: SwapLiveError | undefined;
    onSuccess: () => void;
    onCancel: () => void;
  }) => void;
  "custom.isReady": (params: { onSuccess: () => void; onCancel: () => void }) => void;
  "custom.exchange.swap": (params: {
    exchangeParams: SwapUiRequest;
    onSuccess: ({ operationHash, swapId }: { operationHash: string; swapId: string }) => void;
    onCancel: (error: Error) => void;
  }) => void;
};

export const handlers = ({
  accounts,
  tracking,
  manifest,
  flags,
  uiHooks: {
    "custom.exchange.start": uiExchangeStart,
    "custom.exchange.complete": uiExchangeComplete,
    "custom.exchange.error": uiError,
    "custom.isReady": uiIsReady,
    "custom.exchange.swap": uiSwap,
  },
}: {
  accounts: AccountLike[];
  tracking: TrackingAPI;
  manifest: AppManifest;
  flags?: FeatureFlags;
  uiHooks: ExchangeUiHooks;
}) =>
  ({
    "custom.exchange.start": customWrapper<ExchangeStartParams, ExchangeStartResult>(
      async params => {
        if (!params) {
          tracking.startExchangeNoParams(manifest);
          return { transactionId: "" };
        }

        const trackingParams = {
          provider: params.provider,
          exchangeType: params.exchangeType,
        };

        tracking.startExchangeRequested(trackingParams);

        let exchangeParams: ExchangeStartParamsUiRequest;

        // Use `if else` instead of switch to leverage TS type narrowing and avoid `params` force cast.
        if (params.exchangeType == "SWAP") {
          exchangeParams = await extractSwapStartParam(params, accounts);
        } else if (params.exchangeType == "SELL") {
          exchangeParams = extractSellStartParam(params, accounts);
        } else {
          exchangeParams = extractFundStartParam(params, accounts);
        }

        return new Promise((resolve, reject) =>
          uiExchangeStart({
            exchangeParams,
            onSuccess: (nonce: string, device) => {
              tracking.startExchangeSuccess(trackingParams);
              resolve({ transactionId: nonce, device });
            },
            onCancel: error => {
              tracking.startExchangeFail(trackingParams);
              reject(error);
            },
          }),
        );
      },
    ),
    "custom.exchange.complete": customWrapper<ExchangeCompleteParams, ExchangeCompleteResult>(
      async params => {
        if (!params) {
          tracking.completeExchangeNoParams(manifest);
          return { transactionHash: "" };
        }
        const trackingParams = {
          provider: params.provider,
          exchangeType: params.exchangeType,
        };
        tracking.completeExchangeRequested(trackingParams);

        const realFromAccountId = getAccountIdFromWalletAccountId(params.fromAccountId);
        if (!realFromAccountId) {
          return Promise.reject(new Error(`accountId ${params.fromAccountId} unknown`));
        }

        const fromAccount = accounts.find(acc => acc.id === realFromAccountId);

        if (!fromAccount) {
          throw new ServerError(createAccountNotFound(params.fromAccountId));
        }

        const fromParentAccount = getParentAccount(fromAccount, accounts);

        let exchange: Exchange;

        if (params.exchangeType === "SWAP") {
          const realToAccountId = getAccountIdFromWalletAccountId(params.toAccountId);
          if (!realToAccountId) {
            return Promise.reject(new Error(`accountId ${params.toAccountId} unknown`));
          }

          const toAccount = accounts.find(a => a.id === realToAccountId);

          if (!toAccount) {
            throw new ServerError(createAccountNotFound(params.toAccountId));
          }

          // TODO: check logic for EmptyTokenAccount
          let toParentAccount = getParentAccount(toAccount, accounts);
          let newTokenAccount: TokenAccount | undefined;
          if (params.tokenCurrency) {
            const currency = await getCryptoAssetsStore().findTokenById(params.tokenCurrency);
            if (!currency) {
              throw new ServerError(createCurrencyNotFound(params.tokenCurrency));
            }
            if (toAccount.type === "Account") {
              newTokenAccount = makeEmptyTokenAccount(toAccount, currency);
              toParentAccount = toAccount;
            } else {
              newTokenAccount = makeEmptyTokenAccount(toParentAccount, currency);
            }
          }

          const toCurrency = await getToCurrency(
            params.hexBinaryPayload,
            toAccount,
            newTokenAccount,
          );

          exchange = {
            fromAccount,
            fromParentAccount,
            fromCurrency: getCurrencyForAccount(fromAccount),
            toAccount: newTokenAccount ? newTokenAccount : toAccount,
            toParentAccount,
            toCurrency,
          };
        } else {
          exchange = {
            fromAccount,
            fromParentAccount,
            fromCurrency: getCurrencyForAccount(fromAccount),
          };
        }

        const mainFromAccount = getMainAccount(fromAccount, fromParentAccount);
        const mainFromAccountFamily = mainFromAccount.currency.family;

        const transaction = deserializeTransaction(params.rawTransaction);

        const { liveTx } = getWalletAPITransactionSignFlowInfos({
          walletApiTransaction: transaction,
          account: fromAccount,
        });

        if (liveTx.family !== mainFromAccountFamily) {
          return Promise.reject(
            new Error(
              `Account and transaction must be from the same family. Account family: ${mainFromAccountFamily}, Transaction family: ${liveTx.family}`,
            ),
          );
        }

        const accountBridge = getAccountBridge(fromAccount, fromParentAccount);

        /**
         * 'subAccountId' is used for ETH and it's ERC-20 tokens.
         * This field is ignored for BTC
         */
        const subAccountId =
          fromParentAccount && fromParentAccount.id !== fromAccount.id ? fromAccount.id : undefined;

        const bridgeTx = accountBridge.createTransaction(fromAccount);
        /**
         * We append the `recipient` to the tx created from `createTransaction`
         * to avoid having userGasLimit reset to null for ETH txs
         * cf. libs/ledger-live-common/src/families/ethereum/updateTransaction.ts
         */
        const tx = accountBridge.updateTransaction(
          {
            ...bridgeTx,
            recipient: liveTx.recipient,
          },
          {
            ...liveTx,
            feesStrategy: params.feeStrategy.toLowerCase(),
            subAccountId,
          },
        );

        let amountExpectedTo;
        let magnitudeAwareRate;
        let refundAddress;
        let payoutAddress;
        if (params.exchangeType === "SWAP") {
          // Get amountExpectedTo and magnitudeAwareRate from binary payload
          const decodePayload = await decodeSwapPayload(params.hexBinaryPayload);
          amountExpectedTo = new BigNumber(decodePayload.amountToWallet.toString());
          magnitudeAwareRate = tx.amount && amountExpectedTo.dividedBy(tx.amount);
          refundAddress = decodePayload.refundAddress;
          payoutAddress = decodePayload.payoutAddress;
        }

        return new Promise((resolve, reject) =>
          uiExchangeComplete({
            exchangeParams: {
              exchangeType: ExchangeType[params.exchangeType],
              provider: params.provider,
              transaction: tx,
              signature: params.hexSignature,
              binaryPayload: params.hexBinaryPayload,
              exchange,
              feesStrategy: params.feeStrategy,
              swapId: params.exchangeType === "SWAP" ? params.swapId : undefined,
              amountExpectedTo,
              magnitudeAwareRate,
              refundAddress,
              payoutAddress,
            },
            onSuccess: (transactionHash: string) => {
              tracking.completeExchangeSuccess({
                ...trackingParams,
                currency: params.rawTransaction.family,
              });
              resolve({ transactionHash });
            },
            onCancel: error => {
              tracking.completeExchangeFail(trackingParams);
              reject(error);
            },
          }),
        );
      },
    ),
    "custom.exchange.error": customWrapper<SwapLiveError, void>(async params => {
      return new Promise((resolve, reject) =>
        uiError({
          error: params,
          onSuccess: () => {
            resolve();
          },
          onCancel: () => {
            reject();
          },
        }),
      );
    }),
    "custom.exchange.swap": customWrapper<ExchangeSwapParams, SwapResult>(async params => {
      try {
        if (!params) {
          tracking.startExchangeNoParams(manifest);
          throw new ServerError(createUnknownError({ message: "params is undefined" }));
        }

        const {
          provider,
          fromAmount,
          fromAmountAtomic,
          quoteId,
          toNewTokenId,
          customFeeConfig,
          swapAppVersion,
          sponsored,
          isEmbedded,
        } = params;

        const trackingParams = {
          provider: params.provider,
          exchangeType: params.exchangeType,
          isEmbeddedSwap: isEmbedded,
        };

        tracking.startExchangeRequested(trackingParams);

        const exchangeStartParams: ExchangeStartParamsUiRequest = (await extractSwapStartParam(
          params,
          accounts,
        )) as SwapStartParamsUiRequest;

        const {
          fromCurrency,
          fromAccount,
          fromParentAccount,
          toCurrency,
          toAccount,
          toParentAccount,
        } = exchangeStartParams.exchange;

        if (!fromAccount || !fromCurrency) {
          throw new ServerError(createAccountNotFound(params.fromAccountId));
        }

        const fromAccountAddress = fromParentAccount
          ? fromParentAccount.freshAddress
          : (fromAccount as Account).freshAddress;

        const toAccountAddress = toParentAccount
          ? toParentAccount.freshAddress
          : (toAccount as Account).freshAddress;

        // Step 1: Open the drawer and open exchange app
        const startExchange = async () => {
          return new Promise<{ transactionId: string; device?: ExchangeStartResult["device"] }>(
            (resolve, reject) => {
              uiExchangeStart({
                exchangeParams: exchangeStartParams,
                onSuccess: (nonce, device) => {
                  tracking.startExchangeSuccess(trackingParams);
                  resolve({ transactionId: nonce, device });
                },
                onCancel: error => {
                  tracking.startExchangeFail(trackingParams);
                  reject(error);
                },
              });
            },
          );
        };

        let transactionId: string;
        let deviceInfo: ExchangeStartResult["device"];

        try {
          const result = await startExchange();
          transactionId = result.transactionId;
          deviceInfo = result.device;
        } catch (error) {
          const rawError = get(error, "response.data.error", error);
          const wrappedError = createStepError({
            error: toError(rawError),
            step: StepError.NONCE,
          });
          throw wrappedError;
        }

        tracking.swapPayloadRequested({
          provider,
          transactionId,
          fromAccountAddress,
          toAccountAddress,
          fromCurrencyId: fromCurrency!.id,
          toCurrencyId: toCurrency?.id,
          fromAmount,
          quoteId,
        });

        const {
          binaryPayload,
          signature,
          payinAddress,
          swapId,
          payinExtraId,
          extraTransactionParameters,
        } = await retrieveSwapPayload({
          provider,
          deviceTransactionId: transactionId,
          fromAccountAddress,
          toAccountAddress,
          fromAccountCurrency: fromCurrency!.id,
          toAccountCurrency: toCurrency!.id,
          amount: fromAmount,
          amountInAtomicUnit: fromAmountAtomic,
          quoteId,
          toNewTokenId,
          flags,
        }).catch((error: Error) => {
          const wrappedError = createStepError({
            error: get(error, "response.data.error", error),
            step: StepError.PAYLOAD,
          });

          throw wrappedError;
        });

        tracking.swapResponseRetrieved({
          binaryPayload,
          signature,
          payinAddress,
          swapId,
          payinExtraId,
          extraTransactionParameters,
        });

        // Complete Swap
        const trackingCompleteParams = {
          provider: params.provider,
          exchangeType: params.exchangeType,
          isEmbeddedSwap: isEmbedded,
        };
        tracking.completeExchangeRequested(trackingCompleteParams);

        const strategyData = {
          recipient: payinAddress,
          amount: fromAmountAtomic,
          currency: fromCurrency as CryptoOrTokenCurrency,
          customFeeConfig: customFeeConfig ?? {},
          payinExtraId,
          extraTransactionParameters,
          sponsored,
        };

        const transaction: Transaction = await getStrategy(strategyData, "swap");

        const mainFromAccount = getMainAccount(fromAccount, fromParentAccount);

        if (transaction.family !== mainFromAccount.currency.family) {
          return Promise.reject(
            new Error(
              `Account and transaction must be from the same family. Account family: ${mainFromAccount.currency.family}, Transaction family: ${transaction.family}`,
            ),
          );
        }

        const accountBridge = getAccountBridge(fromAccount, fromParentAccount);

        /**
         * 'subAccountId' is used for ETH and it's ERC-20 tokens.
         * This field is ignored for BTC
         */
        const subAccountId =
          fromParentAccount && fromParentAccount.id !== fromAccount.id ? fromAccount.id : undefined;

        const bridgeTx = accountBridge.createTransaction(fromAccount);
        /**
         * We append the `recipient` to the tx created from `createTransaction`
         * to avoid having userGasLimit reset to null for ETH txs
         * cf. libs/ledger-live-common/src/families/ethereum/updateTransaction.ts
         */
        const tx = accountBridge.updateTransaction(
          {
            ...bridgeTx,
            recipient: transaction.recipient,
          },
          {
            ...transaction,
            feesStrategy: params.feeStrategy.toLowerCase(),
            subAccountId,
          },
        );

        // Get amountExpectedTo and magnitudeAwareRate from binary payload
        const decodePayload = await decodeSwapPayload(binaryPayload);
        const amountExpectedTo = new BigNumber(decodePayload.amountToWallet.toString());
        const magnitudeAwareRate = tx.amount && amountExpectedTo.dividedBy(tx.amount);
        const refundAddress = decodePayload.refundAddress;
        const payoutAddress = decodePayload.payoutAddress;

        // tx.amount should be BigNumber
        tx.amount = new BigNumber(tx.amount);

        return new Promise((resolve, reject) =>
          uiSwap({
            exchangeParams: {
              exchangeType: ExchangeType.SWAP,
              provider: params.provider,
              transaction: tx,
              signature: signature,
              binaryPayload: binaryPayload,
              exchange: {
                fromAccount,
                fromParentAccount,
                toAccount,
                toParentAccount,
                fromCurrency: fromCurrency!,
                toCurrency: toCurrency!,
              },
              feesStrategy: params.feeStrategy,
              swapId: swapId,
              amountExpectedTo: amountExpectedTo.toNumber(),
              magnitudeAwareRate,
              refundAddress,
              payoutAddress,
              sponsored,
              isEmbeddedSwap: isEmbedded,
            },
            onSuccess: ({ operationHash, swapId }: { operationHash: string; swapId: string }) => {
              tracking.completeExchangeSuccess({
                ...trackingParams,
                currency: transaction.family,
              });

              setBroadcastTransaction({
                provider,
                result: { operation: operationHash, swapId },
                sourceCurrencyId: fromCurrency.id,
                targetCurrencyId: toCurrency?.id,
                hardwareWalletType: deviceInfo?.modelId as DeviceModelId,
                swapAppVersion,
                fromAccountAddress,
                toAccountAddress,
                fromAmount,
                flags,
              });

              resolve({ operationHash, swapId });
            },
            onCancel: error => {
              const {
                name: rawErrorName,
                message: rawErrorMessage,
                cause: rawErrorCause,
              } = getErrorDetails(error);
              const causeSuffix = rawErrorCause ? `, ${JSON.stringify(rawErrorCause)}` : "";
              const errorMessageWithCause = rawErrorMessage + causeSuffix;

              const completeExchangeError =
                // step provided in libs/ledger-live-common/src/exchange/platform/transfer/completeExchange.ts
                error instanceof CompleteExchangeError
                  ? error
                  : new CompleteExchangeError("INIT", rawErrorName, errorMessageWithCause);

              postSwapCancelled({
                provider: provider,
                swapId: swapId,
                swapStep: getSwapStepFromError(completeExchangeError),
                statusCode: completeExchangeError.title || completeExchangeError.name,
                errorMessage: completeExchangeError.message || errorMessageWithCause,
                sourceCurrencyId: fromCurrency.id,
                targetCurrencyId: toCurrency?.id,
                hardwareWalletType: deviceInfo?.modelId as DeviceModelId,
                swapType: quoteId ? "fixed" : "float",
                swapAppVersion,
                fromAccountAddress,
                toAccountAddress,
                refundAddress,
                payoutAddress,
                fromAmount,
                seedIdFrom: mainFromAccount.seedIdentifier,
                seedIdTo: toParentAccount?.seedIdentifier || (toAccount as Account)?.seedIdentifier,
                data: (transaction as EvmTransaction).data
                  ? `0x${padHexString((transaction as EvmTransaction).data?.toString("hex") || "")}`
                  : "0x",
                flags,
              });

              reject(completeExchangeError);
            },
          }),
        );
      } catch (error) {
        // Skip DrawerClosedError
        // do not redirect to the error screen
        if (isDrawerClosedError(error)) {
          throw error;
        }

        // Global catch for any errors during the swap process
        // moved out as sonarcloud suggested to avoid 4 level nested functions
        const createErrorRejector = (error: SwapError, reject: (error: SwapError) => void) => {
          return () => reject(error);
        };

        const displayError = (error: SwapError): Promise<void> =>
          new Promise((resolve, reject) => {
            const rejectWithError = createErrorRejector(error, reject);
            uiError({
              error,
              onSuccess: rejectWithError,
              onCancel: rejectWithError,
            });
          });

        await handleErrors(error, {
          onDisplayError: displayError,
        });

        throw error;
      }
    }),

    "custom.isReady": customWrapper<void, void>(async () => {
      return new Promise((resolve, reject) =>
        uiIsReady({
          onSuccess: () => {
            resolve();
          },
          onCancel: () => {
            reject();
          },
        }),
      );
    }),
  }) as const satisfies Handlers;

async function extractSwapStartParam(
  params: ExchangeStartSwapParams,
  accounts: AccountLike[],
): Promise<ExchangeStartParamsUiRequest> {
  if (!("fromAccountId" in params && "toAccountId" in params)) {
    throw new ExchangeError(createWrongSwapParams(params));
  }

  const realFromAccountId = getAccountIdFromWalletAccountId(params.fromAccountId);
  if (!realFromAccountId) {
    throw new ExchangeError(createAccounIdNotFound(params.fromAccountId));
  }

  const fromAccount = accounts.find(acc => acc.id === realFromAccountId);
  if (!fromAccount) {
    throw new ServerError(createAccountNotFound(params.fromAccountId));
  }

  let toAccount;

  if (params.exchangeType === "SWAP" && params.toAccountId) {
    const realToAccountId = getAccountIdFromWalletAccountId(params.toAccountId);
    if (!realToAccountId) {
      throw new ExchangeError(createAccounIdNotFound(params.toAccountId));
    }

    toAccount = accounts.find(a => a.id === realToAccountId);

    if (!toAccount) {
      throw new ServerError(createAccountNotFound(params.toAccountId));
    }
  }

  const fromParentAccount = getParentAccount(fromAccount, accounts);
  const toParentAccount = toAccount ? getParentAccount(toAccount, accounts) : undefined;

  const currency = params.tokenCurrency
    ? await getCryptoAssetsStore().findTokenById(params.tokenCurrency)
    : null;
  const newTokenAccount = currency ? makeEmptyTokenAccount(toAccount, currency) : null;

  return {
    exchangeType: params.exchangeType,
    provider: params.provider,
    exchange: {
      fromAccount,
      fromParentAccount,
      fromCurrency: getCurrencyForAccount(fromAccount),
      toAccount: newTokenAccount ? newTokenAccount : toAccount,
      toParentAccount: toParentAccount,
      toCurrency: getCurrencyForAccount(newTokenAccount ? newTokenAccount : toAccount),
    },
  };
}

function extractSellStartParam(
  params: ExchangeStartSellParams,
  accounts: AccountLike[],
): ExchangeStartParamsUiRequest {
  if (!("provider" in params)) {
    throw new ExchangeError(createWrongSellParams(params));
  }

  if (!params.fromAccountId) {
    return {
      exchangeType: params.exchangeType,
      provider: params.provider,
    } as ExchangeStartParamsUiRequest;
  }

  const realFromAccountId = getAccountIdFromWalletAccountId(params?.fromAccountId);

  if (!realFromAccountId) {
    throw new ExchangeError(createAccounIdNotFound(params.fromAccountId));
  }

  const fromAccount = accounts?.find(acc => acc.id === realFromAccountId);

  if (!fromAccount) {
    throw new ServerError(createAccountNotFound(params.fromAccountId));
  }

  const fromParentAccount = getParentAccount(fromAccount, accounts);

  return {
    exchangeType: params.exchangeType,
    provider: params.provider,
    exchange: {
      fromAccount,
      fromParentAccount,
    },
  };
}

function extractFundStartParam(
  params: ExchangeStartFundParams,
  accounts: AccountLike[],
): ExchangeStartParamsUiRequest {
  if (!("provider" in params)) {
    throw new ExchangeError(createWrongFundParams(params));
  }

  if (!params.fromAccountId) {
    return {
      exchangeType: params.exchangeType,
      provider: params.provider,
    } as ExchangeStartParamsUiRequest;
  }

  const realFromAccountId = getAccountIdFromWalletAccountId(params?.fromAccountId);

  if (!realFromAccountId) {
    throw new ExchangeError(createAccounIdNotFound(params.fromAccountId));
  }

  const fromAccount = accounts?.find(acc => acc.id === realFromAccountId);

  if (!fromAccount) {
    throw new ServerError(createAccountNotFound(params.fromAccountId));
  }

  const fromParentAccount = getParentAccount(fromAccount, accounts);

  return {
    exchangeType: params.exchangeType,
    provider: params.provider,
    exchange: {
      fromAccount,
      fromParentAccount,
    },
  };
}

async function getToCurrency(
  binaryPayload: string,
  toAccount: AccountLike,
  newTokenAccount?: TokenAccount,
): Promise<CryptoOrTokenCurrency> {
  const { payoutAddress: tokenAddress, currencyTo } = await decodeSwapPayload(binaryPayload);

  // In case of an SPL Token recipient and no TokenAccount exists.
  if (
    toAccount.type !== "TokenAccount" && // it must not be a SPL Token
    toAccount.currency.id === "solana" && // the target account must be a SOL Account
    tokenAddress !== toAccount.freshAddress
  ) {
    // tokenAddress is the SPL token mint address for Solana tokens
    const splTokenCurrency = await getCryptoAssetsStore().findTokenByAddressInCurrency(
      tokenAddress,
      "solana",
    );
    if (splTokenCurrency && splTokenCurrency.ticker === currencyTo) return splTokenCurrency;
  }

  return newTokenAccount?.token ?? getCurrencyForAccount(toAccount);
}

interface StrategyParams {
  recipient: string;
  amount: BigNumber | number | string;
  currency: CryptoOrTokenCurrency;
  customFeeConfig?: Record<string, unknown>;
  payinExtraId?: string;
  extraTransactionParameters?: string;
  sponsored?: boolean;
}

async function getStrategy(
  {
    recipient,
    amount,
    currency,
    customFeeConfig,
    payinExtraId,
    extraTransactionParameters,
    sponsored,
  }: StrategyParams,
  customErrorType?: any,
): Promise<Transaction> {
  const family =
    currency.type === "TokenCurrency" ? currency.parentCurrency?.family : currency.family;

  if (!family) {
    throw new Error(`TokenCurrency missing parentCurrency family: ${currency.id}`);
  }

  // Remove unsupported utxoStrategy for now
  if (customFeeConfig?.utxoStrategy) {
    delete customFeeConfig.utxoStrategy;
  }

  const strategy = transactionStrategy?.[family];

  if (!strategy) {
    throw new Error(`No transaction strategy found for family: ${family}`);
  }

  // Convert customFeeConfig values to BigNumber
  const convertedCustomFeeConfig: { [key: string]: BigNumber } = {};
  if (customFeeConfig) {
    for (const [key, value] of Object.entries(customFeeConfig)) {
      convertedCustomFeeConfig[key] = new BigNumber(value?.toString() || 0);
    }
  }

  return strategy({
    family,
    amount: new BigNumber(amount),
    recipient,
    customFeeConfig: convertedCustomFeeConfig,
    payinExtraId,
    extraTransactionParameters,
    customErrorType,
    sponsored,
  });
}

function isDrawerClosedError(error: unknown) {
  if (!error || typeof error !== "object") return false;
  const details = getErrorDetails(error);
  return details.name === "DrawerClosedError" || details.cause?.name === "DrawerClosedError";
}
