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,
  getCanonicalTokenBySymbol,
  resolveCanonicalQuoteAddress,
} from '../../config/canonical-tokens';
import { resolveCanonicalPriceUsd } from '../../services/canonical-price-oracle';
import { getLiveDodoPools } from '../../services/live-dodo-fallback';
import {
  buildExplorerLinks,
  mergeMarketWithValuation,
  resolveUsdValuation,
} from '../../services/valuation-precedence';
import { fetchBlockscoutTokenMeta } from '../../services/blockscout-token-meta';
import { refreshChain138LiveReferencePrices } from '../../services/chain138-onchain-reference-prices';
import { MAX_SINGLE_POOL_TVL_USD, resolveVisibleTokenLiquidityUsd } from '../../services/token-visible-liquidity';
import { resolveTokenCashLiquidityUsd } from '../../services/token-cash-liquidity';
import { isChain138CashUsdTokenAddress } from '../../config/chain138-cash-tokens';
import { isChain138IssuedCStarTokenAddress } from '../../config/chain138-gru-cstar-tokens';
import {
  evaluateSupplyNostroMatch,
  getCachedCashInBankAuditSnapshot,
  mergeCashInBankAuditWithSupplyMatch,
  type CashInBankAuditInfo,
  type SupplyNostroMatchInfo,
} from '../../services/omnl-cash-in-bank-audit';
import { getCachedHoReserveCoverageSnapshot, type HoReserveCoverageInfo } from '../../services/chain138-ho-reserve-coverage';

const router: Router = Router();
const tokenRepo = new TokenRepository();
const marketDataRepo = new MarketDataRepository();
const poolRepo = new PoolRepository();
const ohlcvGenerator = new OHLCVGenerator();

type HistoricalPriceSource =
  | 'swap_event'
  | 'ohlcv_5m'
  | 'ohlcv_15m'
  | 'ohlcv_1h'
  | 'ohlcv_4h'
  | 'ohlcv_24h'
  | 'coingecko_history'
  | 'current_market_fallback'
  | 'canonical_fallback'
  | 'unavailable';
const coingeckoAdapter = new CoinGeckoAdapter();
const cmcAdapter = new CoinMarketCapAdapter();
const dexscreenerAdapter = new DexScreenerAdapter();

function firstString(value: string | string[] | undefined): string | undefined {
  return Array.isArray(value) ? value[0] : value;
}

type HistoricalPriceSnapshot = {
  chainId: number;
  tokenAddress: string;
  requestedTimestamp: string;
  effectiveTimestamp?: string;
  priceUsd?: number;
  source: HistoricalPriceSource;
};

type CurrentPriceSnapshot = {
  chainId: number;
  tokenAddress: string;
  priceUsd?: number;
  asOf?: string;
  sourceLayer: string;
  stale: boolean;
  marketLastUpdated?: string;
};

const HISTORICAL_INTERVAL_MAX_AGE_MS: Record<'5m' | '15m' | '1h' | '4h' | '24h', number> = {
  '5m': 15 * 60 * 1000,
  '15m': 45 * 60 * 1000,
  '1h': 3 * 60 * 60 * 1000,
  '4h': 12 * 60 * 60 * 1000,
  '24h': 36 * 60 * 60 * 1000,
};

const COINGECKO_HISTORY_MAX_SKEW_MS = 6 * 60 * 60 * 1000;

function isAcceptableHistoricalTimestamp(
  candidateTimestamp: Date,
  requestedTimestamp: Date,
  maxAgeMs: number
): boolean {
  const deltaMs = requestedTimestamp.getTime() - candidateTimestamp.getTime();
  return deltaMs >= 0 && deltaMs <= maxAgeMs;
}

type ExternalMarketFeeds = {
  coingecko?: Awaited<ReturnType<CoinGeckoAdapter['getMarketData']>>;
  cmc?: Awaited<ReturnType<CoinMarketCapAdapter['getMarketData']>>;
  dexscreener?: Awaited<ReturnType<DexScreenerAdapter['getMarketData']>>;
};

async function enrichExternalFeedsWithReferencePrice(
  chainId: number,
  lookupAddress: string,
  external: ExternalMarketFeeds | null
): Promise<ExternalMarketFeeds> {
  const feeds: ExternalMarketFeeds = { ...(external ?? {}) };
  if (feeds.coingecko?.priceUsd != null && feeds.coingecko.priceUsd > 0) {
    return feeds;
  }

  const canonical = resolveCanonicalPriceUsd(chainId, lookupAddress.toLowerCase());
  if (!canonical.referenceSymbol) {
    return feeds;
  }

  const referenceMarket = await coingeckoAdapter.getReferenceMarketData(canonical.referenceSymbol);
  if (referenceMarket?.priceUsd != null && referenceMarket.priceUsd > 0) {
    feeds.coingecko = referenceMarket;
  }

  return feeds;
}

function buildMarketPricingExplorer(
  chainId: number,
  displayAddress: string,
  lookupAddress: string,
  marketData: Awaited<ReturnType<MarketDataRepository['getMarketData']>>,
  external: ExternalMarketFeeds | 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 decimalStringToNumber(value?: string, decimals?: number): number | null {
  if (!value || decimals === undefined || decimals < 0) return null;
  try {
    const raw = BigInt(value);
    if (raw <= 0n) return 0;
    const scale = 10n ** BigInt(decimals);
    const whole = raw / scale;
    const fraction = raw % scale;
    const normalized = Number(whole) + Number(fraction) / Number(scale);
    return Number.isFinite(normalized) ? normalized : null;
  } catch {
    return null;
  }
}

function normalizePossiblyRawLiquidityUsd<T extends { liquidityUsd: number; priceUsd?: number }>(
  market: T | null,
  token: Token
): T | null {
  if (!market || !(market.liquidityUsd > 0)) return market;
  const decimals = Number(token.decimals);
  if (!Number.isInteger(decimals) || decimals <= 0) return market;

  const supplyUnits = decimalStringToNumber(token.totalSupply, decimals);
  const priceUsd = market.priceUsd && market.priceUsd > 0 ? market.priceUsd : 1;
  if (supplyUnits === null || supplyUnits <= 0) return market;

  const plausibleSupplyValue = supplyUnits * priceUsd;
  const scale = 10 ** decimals;
  const normalizedLiquidity = market.liquidityUsd / scale;

  if (
    Number.isFinite(plausibleSupplyValue) &&
    Number.isFinite(normalizedLiquidity) &&
    market.liquidityUsd > plausibleSupplyValue &&
    normalizedLiquidity <= plausibleSupplyValue
  ) {
    return {
      ...market,
      liquidityUsd: normalizedLiquidity,
    };
  }

  return market;
}

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) {
    const display = await resolveTokenDisplay(tokenRepo, chainId, liveAddress);
    return {
      chainId,
      address: normalized,
      name: display.name,
      symbol: display.symbol,
      decimals: display.decimals,
      verified: display.source !== 'fallback',
    };
  }

  const blockscoutMeta = await fetchBlockscoutTokenMeta(chainId, normalized);
  if (blockscoutMeta) {
    return {
      chainId,
      address: normalized,
      name: blockscoutMeta.name,
      symbol: blockscoutMeta.symbol,
      decimals: blockscoutMeta.decimals,
      totalSupply: blockscoutMeta.totalSupply,
      verified: false,
    };
  }

  return null;
}

export interface TokenMarketSnapshot {
  address: string;
  symbol?: string;
  name?: string;
  decimals?: number;
  priceUsd?: number;
  liquidityUsd?: number;
  cashLiquidityUsd?: number;
  cashInBankUsd?: number;
  cashInBankAudit?: CashInBankAuditInfo;
  totalSupplyUsd?: number;
  supplyNostroMatch?: SupplyNostroMatchInfo;
  hoReserveCoverage?: HoReserveCoverageInfo;
  volume24h?: number;
  lastUpdated?: string;
  source?: string;
  blockscoutExchangeRate?: number;
  pricingKind?: 'spot' | 'lp-share';
  isCanonicalClone?: boolean;
}

function isLikelyLpReceiptSymbol(symbol: string | undefined): boolean {
  if (!symbol) {
    return false;
  }
  const upper = symbol.toUpperCase();
  return upper.includes('DLP') || upper.endsWith('-LP') || upper.startsWith('LP-') || upper === 'LP';
}

async function resolveTokenRow(
  chainId: number,
  normalizedAddress: string,
  hint?: { symbol?: string; name?: string; decimals?: number },
): Promise<Token | null> {
  const fromRepo = await getTokenWithFallback(chainId, normalizedAddress);
  if (fromRepo) {
    return fromRepo;
  }

  const blockscoutMeta = await fetchBlockscoutTokenMeta(chainId, normalizedAddress);
  if (blockscoutMeta) {
    return {
      chainId,
      address: normalizedAddress,
      name: blockscoutMeta.name,
      symbol: blockscoutMeta.symbol,
      decimals: blockscoutMeta.decimals,
      totalSupply: blockscoutMeta.totalSupply,
      verified: false,
    };
  }

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

  if (hint?.symbol) {
    return {
      chainId,
      address: normalizedAddress,
      name: hint.name || hint.symbol,
      symbol: hint.symbol,
      decimals: Number.isInteger(hint.decimals) ? Number(hint.decimals) : 18,
      verified: false,
    };
  }

  return null;
}

async function resolveSymbolFallbackPriceUsd(
  chainId: number,
  normalizedAddress: string,
  symbol: string | undefined,
): Promise<number | undefined> {
  if (!symbol) {
    return undefined;
  }

  const symbolSpec = getCanonicalTokenBySymbol(chainId, symbol);
  if (!symbolSpec) {
    return undefined;
  }

  const canonicalAddress = symbolSpec.addresses?.[chainId]?.toLowerCase();
  if (canonicalAddress && canonicalAddress !== normalizedAddress) {
    const canonicalSnapshot = await resolveTokenMarketSnapshot(chainId, canonicalAddress, {
      allowSymbolFallback: false,
    });
    return canonicalSnapshot?.priceUsd;
  }

  const canonicalPrice = resolveCanonicalPriceUsd(chainId, normalizedAddress);
  return canonicalPrice.priceUsd;
}

async function resolveTokenMarketSnapshot(
  chainId: number,
  address: string,
  options?: { allowSymbolFallback?: boolean; hint?: { symbol?: string; name?: string; decimals?: number } },
): Promise<TokenMarketSnapshot | null> {
  const normalizedAddress = address.toLowerCase();
  const allowSymbolFallback = options?.allowSymbolFallback !== false;
  const resolution = resolveCanonicalQuoteAddress(chainId, normalizedAddress);
  const token = await resolveTokenRow(chainId, normalizedAddress, options?.hint);
  if (!token) {
    return null;
  }

  const canonicalSpec = getCanonicalTokenByAddress(chainId, normalizedAddress);
  const symbolSpec = token.symbol ? getCanonicalTokenBySymbol(chainId, token.symbol) : undefined;
  const canonicalAddressForSymbol = symbolSpec?.addresses?.[chainId]?.toLowerCase();
  const isCanonicalClone = Boolean(
    symbolSpec &&
      canonicalAddressForSymbol &&
      canonicalAddressForSymbol !== normalizedAddress &&
      !canonicalSpec,
  );
  const pricingKind = isLikelyLpReceiptSymbol(token.symbol) ? 'lp-share' : 'spot';

  const [marketDataRaw, blockscoutMeta, coingeckoMarket, cmcMarket, dexscreenerMarket] = await Promise.all([
    marketDataRepo.getMarketData(chainId, resolution.lookupAddress),
    fetchBlockscoutTokenMeta(chainId, normalizedAddress),
    coingeckoAdapter.getMarketData(chainId, resolution.lookupAddress),
    cmcAdapter.getMarketData(chainId, resolution.lookupAddress),
    dexscreenerAdapter.getMarketData(chainId, resolution.lookupAddress),
  ]);

  const externalFeeds = await enrichExternalFeedsWithReferencePrice(chainId, resolution.lookupAddress, {
    coingecko: coingeckoMarket,
    cmc: cmcMarket,
    dexscreener: dexscreenerMarket,
  });
  const { market: marketData, pricing } = buildMarketPricingExplorer(
    chainId,
    normalizedAddress,
    resolution.lookupAddress,
    marketDataRaw,
    externalFeeds,
  );
  const market = normalizePossiblyRawLiquidityUsd(marketData, token);

  let priceUsd = market?.priceUsd;
  if ((priceUsd == null || !(priceUsd > 0)) && blockscoutMeta?.exchangeRate != null) {
    priceUsd = blockscoutMeta.exchangeRate;
  }
  if (
    allowSymbolFallback &&
    (priceUsd == null || !(priceUsd > 0)) &&
    pricingKind !== 'lp-share'
  ) {
    const fallbackPrice = await resolveSymbolFallbackPriceUsd(chainId, normalizedAddress, token.symbol);
    if (fallbackPrice != null && fallbackPrice > 0) {
      priceUsd = fallbackPrice;
    }
  }

  const MIN_MEANINGFUL_LIQUIDITY_USD = 1000;
  const marketLiquidityRaw =
    market?.liquidityUsd != null && market.liquidityUsd > 0 ? market.liquidityUsd : 0;
  const marketLiquidityUsd =
    marketLiquidityRaw > MAX_SINGLE_POOL_TVL_USD ? 0 : marketLiquidityRaw;
  const poolLiquidityUsd = await resolveVisibleTokenLiquidityUsd(chainId, normalizedAddress);
  const bestLiquidityUsd = Math.max(
    marketLiquidityUsd >= MIN_MEANINGFUL_LIQUIDITY_USD ? marketLiquidityUsd : 0,
    poolLiquidityUsd,
  );
  const liquidityUsd = bestLiquidityUsd > 0 ? bestLiquidityUsd : undefined;
  const cashLiquidityUsdRaw = chainId === 138
    ? await resolveTokenCashLiquidityUsd(chainId, normalizedAddress)
    : 0;
  const cashLiquidityUsd = cashLiquidityUsdRaw > 0 ? cashLiquidityUsdRaw : undefined;

  let cashInBankUsd: number | undefined;
  let cashInBankAudit: CashInBankAuditInfo | undefined;
  let totalSupplyUsd: number | undefined;
  let supplyNostroMatch: SupplyNostroMatchInfo | undefined;
  let hoReserveCoverage: HoReserveCoverageInfo | undefined;
  const supplyUnits = decimalStringToNumber(token.totalSupply, token.decimals);
  const pegPriceUsd = priceUsd != null && priceUsd > 0 ? priceUsd : 1;
  if (supplyUnits != null && supplyUnits > 0) {
    totalSupplyUsd = supplyUnits * pegPriceUsd;
  }
  if (chainId === 138 && isChain138IssuedCStarTokenAddress(normalizedAddress)) {
    try {
      const [bankSnapshot, reserveSnapshot] = await Promise.all([
        getCachedCashInBankAuditSnapshot({ skipTripleState: true }),
        getCachedHoReserveCoverageSnapshot(),
      ]);
      if (bankSnapshot.cashInBankUsd > 0) {
        cashInBankUsd = bankSnapshot.cashInBankUsd;
      }
      supplyNostroMatch = evaluateSupplyNostroMatch({
        totalSupplyUsd,
        externalNostroCashUsd: bankSnapshot.cashInBankUsd,
        headOfficeId: bankSnapshot.officeId,
      });
      cashInBankAudit = mergeCashInBankAuditWithSupplyMatch(bankSnapshot.audit, supplyNostroMatch);
      hoReserveCoverage = reserveSnapshot;
    } catch (error) {
      logger.warn('HO liquidity snapshot unavailable', {
        chainId,
        address: normalizedAddress,
        error: error instanceof Error ? error.message : String(error),
      });
    }
  } else if (chainId === 138 && isChain138CashUsdTokenAddress(normalizedAddress)) {
    try {
      const bankSnapshot = await getCachedCashInBankAuditSnapshot({ skipTripleState: true });
      if (bankSnapshot.cashInBankUsd > 0) {
        cashInBankUsd = bankSnapshot.cashInBankUsd;
      }
      supplyNostroMatch = evaluateSupplyNostroMatch({
        totalSupplyUsd,
        externalNostroCashUsd: bankSnapshot.cashInBankUsd,
        headOfficeId: bankSnapshot.officeId,
      });
      cashInBankAudit = mergeCashInBankAuditWithSupplyMatch(bankSnapshot.audit, supplyNostroMatch);
    } catch (error) {
      logger.warn('Cash-in-bank audit snapshot unavailable', {
        chainId,
        address: normalizedAddress,
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }

  return {
    address: normalizedAddress,
    symbol: token.symbol,
    name: token.name,
    decimals: token.decimals,
    priceUsd: priceUsd != null && priceUsd > 0 ? priceUsd : undefined,
    liquidityUsd,
    cashLiquidityUsd,
    cashInBankUsd,
    cashInBankAudit,
    totalSupplyUsd,
    supplyNostroMatch,
    hoReserveCoverage,
    volume24h: market?.volume24h,
    lastUpdated: (() => {
      const marketLastUpdated = market?.lastUpdated instanceof Date
        ? market.lastUpdated.toISOString()
        : market?.lastUpdated
          ? new Date(market.lastUpdated).toISOString()
          : undefined;
      const auditAt = hoReserveCoverage?.generatedAt;
      const candidates = [auditAt, marketLastUpdated].filter(Boolean) as string[];
      return candidates.length > 0 ? candidates.sort().reverse()[0] : undefined;
    })(),
    source: pricing?.sourceLayer || (blockscoutMeta?.exchangeRate != null ? 'blockscout' : undefined),
    blockscoutExchangeRate: blockscoutMeta?.exchangeRate,
    pricingKind,
    isCanonicalClone,
  };
}

async function resolveHistoricalPriceAt(
  chainId: number,
  address: string,
  timestamp: Date
): Promise<HistoricalPriceSnapshot> {
  const normalizedAddress = address.toLowerCase();
  const resolution = resolveCanonicalQuoteAddress(chainId, normalizedAddress);
  const requestedTimestamp = timestamp.toISOString();

  try {
    const direct5m = await ohlcvGenerator.getOHLCV(
      chainId,
      resolution.lookupAddress,
      '5m',
      new Date(timestamp.getTime() - 10 * 60 * 1000),
      timestamp
    );
    const directCandidate = direct5m[direct5m.length - 1];
    if (
      directCandidate?.close != null &&
      isAcceptableHistoricalTimestamp(
        directCandidate.timestamp,
        timestamp,
        HISTORICAL_INTERVAL_MAX_AGE_MS['5m']
      )
    ) {
      return {
        chainId,
        tokenAddress: normalizedAddress,
        requestedTimestamp,
        effectiveTimestamp: directCandidate.timestamp.toISOString(),
        priceUsd: directCandidate.close,
        source: 'ohlcv_5m',
      };
    }

    const intervalWindows: Array<{ interval: '15m' | '1h' | '4h' | '24h'; windowMs: number }> = [
      { interval: '15m', windowMs: 2 * 60 * 60 * 1000 },
      { interval: '1h', windowMs: 24 * 60 * 60 * 1000 },
      { interval: '4h', windowMs: 7 * 24 * 60 * 60 * 1000 },
      { interval: '24h', windowMs: 60 * 24 * 60 * 60 * 1000 },
    ];

    for (const { interval, windowMs } of intervalWindows) {
      const candles = await ohlcvGenerator.getOHLCV(
        chainId,
        resolution.lookupAddress,
        interval,
        new Date(timestamp.getTime() - windowMs),
        timestamp
      );
      const candidate = candles[candles.length - 1];
      if (
        candidate?.close != null &&
        isAcceptableHistoricalTimestamp(
          candidate.timestamp,
          timestamp,
          HISTORICAL_INTERVAL_MAX_AGE_MS[interval]
        )
      ) {
        return {
          chainId,
          tokenAddress: normalizedAddress,
          requestedTimestamp,
          effectiveTimestamp: candidate.timestamp.toISOString(),
          priceUsd: candidate.close,
          source: `ohlcv_${interval}`,
        };
      }
    }
  } catch (error) {
    logger.warn('Historical OHLCV lookup failed; falling back to current layers', {
      chainId,
      address: resolution.lookupAddress,
      requestedTimestamp,
      error,
    });
  }

  const canonicalResolution = resolveCanonicalPriceUsd(chainId, resolution.lookupAddress);
  if (canonicalResolution.referenceSymbol) {
    const coingeckoHistorical = await coingeckoAdapter.getHistoricalReferencePrice(
      canonicalResolution.referenceSymbol,
      timestamp
    );
    if (
      coingeckoHistorical?.priceUsd != null &&
      isAcceptableHistoricalTimestamp(
        coingeckoHistorical.effectiveTimestamp,
        timestamp,
        COINGECKO_HISTORY_MAX_SKEW_MS
      )
    ) {
      return {
        chainId,
        tokenAddress: normalizedAddress,
        requestedTimestamp,
        effectiveTimestamp: coingeckoHistorical.effectiveTimestamp.toISOString(),
        priceUsd: coingeckoHistorical.priceUsd,
        source: 'coingecko_history',
      };
    }
  }

  const marketData = await marketDataRepo.getMarketData(chainId, resolution.lookupAddress);
  if (marketData?.priceUsd != null) {
    return {
      chainId,
      tokenAddress: normalizedAddress,
      requestedTimestamp,
      effectiveTimestamp: marketData.lastUpdated instanceof Date ? marketData.lastUpdated.toISOString() : new Date(marketData.lastUpdated).toISOString(),
      priceUsd: marketData.priceUsd,
      source: 'current_market_fallback',
    };
  }

  const canonicalPricing = resolveUsdValuation({
    chainId,
    normalizedAddress: resolution.lookupAddress,
    indexer: null,
    coingecko: undefined,
    cmc: undefined,
    dexscreener: undefined,
  });

  if (canonicalPricing.priceUsd != null) {
    return {
      chainId,
      tokenAddress: normalizedAddress,
      requestedTimestamp,
      effectiveTimestamp: canonicalPricing.asOf,
      priceUsd: canonicalPricing.priceUsd,
      source: 'canonical_fallback',
    };
  }

  return {
    chainId,
    tokenAddress: normalizedAddress,
    requestedTimestamp,
    source: 'unavailable',
  };
}

async function resolveCurrentPriceSnapshot(
  chainId: number,
  address: string
): Promise<CurrentPriceSnapshot> {
  const normalizedAddress = address.toLowerCase();
  const resolution = resolveCanonicalQuoteAddress(chainId, normalizedAddress);
  const marketData = await marketDataRepo.getMarketData(chainId, resolution.lookupAddress);
  const pricing = resolveUsdValuation({
    chainId,
    normalizedAddress: resolution.lookupAddress,
    indexer: marketData,
    coingecko: undefined,
    cmc: undefined,
    dexscreener: undefined,
  });

  const asOf = pricing.asOf
    || (marketData?.lastUpdated instanceof Date
      ? marketData.lastUpdated.toISOString()
      : marketData?.lastUpdated
        ? new Date(marketData.lastUpdated).toISOString()
        : undefined);

  return {
    chainId,
    tokenAddress: normalizedAddress,
    priceUsd: pricing.priceUsd,
    asOf,
    sourceLayer: pricing.sourceLayer,
    stale: pricing.stale,
    marketLastUpdated:
      marketData?.lastUpdated instanceof Date
        ? marketData.lastUpdated.toISOString()
        : marketData?.lastUpdated
          ? new Date(marketData.lastUpdated).toISOString()
          : undefined,
  };
}

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

    await refreshChain138LiveReferencePrices(chainId);

    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: normalizePossiblyRawLiquidityUsd(market, token) || 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.post('/tokens/market-batch', cacheMiddleware(30 * 1000), async (req: Request, res: Response) => {
  try {
    const chainId = parseInt(String(req.body?.chainId ?? req.query.chainId ?? ''), 10);
    const rawAddresses = req.body?.addresses;
    if (!chainId) {
      return res.status(400).json({ error: 'chainId is required' });
    }
    const hasAddressList = Array.isArray(rawAddresses);
    const hasTokenHints = Array.isArray(req.body?.tokens);
    if (!hasAddressList && !hasTokenHints) {
      return res.status(400).json({ error: 'addresses array or tokens array is required' });
    }

    await refreshChain138LiveReferencePrices(chainId);

    const hintByAddress = new Map<string, { symbol?: string; name?: string; decimals?: number }>();
    const rawTokens = req.body?.tokens;
    if (Array.isArray(rawTokens)) {
      for (const row of rawTokens) {
        if (!row || typeof row !== 'object') continue;
        const hintAddress = String((row as Record<string, unknown>).address || '').trim().toLowerCase();
        if (!/^0x[a-f0-9]{40}$/.test(hintAddress)) continue;
        hintByAddress.set(hintAddress, {
          symbol: typeof (row as Record<string, unknown>).symbol === 'string'
            ? String((row as Record<string, unknown>).symbol)
            : undefined,
          name: typeof (row as Record<string, unknown>).name === 'string'
            ? String((row as Record<string, unknown>).name)
            : undefined,
          decimals: Number((row as Record<string, unknown>).decimals) || undefined,
        });
      }
    }

    const addressSet = new Set<string>();
    if (Array.isArray(rawAddresses)) {
      for (const value of rawAddresses) {
        if (typeof value === 'string') {
          const normalized = value.trim().toLowerCase();
          if (/^0x[a-f0-9]{40}$/.test(normalized)) {
            addressSet.add(normalized);
          }
        }
      }
    }
    for (const hintAddress of hintByAddress.keys()) {
      addressSet.add(hintAddress);
    }

    const addresses = [...addressSet].slice(0, 120);

    const settled = await Promise.allSettled(
      addresses.map(async (address) => resolveTokenMarketSnapshot(chainId, address, {
        hint: hintByAddress.get(address),
      })),
    );

    const snapshots: TokenMarketSnapshot[] = [];
    for (const row of settled) {
      if (row.status === 'fulfilled' && row.value) {
        snapshots.push(row.value);
      }
    }

    return res.json({
      chainId,
      generatedAt: new Date().toISOString(),
      requested: addresses.length,
      resolved: snapshots.length,
      snapshots,
    });
  } catch (error) {
    logger.error('Error fetching token market batch:', error);
    return 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 = firstString(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' });
    }

    await refreshChain138LiveReferencePrices(chainId);

    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 externalFeeds = await enrichExternalFeedsWithReferencePrice(chainId, resolution.lookupAddress, {
      coingecko: coingeckoMarket,
      cmc: cmcMarket,
      dexscreener: dexscreenerMarket,
    });
    const { market: marketData, pricing, explorer } = buildMarketPricingExplorer(
      chainId,
      normalizedAddress,
      resolution.lookupAddress,
      marketDataRaw,
      externalFeeds
    );

    res.json({
      token: {
        ...token,
        onChain: {
          totalSupply: token.totalSupply,
        },
        market: normalizePossiblyRawLiquidityUsd(marketData, token) || 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 = firstString(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 = firstString(req.params.address) || '';
    const interval = firstString(req.query.interval as string | string[] | undefined) || '1h';
    const fromRaw = firstString(req.query.from as string | string[] | undefined);
    const toRaw = firstString(req.query.to as string | string[] | undefined);
    const from = fromRaw ? new Date(fromRaw) : new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
    const to = toRaw ? new Date(toRaw) : new Date();
    const poolAddress = firstString(req.query.poolAddress as string | 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/price-at', cacheMiddleware(60 * 1000), async (req: Request, res: Response) => {
  try {
    const chainId = parseInt(req.query.chainId as string, 10);
    const address = firstString(req.params.address) || '';
    const rawTimestamp = String(firstString(req.query.timestamp as string | string[] | undefined) || '').trim();

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

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

    const timestamp = new Date(rawTimestamp);
    if (Number.isNaN(timestamp.getTime())) {
      return res.status(400).json({ error: 'timestamp must be a valid ISO-8601 datetime' });
    }

    const historicalPrice = await resolveHistoricalPriceAt(chainId, address, timestamp);
    res.json(historicalPrice);
  } catch (error) {
    logger.error('Error fetching historical token price:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

router.get('/tokens/:address/pricing-context', cacheMiddleware(60 * 1000), async (req: Request, res: Response) => {
  try {
    const chainId = parseInt(req.query.chainId as string, 10);
    const address = firstString(req.params.address) || '';
    const rawTimestamp = String(firstString(req.query.timestamp as string | string[] | undefined) || '').trim();

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

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

    const current = await resolveCurrentPriceSnapshot(chainId, address);
    let historical: (HistoricalPriceSnapshot & { locked: boolean }) | undefined;

    if (rawTimestamp) {
      const timestamp = new Date(rawTimestamp);
      if (Number.isNaN(timestamp.getTime())) {
        return res.status(400).json({ error: 'timestamp must be a valid ISO-8601 datetime' });
      }

      const historicalPrice = await resolveHistoricalPriceAt(chainId, address, timestamp);
      historical = {
        ...historicalPrice,
        locked:
          historicalPrice.source.startsWith('ohlcv_') ||
          historicalPrice.source === 'swap_event' ||
          historicalPrice.source === 'coingecko_history',
      };
    }

    return res.json({
      chainId,
      tokenAddress: address.toLowerCase(),
      current,
      historical,
    });
  } catch (error) {
    logger.error('Error fetching pricing context:', error);
    return 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 = firstString(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 = firstString(req.query.q as string | string[] | undefined) || '';

    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 = firstString(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;
