import fp from 'fastify-plugin'
import {FastifyInstance, FastifyReply, FastifyRequest} from 'fastify'
import {z} from 'zod'
import canonicalize from 'canonicalize'
import crypto from 'crypto'
import {PrivyClient} from '@privy-io/server-auth'
import {ethers} from 'ethers'
import dotenv from 'dotenv'
import Token from '../../generated/gmxsdk/abis/Token.json' with {type: 'json'}
import {ARBITRUM} from '../../generated/gmxsdk/configs/chains.js'
import {TOKENS} from '../../generated/gmxsdk/configs/tokens.js'
import {CONTRACTS} from '../../generated/gmxsdk/configs/contracts.js'
import {getClientForAddress, getTokenDataFromTicker} from './gmx.js'
import {Balance, Chain, Ticker} from '../../generated/ManagingApiTypes.js'
import {Address} from 'viem'

// Load environment variables
dotenv.config()

/**
 * Privy Plugin
 * 
 * This plugin adds functionality for interacting with the Privy API,
 * including signing messages and token approvals.
 * 
 * Token Approval Process:
 * 1. Client sends a request to /approve-token with:
 *    - walletId: The user's wallet ID
 *    - address: The user's wallet address
 *    - ticker: The token ticker or enum value (as string)
 *    - amount: The amount to approve (optional, defaults to max amount)
 *    - chainId: The chain ID where the approval will take place
 * 
 * 2. The server handles the request by:
 *    - Validating the input parameters
 *    - Getting the token data based on the ticker
 *    - Creating and sending an ERC20 approval transaction using ethers.js and Privy
 *    - Returning the transaction hash to the client
 */


declare module 'fastify' {
  export interface FastifyRequest {
    signPrivyMessage: typeof signPrivyMessage;
    approveToken: typeof approveToken;
    initAddress: typeof initAddress;
    sendToken: typeof sendToken;
    getWalletBalance: typeof getWalletBalance;
  }
}

/**
 * Returns an initialized PrivyClient instance with configuration from environment variables
 * @returns The configured PrivyClient instance
 */
export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => {
  const appId = fastify?.config?.PRIVY_APP_ID || process.env.PRIVY_APP_ID;
  const appSecret = fastify?.config?.PRIVY_APP_SECRET || process.env.PRIVY_APP_SECRET;
  const authKey = fastify?.config?.PRIVY_AUTHORIZATION_KEY || process.env.PRIVY_AUTHORIZATION_KEY;

  if (!appId || !appSecret || !authKey) {
    console.error('Missing Privy environment variables:');
    console.error('PRIVY_APP_ID:', appId ? 'present' : 'missing');
    console.error('PRIVY_APP_SECRET:', appSecret ? 'present' : 'missing');
    console.error('PRIVY_AUTHORIZATION_KEY:', authKey ? 'present' : 'missing');
    throw new Error('Missing required Privy environment variables');
  }

  return new PrivyClient(
    appId,
    appSecret,
    {
      walletApi: {
        authorizationPrivateKey: authKey
      }
    },
  );
};

/**
 * Authentication function for Privy API calls
 * @param url The URL for the API endpoint
 * @param body The request body
 * @returns The authorization signature
 */
export function getAuthorizationSignature({url, body}: {url: string; body: object}) {
  const payload = {
    version: 1,
    method: 'POST',
    url,
    body,
    headers: {
      'privy-app-id': process.env.PRIVY_APP_ID
    }
  };

  // JSON-canonicalize the payload and convert it to a buffer
  // @ts-ignore - canonicalize is a callable function despite TypeScript error
  const serializedPayload = canonicalize(payload) as string;
  const serializedPayloadBuffer = Buffer.from(serializedPayload);

  // Get the authorization key from environment variables
  const privateKeyAsString = (process.env.PRIVY_AUTHORIZATION_KEY ?? "").replace('wallet-auth:', '');

  // Convert private key to PEM format and create a key object
  const privateKeyAsPem = `-----BEGIN PRIVATE KEY-----\n${privateKeyAsString}\n-----END PRIVATE KEY-----`;
  const privateKey = crypto.createPrivateKey({
    key: privateKeyAsPem,
    format: 'pem',
  });

  // Sign the payload buffer with the private key
  const signatureBuffer = crypto.sign('sha256', serializedPayloadBuffer, privateKey);
  const signature = signatureBuffer.toString('base64');
  return signature;
}

/**
 * Makes a request to the Privy API with proper authentication and headers.
 * @param url - The full URL for the API endpoint.
 * @param body - The request body (optional for GET requests).
 * @param requiresAuth - Whether the request requires authentication.
 * @param method - The HTTP method (defaults to POST).
 * @returns The response data.
 */
export const makePrivyRequest = async <T>(
  url: string, 
  body: object = {}, 
  requiresAuth = true, 
  method: 'GET' | 'POST' = 'POST'
): Promise<T> => {
  try {
    let headers: Record<string, string> = {
      'Content-Type': 'application/json',
      'privy-app-id': process.env.PRIVY_APP_ID ?? "",
    };

    if (requiresAuth) {
      // Create Basic Authentication header
      const appId = process.env.PRIVY_APP_ID ?? "";
      const appSecret = process.env.PRIVY_APP_SECRET ?? "";
      const basicAuthString = `${appId}:${appSecret}`;
      const base64Auth = Buffer.from(basicAuthString).toString('base64');
      
      headers.Authorization = `Basic ${base64Auth}`;
    }

    const requestInit: RequestInit = {
      method: method,
      headers,
    };

    // Only add body for non-GET requests
    if (method !== 'GET') {
      requestInit.body = JSON.stringify(body);
    }

    // Make the API request
    const response = await fetch(url, requestInit).then(res => {
      return res;
    }).catch(err => {
      console.log("error", err);
      throw err;
    });
    
    if (!response.ok) {
      throw new Error(`Privy API request failed: ${response.status}`);
    }

    return await response.json() as T;
  } catch (error) {
    console.error('Error making Privy API request:', error);
    throw new Error('Failed to make Privy API request');
  }
};

// Schema for sign-message request
const signMessageSchema = z.object({
  walletId: z.string().nonempty(),
  message: z.string().nonempty(),
  address: z.string().nonempty()
});

// Schema for token-approval request
const tokenApprovalSchema = z.object({
  walletId: z.string().nonempty(),
  address: z.string().nonempty(),
  ticker: z.string().nonempty(),
  amount: z.bigint().positive().optional(),
  chainId: z.number().positive().optional()
});

// Schema for token-sending request
const tokenSendSchema = z.object({
  senderAddress: z.string().nonempty(),
  recipientAddress: z.string().nonempty(),
  ticker: z.string().nonempty(),
  amount: z.bigint().positive(),
  chainId: z.number().positive().optional()
});

// Schema for wallet balance request
const walletBalanceSchema = z.object({
  address: z.string().nonempty(),
  assets: z.array(z.nativeEnum(Ticker)).min(1), // Required: array of valid Ticker enum values
  chains: z.array(z.string().nonempty()).min(1) // Required: array of chain names
});

/**
 * Gets the chain name based on chain ID
 * @param chainId The chain ID
 * @returns The CAIP-2 identifier for the chain
 */
export const getChainName = (chainId: number): string => {
  switch (chainId) {
    case 1:
      return 'eip155:1'; // Ethereum Mainnet
    case 42161:
      return 'eip155:42161'; // Arbitrum One
    case 421613:
      return 'eip155:421613'; // Arbitrum Goerli
    case 8453:
      return 'eip155:8453'; // Base
    case 84531:
      return 'eip155:84531'; // Base Goerli
    default:
      return `eip155:${chainId}`;
  }
};

/**
 * Signs a message using a Privy embedded wallet.
 * @param walletId - The ID of the wallet to use for signing.
 * @param message - The message to sign.
 * @param address - The wallet address to use for signing.
 * @returns The signature for the message.
 */
export const signMessage = async (walletId: string, message: string, address: string): Promise<string> => {
  try {
    const privy = getPrivyClient();
    
    const {signature} = await privy.walletApi.ethereum.signMessage({
        address: address,
        chainType: 'ethereum',
        message: message,
      });

    return signature;
  } catch (error) {
    console.error('Error signing message:', error);
    throw new Error('Failed to sign message with embedded wallet');
  }
};

/**
 * Approves a token for spending using Privy wallet (implementation)
 * @param walletId The wallet ID to use
 * @param walletAddress The wallet address
 * @param ticker The token ticker or enum value
 * @param amount The amount to approve (optional, defaults to max amount)
 * @param chainId The chain ID
 * @param spenderAddress The address that will be allowed to spend the tokens
 * @returns The transaction hash
 */
export const approveTokenImpl = async (
  walletAddress: string,
  ticker: string, 
  chainId?: number,
  amount?: bigint,
): Promise<string> => {
  try {
    
    // Get token data from ticker
    const tokenData = GetToken(ticker);

    // Create contract interface for ERC20 token
    const contractInterface = new ethers.Interface(Token.abi);
    
    // Max uint256 value for unlimited approval
    const approveAmount = amount ? 
      ethers.parseUnits(amount.toString(), tokenData.decimals) : 
      ethers.MaxUint256;
    
    // Encode the approve function call
    const data = contractInterface.encodeFunctionData("approve", [walletAddress, approveAmount]);
    
    chainId = chainId ?? ARBITRUM;

    // Get chain name in CAIP-2 format
    const networkName = getChainName(chainId);
    const privy = getPrivyClient();

    // Send the transaction
    const { hash } = await privy.walletApi.ethereum.sendTransaction({
      address: walletAddress as Address,
      chainType: 'ethereum',
      caip2: networkName as string,
      transaction: {
        to: tokenData.address as Address,
        data: data,
        chainId: chainId,
      },
    } as any);
    
    return hash;
  } catch (error) {
    console.error('Error approving token:', error);
    throw new Error(`Failed to approve token: ${error instanceof Error ? error.message : 'Unknown error'}`);
  }
};

function GetToken(symbol: string) {
  return TOKENS[ARBITRUM].find(token => token.symbol === symbol)
}

/**
 * Approves a token for spending using Privy wallet
 * @param this The FastifyRequest instance
 * @param reply The FastifyReply instance
 * @param address The wallet address
 * @param ticker The token ticker or enum value
 * @param chainId The chain ID
 * @param amount The amount to approve (optional, defaults to max amount)
 * @returns The response object with success status and transaction hash
 */
export async function approveToken(
  this: FastifyRequest,
  reply: FastifyReply,
  address: string,
  ticker: string,
  chainId: number,
  amount?: bigint
) {
  try {
    // Validate the request parameters
    tokenApprovalSchema.parse({ 
      address, 
      ticker,
      amount,
      chainId
    });
    
    if (!address) {
      throw new Error('Wallet address is required for token approval');
    }
    
    if (!chainId) {
      throw new Error('Chain ID is required for token approval');
    }
    
    // Call the approveTokenImpl function
    const hash = await approveTokenImpl(address, ticker, chainId, amount);
    
    return {
      success: true,
      hash: hash
    };
  } catch (error) {
    this.log.error(error);
    
    // Return appropriate error response
    reply.status(error instanceof z.ZodError ? 400 : 500);
    return {
      success: false,
      error: error instanceof Error ? error.message : 'An unknown error occurred'
    };
  }
}

/**
 * Signs a message using Privy embedded wallet
 * @param this The FastifyRequest instance
 * @param reply The FastifyReply instance
 * @param walletId The wallet ID
 * @param message The message to sign
 * @param address The wallet address
 * @returns The signature of the signed message
 */
export async function signPrivyMessage(
  this: FastifyRequest,
  reply: FastifyReply,
  walletId: string,
  message: string,
  address: string
) {
  try {
    // Validate the request parameters
    signMessageSchema.parse({ walletId, message, address });
    
    // Call the signMessage function
    const signature = await signMessage(walletId, message, address);
    
    return {
      success: true,
      signature: signature
    };
  } catch (error) {
    this.log.error(error);
    
    // Return appropriate error response
    reply.status(error instanceof z.ZodError ? 400 : 500);
    return {
      success: false,
      error: error instanceof Error ? error.message : 'An unknown error occurred'
    };
  }
}

/**
 * Approves a specific contract address for spending using Privy wallet
 * @param walletAddress The wallet address
 * @param tokenAddress The token address to approve
 * @param spenderAddress The contract address to approve
 * @param chainId The chain ID
 * @param amount The amount to approve (optional, defaults to max amount)
 * @returns The transaction hash
 */
export const approveContractImpl = async (
  walletAddress: string,
  tokenAddress: string,
  spenderAddress: string,
  chainId?: number,
  amount?: bigint,
): Promise<string> => {
  try {
    // Create contract interface for ERC20 token
    const contractInterface = new ethers.Interface(Token.abi);
    
    // Max uint256 value for unlimited approval
    const approveAmount = amount ?? ethers.MaxUint256;
    
    // Encode the approve function call
    const data = contractInterface.encodeFunctionData("approve", [spenderAddress, approveAmount]);
    
    chainId = chainId ?? ARBITRUM;

    // Get chain name in CAIP-2 format
    const networkName = getChainName(chainId);
    const privy = getPrivyClient();

    // Send the transaction
    const { hash } = await privy.walletApi.ethereum.sendTransaction({
      address: walletAddress as Address,
      chainType: 'ethereum',
      caip2: networkName as string,
      transaction: {
        to: tokenAddress as Address,
        data: data,
        chainId: chainId,
      },
    } as any);
    
    return hash;
  } catch (error) {
    console.error('Error approving contract:', error);
    throw new Error(`Failed to approve contract: ${error instanceof Error ? error.message : 'Unknown error'}`);
  }
};

/**
 * Gets the current allowance for a token for the GMX OrderVault contract
 * @param ownerAddress The address of the token owner
 * @param tokenAddress The address of the token contract
 * @returns The current allowance as a BigNumber
 */
export const getTokenAllowance = async (
  ownerAddress: string,
  tokenAddress: string,
  spenderAddress: string
): Promise<bigint> => {
  try {
   // use gmx sdk to get allowance
   const sdk = await getClientForAddress(ownerAddress);
   const allowance = await sdk.executeMulticall({
    token: {
      contractAddress: tokenAddress,
      abiId: "ERC20",
      calls: {
        allowance: {
          methodName: "allowance",
          params: [ownerAddress, spenderAddress]
        }
      }
    }
   }).
   then(res => {
    return res.data.token.allowance.returnValues[0];
   });

   return allowance;
  } catch (error) {
    console.error('Error getting token allowance:', error);
    throw new Error(`Failed to get token allowance: ${error instanceof Error ? error.message : 'Unknown error'}`);
  }
};

/**
 * Initializes a wallet address by approving USDC for GMX trading and GMX OrderVault
 * @param address The wallet address to initialize
 * @returns The transaction hashes for both approvals
 */
export const initAddressImpl = async (
  address: string,
): Promise<{ usdcHash: string, orderVaultHash: string, exchangeRouterHash: string, wethSyntheticsRouterHash: string }> => {
  try {
    console.log('🚀 Starting GMX V2 wallet initialization for address:', address);
    
    const sdk = await getClientForAddress(address);
    const {tokensData} = await sdk.tokens.getTokensData();
    const usdcTokenData = getTokenDataFromTicker(Ticker.USDC, tokensData);
    const wrapperEtherData = getTokenDataFromTicker("WETH", tokensData);
    
    // Use maximum uint256 value for unlimited approval to ensure sufficient allowance for all trading scenarios
    const approveAmount = ethers.MaxUint256;
    console.log('📊 Approval amount set to:', approveAmount.toString());
    
    // Get USDC token data for contract interactions
    const usdcToken = GetToken('USDC');
    console.log('💱 USDC Token Address:', usdcToken.address);
    console.log('💱 WETH Token Address:', wrapperEtherData.address);
    
    // ============================================================================
    // CRITICAL: OrderVault Approvals (Required for order creation)
    // According to GMX V2 docs: "collateral needs to first be transferred to the OrderVault"
    // ============================================================================
    console.log('\n🔐 Step 1: Approving OrderVault contracts...');
    
    // USDC approval for OrderVault
    const orderVaultUsdcAllowance = await getTokenAllowance(address, usdcToken.address, CONTRACTS[ARBITRUM].OrderVault);
    console.log('   USDC -> OrderVault allowance:', orderVaultUsdcAllowance.toString());
    
    let orderVaultHash = "";
    if (orderVaultUsdcAllowance < approveAmount) {
      console.log('   ⚠️  USDC allowance insufficient, approving OrderVault...');
      orderVaultHash = await approveContractImpl(
        address,
        usdcToken.address,
        CONTRACTS[ARBITRUM].OrderVault,
        ARBITRUM,
        approveAmount
      );
      console.log('   ✅ USDC OrderVault approval hash:', orderVaultHash);
    } else {
      orderVaultHash = `Already approved: ${orderVaultUsdcAllowance.toString()}`;
      console.log('   ✅ USDC already approved for OrderVault');
    }
    
    // WETH approval for OrderVault
    const orderVaultWethAllowance = await getTokenAllowance(address, wrapperEtherData.address, CONTRACTS[ARBITRUM].OrderVault);
    console.log('   WETH -> OrderVault allowance:', orderVaultWethAllowance.toString());
    
    let orderVaultWethHash = "";
    if (orderVaultWethAllowance < approveAmount) {
      console.log('   ⚠️  WETH allowance insufficient, approving OrderVault...');
      orderVaultWethHash = await approveContractImpl(
        address,
        wrapperEtherData.address,
        CONTRACTS[ARBITRUM].OrderVault,
        ARBITRUM,
        approveAmount
      );
      console.log('   ✅ WETH OrderVault approval hash:', orderVaultWethHash);
    } else {
      orderVaultWethHash = `Already approved: ${orderVaultWethAllowance.toString()}`;
      console.log('   ✅ WETH already approved for OrderVault');
    }
    
    // ============================================================================
    // CRITICAL: ExchangeRouter Approvals (Required for createOrder calls)
    // According to GMX V2 docs: "ExchangeRouter.createOrder can then be called after"
    // ============================================================================
    console.log('\n🔐 Step 2: Approving ExchangeRouter contracts...');
    
    // USDC approval for ExchangeRouter
    const exchangeRouterUsdcAllowance = await getTokenAllowance(address, usdcToken.address, CONTRACTS[ARBITRUM].ExchangeRouter);
    console.log('   USDC -> ExchangeRouter allowance:', exchangeRouterUsdcAllowance.toString());
    
    let exchangeRouterHash = "";
    if (exchangeRouterUsdcAllowance < approveAmount) {
      console.log('   ⚠️  USDC allowance insufficient, approving ExchangeRouter...');
      exchangeRouterHash = await approveContractImpl(
        address,
        usdcToken.address,
        CONTRACTS[ARBITRUM].ExchangeRouter,
        ARBITRUM,
        approveAmount
      );
      console.log('   ✅ USDC ExchangeRouter approval hash:', exchangeRouterHash);
    } else {
      exchangeRouterHash = `Already approved: ${exchangeRouterUsdcAllowance.toString()}`;
      console.log('   ✅ USDC already approved for ExchangeRouter');
    }
    
    // WETH approval for ExchangeRouter
    const exchangeRouterWethAllowance = await getTokenAllowance(address, wrapperEtherData.address, CONTRACTS[ARBITRUM].ExchangeRouter);
    console.log('   WETH -> ExchangeRouter allowance:', exchangeRouterWethAllowance.toString());
    
    let exchangeRouterWethHash = "";
    if (exchangeRouterWethAllowance < approveAmount) {
      console.log('   ⚠️  WETH allowance insufficient, approving ExchangeRouter...');
      exchangeRouterWethHash = await approveContractImpl(
        address,
        wrapperEtherData.address,
        CONTRACTS[ARBITRUM].ExchangeRouter,
        ARBITRUM,
        approveAmount
      );
      console.log('   ✅ WETH ExchangeRouter approval hash:', exchangeRouterWethHash);
    } else {
      exchangeRouterWethHash = `Already approved: ${exchangeRouterWethAllowance.toString()}`;
      console.log('   ✅ WETH already approved for ExchangeRouter');
    }
    
    // ============================================================================
    // OPTIONAL: SyntheticsRouter Approvals (For synthetic positions)
    // ============================================================================
    console.log('\n🔐 Step 3: Approving SyntheticsRouter contracts...');
    
    // USDC approval for SyntheticsRouter
    const syntheticsRouterUsdcAllowance = await getTokenAllowance(address, usdcToken.address, CONTRACTS[ARBITRUM].SyntheticsRouter);
    console.log('   USDC -> SyntheticsRouter allowance:', syntheticsRouterUsdcAllowance.toString());
    
    let syntheticsRouterUsdcHash = "";
    if (syntheticsRouterUsdcAllowance < approveAmount) {
      console.log('   ⚠️  USDC allowance insufficient, approving SyntheticsRouter...');
      syntheticsRouterUsdcHash = await approveContractImpl(
        address,
        usdcToken.address,
        CONTRACTS[ARBITRUM].SyntheticsRouter,
        ARBITRUM,
        approveAmount
      );
      console.log('   ✅ USDC SyntheticsRouter approval hash:', syntheticsRouterUsdcHash);
    } else {
      syntheticsRouterUsdcHash = `Already approved: ${syntheticsRouterUsdcAllowance.toString()}`;
      console.log('   ✅ USDC already approved for SyntheticsRouter');
    }
    
    // WETH approval for SyntheticsRouter
    const syntheticsRouterWethAllowance = await getTokenAllowance(address, wrapperEtherData.address, CONTRACTS[ARBITRUM].SyntheticsRouter);
    console.log('   WETH -> SyntheticsRouter allowance:', syntheticsRouterWethAllowance.toString());
    
    let wethSyntheticsRouterHash = "";
    if (syntheticsRouterWethAllowance < approveAmount) {
      console.log('   ⚠️  WETH allowance insufficient, approving SyntheticsRouter...');
      wethSyntheticsRouterHash = await approveContractImpl(
        address,
        wrapperEtherData.address,
        CONTRACTS[ARBITRUM].SyntheticsRouter,
        ARBITRUM,
        approveAmount
      );
      console.log('   ✅ WETH SyntheticsRouter approval hash:', wethSyntheticsRouterHash);
    } else {
      wethSyntheticsRouterHash = `Already approved: ${syntheticsRouterWethAllowance.toString()}`;
      console.log('   ✅ WETH already approved for SyntheticsRouter');
    }
    
    // ============================================================================
    // CRITICAL: Additional GMX V2 Contract Approvals
    // These contracts might be needed for order creation and execution
    // ============================================================================
    console.log('\n🔐 Step 4: Approving additional GMX V2 contracts...');
    
    // USDC approval for DataStore (required for order execution)
    const dataStoreUsdcAllowance = await getTokenAllowance(address, usdcToken.address, CONTRACTS[ARBITRUM].DataStore);
    console.log('   USDC -> DataStore allowance:', dataStoreUsdcAllowance.toString());
    
    let dataStoreUsdcHash = "";
    if (dataStoreUsdcAllowance < approveAmount) {
      console.log('   ⚠️  USDC allowance insufficient, approving DataStore...');
      dataStoreUsdcHash = await approveContractImpl(
        address,
        usdcToken.address,
        CONTRACTS[ARBITRUM].DataStore,
        ARBITRUM,
        approveAmount
      );
      console.log('   ✅ USDC DataStore approval hash:', dataStoreUsdcHash);
    } else {
      dataStoreUsdcHash = `Already approved: ${dataStoreUsdcAllowance.toString()}`;
      console.log('   ✅ USDC already approved for DataStore');
    }
    
    // WETH approval for DataStore
    const dataStoreWethAllowance = await getTokenAllowance(address, wrapperEtherData.address, CONTRACTS[ARBITRUM].DataStore);
    console.log('   WETH -> DataStore allowance:', dataStoreWethAllowance.toString());
    
    let dataStoreWethHash = "";
    if (dataStoreWethAllowance < approveAmount) {
      console.log('   ⚠️  WETH allowance insufficient, approving DataStore...');
      dataStoreWethHash = await approveContractImpl(
        address,
        wrapperEtherData.address,
        CONTRACTS[ARBITRUM].DataStore,
        ARBITRUM,
        approveAmount
      );
      console.log('   ✅ WETH DataStore approval hash:', dataStoreWethHash);
    } else {
      dataStoreWethHash = `Already approved: ${dataStoreWethAllowance.toString()}`;
      console.log('   ✅ WETH already approved for DataStore');
    }
    
    // USDC approval for DepositVault (for deposits)
    const depositVaultUsdcAllowance = await getTokenAllowance(address, usdcToken.address, CONTRACTS[ARBITRUM].DepositVault);
    console.log('   USDC -> DepositVault allowance:', depositVaultUsdcAllowance.toString());
    
    let depositVaultUsdcHash = "";
    if (depositVaultUsdcAllowance < approveAmount) {
      console.log('   ⚠️  USDC allowance insufficient, approving DepositVault...');
      depositVaultUsdcHash = await approveContractImpl(
        address,
        usdcToken.address,
        CONTRACTS[ARBITRUM].DepositVault,
        ARBITRUM,
        approveAmount
      );
      console.log('   ✅ USDC DepositVault approval hash:', depositVaultUsdcHash);
    } else {
      depositVaultUsdcHash = `Already approved: ${depositVaultUsdcAllowance.toString()}`;
      console.log('   ✅ USDC already approved for DepositVault');
    }
    
    // WETH approval for DepositVault
    const depositVaultWethAllowance = await getTokenAllowance(address, wrapperEtherData.address, CONTRACTS[ARBITRUM].DepositVault);
    console.log('   WETH -> DepositVault allowance:', depositVaultWethAllowance.toString());
    
    let depositVaultWethHash = "";
    if (depositVaultWethAllowance < approveAmount) {
      console.log('   ⚠️  WETH allowance insufficient, approving DepositVault...');
      depositVaultWethHash = await approveContractImpl(
        address,
        wrapperEtherData.address,
        CONTRACTS[ARBITRUM].DepositVault,
        ARBITRUM,
        approveAmount
      );
      console.log('   ✅ WETH DepositVault approval hash:', depositVaultWethHash);
    } else {
      depositVaultWethHash = `Already approved: ${depositVaultWethAllowance.toString()}`;
      console.log('   ✅ WETH already approved for DepositVault');
    }
    
    // ============================================================================
    // SUMMARY: Final approval status
    // ============================================================================
    console.log('\n📋 Final Approval Summary:');
    console.log('   OrderVault USDC:', orderVaultHash);
    console.log('   OrderVault WETH:', orderVaultWethHash);
    console.log('   ExchangeRouter USDC:', exchangeRouterHash);
    console.log('   ExchangeRouter WETH:', exchangeRouterWethHash);
    console.log('   SyntheticsRouter USDC:', syntheticsRouterUsdcHash);
    console.log('   SyntheticsRouter WETH:', wethSyntheticsRouterHash);
    console.log('   DataStore USDC:', dataStoreUsdcHash);
    console.log('   DataStore WETH:', dataStoreWethHash);
    console.log('   DepositVault USDC:', depositVaultUsdcHash);
    console.log('   DepositVault WETH:', depositVaultWethHash);
    
    console.log('\n✅ GMX V2 wallet initialization completed successfully!');
    console.log('   All necessary contracts are now approved for trading.');
    
    return {
      usdcHash: `OrderVault: ${orderVaultHash}, ExchangeRouter: ${exchangeRouterHash}, SyntheticsRouter: ${syntheticsRouterUsdcHash}, DataStore: ${dataStoreUsdcHash}, DepositVault: ${depositVaultUsdcHash}`,
      orderVaultHash: `USDC: ${orderVaultHash}, WETH: ${orderVaultWethHash}`,
      exchangeRouterHash: `USDC: ${exchangeRouterHash}, WETH: ${exchangeRouterWethHash}`,
      wethSyntheticsRouterHash: `SyntheticsRouter: ${wethSyntheticsRouterHash}, DataStore: ${dataStoreWethHash}, DepositVault: ${depositVaultWethHash}`
    };
  } catch (error) {
    console.error('❌ Error initializing GMX V2 wallet:', error);
    throw new Error(`Failed to initialize GMX V2 wallet: ${error instanceof Error ? error.message : 'Unknown error'}`);
  }
};

/**
 * Initializes a wallet address for GMX trading
 * @param this The FastifyRequest instance
 * @param reply The FastifyReply instance
 * @param address The wallet address to initialize
 * @returns The response object with success status and transaction hashes
 */
export async function initAddress(
  this: FastifyRequest,
  reply: FastifyReply,
  address: string
) {
  try {
    if (!address) {
      throw new Error('Wallet address is required for initialization');
    }
    
    const { usdcHash, orderVaultHash, exchangeRouterHash, wethSyntheticsRouterHash } = await initAddressImpl(address);
    
    return {
      success: true,
      usdcHash,
      orderVaultHash,
      exchangeRouterHash,
      wethSyntheticsRouterHash
    };
  } catch (error) {
    this.log.error(error);
    
    reply.status(500);
    return {
      success: false,
      error: error instanceof Error ? error.message : 'An unknown error occurred'
    };
  }
}

/**
 * Sends tokens from one address to another using Privy wallet (implementation)
 * @param senderAddress The sender's wallet address
 * @param recipientAddress The recipient's wallet address
 * @param ticker The token ticker or enum value
 * @param amount The amount to send
 * @param chainId The chain ID (optional, defaults to ARBITRUM)
 * @returns The transaction hash
 */
export const sendTokenImpl = async (
  senderAddress: string,
  recipientAddress: string,
  ticker: string,
  amount: bigint,
  chainId?: number,
): Promise<string> => {
  try {
    chainId = chainId ?? ARBITRUM;
    const networkName = getChainName(chainId);
    const privy = getPrivyClient();

    if (ticker === 'ETH') {
      // Native ETH transfer: no allowance, no data, value is amount as hex string
      const { hash } = await privy.walletApi.ethereum.sendTransaction({
        address: senderAddress as Address,
        chainType: 'ethereum',
        caip2: networkName as string,
        transaction: {
          to: recipientAddress as Address,
          value: '0x' + amount.toString(16), // value in wei as hex string
          chainId: chainId,
        },
      } as any);
      return hash;
    }

    // ERC20 logic
    // Get token data from ticker
    const tokenData = GetToken(ticker);
    if (!tokenData) {
      throw new Error(`Token not found: ${ticker}`);
    }
    // Check if sender has sufficient allowance for the token transfer
    const senderAllowance = await getTokenAllowance(senderAddress, tokenData.address, senderAddress);
    // If insufficient allowance, approve the token first
    if (senderAllowance < amount) {
      console.log(`Insufficient allowance (${senderAllowance}). Approving token for amount: ${amount}`);
      await approveContractImpl(
        senderAddress,
        tokenData.address,
        senderAddress, // Approve self to spend tokens
        chainId,
        amount
      );
      console.log('Token approval completed');
    }
    // Create contract interface for ERC20 token
    const contractInterface = new ethers.Interface(Token.abi);
    // Amount is already in the smallest units (wei), so we don't need to convert it
    const transferAmount = amount;
    // Encode the transfer function call
    const data = contractInterface.encodeFunctionData("transfer", [recipientAddress, transferAmount]);
    // Send the transaction
    const { hash } = await privy.walletApi.ethereum.sendTransaction({
      address: senderAddress as Address,
      chainType: 'ethereum',
      caip2: networkName as string,
      transaction: {
        to: tokenData.address as Address,
        data: data,
        chainId: chainId,
      },
    } as any);
    return hash;
  } catch (error) {
    console.error('Error sending token:', error);
    throw new Error(`Failed to send token: ${error instanceof Error ? error.message : 'Unknown error'}`);
  }
};

/**
 * Gets the wallet balance for a specific address using Privy API
 * @param address The wallet address to get balance for
 * @param assets Array of assets to filter by using Ticker enum (e.g. [Ticker.USDC, Ticker.ETH])
 * @param chains Array of chain names to filter by (e.g. ['arbitrum', 'ethereum'])
 * @returns Array of Balance objects
 */
export const getWalletBalanceImpl = async (
  address: string,
  assets: Ticker[],
  chains: string[]
): Promise<Balance[]> => {
  try {
    // First, get the user by wallet address using Privy Client
    const privy = getPrivyClient();
    
    // Get user by wallet address
    const user = await privy.getUserByWalletAddress(address);
    if (!user) {
      throw new Error(`User not found for wallet address: ${address}`);
    }

    // Find the embedded wallet for this address
    const embeddedWallet = user.linkedAccounts.find(
      (account: any) => 
        account.type === 'wallet' && 
        account.chainType === 'ethereum' &&
        account.address?.toLowerCase() === address.toLowerCase()
    );

    if (!embeddedWallet) {
      throw new Error(`Embedded wallet not found for address: ${address}`);
    }

    // Extract wallet ID from the embedded wallet
    const walletId = (embeddedWallet as any).walletId || (embeddedWallet as any).id;
    
    if (!walletId) {
      throw new Error(`Wallet ID not found for embedded wallet`);
    }

    // Make authenticated request to get wallet balance
    let balanceUrl = `https://auth.privy.io/api/v1/wallets/${walletId}/balance`;
    
    // Add required query parameters for multiple assets and chains
    const queryParams = new URLSearchParams();
    
    // Add multiple asset parameters
    assets.forEach(asset => {
      queryParams.append('asset', asset.toLowerCase()); // Convert Ticker enum to lowercase
    });
    
    // Add multiple chain parameters
    chains.forEach(chain => {
      queryParams.append('chain', chain);
    });
    
    // Add include_currency parameter for USD pricing
    queryParams.append('include_currency', 'usd');
    
    balanceUrl += `?${queryParams.toString()}`;
    const balanceResponse = await makePrivyRequest<{
      balances: Array<{
        chain: string;
        asset: string;
        raw_value: string;
        raw_value_decimals: number;
        display_values: {
          [key: string]: string;
          usd: string;
        };
      }>;
    }>(balanceUrl, {}, true, 'GET');

    // Convert to Balance interface format (matching ManagingApiTypes.ts)
    const balances: Balance[] = balanceResponse.balances
      .map(balance => {
        // Parse the raw value using decimals
        const amount = parseFloat(balance.raw_value) / Math.pow(10, balance.raw_value_decimals);
        
        // Get USD price from display_values
        const usdValue = parseFloat(balance.display_values.usd);
        
        // Calculate price per token (if amount > 0)
        const price = amount > 0 ? usdValue / amount : 0;
        
        // Get chain ID from chain name
        const chainId = getChainIdFromName(balance.chain);
        const caip2 = getChainName(chainId);

        const chain: Chain = {
          id: caip2,
          rpcUrl: undefined,
          name: getChainNameFromCaip2(caip2),
          chainId: chainId
        };

        return {
          tokenImage: '', // Not provided in the response
          tokenName: balance.asset.toUpperCase(), // Use asset name (usdc -> USDC)
          amount: amount,
          price: price,
          value: usdValue,
          tokenAdress: '', // Not provided in this response format
          chain: chain
        };
      });

    return balances;
  } catch (error) {
    console.error('Error getting wallet balance:', error);
    throw new Error(`Failed to get wallet balance: ${error instanceof Error ? error.message : 'Unknown error'}`);
  }
};

/**
 * Helper function to get chain name from CAIP-2 identifier
 * @param caip2 The CAIP-2 identifier
 * @returns Human readable chain name
 */
const getChainNameFromCaip2 = (caip2: string): string => {
  switch (caip2) {
    case 'eip155:1':
      return 'Ethereum Mainnet';
    case 'eip155:42161':
      return 'Arbitrum One';
    case 'eip155:421613':
      return 'Arbitrum Goerli';
    case 'eip155:8453':
      return 'Base';
    case 'eip155:84531':
      return 'Base Goerli';
    default:
      return `Chain ${caip2}`;
  }
};

/**
 * Helper function to get chain ID from chain name
 * @param chainName The chain name (e.g., 'arbitrum', 'ethereum', 'base')
 * @returns The corresponding chain ID
 */
const getChainIdFromName = (chainName: string): number => {
  switch (chainName.toLowerCase()) {
    case 'ethereum':
    case 'mainnet':
      return 1;
    case 'arbitrum':
    case 'arbitrum-one':
      return 42161;
    case 'arbitrum-goerli':
      return 421613;
    case 'base':
      return 8453;
    case 'base-goerli':
      return 84531;
    default:
      // Default to Arbitrum if unknown chain
      return 42161;
  }
};

/**
 * Gets wallet balance using Privy embedded wallet
 * @param this The FastifyRequest instance
 * @param reply The FastifyReply instance
 * @param address The wallet address to get balance for
 * @param assets Array of assets to filter by using Ticker enum (e.g. [Ticker.USDC, Ticker.ETH])
 * @param chains Array of chain names to filter by (e.g. ['arbitrum', 'ethereum'])
 * @returns The response object with success status and balances array
 */
export async function getWalletBalance(
  this: FastifyRequest,
  reply: FastifyReply,
  address: string,
  assets: Ticker[],
  chains: string[]
) {
  try {
    // Validate the request parameters
    walletBalanceSchema.parse({ 
      address,
      assets,
      chains
    });
    
    if (!address) {
      throw new Error('Wallet address is required for balance retrieval');
    }
    
    if (!assets || assets.length === 0) {
      throw new Error('At least one asset is required for balance retrieval');
    }
    
    if (!chains || chains.length === 0) {
      throw new Error('At least one chain is required for balance retrieval');
    }
    
    // Call the getWalletBalanceImpl function
    const balances = await getWalletBalanceImpl(address, assets, chains);
    
    return {
      success: true,
      balances: balances
    };
  } catch (error) {
    this.log.error(error);
    
    // Return appropriate error response
    reply.status(error instanceof z.ZodError ? 400 : 500);
    return {
      success: false,
      error: error instanceof Error ? error.message : 'An unknown error occurred'
    };
  }
}

/**
 * Sends tokens from one address to another using Privy wallet
 * @param this The FastifyRequest instance
 * @param reply The FastifyReply instance
 * @param senderAddress The sender's wallet address
 * @param recipientAddress The recipient's wallet address
 * @param ticker The token ticker or enum value
 * @param amount The amount to send
 * @param chainId The chain ID (optional, defaults to ARBITRUM)
 * @returns The response object with success status and transaction hash
 */
export async function sendToken(
  this: FastifyRequest,
  reply: FastifyReply,
  senderAddress: string,
  recipientAddress: string,
  ticker: string,
  amount: bigint,
  chainId?: number
) {
  try {
    // Validate the request parameters
    tokenSendSchema.parse({ 
      senderAddress, 
      recipientAddress,
      ticker,
      amount,
      chainId
    });
    
    if (!senderAddress) {
      throw new Error('Sender address is required for token transfer');
    }
    
    if (!recipientAddress) {
      throw new Error('Recipient address is required for token transfer');
    }
    
    if (!ticker) {
      throw new Error('Token ticker is required for token transfer');
    }
    
    if (!amount || amount <= 0n) {
      throw new Error('Valid amount is required for token transfer');
    }
    
    // Call the sendTokenImpl function
    const hash = await sendTokenImpl(senderAddress, recipientAddress, ticker, amount, chainId);
    
    return {
      success: true,
      hash: hash
    };
  } catch (error) {
    this.log.error(error);
    
    // Return appropriate error response
    reply.status(error instanceof z.ZodError ? 400 : 500);
    return {
      success: false,
      error: error instanceof Error ? error.message : 'An unknown error occurred'
    };
  }
}

/**
 * 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) => {
  // Decorate request with methods that use the Fastify instance
  fastify.decorateRequest('signPrivyMessage', async function(this: FastifyRequest, reply: FastifyReply, walletId: string, message: string, address: string) {
    return signPrivyMessage.call(this, reply, walletId, message, address);
  });

  fastify.decorateRequest('approveToken', async function(this: FastifyRequest, reply: FastifyReply, address: string, ticker: string, chainId: number, amount?: bigint) {
    return approveToken.call(this, reply, address, ticker, chainId, amount);
  });

  fastify.decorateRequest('initAddress', async function(this: FastifyRequest, reply: FastifyReply, address: string) {
    return initAddress.call(this, reply, address);
  });

  fastify.decorateRequest('sendToken', async function(this: FastifyRequest, reply: FastifyReply, senderAddress: string, recipientAddress: string, ticker: string, amount: bigint, chainId?: number) {
    return sendToken.call(this, reply, senderAddress, recipientAddress, ticker, amount, chainId);
  });

  fastify.decorateRequest('getWalletBalance', async function(this: FastifyRequest, reply: FastifyReply, address: string, assets: Ticker[], chains: string[]) {
    return getWalletBalance.call(this, reply, address, assets, chains);
  });

  // Test the Privy client initialization
  try {
    const testClient = getPrivyClient(fastify);
    fastify.log.info('Privy client initialized successfully');
  } catch (error) {
    fastify.log.error('Failed to initialize Privy client:', error);
    throw error;
  }
}, {
  name: 'privy-plugin'
});

