import { Router, Request, Response } from 'express';
import { PoolRepository } from '../../database/repositories/pool-repo';
import { cacheMiddleware } from '../middleware/cache';
import { logger } from '../../utils/logger';
import { filterPoolsForRouting } from '../../config/gru-transport';
import { resolveCanonicalQuoteAddress } from '../../config/canonical-tokens';
import { getLiveDodoPools } from '../../services/live-dodo-fallback';
import {
  pmmQuoteAmountOutFromChain,
  resolvePmmQuoteRpcUrl,
  resolvePmmQuoteTrader,
} from '../../services/pmm-onchain-quote';

const router: Router = Router();
const poolRepo = new PoolRepository();

/**
 * Uniswap V2-style constant-product quote: amountOut = (reserveOut * amountIn * 997) / (reserveIn * 1000 + amountIn * 997)
 *
 * Note: DODO PMM pools do not follow this curve; `amountOut` for dexType `dodo` can diverge from
 * on-chain `querySellBase` / `querySellQuote`. Clients that set swap minOut from this endpoint alone
 * may revert (wallets show failed gas estimation). Prefer on-chain PMM quotes for execution bounds.
 */
function quoteAmountOut(
  amountIn: bigint,
  reserveIn: bigint,
  reserveOut: bigint
): bigint {
  if (reserveIn === BigInt(0)) {
    return BigInt(0);
  }
  const amountInWithFee = amountIn * BigInt(997);
  const numerator = reserveOut * amountInWithFee;
  const denominator = reserveIn * BigInt(1000) + amountInWithFee;
  return numerator / denominator;
}

/**
 * GET /api/v1/quote
 * Returns an estimated amountOut for a token swap (constant-product from first available pool).
 * Query: chainId, tokenIn, tokenOut, amountIn (raw amount in token's smallest unit).
 */
router.get(
  '/quote',
  cacheMiddleware(60 * 1000),
  async (req: Request, res: Response) => {
    try {
      const chainId = parseInt(req.query.chainId as string, 10);
      const tokenIn = (req.query.tokenIn as string)?.toLowerCase();
      const tokenOut = (req.query.tokenOut as string)?.toLowerCase();
      const amountInRaw = req.query.amountIn as string;

      if (!chainId || !tokenIn || !tokenOut || amountInRaw === undefined) {
        return res.status(400).json({
          error: 'Missing required query: chainId, tokenIn, tokenOut, amountIn',
          amountOut: null,
          quoteEngine: null,
        });
      }

      let amountIn: bigint;
      try {
        amountIn = BigInt(amountInRaw);
      } catch {
        return res.status(400).json({
          error: 'Invalid amountIn (must be integer string)',
          amountOut: null,
          quoteEngine: null,
        });
      }

      if (tokenIn === tokenOut) {
        return res.json({
          amountOut: amountInRaw,
          poolAddress: null,
          quoteEngine: 'identity',
        });
      }

      const tokenInResolution = resolveCanonicalQuoteAddress(chainId, tokenIn);
      const tokenOutResolution = resolveCanonicalQuoteAddress(chainId, tokenOut);
      const indexedPoolsRaw = await poolRepo.getPoolsByToken(
        chainId,
        tokenInResolution.lookupAddress
      );
      const indexedPools = filterPoolsForRouting(chainId, indexedPoolsRaw ?? []);
      const livePools =
        indexedPools.length > 0
          ? []
          : filterPoolsForRouting(chainId, (await getLiveDodoPools(chainId)) ?? []).filter(
              (pool) =>
                pool.token0Address.toLowerCase() === tokenInResolution.lookupAddress ||
                pool.token1Address.toLowerCase() === tokenInResolution.lookupAddress
            );
      const pairPools = [...indexedPools, ...livePools].filter(
        (p) =>
          p.token0Address.toLowerCase() === tokenOutResolution.lookupAddress ||
          p.token1Address.toLowerCase() === tokenOutResolution.lookupAddress
      );

      if (pairPools.length === 0) {
        return res.json({
          amountOut: null,
          error: 'No pool found for this token pair',
          poolAddress: null,
          quoteEngine: null,
          canonicalLiquidity: {
            requestedTokenInAddress: tokenInResolution.requestedAddress,
            requestedTokenOutAddress: tokenOutResolution.requestedAddress,
            requestedTokenInSymbol: tokenInResolution.requestedSymbol,
            requestedTokenOutSymbol: tokenOutResolution.requestedSymbol,
            lookupTokenInAddress: tokenInResolution.lookupAddress,
            lookupTokenOutAddress: tokenOutResolution.lookupAddress,
            lookupTokenInSymbol: tokenInResolution.lookupSymbol,
            lookupTokenOutSymbol: tokenOutResolution.lookupSymbol,
            usedFallback: tokenInResolution.usedFallback || tokenOutResolution.usedFallback,
          },
        });
      }

      let bestAmountOut = BigInt(0);
      let bestPool = pairPools[0];

      for (const pool of pairPools) {
        const reserveIn =
          pool.token0Address.toLowerCase() === tokenInResolution.lookupAddress
            ? BigInt(pool.reserve0)
            : BigInt(pool.reserve1);
        const reserveOut =
          pool.token0Address.toLowerCase() === tokenOutResolution.lookupAddress
            ? BigInt(pool.reserve0)
            : BigInt(pool.reserve1);
        const out = quoteAmountOut(amountIn, reserveIn, reserveOut);
        if (out > bestAmountOut) {
          bestAmountOut = out;
          bestPool = pool;
        }
      }

      let quoteEngine: 'constant-product' | 'pmm-onchain' = 'constant-product';
      const pmmRpc = resolvePmmQuoteRpcUrl();
      if (chainId === 138 && bestPool.dexType === 'dodo' && pmmRpc) {
        const onChainOut = await pmmQuoteAmountOutFromChain({
          rpcUrl: pmmRpc,
          poolAddress: bestPool.poolAddress,
          tokenInLookup: tokenInResolution.lookupAddress,
          amountIn,
          traderForView: resolvePmmQuoteTrader(),
        });
        if (onChainOut !== null && onChainOut > BigInt(0)) {
          bestAmountOut = onChainOut;
          quoteEngine = 'pmm-onchain';
        }
      }

      res.json({
        amountOut: bestAmountOut.toString(),
        poolAddress: bestPool.poolAddress,
        dexType: bestPool.dexType,
        executorAddress: bestPool.factoryAddress || null,
        quoteEngine,
        canonicalLiquidity: {
          requestedTokenInAddress: tokenInResolution.requestedAddress,
          requestedTokenOutAddress: tokenOutResolution.requestedAddress,
          requestedTokenInSymbol: tokenInResolution.requestedSymbol,
          requestedTokenOutSymbol: tokenOutResolution.requestedSymbol,
          lookupTokenInAddress: tokenInResolution.lookupAddress,
          lookupTokenOutAddress: tokenOutResolution.lookupAddress,
          lookupTokenInSymbol: tokenInResolution.lookupSymbol,
          lookupTokenOutSymbol: tokenOutResolution.lookupSymbol,
          usedFallback: tokenInResolution.usedFallback || tokenOutResolution.usedFallback,
        },
      });
    } catch (error) {
      logger.error('Quote error:', error);
      res.status(500).json({
        error: error instanceof Error ? error.message : 'Internal server error',
        amountOut: null,
        quoteEngine: null,
      });
    }
  }
);

export default router;
