import { AbiCoder, Contract, JsonRpcProvider, formatUnits } from 'ethers';
import { resolveChain138RpcUrl } from '../config/chain138-rpc';
import { SwapGraphEdge } from './planner-v2-types';

const CHAIN_138 = 138;
const abiCoder = AbiCoder.defaultAbiCoder();

const DEFAULT_PILOT_UNISWAP_ROUTER = '0xd164d9ccfacf5d9f91698f296ae0cd245d964384';
const DEFAULT_NATIVE_UNISWAP_FACTORY = '0x2f7219276e3ce367db9ec74c1196a8ecee67841c';
const DEFAULT_NATIVE_UNISWAP_ROUTER = '0xde9cd8ee2811e6e64a41d5f68be315d33995975e';
const DEFAULT_NATIVE_UNISWAP_QUOTER = '0x6abbb1ceb2468e748a03a00cd6aa9bfe893afa1f';
const DEFAULT_NATIVE_UNISWAP_WETH_USDT_POOL = '0xa893add35aefe6a6d858eb01828be4592f12c9f5';
const DEFAULT_NATIVE_UNISWAP_WETH_USDC_POOL = '0xec745bfb6b3cd32f102d594e5f432d8d85b19391';
const DEFAULT_BALANCER_VAULT = '0x96423d7c1727698d8a25ebfb88131e9422d1a3c3';
const DEFAULT_CURVE_3POOL = '0xe440ec15805be4c7babcd17a63b8c8a08a492e0f';
const DEFAULT_ONEINCH_ROUTER = '0x500b84b1bc6f59c1898a5fe538ea20a758757a4f';
const DEFAULT_BALANCER_WETH_USDT_POOL_ID = '0x877cd220759e8c94b82f55450c85d382ae06856c426b56d93092a420facbc324';
const DEFAULT_BALANCER_WETH_USDC_POOL_ID = '0xd8dfb18a6baf9b29d8c2dbd74639db87ac558af120df5261dab8e2a5de69013b';
const DEFAULT_PILOT_UNISWAP_FEE = 3000;
const DEFAULT_NATIVE_UNISWAP_FEE = 500;
const DEFAULT_WETH_USD_PRICE = 2100;

const WETH = '0xc02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'.toLowerCase();
const USDT = '0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1'.toLowerCase();
const USDC = '0x71D6687F38b93CCad569Fa6352c876eea967201b'.toLowerCase();

const uniswapAbi = [
  'function getPairReserves(address tokenA,address tokenB,uint24 fee) view returns (uint256 reserveIn,uint256 reserveOut,bool exists)',
];

const nativeUniswapFactoryAbi = [
  'function getPool(address tokenA,address tokenB,uint24 fee) view returns (address)',
];

const erc20Abi = [
  'function balanceOf(address account) view returns (uint256)',
];

const balancerAbi = [
  'function getPoolTokens(bytes32 poolId) view returns (address[] tokens, uint256[] balances, uint256 lastChangeBlock)',
];

const curveAbi = [
  'function reserves(uint256 index) view returns (uint256)',
];

const oneInchAbi = [
  'function getRouteReserves(address tokenA,address tokenB) view returns (uint256 reserveIn,uint256 reserveOut,bool exists)',
];

function normalizeAddress(value?: string): string {
  return String(value || '').trim().toLowerCase();
}

function providerDataHexForUniswap(router: string, quoter: string, fee: number): string {
  return abiCoder.encode(['bytes', 'uint24', 'address', 'bool'], ['0x', fee, quoter, false]);
}

function providerDataHexForBalancer(poolId: string): string {
  return abiCoder.encode(['bytes32'], [poolId]);
}

function providerDataHexForCurve(): string {
  return abiCoder.encode(['int128', 'int128', 'bool'], [0, 1, false]);
}

function providerDataHexForOneInch(router: string): string {
  return abiCoder.encode(['address', 'address', 'bytes'], [router, router, '0x']);
}

let sharedProvider: JsonRpcProvider | null = null;
let sharedProviderUrl = '';

function getProvider(rpcUrl: string): JsonRpcProvider {
  if (!sharedProvider || sharedProviderUrl !== rpcUrl) {
    sharedProvider = new JsonRpcProvider(rpcUrl);
    sharedProviderUrl = rpcUrl;
  }
  return sharedProvider;
}

function totalLiquidityUsdForWethPair(reserveWeth: bigint, reserveStable: bigint): number {
  return Number(formatUnits(reserveWeth, 18)) * DEFAULT_WETH_USD_PRICE + Number(formatUnits(reserveStable, 6));
}

function totalLiquidityUsdForStablePair(reserveA: bigint, reserveB: bigint): number {
  return Number(formatUnits(reserveA, 6)) + Number(formatUnits(reserveB, 6));
}

function isNativeUniswapConfigured(router: string, quoter: string): boolean {
  return Boolean(
    process.env.CHAIN138_UNISWAP_V3_NATIVE_FACTORY ||
      process.env.CHAIN_138_UNISWAP_V3_FACTORY ||
      process.env.CHAIN138_UNISWAP_V3_NATIVE_WETH_USDT_POOL ||
      process.env.CHAIN138_UNISWAP_V3_NATIVE_WETH_USDC_POOL ||
      router === DEFAULT_NATIVE_UNISWAP_ROUTER ||
      quoter === DEFAULT_NATIVE_UNISWAP_QUOTER ||
      (router && router !== DEFAULT_PILOT_UNISWAP_ROUTER)
  );
}

async function resolveNativeUniswapPoolAddress(args: {
  factory: string;
  configuredPool: string;
  tokenA: string;
  tokenB: string;
  fee: number;
  rpcUrl: string;
}): Promise<string> {
  if (args.configuredPool) {
    return args.configuredPool;
  }
  if (!args.factory) {
    return '';
  }

  try {
    const factory = new Contract(args.factory, nativeUniswapFactoryAbi, getProvider(args.rpcUrl));
    return normalizeAddress(await factory.getPool(args.tokenA, args.tokenB, args.fee));
  } catch {
    return '';
  }
}

async function erc20Balance(token: string, account: string, rpcUrl: string): Promise<bigint> {
  const contract = new Contract(token, erc20Abi, getProvider(rpcUrl));
  const balance = await contract.balanceOf(account);
  return BigInt(String(balance));
}

function buildUniswapEdges(args: {
  router: string;
  quoter: string;
  fee: number;
  reserveWeth: bigint;
  reserveStable: bigint;
  stableAddress: string;
  stableSymbol: 'USDT' | 'USDC';
  notes: string[];
}): SwapGraphEdge[] {
  const edges: SwapGraphEdge[] = [];
  if (args.reserveWeth <= 0n || args.reserveStable <= 0n) {
    return edges;
  }
  const providerDataHex = providerDataHexForUniswap(args.router, args.quoter, args.fee);
  const liquidity = totalLiquidityUsdForWethPair(args.reserveWeth, args.reserveStable);
  edges.push(
    {
      kind: 'swap',
      provider: 'uniswap_v3',
      chainId: CHAIN_138,
      tokenInAddress: WETH,
      tokenOutAddress: args.stableAddress,
      tokenInSymbol: 'WETH',
      tokenOutSymbol: args.stableSymbol,
      reserveIn: args.reserveWeth.toString(),
      reserveOut: args.reserveStable.toString(),
      target: args.router,
      providerData: { fee: args.fee, quoter: args.quoter },
      providerDataHex,
      totalLiquidityUsd: liquidity,
      freshnessSeconds: 0,
      notes: args.notes,
    },
    {
      kind: 'swap',
      provider: 'uniswap_v3',
      chainId: CHAIN_138,
      tokenInAddress: args.stableAddress,
      tokenOutAddress: WETH,
      tokenInSymbol: args.stableSymbol,
      tokenOutSymbol: 'WETH',
      reserveIn: args.reserveStable.toString(),
      reserveOut: args.reserveWeth.toString(),
      target: args.router,
      providerData: { fee: args.fee, quoter: args.quoter },
      providerDataHex,
      totalLiquidityUsd: liquidity,
      freshnessSeconds: 0,
      notes: args.notes,
    }
  );
  return edges;
}

async function getNativeUniswapEdges(args: {
  rpcUrl: string;
  router: string;
  quoter: string;
  feeUsdt: number;
  feeUsdc: number;
}): Promise<SwapGraphEdge[]> {
  const factory = normalizeAddress(
    process.env.CHAIN_138_UNISWAP_V3_FACTORY ||
      process.env.CHAIN138_UNISWAP_V3_NATIVE_FACTORY ||
      DEFAULT_NATIVE_UNISWAP_FACTORY
  );
  const configuredWethUsdtPool = normalizeAddress(
    process.env.UNISWAP_V3_WETH_USDT_POOL ||
      process.env.CHAIN138_UNISWAP_V3_NATIVE_WETH_USDT_POOL ||
      DEFAULT_NATIVE_UNISWAP_WETH_USDT_POOL
  );
  const configuredWethUsdcPool = normalizeAddress(
    process.env.UNISWAP_V3_WETH_USDC_POOL ||
      process.env.CHAIN138_UNISWAP_V3_NATIVE_WETH_USDC_POOL ||
      DEFAULT_NATIVE_UNISWAP_WETH_USDC_POOL
  );

  const [wethUsdtPool, wethUsdcPool] = await Promise.all([
    resolveNativeUniswapPoolAddress({
      factory,
      configuredPool: configuredWethUsdtPool,
      tokenA: WETH,
      tokenB: USDT,
      fee: args.feeUsdt,
      rpcUrl: args.rpcUrl,
    }),
    resolveNativeUniswapPoolAddress({
      factory,
      configuredPool: configuredWethUsdcPool,
      tokenA: WETH,
      tokenB: USDC,
      fee: args.feeUsdc,
      rpcUrl: args.rpcUrl,
    }),
  ]);

  const [wethUsdtBalances, wethUsdcBalances] = await Promise.all([
    wethUsdtPool
      ? Promise.all([
          erc20Balance(WETH, wethUsdtPool, args.rpcUrl),
          erc20Balance(USDT, wethUsdtPool, args.rpcUrl),
        ])
      : Promise.resolve<[bigint, bigint]>([0n, 0n]),
    wethUsdcPool
      ? Promise.all([
          erc20Balance(WETH, wethUsdcPool, args.rpcUrl),
          erc20Balance(USDC, wethUsdcPool, args.rpcUrl),
        ])
      : Promise.resolve<[bigint, bigint]>([0n, 0n]),
  ]);

  return [
    ...buildUniswapEdges({
      router: args.router,
      quoter: args.quoter,
      fee: args.feeUsdt,
      reserveWeth: wethUsdtBalances[0],
      reserveStable: wethUsdtBalances[1],
      stableAddress: USDT,
      stableSymbol: 'USDT',
      notes: ['Chain 138 upstream-native Uniswap v3 WETH/USDT venue.'],
    }),
    ...buildUniswapEdges({
      router: args.router,
      quoter: args.quoter,
      fee: args.feeUsdc,
      reserveWeth: wethUsdcBalances[0],
      reserveStable: wethUsdcBalances[1],
      stableAddress: USDC,
      stableSymbol: 'USDC',
      notes: ['Chain 138 upstream-native Uniswap v3 WETH/USDC venue.'],
    }),
  ];
}

async function getPilotUniswapEdges(args: {
  rpcUrl: string;
  router: string;
  quoter: string;
  feeUsdt: number;
  feeUsdc: number;
}): Promise<SwapGraphEdge[]> {
  const contract = new Contract(args.router, uniswapAbi, getProvider(args.rpcUrl));
  const [wethUsdt, wethUsdc] = await Promise.all([
    contract.getPairReserves(WETH, USDT, args.feeUsdt),
    contract.getPairReserves(WETH, USDC, args.feeUsdc),
  ]);

  return [
    ...(Boolean(wethUsdt[2])
      ? buildUniswapEdges({
          router: args.router,
          quoter: args.quoter,
          fee: args.feeUsdt,
          reserveWeth: BigInt(String(wethUsdt[0])),
          reserveStable: BigInt(String(wethUsdt[1])),
          stableAddress: USDT,
          stableSymbol: 'USDT',
          notes: ['Chain 138 pilot-compatible Uniswap v3 WETH/USDT venue.'],
        })
      : []),
    ...(Boolean(wethUsdc[2])
      ? buildUniswapEdges({
          router: args.router,
          quoter: args.quoter,
          fee: args.feeUsdc,
          reserveWeth: BigInt(String(wethUsdc[0])),
          reserveStable: BigInt(String(wethUsdc[1])),
          stableAddress: USDC,
          stableSymbol: 'USDC',
          notes: ['Chain 138 pilot-compatible Uniswap v3 WETH/USDC venue.'],
        })
      : []),
  ];
}

async function getUniswapEdges(rpcUrl: string): Promise<SwapGraphEdge[]> {
  const router = normalizeAddress(process.env.UNISWAP_V3_ROUTER || DEFAULT_NATIVE_UNISWAP_ROUTER);
  const quoter = normalizeAddress(
    process.env.UNISWAP_QUOTER_ADDRESS ||
      process.env.UNISWAP_QUOTER ||
      (router === DEFAULT_PILOT_UNISWAP_ROUTER ? router : DEFAULT_NATIVE_UNISWAP_QUOTER)
  );
  if (!router || !quoter) {
    return [];
  }

  if (isNativeUniswapConfigured(router, quoter)) {
    return getNativeUniswapEdges({
      rpcUrl,
      router,
      quoter,
      feeUsdt: Number(process.env.UNISWAP_V3_WETH_USDT_FEE || DEFAULT_NATIVE_UNISWAP_FEE),
      feeUsdc: Number(process.env.UNISWAP_V3_WETH_USDC_FEE || DEFAULT_NATIVE_UNISWAP_FEE),
    });
  }

  return getPilotUniswapEdges({
    rpcUrl,
    router,
    quoter,
    feeUsdt: Number(process.env.UNISWAP_V3_WETH_USDT_FEE || DEFAULT_PILOT_UNISWAP_FEE),
    feeUsdc: Number(process.env.UNISWAP_V3_WETH_USDC_FEE || DEFAULT_PILOT_UNISWAP_FEE),
  });
}

async function getBalancerEdges(rpcUrl: string): Promise<SwapGraphEdge[]> {
  const vault = normalizeAddress(process.env.BALANCER_VAULT || DEFAULT_BALANCER_VAULT);
  const wethUsdtPoolId = process.env.BALANCER_WETH_USDT_POOL_ID || DEFAULT_BALANCER_WETH_USDT_POOL_ID;
  const wethUsdcPoolId = process.env.BALANCER_WETH_USDC_POOL_ID || DEFAULT_BALANCER_WETH_USDC_POOL_ID;
  if (!vault) {
    return [];
  }

  const contract = new Contract(vault, balancerAbi, getProvider(rpcUrl));
  const edges: SwapGraphEdge[] = [];

  for (const pair of [
    { poolId: wethUsdtPoolId, stable: USDT, stableSymbol: 'USDT' },
    { poolId: wethUsdcPoolId, stable: USDC, stableSymbol: 'USDC' },
  ]) {
    if (!pair.poolId) continue;
    const result = await contract.getPoolTokens(pair.poolId);
    const tokens = (result[0] as string[]).map(normalizeAddress);
    const balances = (result[1] as bigint[]).map((value) => BigInt(String(value)));
    const wethIndex = tokens.indexOf(WETH);
    const stableIndex = tokens.indexOf(pair.stable);
    if (wethIndex === -1 || stableIndex === -1) continue;
    const reserveWeth = balances[wethIndex];
    const reserveStable = balances[stableIndex];
    const providerDataHex = providerDataHexForBalancer(pair.poolId);
    const liquidity = totalLiquidityUsdForWethPair(reserveWeth, reserveStable);
    edges.push(
      {
        kind: 'swap',
        provider: 'balancer',
        chainId: CHAIN_138,
        tokenInAddress: WETH,
        tokenOutAddress: pair.stable,
        tokenInSymbol: 'WETH',
        tokenOutSymbol: pair.stableSymbol,
        reserveIn: reserveWeth.toString(),
        reserveOut: reserveStable.toString(),
        target: vault,
        providerData: { poolId: pair.poolId },
        providerDataHex,
        totalLiquidityUsd: liquidity,
        freshnessSeconds: 0,
        notes: [`Chain 138 pilot-compatible Balancer ${pair.stableSymbol}/WETH venue.`],
      },
      {
        kind: 'swap',
        provider: 'balancer',
        chainId: CHAIN_138,
        tokenInAddress: pair.stable,
        tokenOutAddress: WETH,
        tokenInSymbol: pair.stableSymbol,
        tokenOutSymbol: 'WETH',
        reserveIn: reserveStable.toString(),
        reserveOut: reserveWeth.toString(),
        target: vault,
        providerData: { poolId: pair.poolId },
        providerDataHex,
        totalLiquidityUsd: liquidity,
        freshnessSeconds: 0,
        notes: [`Chain 138 pilot-compatible Balancer ${pair.stableSymbol}/WETH venue.`],
      }
    );
  }

  return edges;
}

async function getCurveEdges(rpcUrl: string): Promise<SwapGraphEdge[]> {
  const curvePool = normalizeAddress(process.env.CURVE_3POOL || DEFAULT_CURVE_3POOL);
  if (!curvePool) {
    return [];
  }
  const contract = new Contract(curvePool, curveAbi, getProvider(rpcUrl));
  const [reserveUsdtRaw, reserveUsdcRaw] = await Promise.all([contract.reserves(0), contract.reserves(1)]);
  const reserveUsdt = BigInt(String(reserveUsdtRaw));
  const reserveUsdc = BigInt(String(reserveUsdcRaw));
  const providerDataHex = providerDataHexForCurve();
  const liquidity = totalLiquidityUsdForStablePair(reserveUsdt, reserveUsdc);

  return [
    {
      kind: 'swap',
      provider: 'curve',
      chainId: CHAIN_138,
      tokenInAddress: USDT,
      tokenOutAddress: USDC,
      tokenInSymbol: 'USDT',
      tokenOutSymbol: 'USDC',
      reserveIn: reserveUsdt.toString(),
      reserveOut: reserveUsdc.toString(),
      target: curvePool,
      providerData: { i: 0, j: 1, useUnderlying: false },
      providerDataHex,
      totalLiquidityUsd: liquidity,
      freshnessSeconds: 0,
      notes: ['Chain 138 pilot-compatible Curve 3Pool stable/stable venue.'],
    },
    {
      kind: 'swap',
      provider: 'curve',
      chainId: CHAIN_138,
      tokenInAddress: USDC,
      tokenOutAddress: USDT,
      tokenInSymbol: 'USDC',
      tokenOutSymbol: 'USDT',
      reserveIn: reserveUsdc.toString(),
      reserveOut: reserveUsdt.toString(),
      target: curvePool,
      providerData: { i: 0, j: 1, useUnderlying: false },
      providerDataHex,
      totalLiquidityUsd: liquidity,
      freshnessSeconds: 0,
      notes: ['Chain 138 pilot-compatible Curve 3Pool stable/stable venue.'],
    },
  ];
}

async function getOneInchEdges(rpcUrl: string): Promise<SwapGraphEdge[]> {
  const router = normalizeAddress(process.env.ONEINCH_ROUTER || DEFAULT_ONEINCH_ROUTER);
  if (!router) {
    return [];
  }
  const contract = new Contract(router, oneInchAbi, getProvider(rpcUrl));
  const [wethUsdt, wethUsdc] = await Promise.all([
    contract.getRouteReserves(WETH, USDT),
    contract.getRouteReserves(WETH, USDC),
  ]);

  const providerDataHex = providerDataHexForOneInch(router);
  const edges: SwapGraphEdge[] = [];
  if (Boolean(wethUsdt[2])) {
    const reserveWeth = BigInt(String(wethUsdt[0]));
    const reserveUsdt = BigInt(String(wethUsdt[1]));
    const liquidity = totalLiquidityUsdForWethPair(reserveWeth, reserveUsdt);
    edges.push(
      {
        kind: 'swap',
        provider: 'one_inch',
        chainId: CHAIN_138,
        tokenInAddress: WETH,
        tokenOutAddress: USDT,
        tokenInSymbol: 'WETH',
        tokenOutSymbol: 'USDT',
        reserveIn: reserveWeth.toString(),
        reserveOut: reserveUsdt.toString(),
        target: router,
        providerData: { executor: router, allowanceTarget: router },
        providerDataHex,
        totalLiquidityUsd: liquidity,
        freshnessSeconds: 0,
        notes: ['Chain 138 pilot-compatible 1inch router lane.'],
      },
      {
        kind: 'swap',
        provider: 'one_inch',
        chainId: CHAIN_138,
        tokenInAddress: USDT,
        tokenOutAddress: WETH,
        tokenInSymbol: 'USDT',
        tokenOutSymbol: 'WETH',
        reserveIn: reserveUsdt.toString(),
        reserveOut: reserveWeth.toString(),
        target: router,
        providerData: { executor: router, allowanceTarget: router },
        providerDataHex,
        totalLiquidityUsd: liquidity,
        freshnessSeconds: 0,
        notes: ['Chain 138 pilot-compatible 1inch router lane.'],
      }
    );
  }
  if (Boolean(wethUsdc[2])) {
    const reserveWeth = BigInt(String(wethUsdc[0]));
    const reserveUsdc = BigInt(String(wethUsdc[1]));
    const liquidity = totalLiquidityUsdForWethPair(reserveWeth, reserveUsdc);
    edges.push(
      {
        kind: 'swap',
        provider: 'one_inch',
        chainId: CHAIN_138,
        tokenInAddress: WETH,
        tokenOutAddress: USDC,
        tokenInSymbol: 'WETH',
        tokenOutSymbol: 'USDC',
        reserveIn: reserveWeth.toString(),
        reserveOut: reserveUsdc.toString(),
        target: router,
        providerData: { executor: router, allowanceTarget: router },
        providerDataHex,
        totalLiquidityUsd: liquidity,
        freshnessSeconds: 0,
        notes: ['Chain 138 pilot-compatible 1inch router lane.'],
      },
      {
        kind: 'swap',
        provider: 'one_inch',
        chainId: CHAIN_138,
        tokenInAddress: USDC,
        tokenOutAddress: WETH,
        tokenInSymbol: 'USDC',
        tokenOutSymbol: 'WETH',
        reserveIn: reserveUsdc.toString(),
        reserveOut: reserveWeth.toString(),
        target: router,
        providerData: { executor: router, allowanceTarget: router },
        providerDataHex,
        totalLiquidityUsd: liquidity,
        freshnessSeconds: 0,
        notes: ['Chain 138 pilot-compatible 1inch router lane.'],
      }
    );
  }
  return edges;
}

export async function getChain138PilotVenueEdges(): Promise<SwapGraphEdge[]> {
  const rpcUrl = resolveChain138RpcUrl();
  const [uniswapEdges, balancerEdges, curveEdges, oneInchEdges] = await Promise.all([
    getUniswapEdges(rpcUrl),
    getBalancerEdges(rpcUrl),
    getCurveEdges(rpcUrl),
    getOneInchEdges(rpcUrl),
  ]);
  return [...uniswapEdges, ...balancerEdges, ...curveEdges, ...oneInchEdges];
}
