import {
  LucidEvolution,
  TxBuilder,
  OutRef,
  UTxO,
  addAssets,
  slotToUnixTime,
  Data,
  fromHex,
  getInputIndices,
} from '@lucid-evolution/lucid';
import {
  addrDetails,
  createScriptAddress,
  getInlineDatumOrThrow,
} from '../../utils/lucid-utils';
import { serialiseCdpDatum } from '../cdp/types-new';
import { matchSingle } from '../../utils/utils';
import {
  fromSystemParamsAsset,
  fromSystemParamsScriptRef,
  SystemParams,
} from '../../types/system-params';
import { parseInterestOracleDatum } from '../interest-oracle/types-new';
import {
  serialiseCDPCreatorDatum,
  serialiseCDPCreatorRedeemer,
} from '../cdp-creator/types-new';
import { calculateUnitaryInterestSinceOracleLastUpdated } from '../interest-oracle/helpers';
import {
  approximateLeverageRedemptions,
  summarizeActualLeverageRedemptions,
  calculateLeverageFromCollateralRatio,
  MAX_REDEMPTIONS_WITH_CDP_OPEN,
} from './helpers';

import {
  parseCollateralAssetDatumOrThrow,
  parseIAssetDatumOrThrow,
} from '../iasset/types';
import { mkAssetsOf } from '@3rd-eye-labs/cardano-offchain-common';
import { RobDatum } from '../rob/types-new';
import { rationalToFloat } from '../../types/rational';
import {
  buildRedemptionsTx,
  randomRobsSubsetSatisfyingTargetCollateral,
} from '../rob/helpers';
import { treasuryFeeTx } from '../treasury/transactions';
import Decimal from 'decimal.js';
import { fromDecimal } from '../../utils/bigint-utils';
import { calculateFeeFromRatio } from '../../utils/indigo-helpers';
import { retrieveAdjustedPrice } from '../../utils/oracle-helpers';
import { attachOracle } from '../iasset/helpers';
import { OracleIdx } from '../price-oracle/types-new';

export async function leverageCdpWithRob(
  leverage: number,
  baseCollateral: bigint,
  priceOracleOutRef: OutRef | undefined,
  iassetOutRef: OutRef,
  collateralAssetOutRef: OutRef,
  cdpCreatorOref: OutRef,
  interestOracleOref: OutRef,
  treasuryOref: OutRef,
  sysParams: SystemParams,
  lucid: LucidEvolution,
  allRobs: [UTxO, RobDatum][],
  currentSlot: number,
  pythMessage: string | undefined = undefined,
  pythStateOref: OutRef | undefined = undefined,
): Promise<TxBuilder> {
  const network = lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, currentSlot));

  const [pkh, skh] = await addrDetails(lucid);

  const robScriptRefUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.robValidatorRef),
    ]),
    (_) => new Error('Expected a single ROB Ref Script UTXO'),
  );

  const cdpCreatorRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.cdpCreatorValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single cdp creator Ref Script UTXO'),
  );
  const cdpAuthTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.authTokenPolicies.cdpAuthTokenRef,
      ),
    ]),
    (_) => new Error('Expected a single cdp auth token policy Ref Script UTXO'),
  );
  const iAssetTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.iAssetTokenPolicyRef,
      ),
    ]),
    (_) => new Error('Expected a single iasset token policy Ref Script UTXO'),
  );

  const cdpCreatorUtxo = matchSingle(
    await lucid.utxosByOutRef([cdpCreatorOref]),
    (_) => new Error('Expected a single CDP creator UTXO'),
  );

  const interestOracleUtxo = matchSingle(
    await lucid.utxosByOutRef([interestOracleOref]),
    (_) => new Error('Expected a single interest oracle UTXO'),
  );
  const interestOracleDatum = parseInterestOracleDatum(
    getInlineDatumOrThrow(interestOracleUtxo),
  );

  const iassetUtxo = matchSingle(
    await lucid.utxosByOutRef([iassetOutRef]),
    (_) => new Error('Expected a single IAsset UTXO'),
  );
  const iassetDatum = parseIAssetDatumOrThrow(
    getInlineDatumOrThrow(iassetUtxo),
  );

  const collateralAssetUtxo = matchSingle(
    await lucid.utxosByOutRef([collateralAssetOutRef]),
    (_) => new Error('Expected a single collateral asset UTXO'),
  );
  const collateralAssetDatum = parseCollateralAssetDatumOrThrow(
    getInlineDatumOrThrow(collateralAssetUtxo),
  );

  const [price, _] = await retrieveAdjustedPrice(
    iassetDatum.assetName,
    collateralAssetDatum.collateralAsset,
    collateralAssetDatum.priceInfo,
    collateralAssetDatum.extraDecimals,
    priceOracleOutRef,
    pythMessage,
    sysParams.pythConfig,
    lucid,
  );

  const maxLeverage = calculateLeverageFromCollateralRatio(
    collateralAssetDatum.iasset,
    collateralAssetDatum.collateralAsset,
    collateralAssetDatum.maintenanceRatio,
    baseCollateral,
    price,
    iassetDatum.debtMintingFeeRatio,
    iassetDatum.redemptionReimbursementRatio,
    allRobs,
  );

  if (!maxLeverage) {
    throw new Error("Can't calculate max leverage with those parameters.");
  }

  const leverageSummary = approximateLeverageRedemptions(
    baseCollateral,
    leverage,
    iassetDatum.redemptionProcessingFeeRatio,
    iassetDatum.debtMintingFeeRatio,
  );

  if (maxLeverage < leverageSummary.leverage) {
    throw new Error("Can't use more leverage than max.");
  }

  if (
    rationalToFloat(leverageSummary.collateralRatio) <
    rationalToFloat(collateralAssetDatum.maintenanceRatio)
  ) {
    throw new Error(
      "Can't have collateral ratio smaller than maintenance ratio",
    );
  }

  const redemptionDetails = summarizeActualLeverageRedemptions(
    leverageSummary.redeemedCollateral,
    iassetDatum.redemptionReimbursementRatio,
    price,
    randomRobsSubsetSatisfyingTargetCollateral(
      iassetDatum.assetName,
      collateralAssetDatum.collateralAsset,
      leverageSummary.redeemedCollateral,
      price,
      allRobs,
      MAX_REDEMPTIONS_WITH_CDP_OPEN,
    ),
  );

  // payout / (1 - debtMintingFee) = total minted amount
  const mintedAmt = fromDecimal(
    Decimal(redemptionDetails.totalIAssetPayout)
      .div(
        Decimal(1).minus(
          Decimal(iassetDatum.debtMintingFeeRatio.numerator).div(
            iassetDatum.debtMintingFeeRatio.denominator,
          ),
        ),
      )
      .floor(),
  );

  const debtMintingFee = calculateFeeFromRatio(
    iassetDatum.debtMintingFeeRatio,
    mintedAmt,
  );

  const collateralAmt =
    redemptionDetails.totalRedeemedCollateral + baseCollateral;

  const cdpNftVal = mkAssetsOf(
    fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
    1n,
  );

  const iassetClass = {
    currencySymbol: fromHex(
      sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
    ),
    tokenName: iassetDatum.assetName,
  };

  const iassetTokensVal = mkAssetsOf(iassetClass, mintedAmt);

  const refScripts = [
    cdpCreatorRefScriptUtxo,
    cdpAuthTokenPolicyRefScriptUtxo,
    iAssetTokenPolicyRefScriptUtxo,
    robScriptRefUtxo,
  ];

  const tx = lucid
    .newTx()
    // Ref scripts
    .readFrom(refScripts)
    .mintAssets(cdpNftVal, Data.void())
    .mintAssets(iassetTokensVal, Data.void())
    .pay.ToContract(
      createScriptAddress(network, sysParams.validatorHashes.cdpHash, skh),
      {
        kind: 'inline',
        value: serialiseCdpDatum({
          cdpOwner: fromHex(pkh.hash),
          iasset: iassetDatum.assetName,
          collateralAsset: collateralAssetDatum.collateralAsset,
          mintedAmt: mintedAmt,
          cdpFees: {
            ActiveCDPInterestTracking: {
              lastSettled: currentTime,
              unitaryInterestSnapshot:
                calculateUnitaryInterestSinceOracleLastUpdated(
                  currentTime,
                  interestOracleDatum,
                ) + interestOracleDatum.unitaryInterest,
            },
          },
        }),
      },
      addAssets(
        cdpNftVal,
        mkAssetsOf(collateralAssetDatum.collateralAsset, collateralAmt),
      ),
    )
    .pay.ToContract(
      cdpCreatorUtxo.address,
      {
        kind: 'inline',
        value: serialiseCDPCreatorDatum({
          creatorInputOref: {
            outputIndex: BigInt(cdpCreatorUtxo.outputIndex),
            txHash: fromHex(cdpCreatorUtxo.txHash),
          },
        }),
      },
      cdpCreatorUtxo.assets,
    );

  const treasuryRefScriptUtxo =
    debtMintingFee > 0n
      ? await treasuryFeeTx(
          iassetClass,
          debtMintingFee,
          0n,
          lucid,
          sysParams,
          tx,
          cdpCreatorUtxo,
          treasuryOref,
        )
      : undefined;

  const { interval, referenceInputs: refInputs } = await attachOracle(
    iassetDatum.assetName,
    collateralAssetDatum.collateralAsset,
    collateralAssetDatum.priceInfo,
    priceOracleOutRef,
    pythStateOref,
    pythMessage,
    sysParams.pythConfig,
    sysParams.cdpCreatorParams.biasTime,
    currentSlot,
    lucid,
    tx,
  );

  const referenceInputs = [
    interestOracleUtxo,
    iassetUtxo,
    collateralAssetUtxo,
    ...refInputs,
  ];

  const refInputsIndices = getInputIndices(referenceInputs, [
    ...referenceInputs,
    ...refScripts,
    ...(treasuryRefScriptUtxo != null ? [treasuryRefScriptUtxo] : []),
  ]);

  const oracleIdx: OracleIdx =
    priceOracleOutRef !== undefined
      ? { OracleRefInputIdx: refInputsIndices[3] }
      : 'OracleVoid';

  tx.validFrom(interval.validFrom)
    .validTo(interval.validTo)
    .readFrom(referenceInputs)
    .collectFrom([cdpCreatorUtxo], {
      kind: 'self',
      makeRedeemer: (inputIdx) => {
        return serialiseCDPCreatorRedeemer({
          CreateCDP: {
            cdpOwner: fromHex(pkh.hash),
            minted: mintedAmt,
            collateralAmt: collateralAmt,
            currentTime: currentTime,
            creatorInputIdx: inputIdx,
            creatorOutputIdx: 1n,
            cdpOutputIdx: 0n,
            iassetRefInputIdx: refInputsIndices[1],
            collateralAssetRefInputIdx: refInputsIndices[2],
            interestOracleRefInputIdx: refInputsIndices[0],
            priceOracleIdx: oracleIdx,
          },
        });
      },
    });

  buildRedemptionsTx(
    redemptionDetails.redemptions.map((r) => [r.utxo, r.iassetsPayoutAmt]),
    iassetDatum.assetName,
    collateralAssetDatum.collateralAsset,
    price,
    iassetDatum.redemptionReimbursementRatio,
    sysParams,
    tx,
    2n + (debtMintingFee > 0n ? 1n : 0n),
    refInputsIndices[2],
    refInputsIndices[1],
    oracleIdx,
  );

  return tx;
}
