import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';

import { trimDecimals } from './math-utils';

export const SLIPPAGE_BPS_BASE = new BN(10000);
export const DEFAULT_SLIPPAGE_PERCENT = 0.5;

export type SlippageConfig = {
  mode: 'percent' | 'rate';
  percent: number;
  min_acceptable_rate?: string;
  base_currency?: string;
  updated_at_ms?: number;
};

type SlippageSource = {
  slippage_percent?: number | string | null;
  metadata?: any;
};

export function normalizeSlippagePercent(value: unknown, fallback = DEFAULT_SLIPPAGE_PERCENT): number {
  const normalized = typeof value === 'string' ? Number(value) : Number(value);
  if (!Number.isFinite(normalized) || normalized < 0) {
    return fallback;
  }
  return normalized;
}

export function normalizeSlippageConfigFromMetadata(
  metadata: any,
  fallbackPercent: number = DEFAULT_SLIPPAGE_PERCENT
): SlippageConfig | null {
  const fromMeta = metadata?.slippage;
  if (!fromMeta || typeof fromMeta !== 'object') {
    return null;
  }
  const mode = fromMeta.mode === 'rate' ? 'rate' : 'percent';
  const percent = normalizeSlippagePercent(fromMeta.percent, fallbackPercent);
  return {
    mode,
    percent,
    min_acceptable_rate: fromMeta.min_acceptable_rate,
    base_currency: fromMeta.base_currency,
    updated_at_ms: fromMeta.updated_at_ms,
  };
}

export function resolveSlippagePercent(
  quote: SlippageSource,
  checkoutSession?: SlippageSource,
  fallbackPercent: number = DEFAULT_SLIPPAGE_PERCENT
): number {
  const fromQuote = quote.slippage_percent ?? quote.metadata?.slippage?.percent;
  const fromSessionMeta = normalizeSlippageConfigFromMetadata(checkoutSession?.metadata, fallbackPercent)?.percent;
  const fromSession = checkoutSession?.slippage_percent;
  const value = fromQuote ?? fromSessionMeta ?? fromSession ?? fallbackPercent;
  return normalizeSlippagePercent(value, fallbackPercent);
}

export function buildSlippageSnapshot(params: {
  quote: SlippageSource & { quoted_amount?: string | null; exchange_rate?: string | null };
  checkoutSession?: SlippageSource;
  nowMs?: number;
}): { percent: number; max_payable_token: string; min_acceptable_rate: string; derived_at_ms: number } | null {
  const { quote, checkoutSession, nowMs = Date.now() } = params;
  if (!quote.quoted_amount || !quote.exchange_rate) {
    return null;
  }

  // Check if user is using rate mode with a specific min_acceptable_rate
  // In this case, preserve user's precise rate value instead of recalculating from percent
  const sessionSlippage = checkoutSession?.metadata?.slippage;
  const userSetMinRate = sessionSlippage?.mode === 'rate' && sessionSlippage?.min_acceptable_rate;

  const slippagePercent = resolveSlippagePercent(quote, checkoutSession);
  const slippageBps = Math.round(slippagePercent * 100);
  const multiplier = SLIPPAGE_BPS_BASE.add(new BN(slippageBps));
  const quotedAmountUnit = new BN(quote.quoted_amount);
  const maxPayableToken = quotedAmountUnit
    .mul(multiplier)
    .add(SLIPPAGE_BPS_BASE.sub(new BN(1)))
    .div(SLIPPAGE_BPS_BASE);

  let minAcceptableRate: string;
  if (userSetMinRate) {
    // Rate mode: preserve user's precise min_acceptable_rate value
    minAcceptableRate = sessionSlippage.min_acceptable_rate;
  } else {
    // Percent mode: calculate min_acceptable_rate from percent
    const USD_DECIMALS = 8;
    const rateBN = fromTokenToUnit(trimDecimals(quote.exchange_rate, USD_DECIMALS), USD_DECIMALS);
    const minRateBN = rateBN.mul(SLIPPAGE_BPS_BASE).div(multiplier);
    minAcceptableRate = fromUnitToToken(minRateBN.toString(), USD_DECIMALS);
  }

  return {
    percent: slippagePercent,
    max_payable_token: maxPayableToken.toString(),
    min_acceptable_rate: minAcceptableRate,
    derived_at_ms: nowMs,
  };
}

export function isRateBelowMinAcceptableRate(
  currentRate?: string | number | null,
  minAcceptableRate?: string | number | null
): boolean {
  const current = Number(currentRate);
  const min = Number(minAcceptableRate);
  if (!Number.isFinite(current) || !Number.isFinite(min)) {
    return false;
  }
  return current < min;
}

/**
 * Apply slippage to an amount to get the maximum payable amount
 * This is used for delegation authorization to ensure sufficient buffer
 *
 * @param amount - Base amount in smallest unit (e.g., wei)
 * @param slippagePercent - Slippage percentage (e.g., 15 for 15%)
 * @returns Maximum amount with slippage applied (rounded up)
 */
export function applySlippageToAmount(amount: BN, slippagePercent: number): BN {
  if (!slippagePercent || slippagePercent <= 0) {
    return amount;
  }
  const slippageBps = Math.round(slippagePercent * 100);
  const multiplier = SLIPPAGE_BPS_BASE.add(new BN(slippageBps));
  // Round up: (amount * multiplier + BASE - 1) / BASE
  return amount
    .mul(multiplier)
    .add(SLIPPAGE_BPS_BASE.sub(new BN(1)))
    .div(SLIPPAGE_BPS_BASE);
}
