/**
 * Copyright (c) TonTech.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import type { FC, PropsWithChildren } from 'react';
import { formatUnits } from '@ton/appkit';
import type { Network } from '@ton/appkit';
import type { GetSwapQuoteData } from '@ton/appkit/queries';
import type { SwapProvider } from '@ton/appkit';
import { checkTransferBalance } from '@ton/appkit';
import type { TransferShortfall } from '@ton/appkit';
import { calcMaxSpendable } from '@ton/appkit';

import { useSwapQuote } from '../../hooks/use-swap-quote';
import { useSwapProvider } from '../../hooks/use-swap-provider';
import { useSwapProviders } from '../../hooks/use-swap-providers';
import { useBuildSwapTransaction } from '../../hooks/use-build-swap-transaction';
import { useAddress } from '../../../wallets';
import { useBalance } from '../../../balances/hooks/use-balance';
import { useNetwork } from '../../../network';
import { useSendTransaction } from '../../../transaction/hooks/use-send-transaction';
import { useDebounceValue } from '../../../../hooks/use-debounce-value';
import type { LowBalanceMode } from '../../../../components/shared/low-balance-modal/low-balance-modal';
import type { AppkitUIToken } from '../../../../types/appkit-ui-token';
import { mapSwapWidgetTokens } from '../../utils/map-swap-widget-tokens';
import { useSwapTokenState } from './use-swap-token-state';
import { useSwapBalances } from './use-swap-balances';
import { useSwapValidation } from './use-swap-validation';

export type { AppkitUIToken };

/**
 * Context type for the SwapWidget.
 * Provides all necessary state and actions for building custom swap UIs.
 */
export interface SwapContextType {
    /** Full list of available tokens for swapping */
    tokens: AppkitUIToken[];
    /** Currently selected source token */
    fromToken: AppkitUIToken | null;
    /** Currently selected target token */
    toToken: AppkitUIToken | null;
    /** Amount the user wants to swap (string to preserve input UX) */
    fromAmount: string;
    /** Calculated receive amount from the current quote */
    toAmount: string;
    /** Fiat currency symbol for price display, e.g. "$" */
    fiatSymbol: string;
    /** User's balance of the "from" token */
    fromBalance: string | undefined;
    /** User's balance of the "to" token */
    toBalance: string | undefined;
    /** True while the "from" balance is being fetched */
    isFromBalanceLoading: boolean;
    /** True while the "to" balance is being fetched */
    isToBalanceLoading: boolean;
    /** Whether the user can proceed with the swap (checks balance, amount, quote) */
    canSubmit: boolean;
    /** Raw swap quote from the provider */
    quote: GetSwapQuoteData | undefined;
    /** True while the quote is being fetched from the API */
    isQuoteLoading: boolean;
    /** Current validation or fetch error, null when everything is ok */
    error: string | null;
    /** Slippage tolerance in basis points (100 = 1%) */
    slippage: number;
    /** Currently selected swap provider (defaults to the first registered one) */
    swapProvider: SwapProvider | undefined;
    /** All registered swap providers */
    swapProviders: SwapProvider[];
    /** Updates the selected swap provider */
    setSwapProviderId: (providerId: string) => void;
    /** Updates the source token */
    setFromToken: (token: AppkitUIToken) => void;
    /** Updates the target token */
    setToToken: (token: AppkitUIToken) => void;
    /** Updates the swap amount */
    setFromAmount: (amount: string) => void;
    /** Updates the slippage tolerance */
    setSlippage: (slippage: number) => void;
    /** Swaps source and target tokens */
    onFlip: () => void;
    /** Sets the "from" amount to the maximum available balance */
    onMaxClick: () => void;
    /** Executes the swap transaction */
    sendSwapTransaction: () => Promise<void>;
    /** True while a transaction is being built or sent */
    isSendingTransaction: boolean;
    /** True when the built transaction outflow exceeds the user's TON balance */
    isLowBalanceWarningOpen: boolean;
    /** `reduce` when the outgoing token is TON (user can fix by changing amount), `topup` otherwise. */
    lowBalanceMode: LowBalanceMode;
    /** Required TON amount for the pending operation, formatted as a decimal string. Empty when no pending op. */
    lowBalanceRequiredTon: string;
    /** Replace the input with a value that fits into the current TON balance and close the warning */
    onLowBalanceChange: () => void;
    /** Dismiss the low-balance warning without changing the input */
    onLowBalanceCancel: () => void;
}

export const SwapContext = createContext<SwapContextType>({
    tokens: [],
    fromToken: null,
    toToken: null,
    fromAmount: '',
    toAmount: '',
    fiatSymbol: '$',
    fromBalance: undefined,
    toBalance: undefined,
    isFromBalanceLoading: false,
    isToBalanceLoading: false,
    canSubmit: false,
    quote: undefined,
    isQuoteLoading: false,
    error: null,
    slippage: 50,
    swapProvider: undefined,
    swapProviders: [],
    setSwapProviderId: () => {},
    setFromToken: () => {},
    setToToken: () => {},
    setFromAmount: () => {},
    setSlippage: () => {},
    onFlip: () => {},
    onMaxClick: () => {},
    sendSwapTransaction: () => Promise.resolve(),
    isSendingTransaction: false,
    isLowBalanceWarningOpen: false,
    lowBalanceMode: 'reduce',
    lowBalanceRequiredTon: '',
    onLowBalanceChange: () => {},
    onLowBalanceCancel: () => {},
});

/**
 * Hook to access the swap context.
 * Must be used within a SwapWidgetProvider (or SwapWidget).
 */
export function useSwapContext() {
    return useContext(SwapContext);
}

/**
 * Props for the SwapWidgetProvider.
 */
export interface SwapProviderProps extends PropsWithChildren {
    /** Full list of tokens available for swapping in the UI */
    tokens: AppkitUIToken[];
    /** Network to use for quote fetching. When omitted, uses the selected wallet's network. */
    network?: Network;
    /** Fiat currency symbol for price display, defaults to "$" */
    fiatSymbol?: string;
    /** Ticker of the token pre-selected for the source */
    defaultFromSymbol?: string;
    /** Ticker of the token pre-selected for the target */
    defaultToSymbol?: string;
    /** Initial slippage in basis points (100 = 1%), defaults to 100 (1%) */
    defaultSlippage?: number;
}

export const SwapWidgetProvider: FC<SwapProviderProps> = ({
    children,
    tokens,
    network: networkProp,
    fiatSymbol = '$',
    defaultFromSymbol,
    defaultToSymbol,
    defaultSlippage = 100,
}) => {
    // Input prep — derived from props, consumed by local-state hooks below.
    const mappedTokens = useMemo(() => mapSwapWidgetTokens(tokens), [tokens]);

    // 2. Queries and external readers (hoisted: `network` gates token filtering for local state below)
    const walletNetwork = useNetwork();
    const network = networkProp ?? walletNetwork;

    const networkFilteredTokens = useMemo(
        () => (network ? mappedTokens.filter((t) => t.network.chainId === network.chainId) : mappedTokens),
        [mappedTokens, network],
    );

    // 1. Local state
    const { fromToken, toToken, fromAmount, setFromToken, setToToken, setFromAmount, onFlip } = useSwapTokenState({
        mappedTokens: networkFilteredTokens,
        defaultFromSymbol,
        defaultToSymbol,
    });
    const [slippage, setSlippage] = useState(defaultSlippage);
    const [fromAmountDebounced] = useDebounceValue(fromAmount, 500);
    const [pendingSwap, setPendingSwap] = useState<TransferShortfall | undefined>(undefined);
    const address = useAddress();
    const [swapProvider, setSwapProviderId] = useSwapProvider();
    const swapProviders = useSwapProviders();

    // Stabilized query inputs — kept next to the query that consumes them.
    const fromTokenParam = useMemo(
        () =>
            fromToken
                ? {
                      address: fromToken.address,
                      decimals: fromToken.decimals,
                      symbol: fromToken.symbol,
                      name: fromToken.name,
                  }
                : undefined,
        [fromToken],
    );
    const toTokenParam = useMemo(
        () =>
            toToken
                ? { address: toToken.address, decimals: toToken.decimals, symbol: toToken.symbol, name: toToken.name }
                : undefined,
        [toToken],
    );

    const isNetworkSupported = useMemo(
        () =>
            !swapProvider || !network || swapProvider.getSupportedNetworks().some((n) => n.chainId === network.chainId),
        [swapProvider, network],
    );

    const {
        data: quote,
        isFetching: isQuoteFetching,
        error: quoteError,
    } = useSwapQuote({
        from: fromTokenParam,
        to: toTokenParam,
        amount: fromAmountDebounced,
        network,
        slippageBps: slippage,
        providerId: swapProvider?.providerId,
        query: { enabled: isNetworkSupported, networkMode: 'always', retry: false, gcTime: 0 },
    });
    // Also show "loading" while the user is still typing (debounce in-flight) so the UI doesn't flash
    // the previous quote as if it were final.
    const isQuoteLoading = isQuoteFetching || fromAmount !== fromAmountDebounced;
    const { fromBalance, toBalance, isFromBalanceLoading, isToBalanceLoading } = useSwapBalances({
        fromToken,
        toToken,
        ownerAddress: address ?? undefined,
        network,
    });
    const { data: tonBalance } = useBalance({ network, query: { refetchInterval: 5000 } });

    // 4. Mutations (hoisted above validation: the mutation `error` is one of its inputs)
    const {
        mutateAsync: buildTransaction,
        isPending: isBuildingTransaction,
        error: buildError,
        reset: resetBuild,
    } = useBuildSwapTransaction({ mutation: { networkMode: 'always' } });
    const {
        mutateAsync: sendTransaction,
        isPending: isSendingPending,
        error: sendMutationError,
        reset: resetSend,
    } = useSendTransaction({ mutation: { networkMode: 'always' } });
    const isSendingTransaction = isBuildingTransaction || isSendingPending;
    const sendError = sendMutationError ?? buildError;

    // Drop the previous send error when the user changes anything that would invalidate it —
    // the next attempt is conceptually a new swap, no need to keep the old message on screen.
    const resetSendError = useCallback(() => {
        resetBuild();
        resetSend();
    }, [resetBuild, resetSend]);

    // 3. Derivations
    const toAmount = quote?.toAmount ?? '';
    const { error, canSubmit } = useSwapValidation({
        fromAmount,
        fromAmountDebounced,
        fromToken,
        toToken,
        fromBalance,
        quote,
        quoteError,
        sendError,
        isNetworkSupported,
    });
    const isLowBalanceWarningOpen = pendingSwap !== undefined;
    const lowBalanceMode: LowBalanceMode = pendingSwap?.mode ?? 'reduce';
    const lowBalanceRequiredTon = useMemo(() => {
        if (!pendingSwap) return '';
        return formatUnits(pendingSwap.requiredNanos, 9);
    }, [pendingSwap]);

    // Drop the previous send error when the user changes anything that would invalidate it —
    // the next attempt is conceptually a new swap, no need to keep the old message on screen.
    useEffect(() => {
        resetSendError();
    }, [fromToken?.address, toToken?.address, fromAmount, resetSendError]);

    // Auto-clear the send error after a short delay so a stale failure doesn't linger in the
    // submit button — the user is expected to act on it within seconds or move on.
    useEffect(() => {
        if (!sendError) return;
        const id = setTimeout(resetSendError, 5000);
        return () => clearTimeout(id);
    }, [sendError, resetSendError]);

    // 5. Callbacks
    const handleMaxClick = useCallback(() => {
        if (!fromBalance || !fromToken) return;
        setFromAmount(calcMaxSpendable({ balance: fromBalance, token: fromToken, feeReserveNanos: 350_000_000n }));
    }, [fromBalance, fromToken, setFromAmount]);

    const sendSwapTransaction = useCallback(async () => {
        if (!quote || !address || !fromToken) return;

        const tx = await buildTransaction({ quote, userAddress: address });

        const shortfall = checkTransferBalance({
            messages: tx.messages,
            tonBalance,
            gasBufferNanos: 100_000_000n,
            fromToken,
            fromAmount,
        });

        if (shortfall) {
            setPendingSwap(shortfall);
            return;
        }

        await sendTransaction(tx);
    }, [quote, address, fromToken, fromAmount, buildTransaction, sendTransaction, tonBalance]);

    const onLowBalanceChange = useCallback(() => {
        if (!pendingSwap || pendingSwap.mode !== 'reduce') return;
        setFromAmount(pendingSwap.suggestedFromAmount);
        setPendingSwap(undefined);
    }, [pendingSwap, setFromAmount]);

    const onLowBalanceCancel = useCallback(() => {
        setPendingSwap(undefined);
    }, []);

    const value = useMemo(
        () => ({
            tokens: networkFilteredTokens,
            fromToken,
            toToken,
            fromAmount,
            toAmount,
            fiatSymbol,
            fromBalance,
            toBalance,
            isFromBalanceLoading,
            isToBalanceLoading,
            canSubmit,
            quote,
            isQuoteLoading,
            error,
            slippage,
            swapProvider,
            swapProviders,
            setSwapProviderId,
            setFromToken,
            setToToken,
            setFromAmount,
            setSlippage,
            onFlip,
            onMaxClick: handleMaxClick,
            sendSwapTransaction,
            isSendingTransaction,
            isLowBalanceWarningOpen,
            lowBalanceMode,
            lowBalanceRequiredTon,
            onLowBalanceChange,
            onLowBalanceCancel,
        }),
        [
            networkFilteredTokens,
            fromToken,
            toToken,
            fromAmount,
            toAmount,
            fiatSymbol,
            fromBalance,
            toBalance,
            isFromBalanceLoading,
            isToBalanceLoading,
            canSubmit,
            quote,
            isQuoteLoading,
            error,
            slippage,
            swapProvider,
            swapProviders,
            setSwapProviderId,
            setFromToken,
            setToToken,
            setFromAmount,
            setSlippage,
            onFlip,
            handleMaxClick,
            sendSwapTransaction,
            isSendingTransaction,
            isLowBalanceWarningOpen,
            lowBalanceMode,
            lowBalanceRequiredTon,
            onLowBalanceChange,
            onLowBalanceCancel,
        ],
    );

    return <SwapContext.Provider value={value}>{children}</SwapContext.Provider>;
};
