import fp from 'fastify-plugin'
import {FastifyReply, FastifyRequest} from 'fastify'
import {z} from 'zod'
import {GmxSdk} from '../../generated/gmxsdk/index.js'

import {arbitrum} from 'viem/chains';
import {getTokenBySymbol} from '../../generated/gmxsdk/configs/tokens.js';
import {
    Position,
    PositionStatus,
    Trade,
    TradeDirection,
    TradeStatus,
    TradeType
} from '../../generated/ManagingApiTypes.js';
import {MarketInfo, MarketsInfoData} from '../../generated/gmxsdk/types/markets.js';
import {MarketConfig, MARKETS} from '../../generated/gmxsdk/configs/markets.js'
import {ARBITRUM} from '../../generated/gmxsdk/configs/chains.js'
import {TokenData, TokensData} from '../../generated/gmxsdk/types/tokens.js';
import {getByKey} from '../../generated/gmxsdk/utils/objects.js';
import {GmxSdkConfig} from '../../generated/gmxsdk/types/sdk.js';
import {PositionIncreaseParams, SwapParams} from '../../generated/gmxsdk/modules/orders/helpers.js';
import {
    basisPointsToFloat,
    bigintToNumber,
    numberToBigint,
    PRECISION_DECIMALS
} from '../../generated/gmxsdk/utils/numbers.js';
import {DecreasePositionSwapType, OrderType, PositionOrderInfo} from '../../generated/gmxsdk/types/orders.js';
import {DecreasePositionAmounts} from '../../generated/gmxsdk/types/trade.js';
import {decodeReferralCode, encodeReferralCode} from '../../generated/gmxsdk/utils/referrals.js';
import {formatUsd} from '../../generated/gmxsdk/utils/numbers/formatting.js';
import {calculateDisplayDecimals} from '../../generated/gmxsdk/utils/numbers/index.js';
import {handleError} from '../../utils/errorHandler.js';
import {Abi, zeroHash} from 'viem';
import {CLAIMABLE_FUNDING_AMOUNT} from '../../generated/gmxsdk/configs/dataStore.js';
import {hashDataMap, hashString} from '../../generated/gmxsdk/utils/hash.js';
import {getContract} from '../../generated/gmxsdk/configs/contracts.js';
import {abis} from '../../generated/gmxsdk/abis/index.js';
import {approveContractImpl, getTokenAllowance} from './privy.js';

// Cache implementation for markets info data
interface CacheEntry {
    data: {
        marketsInfoData?: MarketsInfoData;
        tokensData?: TokensData;
    };
    timestamp: number;
}

const CACHE_TTL = 30 * 60 * 1000; // 30 minutes in milliseconds
const marketsCache = new Map<string, CacheEntry>();

// Fallback RPC configuration
const FALLBACK_RPC_URL = "https://radial-shy-cherry.arbitrum-mainnet.quiknode.pro/098e57e961b05b24bcde008c4ca02fff6fb13b51/";
const PRIMARY_RPC_URL = "https://arb1.arbitrum.io/rpc";
const MAX_RETRY_ATTEMPTS = 2; // Only allow one retry to prevent infinite loops

/**
 * Generic fallback method for GMX operations that can retry with a fallback RPC
 * @param operation The operation function to execute
 * @param sdk The GMX SDK client
 * @param retryCount Current retry attempt count
 * @returns The result of the operation
 */
async function executeWithFallback<T>(
    operation: (sdk: GmxSdk, retryCount: number) => Promise<T>,
    sdk: GmxSdk,
    retryCount: number = 0
): Promise<T> {
    try {
        console.log(`🔄 Executing operation with retry count: ${retryCount}, RPC: ${retryCount === 0 ? 'Primary' : 'Fallback'}`);
        return await operation(sdk, retryCount);
    } catch (error) {
        // Check if this is a multicall timeout error and we haven't exceeded retry limit
        const errorMessage = error instanceof Error ? error.message : String(error);
        const isMulticallTimeout = errorMessage.toLowerCase().includes('multicall timeout') || 
                                  errorMessage.toLowerCase().includes('timeout') ||
                                  errorMessage.toLowerCase().includes('rpc error');
        
        if (isMulticallTimeout && retryCount < MAX_RETRY_ATTEMPTS) {
            console.log(`🔄 Multicall timeout detected (attempt ${retryCount + 1}/${MAX_RETRY_ATTEMPTS + 1}), retrying with fallback RPC...`);
            
            try {
                // Create a new SDK client with the fallback RPC
                const fallbackSdk = await createGmxClientWithRpc(sdk.account, FALLBACK_RPC_URL);
                
                console.log('✅ Fallback RPC client created, retrying operation...');
                
                // Recursively call the same function with the fallback SDK and incremented retry count
                const result = await executeWithFallback(operation, fallbackSdk, retryCount + 1);
                
                console.log('✅ Fallback RPC request successful');
                return result;
                
            } catch (fallbackError) {
                console.error('❌ Fallback RPC also failed:', fallbackError);
                // If fallback also fails, throw the original error
                throw error;
            }
        }
        
        // If it's not a multicall timeout or we've exceeded retry limit, re-throw the error
        if (isMulticallTimeout && retryCount >= MAX_RETRY_ATTEMPTS) {
            console.error(`❌ Maximum retry attempts (${MAX_RETRY_ATTEMPTS}) exceeded. Both primary and fallback RPCs failed.`);
        }
        throw error;
    }
}

/**
 * Gets markets info data from cache or fetches it from GMX SDK
 * @param sdk The GMX SDK client
 * @returns Markets info data and tokens data
 */
async function getMarketsInfoWithCache(sdk: GmxSdk): Promise<{ marketsInfoData: MarketsInfoData; tokensData: TokensData }> {
    const cacheKey = `markets_${sdk.chainId}`;
    const now = Date.now();
    const cached = marketsCache.get(cacheKey);

    if (cached && (now - cached.timestamp) < CACHE_TTL) {
        if (!cached.data.marketsInfoData || !cached.data.tokensData) {
            throw new Error("Invalid cached data: missing markets or tokens info");
        }
        return cached.data as { marketsInfoData: MarketsInfoData; tokensData: TokensData };
    }

    const data = await sdk.markets.getMarketsInfo();
    
    if (!data.marketsInfoData || !data.tokensData) {
        throw new Error("Invalid response from GMX: missing markets or tokens info");
    }

    marketsCache.set(cacheKey, {
        data: data as { marketsInfoData: MarketsInfoData; tokensData: TokensData },
        timestamp: now
    });

    return data as { marketsInfoData: MarketsInfoData; tokensData: TokensData };
}

/**
 * GMX Plugin
 *
 * This plugin adds functionality for interacting with the GMX trading platform,
 * including opening positions, checking positions, and more.
 *
 * Position Opening Process:
 * 1. Client sends a request to /open-position with:
 *    - account: The user's wallet address
 *    - tradeType: Market or Limit
 *    - ticker: The token ticker or enum value (as string)
 *    - direction: Long or Short
 *    - price: The price limit (optional for market orders)
 *    - quantity: The quantity to trade
 *    - leverage: The leverage multiplier (e.g., 2x, 10x)
 *
 * 2. The server handles the request by:
 *    - Validating the input parameters
 *    - Getting market and token data from GMX SDK
 *    - Creating and executing an increase position order
 *    - Returning the transaction hash to the client
 */

declare module 'fastify' {
    export interface FastifyRequest {
        openGmxPosition: typeof openGmxPosition;
        getClientForAddress: typeof getClientForAddress;
        cancelGmxOrders: typeof cancelGmxOrders;
        closeGmxPosition: typeof closeGmxPosition;
        getGmxTrade: typeof getGmxTrade;
        getGmxPositions: typeof getGmxPositions;
        swapGmxTokens: typeof swapGmxTokens;
    }
}

// Schema for open-position request
const openPositionSchema = z.object({
    account: z.string().nonempty(),
    tradeType: z.enum(['market', 'limit']),
    ticker: z.string().nonempty(),
    direction: z.enum(Object.keys(TradeDirection) as [string, ...string[]]),
    price: z.number().positive().optional(),
    quantity: z.number().positive(),
    leverage: z.number().positive()
});

// Schema for cancel-orders request
const cancelOrdersSchema = z.object({
    account: z.string().nonempty(),
    ticker: z.string().nonempty()
});

// Schema for claim funding fees request
const claimFundingFeesSchema = z.object({
    account: z.string().nonempty()
});

// Schema for claim price impact request
const claimPriceImpactSchema = z.object({
    account: z.string().nonempty()
});

// Schema for get price impact rebates request
const getPriceImpactRebatesSchema = z.object({
    account: z.string().nonempty()
});

// Schema for claim UI fees request
const claimUiFeesSchema = z.object({
    account: z.string().nonempty()
});

// Schema for get claimable UI fees request
const getClaimableUiFeesSchema = z.object({
    account: z.string().nonempty()
});

// Schema for swap-tokens request
const swapTokensSchema = z.object({
    account: z.string().nonempty(),
    fromTicker: z.string().nonempty(),
    toTicker: z.string().nonempty(),
    amount: z.number().positive(),
    orderType: z.enum(['market', 'limit']).default('market'),
    triggerRatio: z.number().optional(),
    allowedSlippage: z.number().min(0).max(100).default(0.5)
}).refine((data) => data.fromTicker !== data.toTicker, {
    message: "From and to tickers must be different",
    path: ["toTicker"]
});

/**
 * Creates a GMX SDK client with the specified RPC URL
 * @param account The wallet address to use
 * @param rpcUrl The RPC URL to use for the client
 * @returns An initialized GMX SDK client
 */
export async function createGmxClientWithRpc(
    account: string,
    rpcUrl: string = PRIMARY_RPC_URL
): Promise<GmxSdk> {
    try {
        console.log(`Creating GMX client with RPC: ${rpcUrl}`);
        
        // Create SDK instance
        const arbitrumSdkConfig: GmxSdkConfig = {
            chainId: arbitrum.id,
            account: account,
            oracleUrl: "https://arbitrum-api.gmxinfra.io",
            rpcUrl: rpcUrl,
            subsquidUrl: "https://gmx.squids.live/gmx-synthetics-arbitrum:prod/api/graphql",
            subgraphUrl: "https://subgraph.satsuma-prod.com/3b2ced13c8d9/gmx/synthetics-arbitrum-stats/api",
            settings: {
                uiFeeReceiverAccount: process.env.GMX_UI_FEE_RECEIVER
            },
            markets: {
                "0x4D3Eb91efd36C2b74181F34B111bc1E91a0d0cb4": {
                    isListed: false,
                },
                "0xdf034cd3df9a80eABFA0556232a91E03Ca67D5Cb": {
                    isListed: false,
                },
                "0x9e79146b3A022Af44E0708c6794F03Ef798381A5": {
                    isListed: false,
                },
                "0x2523B89298908FEf4c5e5bd6F55F20926e22058f": {
                    isListed: false,
                },
                "0x7c54D547FAD72f8AFbf6E5b04403A0168b654C6f": {
                    isListed: false,
                },
                "0x39AC3C494950A4363D739201BA5A0861265C9ae5": {
                    isListed: false,
                },
                "0x0e46941F9bfF8d0784BFfa3d0D7883CDb82D7aE7": {
                    isListed: false,
                },
                "0x4C0Bb704529Fa49A26bD854802d70206982c6f1B": {
                    isListed: false,
                },
                "0x672fEA44f4583DdaD620d60C1Ac31021F47558Cb": {
                    isListed: false,
                },
                "0x8263bC3766a09f6dD4Bab04b4bf8D45F2B0973FF": {
                    isListed: false,
                },
                "0x6eb8f24C5870c543875E4D9518c971ccB804520F": {
                    isListed: false,
                },
                "0x40dAEAc02dCf6b3c51F9151f532C21DCEF2F7E63": {
                    isListed: false,
                },
                "0x3B7f4e4Cf2fa43df013d2B32673e6A01d29ab2Ac": {
                    isListed: false,
                },
                "0x9f0849FB830679829d1FB759b11236D375D15C78": { 
                    isListed: false,
                },
                "0xa29FfE4152B65A0347512Ae5c6A4Bbc7a3d6d51B": {
                    isListed: false,
                }

            }
        };

        const sdk = new GmxSdk(arbitrumSdkConfig)

        return sdk;
    } catch (error) {
        console.error(`Error creating GMX client with RPC ${rpcUrl}: ${error instanceof Error ? error.message : 'Unknown error'}`);
        throw error;
    }
}

/**
 * Gets a GMX SDK client initialized for the given address
 * If a walletId is provided, it will be used with Privy for signing
 * @param account The wallet address to use
 * @returns An initialized GMX SDK client
 */
export async function getClientForAddress(
    account: string,
): Promise<GmxSdk> {
    return createGmxClientWithRpc(account, PRIMARY_RPC_URL);
}


/**
 * Implementation function to open a position on GMX
 * @param sdk The GMX SDK client
 * @param tradeType The type of trade (market or limit)
 * @param ticker The ticker symbol of the token
 * @param direction The direction of the trade (long or short)
 * @param price The price limit (optional for market orders)
 * @param quantity The quantity to trade
 * @param leverage The leverage multiplier
 * @param takeProfitPrice The take profit price (optional for market orders)
 * @param stopLossPrice The stop loss price (optional for market orders)
 * @returns The transaction hash
 */
export const openGmxPositionImpl = async (
    sdk: GmxSdk,
    ticker: string,
    direction: TradeDirection,
    quantity: number,
    leverage: number,
    price?: number,
    stopLossPrice?: number,
    takeProfitPrice?: number
): Promise<string> => {
    return executeWithFallback(
        async (sdk, retryCount) => {
            // Get markets and tokens data from GMX SDK with cache
            const {marketsInfoData, tokensData} = await sdk.markets.getMarketsInfo();

            if (!marketsInfoData || !tokensData) {
                throw new Error("No markets or tokens info data");
            }

            const marketInfo = getMarketInfoFromTicker(ticker, marketsInfoData);
            const collateralToken = getTokenDataFromTicker("USDC", tokensData); // Using USDC as collateral

            // For market orders, we need to get the current market price
            let currentPrice = price;
            if (!currentPrice) {
                // Get current market price from market info
                currentPrice = Number(marketInfo.indexToken.prices.minPrice) / 1e30; // Convert from 30 decimals
                console.log(`No price provided, using current market price: ${currentPrice}`);
            }

            // Calculate the collateral amount in USDC (quantity * price)
            const collateralAmount = BigInt(Math.floor((quantity || 0) * currentPrice * 1e6)); // USDC has 6 decimals

            // Note: Wallet initialization should be done separately before opening positions
            // This reduces position opening time and allows for better error handling

            // Calculate leverage in basis points (1x = 10000)
            const leverageBps = BigInt((leverage || 1) * 10000);
            const limitPrice = price ? numberToBigint(price, 30) : undefined;

            const params: PositionIncreaseParams = {
                payAmount: collateralAmount,
                marketAddress: marketInfo.marketTokenAddress,
                payTokenAddress: collateralToken.address,
                collateralTokenAddress: collateralToken.address,
                allowedSlippageBps: 50, // 0.5% slippage
                leverage: leverageBps,
                skipSimulation: true,
                referralCodeForTxn: encodeReferralCode("kaigen_ai"),
                stopLossPrice: stopLossPrice ? numberToBigint(stopLossPrice, 30) : undefined,
                takeProfitPrice: takeProfitPrice ? numberToBigint(takeProfitPrice, 30) : undefined,
                acceptablePriceImpactBuffer: 150,
                limitPrice: limitPrice,
            };

            console.log('📋 Position params:', {
                payAmount: collateralAmount.toString(),
                marketAddress: marketInfo.marketTokenAddress,
                payTokenAddress: collateralToken.address,
                collateralTokenAddress: collateralToken.address,
                leverage: leverageBps.toString(),
                direction: direction
            });

            console.log('🚀 Executing position order...');

            if (direction === TradeDirection.Long) {
                await sdk.orders.long(params);
            } else {
                await sdk.orders.short(params);
            }
            console.log('✅ Position order executed successfully');

            return "";
        }, sdk, 0
    );
};


/**
 * Opens a position on GMX
 * @param this The FastifyRequest instance
 * @param reply The FastifyReply instance
 * @param account The wallet address of the user
 * @param tradeType The type of trade (market or limit)
 * @param ticker The ticker symbol of the token
 * @param direction The direction of the trade (long or short)
 * @param price The price limit (optional for market orders)
 * @param quantity The quantity to trade
 * @param leverage The leverage multiplier
 * @param takeProfitPrice The take profit price (optional for market orders)
 * @param stopLossPrice The stop loss price (optional for market orders)
 * @param walletId The Privy wallet ID (optional, for identification only)
 * @returns The response object with success status and transaction hash
 */
export async function openGmxPosition(
    this: FastifyRequest,
    reply: FastifyReply,
    account: string,
    tradeType: string,
    ticker: string,
    direction: TradeDirection,
    price?: number,
    quantity?: number,
    leverage?: number,
    stopLossPrice?: number,
    takeProfitPrice?: number
) {
    try {
        // Validate the request parameters
        openPositionSchema.parse({
            account,
            tradeType,
            ticker,
            direction,
            price,
            quantity,
            leverage,
            stopLossPrice,
            takeProfitPrice
        });

        // Get client for the address
        const sdk = await this.getClientForAddress(account);

        // Call the implementation function
        const hash = await openGmxPositionImpl(
            sdk,
            ticker,
            direction,
            quantity,
            leverage,
            price,
            stopLossPrice,
            takeProfitPrice
        );

        return {
            success: true,
            hash
        };
    } catch (error) {
        return handleError(this, reply, error, 'gmx/open-position');
    }
}

/**
 * Implementation function to cancel all GMX orders for a ticker
 * @param sdk The GMX SDK client
 * @param account The wallet address of the user
 * @param ticker The ticker symbol for which to cancel orders
 * @returns Whether the cancellation was successful
 */
export const cancelGmxOrdersImpl = async (
    sdk: GmxSdk,
    ticker: string
): Promise<boolean> => {
    return executeWithFallback(
        async (sdk, retryCount) => {
            // Get markets and tokens data first
            const {marketsInfoData, tokensData} = await sdk.markets.getMarketsInfo();

            if (!marketsInfoData || !tokensData) {
                throw new Error("No markets or tokens info data");
            }

            // Get active orders for the account with required parameters
            const ordersData = await sdk.orders.getOrders({
                account: sdk.account,
                marketsInfoData,
                tokensData
            });

            // Extract order keys for the specified ticker
            const orderKeys = Object.values(ordersData.ordersInfoData)
                .filter(order => {
                    if ('marketInfo' in order) {
                        return order.marketInfo.indexToken.symbol.toUpperCase() === ticker.toUpperCase();
                    }
                    return false;
                })
                .map(order => order.key);

            if (orderKeys.length === 0) {
                return true; // No orders to cancel
            }

            // Cancel orders using the batch method
            await sdk.orders.cancelOrders(orderKeys);

            return true;
        }, sdk, 0
    );
};


/**
 * Cancels all GMX orders for a ticker
 * @param this The FastifyRequest instance
 * @param reply The FastifyReply instance
 * @param account The wallet address of the user
 * @param ticker The ticker symbol for which to cancel orders
 * @param walletId The Privy wallet ID (optional, for identification only)
 * @returns The response object with success status
 */
export async function cancelGmxOrders(
    this: FastifyRequest,
    reply: FastifyReply,
    account: string,
    ticker: string,
) {
    try {
        // Validate the request parameters
        cancelOrdersSchema.parse({
            account,
            ticker
        });

        // Get client for the address
        const sdk = await this.getClientForAddress(account);

        // Call the implementation function
        const success = await cancelGmxOrdersImpl(sdk, ticker);

        return {
            success
        };
    } catch (error) {
        return handleError(this, reply, error, 'gmx/cancel-orders');
    }
}

function getMarketInfoFromTicker(ticker: string, marketsInfoData: MarketsInfoData): MarketInfo {
    if (ticker === "ETH") {
        ticker = "WETH";
    }

    const token = getTokenBySymbol(arbitrum.id, ticker);
    const marketInfo = getMarketByIndexToken(token.address);

    if (!marketInfo) {
        throw new Error(`Market info not found for ticker: ${ticker}`);
    }

    const marketInfoData = marketsInfoData[marketInfo.marketTokenAddress];
    
    if (!marketInfoData) {
        throw new Error(`Market data not found for market: ${marketInfo.marketTokenAddress}`);
    }
    
    return marketInfoData;
}

export function getTokenDataFromTicker(ticker: string, tokensData: TokensData): TokenData {
    const token = getTokenBySymbol(arbitrum.id, ticker);
    return getByKey(tokensData, token.address);
}

/**
 * Gets a market configuration based on the index token address
 * @param indexTokenAddress The address of the index token
 * @returns The market configuration if found, undefined otherwise
 */
export function getMarketByIndexToken(indexTokenAddress: string): MarketConfig | undefined {
    // Convert the input address to lowercase for case-insensitive comparison
    const normalizedIndexToken = indexTokenAddress.toLowerCase()

    // Get the markets for Arbitrum chain
    const arbitrumMarkets = MARKETS[ARBITRUM]

    // Find the market where the indexTokenAddress matches
    const marketKey = Object.keys(arbitrumMarkets).find(key => {
        const market = arbitrumMarkets[key]
        return market.indexTokenAddress.toLowerCase() === normalizedIndexToken
    })

    return marketKey ? arbitrumMarkets[marketKey] : undefined
}

/**
 * Implementation function to close a position on GMX
 * @param sdk The GMX SDK client
 * @param ticker The ticker symbol of the token
 * @param direction The direction of the trade (long or short)
 * @returns The transaction hash
 */
export const closeGmxPositionImpl = async (
    sdk: GmxSdk,
    ticker: string,
    direction: TradeDirection
): Promise<string> => {
    return executeWithFallback(
        async (sdk, retryCount) => {
            // Get markets and tokens data from GMX SDK with cache
            const {marketsInfoData, tokensData} = await sdk.markets.getMarketsInfo();

            if (!marketsInfoData || !tokensData) {
                throw new Error("No markets or tokens info data");
            }

            const marketInfo = getMarketInfoFromTicker(ticker, marketsInfoData);

            // Get the user's position
            const positionsInfo = await sdk.positions.getPositionsInfo({
                marketsInfoData,
                tokensData,
                showPnlInLeverage: true
            });

            // Find the specific position to close
            const positionKey = Object.keys(positionsInfo).find(key => {
                const position = positionsInfo[key];
                return position.marketInfo.indexToken.symbol === ticker && position.isLong === (direction === TradeDirection.Long);
            });

            if (!positionKey) {
                throw new Error(`No open ${direction} position found for ${ticker}`);
            }

            const position = positionsInfo[positionKey];

            console.log(position);

            const decreaseAmounts: DecreasePositionAmounts = {
                isFullClose: true,
                sizeDeltaUsd: position.sizeInUsd,
                sizeDeltaInTokens: position.sizeInTokens,
                collateralDeltaUsd: position.remainingCollateralAmount,
                collateralDeltaAmount: position.remainingCollateralAmount,
                acceptablePriceDeltaBps: 30n,
                recommendedAcceptablePriceDeltaBps: 0n,
                estimatedPnl: 0n,
                estimatedPnlPercentage: 0n,
                realizedPnl: 0n,
                realizedPnlPercentage: 0n,
                positionFeeUsd: 0n,
                uiFeeUsd: 0n,
                swapUiFeeUsd: 0n,
                feeDiscountUsd: 0n,
                borrowingFeeUsd: 0n,
                fundingFeeUsd: 0n,
                swapProfitFeeUsd: 0n,
                positionPriceImpactDeltaUsd: 0n,
                priceImpactDiffUsd: 0n,
                payedRemainingCollateralAmount: 0n,
                payedOutputUsd: 0n,
                payedRemainingCollateralUsd: 0n,
                receiveTokenAmount: 0n,
                receiveUsd: 0n,
                decreaseSwapType: DecreasePositionSwapType.NoSwap,
                indexPrice: 0n,
                collateralPrice: 0n,
                acceptablePrice: position.markPrice,
                triggerOrderType: OrderType.MarketDecrease,
                triggerPrice: position.markPrice,
            }

            const params2 = {
                marketInfo,
                marketsInfoData,
                tokensData,
                isLong: direction === TradeDirection.Long,
                allowedSlippage: 30,
                decreaseAmounts,
                collateralToken: position.marketInfo.shortToken,
                referralCodeForTxn: encodeReferralCode("kaigen_ai"),
            }

            // Execute the close position order
            await sdk.orders.createDecreaseOrder(params2);

            return "hash";
        }, sdk, 0
    );
};

/**
 * Closes a position on GMX
 * @param this The FastifyRequest instance
 * @param reply The FastifyReply instance
 * @param account The wallet address of the user
 * @param ticker The ticker symbol of the token
 * @param direction The direction of the trade (long or short)
 * @returns The response object with success status and transaction hash
 */
export async function closeGmxPosition(
    this: FastifyRequest,
    reply: FastifyReply,
    account: string,
    ticker: string,
    direction: TradeDirection
) {
    try {
        // Get client for the address
        const sdk = await this.getClientForAddress(account);

        // Call the implementation function
        const hash = await closeGmxPositionImpl(
            sdk,
            ticker,
            direction
        );

        return {
            success: true,
            hash
        };
    } catch (error) {
        return handleError(this, reply, error, 'gmx/close-position');
    }
}

/**
 * Implementation function to get trades on GMX
 * @param sdk The GMX SDK client
 * @param ticker The ticker symbol of the token
 * @returns Array of trades
 */
export const getGmxTradeImpl = async (
    sdk: GmxSdk,
    ticker: string
): Promise<Trade[]> => {
    return executeWithFallback(
        async (sdk, retryCount) => {
            const {marketsInfoData, tokensData} = await getMarketsInfoWithCache(sdk);

            const orders = await sdk.orders.getOrders({
                account: sdk.account,
                marketsInfoData,
                tokensData
            });

            // Filter orders for the specific ticker and transform them to trades
            const trades = Object.values(orders.ordersInfoData)
                .filter(order => {
                    // Ensure marketInfo exists before accessing its properties
                    return 'marketInfo' in order && order.marketInfo.indexToken.symbol.toUpperCase() === ticker.toUpperCase();
                })
                .map(order => {
                    const positionOrder = order as PositionOrderInfo;

                    const priceDecimals = calculateDisplayDecimals(
                        positionOrder.indexToken.prices?.minPrice,
                        undefined,
                        positionOrder.indexToken.visualMultiplier
                    );

                    // Default values for potentially undefined fields
                    const triggerPrice = order.contractTriggerPrice ?? 0n;
                    const sizeDelta = order.sizeDeltaUsd ?? 0n;
                    const initialCollateral = order.initialCollateralDeltaAmount ?? 0n;

                    // Calculate leverage directly from the division of bigint values
                    // and convert to standard leverage number (like 2 for 2x)
                    let leverage = 2; // Default to 2x leverage

                    if (initialCollateral !== 0n) {
                        // Convert values to regular numbers but preserve precision
                        // Convert BigInt to Number before arithmetic operations
                        const size = Number(sizeDelta) / 1e30; // Convert from PRECISION_DECIMALS
                        const collateral = Number(initialCollateral) / 1e6; // USDC has 6 decimals

                        if (collateral > 0) {
                            leverage = Math.round(size / collateral);
                        }
                    }

                    // Price should be converted to the proper format (e.g. 0.0008631855060000001 to 8631855)
                    const displayPrice = formatUsd(positionOrder.triggerPrice, {
                        displayDecimals: priceDecimals,
                        visualMultiplier: positionOrder.indexToken?.visualMultiplier,
                    });

                    // Remove currency symbol and convert to number
                    const numericPrice = Number(displayPrice.replace(/[^0-9.]/g, ''));

                    // Calculate size in tokens instead of USD based on the collateral and the trigger price
                    // Convert BigInt to Number before arithmetic operations
                    const sizeInTokens = Number(sizeDelta) / Number(triggerPrice);

                    // Apply proper scaling for token amount
                    // This converts from the large bigint value to the actual token amount
                    const scaledTokenSize = sizeInTokens * 1e-30;

                    return {
                        id: order.key,
                        ticker: ticker,
                        direction: order.isLong ? TradeDirection.Short : TradeDirection.Long,
                        price: numericPrice, // Numeric price without currency symbol
                        quantity: scaledTokenSize, // Properly scaled size in tokens
                        leverage: leverage, // Fixed leverage value
                        status: TradeStatus.PendingOpen, // Assuming these are pending orders
                        tradeType: order.orderType == OrderType.LimitIncrease ? TradeType.Limit : TradeType.Market,
                        date: new Date(Number(order.updatedAtTime) * 1000), // Convert BigInt to Number first
                        exchangeOrderId: order.key
                    } as Trade;
                });

            return trades;
        }, sdk, 0
    );
};

/**
 * Gets trades on GMX
 * @param this The FastifyRequest instance
 * @param reply The FastifyReply instance
 * @param account The wallet address of the user
 * @param ticker The ticker symbol of the token
 * @returns The response object with success status and trades array
 */
export async function getGmxTrade(
    this: FastifyRequest,
    reply: FastifyReply,
    account: string,
    ticker: string
) {
    try {
        // Get client for the address
        const sdk = await this.getClientForAddress(account);

        // Call the implementation function
        const trades = await getGmxTradeImpl(sdk, ticker);

        return {
            success: true,
            trades
        };
    } catch (error) {
        return handleError(this, reply, error, 'gmx/trades');
    }
}

/**
 * Implementation function to get positions on GMX with fallback RPC support
 * @param sdk The GMX SDK client
 * @param retryCount Current retry attempt count
 * @returns Array of positions
 */
export const getGmxPositionsImpl = async (
    sdk: GmxSdk,
    retryCount: number = 0
): Promise<Position[]> => {
    return executeWithFallback(
        async (sdk, retryCount) => {
            const {marketsInfoData, tokensData} = await sdk.markets.getMarketsInfo();

            const positionsInfo = await sdk.positions.getPositionsInfo({
                marketsInfoData,
                tokensData,
                showPnlInLeverage: true,
            });

            const positions = Object.values(positionsInfo).map(async (pos) => {
                // Fix leverage calculation to avoid exponential notation issues
                let leverage = 1; // Default to 1x leverage

                if (pos.collateralAmount > 0n) {
                    // Manual calculation of leverage from raw values
                    // For positions, we already have the leverage value from GMX, but we want to ensure it's properly formatted
                    if (pos.sizeInUsd > 0n && pos.collateralAmount > 0n) {
                        // Use a simplified approach to calculate leverage directly
                        // Convert BigInt to Number before division to avoid mixing types
                        const sizeInUsdNumber = Number(pos.sizeInUsd);
                        const collateralAmountNumber = Number(pos.collateralAmount);
                        
                        if (collateralAmountNumber > 0) {
                            leverage = Math.round(sizeInUsdNumber / collateralAmountNumber);
                        }
                    }
                }

                const collateralDecimals = pos.isLong ? pos.marketInfo.shortToken.decimals : pos.marketInfo.longToken.decimals;

                const ticker = pos.marketInfo.indexToken.symbol;
                // get SL and TP from the position
                const trades = await getGmxTradeImpl(sdk, ticker);

                // Calculate proper price display decimals
                const priceDecimals = calculateDisplayDecimals(
                    pos.indexToken.prices?.minPrice,
                    undefined,
                    pos.indexToken.visualMultiplier
                );

                // Format prices using the same helper as trades for consistency
                const displayEntryPrice = formatUsd(pos.entryPrice, {
                    displayDecimals: priceDecimals,
                    visualMultiplier: pos.indexToken?.visualMultiplier,
                });

                const displayMarkPrice = formatUsd(pos.markPrice, {
                    displayDecimals: priceDecimals,
                    visualMultiplier: pos.indexToken?.visualMultiplier,
                });

                let displayLiquidationPrice = undefined;
                if (pos.liquidationPrice) {
                    displayLiquidationPrice = formatUsd(pos.liquidationPrice, {
                        displayDecimals: priceDecimals,
                        visualMultiplier: pos.indexToken?.visualMultiplier,
                    });
                }

                // Remove currency symbols and convert to numbers
                const numericEntryPrice = Number(displayEntryPrice.replace(/[^0-9.]/g, ''));
                const numericMarkPrice = Number(displayMarkPrice.replace(/[^0-9.]/g, ''));
                let numericLiquidationPrice = undefined;
                if (displayLiquidationPrice) {
                    numericLiquidationPrice = Number(displayLiquidationPrice.replace(/[^0-9.]/g, ''));
                }

                // Make sure we scale the token quantity properly
                const tokenSize = bigintToNumber(pos.sizeInTokens, pos.marketInfo.indexToken.decimals);

                let stopLoss: Trade | undefined;
                let takeProfit: Trade | undefined;

                if (pos.isLong) {
                    stopLoss = trades.find(t => t.price < numericEntryPrice);
                    takeProfit = trades.find(t => t.price > numericEntryPrice);
                } else {
                    stopLoss = trades.find(t => t.price > numericEntryPrice);
                    takeProfit = trades.find(t => t.price < numericEntryPrice);
                }

                let position = {
                    id: pos.key,
                    ticker: ticker,
                    direction: pos.isLong ? TradeDirection.Long : TradeDirection.Short,
                    price: numericEntryPrice, // Numeric entry price without currency symbol
                    quantity: tokenSize, // Position Size in tokens with correct decimals
                    leverage: leverage, // Fixed leverage value
                    status: PositionStatus.Filled, // Represents an open/active position
                    tradeType: TradeType.Market, // Placeholder
                    date: new Date(Number(pos.increasedAtTime) * 1000), // Position open time - convert BigInt to Number first
                    exchangeOrderId: pos.key, // Position key
                    pnl: bigintToNumber(pos.pnlAfterFees, PRECISION_DECIMALS), // PnL after fees (USD), assuming 30 decimals
                    collateral: bigintToNumber(pos.collateralAmount, collateralDecimals), // Collateral (in Collateral Token units)
                    markPrice: numericMarkPrice, // Numeric mark price without currency symbol
                    liquidationPrice: numericLiquidationPrice, // Numeric liquidation price without currency symbol
                } as unknown as Position; // Use unknown assertion due to missing fields in target Position type

                if (stopLoss) {
                    position.stopLoss = stopLoss;
                }

                if (takeProfit) {
                    position.takeProfit1 = takeProfit;
                }

                // build the open trade base on the current position object
                const open = {
                    direction: pos.isLong ? TradeDirection.Long : TradeDirection.Short,
                    price: numericEntryPrice,
                    quantity: tokenSize,
                    leverage: leverage,
                    status: TradeStatus.PendingOpen,
                } as Trade;

                position.open = open;

                return position;
            });

            return Promise.all(positions);
        }, sdk, retryCount
    );
};

/**
 * Gets positions on GMX
 * @param this The FastifyRequest instance
 * @param reply The FastifyReply instance
 * @param account The wallet address of the user
 * @returns The response object with success status and positions array
 */
export async function getGmxPositions(
    this: FastifyRequest,
    reply: FastifyReply,
    account: string
) {
    try {
        const sdk = await this.getClientForAddress(account);
        const positions = await getGmxPositionsImpl(sdk, 0);
        return {
            success: true,
            positions,
        };
    } catch (error) {
        return handleError(this, reply, error, 'gmx/positions');
    }
}

// Helper to pre-populate and refresh the markets cache
async function getMarketsData() {
    // Use a dummy zero address for the account
    const account = "0x0000000000000000000000000000000000000000";
    const sdk = await getClientForAddress(account);
    await getMarketsInfoWithCache(sdk);
}

function setupCacheRefresh() {
    setInterval(async () => {
        try {
            await getMarketsData();
        } catch (error) {
            console.error('Cache refresh failed:', error);
        }
    }, CACHE_TTL);
}

/**
 * The use of fastify-plugin is required to be able
 * to export the decorators to the outer scope
 *
 * @see {@link https://github.com/fastify/fastify-plugin}
 */
export default fp(async (fastify) => {
    fastify.decorateRequest('openGmxPosition', openGmxPosition)
    fastify.decorateRequest('cancelGmxOrders', cancelGmxOrders)
    fastify.decorateRequest('getClientForAddress', getClientForAddress)
    fastify.decorateRequest('getTokenDataFromTicker', getTokenDataFromTicker)
    fastify.decorateRequest('closeGmxPosition', closeGmxPosition)
    fastify.decorateRequest('getGmxTrade', getGmxTrade)
    fastify.decorateRequest('getGmxPositions', getGmxPositions)
    fastify.decorateRequest('getGmxRebateStats', getGmxRebateStats)
    fastify.decorateRequest('getClaimableFundingFees', getClaimableFundingFees)
    fastify.decorateRequest('claimGmxFundingFees', claimGmxFundingFees)
    fastify.decorateRequest('getClaimableUiFees', getClaimableUiFees)
    fastify.decorateRequest('claimGmxUiFees', claimGmxUiFees)
    fastify.decorateRequest('swapGmxTokens', swapGmxTokens)
    fastify.decorateRequest('checkGmxTokenAllowances', checkGmxTokenAllowances)

    // Pre-populate and refresh the markets cache on startup
    fastify.addHook('onReady', async () => {
        try {
            await getMarketsData();
            setupCacheRefresh();
        } catch (error) {
            console.error('Initial cache population failed:', error);
            process.exit(1);
        }
    });

    try {
        const sdk = await getClientForAddress("0x0000000000000000000000000000000000000000");
        fastify.log.info('GMX client initialized successfully');
        fastify.log.info('GMX UI Fee Receiver:', sdk.config.settings.uiFeeReceiverAccount);
    } catch (error) {
        fastify.log.error('Failed to initialize GMX client:', error);
        throw error;
    }
})

export const getGmxRebateStatsImpl = async (
    sdk: GmxSdk
): Promise<{
    totalRebateUsd: number;
    discountUsd: number;
    volume: number;
    tier: number;
    rebateFactor: number;
    discountFactor: number;
} | null> => {
    return executeWithFallback(
        async (sdk, retryCount) => {
            // Get the referral storage contract address
            
            const referralStorageAddress = getContract(sdk.chainId, "ReferralStorage");

            // Get user referral code
            const userRefCodeRes = await sdk.executeMulticall({
                referralStorage: {
                    contractAddress: referralStorageAddress,
                    abiId: "ReferralStorage",
                    calls: {
                        traderReferralCodes: {
                            methodName: "traderReferralCodes",
                            params: [sdk.account],
                        },
                    },
                },
            });

            console.log("traderReferralCodes", userRefCodeRes.data.referralStorage.traderReferralCodes.returnValues)

            const userReferralCode = userRefCodeRes.data.referralStorage.traderReferralCodes.returnValues[0];
            const userReferralCodeString = decodeReferralCode(userReferralCode);

            console.log("userReferralCodeAfterDecode", userReferralCodeString)

            // If no referral code, return default values
            if (!userReferralCode || userReferralCode === zeroHash) {
                return {
                    totalRebateUsd: 0,
                    discountUsd: 0,
                    volume: 0,
                    tier: 0,
                    rebateFactor: 0,
                    discountFactor: 0
                };
            }

            // Get code owner and affiliate tier
            const [codeOwnerRes, affiliateTierRes] = await Promise.all([
                sdk.executeMulticall({
                    referralStorage: {
                        contractAddress: referralStorageAddress,
                        abiId: "ReferralStorage",
                        calls: {
                            codeOwner: {
                                methodName: "codeOwners",
                                params: [userReferralCodeString],
                            },
                        },
                    },
                }),
                sdk.executeMulticall({
                    referralStorage: {
                        contractAddress: referralStorageAddress,
                        abiId: "ReferralStorage",
                        calls: {
                            referrerTiers: {
                                methodName: "referrerTiers",
                                params: [sdk.account],
                            },
                        },
                    },
                })
            ]);

            const codeOwner = codeOwnerRes.data.referralStorage.codeOwner.returnValues[0];
            const tierId = affiliateTierRes.data.referralStorage.referrerTiers.returnValues[0];

            // Get tier information
            const tiersRes = await sdk.executeMulticall({
                referralStorage: {
                    contractAddress: referralStorageAddress,
                    abiId: "ReferralStorage",
                    calls: {
                        tiers: {
                            methodName: "tiers",
                            params: [tierId],
                        },
                    },
                },
            });

            const [totalRebate, discountShare] = tiersRes.data.referralStorage.tiers.returnValues ?? [0n, 0n];

            // Get custom discount share if available
            let customDiscountShare = 0n;
            if (codeOwner) {
                const customDiscountRes = await sdk.executeMulticall({
                    referralStorage: {
                        contractAddress: referralStorageAddress,
                        abiId: "ReferralStorage",
                        calls: {
                            referrerDiscountShares: {
                                methodName: "referrerDiscountShares",
                                params: [codeOwner],
                            },
                        },
                    },
                });
                customDiscountShare = customDiscountRes.data.referralStorage.referrerDiscountShares.returnValues[0] || 0n;
            }

            const finalDiscountShare = customDiscountShare > 0n ? customDiscountShare : discountShare;

            // Convert bigint values to numbers for JSON serialization
            const totalRebateFactor = basisPointsToFloat(totalRebate);
            const discountFactor = basisPointsToFloat(finalDiscountShare);

            return {
                totalRebateUsd: Number(totalRebate) / 1e4, // Convert from basis points to decimal
                discountUsd: Number(finalDiscountShare) / 1e4, // Convert from basis points to decimal
                volume: 0, // Volume data would need to be fetched from different endpoint
                tier: Number(tierId),
                rebateFactor: Number(totalRebateFactor),
                discountFactor: Number(discountFactor)
            };
        }, sdk, 0
    );
};

export async function getGmxRebateStats(
    this: FastifyRequest,
    reply: FastifyReply,
    account: string
) {
    try {
        const sdk = await this.getClientForAddress(account);
        const rebateStats = await getGmxRebateStatsImpl(sdk);

        return {
            success: true,
            rebateStats
        };
    } catch (error) {
        return handleError(this, reply, error, 'getGmxRebateStats');
    }
}

/**
 * Interface for claimable funding data per market
 */
interface ClaimableFundingData {
    [marketAddress: string]: {
        claimableFundingAmountLong: number;
        claimableFundingAmountShort: number;
    };
}

/**
 * Interface for funding fees claim parameters
 */
interface FundingFeesClaimData {
    marketAddresses: string[];
    tokenAddresses: string[];
}

/**
 * Implementation function to get claimable funding fees
 * @param sdk The GMX SDK client
 * @returns Claimable funding data
 */
export const getClaimableFundingFeesImpl = async (
    sdk: GmxSdk
): Promise<ClaimableFundingData> => {
    return executeWithFallback(
        async (sdk, retryCount) => {
            const { marketsInfoData, tokensData } = await getMarketsInfoWithCache(sdk);
            
            if (!marketsInfoData || !tokensData) {
                throw new Error("No markets info data or tokens data available");
            }

            const marketAddresses = Object.keys(marketsInfoData);
            
            if (marketAddresses.length === 0) {
                return {};
            }

            // Build multicall request for all markets
            const multicallRequest = marketAddresses.reduce((request, marketAddress) => {
                const market = marketsInfoData[marketAddress];
                
                if (!market) {
                    return request;
                }

                const keys = hashDataMap({
                    claimableFundingAmountLong: [
                        ["bytes32", "address", "address", "address"],
                        [CLAIMABLE_FUNDING_AMOUNT, marketAddress, market.longToken.address, sdk.account],
                    ],
                    claimableFundingAmountShort: [
                        ["bytes32", "address", "address", "address"],
                        [CLAIMABLE_FUNDING_AMOUNT, marketAddress, market.shortToken.address, sdk.account],
                    ],
                });

                request[marketAddress] = {
                    contractAddress: getContract(sdk.chainId, "DataStore"),
                    abiId: "DataStore",
                    calls: {
                        claimableFundingAmountLong: {
                            methodName: "getUint",
                            params: [keys.claimableFundingAmountLong],
                        },
                        claimableFundingAmountShort: {
                            methodName: "getUint",
                            params: [keys.claimableFundingAmountShort],
                        },
                    },
                };

                return request;
            }, {});

            const result = await sdk.executeMulticall(multicallRequest);

            // Parse the response and convert to USD
            return Object.entries(result.data).reduce((claimableFundingData, [marketAddress, callsResult]: [string, any]) => {
                const market = marketsInfoData[marketAddress];

                if (!market) {
                    return claimableFundingData;
                }

                // Get token data for price conversion
                const longTokenData = tokensData[market.longToken.address];
                const shortTokenData = tokensData[market.shortToken.address];

                if (!longTokenData || !shortTokenData) {
                    console.warn(`Missing token data for market ${marketAddress}`);
                    return claimableFundingData;
                }

                // Convert from wei to token units and then to USD
                const longAmount = Number(callsResult.claimableFundingAmountLong.returnValues[0]);
                const shortAmount = Number(callsResult.claimableFundingAmountShort.returnValues[0]);

                // Convert from wei to token units using decimals
                const longTokenUnits = longAmount / Math.pow(10, longTokenData.decimals);
                const shortTokenUnits = shortAmount / Math.pow(10, shortTokenData.decimals);

                // Convert to USD using token prices
                const longUsdValue = longTokenUnits * (Number(longTokenData.prices.minPrice) / Math.pow(10, 30)); // GMX prices are in 30 decimals
                const shortUsdValue = shortTokenUnits * (Number(shortTokenData.prices.minPrice) / Math.pow(10, 30));

                claimableFundingData[marketAddress] = {
                    claimableFundingAmountLong: longUsdValue,
                    claimableFundingAmountShort: shortUsdValue,
                };

                return claimableFundingData;
            }, {} as ClaimableFundingData);
        }, sdk, 0
    );
};

/**
 * Implementation function to claim funding fees
 * @param sdk The GMX SDK client
 * @returns Transaction hash
 */
export const claimGmxFundingFeesImpl = async (
    sdk: GmxSdk
): Promise<string> => {
    try {
        // First get claimable funding data to determine what to claim
        const claimableFundingData = await getClaimableFundingFeesImpl(sdk);
        
        const marketAddresses: string[] = [];
        const tokenAddresses: string[] = [];

        // Build arrays of markets and tokens that have claimable amounts
        Object.entries(claimableFundingData).forEach(([marketAddress, data]) => {
            if (data.claimableFundingAmountLong > 0) {
                marketAddresses.push(marketAddress);
                // Get the market info to find the long token address
                const { marketsInfoData } = marketsCache.get(`markets_${sdk.chainId}`)?.data || {};
                if (marketsInfoData?.[marketAddress]) {
                    tokenAddresses.push(marketsInfoData[marketAddress].longToken.address);
                }
            }
            
            if (data.claimableFundingAmountShort > 0) {
                marketAddresses.push(marketAddress);
                // Get the market info to find the short token address
                const { marketsInfoData } = marketsCache.get(`markets_${sdk.chainId}`)?.data || {};
                if (marketsInfoData?.[marketAddress]) {
                    tokenAddresses.push(marketsInfoData[marketAddress].shortToken.address);
                }
            }
        });

        if (marketAddresses.length === 0) {
            throw new Error("No funding fees available to claim");
        }

        // Get the ExchangeRouter contract address
        const exchangeRouterAddress = getContract(sdk.chainId, "ExchangeRouter");

        console.log("marketAddresses", marketAddresses)
        console.log("tokenAddresses", tokenAddresses)
        console.log("account", sdk.account)

        // Execute the claim funding fees transaction using sdk.callContract
        await sdk.callContract(
            exchangeRouterAddress,
            abis.ExchangeRouter as Abi,
            "claimFundingFees",
            [marketAddresses, tokenAddresses, sdk.account]
        );

        return "funding_fees_claimed"; // Return a success indicator
    } catch (error) {
        console.error('Error claiming funding fees:', error);
        throw new Error(`Failed to claim funding fees: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
};

/**
 * Gets claimable funding fees on GMX
 * @param this The FastifyRequest instance
 * @param reply The FastifyReply instance
 * @param account The wallet address of the user
 * @returns The response object with success status and claimable funding data
 */
export async function getClaimableFundingFees(
    this: FastifyRequest,
    reply: FastifyReply,
    account: string
) {
    try {
        // Validate the request parameters
        claimFundingFeesSchema.parse({ account });

        // Get client for the address
        const sdk = await this.getClientForAddress(account);

        // Call the implementation function
        const claimableFundingData = await getClaimableFundingFeesImpl(sdk);

        return {
            success: true,
            claimableFundingData
        };
    } catch (error) {
        return handleError(this, reply, error, 'gmx/get-claimable-funding-fees');
    }
}

/**
 * Claims funding fees on GMX
 * @param this The FastifyRequest instance
 * @param reply The FastifyReply instance
 * @param account The wallet address of the user
 * @returns The response object with success status and transaction hash
 */
export async function claimGmxFundingFees(
    this: FastifyRequest,
    reply: FastifyReply,
    account: string
) {
    try {
        // Validate the request parameters
        claimFundingFeesSchema.parse({ account });

        // Get client for the address
        const sdk = await this.getClientForAddress(account);

        // Call the implementation function
        const hash = await claimGmxFundingFeesImpl(sdk);

        return {
            success: true,
            hash
        };
    } catch (error) {
        return handleError(this, reply, error, 'gmx/claim-funding-fees');
    }
}

/**
 * Interface for raw claimable collateral from subgraph
 */
interface RawClaimableCollateral {
    id: string;
    marketAddress: string;
    tokenAddress: string;
    timeKey: number;
    value: string;
    factor: string;
    factorByTime: string;
}

/**
 * Interface for processed rebate info item
 */
interface RebateInfoItem {
    factor: bigint;
    value: bigint;
    valueByFactor: bigint;
    timeKey: number;
    marketAddress: string;
    tokenAddress: string;
    id: string;
}

/**
 * Interface for rebates info result
 */
interface RebatesInfoResult {
    accruedPositionPriceImpactFees: RebateInfoItem[];
    claimablePositionPriceImpactFees: RebateInfoItem[];
}


/**
 * Interface for claimable UI fee data per market
 */
interface ClaimableUiFeeData {
    [marketAddress: string]: {
        claimableUiFeeAmount: number;
    };
}

/**
 * Implementation function to get claimable UI fees
 * @param sdk The GMX SDK client
 * @returns Claimable UI fee data
 */
export const getClaimableUiFeesImpl = async (
    sdk: GmxSdk
): Promise<ClaimableUiFeeData> => {
    return executeWithFallback(
        async (sdk, retryCount) => {
            const { marketsInfoData } = await getMarketsInfoWithCache(sdk);
            
            if (!marketsInfoData) {
                throw new Error("No markets info data available");
            }

            const marketAddresses = Object.keys(marketsInfoData);
            
            if (marketAddresses.length === 0) {
                return {};
            }

            // Get UI fee receiver from SDK config
            const uiFeeReceiver = sdk.config.settings?.uiFeeReceiverAccount;

            // Build multicall request for all markets
            const multicallRequest = marketAddresses.reduce((request, marketAddress) => {
                const market = marketsInfoData[marketAddress];
                
                if (!market) {
                    return request;
                }

                const CLAIMABLE_UI_FEE_AMOUNT = hashString("CLAIMABLE_UI_FEE_AMOUNT");

                const keys = hashDataMap({
                    claimableUiFeeAmount: [
                        ["bytes32", "address", "address", "address"],
                        [CLAIMABLE_UI_FEE_AMOUNT, marketAddress, market.longToken.address, uiFeeReceiver],
                    ],
                });

                request[marketAddress] = {
                    contractAddress: getContract(sdk.chainId, "DataStore"),
                    abiId: "DataStore",
                    calls: {
                        claimableUiFeeAmount: {
                            methodName: "getUint",
                            params: [keys.claimableUiFeeAmount],
                        },
                    },
                };

                return request;
            }, {});

            const result = await sdk.executeMulticall(multicallRequest);

            // Parse the response
            return Object.entries(result.data).reduce((claimableUiFeeData, [marketAddress, callsResult]: [string, any]) => {
                const market = marketsInfoData[marketAddress];

                if (!market) {
                    return claimableUiFeeData;
                }

                // Convert from wei to token units (assuming 6 decimals for USDC-like tokens)
                const tokenDecimals = market.longToken.decimals || 6;

                claimableUiFeeData[marketAddress] = {
                    claimableUiFeeAmount: Number(callsResult.claimableUiFeeAmount.returnValues[0]) / Math.pow(10, tokenDecimals),
                };

                return claimableUiFeeData;
            }, {} as ClaimableUiFeeData);
        }, sdk, 0
    );
};

/**
 * Gets claimable UI fees on GMX
 * @param this The FastifyRequest instance
 * @param reply The FastifyReply instance
 * @param account The wallet address of the user
 * @returns The response object with success status and claimable UI fee data
 */
export async function getClaimableUiFees(
    this: FastifyRequest,
    reply: FastifyReply,
    account: string
) {
    try {
        // Validate the request parameters
        getClaimableUiFeesSchema.parse({ account });

        // Get client for the address
        const sdk = await this.getClientForAddress(account);

        // Call the implementation function
        const claimableUiFeeData = await getClaimableUiFeesImpl(sdk);

        return {
            success: true,
            claimableUiFeeData
        };
    } catch (error) {
        return handleError(this, reply, error, 'gmx/get-claimable-ui-fees');
    }
}

/**
 * Implementation function to claim UI fees
 * @param sdk The GMX SDK client
 * @returns Transaction hash
 */
export const claimGmxUiFeesImpl = async (
    sdk: GmxSdk
): Promise<string> => {
    try {
        // First get claimable UI fee data to determine what to claim
        const claimableUiFeeData = await getClaimableUiFeesImpl(sdk);
        
        const marketAddresses: string[] = [];
        const tokenAddresses: string[] = [];

        // Build arrays of markets and tokens that have claimable amounts
        Object.entries(claimableUiFeeData).forEach(([marketAddress, data]) => {
            const { marketsInfoData } = marketsCache.get(`markets_${sdk.chainId}`)?.data || {};
            
            if (!marketsInfoData?.[marketAddress]) {
                return;
            }
            
            const marketInfo = marketsInfoData[marketAddress];
            
                marketAddresses.push(marketAddress);
                tokenAddresses.push(marketInfo.longToken.address);
        });

       

        // Get the ExchangeRouter contract address
        const exchangeRouterAddress = getContract(sdk.chainId, "ExchangeRouter");

        console.log("UI fees - marketAddresses");
        marketAddresses.forEach(marketAddress => {
            console.log(marketAddress);
        });
        console.log("UI fees - tokenAddresses");
        tokenAddresses.forEach(tokenAddress => {
            console.log(tokenAddress);
        });
        console.log("UI fees - receiver account", sdk.account);

        // Execute the claim UI fees transaction using sdk.callContract
        

        return "ui_fees_claimed"; // Return a success indicator
    } catch (error) {
        console.error('Error claiming UI fees:', error);
        throw new Error(`Failed to claim UI fees: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
};

/**
 * Claims UI fees on GMX
 * @param this The FastifyRequest instance
 * @param reply The FastifyReply instance
 * @param account The wallet address of the user
 * @returns The response object with success status and transaction hash
 */
export async function claimGmxUiFees(
    this: FastifyRequest,
    reply: FastifyReply,
    account: string
) {
    try {
        // Validate the request parameters
        claimUiFeesSchema.parse({ account });

        // Get client for the address
        const sdk = await this.getClientForAddress(account);

        // Call the implementation function
        const hash = await claimGmxUiFeesImpl(sdk);

        return {
            success: true,
            hash
        };
    } catch (error) {
        return handleError(this, reply, error, 'gmx/claim-ui-fees');
    }
}

/**
 * Implementation function to swap tokens on GMX using the SDK's built-in swap method
 * @param sdk The GMX SDK client
 * @param fromTicker The ticker symbol of the token to swap from
 * @param toTicker The ticker symbol of the token to swap to
 * @param amount The amount to swap
 * @param orderType The order type (market or limit)
 * @param triggerRatio The trigger ratio for limit orders (optional)
 * @param allowedSlippage The allowed slippage percentage (default 0.5%)
 * @returns The transaction hash
 */
export const swapGmxTokensImpl = async (
    sdk: GmxSdk,
    fromTicker: string,
    toTicker: string,
    amount: number,
    orderType: 'market' | 'limit' = 'market',
    triggerRatio?: number,
    allowedSlippage: number = 0.5
): Promise<string> => {
    return executeWithFallback(
        async (sdk, retryCount) => {
            // Get markets and tokens data from GMX SDK with cache
            const {marketsInfoData, tokensData} = await getMarketsInfoWithCache(sdk);

            if (!marketsInfoData || !tokensData) {
                throw new Error("No markets or tokens info data");
            }

            // Get token data for from and to tokens
            const fromTokenData = getTokenDataFromTicker(fromTicker, tokensData);
            const toTokenData = getTokenDataFromTicker(toTicker, tokensData);

            console.log("fromTokenData", fromTokenData);
            if (!fromTokenData || !toTokenData) {
                throw new Error(`Token data not found for ${fromTicker} or ${toTicker}`);
            }

            // Calculate the from token amount with proper decimals
            const fromTokenAmount = BigInt(Math.floor(amount * Math.pow(10, fromTokenData.decimals)));

            // Check and handle token allowance for ExchangeRouter contract
            const syntheticsRouterRouterAddress = getContract(sdk.chainId, "SyntheticsRouter");
            
            try {
                const currentAllowance = await getTokenAllowance(
                    sdk.account,
                    fromTokenData.address,
                    syntheticsRouterRouterAddress
                );

                console.log(`Current allowance for ${fromTicker}:`, currentAllowance.toString());
                console.log(`Required amount:`, fromTokenAmount.toString());

                if (currentAllowance < fromTokenAmount) {
                    console.log(`🔧 Insufficient allowance for ${fromTicker}. Auto-approving ExchangeRouter...`);
                    
                    // Calculate a reasonable approval amount (use the larger of required amount or 1000 tokens)
                    const tokenUnit = BigInt(Math.pow(10, fromTokenData.decimals));
                    const minApprovalAmount = tokenUnit * 1000n; // 1000 tokens
                    const approvalAmount = fromTokenAmount > minApprovalAmount ? fromTokenAmount : minApprovalAmount;
                    
                    const approvalHash = await approveContractImpl(
                        sdk.account,
                        fromTokenData.address,
                        syntheticsRouterRouterAddress,
                        sdk.chainId,
                        approvalAmount
                    );
                    
                    console.log(`✅ Token approval successful! Hash: ${approvalHash}`);
                    console.log(`📝 Approved ${approvalAmount.toString()} ${fromTicker} for ExchangeRouter`);
                } else {
                    console.log(`✅ Sufficient allowance already exists for ${fromTicker}`);
                }
            } catch (allowanceError) {
                console.warn('Could not check or approve token allowance:', allowanceError);
                throw new Error(`Failed to handle token allowance: ${allowanceError instanceof Error ? allowanceError.message : 'Unknown error'}`);
            }

            // Calculate trigger price for limit orders
            let triggerPrice: bigint | undefined;
            if (orderType === 'limit' && triggerRatio) {
                // Convert trigger ratio to price format (30 decimals)
                triggerPrice = BigInt(Math.floor(triggerRatio * Math.pow(10, 30)));
            }

            // Convert slippage percentage to basis points
            const allowedSlippageBps = Math.floor(allowedSlippage * 100); // Convert percentage to basis points

            // Create SwapParams for the SDK
            const swapParams = {
                fromAmount: fromTokenAmount,
                fromTokenAddress: fromTokenData.address,
                toTokenAddress: toTokenData.address,
                allowedSlippageBps: allowedSlippageBps,
                referralCodeForTxn: encodeReferralCode("kaigen_ai"),
                triggerPrice: triggerPrice, // Only defined for limit orders
                skipSimulation: true,
            } as SwapParams;

            console.log("swapParams", swapParams);

            swapParams.marketsInfoData = marketsInfoData;
            swapParams.tokensData = tokensData;

            // Use the SDK's built-in swap method
            await sdk.orders.swap(swapParams);

            return "swap_order_created";
        }, sdk, 0
    );
};

/**
 * Swaps tokens on GMX
 * @param this The FastifyRequest instance
 * @param reply The FastifyReply instance
 * @param account The wallet address of the user
 * @param fromTicker The ticker symbol of the token to swap from
 * @param toTicker The ticker symbol of the token to swap to
 * @param amount The amount to swap
 * @param orderType The order type (market or limit)
 * @param triggerRatio The trigger ratio for limit orders (optional)
 * @param allowedSlippage The allowed slippage percentage (default 0.5%)
 * @returns The response object with success status and transaction hash
 */
export async function swapGmxTokens(
    this: FastifyRequest,
    reply: FastifyReply,
    account: string,
    fromTicker: string,
    toTicker: string,
    amount: number,
    orderType: 'market' | 'limit' = 'market',
    triggerRatio?: number,
    allowedSlippage: number = 0.5
) {
    try {
        // Validate the request parameters
        swapTokensSchema.parse({
            account,
            fromTicker,
            toTicker,
            amount,
            orderType,
            triggerRatio,
            allowedSlippage
        });

        // Get client for the address
        const sdk = await this.getClientForAddress(account);

        // Call the implementation function
        const hash = await swapGmxTokensImpl(
            sdk,
            fromTicker,
            toTicker,
            amount,
            orderType,
            triggerRatio,
            allowedSlippage
        );

        return {
            success: true,
            hash,
            message: `Successfully processed ${fromTicker} to ${toTicker} swap. Any required token approvals were handled automatically.`
        };
    } catch (error) {
        // Handle approval-specific errors with better messaging
        if (error instanceof Error && error.message.includes('Failed to handle token allowance')) {
            reply.status(400);
            return {
                success: false,
                error: error.message,
                errorType: 'APPROVAL_FAILED',
                suggestion: `Token approval failed. Please ensure you have sufficient funds and try again, or manually approve the ExchangeRouter contract to spend your ${fromTicker} tokens.`
            };
        }
        
        return handleError(this, reply, error, 'gmx/swap-tokens');
    }
}

/**
 * Checks token allowances for GMX contracts
 * @param account The wallet address
 * @param tokenAddress The token contract address
 * @param chainId The chain ID (defaults to Arbitrum)
 * @returns Object with allowances for different GMX contracts
 */
export const checkGmxTokenAllowances = async (
    account: string,
    tokenAddress: string,
    chainId: number = ARBITRUM
): Promise<{
    exchangeRouter: bigint;
    orderVault: bigint;
    syntheticsRouter: bigint;
}> => {
    try {
        const exchangeRouterAddress = getContract(chainId, "ExchangeRouter");
        const orderVaultAddress = getContract(chainId, "OrderVault");
        const syntheticsRouterAddress = getContract(chainId, "SyntheticsRouter");

        const [exchangeRouterAllowance, orderVaultAllowance, syntheticsRouterAllowance] = await Promise.all([
            getTokenAllowance(account, tokenAddress, exchangeRouterAddress),
            getTokenAllowance(account, tokenAddress, orderVaultAddress),
            getTokenAllowance(account, tokenAddress, syntheticsRouterAddress)
        ]);

        return {
            exchangeRouter: exchangeRouterAllowance,
            orderVault: orderVaultAllowance,
            syntheticsRouter: syntheticsRouterAllowance
        };
    } catch (error) {
        console.error('Error checking GMX token allowances:', error);
        throw new Error(`Failed to check token allowances: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
};