
import BigNumber from "bignumber.js";

import { WEI_DECIMALS_POW, absolutizeValue, convertRatePerSecToRatePerYear, normalizeValue } from "./numeric.helpers";
import { caseInsensitiveCompare, isPoolConnectedToAggregatedPool } from "./object.helpers";
import { getAggregatedPoolsDistributionOrder } from "./market-formulas.helper";
// import { getOracleRate } from "./rate.helpers";
import { Market } from "./interfaces/market.interface";
import { Bid } from "./interfaces/bid.interface";

const ONE = absolutizeValue(1, WEI_DECIMALS_POW);
const ZERO = new BigNumber(0);

const dynamicPremiumRate = (
  baseRatePerSec: BigNumber,
  kink: BigNumber,
  normalRatePerSec: BigNumber,
  jumpRatePerSec: BigNumber,
  capacity: BigNumber,
  coverage: BigNumber,
) => {
  let utilization = coverage.isZero() ? ZERO : coverage.div(capacity).times(ONE);
  if (utilization.isGreaterThan(ONE)) {
    utilization = ONE;
  }

  const multiplierPerSec = normalRatePerSec.minus(baseRatePerSec).times(ONE).div(kink);

  if (utilization.isLessThanOrEqualTo(kink)) {
    return utilization.times(multiplierPerSec).div(ONE).plus(baseRatePerSec);
  }

  const jumpMultiplierPerSec = jumpRatePerSec.minus(normalRatePerSec).times(ONE).div(ONE.minus(kink));

  return utilization.minus(kink).times(jumpMultiplierPerSec).div(ONE).plus(kink.times(multiplierPerSec).div(ONE)).plus(baseRatePerSec);
};

const linealPremiumRate = (baseRatePerSec: BigNumber, normalRatePerSec: BigNumber, capacity: BigNumber, coverage: BigNumber) =>
  coverage.isZero() ? baseRatePerSec : coverage.times(normalRatePerSec.minus(baseRatePerSec)).div(capacity).plus(baseRatePerSec);

export class ForecastPremiumRateFormula {
  public static getQuotePerSec = (market: Market, rateMap: Map<string, string>, changeCoverage: string = "0") => {
    const newDesiredCover = new BigNumber(market.desiredCover).plus(changeCoverage);
    const updatedMarket: Market = {
      ...market,
      desiredCover: newDesiredCover.toFixed(0),
    };

    const { pureChargePerSecInCapitalToken, actualCover } = ForecastPremiumRateFormula.getMarketRateAndCharge(updatedMarket, undefined, undefined);

    // const capitalToPremiumRate = getOracleRate(market.capitalToken, market.premiumToken, market.rateOracle, rateMap);

    // const chargePerSecInPremiumToken = pureChargePerSecInCapitalToken.times(capitalToPremiumRate);

    // return { charge: chargePerSecInPremiumToken, desiredCover: newDesiredCover, actualCover };
  };

  public static getAprAndMarketCapacityWithPoolChanges = (market: Market, bids: Bid[], amountInCapitalToken: BigNumber, poolId: string) => {
    const targetBid = (bids || []).find((bid) => caseInsensitiveCompare(poolId, bid.poolId) && isPoolConnectedToAggregatedPool(bid));

    let aggregatedPoolId = targetBid?.aggregatedPoolId;
    let capacityDelta = new BigNumber(0);

    if (targetBid && market.aggregatedPools) {
      const aggregatedPool = market.aggregatedPools.find(({ id }) => id === targetBid.aggregatedPoolId);

      if (aggregatedPool) {
        const poolCapacityDelta = amountInCapitalToken.times(normalizeValue(targetBid.capacityAllowance, WEI_DECIMALS_POW));

        const newCapacity = BigNumber.min(poolCapacityDelta.plus(targetBid.capacity), targetBid.marketCapacityLimit, targetBid.maxCapacityLimit);

        capacityDelta = newCapacity.minus(targetBid.capacity);
      }
    }

    const { pureApr, totalCapacity } = ForecastPremiumRateFormula.getMarketRateAndCharge(market, capacityDelta, aggregatedPoolId);

    return { apr: pureApr, marketCapacity: totalCapacity };
  };

  public static getMarketRateAndCharge = (
    market: Market,
    capacityDelta: BigNumber | undefined,
    aggregatedPoolId: string | undefined,
    frontendOperatorFee?: string,
    policyBuyerReferralBonus?: string,
  ) => {
    let remainder = new BigNumber(market.desiredCover);
    const aggregatedPools = getAggregatedPoolsDistributionOrder(market);
    const totalCapacity = aggregatedPools.reduce(
      (acc, aggPool) => acc.plus(aggPool.totalCapacity).plus((aggPool.id === aggregatedPoolId && capacityDelta) || 0),
      new BigNumber(0),
    );

    const DEFAULT_RESULT = {
      actualCover: ZERO,
      totalCapacity,
      pureChargePerSecInCapitalToken: ZERO,
      pureRatePerSec: ZERO,
      ratePerSec: ZERO,
      pureApr: ZERO,
      apr: ZERO,
      aggregatedRateDetails: new Map<string, BigNumber>(),
    };

    if (remainder.isZero()) {
      return DEFAULT_RESULT;
    }

    let pureChargePerSecInCapitalToken = new BigNumber(0);

    let actualCover = new BigNumber(0);
    const aggregatedRateDetails = new Map<string, BigNumber>();

    for (let i = 0; i < aggregatedPools.length; i++) {
      if (remainder.isZero()) {
        break;
      }

      const aggregatedPool = aggregatedPools[i];
      const poolCapacity = new BigNumber(aggregatedPool.totalCapacity).plus((aggregatedPool.id === aggregatedPoolId && capacityDelta) || 0);
      const cover = BigNumber.min(poolCapacity, remainder);

      let rate = new BigNumber(aggregatedPool.rate);
      if (i === aggregatedPools.length - 1) {
        rate = ForecastPremiumRateFormula.getTailRate(
          rate,
          new BigNumber(market.tailKink),
          new BigNumber(market.maxPremiumRatePerSec),
          new BigNumber(market.tailJumpPremiumRatePerSec),
          poolCapacity,
          cover,
        );
      }

      aggregatedRateDetails.set(aggregatedPool.id, rate);

      pureChargePerSecInCapitalToken = pureChargePerSecInCapitalToken.plus(normalizeValue(rate, WEI_DECIMALS_POW).times(cover));
      actualCover = actualCover.plus(cover);
      remainder = remainder.minus(cover);
    }

    if (actualCover.isZero()) {
      return DEFAULT_RESULT;
    }

    let multiplier = new BigNumber(1);

    if (frontendOperatorFee) {
      multiplier = multiplier
        .plus(normalizeValue(frontendOperatorFee, WEI_DECIMALS_POW))
        .times(new BigNumber(1).minus(normalizeValue(policyBuyerReferralBonus, WEI_DECIMALS_POW)));
    }

    const pureRatePerSec = absolutizeValue(pureChargePerSecInCapitalToken, WEI_DECIMALS_POW).div(actualCover);
    const pureApr = convertRatePerSecToRatePerYear(pureRatePerSec);

    return {
      actualCover,
      totalCapacity,
      pureChargePerSecInCapitalToken,
      pureRatePerSec,
      ratePerSec: pureRatePerSec.times(multiplier),
      pureApr,
      aggregatedRateDetails,
      apr: pureApr.times(multiplier),
    };
  };

  public static getTailRate = (
    baseRatePerSec: BigNumber,
    tailKink: BigNumber,
    maxPremiumRatePerSec: BigNumber,
    tailJumpPremiumRatePerSec: BigNumber,
    capacity: BigNumber,
    coverage: BigNumber,
  ) => {
    return !tailKink.isZero()
      ? dynamicPremiumRate(baseRatePerSec, tailKink, maxPremiumRatePerSec, tailJumpPremiumRatePerSec, capacity, coverage)
      : linealPremiumRate(baseRatePerSec, maxPremiumRatePerSec, capacity, coverage);
  };
}
