import { formatBigIntWhole } from './format'

export type EthereumRpcProvider = {
  request: (args: { method: string; params?: unknown }) => Promise<unknown>
}

export type WalletBalanceToken = {
  address: string
}

const BALANCE_OF_SELECTOR = '0x70a08231'

/** ERC-20 balanceOf(address) calldata for eth_call. */
export function encodeErc20BalanceOfCall(walletAddress: string): string {
  const addr = walletAddress.toLowerCase().replace(/^0x/, '')
  if (!/^[\da-f]{40}$/.test(addr)) {
    throw new Error('invalid wallet address')
  }
  return BALANCE_OF_SELECTOR + addr.padStart(64, '0')
}

export function parseHexBigInt(value: unknown): bigint {
  if (typeof value !== 'string' || !value.startsWith('0x')) return 0n
  if (value === '0x') return 0n
  return BigInt(value)
}

export async function hasContractCode(
  provider: EthereumRpcProvider,
  tokenAddress: string,
): Promise<boolean> {
  const code = (await provider.request({
    method: 'eth_getCode',
    params: [tokenAddress, 'latest'],
  })) as string
  return typeof code === 'string' && code.length > 2
}

export async function readErc20BalanceRaw(
  provider: EthereumRpcProvider,
  tokenAddress: string,
  walletAddress: string,
): Promise<bigint> {
  const result = (await provider.request({
    method: 'eth_call',
    params: [{ to: tokenAddress, data: encodeErc20BalanceOfCall(walletAddress) }, 'latest'],
  })) as string
  return parseHexBigInt(result)
}

export type WalletBalanceTokenWithRaw<T extends WalletBalanceToken = WalletBalanceToken> = T & {
  balanceRaw: string
}

/** Like filterTokensWithNonZeroBalance but retains raw balances for listings. */
export async function listTokensWithNonZeroBalance<T extends WalletBalanceToken>(
  provider: EthereumRpcProvider,
  walletAddress: string,
  tokens: T[],
  onProgress?: (current: number, total: number) => void,
): Promise<Array<WalletBalanceTokenWithRaw<T>>> {
  const funded: Array<WalletBalanceTokenWithRaw<T>> = []

  for (let index = 0; index < tokens.length; index += 1) {
    const token = tokens[index]
    onProgress?.(index + 1, tokens.length)

    const deployed = await hasContractCode(provider, token.address)
    if (!deployed) continue

    const balance = await readErc20BalanceRaw(provider, token.address, walletAddress)
    if (balance > 0n) {
      funded.push({ ...token, balanceRaw: balance.toString() })
    }
  }

  return funded
}

export async function readNativeBalanceRaw(
  provider: EthereumRpcProvider,
  walletAddress: string,
): Promise<bigint> {
  const result = (await provider.request({
    method: 'eth_getBalance',
    params: [walletAddress, 'latest'],
  })) as string
  return parseHexBigInt(result)
}

/** Keep catalog tokens whose on-chain ERC-20 balance is &gt; 0 (skip undeployed addresses). */
export async function filterTokensWithNonZeroBalance<T extends WalletBalanceToken>(
  provider: EthereumRpcProvider,
  walletAddress: string,
  tokens: T[],
  onProgress?: (current: number, total: number) => void,
): Promise<T[]> {
  const funded = await listTokensWithNonZeroBalance(provider, walletAddress, tokens, onProgress)
  return funded.map(({ balanceRaw: _ignored, ...token }) => token as unknown as T)
}

export function formatNativeEthFromWei(wei: bigint, maxFractionDigits = 4): string {
  const whole = wei / 10n ** 18n
  const fraction = wei % 10n ** 18n
  if (fraction === 0n) return `${formatBigIntWhole(whole)} ETH`

  const fractionStr = fraction.toString().padStart(18, '0').replace(/0+$/, '')
  const trimmed = fractionStr.slice(0, maxFractionDigits).replace(/0+$/, '')
  if (!trimmed) return `${formatBigIntWhole(whole)} ETH`
  return `${formatBigIntWhole(whole)}.${trimmed} ETH`
}
