import { ChainID, type SupportedChain } from '../chains.js';
import { NATIVE_EVM_ETH_ADDRESSES, NATIVE_SOLANA_TOKEN_ADDRESS, WRAPPED_SOL_MINT_ADDRESS } from '../constants.js';
import { Parsers } from './parsers.js';

const TOKEN_PRICE_BASE_URL: string = 'https://coins.llama.fi/prices/current/';
const REDUCTION_FACTOR = 0.8; // 80% of the input value
const STABLECOIN_DECIMALS = 6; // USDC/USDT typically use 6 decimals

/**
 * Response structure from DefiLlama API for token prices
 */
export interface DefiLlamaTokensResponse {
  coins: Record<string, DefiLlamaCoinData>;
}

/**
 * Individual token data from DefiLlama API
 */
export interface DefiLlamaCoinData {
  /** Number of decimal places for the token */
  decimals: number;
  /** Token symbol (e.g., "ETH", "USDC") */
  symbol: string;
  /** Current price in USD */
  price: number;
  /** Unix timestamp of the price data */
  timestamp: number;
  /** Confidence level of the price data (0-1) */
  confidence: number;
}

/**
 * Maps chain IDs to their corresponding chain types
 * Used to determine which SDK implementation to use for a given chain
 */
export const chainIdToDefiLlamaChainMap = {
  [ChainID.Arbitrum]: 'arbitrum',
  [ChainID.Base]: 'base',
  [ChainID.Optimism]: 'optimism',
  [ChainID.Hyperliquid]: 'hyperliquid',
  [ChainID.Solana]: 'solana',
  [ChainID.Sui]: 'sui',
} as const;

/**
 * Creates DefiLlama coin key from chain ID and token address
 * Format: "chainName:tokenAddress"
 */
export const createDefiLlamaCoinKey = (chainId: ChainID, tokenAddress: string): string => {
  const chainName = chainIdToDefiLlamaChainMap[chainId];

  return `${chainName}:${toDefiLlamaToken(tokenAddress)}`;
};

/**
 * Retrieves token data from DefiLlama response by chain and address
 */
export const getCoinFromResponse = (
  response: DefiLlamaTokensResponse,
  chainId: ChainID,
  tokenAddress: string,
): DefiLlamaCoinData => {
  const key = createDefiLlamaCoinKey(chainId, tokenAddress);
  const coin = response.coins[key];

  if (!coin) {
    throw new Error(`DefiLlama coin not found for ${key}`);
  }

  return coin;
};

/**
 * Converts Solana native token address to wrapped SOL mint address
 * DefiLlama uses wrapped SOL mint address for native SOL
 *
 * @param tokenAddress The token address to convert
 * @returns The DefiLlama-compatible token address
 */
function toDefiLlamaToken(tokenAddress: string): string {
  if (isNativeTokenEvmAddress(tokenAddress)) {
    return '0x0000000000000000000000000000000000000000';
  }

  if (tokenAddress === NATIVE_SOLANA_TOKEN_ADDRESS) {
    return WRAPPED_SOL_MINT_ADDRESS;
  }

  return tokenAddress;
}

function isNativeTokenEvmAddress(tokenAddress: string): boolean {
  const normalizedAddress = tokenAddress.toLowerCase();

  return NATIVE_EVM_ETH_ADDRESSES.some((addr) => addr.toLowerCase() === normalizedAddress);
}

/**
 * Builds a comma-separated query string for DefiLlama API token requests
 *
 * Converts an array of chain/token pairs into the format expected by DefiLlama's
 * bulk price endpoint: "chain1:token1,chain2:token2,..."
 *
 * @param tokens - Array of [ChainID, token address] tuples to query
 * @returns Comma-separated string of DefiLlama coin keys
 */
function buildTokensQueryString(tokens: Array<[ChainID, string]>): string {
  return tokens.map(([chainId, tokenAddress]) => createDefiLlamaCoinKey(chainId, tokenAddress)).join(',');
}

/**
 * Fetch tokens data for array of coins
 *
 * @param tokens Array of [ChainID, Token Address] tuples
 * @returns Promise resolving to DefiLlama tokens response
 */
export async function getTokensData(tokens: Array<[ChainID, string]>): Promise<DefiLlamaTokensResponse> {
  const tokensStr = buildTokensQueryString(tokens);

  const url = `${TOKEN_PRICE_BASE_URL}${tokensStr}`;
  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
    },
  });

  return response.json();
}

export async function calculateAmounts({
  srcChainId,
  tokenIn,
  amountIn,
  destChainId,
  tokenOut,
}: {
  srcChainId: SupportedChain;
  tokenIn: string;
  amountIn: bigint;
  destChainId: SupportedChain;
  tokenOut: string;
}): Promise<{ amountOut: bigint; minStablecoinsAmount: bigint }> {
  // Fetch token data from DefiLlama
  const tokensResponse = await getTokensData([
    [srcChainId, tokenIn],
    [destChainId, tokenOut],
  ]);

  const tokenInData = getCoinFromResponse(tokensResponse, srcChainId, tokenIn);

  const tokenOutData = getCoinFromResponse(tokensResponse, destChainId, tokenOut);

  // Calculate 80% of the amount_in value as amount_out_min and min_stablecoins_amount
  const tokenInUsdAmount = Parsers.bigintToFloat(amountIn, tokenInData.decimals) * tokenInData.price;
  const amountInUsdReduced = tokenInUsdAmount * REDUCTION_FACTOR;
  const amountOutReduced = amountInUsdReduced / tokenOutData.price;

  const amountOut = Parsers.floatToBigint(amountOutReduced, tokenOutData.decimals);
  const minStablecoinsAmount = Parsers.floatToBigint(amountInUsdReduced, STABLECOIN_DECIMALS);

  return { amountOut, minStablecoinsAmount };
}
