import { Router, Request, Response } from 'express';
import { TokenRepository, Token } from '../../database/repositories/token-repo';
import { MarketDataRepository } from '../../database/repositories/market-data-repo';
import { PoolRepository, LiquidityPool } from '../../database/repositories/pool-repo';
import { OHLCVGenerator } from '../../indexer/ohlcv-generator';
import { CoinGeckoAdapter } from '../../adapters/coingecko-adapter';
import { CoinMarketCapAdapter } from '../../adapters/cmc-adapter';
import { DexScreenerAdapter } from '../../adapters/dexscreener-adapter';
import { cacheMiddleware } from '../middleware/cache';
import { resolvePoolTokenDisplays, resolveTokenDisplay } from '../../services/token-display';
import { logger } from '../../utils/logger';
import { filterPoolsForExposure, shouldExposePublicPool } from '../../config/gru-transport';
import {
  getCanonicalTokenByAddress,
  getCanonicalTokensByChain,
  resolveCanonicalQuoteAddress,
} from '../../config/canonical-tokens';
import { getLiveDodoPools } from '../../services/live-dodo-fallback';
import {
  buildExplorerLinks,
  mergeMarketWithValuation,
  resolveUsdValuation,
} from '../../services/valuation-precedence';

const router: Router = Router();
const tokenRepo = new TokenRepository();
const marketDataRepo = new MarketDataRepository();
const poolRepo = new PoolRepository();
const ohlcvGenerator = new OHLCVGenerator();
const coingeckoAdapter = new CoinGeckoAdapter();
const cmcAdapter = new CoinMarketCapAdapter();
const dexscreenerAdapter = new DexScreenerAdapter();

function buildMarketPricingExplorer(
  chainId: number,
  displayAddress: string,
  lookupAddress: string,
  marketData: Awaited<ReturnType<MarketDataRepository['getMarketData']>>,
  external: { coingecko?: Awaited<ReturnType<CoinGeckoAdapter['getMarketData']>>; cmc?: Awaited<ReturnType<CoinMarketCapAdapter['getMarketData']>>; dexscreener?: Awaited<ReturnType<DexScreenerAdapter['getMarketData']>> } | null
) {
  const pricing = resolveUsdValuation({
    chainId,
    normalizedAddress: lookupAddress.toLowerCase(),
    indexer: marketData,
    coingecko: external?.coingecko ?? undefined,
    cmc: external?.cmc ?? undefined,
    dexscreener: external?.dexscreener ?? undefined,
  });
  const market = mergeMarketWithValuation(chainId, displayAddress.toLowerCase(), marketData, pricing);
  const explorer = buildExplorerLinks(chainId, displayAddress);
  return { market, pricing, explorer };
}

function tokenFromCanonical(chainId: number, address: string): Token | null {
  const spec = getCanonicalTokenByAddress(chainId, address.toLowerCase());
  if (!spec) {
    return null;
  }

  return {
    chainId,
    address: address.toLowerCase(),
    name: spec.name,
    symbol: spec.symbol,
    decimals: spec.decimals,
    verified: true,
  };
}

async function getPoolsByTokenWithFallback(chainId: number, address: string): Promise<LiquidityPool[]> {
  const normalized = address.toLowerCase();
  const resolution = resolveCanonicalQuoteAddress(chainId, normalized);
  let dbPools: LiquidityPool[] = [];
  try {
    dbPools = filterPoolsForExposure(
      chainId,
      await poolRepo.getPoolsByToken(chainId, resolution.lookupAddress)
    );
  } catch (error) {
    logger.warn('DB pool lookup failed; using live DODO fallback', { chainId, address: resolution.lookupAddress, error });
  }
  if (dbPools.length > 0) {
    return dbPools;
  }

  const livePools = filterPoolsForExposure(chainId, await getLiveDodoPools(chainId));
  return livePools.filter(
    (pool) =>
      pool.token0Address === resolution.lookupAddress ||
      pool.token1Address === resolution.lookupAddress ||
      pool.token0Address === normalized ||
      pool.token1Address === normalized
  );
}

async function getTokenWithFallback(chainId: number, address: string): Promise<Token | null> {
  const normalized = address.toLowerCase();
  const token = await tokenRepo.getToken(chainId, normalized);
  if (token) {
    return token;
  }

  const canonical = tokenFromCanonical(chainId, normalized);
  if (canonical) {
    return canonical;
  }

  const resolution = resolveCanonicalQuoteAddress(chainId, normalized);
  const livePools = await getLiveDodoPools(chainId);
  const liveAddress =
    livePools.find((pool) => pool.token0Address === resolution.lookupAddress || pool.token1Address === resolution.lookupAddress)
      ? resolution.lookupAddress
      : null;
  if (!liveAddress) {
    return null;
  }

  const display = await resolveTokenDisplay(tokenRepo, chainId, liveAddress);
  return {
    chainId,
    address: normalized,
    name: display.name,
    symbol: display.symbol,
    decimals: display.decimals,
    verified: display.source !== 'fallback',
  };
}

async function getTokensWithFallback(
  chainId: number,
  limit: number,
  offset: number
): Promise<{ tokens: Token[]; source: 'db' | 'live-dodo' | 'canonical' }> {
  const dbTokens = await tokenRepo.getTokens(chainId, limit, offset);
  if (dbTokens.length > 0) {
    return { tokens: dbTokens, source: 'db' };
  }

  const livePools = await getLiveDodoPools(chainId);
  if (livePools.length > 0) {
    const tokenAddresses = [...new Set(livePools.flatMap((pool) => [pool.token0Address, pool.token1Address]))];
    const liveTokens = await Promise.all(
      tokenAddresses.map(async (address) => {
        const display = await resolveTokenDisplay(tokenRepo, chainId, address);
        return {
          chainId,
          address: display.address,
          name: display.name,
          symbol: display.symbol,
          decimals: display.decimals,
          verified: display.source !== 'fallback',
        } as Token;
      })
    );
    const sorted = liveTokens.sort((a, b) =>
      `${a.symbol || ''}${a.address}`.localeCompare(`${b.symbol || ''}${b.address}`)
    );
    return { tokens: sorted.slice(offset, offset + limit), source: 'live-dodo' };
  }

  const canonicalTokens = getCanonicalTokensByChain(chainId)
    .map((spec) => ({
      chainId,
      address: String(spec.addresses[chainId]).toLowerCase(),
      name: spec.name,
      symbol: spec.symbol,
      decimals: spec.decimals,
      verified: true,
    }) as Token)
    .sort((a, b) => `${a.symbol || ''}${a.address}`.localeCompare(`${b.symbol || ''}${b.address}`));

  return { tokens: canonicalTokens.slice(offset, offset + limit), source: 'canonical' };
}

router.get('/chains', cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
  try {
    res.json({
      chains: [
        {
          chainId: 138,
          name: 'DeFi Oracle Meta Mainnet',
          explorerUrl: 'https://explorer.d-bis.org',
        },
        {
          chainId: 651940,
          name: 'ALL Mainnet',
          explorerUrl: 'https://alltra.global',
        },
      ],
    });
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

router.get('/tokens', cacheMiddleware(60 * 1000), async (req: Request, res: Response) => {
  try {
    const chainId = parseInt(req.query.chainId as string, 10);
    const limit = parseInt(req.query.limit as string, 10) || 50;
    const offset = parseInt(req.query.offset as string, 10) || 0;
    const includeDodoPool = (req.query.includeDodoPool as string) === '1' || (req.query.includeDodoPool as string) === 'true';

    if (!chainId) {
      return res.status(400).json({ error: 'chainId is required' });
    }

    const { tokens, source } = await getTokensWithFallback(chainId, limit, offset);
    const tokensWithMarketData = await Promise.all(
      tokens.map(async (token) => {
        const resolution = resolveCanonicalQuoteAddress(chainId, token.address);
        const marketData = await marketDataRepo.getMarketData(chainId, resolution.lookupAddress);
        const { market, pricing, explorer } = buildMarketPricingExplorer(
          chainId,
          token.address,
          resolution.lookupAddress,
          marketData,
          null
        );
        const out: Record<string, unknown> = {
          ...token,
          market: market || undefined,
          pricing,
          explorer,
        };
        if (includeDodoPool) {
          const pools = await getPoolsByTokenWithFallback(chainId, token.address);
          const dodoPool = pools.find((p) => (p.dexType || '').toLowerCase() === 'dodo');
          out.hasDodoPool = !!dodoPool;
          out.pmmPool = dodoPool?.poolAddress || undefined;
        }
        return out;
      })
    );

    res.json({
      tokens: tokensWithMarketData,
      pagination: {
        limit,
        offset,
        count: tokensWithMarketData.length,
      },
      source,
    });
  } catch (error) {
    logger.error('Error fetching tokens:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

router.get('/tokens/:address', cacheMiddleware(60 * 1000), async (req: Request, res: Response) => {
  try {
    const chainId = parseInt(req.query.chainId as string, 10);
    const address = req.params.address;

    if (!chainId) {
      return res.status(400).json({ error: 'chainId is required' });
    }

    const normalizedAddress = address.toLowerCase();
    const resolution = resolveCanonicalQuoteAddress(chainId, normalizedAddress);
    const token = await getTokenWithFallback(chainId, normalizedAddress);
    if (!token) {
      return res.status(404).json({ error: 'Token not found' });
    }

    const [
      marketDataRaw,
      pools,
      coingeckoData,
      cmcData,
      dexscreenerData,
      coingeckoMarket,
      cmcMarket,
      dexscreenerMarket,
    ] = await Promise.all([
      marketDataRepo.getMarketData(chainId, resolution.lookupAddress),
      getPoolsByTokenWithFallback(chainId, normalizedAddress),
      coingeckoAdapter.getTokenByContract(chainId, resolution.lookupAddress),
      cmcAdapter.getTokenByContract(chainId, resolution.lookupAddress),
      dexscreenerAdapter.getTokenByContract(chainId, resolution.lookupAddress),
      coingeckoAdapter.getMarketData(chainId, resolution.lookupAddress),
      cmcAdapter.getMarketData(chainId, resolution.lookupAddress),
      dexscreenerAdapter.getMarketData(chainId, resolution.lookupAddress),
    ]);
    const { market: marketData, pricing, explorer } = buildMarketPricingExplorer(
      chainId,
      normalizedAddress,
      resolution.lookupAddress,
      marketDataRaw,
      { coingecko: coingeckoMarket, cmc: cmcMarket, dexscreener: dexscreenerMarket }
    );

    res.json({
      token: {
        ...token,
        onChain: {
          totalSupply: token.totalSupply,
        },
        market: marketData || undefined,
        pricing,
        explorer,
        external: {
          coingecko: coingeckoData || undefined,
          cmc: cmcData || undefined,
          dexscreener: dexscreenerData || undefined,
        },
        pools: pools.map((pool) => ({
          address: pool.poolAddress,
          dex: pool.dexType,
          token0: pool.token0Address,
          token1: pool.token1Address,
          reserves: {
            token0: pool.reserve0,
            token1: pool.reserve1,
          },
          tvl: pool.totalLiquidityUsd,
          volume24h: pool.volume24h,
        })),
        hasDodoPool: pools.some((p) => (p.dexType || '').toLowerCase() === 'dodo'),
        pmmPool: pools.find((p) => (p.dexType || '').toLowerCase() === 'dodo')?.poolAddress || undefined,
        canonicalLiquidity:
          resolution.usedFallback
            ? {
                requestedAddress: normalizedAddress,
                lookupAddress: resolution.lookupAddress,
                requestedSymbol: resolution.requestedSymbol,
                lookupSymbol: resolution.lookupSymbol,
              }
            : undefined,
      },
    });
  } catch (error) {
    logger.error('Error fetching token:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

router.get('/tokens/:address/pools', cacheMiddleware(60 * 1000), async (req: Request, res: Response) => {
  try {
    const chainId = parseInt(req.query.chainId as string, 10);
    const address = req.params.address;

    if (!chainId) {
      return res.status(400).json({ error: 'chainId is required' });
    }

    let pools: LiquidityPool[];
    try {
      pools = await getPoolsByTokenWithFallback(chainId, address);
    } catch (error) {
      logger.error('Error resolving pools list:', error);
      pools = [];
    }

    const settled = await Promise.allSettled(
      pools.map(async (pool) => {
        const { token0, token1 } = await resolvePoolTokenDisplays(tokenRepo, chainId, pool.token0Address, pool.token1Address);
        return {
          address: pool.poolAddress,
          dex: String(pool.dexType ?? ''),
          token0: {
            address: pool.token0Address,
            symbol: token0.symbol,
            name: token0.name,
            source: token0.source,
          },
          token1: {
            address: pool.token1Address,
            symbol: token1.symbol,
            name: token1.name,
            source: token1.source,
          },
          reserves: {
            token0: pool.reserve0,
            token1: pool.reserve1,
          },
          tvl: pool.totalLiquidityUsd,
          volume24h: pool.volume24h,
          feeTier: pool.feeTier,
        };
      })
    );

    const poolsOut = [];
    for (const row of settled) {
      if (row.status === 'fulfilled') {
        poolsOut.push(row.value);
      } else {
        logger.warn('Skipping pool row in /tokens/:address/pools:', row.reason);
      }
    }

    // BigInt (e.g. from live RPC paths) breaks res.json; stringify replacer keeps Mission Control / E2E stable.
    const payload = JSON.parse(
      JSON.stringify({ pools: poolsOut }, (_key, value) => (typeof value === 'bigint' ? value.toString() : value))
    ) as { pools: typeof poolsOut };
    res.json(payload);
  } catch (error) {
    logger.error('Error fetching pools:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

router.get('/tokens/:address/ohlcv', cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
  try {
    const chainId = parseInt(req.query.chainId as string, 10);
    const address = req.params.address;
    const interval = (req.query.interval as string) || '1h';
    const from = req.query.from ? new Date(req.query.from as string) : new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
    const to = req.query.to ? new Date(req.query.to as string) : new Date();
    const poolAddress = req.query.poolAddress as string | undefined;

    if (!chainId) {
      return res.status(400).json({ error: 'chainId is required' });
    }

    if (!['5m', '15m', '1h', '4h', '24h'].includes(interval)) {
      return res.status(400).json({ error: 'Invalid interval. Must be one of: 5m, 15m, 1h, 4h, 24h' });
    }

    const ohlcv = await ohlcvGenerator.getOHLCV(
      chainId,
      address,
      interval as '5m' | '15m' | '1h' | '4h' | '24h',
      from,
      to,
      poolAddress
    );

    res.json({
      chainId,
      tokenAddress: address,
      interval,
      data: ohlcv,
    });
  } catch (error) {
    logger.error('Error fetching OHLCV:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

router.get('/tokens/:address/signals', cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
  try {
    const chainId = parseInt(req.query.chainId as string, 10);
    const address = req.params.address;

    if (!chainId) {
      return res.status(400).json({ error: 'chainId is required' });
    }

    const trending = await coingeckoAdapter.getTrending();

    res.json({
      chainId,
      tokenAddress: address,
      signals: {
        trendingRank: trending.findIndex((t) => t.symbol.toLowerCase() === address.toLowerCase()),
      },
    });
  } catch (error) {
    logger.error('Error fetching signals:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

router.get('/search', cacheMiddleware(60 * 1000), async (req: Request, res: Response) => {
  try {
    const chainId = parseInt(req.query.chainId as string, 10);
    const query = req.query.q as string;

    if (!chainId || !query) {
      return res.status(400).json({ error: 'chainId and q (query) are required' });
    }

    const tokens = await tokenRepo.searchTokens(chainId, query, 20);

    res.json({
      query,
      chainId,
      results: tokens,
    });
  } catch (error) {
    logger.error('Error searching tokens:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

router.get('/pools/:poolAddress', cacheMiddleware(60 * 1000), async (req: Request, res: Response) => {
  try {
    const chainId = parseInt(req.query.chainId as string, 10);
    const poolAddress = req.params.poolAddress;

    if (!chainId) {
      return res.status(400).json({ error: 'chainId is required' });
    }

    const pool = await poolRepo.getPool(chainId, poolAddress);
    if (
      !pool ||
      !shouldExposePublicPool(chainId, pool.poolAddress, pool.token0Address, pool.token1Address)
    ) {
      return res.status(404).json({ error: 'Pool not found' });
    }

    res.json({
      pool: {
        address: pool.poolAddress,
        dex: pool.dexType,
        token0: {
          address: pool.token0Address,
        },
        token1: {
          address: pool.token1Address,
        },
        reserves: {
          token0: pool.reserve0,
          token1: pool.reserve1,
        },
        tvl: pool.totalLiquidityUsd,
        volume24h: pool.volume24h,
        feeTier: pool.feeTier,
      },
    });
  } catch (error) {
    logger.error('Error fetching pool:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

export default router;
