import { TxConfigFormPayload } from "@src/components";
import {
  DEFAULT_DESTINATION_CHAIN_ID,
  DEFAULT_REQUEST_ABORT_MSG,
  DEFAULT_SOURCE_CHAIN_ID,
  SOLANA_NATIVE_TOKEN_ADDRESS,
  SOLANA_TOKEN_2022_PROGRAM_ID,
  SOLANA_TOKEN_PROGRAM_ID,
} from "@src/constants";
import { ADDRESSES } from "@src/contracts";
import {
  BridgeToken,
  BridgeTokensDictionary,
  Chain,
  Ecosystem,
  EVMRoute,
  SolanaRoute,
  Token,
  TokenOption,
  TokenPrices,
  UnifiedRoute,
  Web3Environment,
} from "@src/models";
import {
  getBalances,
  getBridgeTokens,
  getPrices,
  getRoute,
  getSolanaRoute,
} from "@src/services";
import {
  deepMergeObjects,
  getBalanceOf,
  humanReadableToWei,
  isServer,
  resolveTokenAddress,
  safeBigNumberFrom,
  sortChains,
  weiToHumanReadable,
} from "@src/utils";
import BigNumberJS from "bignumber.js";
import { BigNumber, constants } from "ethers";
import {
  createContext,
  Dispatch,
  ReactNode,
  SetStateAction,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { mapToValidPath } from "@src/utils/validation";
import { useWallet } from "./WalletProvider";
import { Connection, PublicKey } from "@solana/web3.js";

type Tab = "Swap" | "History" | "Settings";

type ChainOption = Chain & {
  disabled: boolean;
};

type SwapContext = {
  bridgeUI: boolean;
  integrationConfig: Partial<TxConfigFormPayload>;
  supportedChains: Chain[];
  tab: Tab;
  setTab: Dispatch<SetStateAction<Tab>>;
  overlay: boolean;
  error: string;
  setError: Dispatch<SetStateAction<string>>;
  feeToken: Token | undefined;
  srcChain: Chain | undefined;
  setSrcChain: Dispatch<SetStateAction<Chain | undefined>>;
  srcChainTokensOptions: TokenOption[];
  srcChainOtherTokensOptions: TokenOption[];
  srcChainOptions: ChainOption[];
  dstChainOptions: ChainOption[];
  tokenPrices: TokenPrices;
  srcToken: Token | undefined;
  setSrcToken: Dispatch<SetStateAction<Token | undefined>>;
  dstChain: Chain | undefined;
  setDstChain: Dispatch<SetStateAction<Chain | undefined>>;
  dstChainTokensOptions: TokenOption[];
  dstChainOtherTokensOptions: TokenOption[];
  dstTokenLocked: boolean;
  dstChainLocked?: boolean;
  srcTokenLocked?: boolean;
  srcChainLocked?: boolean;
  dstToken: Token | undefined;
  setDstToken: Dispatch<SetStateAction<Token | undefined>>;
  srcValue: string;
  setSrcValue: Dispatch<SetStateAction<string>>;
  srcValueWei: string;
  srcValueUsd: string | null;
  dstValue: string;
  dstValueWei: string;
  dstValueUsd: string | null;
  dstValueMin: string;
  expressDelivery: boolean;
  setExpressDelivery: Dispatch<SetStateAction<boolean>>;
  infiniteApproval: boolean;
  setInfiniteApproval: Dispatch<SetStateAction<boolean>>;
  slippage: string;
  setSlippage: Dispatch<SetStateAction<string>>;
  route: UnifiedRoute | undefined;
  setRoute: Dispatch<SetStateAction<UnifiedRoute | undefined>>;
  isAsyncDataLoaded: boolean;
  srcTokenBalanceWei: string;
  setSrcTokenBalanceWei: Dispatch<SetStateAction<string>>;
  srcTokenBalanceInfo: string;
  isFetchingRoute: boolean;
  isFetchingBalance: boolean;
  isFetchingBalances: boolean;
  isExpressDeliveryActive: boolean;
  quoteRoute: () => Promise<void>;
  refreshBalance: () => void;
  findTokenDataByAddressAndChain: (
    tokenAddress: string,
    chainId: string,
  ) => Token | undefined;
  bridgeTokensDictionary: BridgeTokensDictionary | null;
  onConnectWallet: () => void;
  hasOnConnectWalletCallback: boolean;
  addTokenToChainIfNotExists: (
    chainId: string | undefined,
    token: TokenOption,
  ) => void;
};

const functionNotImplemented = () => {
  throw new Error("Not implemented");
};

const init: SwapContext = {
  bridgeUI: false,
  integrationConfig: {},
  supportedChains: [],
  tab: "Swap",
  overlay: false,
  error: "",
  feeToken: undefined,
  srcChain: undefined,
  srcChainTokensOptions: [],
  srcChainOtherTokensOptions: [],
  tokenPrices: {},
  dstChain: undefined,
  dstChainTokensOptions: [],
  dstChainOtherTokensOptions: [],
  srcToken: undefined,
  dstTokenLocked: false,
  dstChainLocked: false,
  srcTokenLocked: false,
  srcChainLocked: false,
  dstToken: undefined,
  srcValue: "",
  srcValueWei: "",
  srcValueUsd: null,
  dstValue: "",
  dstValueWei: "",
  dstValueUsd: null,
  dstValueMin: "",
  expressDelivery: true,
  infiniteApproval: true,
  slippage: "1.5",
  route: undefined,
  isAsyncDataLoaded: false,
  srcTokenBalanceWei: "",
  srcTokenBalanceInfo: "-",
  isFetchingBalance: false,
  isFetchingBalances: false,
  isFetchingRoute: false,
  isExpressDeliveryActive: true,
  setTab: functionNotImplemented,
  setError: functionNotImplemented,
  setSrcChain: functionNotImplemented,
  setDstChain: functionNotImplemented,
  setSrcToken: functionNotImplemented,
  setDstToken: functionNotImplemented,
  setSrcValue: functionNotImplemented,
  setExpressDelivery: functionNotImplemented,
  setInfiniteApproval: functionNotImplemented,
  setSlippage: functionNotImplemented,
  setRoute: functionNotImplemented,
  setSrcTokenBalanceWei: functionNotImplemented,
  quoteRoute: functionNotImplemented,
  refreshBalance: functionNotImplemented,
  findTokenDataByAddressAndChain: functionNotImplemented,
  bridgeTokensDictionary: null,
  onConnectWallet: functionNotImplemented,
  hasOnConnectWalletCallback: false,
  srcChainOptions: [],
  dstChainOptions: [],
  addTokenToChainIfNotExists: functionNotImplemented,
};

const swapContext = createContext(init);

type Props = {
  children: ReactNode;
  integrationConfig: Partial<TxConfigFormPayload>;
  supportedChains: Chain[];
  overlay: boolean;
};
export const SwapProvider = ({
  children,
  integrationConfig,
  supportedChains,
  overlay,
}: Props) => {
  // =============================================================================
  // Hooks
  // =============================================================================
  const { evm, solana } = useWallet();
  const { address: evmAddress, chainId: walletChainId } = evm;
  const { address: solanaAddress } = solana;
  // =============================================================================
  // State
  // =============================================================================
  const [isInitialStateSet, setIsInitialStateSet] = useState(false);
  // Settings
  const [tab, setTab] = useState(init.tab);
  const [error, setError] = useState(init.error);
  const [expressDelivery, setExpressDelivery] = useState(init.expressDelivery);
  const [slippage, setSlippage] = useState(init.slippage);
  const [infiniteApproval, setInfiniteApproval] = useState(
    init.infiniteApproval,
  );
  // Chains
  const [srcChain, setSrcChain] = useState(init.srcChain);
  const [dstChain, setDstChain] = useState(init.dstChain);
  const [feeToken, setFeeToken] = useState(init.feeToken);
  // Tokens
  const [srcToken, setSrcToken] = useState(init.srcToken);
  const [dstToken, setDstToken] = useState(init.dstToken);
  const [tokenPrices, setTokenPrices] = useState(init.tokenPrices);
  const [tokenBalances, setTokenBalances] = useState<
    Record<string, Record<string, string>>
  >({});
  const [bridgeTokens, setBridgeTokens] = useState<BridgeToken[]>([]);
  // Values
  const [srcValue, setSrcValue] = useState(init.srcValue);
  const [srcTokenBalanceWei, setSrcTokenBalanceWei] = useState(
    init.srcTokenBalanceWei,
  );
  // Chains and Token options for pickers
  const [srcChainTokensOptions, setSrcChainTokensOptions] = useState(
    init.srcChainTokensOptions,
  );
  const [dstChainTokensOptions, setDstChainTokensOptions] = useState(
    init.dstChainTokensOptions,
  );
  const [srcChainOtherTokensOptions, setSrcChainOtherTokensOptions] = useState(
    init.srcChainOtherTokensOptions,
  );
  const [dstChainOtherTokensOptions, setDstChainOtherTokensOptions] = useState(
    init.dstChainOtherTokensOptions,
  );

  const [srcChainOptions, setSrcChainOptions] = useState<ChainOption[]>([]);
  const [dstChainOptions, setDstChainOptions] = useState<ChainOption[]>([]);
  // Loaders
  const [isFetchingBalance, setIsFetchingBalance] = useState(
    init.isFetchingBalance,
  );
  const [isFetchingBalances, setIsFetchingBalances] = useState(
    init.isFetchingBalances,
  );
  const [isFetchingRoute, setFetchingRoute] = useState(init.isFetchingRoute);
  // Route and tx data
  const [route, setRoute] = useState(init.route);

  // =============================================================================
  // Refs
  // =============================================================================

  const routeAbortController = useRef<AbortController | null>(null);
  const refreshSelectedTokenBalanceNonce = useRef(crypto.randomUUID());

  // =============================================================================
  // Memoized Values
  // =============================================================================

  // useMemo: determines if the source token is locked based on integration config.
  const srcTokenLocked = useMemo(
    () => !!integrationConfig?.srcTokenLocked,
    [integrationConfig?.srcTokenLocked],
  );
  // useMemo: determines if the destination token is locked based on integration config.
  const dstTokenLocked = useMemo(
    () => !!integrationConfig?.dstTokenLocked,
    [integrationConfig?.dstTokenLocked],
  );
  // useMemo: determines if the destination chain is locked from integration config.
  const dstChainLocked = useMemo(
    () => !!integrationConfig?.dstChainLocked,
    [integrationConfig?.dstChainLocked],
  );
  // useMemo: determines if the source chain is locked from integration config.
  const srcChainLocked = useMemo(
    () => !!integrationConfig?.srcChainLocked,
    [integrationConfig?.srcChainLocked],
  );
  // useMemo: sets a flag to indicate if the bridge UI is enabled based on integration config.
  const bridgeUI = useMemo(
    () => !!integrationConfig.bridge,
    [integrationConfig.bridge],
  );

  // useMemo: determines if a wallet connection callback exists.
  const hasOnConnectWalletCallback = useMemo(() => {
    return !!(integrationConfig.onConnectWallet || window.xPayOnConnectWallet);
  }, [integrationConfig.onConnectWallet]);

  // useMemo: merges supported chain tokens with custom tokens from localStorage.
  const supportedTokens: Record<string, Record<string, Token>> = useMemo(() => {
    const customTokens = !isServer
      ? JSON.parse(localStorage.getItem("custom-tokens") || "{}")
      : {};

    const chainIdToTokenAddressToData: {
      [chainId: string]: {
        [tokenAddress: string]: Token;
      };
    } = {};
    supportedChains.forEach((chain) => {
      chainIdToTokenAddressToData[chain.chainId] = {};
      chain.tokens.forEach((token) => {
        chainIdToTokenAddressToData[chain.chainId]![token.address] = token;
      });
    });
    return deepMergeObjects(chainIdToTokenAddressToData, customTokens);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [supportedChains]);

  // useMemo: creates a mapping from each chain ID to available token options sorted by token priority.
  const chainToTokenOptionsMap = useMemo(() => {
    const tempChainToTokenOptionsMap: Record<string, TokenOption[]> = {};
    Object.entries(supportedTokens).forEach(([chainId, chainData]) => {
      tempChainToTokenOptionsMap[chainId] = Object.entries(chainData)
        .filter(([, tokenData]) => tokenData.supported)
        .sort(([, tokenDataA], [, tokenDataB]) => {
          return tokenDataB.priority - tokenDataA.priority;
        })
        .map(([, tokenData]) => {
          return {
            chainId: chainId,
            ...tokenData,
          };
        });
    });
    return tempChainToTokenOptionsMap;
  }, [supportedTokens]);

  // useMemo: constructs a dictionary mapping tokens across chains for bridging based on bridgeTokens data.
  const bridgeTokensDictionary = useMemo(() => {
    // If there are no bridge tokens, return null directly.
    if (!bridgeTokens || bridgeTokens.length === 0) return null;

    const dictionary: BridgeTokensDictionary = {};
    // Helper to add a mapping from one token to its counterpart.
    const addMapping = (
      chainIdFrom: string,
      tokenAddressFrom: string,
      chainIdTo: string,
      tokenAddressTo: string,
    ) => {
      if (!dictionary[chainIdFrom]) {
        dictionary[chainIdFrom] = {};
      }
      if (!dictionary[chainIdFrom][tokenAddressFrom]) {
        dictionary[chainIdFrom][tokenAddressFrom] = {};
      }
      dictionary[chainIdFrom][tokenAddressFrom][chainIdTo] = tokenAddressTo;
    };
    // Process each token pair from bridgeTokens.
    for (const tokenPair of bridgeTokens) {
      const entries = Object.entries(tokenPair);
      if (entries.length !== 2) {
        // Skip invalid token pairs.
        continue;
      }
      const [chainId0, address0] = entries[0]!;
      const [chainId1, address1] = entries[1]!;

      addMapping(chainId0, address0, chainId1, address1);
      addMapping(chainId1, address1, chainId0, address0);
    }
    return Object.keys(dictionary).length > 0 ? dictionary : null;
  }, [bridgeTokens]);
  // useMemo: calculates the USD value for the source token amount using the current token prices.
  const srcValueUsd = useMemo(() => {
    if (
      srcToken &&
      srcChain &&
      tokenPrices &&
      tokenPrices[srcChain.chainId] &&
      tokenPrices[srcChain.chainId]?.[srcToken.address]
    ) {
      return (
        Number(srcValue) *
        Number(tokenPrices[srcChain.chainId]?.[srcToken.address])
      ).toFixed(2);
    }
    return null;
  }, [tokenPrices, srcToken, srcValue, srcChain]);
  // useMemo: converts the source token value from human-readable format to Wei.
  const srcValueWei = useMemo(
    () =>
      humanReadableToWei({
        amount: srcValue,
        decimals: srcToken?.decimals || 18,
      }),
    [srcToken?.decimals, srcValue],
  );
  // useMemo: converts the estimated output amount from the route to a human-readable destination token value.
  const dstValue = useMemo(
    () =>
      weiToHumanReadable({
        amount: route?.estAmountOut || "0", // TODO include fees.xSwapFee.tokenFee
        decimals: dstToken?.decimals || 18,
        precisionFractionalPlaces: Math.min(8, dstToken?.decimals || 18),
      }),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [route],
  );
  // useMemo: converts the human-readable destination value back to Wei using the destination token's decimals.
  const dstValueWei = useMemo(
    () =>
      humanReadableToWei({
        amount: dstValue,
        decimals: dstToken?.decimals || 18,
      }),
    [dstToken?.decimals, dstValue],
  );
  // useMemo: calculates the USD value for the destination token amount using the token prices.
  const dstValueUsd = useMemo(() => {
    if (
      dstToken &&
      dstChain &&
      tokenPrices &&
      tokenPrices[dstChain?.chainId] &&
      tokenPrices[dstChain?.chainId]?.[dstToken.address]
    ) {
      return (
        Number(dstValue) *
        Number(tokenPrices[dstChain?.chainId]?.[dstToken.address])
      ).toFixed(2);
    }
    return null;
  }, [tokenPrices, dstToken, dstValue, dstChain]);
  // useMemo: computes the minimum destination token value in human-readable format from route data.
  const dstValueMin = useMemo(
    () =>
      weiToHumanReadable({
        amount: route?.minAmountOut || "0",
        decimals: dstToken?.decimals || 18,
        precisionFractionalPlaces: Math.min(4, dstToken?.decimals || 18),
      }),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [route],
  );
  // useMemo: checks if express delivery is enabled and if the route includes a non-zero express delivery fee.
  const isExpressDeliveryActive = useMemo(
    () =>
      expressDelivery &&
      !!route &&
      BigNumber.from(route.xSwapFees?.expressDeliveryFee || "0").gt("0"),
    [expressDelivery, route],
  );
  // useMemo: indicates whether asynchronous data is loaded based on the presence of supported chains.
  const isAsyncDataLoaded = useMemo(
    () => supportedChains.length > 0 && bridgeTokens.length > 0,
    [supportedChains, bridgeTokens],
  );
  // useMemo: computes a formatted balance string for the source token based on its balance in Wei.
  const srcTokenBalanceInfo = useMemo(() => {
    if (!srcToken || !srcTokenBalanceWei || (!evmAddress && !solanaAddress)) {
      return "";
    }
    if (safeBigNumberFrom(srcTokenBalanceWei).eq(0)) {
      return "0";
    }

    const fullHumanReadableBalance = weiToHumanReadable({
      amount: srcTokenBalanceWei,
      decimals: srcToken.decimals,
      precisionFractionalPlaces: srcToken.decimals,
    });

    if (new BigNumberJS(fullHumanReadableBalance).lt("0.00001")) {
      return "<0.00001";
    }

    // Shortened to cut non-significant decimals
    return weiToHumanReadable({
      amount: srcTokenBalanceWei,
      decimals: srcToken.decimals,
      precisionFractionalPlaces: 5,
    });
  }, [evmAddress, solanaAddress, srcToken, srcTokenBalanceWei]);

  // =============================================================================
  // Callbacks
  // =============================================================================

  // returns available token options for a given chain, filtering by bridge support if bridge UI is enabled.
  // NOTE: This function does not handle any of the filtering
  const getTokenOptions = useCallback(
    (chainId: string | undefined) => {
      if (!chainId) {
        return [];
      }

      // swap
      if (!bridgeUI) {
        return chainToTokenOptionsMap[chainId] || [];
      }

      // bridge
      if (!chainToTokenOptionsMap[chainId] || !bridgeTokensDictionary) {
        return [];
      }

      const supportedTokensAddresses = Object.keys(
        bridgeTokensDictionary[chainId] || {},
      );

      return chainToTokenOptionsMap[chainId].filter((tokenData) =>
        supportedTokensAddresses.includes(tokenData.address),
      );
    },
    [bridgeTokensDictionary, bridgeUI, chainToTokenOptionsMap],
  );
  // fetches and sets the route quote using the current input parameters, aborting any previous request.
  const quoteRoute = useCallback(async () => {
    if (
      !srcChain ||
      !dstChain ||
      !srcToken ||
      !dstToken ||
      !srcValueWei ||
      safeBigNumberFrom(srcValueWei).eq(0) ||
      (bridgeUI && !bridgeTokensDictionary) ||
      (bridgeUI && srcChain.ecosystem !== Ecosystem.EVM)
    ) {
      setRoute(undefined);
      return;
    }

    // Validate the current path before requesting a route
    const { source, target } = mapToValidPath({
      source: { chain: srcChain, token: srcToken },
      target: { chain: dstChain, token: dstToken },
      type: bridgeUI ? "BRIDGE" : "SWAP",
      supportedChains,
      bridgeTokensDictionary: bridgeTokensDictionary || {},
    });

    // If any of the chains or tokens would be changed by validation, don't request a route
    if (
      source.chain?.chainId !== srcChain?.chainId ||
      target.chain?.chainId !== dstChain?.chainId ||
      source.token?.address !== srcToken?.address ||
      target.token?.address !== dstToken?.address
    ) {
      return;
    }

    try {
      setError("");
      setFetchingRoute(true);

      if (routeAbortController.current) {
        routeAbortController.current.abort(DEFAULT_REQUEST_ABORT_MSG);
      }
      routeAbortController.current = new AbortController();
      if (srcChain.ecosystem === Ecosystem.SOLANA) {
        const solanaRouteData = await getSolanaRoute({
          integratorId: integrationConfig.integratorId!,
          fromToken: srcToken.address,
          toToken: dstToken.address,
          fromAmount: srcValueWei,
          fromAddress:
            solanaAddress || ADDRESSES[srcChain.chainId]?.FeeCollector || "",
          slippage: Number(slippage),
        });

        const unifiedRoute: SolanaRoute = {
          ...solanaRouteData,
          ecosystem: "solana",
        };
        setRoute(unifiedRoute);
      } else {
        const evmRouteData = await getRoute(
          {
            integratorId: integrationConfig.integratorId!,
            fromChain: srcChain.chainId,
            toChain: dstChain.chainId,
            fromAddress:
              evmAddress || ADDRESSES[srcChain.chainId]?.FeeCollector || "",
            toAddress:
              dstChain.ecosystem === Ecosystem.SOLANA
                ? solanaAddress ||
                  ADDRESSES[dstChain.chainId]?.FeeCollector ||
                  ""
                : evmAddress || ADDRESSES[dstChain.chainId]?.FeeCollector || "",
            fromToken: srcToken.address,
            toToken: dstToken.address,
            fromAmount: srcValueWei,
            slippage: Number(slippage),
            customContractCalls: integrationConfig.customContractCalls,
            paymentToken: constants.AddressZero,
            expressDelivery: expressDelivery,
            infiniteApproval: infiniteApproval,
            integratorFee: integrationConfig.integratorFee,
            integratorFeeReceiverAddress:
              integrationConfig.integratorFeeReceiverAddress,
          },
          routeAbortController.current.signal,
        );

        const unifiedRoute: EVMRoute = {
          ...evmRouteData,
          ecosystem: "evm",
        };
        setRoute(unifiedRoute);
      }

      routeAbortController.current = null;
    } catch (error) {
      if (error !== DEFAULT_REQUEST_ABORT_MSG) {
        routeAbortController.current = null;
        console.error(error);
        setError("Could not find route!");
        setRoute(undefined);
      }
    } finally {
      if (!routeAbortController.current) {
        setFetchingRoute(false);
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    evmAddress,
    bridgeUI,
    dstChain,
    dstToken,
    getRoute,
    isExpressDeliveryActive,
    slippage,
    srcChain,
    srcToken,
    srcValueWei,
  ]);
  // refreshes and updates the balance of the source token using its RPC URL.
  const refreshBalance = useCallback(async () => {
    if (
      (!evmAddress && !solanaAddress) ||
      !srcChain?.publicRpcUrls[0] ||
      !srcToken ||
      !srcToken?.address
    ) {
      return;
    }

    const nonce = crypto.randomUUID();
    refreshSelectedTokenBalanceNonce.current = nonce;

    setIsFetchingBalance(true);
    let balance = "0";
    if (srcChain?.ecosystem === Ecosystem.EVM && evmAddress) {
      const balanceUI = await getBalanceOf(
        evmAddress,
        srcToken.address,
        srcChain?.publicRpcUrls[0],
      );
      balance = humanReadableToWei({
        amount: balanceUI,
        decimals: srcToken.decimals,
      });
    } else if (srcChain?.ecosystem === Ecosystem.SOLANA && solanaAddress) {
      const solanaPublicKey = new PublicKey(solanaAddress);
      const connection = new Connection(srcChain.publicRpcUrls[0]!);
      if (srcToken.address === SOLANA_NATIVE_TOKEN_ADDRESS.toBase58()) {
        balance = new BigNumberJS(
          await connection.getBalance(solanaPublicKey),
        ).toString();
      } else {
        const tokenAccounts = await connection.getParsedTokenAccountsByOwner(
          solanaPublicKey,
          {
            programId: SOLANA_TOKEN_PROGRAM_ID,
          },
        );
        const token2022Accounts =
          await connection.getParsedTokenAccountsByOwner(solanaPublicKey, {
            programId: SOLANA_TOKEN_2022_PROGRAM_ID,
          });
        const allTokenAccount = [
          ...tokenAccounts.value,
          ...token2022Accounts.value,
        ];
        const tokenAccount = allTokenAccount.find(
          (account) =>
            account.account.data.parsed.info.mint === srcToken.address,
        );
        if (!tokenAccount) {
          balance = "0";
        } else {
          balance = tokenAccount.account.data.parsed.info.tokenAmount.amount;
        }
      }
    }

    if (nonce !== refreshSelectedTokenBalanceNonce.current) {
      return;
    }

    setSrcTokenBalanceWei(balance);

    setIsFetchingBalance(false);
  }, [evmAddress, solanaAddress, srcChain, srcToken]);
  // retrieves token details for a given token address and chain from supported tokens mapping. (including imported tokens)
  const findTokenDataByAddressAndChain = useCallback(
    (tokenAddress: string, chainId: string): Token | undefined => {
      return supportedTokens[chainId]?.[tokenAddress];
    },
    [supportedTokens],
  );
  // triggers the wallet connection process.
  const onConnectWallet = useCallback(() => {
    if (integrationConfig.onConnectWallet) {
      integrationConfig.onConnectWallet();
    } else if (window.xPayOnConnectWallet) {
      window.xPayOnConnectWallet();
    }
  }, [integrationConfig]);

  const addTokenToChainIfNotExists = useCallback(
    (chainId: string | undefined, token: TokenOption) => {
      const chain = supportedChains.find((chain) => chain.chainId === chainId);

      if (!chain) return chain;

      const tokenExists = chain.tokens.some(
        (existingToken) =>
          existingToken.address.toLowerCase() === token.address.toLowerCase(),
      );

      const updated = tokenExists
        ? chain
        : { ...chain, tokens: [...chain.tokens, token] };

      setSrcChain(updated);
      setSrcToken(token);
    },
    [supportedChains],
  );

  // =============================================================================
  // Effects
  // =============================================================================

  // fetches token prices for supported chains once they are loaded.
  useEffect(() => {
    if (supportedChains.length > 0) {
      const chainIds = supportedChains.map((chain) => chain.chainId);
      getPrices({ chainId: chainIds }).then((prices) => {
        setTokenPrices(prices);
      });
    }
  }, [supportedChains]);
  // fetches and sets bridge tokens on component mount.
  useEffect(() => {
    (async () => {
      const data = await getBridgeTokens();
      setBridgeTokens(data);
    })();
  }, []);
  // recalculates the route quote when its dependency changes.
  useEffect(() => {
    quoteRoute();
  }, [quoteRoute]);
  // calls refreshBalance to update source token balance on mount or when refreshBalance changes.
  useEffect(() => {
    refreshBalance();
  }, [refreshBalance]);
  // fetches user all token balances when a wallet address is available and supported chains are loaded.
  useEffect(() => {
    let isCancelled = false;

    if ((evmAddress || solanaAddress) && supportedChains.length > 0) {
      setIsFetchingBalances(true);

      getBalances({
        walletAddress: evmAddress,
        solanaWalletAddress: solanaAddress,
      }).then((balances) => {
        if (!isCancelled) {
          setTokenBalances(balances);
          setIsFetchingBalances(false);
        }
      });
    }

    return () => {
      isCancelled = true; // prevent state update from stale async call
    };
  }, [evmAddress, solanaAddress, supportedChains.length]);
  // sets the fee token from the source chain's tokens by selecting the native token (AddressZero).
  useEffect(() => {
    if (srcChain?.chainId === "mainnet-beta") {
      setFeeToken(
        srcChain?.tokens.find(
          ({ address }) =>
            address === "So11111111111111111111111111111111111111112",
        ),
      );
    } else {
      setFeeToken(
        srcChain?.tokens.find(
          ({ address }) =>
            address.toLowerCase() === constants.AddressZero.toLowerCase(),
        ),
      );
    }
  }, [srcChain]);

  // =============================================================================
  // useEffects responsible for providing token and chain options
  // =============================================================================

  // updates token options for source and destination chains and other networks, incorporating balance info.
  useEffect(() => {
    (async () => {
      const isSingleChainSelected = dstChain?.chainId === srcChain?.chainId;
      // source chain options
      const srcTokenOptions: TokenOption[] = getTokenOptions(
        srcChain?.chainId,
      ).filter(({ symbol }) =>
        isSingleChainSelected ? symbol !== dstToken?.symbol : true,
      );
      setSrcChainTokensOptions(srcTokenOptions);
      // destination chain options
      const dstTokenOptions: TokenOption[] = getTokenOptions(
        dstChain?.chainId,
      ).filter(({ symbol }) =>
        isSingleChainSelected ? symbol !== srcToken?.symbol : true,
      );
      setDstChainTokensOptions(dstTokenOptions);
      // token options from other source networks
      const srcOtherTokenOptions: TokenOption[] = [];
      const srcOtherChains = supportedChains.filter(
        ({ chainId, web3Environment, swapSupported, bridgeSupported }) =>
          // same chain
          chainId !== srcChain?.chainId &&
          // not mainnet
          web3Environment === Web3Environment.MAINNET &&
          // bridgeUI and supported for bridge or !bridgeUI and supported for swap
          ((bridgeUI && bridgeSupported) || (!bridgeUI && swapSupported)),
      );
      // create token options from other source networks and filter out the destination token
      for (const { chainId } of srcOtherChains) {
        srcOtherTokenOptions.push(
          ...getTokenOptions(chainId).filter(({ symbol }) =>
            chainId === dstChain?.chainId ? symbol !== dstToken?.symbol : true,
          ),
        );
      }
      setSrcChainOtherTokensOptions(srcOtherTokenOptions);
      // token options from other destination networks
      const dstOtherTokenOptions: TokenOption[] = [];
      const dstOtherChains = supportedChains.filter(
        ({ chainId, web3Environment, swapSupported, bridgeSupported }) =>
          chainId !== dstChain?.chainId &&
          // not mainnet
          web3Environment === Web3Environment.MAINNET &&
          // bridgeUI and supported for bridge as destination or !bridgeUI and supported for swap as destination
          ((bridgeUI &&
            bridgeSupported &&
            srcChain?.supportedDstForBridge?.includes(chainId)) ||
            (!bridgeUI &&
              swapSupported &&
              srcChain?.supportedDstForSwap?.includes(chainId))),
      );
      // create token options from other destination networks and filter out the source token
      for (const { chainId } of dstOtherChains) {
        dstOtherTokenOptions.push(
          ...getTokenOptions(chainId).filter(({ symbol }) =>
            chainId === srcChain?.chainId ? symbol !== srcToken?.symbol : true,
          ),
        );
      }
      setDstChainOtherTokensOptions(dstOtherTokenOptions);
      // add balances to the token options
      const addBalances = (tokenOptions: TokenOption[]) => {
        return tokenOptions.map((token) => ({
          ...token,
          balance: BigNumber.from(
            tokenBalances[token.chainId]?.[token.address] || 0,
          ),
        }));
      };
      setSrcChainTokensOptions(addBalances(srcTokenOptions));
      setDstChainTokensOptions(addBalances(dstTokenOptions));
      setSrcChainOtherTokensOptions(addBalances(srcOtherTokenOptions));
      setDstChainOtherTokensOptions(addBalances(dstOtherTokenOptions));
    })();
  }, [
    bridgeUI,
    srcChain,
    dstChain,
    getTokenOptions,
    supportedChains,
    srcToken?.symbol,
    dstToken?.symbol,
    tokenBalances,
  ]);

  // updates the source and destination chain options based on network support and bridge configurations.
  useEffect(() => {
    const baseChains = supportedChains.filter(
      ({ web3Environment, swapSupported, bridgeSupported }) =>
        web3Environment === Web3Environment.MAINNET &&
        ((bridgeUI && bridgeSupported) || (!bridgeUI && swapSupported)),
    );
    // Determine disabled source chains for bridge UI. For swap, the source chains are all the supported chains.
    let sourceChainsDisabled: string[] = [];
    // If we block any bridge source chain, we need to have dstChain and dstToken selected, otherwise we allow to pick any source chain.
    if (bridgeUI && dstChain && dstToken && bridgeTokensDictionary) {
      // Find chains that have tokens available for the selected destination chain
      const chainsWithTokensForDestination = baseChains.filter(
        ({ chainId }) => {
          const bridgeTokens = bridgeTokensDictionary[chainId] || {};
          return Object.values(bridgeTokens).some(
            (targetChains) => targetChains[dstChain.chainId],
          );
        },
      );

      // Disable chains that don't have tokens available for the destination
      sourceChainsDisabled = baseChains
        .filter(
          (chain) =>
            !chainsWithTokensForDestination.some(
              (availableChain) => availableChain.chainId === chain.chainId,
            ),
        )
        .map((chain) => chain.chainId);
    }

    // Determine disabled destination chains.
    const destinationChainsDisabled = new Set<string>();

    // If there is a source chain, we need to disable the destination chains that are not supported for the source chain.
    if (srcChain) {
      baseChains
        .filter((chain) =>
          bridgeUI
            ? !srcChain.supportedDstForBridge?.includes(chain.chainId)
            : !srcChain.supportedDstForSwap?.includes(chain.chainId),
        )
        .forEach((chain) => destinationChainsDisabled.add(chain.chainId));
    }

    // If there is no source token, we need to disable the chains without any supported tokens.
    if (bridgeUI && srcChain && bridgeTokensDictionary && !srcToken) {
      const srcChainTokens = bridgeTokensDictionary[srcChain.chainId];
      if (!srcChainTokens) {
        // If source chain has no bridge tokens, disable all destination chains

        baseChains.forEach((chain) =>
          destinationChainsDisabled.add(chain.chainId),
        );
      } else {
        // Disable chains that have no tokens available from the source chain
        baseChains
          .filter((chain) => {
            // Check if any token on source chain can bridge to this destination chain
            return !Object.values(srcChainTokens).some(
              (targetChains) => targetChains[chain.chainId],
            );
          })
          .forEach((chain) => destinationChainsDisabled.add(chain.chainId));
      }
    }

    // If there is a source token, we need to disable the chains that don't have tokens available for the source token.
    if (bridgeUI && srcChain && bridgeTokensDictionary && srcToken) {
      const availableChains =
        bridgeTokensDictionary[srcChain.chainId]?.[srcToken.address];
      baseChains

        .filter((chain) => !availableChains?.[chain.chainId])
        .forEach((chain) => destinationChainsDisabled.add(chain.chainId));
    }

    setSrcChainOptions(sortChains(baseChains, sourceChainsDisabled));
    setDstChainOptions(sortChains(baseChains, [...destinationChainsDisabled]));
  }, [
    supportedChains,
    bridgeUI,
    srcChain,
    srcToken,
    dstChain,
    dstToken,
    bridgeTokensDictionary,
  ]);

  // =============================================================================
  // useEffects responsible for state updates and validations
  // =============================================================================

  // sets the initial source chain based on the wallet's chain ID if not already initialized.
  useEffect(() => {
    if (!walletChainId || !isInitialStateSet || integrationConfig?.srcChainId) {
      return;
    }
    const initSrcChain = supportedChains.find(
      ({ chainId }) => chainId === `${walletChainId}`,
    );
    if (initSrcChain) {
      setSrcChain(initSrcChain);
    }
  }, [
    walletChainId,
    isInitialStateSet,
    supportedChains,
    integrationConfig?.srcChainId,
  ]);

  // resets source and destination tokens and chains when bridgeUI changes and initial state is not yet set.
  useEffect(() => {
    if (!isInitialStateSet) {
      return;
    }

    setSrcToken(undefined);
    setDstToken(undefined);
    setSrcChain(undefined);
    setDstChain(undefined);

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [bridgeUI]);

  // This useEffect runs just once in full. It initializes source and destination chains and tokens based on integration config, wallet, and default values, then marks initial state as set.
  useEffect(() => {
    if (
      isInitialStateSet ||
      !supportedChains ||
      supportedChains.length === 0 ||
      !bridgeTokensDictionary
    ) {
      return;
    }

    // find chains
    const searchedSrcChainId =
      srcChain ||
      integrationConfig?.srcChainId ||
      (walletChainId ? `${walletChainId}` : undefined) ||
      (bridgeUI ? undefined : DEFAULT_SOURCE_CHAIN_ID);

    const initSrcChain = supportedChains.find(
      ({ chainId }) => chainId === searchedSrcChainId,
    );

    const searchedDstChainId =
      dstChain ||
      integrationConfig?.dstChainId ||
      (bridgeUI ? undefined : DEFAULT_DESTINATION_CHAIN_ID);

    const initDstChain = supportedChains.find(
      ({ chainId }) => chainId === searchedDstChainId,
    );

    // find tokens
    let initDstToken: Token | undefined;
    let initSrcToken: Token | undefined;

    const srcTokenAddress = resolveTokenAddress(
      integrationConfig.srcTokenAddr,
      integrationConfig?.srcChainId,
      supportedChains,
    );
    const dstTokenAddress = resolveTokenAddress(
      integrationConfig.dstTokenAddr,
      integrationConfig?.dstChainId,
      supportedChains,
    );

    if (initSrcChain) {
      initSrcToken = initSrcChain.tokens.find(
        ({ address }) =>
          address.toLowerCase() === srcTokenAddress?.toLowerCase(),
      );
    }
    if (initDstChain) {
      initDstToken = initDstChain.tokens.find(
        ({ address }) =>
          address.toLowerCase() === dstTokenAddress?.toLowerCase(),
      );
    }

    // validate the initial state
    const { source, target } = mapToValidPath({
      source: { chain: initSrcChain, token: initSrcToken },
      target: { chain: initDstChain, token: initDstToken },
      type: bridgeUI ? "BRIDGE" : "SWAP",
      supportedChains,
      bridgeTokensDictionary,
    });
    setSrcChain(source.chain);
    setDstChain(target.chain);
    setSrcToken(source.token);
    setDstToken(target.token);
    // Mark initial state as set
    setIsInitialStateSet(true);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    integrationConfig?.srcChainId,
    integrationConfig?.srcTokenAddr,
    integrationConfig?.dstChainId,
    integrationConfig?.dstTokenAddr,
    supportedChains,
    bridgeUI,
    walletChainId,
    isInitialStateSet,
    bridgeTokensDictionary,
  ]);
  // syncs current chain and token selections with the valid path computed from current state.
  useEffect(() => {
    if (
      !bridgeTokensDictionary ||
      supportedChains.length === 0 ||
      !isInitialStateSet
    ) {
      return;
    }

    const { source, target } = mapToValidPath({
      source: { chain: srcChain, token: srcToken },
      target: { chain: dstChain, token: dstToken },
      type: bridgeUI ? "BRIDGE" : "SWAP",
      supportedChains,
      bridgeTokensDictionary,
    });
    if (source.chain?.chainId !== srcChain?.chainId) {
      setSrcChain(source.chain);
    }
    if (target.chain?.chainId !== dstChain?.chainId) {
      setDstChain(target.chain);
    }
    if (
      source.token?.address !== srcToken?.address ||
      source.token?.name !== srcToken?.name
    ) {
      setSrcToken(source.token);
    }

    if (
      target.token?.address !== dstToken?.address ||
      target.token?.name !== dstToken?.name
    ) {
      setDstToken(target.token);
    }
  }, [
    srcChain,
    srcToken,
    dstChain,
    dstToken,
    bridgeUI,
    supportedChains,
    bridgeTokensDictionary,
    isInitialStateSet,
  ]);
  // notifies integration or global callback when the destination chain changes.
  useEffect(() => {
    if (!isInitialStateSet) {
      return;
    }
    if (integrationConfig.onDstChainChange) {
      integrationConfig.onDstChainChange(dstChain?.chainId);
    } else if (window.xPayOnDstChainChange) {
      window.xPayOnDstChainChange(dstChain?.chainId);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [integrationConfig.onDstChainChange, dstChain]);
  // notifies integration or global callback when the destination token changes.
  useEffect(() => {
    if (!isInitialStateSet) {
      return;
    }
    const token = dstToken
      ? { address: dstToken.address.toLowerCase(), symbol: dstToken.tokenId }
      : undefined;

    if (integrationConfig.onDstTokenChange) {
      integrationConfig.onDstTokenChange(token);
    } else if (window.xPayOnDstTokenChange) {
      window.xPayOnDstTokenChange(token);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [integrationConfig.onDstTokenChange, dstToken]);
  // notifies integration or global callback when the source chain changes.
  useEffect(() => {
    if (!isInitialStateSet) {
      return;
    }
    if (integrationConfig.onSrcChainChange) {
      integrationConfig.onSrcChainChange(srcChain?.chainId);
    } else if (window.xPayOnSrcChainChange) {
      window.xPayOnSrcChainChange(srcChain?.chainId);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [integrationConfig.onSrcChainChange, srcChain]);
  // notifies integration or global callback when the source token changes.
  useEffect(() => {
    if (!isInitialStateSet) {
      return;
    }
    const token = srcToken
      ? { address: srcToken.address.toLowerCase(), symbol: srcToken.tokenId }
      : undefined;

    if (integrationConfig.onSrcTokenChange) {
      integrationConfig.onSrcTokenChange(token);
    } else if (window.xPayOnSrcTokenChange) {
      window.xPayOnSrcTokenChange(token);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [integrationConfig.onSrcTokenChange, srcToken]);

  return (
    <swapContext.Provider
      value={{
        bridgeUI,
        integrationConfig,
        supportedChains,
        tab,
        setTab,
        overlay,
        error,
        setError,
        feeToken,
        srcChain,
        setSrcChain,
        srcChainTokensOptions,
        srcChainOtherTokensOptions,
        srcChainOptions,
        dstChainOptions,
        tokenPrices,
        dstChain,
        setDstChain,
        dstChainTokensOptions,
        dstChainOtherTokensOptions,
        srcToken,
        setSrcToken,
        dstToken,
        setDstToken,
        srcValue,
        setSrcValue,
        srcValueWei,
        srcValueUsd,
        dstValue,
        dstValueWei,
        dstValueUsd,
        dstValueMin,
        expressDelivery,
        setExpressDelivery,
        infiniteApproval,
        setInfiniteApproval,
        slippage,
        setSlippage,
        route,
        setRoute,
        isAsyncDataLoaded,
        srcTokenBalanceWei,
        setSrcTokenBalanceWei,
        srcTokenBalanceInfo,
        isFetchingRoute,
        isFetchingBalance,
        isFetchingBalances,
        isExpressDeliveryActive,
        quoteRoute,
        refreshBalance,
        findTokenDataByAddressAndChain,
        srcTokenLocked,
        srcChainLocked,
        dstTokenLocked,
        dstChainLocked,
        bridgeTokensDictionary,
        onConnectWallet,
        hasOnConnectWalletCallback,
        addTokenToChainIfNotExists,
      }}
    >
      {children}
    </swapContext.Provider>
  );
};

export const useSwapContext = () => {
  return useContext(swapContext);
};
