/**
 * The following is the math related to the leverage calculations.
 *
 * Leverage is the multiplier you apply to the base deposit and you get the amount of final collateral
 * the CDP should have. Additionally, the minted amount is used to pay for fees. The leverage a user picks, is
 * already taking into account the fees, i.e. the fees are paid from the borrowed assets.
 *
 * There's a direct relationship between collateral ratio and leverage multiplier. Each leverage multiplier
 * results in a single collateral ratio and vice versa. Maximum potential leverage is the leverage that
 * results in collateral ratio being the maintenance collateral ratio of the corresponding iAsset.
 *
 * `d` = base deposit
 * `b` = total borrowed value (including the fees)
 * `L` = leverage
 * `f_m` = debt minting fee
 * `f_r` = reimbursement fee
 * `c` = collateral ratio
 *
 * The following is a detailed derivation of the math:
 *
 *  1.  Since the redemption fee is proportional to the borrowed amount,
 *      we can express the ADA we get from the order book as `b'=b*(1-f_r)`,
 *      since some of the borrowed amount goes back to the order book.
 *
 *  2.  Since all the minted iAsset are used to get borrowed ADA,
 *      the value of the minted asset will be `b`.
 *
 *  3.  The minting fee is a percentage of the value of the minted iAsset.
 *      Therefore the available ADA to add as collateral is `b''=b' - b*f_m = b*(1 - f_r - f_m)`.
 *
 *  4.  The collateral ratio can now be expressed as `c = (d + b * (1 - f_r - f_m)) / b`.
 *
 *  5.  Working out the expression, we can express `b` in terms of everything else: `b = d / (c - 1 + f_r + f_m)`.
 *
 *  6.  The minted amount will be `b / asset_price`.
 *
 *  7.  Collateral amount of the CDP is `d + b * (1 - f_r - f_m)`
 *
 *  8.  Leverage calculation: `L = (d + b * (1 - f_r - f_m)) / d`.
 *
 *      Plugging in the `b` formula we get: `L = (d + (d / (c - 1 + f_r + f_m)) * (1 - f_r - f_m)) / d`.
 *
 *      Simplified, yields the following:
 *      `L = 1 + ((1 - f_r - f_m) / (c - 1 + f_r + f_m))`
 *
 *  9.  `b'' = b * (1 - f_r - f_m)`
 *      Solved for `b` yields the following:
 *      `b = b'' / (1 - f_r - f_m)`
 *
 *  10. Having leverage and base deposit, we can find `b''`:
 *      `b’’ = d(L - 1)`
 */

import { UTxO } from '@lucid-evolution/lucid';
import { OCD_DECIMAL_UNIT, OnChainDecimal } from '../../types/on-chain-decimal';
import { bigintMax, bigintMin, fromDecimal } from '../../utils/bigint-utils';
import { array as A, function as F } from 'fp-ts';
import { Decimal } from 'decimal.js';
import {
  calculateSpendAmtWhenRobBuyOrder,
  calculateTotalCollateralForRedemption,
  robCollateralAmtToSpend,
} from '../rob/helpers';
import {
  Rational,
  rationalFloor,
  rationalFromInt,
  rationalMul,
} from '../../types/rational';
import { AssetClass } from '@3rd-eye-labs/cardano-offchain-common';
import { RobDatum } from '../rob/types-new';
import { calculateFeeFromRatio } from '../../utils/indigo-helpers';
import { iassetValueOfCollateral } from '../cdp/helpers';

/**
 * How many LRP redemptions can we fit into a TX with CDP open.
 */
export const MAX_REDEMPTIONS_WITH_CDP_OPEN = 4;

type ROBRedemptionDetails = {
  utxo: UTxO;
  redeemedCollateral: bigint;
  /**
   * The amount of iAssets paid to ROB.
   */
  iassetsPayoutAmt: bigint;
  reimbursementIAssetAmt: bigint;
};

type ApproximateLeverageRedemptionsResult = {
  leverage: number;
  collateralRatio: Rational;
  redeemedCollateral: bigint;
};

/**
 * We assume exact precision. However, actual redemptions include rounding and
 * the rounding behaviour changes based on the number of redemptions.
 * This may slightly tweak the numbers and the result can be different.
 *
 * The math is described at the top of this code file.
 */
export function approximateLeverageRedemptions(
  baseCollateral: bigint,
  targetLeverage: number,
  redemptionReimbursementRatio: Rational,
  debtMintingFeeRatio: Rational,
): ApproximateLeverageRedemptionsResult {
  const debtMintingFeeRatioDecimal = Decimal(debtMintingFeeRatio.numerator).div(
    debtMintingFeeRatio.denominator,
  );

  const redemptionReimbursementRatioDecimal = Decimal(
    redemptionReimbursementRatio.numerator,
  ).div(redemptionReimbursementRatio.denominator);

  const totalFeeRatio = debtMintingFeeRatioDecimal.add(
    redemptionReimbursementRatioDecimal,
  );

  // b''
  const bExFees = Decimal(baseCollateral)
    .mul(targetLeverage)
    .minus(baseCollateral)
    .floor();

  // b = b’’ / (1-f_r - f_m)
  const b = bExFees.div(Decimal(1).minus(totalFeeRatio)).floor();

  // c = (d + b * (1 - f_r - f_m)) / b
  const collateralRatio: Rational = {
    numerator: fromDecimal(Decimal(baseCollateral).add(bExFees)),
    denominator: fromDecimal(b),
  };

  return {
    leverage: targetLeverage,
    collateralRatio: collateralRatio,
    redeemedCollateral: fromDecimal(bExFees),
  };
}

export function summarizeActualLeverageRedemptions(
  lovelacesForRedemptionWithReimbursement: bigint,
  redemptionReimbursementRatio: Rational,
  iassetPrice: Rational,
  // Picking from the beginning until the iasset redemption amount is satisfied.
  redemptionLrps: [UTxO, RobDatum][],
): {
  redemptions: ROBRedemptionDetails[];
  /**
   * The actual amount received from redemptions (i.e. without the reimbursement fee).
   */
  totalRedeemedCollateral: bigint;
  /**
   * Total amount of IAssets to cover the reimbursement fee.
   */
  totalReimbursedIAsset: bigint;
  /**
   * Total amount of IAssets paid to ROBs, including the reimbursement.
   */
  totalIAssetPayout: bigint;
} {
  type Accumulator = {
    /// The remaining collateral to spend from ROBs
    remainingCollateralToSpend: bigint;
    redemptions: ROBRedemptionDetails[];
  };

  const redemptionDetails = F.pipe(
    redemptionLrps,
    A.reduce<[UTxO, RobDatum], Accumulator>(
      {
        remainingCollateralToSpend: lovelacesForRedemptionWithReimbursement,
        redemptions: [],
      },
      (acc, lrp) => {
        if (
          acc.remainingCollateralToSpend <= 0n ||
          iassetValueOfCollateral(
            acc.remainingCollateralToSpend,
            iassetPrice,
          ) <= 0n
        ) {
          return acc;
        }

        const collateralToSpend = robCollateralAmtToSpend(
          lrp[0].assets,
          lrp[1].orderType,
        );

        if (collateralToSpend === 0n) {
          return acc;
        }

        const newRemainingCollateral = bigintMax(
          acc.remainingCollateralToSpend - collateralToSpend,
          0n,
        );
        const collateralToSpendInitial =
          acc.remainingCollateralToSpend - newRemainingCollateral;

        const finalPayoutIAssets = calculateSpendAmtWhenRobBuyOrder(
          collateralToSpendInitial,
          redemptionReimbursementRatio,
          iassetPrice,
        );

        const feeIAssetAmt = calculateFeeFromRatio(
          redemptionReimbursementRatio,
          finalPayoutIAssets,
        );

        // We need to calculate the new number since redemptionIAssets got corrected by rounding.
        const finalCollateralToSpend = rationalFloor(
          rationalMul(
            rationalFromInt(finalPayoutIAssets - feeIAssetAmt),
            iassetPrice,
          ),
        );

        return {
          remainingCollateralToSpend:
            acc.remainingCollateralToSpend - finalCollateralToSpend,
          redemptions: [
            ...acc.redemptions,
            {
              utxo: lrp[0],
              iassetsPayoutAmt: finalPayoutIAssets,
              redeemedCollateral: finalCollateralToSpend,
              reimbursementIAssetAmt: feeIAssetAmt,
            },
          ],
        };
      },
    ),
  );

  const res = F.pipe(
    redemptionDetails.redemptions,
    A.reduce<
      ROBRedemptionDetails,
      {
        redeemedCollateral: bigint;
        payoutIAssets: bigint;
        reimbursementIAssets: bigint;
      }
    >(
      {
        redeemedCollateral: 0n,
        payoutIAssets: 0n,
        reimbursementIAssets: 0n,
      },
      (acc, details) => {
        return {
          redeemedCollateral:
            acc.redeemedCollateral + details.redeemedCollateral,
          reimbursementIAssets:
            acc.reimbursementIAssets + details.reimbursementIAssetAmt,
          payoutIAssets: acc.payoutIAssets + details.iassetsPayoutAmt,
        };
      },
    ),
  );

  return {
    redemptions: redemptionDetails.redemptions,
    totalRedeemedCollateral: res.redeemedCollateral,
    totalReimbursedIAsset: res.reimbursementIAssets,
    totalIAssetPayout: res.payoutIAssets,
  };
}

/**
 * The math is described at the top of this code file.
 */
export function calculateCollateralRatioFromLeverage(
  iasset: Uint8Array<ArrayBufferLike>,
  collateralAsset: AssetClass,
  leverage: number,
  baseCollateral: bigint,
  iassetPrice: Rational,
  debtMintingFeePercentage: OnChainDecimal,
  redemptionReimbursementPercentage: OnChainDecimal,
  allLrps: [UTxO, RobDatum][],
): OnChainDecimal | undefined {
  const debtMintingFeeRatioDecimal = Decimal(
    debtMintingFeePercentage.getOnChainInt,
  )
    .div(OCD_DECIMAL_UNIT)
    .div(100);
  const redemptionReimbursementRatioDecimal = Decimal(
    redemptionReimbursementPercentage.getOnChainInt,
  )
    .div(OCD_DECIMAL_UNIT)
    .div(100);

  const totalFeeRatio = debtMintingFeeRatioDecimal.add(
    redemptionReimbursementRatioDecimal,
  );

  const maxAvailableCollateralForRedemption =
    calculateTotalCollateralForRedemption(
      iasset,
      collateralAsset,
      iassetPrice,
      allLrps,
      MAX_REDEMPTIONS_WITH_CDP_OPEN,
    );

  if (
    leverage <= 1 ||
    baseCollateral <= 0n ||
    maxAvailableCollateralForRedemption <= 0n
  ) {
    return undefined;
  }

  // b''
  const bExFees = Decimal(baseCollateral)
    .mul(leverage)
    .minus(baseCollateral)
    .floor();

  // b = b’’ / (1-f_r - f_m)
  const b = bExFees.div(Decimal(1).minus(totalFeeRatio)).floor();

  const cappedB = bigintMin(
    maxAvailableCollateralForRedemption,
    fromDecimal(b),
  );

  const cappedBExFees = Decimal(cappedB)
    .mul(Decimal(1).minus(totalFeeRatio))
    .floor();

  // c = (d + b * (1 - f_r - f_m)) / b
  const collateralRatio = Decimal(
    Decimal(baseCollateral).add(cappedBExFees),
  ).div(cappedB);

  return {
    getOnChainInt: fromDecimal(
      collateralRatio.mul(100n * OCD_DECIMAL_UNIT).floor(),
    ),
  };
}

/**
 * The math is described at the top of this code file.
 */
export function calculateLeverageFromCollateralRatio(
  iasset: Uint8Array<ArrayBufferLike>,
  collateralAsset: AssetClass,
  collateralRatio: Rational,
  baseCollateral: bigint,
  iassetPrice: Rational,
  debtMintingFeeRatio: Rational,
  redemptionReimbursementRatio: Rational,
  allLrps: [UTxO, RobDatum][],
): number | undefined {
  const debtMintingFeeRatioDecimal = Decimal(debtMintingFeeRatio.numerator).div(
    debtMintingFeeRatio.denominator,
  );
  const redemptionReimbursementRatioDecimal = Decimal(
    redemptionReimbursementRatio.numerator,
  ).div(redemptionReimbursementRatio.denominator);

  const totalFeeRatio = debtMintingFeeRatioDecimal.add(
    redemptionReimbursementRatioDecimal,
  );

  const collateralRatioDecimal = Decimal(collateralRatio.numerator).div(
    collateralRatio.denominator,
  );

  const maxAvailableCollateralForRedemption =
    calculateTotalCollateralForRedemption(
      iasset,
      collateralAsset,
      iassetPrice,
      allLrps,
      MAX_REDEMPTIONS_WITH_CDP_OPEN,
    );

  if (
    collateralRatioDecimal.toNumber() <= 1 ||
    baseCollateral <= 0n ||
    maxAvailableCollateralForRedemption <= 0n
  ) {
    return undefined;
  }

  // The leverage unconstrained by the liquidity in LRP
  const theoreticalMaxLeverage = Decimal(Decimal(1).minus(totalFeeRatio))
    .div(collateralRatioDecimal.minus(1).add(totalFeeRatio))
    .add(1);

  // b''
  const bExFees = theoreticalMaxLeverage
    .mul(baseCollateral)
    .minus(baseCollateral)
    .floor();

  // b = b’’ / (1-f_r - f_m)
  const b = bExFees.div(Decimal(1).minus(totalFeeRatio)).floor();

  const cappedB = bigintMin(
    maxAvailableCollateralForRedemption,
    fromDecimal(b),
  );

  const cappedBExFees = Decimal(cappedB)
    .mul(Decimal(1).minus(totalFeeRatio))
    .floor();

  return Decimal(baseCollateral)
    .add(cappedBExFees)
    .div(baseCollateral)
    .toNumber();
}
