import { ethers } from 'ethers';
import { PoolRepository, LiquidityPool, DexType } from '../database/repositories/pool-repo';
import { getDexFactories, UniswapV2Config, UniswapV3Config, DodoConfig } from '../config/dex-factories';
import { logger } from '../utils/logger';
import { shouldExposePublicPool } from '../config/gru-transport';
import { estimateChain138DodoLiquidityUsd } from '../services/chain138-dodo-liquidity';

// UniswapV2 Factory ABI
const UNISWAP_V2_FACTORY_ABI = [
  'event PairCreated(address indexed token0, address indexed token1, address pair, uint)',
  'function allPairsLength() view returns (uint256)',
  'function allPairs(uint256) view returns (address)',
];

// UniswapV2 Pair ABI
const UNISWAP_V2_PAIR_ABI = [
  'function token0() view returns (address)',
  'function token1() view returns (address)',
  'function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)',
  'function totalSupply() view returns (uint256)',
];

// UniswapV3 Factory ABI
const UNISWAP_V3_FACTORY_ABI = [
  'event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)',
  'function owner() view returns (address)',
];

// UniswapV3 Pool ABI
const UNISWAP_V3_POOL_ABI = [
  'function token0() view returns (address)',
  'function token1() view returns (address)',
  'function fee() view returns (uint24)',
  'function liquidity() view returns (uint128)',
  'function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)',
];

// DODO PoolManager ABI (simplified)
const DODO_POOL_MANAGER_ABI = [
  'function allPools() view returns (address[])',
  'function tokenPools(address) view returns (address[])',
  'function poolRegistry(address) view returns (tuple(address pool, address provider, address tokenA, address tokenB, uint256 liquidityUSD, uint256 volume24h, uint256 createdAt, uint256 lastUpdateTime, bool isActive))',
];

// DODOPMMIntegration ABI (getAllPools + getPoolConfig + getPoolReserves + getPoolPriceOrOracle)
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 POOL_QUERY_FALLBACK_BLOCK_SPAN = Math.max(
  1,
  Number(process.env.POOL_QUERY_FALLBACK_BLOCK_SPAN || 5000)
);

function isRpcRangeLimitError(error: unknown): boolean {
  const parts = [
    typeof error === 'object' && error ? (error as { message?: string }).message : '',
    typeof error === 'object' && error ? (error as { shortMessage?: string }).shortMessage : '',
    typeof error === 'object' && error
      ? ((error as { error?: { message?: string } }).error?.message ?? '')
      : '',
  ];

  return parts.some((part) =>
    typeof part === 'string' && /range limit|exceeds maximum rpc range/i.test(part)
  );
}

export class PoolIndexer {
  private static missingDexConfigLogged = new Set<number>();
  private static staleDodoPoolsLogged = new Set<string>();
  private provider: ethers.JsonRpcProvider;
  private poolRepo: PoolRepository;
  private chainId: number;

  constructor(chainId: number, rpcUrl: string) {
    this.chainId = chainId;
    this.provider = new ethers.JsonRpcProvider(rpcUrl);
    this.poolRepo = new PoolRepository();
  }

  private async persistPoolIfExposed(pool: LiquidityPool, pools: LiquidityPool[]): Promise<void> {
    if (!shouldExposePublicPool(this.chainId, pool.poolAddress, pool.token0Address, pool.token1Address)) {
      logger.info(
        `Skipping inactive GRU public pool ${pool.poolAddress} on chain ${this.chainId} until gru-transport-active.json marks it active`
      );
      return;
    }

    await this.poolRepo.upsertPool(pool);
    pools.push(pool);
  }

  private async queryFilterWithRangeFallback(
    contract: ethers.Contract,
    filter: ReturnType<ethers.Contract['filters'][string]>,
    fromBlock: number,
    toBlock: number
  ): Promise<ethers.EventLog[]> {
    try {
      return (await contract.queryFilter(filter as never, fromBlock, toBlock)) as ethers.EventLog[];
    } catch (error) {
      if (!isRpcRangeLimitError(error) || fromBlock >= toBlock) {
        throw error;
      }
    }

    const logs: ethers.EventLog[] = [];
    const totalSpan = toBlock - fromBlock + 1;
    const chunkSpan = Math.min(POOL_QUERY_FALLBACK_BLOCK_SPAN, totalSpan);

    for (let start = fromBlock; start <= toBlock; start += chunkSpan) {
      const end = Math.min(start + chunkSpan - 1, toBlock);
      if (start === fromBlock && end === toBlock) {
        const mid = Math.floor((start + end) / 2);
        logs.push(...await this.queryFilterWithRangeFallback(contract, filter, start, mid));
        logs.push(...await this.queryFilterWithRangeFallback(contract, filter, mid + 1, end));
        continue;
      }
      logs.push(...await this.queryFilterWithRangeFallback(contract, filter, start, end));
    }
    return logs;
  }

  /**
   * Index all pools for configured DEX types
   */
  async indexAllPools(): Promise<LiquidityPool[]> {
    const dexConfig = getDexFactories(this.chainId);
    const hasDexConfig =
      !!dexConfig &&
      (dexConfig.uniswap_v2?.length ||
        dexConfig.sushiswap?.length ||
        dexConfig.uniswap_v3?.length ||
        dexConfig.dodo?.length ||
        dexConfig.custom?.length);

    if (!hasDexConfig) {
      if (!PoolIndexer.missingDexConfigLogged.has(this.chainId)) {
        logger.info(`Skipping pool indexing for chain ${this.chainId}; no DEX configuration is active yet`);
        PoolIndexer.missingDexConfigLogged.add(this.chainId);
      }
      return [];
    }

    const allPools: LiquidityPool[] = [];

    // Index UniswapV2 pools
    if (dexConfig.uniswap_v2) {
      for (const config of dexConfig.uniswap_v2) {
        const pools = await this.indexUniswapV2Pools(config, 'uniswap_v2');
        allPools.push(...pools);
      }
    }

    if (dexConfig.sushiswap) {
      for (const config of dexConfig.sushiswap) {
        const pools = await this.indexUniswapV2Pools(config, 'sushiswap');
        allPools.push(...pools);
      }
    }

    // Index UniswapV3 pools
    if (dexConfig.uniswap_v3) {
      for (const config of dexConfig.uniswap_v3) {
        const pools = await this.indexUniswapV3Pools(config);
        allPools.push(...pools);
      }
    }

    // Index DODO pools (PoolManager and/or DODOPMMIntegration)
    if (dexConfig.dodo) {
      for (const config of dexConfig.dodo) {
        const pools = await this.indexDodoPools(config);
        allPools.push(...pools);
      }
    }

    return allPools;
  }

  /**
   * Index DODO PMM pools from DODOPMMIntegration contract
   */
  private async indexDodoPmmIntegrationPools(config: DodoConfig): Promise<LiquidityPool[]> {
    const pools: LiquidityPool[] = [];
    const integrationAddress = config.dodoPmmIntegration;
    if (!integrationAddress || integrationAddress.trim() === '') {
      return pools;
    }

    try {
      const integration = new ethers.Contract(
        integrationAddress,
        DODO_PMM_INTEGRATION_ABI,
        this.provider
      );

      const poolAddresses: string[] = await integration.getAllPools();

      for (const poolAddress of poolAddresses) {
        try {
          const [configResult, reservesResult, priceResult] = await Promise.all([
            integration.getPoolConfig(poolAddress),
            integration.getPoolReserves(poolAddress),
            integration.getPoolPriceOrOracle(poolAddress).catch(() => 0n),
          ]);

          const cfg = configResult as unknown as [string, string, string, bigint, bigint, bigint, boolean, bigint];
          const baseToken = cfg[1];
          const quoteToken = cfg[2];
          const createdAt = cfg[7];
          const baseReserve = (reservesResult as [bigint, bigint])[0];
          const quoteReserve = (reservesResult as [bigint, bigint])[1];
          const price = typeof priceResult === 'bigint' ? priceResult : BigInt(0);

          const liquidityUsd = this.chainId === 138
            ? estimateChain138DodoLiquidityUsd({
                token0Address: baseToken,
                token1Address: quoteToken,
                reserve0: baseReserve,
                reserve1: quoteReserve,
                price,
              })
            : { reserve0Usd: 0, reserve1Usd: 0, totalLiquidityUsd: 0 };

          const pool: LiquidityPool = {
            chainId: this.chainId,
            poolAddress: poolAddress.toLowerCase(),
            token0Address: baseToken.toLowerCase(),
            token1Address: quoteToken.toLowerCase(),
            dexType: 'dodo',
            factoryAddress: integrationAddress.toLowerCase(),
            reserve0: baseReserve.toString(),
            reserve1: quoteReserve.toString(),
            reserve0Usd: liquidityUsd.reserve0Usd,
            reserve1Usd: liquidityUsd.reserve1Usd,
            totalLiquidityUsd: liquidityUsd.totalLiquidityUsd,
            volume24h: 0, // No 24h volume from contract; requires event indexer
            createdAtBlock: 0,
            createdAtTimestamp: createdAt ? new Date(Number(createdAt) * 1000) : new Date(),
            lastUpdated: new Date(),
          };

          await this.persistPoolIfExposed(pool, pools);
        } catch (err) {
          const stalePoolKey = `${this.chainId}:${poolAddress.toLowerCase()}`;
          if (!PoolIndexer.staleDodoPoolsLogged.has(stalePoolKey)) {
            logger.info(
              `Skipping stale DODO PMM pool ${poolAddress} on chain ${this.chainId}; it is no longer registered in DODOPMMIntegration`
            );
            PoolIndexer.staleDodoPoolsLogged.add(stalePoolKey);
          }
        }
      }
    } catch (error) {
      logger.error('Error indexing DODO PMM Integration pools:', error);
    }

    return pools;
  }

  /**
   * Index UniswapV2 pools from PairCreated events
   */
  private async indexUniswapV2Pools(
    config: UniswapV2Config,
    dexType: 'uniswap_v2' | 'sushiswap'
  ): Promise<LiquidityPool[]> {
    const pools: LiquidityPool[] = [];
    const factory = new ethers.Contract(config.factory, UNISWAP_V2_FACTORY_ABI, this.provider);

    try {
      // Get current block
      const currentBlock = await this.provider.getBlockNumber();
      const fromBlock = config.startBlock || Math.max(0, currentBlock - 10000);

      // Listen for PairCreated events
      const filter = factory.filters.PairCreated();
      const events = await this.queryFilterWithRangeFallback(factory, filter, fromBlock, currentBlock);

      for (const event of events) {
        const ev = event as ethers.EventLog;
        if (ev.args && ev.args.length >= 3) {
          const token0 = ev.args[0] as string;
          const token1 = ev.args[1] as string;
          const pairAddress = ev.args[2] as string;

          // Get pair reserves
          const pair = new ethers.Contract(pairAddress, UNISWAP_V2_PAIR_ABI, this.provider);
          const [reserve0, reserve1] = await pair.getReserves();

          const pool: LiquidityPool = {
            chainId: this.chainId,
            poolAddress: pairAddress.toLowerCase(),
            token0Address: token0.toLowerCase(),
            token1Address: token1.toLowerCase(),
            dexType,
            factoryAddress: config.factory.toLowerCase(),
            routerAddress: config.router?.toLowerCase(),
            reserve0: reserve0.toString(),
            reserve1: reserve1.toString(),
            reserve0Usd: 0, // Will be calculated with price oracle
            reserve1Usd: 0,
            totalLiquidityUsd: 0,
            volume24h: 0,
            createdAtBlock: event.blockNumber,
            createdAtTimestamp: new Date((await this.provider.getBlock(event.blockNumber))!.timestamp * 1000),
            lastUpdated: new Date(),
          };

          await this.persistPoolIfExposed(pool, pools);
        }
      }
    } catch (error) {
      logger.error(`Error indexing ${dexType} pools:`, error);
    }

    return pools;
  }

  /**
   * Index UniswapV3 pools from PoolCreated events
   */
  private async indexUniswapV3Pools(config: UniswapV3Config): Promise<LiquidityPool[]> {
    const pools: LiquidityPool[] = [];
    const factory = new ethers.Contract(config.factory, UNISWAP_V3_FACTORY_ABI, this.provider);

    try {
      const currentBlock = await this.provider.getBlockNumber();
      const fromBlock = config.startBlock || Math.max(0, currentBlock - 10000);

      const filter = factory.filters.PoolCreated();
      const events = await this.queryFilterWithRangeFallback(factory, filter, fromBlock, currentBlock);

      for (const event of events) {
        const ev = event as ethers.EventLog;
        if (ev.args && ev.args.length >= 5) {
          const token0 = ev.args[0] as string;
          const token1 = ev.args[1] as string;
          const fee = ev.args[2] as number;
          const poolAddress = ev.args[4] as string;

          const poolContract = new ethers.Contract(poolAddress, UNISWAP_V3_POOL_ABI, this.provider);
          await poolContract.liquidity();

          const pool: LiquidityPool = {
            chainId: this.chainId,
            poolAddress: poolAddress.toLowerCase(),
            token0Address: token0.toLowerCase(),
            token1Address: token1.toLowerCase(),
            dexType: 'uniswap_v3',
            factoryAddress: config.factory.toLowerCase(),
            routerAddress: config.router?.toLowerCase(),
            reserve0: '0', // UniswapV3 uses different reserve model
            reserve1: '0',
            reserve0Usd: 0,
            reserve1Usd: 0,
            totalLiquidityUsd: 0,
            volume24h: 0,
            feeTier: fee,
            createdAtBlock: event.blockNumber,
            createdAtTimestamp: new Date((await this.provider.getBlock(event.blockNumber))!.timestamp * 1000),
            lastUpdated: new Date(),
          };

          await this.persistPoolIfExposed(pool, pools);
        }
      }
    } catch (error) {
      logger.error(`Error indexing UniswapV3 pools:`, error);
    }

    return pools;
  }

  /**
   * Index DODO pools from PoolManager and/or DODOPMMIntegration
   */
  private async indexDodoPools(config: DodoConfig): Promise<LiquidityPool[]> {
    const pools: LiquidityPool[] = [];

    // Index from DODOPMMIntegration when configured
    if (config.dodoPmmIntegration?.trim()) {
      const pmmPools = await this.indexDodoPmmIntegrationPools(config);
      pools.push(...pmmPools);
    }

    // Index from PoolManager when configured
    if (!config.poolManager?.trim()) {
      return pools;
    }

    try {
      const poolManager = new ethers.Contract(
        config.poolManager,
        DODO_POOL_MANAGER_ABI,
        this.provider
      );

      const allPools = await poolManager.allPools();

      for (const poolAddress of allPools) {
        try {
          const poolInfo = await poolManager.poolRegistry(poolAddress);
          if (!poolInfo.isActive) continue;

          const pool: LiquidityPool = {
            chainId: this.chainId,
            poolAddress: poolAddress.toLowerCase(),
            token0Address: poolInfo.tokenA.toLowerCase(),
            token1Address: poolInfo.tokenB.toLowerCase(),
            dexType: 'dodo',
            factoryAddress: config.poolManager.toLowerCase(),
            reserve0: '0',
            reserve1: '0',
            reserve0Usd: 0,
            reserve1Usd: 0,
            totalLiquidityUsd: parseFloat(ethers.formatEther(poolInfo.liquidityUSD || '0')),
            volume24h: parseFloat(ethers.formatEther(poolInfo.volume24h || '0')),
            createdAtBlock: parseInt(poolInfo.createdAt?.toString() || '0', 10),
            createdAtTimestamp: poolInfo.createdAt
              ? new Date(parseInt(poolInfo.createdAt.toString(), 10) * 1000)
              : new Date(),
            lastUpdated: new Date(),
          };

          await this.persistPoolIfExposed(pool, pools);
        } catch (error) {
          logger.error(`Error indexing DODO pool ${poolAddress}:`, error);
        }
      }
    } catch (error) {
      logger.error(`Error indexing DODO pools:`, error);
    }

    return pools;
  }

  /**
   * Update pool reserves
   */
  async updatePoolReserves(poolAddress: string, dexType: DexType): Promise<void> {
    const pool = await this.poolRepo.getPool(this.chainId, poolAddress);
    if (!pool) {
      logger.warn(`Pool ${poolAddress} not found`);
      return;
    }

    try {
      if (dexType === 'uniswap_v2' || dexType === 'sushiswap') {
        const pair = new ethers.Contract(poolAddress, UNISWAP_V2_PAIR_ABI, this.provider);
        const [reserve0, reserve1] = await pair.getReserves();

        pool.reserve0 = reserve0.toString();
        pool.reserve1 = reserve1.toString();
        pool.lastUpdated = new Date();

        await this.poolRepo.upsertPool(pool);
      }
      // UniswapV3 and DODO use different models, would need specific implementations
    } catch (error) {
      logger.error(`Error updating pool reserves for ${poolAddress}:`, error);
    }
  }
}
