import { ChainID, isEvmChain, type SupportedEvmChain } from '../../chains.js';
import { JupiterQuoteProvider } from './jupiter.js';
import { type QuoteResponse as JupiterQuoteResponse } from '@jup-ag/api';
import { ParaswapQuoteProvider, type SwapEstimateResult } from './paraswap.js';
import { CROSS_CHAIN_TOKENS } from './stablecoins-tokens.js';
import { AftermathQuoteProvider } from './aftermath.js';
import type { RouterCompleteTradeRoute } from 'aftermath-ts-sdk';
import { calculateAmounts } from '../defillama.js';
import { LiquidSwapQuoteProvider, type LiquidSwapQuoteResponse } from './liquidswap.js';

type SingleChainQuoteParams = {
  chainId: ChainID;
  amount: bigint;
  tokenIn: string;
  tokenOut: string;
  slippageBps?: number;
};

export type RouteProvider = 'paraswap' | 'jupiter' | 'aftermath' | 'liquidswap';

export type Quote = {
  amountIn: bigint;
  amountOut: bigint;
  amountOutUsd: number;
  amountInUsd: number;
  slippage: number;
  priceImpact: number;
  provider: RouteProvider;
  rawQuote: any;
};

export type QuoteParams = {
  sourceChainId: ChainID;
  destChainId: ChainID;
  amount: bigint;
  tokenIn: string;
  tokenOut: string;
};

export type QuoteResponse = {
  amountInUsd: number;
  estimatedAmountInAsMinStablecoinAmount: bigint;

  estimatedAmountOut: bigint;
  estimatedAmountOutUsd: number;

  estimatedAmountOutReduced: bigint;
  estimatedAmountOutUsdReduced: number;
};

export class QuoteProvider {
  private static readonly DEFAULT_BRIDGE_TOKEN = 'USDC';
  private static readonly DEFAULT_BRIDGE_TOKEN_DECIMALS = 6;

  public static async getQuoteFromDefillama(params: QuoteParams): Promise<QuoteResponse> {
    const defillamaQuote = await calculateAmounts({
      amountIn: params.amount,
      tokenIn: params.tokenIn,
      tokenOut: params.tokenOut,
      destChainId: params.destChainId,
      srcChainId: params.sourceChainId,
    });

    return {
      amountInUsd: 0,
      estimatedAmountOutUsd: 0,
      estimatedAmountOutUsdReduced: 0,

      estimatedAmountInAsMinStablecoinAmount: defillamaQuote.minStablecoinsAmount,
      estimatedAmountOut: defillamaQuote.amountOut,
      estimatedAmountOutReduced: defillamaQuote.amountOut,
    };
  }

  public static async getQuoteWithDefillamaFallback(params: QuoteParams): Promise<QuoteResponse> {
    try {
      const quote = await this.getQuoteFromRouters(params);
      return quote;
    } catch (e) {
      const quote = await this.getQuoteFromDefillama(params);
      return quote;
    }
  }

  public static async getQuote(params: QuoteParams): Promise<QuoteResponse> {
    return this.getQuoteWithDefillamaFallback(params);
  }

  private static async getQuoteFromRouters(params: QuoteParams): Promise<QuoteResponse> {
    const bridgeTokenSymbol = QuoteProvider.DEFAULT_BRIDGE_TOKEN;

    const sourceBridgeToken = CROSS_CHAIN_TOKENS[bridgeTokenSymbol]?.[params.sourceChainId]!;
    const destBridgeToken = CROSS_CHAIN_TOKENS[bridgeTokenSymbol]?.[params.destChainId]!;

    const sourceSingleChainQuoteParams: SingleChainQuoteParams = {
      chainId: params.sourceChainId,
      amount: params.amount,
      tokenIn: params.tokenIn,
      tokenOut: sourceBridgeToken,
    };

    const sourceQuote = await this.getSingleChainQuote(sourceSingleChainQuoteParams);

    const destSingleChainQuoteParams: SingleChainQuoteParams = {
      chainId: params.destChainId,
      amount: sourceQuote.amountOut,
      tokenIn: destBridgeToken,
      tokenOut: params.tokenOut,
    };

    const destQuote = await this.getSingleChainQuote(destSingleChainQuoteParams);

    // Use weighted reduction instead of fixed percentage
    const weightedReductionFactor = this.calculateWeightedReductionFactorForUsd(destQuote.amountOutUsd);

    const estimatedAmountOutReduced = BigInt(Math.floor(Number(destQuote.amountOut) * weightedReductionFactor));
    const estimatedAmountOutUsdReduced = destQuote.amountOutUsd * weightedReductionFactor;

    const reducedAmountInUsd = this.calculateWeightedReductionFactorForUsd(sourceQuote.amountInUsd);
    const estimatedAmountInAsMinStablecoinAmount = BigInt(
      Math.floor(reducedAmountInUsd * 10 ** this.DEFAULT_BRIDGE_TOKEN_DECIMALS),
    );

    return {
      estimatedAmountOut: destQuote.amountOut,
      estimatedAmountOutUsd: destQuote.amountOutUsd,
      amountInUsd: sourceQuote.amountInUsd,
      estimatedAmountInAsMinStablecoinAmount,
      estimatedAmountOutReduced,
      estimatedAmountOutUsdReduced,
    };
  }

  public static async getSingleChainQuote(params: SingleChainQuoteParams): Promise<Quote> {
    if (params.tokenIn.toLowerCase() === params.tokenOut.toLowerCase()) {
      let amountUsd = 0;

      if (params.tokenIn.toLowerCase() === CROSS_CHAIN_TOKENS.USDC![params.chainId].toLowerCase()) {
        amountUsd = Number(params.amount) / 10 ** this.DEFAULT_BRIDGE_TOKEN_DECIMALS;
      }

      return {
        amountIn: params.amount,
        amountOut: params.amount,
        amountOutUsd: amountUsd,
        amountInUsd: amountUsd,
        slippage: 0,
        priceImpact: 0,
        provider: getProviderNameByChainId(params.chainId),
        rawQuote: null,
      };
    }

    if (params.chainId === ChainID.Hyperliquid) {
      const liquidSwapQuote = await QuoteProvider.getLiquidSwapQuote(params);
      return QuoteProvider.transformLiquidSwapQuote(liquidSwapQuote);
    }

    if (params.chainId === ChainID.Solana) {
      const jupiterQuote = await QuoteProvider.getJupiterQuote(params);
      return QuoteProvider.transformJupiterQuote(jupiterQuote);
    }

    if (params.chainId === ChainID.Sui) {
      const quote = await QuoteProvider.getAftermathQuote(params);
      return QuoteProvider.transformAftermathQuote({ quote, request: params });
    }

    if (isEvmChain(params.chainId)) {
      const paraswapQuote = await QuoteProvider.getParaswapQuote(params);
      return QuoteProvider.transformParaswapQuote(paraswapQuote);
    }

    throw new Error(`Unsupported chain for quote: ${params.chainId}`);
  }

  static transformLiquidSwapQuote(liquidSwapQuote: LiquidSwapQuoteResponse): Quote {
    return {
      amountIn: BigInt(liquidSwapQuote.amountIn),
      amountOut: BigInt(liquidSwapQuote.amountOut),
      amountOutUsd: 0,
      amountInUsd: 0,
      slippage: 0,
      priceImpact: Number(liquidSwapQuote.averagePriceImpact),
      provider: 'liquidswap',
      rawQuote: liquidSwapQuote,
    };
  }

  /**
   * Calculates the weighted reduction factor for an amount in USD.
   * For amounts <= $5, the reduction factor is 0.65.
   */
  private static calculateWeightedReductionFactorForUsd(amountUsd: number): number {
    if (amountUsd <= 5) {
      return 0.65;
    } else if (amountUsd <= 10) {
      return 0.85;
    } else if (amountUsd <= 20) {
      return 0.90;
    } else if (amountUsd <= 30) {
      return 0.92;
    } else {
      return 0.95;
    }
  }

  protected static async getLiquidSwapQuote(params: SingleChainQuoteParams) {
    const liquidSwapQuoter = new LiquidSwapQuoteProvider();

    return liquidSwapQuoter.getQuote({
      tokenIn: params.tokenIn,
      tokenOut: params.tokenOut,
      amountIn: params.amount.toString(),
    });
  }

  protected static async getJupiterQuote(params: SingleChainQuoteParams) {
    const jupiterQuoter = new JupiterQuoteProvider();

    return jupiterQuoter.getQuote({
      amount: params.amount,
      tokenIn: params.tokenIn,
      tokenOut: params.tokenOut,
      swapMode: 'ExactIn',
      slippageBps: params.slippageBps,
    });
  }

  protected static async getParaswapQuote(params: SingleChainQuoteParams) {
    const paraswapQuoter = new ParaswapQuoteProvider(params.chainId as SupportedEvmChain);

    return paraswapQuoter.getSwapEstimation({
      amount: BigInt(params.amount),
      srcToken: params.tokenIn,
      destToken: params.tokenOut,
      side: 'SELL',
    });
  }

  protected static async getAftermathQuote(params: SingleChainQuoteParams) {
    const aftermathQuoter = new AftermathQuoteProvider();

    return aftermathQuoter.getQuote({
      amountIn: params.amount,
      tokenIn: params.tokenIn,
      tokenOut: params.tokenOut,
    });
  }

  private static transformAftermathQuote({
    quote,
    request,
  }: {
    quote: RouterCompleteTradeRoute;
    request: SingleChainQuoteParams;
  }): Quote {
    let amountUsd = 0;

    if (request.tokenIn === CROSS_CHAIN_TOKENS.USDC![request.chainId]) {
      amountUsd = Number(quote.coinIn.amount) / 10 ** this.DEFAULT_BRIDGE_TOKEN_DECIMALS;
    }

    if (request.tokenOut === CROSS_CHAIN_TOKENS.USDC![request.chainId]) {
      amountUsd = Number(quote.coinOut.amount) / 10 ** this.DEFAULT_BRIDGE_TOKEN_DECIMALS;
    }

    return {
      amountIn: quote.coinIn.amount,
      amountOut: quote.coinOut.amount,
      amountOutUsd: amountUsd,
      amountInUsd: amountUsd,
      slippage: quote.slippage ?? 0,
      priceImpact: 0,
      provider: 'aftermath',
      rawQuote: quote,
    };
  }

  private static transformParaswapQuote(paraswapQuote: SwapEstimateResult): Quote {
    return {
      amountIn: BigInt(paraswapQuote.amountIn),
      amountOut: BigInt(paraswapQuote.amountOut),
      amountOutUsd: paraswapQuote.amountOutUsd,
      amountInUsd: paraswapQuote.amountInUsd,
      slippage: 0, // Calculate from paraswap data if available
      priceImpact: paraswapQuote.quote.maxImpact ? Number(paraswapQuote.quote.maxImpact) * 100 : 0,
      provider: 'paraswap',
      rawQuote: paraswapQuote,
    };
  }

  private static transformJupiterQuote({ quote }: { quote: JupiterQuoteResponse }): Quote {
    const amountIn = Number(quote.inAmount);
    const amountOut = Number(quote.outAmount);
    let amountUsd = 0;
    const priceImpact = quote.priceImpactPct ? Number(quote.priceImpactPct) * 100 : 0;

    if (quote.outputMint === CROSS_CHAIN_TOKENS.USDC![ChainID.Solana]) {
      amountUsd = amountOut / 10 ** this.DEFAULT_BRIDGE_TOKEN_DECIMALS;
    }

    if (quote.inputMint === CROSS_CHAIN_TOKENS.USDC![ChainID.Solana]) {
      amountUsd = amountIn / 10 ** this.DEFAULT_BRIDGE_TOKEN_DECIMALS;
    }

    return {
      amountIn: BigInt(amountIn),
      amountOut: BigInt(amountOut),
      amountOutUsd: amountUsd,
      amountInUsd: amountUsd,
      slippage: quote.slippageBps ? quote.slippageBps / 100 : 0,
      priceImpact,
      provider: 'jupiter',
      rawQuote: quote,
    };
  }
}

function getProviderNameByChainId(chainId: ChainID): RouteProvider {
  switch (chainId) {
    case ChainID.Sui:
      return 'aftermath';
    case ChainID.Solana:
      return 'jupiter';
    case ChainID.Hyperliquid:
      return 'liquidswap';
    default:
      return 'paraswap';
  }
}
