import { formatUnits } from 'ethers';
import { existsSync, readFileSync } from 'fs';
import path from 'path';
import { CHAIN138_CANONICAL_LIVE_POOLS, isChain138CanonicalLivePool } from '../config/chain138-live-dodo-pools';
import { filterPoolsForExposure } from '../config/gru-transport';
import { getGruV2DeploymentPoolRows } from '../config/gru-v2-deployment-pools';
import { resolveCanonicalQuoteAddress } from '../config/canonical-tokens';
import { PoolRepository, LiquidityPool } from '../database/repositories/pool-repo';
import { getLiveDodoPools } from './live-dodo-fallback';
import { pmmVaultReserveFromChain, resolvePmmQuoteRpcUrl } from './pmm-onchain-quote';
import { logger } from '../utils/logger';

const poolRepo = new PoolRepository();
const MAX_SINGLE_POOL_TVL_USD = 500_000_000;
const LIVE_UNISWAP_CATALOG_REL = 'config/live-uniswap-v2-pool-catalog.json';

interface LiveUniswapCatalogRow {
  chainId: number;
  poolAddress: string;
  baseAddress: string;
  quoteAddress: string;
  totalLiquidityUsd?: number;
}

function loadLiveUniswapCatalogRows(): LiveUniswapCatalogRow[] {
  const configured = process.env.TOKEN_AGGREGATION_LIVE_UNISWAP_V2_POOL_CATALOG_JSON?.trim();
  const roots = [
    configured,
    path.join(process.cwd(), LIVE_UNISWAP_CATALOG_REL),
    path.join(process.cwd(), 'smom-dbis-138', 'services', 'token-aggregation', LIVE_UNISWAP_CATALOG_REL),
  ].filter(Boolean) as string[];

  for (const candidate of roots) {
    if (!existsSync(candidate)) continue;
    try {
      const parsed = JSON.parse(readFileSync(candidate, 'utf8')) as { pools?: LiveUniswapCatalogRow[] };
      return (parsed.pools ?? []).filter(
        (row) =>
          Number.isFinite(row.chainId) &&
          row.poolAddress?.startsWith('0x') &&
          row.baseAddress?.startsWith('0x') &&
          row.quoteAddress?.startsWith('0x'),
      );
    } catch (error) {
      logger.warn('token-visible-liquidity: unable to parse UniV2 catalog', { candidate, error });
    }
  }
  return [];
}

function matchesTokenAddress(
  pool: { token0Address: string; token1Address: string },
  lookupAddress: string,
  normalizedAddress: string,
): boolean {
  return (
    pool.token0Address === lookupAddress ||
    pool.token1Address === lookupAddress ||
    pool.token0Address === normalizedAddress ||
    pool.token1Address === normalizedAddress
  );
}

function sanePoolTvlUsd(chainId: number, poolAddress: string, totalLiquidityUsd?: number): number {
  const tvl = Number(totalLiquidityUsd);
  if (!Number.isFinite(tvl) || tvl <= 0) return 0;
  if (chainId === 138 && isChain138CanonicalLivePool(poolAddress)) return tvl;
  if (tvl > MAX_SINGLE_POOL_TVL_USD) return 0;
  return tvl;
}

function mergePoolsByAddress(existing: Map<string, LiquidityPool>, pools: LiquidityPool[]): void {
  for (const pool of pools) {
    const key = pool.poolAddress.toLowerCase();
    const prior = existing.get(key);
    const nextTvl = sanePoolTvlUsd(pool.chainId, pool.poolAddress, pool.totalLiquidityUsd);
    const priorTvl = prior ? sanePoolTvlUsd(prior.chainId, prior.poolAddress, prior.totalLiquidityUsd) : 0;
    if (!prior || nextTvl > priorTvl) {
      existing.set(key, { ...pool, totalLiquidityUsd: nextTvl });
    }
  }
}

export async function collectVisiblePools(chainId: number, address: string): Promise<LiquidityPool[]> {
  const normalized = address.toLowerCase();
  const resolution = resolveCanonicalQuoteAddress(chainId, normalized);
  const merged = new Map<string, LiquidityPool>();

  try {
    const dbPools = filterPoolsForExposure(
      chainId,
      await poolRepo.getPoolsByToken(chainId, resolution.lookupAddress),
    ).filter((pool) => matchesTokenAddress(pool, resolution.lookupAddress, normalized));
    mergePoolsByAddress(merged, dbPools);
  } catch (error) {
    logger.warn('token-visible-liquidity: DB pool lookup failed', {
      chainId,
      address: resolution.lookupAddress,
      error,
    });
  }

  try {
    const livePools = filterPoolsForExposure(chainId, await getLiveDodoPools(chainId)).filter((pool) =>
      matchesTokenAddress(pool, resolution.lookupAddress, normalized),
    );
    mergePoolsByAddress(merged, livePools);
  } catch (error) {
    logger.warn('token-visible-liquidity: live DODO lookup failed', { chainId, address: normalized, error });
  }

  if (chainId === 138 && merged.size === 0) {
    const canonicalOnly = filterPoolsForExposure(chainId, await getLiveDodoPools(chainId)).filter(
      (pool) =>
        CHAIN138_CANONICAL_LIVE_POOLS.some((live) => live.toLowerCase() === pool.poolAddress.toLowerCase()) &&
        matchesTokenAddress(pool, resolution.lookupAddress, normalized),
    );
    mergePoolsByAddress(merged, canonicalOnly);
  }

  return [...merged.values()];
}

function sumLiveUniswapCatalogLiquidityUsd(chainId: number, tokenAddress: string): number {
  const token = tokenAddress.toLowerCase();
  return loadLiveUniswapCatalogRows()
    .filter(
      (row) =>
        row.chainId === chainId &&
        (row.baseAddress.toLowerCase() === token || row.quoteAddress.toLowerCase() === token),
    )
    .reduce((sum, row) => {
      const tvl = sanePoolTvlUsd(chainId, row.poolAddress, row.totalLiquidityUsd);
      return sum + tvl;
    }, 0);
}

function sumPoolTvl(chainId: number, pools: LiquidityPool[]): number {
  return pools.reduce(
    (sum, pool) => sum + sanePoolTvlUsd(chainId, pool.poolAddress, pool.totalLiquidityUsd),
    0,
  );
}

function fallbackPoolUsdPrice(symbol?: string): number {
  const normalized = String(symbol || '').toUpperCase();
  if (normalized.includes('XAU')) return Number(process.env.TOKEN_AGGREGATION_XAU_USD || '0') || 0;
  if (normalized === 'WETH' || normalized === 'ETH') return Number(process.env.TOKEN_AGGREGATION_ETH_USD || '0') || 0;
  return 1;
}

function fallbackPoolDecimals(symbol?: string): number {
  const normalized = String(symbol || '').toUpperCase();
  if (normalized === 'WETH' || normalized === 'ETH' || normalized === 'LINK') return 18;
  return 6;
}

async function sumGruV2DeploymentLiquidityUsd(chainId: number, tokenAddress: string): Promise<number> {
  if (chainId !== 138) return 0;
  const token = tokenAddress.toLowerCase();
  const rpcUrl = resolvePmmQuoteRpcUrl();
  if (!rpcUrl) return 0;

  const rows = getGruV2DeploymentPoolRows().filter(
    (pool) =>
      pool.chainId === chainId &&
      (pool.baseAddress.toLowerCase() === token || pool.quoteAddress.toLowerCase() === token),
  );

  let total = 0;
  for (const row of rows) {
    const reserves = await pmmVaultReserveFromChain({ rpcUrl, poolAddress: row.poolAddress });
    if (!reserves) continue;
    const baseUnits = Number(formatUnits(reserves.baseReserveRaw, fallbackPoolDecimals(row.baseSymbol)));
    const quoteUnits = Number(formatUnits(reserves.quoteReserveRaw, fallbackPoolDecimals(row.quoteSymbol)));
    if (!Number.isFinite(baseUnits) || !Number.isFinite(quoteUnits)) continue;
    const reserveUsd =
      baseUnits * fallbackPoolUsdPrice(row.baseSymbol) + quoteUnits * fallbackPoolUsdPrice(row.quoteSymbol);
    const saneReserveUsd = sanePoolTvlUsd(chainId, row.poolAddress, reserveUsd);
    if (Number.isFinite(saneReserveUsd) && saneReserveUsd > 0) {
      total += saneReserveUsd;
    }
  }
  return total;
}

/** Best-effort visible on-chain / indexed liquidity for wallet and market-batch surfaces. */
export async function resolveVisibleTokenLiquidityUsd(chainId: number, address: string): Promise<number> {
  const normalized = address.toLowerCase();
  const pools = await collectVisiblePools(chainId, normalized);

  if (chainId === 138) {
    const canonicalLiveTvl = pools
      .filter((pool) => isChain138CanonicalLivePool(pool.poolAddress))
      .reduce(
        (sum, pool) => sum + sanePoolTvlUsd(chainId, pool.poolAddress, pool.totalLiquidityUsd),
        0,
      );
    const gruTvl = await sumGruV2DeploymentLiquidityUsd(chainId, normalized);
    const catalogTvl = sumLiveUniswapCatalogLiquidityUsd(chainId, normalized);
    const canonicalFirst = Math.max(canonicalLiveTvl, gruTvl, catalogTvl);
    if (canonicalFirst > 0) {
      return canonicalFirst;
    }
  }

  const indexedTvl = sumPoolTvl(chainId, pools);
  const catalogTvl = sumLiveUniswapCatalogLiquidityUsd(chainId, normalized);
  const gruTvl = await sumGruV2DeploymentLiquidityUsd(chainId, normalized);
  return Math.max(indexedTvl, catalogTvl, gruTvl);
}

export { sanePoolTvlUsd, MAX_SINGLE_POOL_TVL_USD };
