import { BigNumber } from "./BigNumber"
import { concat, last, reduce } from "./arrayHelpers"
import { checkNever, OneOrMore } from "./typeHelpers"
import {
  TransferProphet,
  TransferProphet_Fee_Fixed,
  TransferProphet_Fee_Rate,
  TransferProphetAggregated,
} from "./types/TransferProphet"

export interface TransferProphetAppliedResult {
  fees: (
    | (TransferProphet_Fee_Rate & { amount: BigNumber })
    | TransferProphet_Fee_Fixed
  )[]
  netAmount: BigNumber
}

export const applyTransferProphets = (
  transferProphets: OneOrMore<TransferProphet>,
  amount: BigNumber,
): OneOrMore<TransferProphetAppliedResult> => {
  return reduce(
    (acc, transferProphet) =>
      concat(acc, [applyTransferProphet(transferProphet, last(acc).netAmount)]),
    [{ fees: [], netAmount: amount }],
    transferProphets,
  )
}

export const applyTransferProphet = (
  transferProphet: TransferProphet,
  amount: BigNumber,
): TransferProphetAppliedResult => {
  const fees: TransferProphetAppliedResult["fees"] = []

  let totalFeeAmount = BigNumber.ZERO
  for (const f of transferProphet.fees) {
    let feeAmount = BigNumber.ZERO

    if (f.type === "rate") {
      if (f.token !== transferProphet.bridgeToken) {
        throw new Error(
          `[XLinkSDK#applyTransferProphet] transferProphet.bridgeToken (${transferProphet.bridgeToken}) does not match rateFee.token (${f.token}), which is not expected`,
        )
      }
      feeAmount = BigNumber.max([
        f.minimumAmount,
        BigNumber.mul(f.rate, amount),
      ])
      fees.push({ ...f, amount: feeAmount })
    } else if (f.type === "fixed") {
      if (f.token === transferProphet.bridgeToken) {
        feeAmount = f.amount
      }
      fees.push(f)
    } else {
      checkNever(f)
    }

    totalFeeAmount = BigNumber.add(totalFeeAmount, feeAmount)
  }

  const netAmount = BigNumber.max([
    BigNumber.ZERO,
    BigNumber.minus(amount, totalFeeAmount),
  ])

  return { fees, netAmount }
}

/**
 * @example
 * composeTransferProphets(
 *   [
 *     // tokenA : chain1 -> chain2
 *     transferProphetOf(tokenAChain1, tokenAChain2),
 *     // tokenB : chain2 -> chain3
 *     transferProphetOf(tokenBChain2, tokenBChain3),
 *     // tokenC : chain3 -> chain4
 *     transferProphetOf(tokenCChain3, tokenCChain4),
 *   ],
 *   [
 *     // tokenA -> tokenB : chain2
 *     exchangeRateOf(tokenAChain2, tokenBChain2),
 *     // tokenB -> tokenC : chain3
 *     exchangeRateOf(tokenBChain3, tokenCChain3),
 *   ],
 * )
 */
export const composeTransferProphets = (
  transferProphets: readonly TransferProphet[],
  exchangeRates: readonly BigNumber[],
): TransferProphetAggregated<TransferProphet[]> => {
  const cumulativeExchangeRates = calcCumulativeExchangeRates(exchangeRates)

  return reduce(
    (res, transferProphet, idx) => ({
      ...composeTransferProphet2(
        res,
        transferProphet,
        cumulativeExchangeRates[idx],
      ),
      transferProphets: [...res.transferProphets, transferProphet],
    }),
    composeTransferProphet2(
      transferProphets[0],
      transferProphets[1],
      cumulativeExchangeRates[0],
    ) as TransferProphetAggregated<TransferProphet[]>,
    transferProphets.slice(2),
  )
}
/**
 * @example
 * calcCumulativeExchangeRates([
 *   exchangeRateOf(tokenA, tokenB),
 *   exchangeRateOf(tokenB, tokenC),
 *   exchangeRateOf(tokenC, tokenD),
 *   // ...
 * ])
 *
 * // =>
 *
 * [
 *   exchangeRateOf(tokenA, tokenB),
 *   exchangeRateOf(tokenA, tokenC),
 *   exchangeRateOf(tokenA, tokenD),
 *   // ...
 * ]
 */
const calcCumulativeExchangeRates = (
  exchangeRates: readonly BigNumber[],
): readonly BigNumber[] => {
  return BigNumber.cumulativeMul(BigNumber.ONE, exchangeRates).slice(1)
}

export const composeTransferProphet2 = (
  transferProphet1: TransferProphet,
  transferProphet2: TransferProphet,
  exchangeRate: BigNumber,
): TransferProphetAggregated<[TransferProphet, TransferProphet]> => {
  if (exchangeRate == null) {
    throw new Error(
      "[XLinkSDK#composeTransferProphet2] exchangeRate is required",
    )
  }

  const bridgeTokenMinFeeAmount = BigNumber.sum([
    ...transferProphet1.fees.flatMap(f =>
      f.type === "rate" && f.token === transferProphet1.bridgeToken
        ? [f.minimumAmount]
        : [],
    ),
    ...transferProphet2.fees.flatMap(f =>
      f.type === "rate" && f.token === transferProphet2.bridgeToken
        ? [
            /**
             * convert to the denomination of the first step token
             */
            BigNumber.div(f.minimumAmount, exchangeRate),
          ]
        : [],
    ),
  ])

  /**
   * flatFeeRate = 1 - (amount1 * (1-feeRate1) * (1-feeRate2) * (1-feeRateN...) / amount1)
   * flatFeeRate = 1 - (1-feeRate1) * (1-feeRate2) * (1-feeRateN...)
   */
  const step1FlatFeeRate = BigNumber.minus(
    BigNumber.ONE,
    last(
      BigNumber.cumulativeMul(
        BigNumber.ONE,
        transferProphet1.fees.flatMap(f =>
          f.type === "rate" && f.token === transferProphet1.bridgeToken
            ? [BigNumber.minus(1, f.rate)]
            : [],
        ),
      ),
    ),
  )
  /**
   * step2Amount = step1Amount * (1-feeRate1) * (1-feeRate2) * (1-feeRateN)... * exchangeRate
   * step2Amount = step1Amount * (1-flatFeeRate) * exchangeRate
   */
  const step1ToStep2Rate = BigNumber.mul(
    BigNumber.minus(1, step1FlatFeeRate),
    exchangeRate,
  )

  let minBridgeAmount: BigNumber | null = null
  if (
    BigNumber.isZero(bridgeTokenMinFeeAmount) /* min fee amount not set */ &&
    transferProphet1.minBridgeAmount == null &&
    transferProphet2.minBridgeAmount == null
  ) {
    minBridgeAmount = null
  } else {
    minBridgeAmount = BigNumber.max([
      bridgeTokenMinFeeAmount,
      transferProphet1.minBridgeAmount ?? 0,
      transferProphet2.minBridgeAmount == null
        ? 0
        : BigNumber.div(transferProphet2.minBridgeAmount, step1ToStep2Rate),
    ])
  }

  let maxBridgeAmount: BigNumber | null = null
  if (
    transferProphet1.maxBridgeAmount == null &&
    transferProphet2.maxBridgeAmount == null
  ) {
    maxBridgeAmount = null
  } else if (
    transferProphet1.maxBridgeAmount != null &&
    transferProphet2.maxBridgeAmount != null
  ) {
    maxBridgeAmount = BigNumber.min([
      transferProphet1.maxBridgeAmount,
      BigNumber.div(transferProphet2.maxBridgeAmount, step1ToStep2Rate),
    ])
  } else {
    maxBridgeAmount =
      transferProphet1.maxBridgeAmount ?? transferProphet2.maxBridgeAmount
  }

  return {
    isPaused: transferProphet1.isPaused || transferProphet2.isPaused,
    bridgeToken: transferProphet1.bridgeToken,
    minBridgeAmount,
    maxBridgeAmount,
    fees: [
      ...transferProphet1.fees,
      ...transferProphet2.fees.map(fee => {
        if (fee.type === "fixed") {
          if (fee.token !== transferProphet2.bridgeToken) return fee
          return {
            type: "fixed",
            token: transferProphet1.bridgeToken,
            amount: BigNumber.div(fee.amount, step1ToStep2Rate),
          } satisfies TransferProphet_Fee_Fixed
        }

        if (fee.type === "rate") {
          if (fee.token !== transferProphet2.bridgeToken) return fee
          return {
            type: "rate",
            token: transferProphet1.bridgeToken,
            /**
             * feeAmount = step2Amount * fee.rate
             * feeAmount = (step1Amount * (1 - step1FlatFeeRate) * exchangeRate) * fee.rate
             * step1Amount * newFeeRate = (step1Amount * (1 - step1FlatFeeRate) * exchangeRate) * fee.rate
             * newFeeRate = (1 - step1FlatFeeRate) * exchangeRate * fee.rate
             * newFeeRate = step1ToStep2Rate * fee.rate
             */
            rate: BigNumber.mul(step1ToStep2Rate, fee.rate),
            minimumAmount: BigNumber.div(fee.minimumAmount, step1ToStep2Rate),
          } satisfies TransferProphet_Fee_Rate
        }

        checkNever(fee)
        return fee
      }),
    ],
    transferProphets: [transferProphet1, transferProphet2],
  }
}

export const composeRates2 = (
  rate1: BigNumber,
  rate2: BigNumber,
): BigNumber => {
  /**
   * n = bridge amount
   * rate = ((n * r1) +
   *         (n * (1 - r1)) * r2)
   *        / n
   *      |
   *      V
   *      = (n * r1 / n) +
   *        (n * (1 - r1) * r2 / n)
   *      |
   *      V
   *      = r1 + (1 - r1) * r2
   */
  // prettier-ignore
  return BigNumber.sum([
    rate1,
    BigNumber.mul(
      BigNumber.minus(1, rate1),
      rate2,
    ),
  ])
}
