import { ethers } from 'ethers';
import type { LiquidityPool } from '../database/repositories/pool-repo';
import { getChainConfig } from '../config/chains';
import { getDexFactories } from '../config/dex-factories';
import { CHAIN138_CANONICAL_LIVE_POOLS, isChain138CanonicalLivePool } from '../config/chain138-live-dodo-pools';
import { shouldExposePublicPool } from '../config/gru-transport';
import { logger } from '../utils/logger';
import { estimateChain138DodoLiquidityUsd } from './chain138-dodo-liquidity';

const DODO_PMM_INTEGRATION_ABI = [
  'function getAllPools() view returns (address[])',
  'function getPoolConfig(address) view returns (tuple(address pool, address baseToken, address quoteToken, uint256 lpFeeRate, uint256 i, uint256 k, bool isOpenTWAP, uint256 createdAt))',
  'function getPoolReserves(address) view returns (uint256 baseReserve, uint256 quoteReserve)',
  'function getPoolPriceOrOracle(address) view returns (uint256 price)',
];

const DODO_DVM_POOL_ABI = [
  'function _BASE_TOKEN_() view returns (address)',
  'function _QUOTE_TOKEN_() view returns (address)',
  'function getVaultReserve() view returns (uint256 baseReserve, uint256 quoteReserve)',
  'function getMidPrice() view returns (uint256)',
];

const CACHE_TTL_MS = 15_000;
const livePoolCache = new Map<number, { expiresAt: number; pools: LiquidityPool[] }>();

function bypassGruPoolFilter(chainId: number, poolAddress: string): boolean {
  if (chainId !== 138) return false;
  if (process.env.TOKEN_AGGREGATION_BYPASS_GRU_POOL_FILTER === '1') return true;
  return isChain138CanonicalLivePool(poolAddress);
}

interface LivePoolSnapshot {
  token0Address: string;
  token1Address: string;
  reserve0: bigint;
  reserve1: bigint;
  price: bigint;
  createdAt: Date;
}

function configuredIntegrations(chainId: number): string[] {
  const dodoConfigs = getDexFactories(chainId)?.dodo || [];
  return [...new Set(
    dodoConfigs
      .map((config) => config.dodoPmmIntegration?.trim().toLowerCase())
      .filter((value): value is string => Boolean(value))
  )];
}

async function readPoolViaIntegration(
  integration: ethers.Contract,
  poolAddressRaw: string
): Promise<LivePoolSnapshot> {
  const [configResult, reservesResult, priceResult] = await Promise.all([
    integration.getPoolConfig(poolAddressRaw),
    integration.getPoolReserves(poolAddressRaw),
    integration.getPoolPriceOrOracle(poolAddressRaw).catch(() => 0n),
  ]);

  const cfg = configResult as unknown as [
    string,
    string,
    string,
    bigint,
    bigint,
    bigint,
    boolean,
    bigint,
  ];
  const [reserve0, reserve1] = reservesResult as [bigint, bigint];

  return {
    token0Address: cfg[1].toLowerCase(),
    token1Address: cfg[2].toLowerCase(),
    reserve0,
    reserve1,
    price: typeof priceResult === 'bigint' ? priceResult : 0n,
    createdAt: cfg[7] ? new Date(Number(cfg[7]) * 1000) : new Date(),
  };
}

async function readPoolDirectly(
  provider: ethers.JsonRpcProvider,
  poolAddressRaw: string
): Promise<LivePoolSnapshot> {
  const pool = new ethers.Contract(poolAddressRaw, DODO_DVM_POOL_ABI, provider);
  const [baseToken, quoteToken, reservesResult, midPriceResult] = await Promise.all([
    pool._BASE_TOKEN_(),
    pool._QUOTE_TOKEN_(),
    pool.getVaultReserve(),
    pool.getMidPrice().catch(() => 0n),
  ]);
  const [reserve0, reserve1] = reservesResult as [bigint, bigint];

  return {
    token0Address: String(baseToken).toLowerCase(),
    token1Address: String(quoteToken).toLowerCase(),
    reserve0,
    reserve1,
    price: typeof midPriceResult === 'bigint' ? midPriceResult : 0n,
    createdAt: new Date(),
  };
}

export async function getLiveDodoPools(chainId: number): Promise<LiquidityPool[]> {
  const now = Date.now();
  const cached = livePoolCache.get(chainId);
  if (cached && cached.expiresAt > now) {
    return cached.pools;
  }

  const chainConfig = getChainConfig(chainId);
  const integrations = configuredIntegrations(chainId);
  if (!chainConfig || integrations.length === 0) {
    return [];
  }

  const provider = new ethers.JsonRpcProvider(chainConfig.rpcUrl);
  const poolsByAddress = new Map<string, LiquidityPool>();

  for (const integrationAddress of integrations) {
    try {
      const integration = new ethers.Contract(
        integrationAddress,
        DODO_PMM_INTEGRATION_ABI,
        provider
      );
      const poolAddresses = (await integration.getAllPools()) as string[];

      for (const poolAddressRaw of poolAddresses) {
        try {
          const poolAddress = poolAddressRaw.toLowerCase();
          const snapshot =
            await readPoolViaIntegration(integration, poolAddressRaw).catch(async () =>
              readPoolDirectly(provider, poolAddressRaw)
            );
          const { token0Address, token1Address, reserve0, reserve1, price, createdAt } = snapshot;

          if (
            !bypassGruPoolFilter(chainId, poolAddress) &&
            !shouldExposePublicPool(chainId, poolAddress, token0Address, token1Address)
          ) {
            continue;
          }

          const liquidityUsd = chainId === 138
            ? estimateChain138DodoLiquidityUsd({
                token0Address,
                token1Address,
                reserve0,
                reserve1,
                price,
              })
            : { reserve0Usd: 0, reserve1Usd: 0, totalLiquidityUsd: 0 };

          poolsByAddress.set(poolAddress, {
            chainId,
            poolAddress,
            token0Address,
            token1Address,
            dexType: 'dodo',
            factoryAddress: integrationAddress,
            reserve0: reserve0.toString(),
            reserve1: reserve1.toString(),
            reserve0Usd: liquidityUsd.reserve0Usd,
            reserve1Usd: liquidityUsd.reserve1Usd,
            totalLiquidityUsd: liquidityUsd.totalLiquidityUsd,
            volume24h: 0,
            createdAtBlock: 0,
            createdAtTimestamp: createdAt,
            lastUpdated: new Date(),
          });
        } catch (error) {
          logger.warn(`Skipping unreadable live DODO pool ${poolAddressRaw} on chain ${chainId}: ${String(error)}`);
        }
      }
    } catch (error) {
      logger.warn(`Live DODO fallback failed for chain ${chainId} integration ${integrationAddress}: ${String(error)}`);
    }
  }

  if (chainId === 138 && integrations.length > 0) {
    const integrationAddress = integrations[0];
    const integration = new ethers.Contract(
      integrationAddress,
      DODO_PMM_INTEGRATION_ABI,
      provider
    );
    for (const poolAddressRaw of CHAIN138_CANONICAL_LIVE_POOLS) {
      const poolAddress = poolAddressRaw.toLowerCase();
      if (poolsByAddress.has(poolAddress)) continue;
      try {
        const snapshot =
          await readPoolViaIntegration(integration, poolAddressRaw).catch(async () =>
            readPoolDirectly(provider, poolAddressRaw)
          );
        const { token0Address, token1Address, reserve0, reserve1, price, createdAt } = snapshot;
        const liquidityUsd = estimateChain138DodoLiquidityUsd({
          token0Address,
          token1Address,
          reserve0,
          reserve1,
          price,
        });
        poolsByAddress.set(poolAddress, {
          chainId,
          poolAddress,
          token0Address,
          token1Address,
          dexType: 'dodo',
          factoryAddress: integrationAddress,
          reserve0: reserve0.toString(),
          reserve1: reserve1.toString(),
          reserve0Usd: liquidityUsd.reserve0Usd,
          reserve1Usd: liquidityUsd.reserve1Usd,
          totalLiquidityUsd: liquidityUsd.totalLiquidityUsd,
          volume24h: 0,
          createdAtBlock: 0,
          createdAtTimestamp: createdAt,
          lastUpdated: new Date(),
        });
      } catch (error) {
        logger.warn(
          `Skipping canonical live DODO pool ${poolAddressRaw} on chain ${chainId}: ${String(error)}`
        );
      }
    }
  }

  const pools = Array.from(poolsByAddress.values()).sort(
    (a, b) => b.totalLiquidityUsd - a.totalLiquidityUsd
  );
  livePoolCache.set(chainId, { expiresAt: now + CACHE_TTL_MS, pools });
  return pools;
}
