import { TokenRepository } from '../database/repositories/token-repo';
import { PoolRepository, LiquidityPool, DexType } from '../database/repositories/pool-repo';
import { getChainConfig } from '../config/chains';
import { ResolvedTokenDisplay, resolvePoolTokenDisplays, resolveTokenDisplay } from './token-display';
import { Contract, JsonRpcProvider, formatUnits } from 'ethers';
import { getCanonicalTokenByAddress, getCanonicalTokenBySymbol } from '../config/canonical-tokens';
import { getRouteFromRegistry } from '../config/cross-chain-bridges';
import { filterPoolsForRouting } from '../config/gru-transport';
import { estimateChain138DodoLiquidityUsd } from './chain138-dodo-liquidity';

const CHAIN_138 = 138;
const CHAIN_138_PMM_INTEGRATION =
  process.env.CHAIN_138_DODO_PMM_INTEGRATION ||
  process.env.DODO_PMM_INTEGRATION_ADDRESS ||
  process.env.DODO_PMM_INTEGRATION ||
  '0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895';

const PMM_ABI = [
  'function pools(address,address) view returns (address)',
];

const ERC20_ABI = [
  'function balanceOf(address) view returns (uint256)',
];

const CHAIN_138_FALLBACK_CAPACITY_RATIO = 0.2;

export type RouteNodeKind =
  | 'direct-pool'
  | 'bridge'
  | 'atomic-swap-bridge'
  | 'destination-swap'
  | 'fallback';

export interface RouteDepthMetrics {
  tvlUsd: number;
  reserve0: string;
  reserve1: string;
  estimatedTradeCapacityUsd: number;
  freshnessSeconds: number | null;
  status: 'live' | 'stale' | 'unavailable';
}

export interface RouteNode {
  id: string;
  kind: RouteNodeKind;
  label: string;
  chainId: number;
  chainName: string;
  status: 'live' | 'partial' | 'stale' | 'unavailable';
  depth?: RouteDepthMetrics;
  tokenIn?: ResolvedTokenDisplay;
  tokenOut?: ResolvedTokenDisplay;
  poolAddress?: string;
  dexType?: string;
  path?: string[];
  children?: RouteNode[];
  notes?: string[];
}

export interface RouteDecisionTreeRequest {
  chainId: number;
  tokenIn: string;
  tokenOut?: string;
  amountIn?: string;
  destinationChainId?: number;
}

export interface MissingQuoteTokenPool {
  poolAddress: string;
  chainId: number;
  token0Address: string;
  token1Address: string;
  token0Symbol: string;
  token1Symbol: string;
  reason: string;
}

export interface RouteDecisionTreeResponse {
  generatedAt: string;
  source: {
    chainId: number;
    chainName: string;
    tokenIn: ResolvedTokenDisplay;
    tokenOut?: ResolvedTokenDisplay;
    amountIn?: string;
  };
  destination?: {
    chainId: number;
    chainName: string;
  };
  decision: 'direct-pool' | 'atomic-swap-bridge' | 'bridge-only' | 'destination-swap' | 'unresolved';
  tree: RouteNode[];
  pools: Array<{
    poolAddress: string;
    dexType: DexType;
    token0: ResolvedTokenDisplay;
    token1: ResolvedTokenDisplay;
    depth: RouteDepthMetrics;
  }>;
  missingQuoteTokenPools: MissingQuoteTokenPool[];
}

function safeBigInt(raw: string | undefined): bigint {
  try {
    return BigInt(String(raw || '0').trim() || '0');
  } catch {
    return 0n;
  }
}

function normalizedTvlUsd(pool: LiquidityPool): number {
  let tvl = Math.max(0, pool.totalLiquidityUsd || 0);
  if (pool.chainId === CHAIN_138 && pool.dexType === 'dodo') {
    const estimated = estimateChain138DodoLiquidityUsd({
      token0Address: pool.token0Address,
      token1Address: pool.token1Address,
      reserve0: safeBigInt(pool.reserve0),
      reserve1: safeBigInt(pool.reserve1),
    }).totalLiquidityUsd;
    if (estimated > 0) {
      tvl = Math.max(tvl, estimated);
    }
  }
  return tvl;
}

function estimateTradeCapacityUsd(pool: LiquidityPool): number {
  const tvl = normalizedTvlUsd(pool);
  if (tvl === 0) return 0;
  const freshnessBoost = pool.lastUpdated
    ? Math.max(0.25, 1 - (Date.now() - pool.lastUpdated.getTime()) / (60 * 60 * 1000))
    : 0.5;
  const capacity = tvl * 0.2 * freshnessBoost;
  return Math.max(0, Math.min(tvl, capacity));
}

export function classifyPoolDepthStatus(
  tvlUsd: number,
  estimatedTradeCapacityUsd: number,
  freshnessSeconds: number | null
): RouteDepthMetrics['status'] {
  if (!(tvlUsd > 0) || !(estimatedTradeCapacityUsd > 0)) {
    return 'unavailable';
  }
  if (freshnessSeconds === null) {
    return 'unavailable';
  }
  if (freshnessSeconds < 300) {
    return 'live';
  }
  if (freshnessSeconds < 1800) {
    return 'stale';
  }
  return 'unavailable';
}

function buildDepth(pool: LiquidityPool): RouteDepthMetrics {
  const freshnessSeconds = pool.lastUpdated ? Math.max(0, Math.floor((Date.now() - pool.lastUpdated.getTime()) / 1000)) : null;
  const tvlUsd = normalizedTvlUsd(pool);
  const estimatedTradeCapacityUsd = estimateTradeCapacityUsd(pool);
  const status = classifyPoolDepthStatus(tvlUsd, estimatedTradeCapacityUsd, freshnessSeconds);

  return {
    tvlUsd,
    reserve0: pool.reserve0,
    reserve1: pool.reserve1,
    estimatedTradeCapacityUsd,
    freshnessSeconds,
    status,
  };
}

function isChain138UsdFallbackToken(token: ResolvedTokenDisplay): boolean {
  if (!token?.address) return false;
  const spec = getCanonicalTokenByAddress(CHAIN_138, token.address);
  return spec?.currencyCode === 'USD';
}

function getChain138FallbackTokenDecimals(token: ResolvedTokenDisplay): number {
  const spec = token?.address ? getCanonicalTokenByAddress(CHAIN_138, token.address) : undefined;
  return Number(token?.decimals ?? spec?.decimals ?? 18);
}

function parseFallbackTokenAmount(value: bigint, decimals: number): number {
  if (value <= 0n) return 0;
  const parsed = Number(formatUnits(value, decimals));
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}

export function estimateChain138FallbackDepthUsd(
  token0: ResolvedTokenDisplay,
  reserve0: bigint,
  token1: ResolvedTokenDisplay,
  reserve1: bigint
): Pick<RouteDepthMetrics, 'tvlUsd' | 'estimatedTradeCapacityUsd'> {
  if (reserve0 <= 0n || reserve1 <= 0n) {
    return {
      tvlUsd: 0,
      estimatedTradeCapacityUsd: 0,
    };
  }

  const reserve0Usd = isChain138UsdFallbackToken(token0)
    ? parseFallbackTokenAmount(reserve0, getChain138FallbackTokenDecimals(token0))
    : 0;
  const reserve1Usd = isChain138UsdFallbackToken(token1)
    ? parseFallbackTokenAmount(reserve1, getChain138FallbackTokenDecimals(token1))
    : 0;

  let tvlUsd = 0;
  if (reserve0Usd > 0 && reserve1Usd > 0) {
    tvlUsd = reserve0Usd + reserve1Usd;
  } else if (reserve0Usd > 0) {
    tvlUsd = reserve0Usd * 2;
  } else if (reserve1Usd > 0) {
    tvlUsd = reserve1Usd * 2;
  }

  if (tvlUsd <= 0) {
    return {
      tvlUsd: 0,
      estimatedTradeCapacityUsd: 0,
    };
  }

  return {
    tvlUsd,
    estimatedTradeCapacityUsd: Math.max(0, Math.min(tvlUsd, tvlUsd * CHAIN_138_FALLBACK_CAPACITY_RATIO)),
  };
}

function deriveDecision(
  sourceChainId: number,
  destinationChainId: number | undefined,
  directPoolCount: number
): RouteDecisionTreeResponse['decision'] {
  if (directPoolCount > 0 && (!destinationChainId || destinationChainId === sourceChainId)) {
    return 'direct-pool';
  }
  if (sourceChainId === 138 && destinationChainId && destinationChainId !== 138) {
    return directPoolCount > 0 ? 'atomic-swap-bridge' : 'bridge-only';
  }
  if (destinationChainId && destinationChainId !== sourceChainId) {
    return directPoolCount > 0 ? 'destination-swap' : 'bridge-only';
  }
  return directPoolCount > 0 ? 'direct-pool' : 'unresolved';
}

interface BridgeResolution {
  assetSymbol: string;
  localQuoteAddress?: string;
  route: ReturnType<typeof getRouteFromRegistry>;
}

export class RouteDecisionTreeService {
  private tokenRepo: TokenRepository;
  private poolRepo: PoolRepository;

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

  async build(request: RouteDecisionTreeRequest): Promise<RouteDecisionTreeResponse> {
    const chainConfig = getChainConfig(request.chainId);
    const destinationConfig = request.destinationChainId ? getChainConfig(request.destinationChainId) : undefined;
    const destinationChainId = request.destinationChainId ?? request.chainId;

    const sourceTokenIn = await resolveTokenDisplay(this.tokenRepo, request.chainId, request.tokenIn);
    const sourceTokenOut = request.tokenOut
      ? await resolveTokenDisplay(this.tokenRepo, request.chainId, request.tokenOut)
      : undefined;
    const destinationTokenOut =
      request.destinationChainId && request.tokenOut && request.destinationChainId !== request.chainId
        ? await resolveTokenDisplay(this.tokenRepo, request.destinationChainId, request.tokenOut)
        : sourceTokenOut;

    const bridgeResolution = this.resolveBridgeResolution(
      request.chainId,
      destinationChainId,
      request.tokenIn,
      request.tokenOut
    );
    const acceptableTokenOutAddresses = new Set<string>();
    if (request.tokenOut) acceptableTokenOutAddresses.add(request.tokenOut.toLowerCase());
    if (bridgeResolution?.localQuoteAddress) acceptableTokenOutAddresses.add(bridgeResolution.localQuoteAddress.toLowerCase());

    const pools = filterPoolsForRouting(
      request.chainId,
      await this.poolRepo.getPoolsByToken(request.chainId, request.tokenIn)
    );
    const directPools = request.tokenOut
      ? pools.filter((pool) =>
          acceptableTokenOutAddresses.has(pool.token0Address.toLowerCase()) ||
          acceptableTokenOutAddresses.has(pool.token1Address.toLowerCase())
        )
      : pools;

    const destinationPools =
      request.tokenOut && destinationChainId !== request.chainId
        ? filterPoolsForRouting(
            destinationChainId,
            await this.poolRepo.getPoolsByToken(destinationChainId, request.tokenOut)
          )
        : [];

    let resolvedPools = await Promise.all(
      directPools
        .slice()
        .sort((a, b) => (b.totalLiquidityUsd || 0) - (a.totalLiquidityUsd || 0))
        .map(async (pool) => {
        const { token0, token1 } = await resolvePoolTokenDisplays(
          this.tokenRepo,
          request.chainId,
          pool.token0Address,
          pool.token1Address
        );
        return {
          poolAddress: pool.poolAddress,
          dexType: pool.dexType,
          token0,
          token1,
          depth: buildDepth(pool),
        };
      })
    );

    const resolvedDestinationPools = await Promise.all(
      destinationPools
        .slice()
        .sort((a, b) => (b.totalLiquidityUsd || 0) - (a.totalLiquidityUsd || 0))
        .map(async (pool) => {
          const { token0, token1 } = await resolvePoolTokenDisplays(
            this.tokenRepo,
            destinationChainId,
            pool.token0Address,
            pool.token1Address
          );
          return {
            poolAddress: pool.poolAddress,
            dexType: pool.dexType,
            token0,
            token1,
            depth: buildDepth(pool),
          };
        })
    );

    const shouldTryLiveFallback =
      request.chainId === CHAIN_138 &&
      destinationChainId === CHAIN_138 &&
      request.tokenOut &&
      (resolvedPools.length === 0 || resolvedPools.every((pool) => pool.depth.status === 'unavailable'));

    if (shouldTryLiveFallback) {
      const liveFallbackPool = await this.findLiveDirectPoolFallback(request, sourceTokenIn, sourceTokenOut);
      if (liveFallbackPool) {
        resolvedPools = [
          liveFallbackPool,
          ...resolvedPools.filter((pool) => pool.poolAddress.toLowerCase() !== liveFallbackPool.poolAddress.toLowerCase()),
        ];
      }
    }

    const missingQuoteTokenPools = await this.findMissingQuoteTokenPools(request.chainId, pools);

    const tree: RouteNode[] = resolvedPools.map((pool) => {
      const children: RouteNode[] = [];

      if (destinationConfig && destinationChainId !== request.chainId) {
        children.push({
          id: `${request.chainId}:bridge:${pool.poolAddress}:${destinationChainId}`,
          kind: 'bridge',
          label: `Bridge to ${destinationConfig.name}`,
          chainId: destinationChainId,
          chainName: destinationConfig.name,
          status: bridgeResolution?.route ? 'live' : 'partial',
          notes: bridgeResolution?.route
            ? [
                `Bridge route ${bridgeResolution.route.label} is configured for ${bridgeResolution.assetSymbol}`,
                `Bridge address ${bridgeResolution.route.bridgeAddress}`,
              ]
            : ['Bridge leg requires a configured production bridge lane for this asset'],
        });

        if (resolvedDestinationPools.length > 0) {
          children.push({
            id: `${destinationChainId}:swap:${request.tokenOut || 'unknown'}`,
            kind: 'destination-swap',
            label: `Destination swap to ${destinationTokenOut?.symbol || request.tokenOut || 'target'}`,
            chainId: destinationChainId,
            chainName: destinationConfig.name,
            status: resolvedDestinationPools[0].depth.status,
            depth: resolvedDestinationPools[0].depth,
            tokenIn: destinationTokenOut,
            tokenOut: destinationTokenOut,
            poolAddress: resolvedDestinationPools[0].poolAddress,
            dexType: resolvedDestinationPools[0].dexType,
            notes: [
              `Best destination pool: ${resolvedDestinationPools[0].token0.symbol}/${resolvedDestinationPools[0].token1.symbol}`,
              `Destination TVL $${resolvedDestinationPools[0].depth.tvlUsd.toFixed(2)}`,
            ],
          });
        }
      }

      return {
        id: `${request.chainId}:${pool.dexType}:${pool.poolAddress}`,
        kind: 'direct-pool',
        label: `${pool.token0.symbol}/${pool.token1.symbol}`,
        chainId: request.chainId,
        chainName: chainConfig?.name || `Chain ${request.chainId}`,
        status: pool.depth.status,
        depth: pool.depth,
        tokenIn: sourceTokenIn,
        tokenOut: sourceTokenOut,
        poolAddress: pool.poolAddress,
        dexType: pool.dexType,
        path: [request.tokenIn, pool.token0.address, pool.token1.address].filter(Boolean),
        notes: [
          pool.depth.tvlUsd > 0 ? `TVL $${pool.depth.tvlUsd.toFixed(2)}` : 'No TVL reported',
          `Estimated capacity $${pool.depth.estimatedTradeCapacityUsd.toFixed(2)}`,
        ],
        children,
      };
    });

    if (tree.length === 0 && destinationConfig && destinationChainId !== request.chainId) {
      tree.push({
        id: `${request.chainId}:bridge-only:${destinationChainId}`,
        kind: 'bridge',
        label: `Bridge to ${destinationConfig.name}`,
        chainId: destinationChainId,
        chainName: destinationConfig.name,
        status: bridgeResolution?.route ? 'live' : 'partial',
        tokenIn: sourceTokenIn,
        tokenOut: sourceTokenOut,
        notes: bridgeResolution?.route
          ? [
              `Configured ${bridgeResolution.route.label} route for ${bridgeResolution.assetSymbol}`,
              bridgeResolution.localQuoteAddress
                ? 'Destination asset maps to a source-chain quote mirror before bridging'
                : 'Destination asset is directly bridgeable from the source token',
            ]
          : [
              'No direct local pool for this pair',
              'Use the bridge leg first, then complete with destination liquidity',
            ],
      });
    }

    const decision = deriveDecision(request.chainId, request.destinationChainId, resolvedPools.length);

    return {
      generatedAt: new Date().toISOString(),
      source: {
        chainId: request.chainId,
        chainName: chainConfig?.name || `Chain ${request.chainId}`,
        tokenIn: sourceTokenIn,
        tokenOut: sourceTokenOut,
        amountIn: request.amountIn,
      },
      destination: destinationConfig
        ? { chainId: destinationConfig.chainId, chainName: destinationConfig.name }
        : undefined,
      decision,
      tree,
      pools: resolvedPools,
      missingQuoteTokenPools,
    };
  }

  private async findMissingQuoteTokenPools(
    chainId: number,
    pools: LiquidityPool[]
  ): Promise<MissingQuoteTokenPool[]> {
    const out: MissingQuoteTokenPool[] = [];
    for (const pool of pools) {
      const [token0, token1] = await Promise.all([
        resolveTokenDisplay(this.tokenRepo, chainId, pool.token0Address),
        resolveTokenDisplay(this.tokenRepo, chainId, pool.token1Address),
      ]);

      if (token1.source === 'fallback') {
        out.push({
          poolAddress: pool.poolAddress,
          chainId,
          token0Address: pool.token0Address,
          token1Address: pool.token1Address,
          token0Symbol: token0.symbol,
          token1Symbol: token1.symbol,
          reason: 'Quote token metadata missing in token index; canonical or fallback resolution used',
        });
      }
    }
    return out;
  }

  private async findLiveDirectPoolFallback(
    request: RouteDecisionTreeRequest,
    sourceTokenIn: ResolvedTokenDisplay,
    sourceTokenOut?: ResolvedTokenDisplay
  ): Promise<RouteDecisionTreeResponse['pools'][number] | null> {
    const chainConfig = getChainConfig(request.chainId);
    if (!chainConfig || request.chainId !== CHAIN_138 || !request.tokenOut) return null;

    try {
      const provider = new JsonRpcProvider(chainConfig.rpcUrl);
      const integration = new Contract(CHAIN_138_PMM_INTEGRATION, PMM_ABI, provider);
      const poolAddress = String(await integration.pools(request.tokenIn, request.tokenOut));
      if (!poolAddress || /^0x0{40}$/i.test(poolAddress)) return null;

      const code = await provider.getCode(poolAddress);
      if (!code || code === '0x') return null;

      const tokenInContract = new Contract(request.tokenIn, ERC20_ABI, provider);
      const tokenOutContract = new Contract(request.tokenOut, ERC20_ABI, provider);
      const [reserve0, reserve1] = await Promise.all([
        tokenInContract.balanceOf(poolAddress),
        tokenOutContract.balanceOf(poolAddress),
      ]);
      const live = reserve0 > 0n && reserve1 > 0n;
      const token1 = sourceTokenOut || await resolveTokenDisplay(this.tokenRepo, request.chainId, request.tokenOut);
      const depthUsd = estimateChain138FallbackDepthUsd(sourceTokenIn, reserve0, token1, reserve1);

      return {
        poolAddress,
        dexType: 'dodo',
        token0: sourceTokenIn,
        token1,
        depth: {
          tvlUsd: depthUsd.tvlUsd,
          reserve0: reserve0.toString(),
          reserve1: reserve1.toString(),
          estimatedTradeCapacityUsd: depthUsd.estimatedTradeCapacityUsd,
          freshnessSeconds: 0,
          status: live ? 'live' : 'stale',
        },
      };
    } catch {
      return null;
    }
  }

  private resolveBridgeResolution(
    sourceChainId: number,
    destinationChainId: number,
    tokenInAddress: string,
    tokenOutAddress?: string
  ): BridgeResolution | null {
    if (!tokenOutAddress || sourceChainId === destinationChainId) return null;

    const sourceTokenInSpec = getCanonicalTokenByAddress(sourceChainId, tokenInAddress);
    const destinationTokenOutSpec = getCanonicalTokenByAddress(destinationChainId, tokenOutAddress);
    const bridgeAssetSymbol = destinationTokenOutSpec?.symbol || sourceTokenInSpec?.symbol;
    if (!bridgeAssetSymbol) return null;

    const route = getRouteFromRegistry(sourceChainId, destinationChainId, bridgeAssetSymbol);
    if (!route) return null;

    const localQuoteSpec = getCanonicalTokenBySymbol(sourceChainId, bridgeAssetSymbol);
    return {
      assetSymbol: bridgeAssetSymbol,
      localQuoteAddress: localQuoteSpec?.addresses[sourceChainId],
      route,
    };
  }
}
