/**
 * Floor price helpers for the red-team agent feedback loop.
 *
 * ethPerToken is not a contract view function — it is computed off-chain as
 * lmTotalEth / adjustedOutstandingSupply, where:
 *   - lmTotalEth     = native ETH + WETH held by LiquidityManager
 *                      (mirrors ThreePositionStrategy._getEthBalance())
 *   - adjustedSupply = kraiken.outstandingSupply() minus KRK at
 *                      feeDestination and stakingPool
 *                      (mirrors LiquidityManager._getOutstandingSupply())
 */
import { Interface } from 'ethers';
import { rpcCall } from './rpc.js';

// Base WETH address — stable across Anvil forks of Base Sepolia.
const WETH = '0x4200000000000000000000000000000000000006';

const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';

const KRK_ABI = [
  'function outstandingSupply() external view returns (uint256)',
  'function peripheryContracts() external view returns (address, address)',
  'function balanceOf(address account) external view returns (uint256)',
];

/** feeDestination() is the auto-generated getter for the public storage var on LiquidityManager. */
const LM_ABI = ['function feeDestination() external view returns (address)'];

const ERC20_ABI = ['function balanceOf(address account) external view returns (uint256)'];

const krkIface = new Interface(KRK_ABI);
const lmIface = new Interface(LM_ABI);
const erc20Iface = new Interface(ERC20_ABI);

/**
 * Read full floor diagnostics for the LiquidityManager / Kraiken pair.
 *
 * All reads are issued in at most two parallel rounds to minimise latency:
 *   Round 1: ETH/WETH balances, raw outstanding supply, feeDestination, peripheryContracts
 *   Round 2 (conditional): balanceOf(feeDestination) and/or balanceOf(stakingPool) when non-zero
 *
 * outstandingSupply in the return value mirrors LiquidityManager._getOutstandingSupply():
 * it starts from kraiken.outstandingSupply() then subtracts KRK held at feeDestination
 * and stakingPool, since neither can be sold into the floor.
 *
 * ethPerToken = (lmNativeEth + lmWeth) / adjustedOutstandingSupply.
 * Returns ethPerToken = 0n when outstandingSupply is zero (uninitialized pool).
 *
 * @param lmAddress - LiquidityManager contract address.
 * @param krkAddress - Kraiken contract address.
 */
export async function getFloorState(
  rpcUrl: string,
  lmAddress: string,
  krkAddress: string,
): Promise<{
  ethPerToken: bigint;
  lmEthBalance: bigint;
  lmWethBalance: bigint;
  outstandingSupply: bigint;
}> {
  // Round 1: five reads in parallel.
  const [lmEthHex, lmWethHex, rawSupplyHex, feeDestHex, peripheryHex] = (await Promise.all([
    rpcCall(rpcUrl, 'eth_getBalance', [lmAddress, 'latest']),
    rpcCall(rpcUrl, 'eth_call', [{ to: WETH, data: erc20Iface.encodeFunctionData('balanceOf', [lmAddress]) }, 'latest']),
    rpcCall(rpcUrl, 'eth_call', [{ to: krkAddress, data: krkIface.encodeFunctionData('outstandingSupply', []) }, 'latest']),
    rpcCall(rpcUrl, 'eth_call', [{ to: lmAddress, data: lmIface.encodeFunctionData('feeDestination', []) }, 'latest']),
    rpcCall(rpcUrl, 'eth_call', [{ to: krkAddress, data: krkIface.encodeFunctionData('peripheryContracts', []) }, 'latest']),
  ])) as [string, string, string, string, string];

  const [lmWethRaw] = erc20Iface.decodeFunctionResult('balanceOf', lmWethHex);
  const [rawSupply] = krkIface.decodeFunctionResult('outstandingSupply', rawSupplyHex);
  const [feeDestination] = lmIface.decodeFunctionResult('feeDestination', feeDestHex);
  // peripheryContracts() returns (liquidityManager, stakingPool) — we only need the second.
  const [, stakingPool] = krkIface.decodeFunctionResult('peripheryContracts', peripheryHex);

  const lmEthBalance = BigInt(lmEthHex);
  const lmWethBalance = BigInt(lmWethRaw);

  // Round 2: subtract excluded KRK balances (matches _getOutstandingSupply logic).
  const isZero = (addr: string) => addr.toLowerCase() === ZERO_ADDRESS;
  const excluded: string[] = [];
  if (!isZero(feeDestination as string)) excluded.push(feeDestination as string);
  if (!isZero(stakingPool as string)) excluded.push(stakingPool as string);

  let supply = BigInt(rawSupply);
  if (excluded.length > 0) {
    const balHexes = (await Promise.all(
      excluded.map(addr =>
        rpcCall(rpcUrl, 'eth_call', [{ to: krkAddress, data: krkIface.encodeFunctionData('balanceOf', [addr]) }, 'latest']),
      ),
    )) as string[];
    for (const hex of balHexes) {
      const [bal] = krkIface.decodeFunctionResult('balanceOf', hex);
      supply -= BigInt(bal);
    }
  }

  const lmTotalEth = lmEthBalance + lmWethBalance;
  // Scale by 1e18 (WAD) before dividing so the result is in wei-per-token
  // rather than always 0n. Example: 100 ETH / 1M KRK = 1e20 * 1e18 / 1e24 = 1e14 wei/token.
  const ethPerToken = supply === 0n ? 0n : (lmTotalEth * 10n ** 18n) / supply;

  return { ethPerToken, lmEthBalance, lmWethBalance, outstandingSupply: supply };
}

/**
 * Compute the current floor price (ethPerToken) from LM state.
 *
 * Delegates to getFloorState(); callers that need multiple fields should call
 * getFloorState() directly to avoid redundant RPC calls.
 *
 * @param krkAddress - Kraiken contract address.
 * @param lmAddress - LiquidityManager contract address.
 * @returns ethPerToken in wei, 0n when outstanding supply is zero.
 */
export async function getEthPerToken(rpcUrl: string, krkAddress: string, lmAddress: string): Promise<bigint> {
  const { ethPerToken } = await getFloorState(rpcUrl, lmAddress, krkAddress);
  return ethPerToken;
}
