import { Connection, PublicKey } from "@solana/web3.js";
import { CloseIcon } from "@src/assets/icons";
import { Button, SnackMessage } from "@src/components";
import {
  CCIP_EXPLORER,
  FALLBACK_CROSSCHAIN_ESTIMATION_TIME,
  SOLANA_MAINNET_RPC_URL,
  crosschainEstimationTimes,
} from "@src/constants";
import { useWallet } from "@src/context/WalletProvider";
import { useDebounce } from "@src/hooks";
import { Transaction, TransactionStatus } from "@src/models";
import { TransactionWithMeta } from "@src/models/SolanaTransaction";
import { getHistory } from "@src/services";
import { extractSolanaSwapData } from "@src/services/solana/jupiter/extract-swap-data";
import { isJupiterTransaction } from "@src/services/solana/jupiter/utils";
import { isServer } from "@src/utils";
import BigNumberJS from "bignumber.js";
import { useSnackbar } from "notistack";
import {
  Context,
  ReactNode,
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
} from "react";

export interface HistoryState {
  history: Transaction[];
  addTransactionToLocalHistory: (
    transaction: Transaction,
    userAddress: string,
  ) => void;
  updateTransactionInLocalHistory: (
    transaction: Transaction,
    userAddress: string,
  ) => void;
  historyLoadedOnce: boolean;
  solanaHistory: Transaction[];
  solanaHistoryLoadedOnce: boolean;
}

const HistoryContext: Context<HistoryState> = createContext<HistoryState>(
  {} as HistoryState,
);

interface HistoryProviderProps {
  children: ReactNode;
}

const PENDING_TXS_LOCAL_STORAGE_KEY = "pending_transactions";

type LocalHistory = { [walletAddress: string]: Transaction[] };

const getLocalHistory = () => {
  if (!isServer) {
    return JSON.parse(
      localStorage.getItem(PENDING_TXS_LOCAL_STORAGE_KEY) || "{}",
    ) as LocalHistory;
  }
  return {};
};

const setLocalHistory = (data: LocalHistory) => {
  if (!isServer) {
    localStorage.setItem(PENDING_TXS_LOCAL_STORAGE_KEY, JSON.stringify(data));
  }
};

async function processSolanaTransactions(
  transactions: (TransactionWithMeta | null)[],
): Promise<Transaction[]> {
  const results: Transaction[] = [];

  for (let i = 0; i < transactions.length; i++) {
    try {
      const tx = transactions[i];
      if (!tx) continue;

      const signature = tx.transaction.signatures[0];

      if (!signature) continue;

      const connection = new Connection(SOLANA_MAINNET_RPC_URL, "confirmed");

      const swapDetails = await extractSolanaSwapData(
        signature,
        connection,
        tx,
      );

      if (swapDetails) {
        results.push(swapDetails);
      }
    } catch (error) {
      console.error(`Error processing transaction:`, error);
    }
  }

  return results;
}

export const HistoryProvider = ({ children }: HistoryProviderProps) => {
  const { evm, solana } = useWallet();
  const { address: address } = evm;
  const { address: solanaAddress } = solana;

  const [solanaHistory, setSolanaHistory] = useState<Transaction[]>([]);
  const [history, setHistory] = useState<Transaction[]>([]);
  const [historyLoadedOnce, setHistoryLoadedOnce] = useState(false);
  const [solanaHistoryLoadedOnce, setSolanaHistoryLoadedOnce] = useState(false);

  const { enqueueSnackbar, closeSnackbar } = useSnackbar();
  const { debounce: getHistoryDebounce } = useDebounce({
    delayMs: 600,
  });

  useEffect(() => {
    // @TODO remove this line after some time.
    // It is just to clean up users' browsers from the previous leaderboard implementation.
    if (!isServer) {
      localStorage.removeItem("transactions-history");
    }
  }, []);

  const fetchLastSignatures = async (senderAddress: string, limit = 50) => {
    const connection = new Connection(SOLANA_MAINNET_RPC_URL, "confirmed");
    const userPubkey = new PublicKey(senderAddress);

    try {
      const signatures = await connection.getSignaturesForAddress(userPubkey, {
        limit,
      });

      const transactions = await connection.getParsedTransactions(
        signatures.map((s) => s.signature),
        {
          maxSupportedTransactionVersion: 0,
        },
      );

      const jupiterTransactions = transactions
        .filter((val) => !!val)
        .filter((tx) => isJupiterTransaction(tx));

      const swapDetails = await processSolanaTransactions(
        jupiterTransactions as TransactionWithMeta[],
      );
      return swapDetails;
    } catch (error) {
      console.error("Error fetching transactions:", error);
      return [];
    }
  };

  const fetchSolanaHistory = async () => {
    if (!solanaAddress) {
      setSolanaHistoryLoadedOnce(true);
      return;
    }

    try {
      const lastSignatures = await fetchLastSignatures(solanaAddress);
      setSolanaHistory(lastSignatures);
    } finally {
      setSolanaHistoryLoadedOnce(true);
    }
  };

  useEffect(() => {
    if (solanaAddress) {
      fetchSolanaHistory();
    } else {
      setSolanaHistory([]);
      setSolanaHistoryLoadedOnce(true);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [solanaAddress]);

  const fetchHistory = useCallback(async () => {
    try {
      if (!address) {
        return;
      }

      const fetchedTransactions: Transaction[] = [];
      const fetchedHistory = await getHistory({
        walletAddress: address,
      });

      for (const entry of fetchedHistory.history) {
        if (!entry.source) {
          // In case some processing misalignments that might cause source to be null for a brief moment.
          continue;
        }

        let status: TransactionStatus;

        if (entry.failed) {
          status = "REVERTED";
        }

        if (entry.target) {
          status = "DONE";
        } else {
          status = "IN_PROGRESS";
        }

        const timestampMs = new Date(entry.source?.blockTime).getTime();

        let estimatedDeliveryTimestamp = FALLBACK_CROSSCHAIN_ESTIMATION_TIME;
        if (
          new BigNumberJS(entry.source.valueForInstantCcipRecieve || "0").gt(0)
        ) {
          estimatedDeliveryTimestamp = timestampMs + 30 * 1000;
        } else {
          const blockchainEstimate =
            timestampMs +
            crosschainEstimationTimes?.[entry.source.blockchainId]?.[
              entry.source.targetBlockchainId
            ];

          if (blockchainEstimate) {
            estimatedDeliveryTimestamp = blockchainEstimate;
          }
        }

        fetchedTransactions.push({
          messageId: entry.messageId,
          hash: entry.source.transactionHash,
          timestamp: timestampMs / 1000,
          sourceChainId: entry.source.blockchainId,
          targetChainId: entry.source.targetBlockchainId,
          amountWei: entry.source.tokenAmount,
          tokenAddress: entry.source.tokenAddress,
          tokenOutAddress:
            entry.target?.tokenAddress || entry.source.tokenOutAddress,
          tokenOutAmount:
            entry.target?.tokenAmount || entry.source.estimatedAmountOut,
          estimatedDeliveryTimestamp,
          status,
        });
      }

      const txHashToTxFromAPI: Record<string, Transaction> = {};
      for (const transaction of fetchedTransactions) {
        txHashToTxFromAPI[transaction.hash] = transaction;
      }

      const localHistory = getLocalHistory();
      const newLocalHistory: LocalHistory = {};

      for (const [walletAddress, transactions] of Object.entries(
        localHistory,
      )) {
        if (walletAddress.toLowerCase() !== address.toLowerCase()) {
          continue;
        }
        const newLocalHistoryForWallet: Transaction[] = [];

        for (const transaction of transactions) {
          const txHash = transaction.hash;
          const apiTransaction = txHashToTxFromAPI[txHash];

          if (!apiTransaction) {
            // localStorage has fresher data.
            fetchedTransactions.push(transaction);
            newLocalHistoryForWallet.push(transaction);
            continue;
          }

          if (
            apiTransaction &&
            transaction.status === "DONE" &&
            apiTransaction?.status === "IN_PROGRESS"
          ) {
            // If we know on the frontend that the tx is done we should update backend result
            apiTransaction.status = "DONE";
          }

          if (
            apiTransaction.status === "DONE" ||
            apiTransaction.status === "REVERTED"
          ) {
            // API has the same or newer data.
            continue;
          }
        }

        newLocalHistory[walletAddress.toLowerCase()] = newLocalHistoryForWallet;
      }
      setLocalHistory(newLocalHistory);
      setHistory(fetchedTransactions.sort((a, b) => b.timestamp - a.timestamp));
    } catch (err) {
      console.error(err);
      enqueueSnackbar(
        <SnackMessage
          message={{
            text: `Failed to fetch history!`,
            variant: "error",
          }}
        />,
        { persist: false },
      );
    } finally {
      setHistoryLoadedOnce(true);
    }
  }, [address, enqueueSnackbar]);

  useEffect(() => {
    if (!historyLoadedOnce) {
      getHistoryDebounce(fetchHistory);
    }
    const timer = setInterval(() => {
      getHistoryDebounce(fetchHistory);
    }, 60000);
    return () => clearInterval(timer);
  }, [address, fetchHistory, getHistoryDebounce, historyLoadedOnce]);

  const addTransactionToLocalHistory = useCallback(
    (transaction: Transaction, userAddress: string) => {
      setHistory((prevHistory) => {
        // Remove old tx, if it exists already. It's very unlikely though.
        const clearedPrevHistory = prevHistory.filter(
          (historyTransaction) => historyTransaction.hash !== transaction.hash,
        );

        const localHistory = getLocalHistory();
        setLocalHistory({
          ...localHistory,
          [userAddress]: [transaction, ...clearedPrevHistory],
        });

        return [transaction, ...clearedPrevHistory];
      });
    },
    [],
  );

  const showSnackbarForDoneTx = useCallback(
    (messageId: string) => {
      enqueueSnackbar(
        <SnackMessage
          message={{
            text: `Destination chain transaction mined successfully!`,
            variant: "success",
            action: () => (
              <div className="flex items-center flex-nowrap">
                <Button
                  type="button"
                  onClick={() => {
                    window.open(`${CCIP_EXPLORER}${messageId}`);
                  }}
                >
                  View
                </Button>

                <div
                  className="transaction-provider__close fill-white leading-[0px]"
                  onClick={() => closeSnackbar(messageId)}
                >
                  <CloseIcon />
                </div>
              </div>
            ),
          }}
        />,
        { persist: false, key: messageId },
      );
    },
    [closeSnackbar, enqueueSnackbar],
  );

  const updateTransactionInLocalHistory = useCallback(
    (transaction: Transaction, userAddress: string) => {
      setHistory((prevHistory) => {
        const newHistory = prevHistory.map((historyTransaction) =>
          historyTransaction.hash === transaction.hash &&
          historyTransaction.status !== "DONE"
            ? transaction
            : historyTransaction,
        );

        const localHistory = getLocalHistory();
        setLocalHistory({
          ...localHistory,
          [userAddress]: [...newHistory],
        });

        switch (transaction.status) {
          case "DONE": {
            showSnackbarForDoneTx(transaction.hash);
            break;
          }
          case "REVERTED": {
            // @TODO Add some modal when frontend starts listening for the event
            break;
          }
        }

        return [...newHistory];
      });
    },
    [showSnackbarForDoneTx],
  );

  useEffect(() => {
    setHistoryLoadedOnce(false);
    setSolanaHistoryLoadedOnce(false);
    setHistory([]);
    setSolanaHistory([]);
  }, [address, solanaAddress]);

  const state = useMemo<HistoryState>(
    () => ({
      history,
      addTransactionToLocalHistory,
      updateTransactionInLocalHistory,
      historyLoadedOnce,
      solanaHistory,
      solanaHistoryLoadedOnce,
    }),
    [
      history,
      addTransactionToLocalHistory,
      updateTransactionInLocalHistory,
      historyLoadedOnce,
      solanaHistory,
      solanaHistoryLoadedOnce,
    ],
  );

  return (
    <HistoryContext.Provider value={state}>{children}</HistoryContext.Provider>
  );
};

export function useHistory(): HistoryState {
  return useContext(HistoryContext);
}
