import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CloseIcon, CopyIcon, SearchIcon } from "@src/assets/icons";
import {
  Button,
  Spinner,
  TextInput,
  TokenLogoWithChain,
  UnknownTokenLogo,
} from "@src/components";
import { SwapPanelType } from "@src/components/Swap/SwapView";
import { useSwapContext } from "@src/context";
import { ERC20Abi } from "@src/contracts";
import { useEvmContractApi } from "@src/hooks";
import { Chain, Ecosystem, ImportedTokenData, TokenOption } from "@src/models";
import {
  isETHAddressValid,
  isServer,
  isSolanaAddressValid,
  shortAddress,
} from "@src/utils";
import { BigNumber, ethers } from "ethers";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { GroupedVirtuoso } from "react-virtuoso";
import { QuickPickTokenItem } from "./QuickPickTokenItem";
import { TokenItem } from "./TokenItem";
import { useAccount } from "wagmi";
import { Connection, PublicKey } from "@solana/web3.js";
import { METADATA_PROGRAM_ID } from "@src/constants";
import { useWallet } from "@src/context/WalletProvider";

type Props = {
  type: SwapPanelType;
  chain?: Chain;
  tokens: TokenOption[];
  otherTokens: TokenOption[];
  selectedTokenAddress?: string;
  isOpen: boolean;
  onSelect: (token: TokenOption) => void;
  setModalOpen: (isModalOpen: boolean) => void;
};
export const TokenPicker = ({
  chain,
  tokens,
  otherTokens,
  selectedTokenAddress,
  isOpen,
  onSelect,
  setModalOpen,
  type,
}: Props) => {
  const [searchValue, setSearchValue] = useState("");
  const [acceptRisks, setAcceptRisks] = useState(false);
  const [showImportWarning, setShowImportWarning] = useState(false);
  const [loadingCustomTokenData, setLoadingCustomTokenData] = useState(false);
  const [customTokenData, setCustomTokenData] =
    useState<ImportedTokenData | null>(null);
  const [showQuickPicks, setShowQuickPicks] = useState(true);
  const currentRouteRequestNonce = useRef<number>(0);
  const tokenSearchRef = useRef<HTMLInputElement | null>(null);

  const {
    bridgeUI,
    supportedChains,
    findTokenDataByAddressAndChain,
    tokenPrices,
    addTokenToChainIfNotExists,
  } = useSwapContext();
  const { address } = useAccount();
  const {
    solana: { address: solanaAddress },
  } = useWallet();
  const evmContractApi = useEvmContractApi();

  useEffect(() => {
    setShowImportWarning(false);
    setAcceptRisks(false);
  }, [isOpen]);

  const importCustomToken = useCallback(() => {
    if (
      chain &&
      customTokenData &&
      (isETHAddressValid(searchValue) || isSolanaAddressValid(searchValue))
    ) {
      let customTokens = {};
      if (!isServer) {
        customTokens = JSON.parse(
          localStorage.getItem("custom-tokens") || "{}",
        );
      }

      const newCustomToken = {
        address: customTokenData.address,
        symbol: customTokenData.symbol,
        name: customTokenData.name,
        tokenId: customTokenData.address,
        decimals: customTokenData.decimals,
        quickPick: false,
        supported: true,
        priority: 0,
        chainId: chain.chainId,
      };

      if (!isServer) {
        customTokens[chain.chainId] = {
          ...(customTokens[chain.chainId] || {}),
          [searchValue.toLowerCase()]: newCustomToken,
        };
        localStorage.setItem("custom-tokens", JSON.stringify(customTokens));
      }

      addTokenToChainIfNotExists(newCustomToken.chainId, newCustomToken);

      setModalOpen(false);
    }
  }, [
    chain,
    customTokenData,
    searchValue,
    addTokenToChainIfNotExists,
    setModalOpen,
  ]);

  const getCustomTokenData = useCallback(async () => {
    const nonce = Date.now();
    currentRouteRequestNonce.current = nonce;
    const rpcUrl = supportedChains.find(
      ({ ecosystem, chainId }) =>
        ecosystem === Ecosystem.EVM && chainId === chain?.chainId,
    )?.publicRpcUrls[0];
    const customTokenContract = new ethers.Contract(
      searchValue.toLowerCase(),
      ERC20Abi,
      ethers.getDefaultProvider(rpcUrl),
    );
    setLoadingCustomTokenData(true);
    try {
      const responses = await Promise.all([
        evmContractApi.query(customTokenContract, "decimals"),
        evmContractApi.query(customTokenContract, "name"),
        evmContractApi.query(customTokenContract, "symbol"),
      ]);
      if (nonce !== currentRouteRequestNonce.current) {
        return;
      }
      const [decimals, name, symbol] = responses;
      setCustomTokenData({
        address: searchValue.toLowerCase(),
        decimals,
        name,
        symbol,
      });
    } catch (_) {
      setCustomTokenData(null);
    } finally {
      if (nonce === currentRouteRequestNonce.current) {
        setLoadingCustomTokenData(false);
      }
    }
  }, [chain, searchValue, evmContractApi, supportedChains]);

  const getCustomSolanaTokenData = useCallback(async () => {
    const nonce = Date.now();
    currentRouteRequestNonce.current = nonce;

    const rpcUrl = supportedChains.find(
      ({ ecosystem, chainId }) =>
        ecosystem === Ecosystem.SOLANA && chainId === chain?.chainId,
    )?.publicRpcUrls[0];

    if (!rpcUrl || !searchValue || !isSolanaAddressValid(searchValue)) {
      setCustomTokenData(null);
      setLoadingCustomTokenData(false);
      return;
    }

    setLoadingCustomTokenData(true);

    try {
      const connection = new Connection(rpcUrl, "confirmed");
      const mintPublicKey = new PublicKey(searchValue);
      const mintInfo = await connection.getParsedAccountInfo(mintPublicKey);

      if (
        !mintInfo?.value?.data ||
        !("parsed" in mintInfo.value.data) ||
        mintInfo.value.data.program !== "spl-token"
      ) {
        setCustomTokenData(null);
        return;
      }
      const decimals = mintInfo.value.data.parsed.info.decimals;

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

      const TOKEN_METADATA_PROGRAM_ID = new PublicKey(METADATA_PROGRAM_ID);

      const [metadataPDA] = PublicKey.findProgramAddressSync(
        [
          Buffer.from("metadata"),
          TOKEN_METADATA_PROGRAM_ID.toBuffer(),
          mintPublicKey.toBuffer(),
        ],
        TOKEN_METADATA_PROGRAM_ID,
      );

      const metadataAccount = await connection.getAccountInfo(metadataPDA);

      if (!metadataAccount) {
        throw new Error("Metadata account not found");
      }

      const data = Buffer.from(metadataAccount.data);

      // First byte is key (always 4 for Metadata), then comes actual data
      let offset = 1;

      // Read update authority - 32 bytes
      offset += 32;

      // Read mint - 32 bytes
      offset += 32;

      // Read name
      const nameLen = Math.min(data.readUInt32LE(offset), 32); // Max name length is 32
      offset += 4;
      const name = data
        .slice(offset, offset + nameLen)
        .toString("utf8")
        .replace(/\0/g, "");
      offset += 32; // Name field is always 32 bytes, padded with nulls

      // Read symbol
      const symbolLen = Math.min(data.readUInt32LE(offset), 10); // Max symbol length is 10
      offset += 4;
      const symbol = data
        .slice(offset, offset + symbolLen)
        .toString("utf8")
        .replace(/\0/g, "");

      setCustomTokenData({
        address: searchValue,
        name:
          name || `Token ${searchValue.slice(0, 4)}...${searchValue.slice(-4)}`,
        symbol: symbol || searchValue.slice(0, 4),
        decimals,
      });
    } catch (error) {
      console.error("Error fetching Solana token data:", error);
      setCustomTokenData(null);
    } finally {
      if (nonce === currentRouteRequestNonce.current) {
        setLoadingCustomTokenData(false);
      }
    }
  }, [chain, searchValue, supportedChains]);

  useEffect(() => {
    if (
      !bridgeUI &&
      chain &&
      !findTokenDataByAddressAndChain(
        chain.ecosystem === Ecosystem.EVM
          ? searchValue.toLowerCase()
          : searchValue,
        chain.chainId,
      )
    ) {
      if (chain.ecosystem === Ecosystem.EVM && isETHAddressValid(searchValue)) {
        getCustomTokenData();
      } else if (
        chain.ecosystem === Ecosystem.SOLANA &&
        isSolanaAddressValid(searchValue)
      ) {
        getCustomSolanaTokenData();
      } else {
        setLoadingCustomTokenData(false);
        setCustomTokenData(null);
      }
    } else {
      setLoadingCustomTokenData(false);
      setCustomTokenData(null);
    }
  }, [
    bridgeUI,
    searchValue,
    getCustomTokenData,
    getCustomSolanaTokenData,
    findTokenDataByAddressAndChain,
    chain,
  ]);

  useEffect(() => {
    setSearchValue("");
  }, [isOpen]);

  const isMatch = (value, searchValue) =>
    value.toLowerCase().indexOf(searchValue.toLowerCase()) > -1;

  const filteredTokens = useMemo(
    () =>
      tokens?.filter(
        (token) =>
          isMatch(token.symbol, searchValue) ||
          isMatch(token.name, searchValue) ||
          token.address.toLowerCase() === searchValue.toLowerCase(),
      ),
    [searchValue, tokens],
  );

  const otherFilteredTokens = useMemo(
    () =>
      otherTokens?.filter(
        (token) =>
          isMatch(token.symbol, searchValue) ||
          isMatch(token.name, searchValue) ||
          token.address.toLowerCase() === searchValue.toLowerCase(),
      ),
    [searchValue, otherTokens],
  );

  useEffect(() => {
    if (isOpen && tokenSearchRef.current) {
      setTimeout(() => {
        tokenSearchRef.current?.focus();
      }, 10); // delay focus a tiny bit to ensure it works
    }
  }, [isOpen]);

  const calculateTokenValue = useCallback(
    (token: TokenOption) => {
      if (!token.balance || !token.decimals) return 0;
      const balance = Number(
        ethers.utils.formatUnits(token.balance, token.decimals),
      );
      const price = tokenPrices[token.chainId]?.[token.address];
      return price ? balance * Number(price) : 0;
    },
    [tokenPrices],
  );

  const ownedFilteredTokens = useMemo(() => {
    return [
      ...filteredTokens.filter((token) =>
        BigNumber.from(token?.balance || "0").gt(0),
      ),
      ...otherFilteredTokens.filter((token) =>
        BigNumber.from(token?.balance || "0").gt(0),
      ),
    ].sort((tokenA, tokenB) => {
      const valueA = calculateTokenValue(tokenA);
      const valueB = calculateTokenValue(tokenB);
      // First sort by USD value
      if (valueB !== valueA) {
        return valueB - valueA;
      }
      // If USD values are equal (or both 0), sort by priority
      return tokenB.priority - tokenA.priority;
    });
  }, [filteredTokens, otherFilteredTokens, calculateTokenValue]);

  const allTokens = useMemo(() => {
    const currentChainTokens = filteredTokens
      .filter((token) => BigNumber.from(token?.balance || "0").eq(0))
      .sort(
        (tokenDataA, tokenDataB) => tokenDataB.priority - tokenDataA.priority,
      );
    const otherChainTokens = otherFilteredTokens
      .filter((token) => BigNumber.from(token?.balance || "0").eq(0))
      .sort(
        (tokenDataA, tokenDataB) => tokenDataB.priority - tokenDataA.priority,
      );

    // Create groups based on chain and wallet connection status
    const groups = chain
      ? [
          ...(!!(address || solanaAddress) && ownedFilteredTokens.length
            ? [ownedFilteredTokens]
            : []),
          currentChainTokens,
          otherChainTokens,
        ]
      : [
          ...(!!(address || solanaAddress) && ownedFilteredTokens.length
            ? [ownedFilteredTokens]
            : []),
          [...currentChainTokens, ...otherChainTokens],
        ];

    return groups.flat();
  }, [
    filteredTokens,
    otherFilteredTokens,
    ownedFilteredTokens,
    chain,
    address,
    solanaAddress,
  ]);

  const ownedTokens = useMemo(
    () => allTokens.filter((token) => token.balance?.gt(0)),
    [allTokens],
  );

  const tokenGroups = useMemo(() => {
    const currentChainTokens = filteredTokens
      .filter((token) => BigNumber.from(token?.balance || "0").eq(0))
      .sort(
        (tokenDataA, tokenDataB) => tokenDataB.priority - tokenDataA.priority,
      );
    const otherChainTokens = otherFilteredTokens
      .filter((token) => BigNumber.from(token?.balance || "0").eq(0))
      .sort(
        (tokenDataA, tokenDataB) => tokenDataB.priority - tokenDataA.priority,
      );

    return chain
      ? [
          ...(!!(address || solanaAddress) && ownedFilteredTokens.length
            ? [ownedFilteredTokens]
            : []),
          currentChainTokens,
          otherChainTokens,
        ]
      : [
          ...(!!(address || solanaAddress) && ownedFilteredTokens.length
            ? [ownedFilteredTokens]
            : []),
          [...currentChainTokens, ...otherChainTokens],
        ];
  }, [
    chain,
    address,
    solanaAddress,
    filteredTokens,
    otherFilteredTokens,
    ownedFilteredTokens,
  ]);

  return showImportWarning ? (
    <>
      <div className="flex justify-between">
        <div className="text-base/4 mb-4">Import token</div>
        <div
          className="w-4 h-4 fill-t_text_primary cursor-pointer"
          onClick={() => setModalOpen(false)}
        >
          <CloseIcon />
        </div>
      </div>
      <div className="flex flex-col h-full gap-2">
        {customTokenData && (
          <div
            className={`token-picker-container grow-0 bg-t_bg_secondary border border-solid border-t_text_primary border-opacity-10 rounded-xl py-2 pr-2 pl-4 hover:cursor-pointer`}
          >
            <UnknownTokenLogo
              tokenName={customTokenData.name}
              className="w-9 h-9 text-[15px]"
            />
            <div className="flex justify-between items-center gap-1 overflow-hidden grow">
              <div className="overflow-hidden whitespace-nowrap text-ellipsis text-sm font-medium">
                {customTokenData.name}
              </div>
              <div className="token-picker__all__symbol">
                {customTokenData.symbol}
              </div>
            </div>
            <div className="text-sm flex items-center">
              {shortAddress(searchValue.toLowerCase())}
              <button
                type="button"
                onClick={() =>
                  navigator.clipboard.writeText(
                    customTokenData.address.toLowerCase(),
                  )
                }
                aria-label={`Copy ${customTokenData.symbol} token address`}
                className="flex items-center p-2 fill-white cursor-pointer hover:fill-t_main_accent_light"
              >
                <CopyIcon className="w-5 h-5" />
              </button>
            </div>
          </div>
        )}
        <div className="px-2 py-4 rounded-xl border border-x_orange text-sm">
          Ensure that you have verified the token address and confirmed its
          correctness
        </div>
        <div className="bg-t_bg_secondary border border-solid border-t_text_primary border-opacity-10 rounded-xl p-2">
          <div className="text-sm mb-2">
            Be mindful that anyone can create tokens, even counterfeit versions
            of existing ones. Certain tokens and their technical specifications
            might not be compatible with XSwap services. By importing such
            custom tokens, you acknowledge and consent to the associated risks.
          </div>
          <button
            type="button"
            onClick={() => setAcceptRisks((state) => !state)}
            className="flex items-center"
          >
            <div className="flex justify-center items-center p-1 w-6 h-6 border-2 border-white rounded-md mr-2">
              {acceptRisks && (
                <FontAwesomeIcon icon={faCheck} className="w-4 h-4" />
              )}
            </div>

            <div className="font-bold">Accept</div>
          </button>
        </div>
        <div className="flex grow items-end justify-center">
          <Button
            type="button"
            disabled={!acceptRisks}
            className={`token-picker__import-button w-full ${
              !acceptRisks ? "!cursor-not-allowed" : ""
            }`}
            onClick={importCustomToken}
          >
            Import
          </Button>
        </div>
      </div>
    </>
  ) : (
    <>
      <div className="flex justify-between">
        <div className="text-base/4 text-t_text_primary mb-4">{`Pick ${type} token`}</div>
        <div
          className="w-4 h-4 fill-t_text_primary cursor-pointer"
          onClick={() => setModalOpen(false)}
        >
          <CloseIcon />
        </div>
      </div>

      <TextInput
        ref={tokenSearchRef}
        inputIcon={
          <div className="fill-t_text_primary">
            <SearchIcon />
          </div>
        }
        placeholder="Search name or paste address"
        value={searchValue}
        handleChange={setSearchValue}
      />
      <div
        className={`flex flex-wrap gap-1 my-4 mx-0 transition-all duration-300 ${
          showQuickPicks
            ? "opacity-100 max-h-[200px]"
            : "opacity-0 max-h-0 overflow-hidden"
        }`}
      >
        {tokens
          .filter((token) => token?.quickPick)
          .map((token) => (
            <QuickPickTokenItem
              key={token.address}
              token={token}
              selectedTokenAddress={selectedTokenAddress}
              onClick={() => {
                onSelect(token);
                setModalOpen(false);
              }}
            />
          ))}
      </div>
      <div className="flex flex-col mx-0 h-full">
        {loadingCustomTokenData ? (
          <div className="flex justify-center w-full py-4 px-0">
            <Spinner className="w-9 h-9" />
          </div>
        ) : (
          <>
            {customTokenData ? (
              <div className={`token-picker-container hover:cursor-default`}>
                <TokenLogoWithChain
                  token={customTokenData}
                  chain={chain}
                  tokenLogoClassName="w-9 h-9"
                  generatedLogoClassName="w-9 h-9 text-[16px]"
                  chainLogoClassName="w-5"
                />
                <div className="flex justify-between items-center gap-1 overflow-hidden grow">
                  <div>
                    <div className="text-sm font-medium text-ellipsis">
                      {customTokenData.name}
                    </div>
                    <div className="text-xs text-t_text_primary !text-opacity-50 font-medium">
                      {chain?.displayName}
                    </div>
                  </div>
                  <div className="token-picker__all__symbol">
                    {customTokenData.symbol}
                  </div>
                </div>
                <div>
                  <Button
                    type="button"
                    className="h-8 py-0 px-4 text-t_text_primary leading-[14px] rounded-[32px] bg-gradient-to-r from-t_main_accent_light to-t_main_accent_dark"
                    onClick={() => setShowImportWarning(true)}
                  >
                    Import
                  </Button>
                </div>
              </div>
            ) : (
              <div className="flex flex-col h-full gap-8 w-[calc(100%+32px)] mx-[-16px]">
                <GroupedVirtuoso
                  style={{ maxHeight: "700px", height: "100%" }}
                  groupCounts={tokenGroups.map((group) => group?.length)}
                  scrollerRef={(ref) => {
                    if (ref) {
                      ref.addEventListener("scroll", (e) => {
                        const target = e.target as HTMLDivElement;
                        setShowQuickPicks(target.scrollTop < 10);
                      });
                    }
                  }}
                  groupContent={(index) => (
                    <div className="py-1 bg-t_bg_primary text-t_text_primary">
                      <div className="px-4 text-left">
                        {(() => {
                          if (chain) {
                            // chain is selected
                            switch (index) {
                              case 0: {
                                if (!address && !solanaAddress) {
                                  return `${chain.displayName} network`;
                                }
                                return ownedTokens.length
                                  ? "My tokens"
                                  : `${chain.displayName} network`;
                              }
                              case 1: {
                                if (!address && !solanaAddress) {
                                  return "Other networks";
                                }
                                return `${chain.displayName} network`;
                              }
                              case 2: {
                                return "Other networks";
                              }
                              default: {
                                return "Undefined group";
                              }
                            }
                          } else {
                            // chain is not selected
                            switch (index) {
                              case 0: {
                                if (!address) {
                                  return "All networks";
                                }
                                return ownedTokens.length
                                  ? "My tokens"
                                  : "All networks";
                              }
                              case 1: {
                                return "All networks";
                              }
                              default: {
                                return "Undefined group";
                              }
                            }
                          }
                        })()}
                      </div>
                      <div className="horizontal-separator" />
                      {!tokenGroups[index]?.length && (
                        <div className="text-center py-1">No token found</div>
                      )}
                    </div>
                  )}
                  itemContent={(index) => {
                    return (
                      allTokens[index] && (
                        <TokenItem
                          panelType={type}
                          token={allTokens[index]}
                          selectedChainId={chain?.chainId}
                          selectedTokenAddress={selectedTokenAddress}
                          onClick={() => {
                            onSelect(allTokens[index]!);
                            setModalOpen(false);
                          }}
                        />
                      )
                    );
                  }}
                />
              </div>
            )}
          </>
        )}
      </div>
    </>
  );
};
