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 {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;
  }
}

/**
 * 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.
 * @param requiresAuth - Whether the request requires authentication.
 * @returns The response data.
 */
export const makePrivyRequest = async <T>(url: string, body: object, requiresAuth = true): Promise<T> => {
  try {
    let headers: Record<string, string> = {
      'Content-Type': 'application/json',
      'privy-app-id': process.env.PRIVY_APP_ID ?? "",
    };

    if (requiresAuth) {
      // Generate authorization signature
      const authSig = await getAuthorizationSignature({
        url,
        body
      });
      
      // Create authentication string for basic auth
      const basicAuthString = `${process.env.PRIVY_APP_ID}:${process.env.PRIVY_APP_SECRET}`;
      const base64Auth = Buffer.from(basicAuthString).toString('base64');
      
      headers = {
        ...headers,
        'privy-authorization-signature': authSig,
        'Authorization': `Basic ${base64Auth}`
      };
    }

    const request = {
      method: 'POST',
      headers,
      body: JSON.stringify(body)
    };

    // Make the API request
    const response = await fetch(url, request).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()
});

/**
 * 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 }> => {
  try {
    const sdk = await getClientForAddress(address);

    const {tokensData} = await sdk.tokens.getTokensData();
    const usdcTokenData = getTokenDataFromTicker(Ticker.USDC, tokensData);
    const wrapperEtherData = getTokenDataFromTicker("WETH", tokensData);
    let approveAmount = usdcTokenData.prices.maxPrice; // Large enough amount for trading

    // Check approval for USDC
    const usdcToken = GetToken('USDC');
    const usdcAllowance = await getTokenAllowance(address, usdcToken.address, address);

    let usdcHash = "";
    if (usdcAllowance < approveAmount) {
      // First approve USDC token for GMX trading
      const usdcToken = GetToken('USDC');
      usdcHash = await approveTokenImpl(
        address,
        usdcToken.symbol,
        ARBITRUM,
        usdcTokenData.prices.maxPrice
      );
    }else{
      usdcHash = "Already allowed :" + usdcAllowance;
    }

    const orderVaultAllowance = await getTokenAllowance(address, usdcToken.address, CONTRACTS[ARBITRUM].OrderVault);

    // Then approve GMX OrderVault with the correct amount
    let orderVaultHash = "";
    if (orderVaultAllowance < approveAmount) {
      orderVaultHash = await approveContractImpl(
        address,
        usdcToken.address,
        CONTRACTS[ARBITRUM].OrderVault,
        ARBITRUM,
        approveAmount
      );
    }else{
      orderVaultHash = "Already allowed :" + orderVaultAllowance;
    }

    const wrapperEtherAllowance = await getTokenAllowance(address, wrapperEtherData.address, CONTRACTS[ARBITRUM].OrderVault);

    let wrapperEtherHash = "";
    if (wrapperEtherAllowance < approveAmount) {
      wrapperEtherHash = await approveContractImpl(
        address,
        wrapperEtherData.address,
        CONTRACTS[ARBITRUM].OrderVault,
        ARBITRUM,
        approveAmount
      );
    }else{
      wrapperEtherHash = "Already allowed :" + wrapperEtherAllowance;
    }

    console.log('wrapperEtherAllowance', wrapperEtherAllowance)

    const exchangeRouterAllowance = await getTokenAllowance(address, usdcToken.address, CONTRACTS[ARBITRUM].ExchangeRouter);

    console.log('exchangeRouterAllowance', exchangeRouterAllowance)
    
    let exchangeRouterHash = "";
    if (exchangeRouterAllowance < approveAmount) {
      exchangeRouterHash = await approveContractImpl(
        address,
        usdcToken.address,
        CONTRACTS[ARBITRUM].ExchangeRouter,
        ARBITRUM,
        approveAmount
      );
    }else{
      exchangeRouterHash = "Already allowed :" + exchangeRouterAllowance;
    }

    const wrapperEtherExchangeAllowance = await getTokenAllowance(address, wrapperEtherData.address, CONTRACTS[ARBITRUM].ExchangeRouter);

    let wrapperEtherExchangeHash = "";
    if (wrapperEtherExchangeAllowance < approveAmount) {
      wrapperEtherExchangeHash = await approveContractImpl(
        address,
        wrapperEtherData.address,
        CONTRACTS[ARBITRUM].ExchangeRouter,
        ARBITRUM,
        approveAmount
      );
    }else{
      wrapperEtherExchangeHash = "Already allowed :" + wrapperEtherExchangeAllowance;
    }

    console.log('wrapperEtherExchangeAllowance', wrapperEtherExchangeAllowance)
   
    const usdcSyntheticRouterAllowance = await getTokenAllowance(address, usdcToken.address, CONTRACTS[ARBITRUM].SyntheticsRouter);

    let usdcSyntheticRouterHash = "";
    if (usdcSyntheticRouterAllowance < approveAmount) {
      usdcSyntheticRouterHash = await approveContractImpl(
        address,
        usdcToken.address,
        CONTRACTS[ARBITRUM].SyntheticsRouter,
        ARBITRUM,
        approveAmount
      );
    }else{
      usdcSyntheticRouterHash = "Already allowed :" + usdcSyntheticRouterAllowance;
    }

    console.log('usdcSyntheticRouterAllowance', usdcSyntheticRouterAllowance)

    console.log('usdcHash', usdcHash)
    console.log('orderVaultHash', orderVaultHash)
    console.log('exchangeRouterHash', exchangeRouterHash)

    return {
      usdcHash,
      orderVaultHash,
      exchangeRouterHash
    };
  } catch (error) {
    console.error('Error initializing address:', error);
    throw new Error(`Failed to initialize address: ${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 } = await initAddressImpl(address);
    
    return {
      success: true,
      usdcHash,
      orderVaultHash
    };
  } catch (error) {
    this.log.error(error);
    
    reply.status(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);
  });

  // 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'
});

