import { Contract, JsonRpcProvider, formatUnits } from 'ethers';
import { CHAIN_CONFIGS } from '../config/chains';
import { getCanonicalTokenByAddress } from '../config/canonical-tokens';
import { getCanonicalPriceUsd } from './canonical-price-oracle';
import { estimateChain138DodoLiquidityUsd } from './chain138-dodo-liquidity';
import { pmmVaultReserveFromChain } from './pmm-onchain-quote';
import {
  buildCuratedPoolRegistry,
  type CuratedPoolRegistryEntry,
} from './pool-registry-report';

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

const UNIV2_PAIR_ABI = [
  ...LP_ABI,
  'function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)',
  'function token0() view returns (address)',
  'function token1() view returns (address)',
];

export interface LpPositionRow {
  chainId: number;
  poolAddress: string;
  lpTokenAddress: string;
  lpTokenType: 'dodo_dvm' | 'univ2_pair';
  venue: string;
  pairLabel: string;
  shareBalanceRaw: string;
  shareBalanceUnits: string;
  poolTotalSupplyRaw: string;
  shareOfPool: number;
  estimatedUsd: number | null;
  baseSymbol: string;
  quoteSymbol: string;
  status: 'active' | 'zero';
}

export interface LpPositionsReport {
  generatedAt: string;
  chainId: number;
  address: string;
  rpcUsed: boolean;
  poolsScanned: number;
  activePositions: number;
  totalEstimatedUsd: number | null;
  positions: LpPositionRow[];
  notes: string[];
}

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

function toUnits(raw: bigint, decimals = 18): string {
  const base = 10n ** BigInt(decimals);
  const whole = raw / base;
  const frac = raw % base;
  if (frac === 0n) return whole.toString();
  const fracStr = frac.toString().padStart(decimals, '0').replace(/0+$/, '');
  return `${whole}.${fracStr}`;
}

function shareRatio(balance: bigint, totalSupply: bigint): number {
  if (totalSupply <= 0n) return 0;
  return Number(balance) / Number(totalSupply);
}

function decimalsForToken(chainId: number, address: string): number {
  const spec = getCanonicalTokenByAddress(chainId, normalizeAddress(address));
  return Number(spec?.decimals ?? 18);
}

function reserveAmountUsd(chainId: number, address: string, reserveRaw: bigint): number {
  const decimals = decimalsForToken(chainId, address);
  const amount = Number(formatUnits(reserveRaw, decimals));
  if (!Number.isFinite(amount) || amount <= 0) return 0;
  const priceUsd = getCanonicalPriceUsd(chainId, normalizeAddress(address)) ?? 0;
  return amount * priceUsd;
}

async function estimateDodoUsd(
  pool: CuratedPoolRegistryEntry,
  rpcUrl: string,
  share: number,
): Promise<number | null> {
  if (pool.totalLiquidityUsd != null && pool.totalLiquidityUsd > 0) {
    return pool.totalLiquidityUsd * share;
  }

  const reserves = await pmmVaultReserveFromChain({ rpcUrl, poolAddress: pool.poolAddress });
  if (!reserves) return null;

  let poolUsd = 0;
  if (pool.chainId === 138) {
    poolUsd = estimateChain138DodoLiquidityUsd({
      token0Address: pool.baseAddress,
      token1Address: pool.quoteAddress,
      reserve0: reserves.baseReserveRaw,
      reserve1: reserves.quoteReserveRaw,
    }).totalLiquidityUsd;
  } else {
    poolUsd =
      reserveAmountUsd(pool.chainId, pool.baseAddress, reserves.baseReserveRaw) +
      reserveAmountUsd(pool.chainId, pool.quoteAddress, reserves.quoteReserveRaw);
  }

  return poolUsd > 0 ? poolUsd * share : null;
}

async function estimateUniV2Usd(
  pool: CuratedPoolRegistryEntry,
  provider: JsonRpcProvider,
  share: number,
): Promise<number | null> {
  if (pool.totalLiquidityUsd != null && pool.totalLiquidityUsd > 0) {
    return pool.totalLiquidityUsd * share;
  }
  try {
    const pair = new Contract(pool.poolAddress, UNIV2_PAIR_ABI, provider);
    const [reserve0, reserve1] = await pair.getReserves();
    const token0 = normalizeAddress((await pair.token0()).toString());
    const token1 = normalizeAddress((await pair.token1()).toString());
    const baseAddress = normalizeAddress(pool.baseAddress);
    const quoteAddress = normalizeAddress(pool.quoteAddress);

    let baseReserve = reserve0;
    let quoteReserve = reserve1;
    if (token0 === quoteAddress && token1 === baseAddress) {
      baseReserve = reserve1;
      quoteReserve = reserve0;
    } else if (token0 === baseAddress) {
      baseReserve = reserve0;
      quoteReserve = reserve1;
    } else if (token1 === baseAddress) {
      baseReserve = reserve1;
      quoteReserve = reserve0;
    }

    const poolUsd =
      reserveAmountUsd(pool.chainId, baseAddress, baseReserve) +
      reserveAmountUsd(pool.chainId, quoteAddress, quoteReserve);
    return poolUsd > 0 ? poolUsd * share : null;
  } catch {
    return null;
  }
}

async function probePoolPosition(
  pool: CuratedPoolRegistryEntry,
  userAddress: string,
  rpcUrl: string,
): Promise<LpPositionRow | null> {
  const provider = new JsonRpcProvider(rpcUrl);
  const lp = new Contract(pool.lpTokenAddress, pool.lpTokenType === 'univ2_pair' ? UNIV2_PAIR_ABI : LP_ABI, provider);
  let balance: bigint;
  let totalSupply: bigint;
  try {
    balance = BigInt((await lp.balanceOf(userAddress)).toString());
    totalSupply = BigInt((await lp.totalSupply()).toString());
  } catch {
    return null;
  }

  const share = shareRatio(balance, totalSupply);
  const estimatedUsd =
    balance > 0n
      ? pool.lpTokenType === 'univ2_pair'
        ? await estimateUniV2Usd(pool, provider, share)
        : await estimateDodoUsd(pool, rpcUrl, share)
      : null;

  return {
    chainId: pool.chainId,
    poolAddress: pool.poolAddress,
    lpTokenAddress: pool.lpTokenAddress,
    lpTokenType: pool.lpTokenType,
    venue: pool.venue,
    pairLabel: `${pool.baseSymbol}/${pool.quoteSymbol}`,
    shareBalanceRaw: balance.toString(),
    shareBalanceUnits: toUnits(balance),
    poolTotalSupplyRaw: totalSupply.toString(),
    shareOfPool: share,
    estimatedUsd,
    baseSymbol: pool.baseSymbol,
    quoteSymbol: pool.quoteSymbol,
    status: balance > 0n ? 'active' : 'zero',
  };
}

export async function buildLpPositionsReport(params: {
  chainId: number;
  address: string;
  hintTokenAddresses?: string[];
}): Promise<LpPositionsReport> {
  const { chainId, address, hintTokenAddresses = [] } = params;
  const normalizedUser = normalizeAddress(address);
  if (!/^0x[a-f0-9]{40}$/.test(normalizedUser)) {
    throw new Error('Invalid address');
  }

  const chainConfig = CHAIN_CONFIGS[chainId];
  const rpcUrl = chainConfig?.rpcUrl?.trim() ?? '';
  const registry = buildCuratedPoolRegistry(chainId);
  const hintSet = new Set(hintTokenAddresses.map(normalizeAddress));

  let candidatePools = registry.pools;
  if (hintSet.size > 0) {
    const hinted = registry.pools.filter(
      (pool) =>
        hintSet.has(normalizeAddress(pool.poolAddress)) ||
        hintSet.has(normalizeAddress(pool.lpTokenAddress)),
    );
    if (hinted.length > 0) {
      candidatePools = hinted;
    }
  }

  const notes: string[] = [
    'LP positions are chain-local receipt tokens (DODO DVM or UniV2 pair). They are not bridgeable as LP; bridge underlying c*/cW* instead.',
  ];

  if (!rpcUrl) {
    return {
      generatedAt: new Date().toISOString(),
      chainId,
      address: normalizedUser,
      rpcUsed: false,
      poolsScanned: candidatePools.length,
      activePositions: 0,
      totalEstimatedUsd: null,
      positions: [],
      notes: [...notes, 'No RPC URL configured for this chain; LP scan skipped.'],
    };
  }

  const settled = await Promise.allSettled(
    candidatePools.slice(0, 64).map((pool) => probePoolPosition(pool, normalizedUser, rpcUrl)),
  );

  const positions: LpPositionRow[] = [];
  for (const row of settled) {
    if (row.status !== 'fulfilled' || !row.value) continue;
    if (row.value.status === 'active') positions.push(row.value);
  }

  positions.sort((a, b) => (b.estimatedUsd ?? 0) - (a.estimatedUsd ?? 0));
  const priced = positions.filter((p) => p.estimatedUsd != null);
  const totalEstimatedUsd =
    priced.length > 0 ? priced.reduce((sum, p) => sum + (p.estimatedUsd ?? 0), 0) : null;

  return {
    generatedAt: new Date().toISOString(),
    chainId,
    address: normalizedUser,
    rpcUsed: true,
    poolsScanned: candidatePools.length,
    activePositions: positions.length,
    totalEstimatedUsd,
    positions,
    notes,
  };
}
