import { AbiCoder } from 'ethers';
import { PoolRepository } from '../database/repositories/pool-repo';
import type { LiquidityPool } from '../database/repositories/pool-repo';
import { filterPoolsForRouting } from '../config/gru-transport';
import { findProviderPairCapability } from '../config/provider-capabilities';
import { getRouteFromRegistry } from '../config/cross-chain-bridges';
import {
  getRoutingAddressForSymbol,
  getRoutingSymbolForAddress,
} from '../config/routing-assets';
import { getChain138DodoV3PilotEdges } from './dodo-v3-pilot';
import { getChain138PilotVenueEdges } from './chain138-pilot-venues';
import { getLiveDodoPools } from './live-dodo-fallback';
import { estimateChain138DodoLiquidityUsd } from './chain138-dodo-liquidity';
import { BridgeRouteCandidate, PlannerProvider, SwapGraphEdge } from './planner-v2-types';

const abiCoder = AbiCoder.defaultAbiCoder();
const CHAIN_138 = 138;
const CHAIN_138_MIN_LIVE_DODO_LIQUIDITY_USD = 100;

function normalizeAddress(value: string): string {
  return value.trim().toLowerCase();
}

function providerFromDexType(dexType: string): PlannerProvider | null {
  switch (dexType) {
    case 'dodo':
      return 'dodo';
    case 'dodo_v3':
      return 'dodo_v3';
    case 'uniswap_v3':
      return 'uniswap_v3';
    case 'uniswap_v2':
      return 'uniswap_v2';
    case 'sushiswap':
      return 'sushiswap';
    default:
      return null;
  }
}

function dedupePools<T extends { poolAddress: string }>(items: T[]): T[] {
  const byAddress = new Map<string, T>();
  for (const item of items) {
    byAddress.set(item.poolAddress.toLowerCase(), item);
  }
  return Array.from(byAddress.values());
}

function isMeaningfullyFundedPool(chainId: number, pool: LiquidityPool): boolean {
  if (chainId !== CHAIN_138 || pool.dexType !== 'dodo') {
    return true;
  }
  const reserve0 = BigInt(pool.reserve0 || '0');
  const reserve1 = BigInt(pool.reserve1 || '0');
  const estimatedUsd = estimateChain138DodoLiquidityUsd({
    token0Address: pool.token0Address,
    token1Address: pool.token1Address,
    reserve0,
    reserve1,
  }).totalLiquidityUsd;
  const totalLiquidityUsd = Math.max(pool.totalLiquidityUsd || 0, estimatedUsd);
  return totalLiquidityUsd >= CHAIN_138_MIN_LIVE_DODO_LIQUIDITY_USD;
}

export class RouteGraphBuilder {
  private poolRepo: PoolRepository;

  constructor(poolRepo = new PoolRepository()) {
    this.poolRepo = poolRepo;
  }

  async buildSwapEdges(chainId: number): Promise<SwapGraphEdge[]> {
    let indexedPools: LiquidityPool[] = [];
    try {
      indexedPools = filterPoolsForRouting(chainId, await this.poolRepo.getPoolsByChain(chainId, 500));
    } catch {
      indexedPools = [];
    }
    const liveDodoPools = chainId === 138
      ? filterPoolsForRouting(chainId, await getLiveDodoPools(chainId))
      : [];
    const liveDodoV3PilotEdges = chainId === 138
      ? await getChain138DodoV3PilotEdges()
      : [];
    const livePilotVenueEdges = chainId === 138
      ? await getChain138PilotVenueEdges()
      : [];
    const pools = dedupePools([...indexedPools, ...liveDodoPools]).filter((pool) =>
      isMeaningfullyFundedPool(chainId, pool)
    );

    const poolEdges = pools.flatMap((pool) => {
      const provider = providerFromDexType(pool.dexType);
      if (!provider) return [];

      const token0 = normalizeAddress(pool.token0Address);
      const token1 = normalizeAddress(pool.token1Address);
      const token0Symbol = getRoutingSymbolForAddress(chainId, token0);
      const token1Symbol = getRoutingSymbolForAddress(chainId, token1);
      const capability = findProviderPairCapability(chainId, provider, token0, token1)
        || findProviderPairCapability(chainId, provider, token1, token0);

      const target = capability?.target || (provider === 'dodo' ? normalizeAddress(process.env.DODO_PMM_PROVIDER_ADDRESS || process.env.DODO_PMM_PROVIDER || '0x3f729632E9553EBacCdE2e9b4c8F2B285b014F2e') : undefined);
      const providerData = capability?.providerData || (provider === 'dodo' ? { poolAddress: pool.poolAddress.toLowerCase() } : undefined);
      const providerDataHex = capability?.providerDataHex || (provider === 'dodo'
        ? abiCoder.encode(['address'], [pool.poolAddress.toLowerCase()])
        : undefined);

      const baseEdge = {
        kind: 'swap' as const,
        provider,
        chainId,
        target,
        poolAddress: pool.poolAddress.toLowerCase(),
        providerData,
        providerDataHex,
        totalLiquidityUsd: pool.totalLiquidityUsd || 0,
        freshnessSeconds: pool.lastUpdated
          ? Math.max(0, Math.floor((Date.now() - pool.lastUpdated.getTime()) / 1000))
          : null,
        notes: [],
      };

      return [
        {
          ...baseEdge,
          tokenInAddress: token0,
          tokenOutAddress: token1,
          tokenInSymbol: token0Symbol,
          tokenOutSymbol: token1Symbol,
          reserveIn: String(pool.reserve0),
          reserveOut: String(pool.reserve1),
        },
        {
          ...baseEdge,
          tokenInAddress: token1,
          tokenOutAddress: token0,
          tokenInSymbol: token1Symbol,
          tokenOutSymbol: token0Symbol,
          reserveIn: String(pool.reserve1),
          reserveOut: String(pool.reserve0),
        },
      ];
    });

    return [...poolEdges, ...liveDodoV3PilotEdges, ...livePilotVenueEdges];
  }

  buildBridgeCandidates(
    fromChainId: number,
    toChainId: number,
    assetSymbols: string[]
  ): BridgeRouteCandidate[] {
    return assetSymbols.flatMap((symbol) => {
      const route = getRouteFromRegistry(fromChainId, toChainId, symbol);
      if (!route) return [];

      const sourceTokenAddress = getRoutingAddressForSymbol(fromChainId, symbol);
      const destinationTokenAddress = getRoutingAddressForSymbol(toChainId, symbol);
      if (!sourceTokenAddress || !destinationTokenAddress) return [];

      return [{
        bridgeType: route.pathType,
        bridgeAddress: route.bridgeAddress.toLowerCase(),
        bridgeLabel: route.label,
        assetSymbol: route.asset || symbol,
        sourceTokenAddress: sourceTokenAddress.toLowerCase(),
        destinationTokenAddress: destinationTokenAddress.toLowerCase(),
        fromChainId,
        toChainId,
        notes: [`Registry route ${route.label}`],
      }];
    });
  }
}
