import {
  applyParamsToScript,
  Assets,
  Constr,
  Credential,
  Data,
  fromText,
  LucidEvolution,
  OutRef,
  SpendingValidator,
  toText,
  TxBuilder,
  UTxO,
  validatorToAddress,
  validatorToScriptHash,
} from '@lucid-evolution/lucid';
import {
  CdpParams,
  ScriptReferences,
  SystemParams,
} from '../types/system-params';
import { IAssetHelpers } from '../helpers/asset-helpers';
import { PriceOracleContract } from './price-oracle';
import { CDPCreatorContract } from './cdp-creator';
import { CollectorContract } from './collector';
import { InterestOracleContract } from './interest-oracle';
import { GovContract } from './gov';
import { TreasuryContract } from './treasury';
import { addrDetails, getRandomElement, scriptRef } from '../helpers/lucid-utils';
import { AssetClass } from '../types/generic';
import { calculateFeeFromPercentage } from '../helpers/helpers';
import { CDP, CDPDatum, CDPFees } from '../types/indigo/cdp';
import { _cdpValidator } from '../scripts/cdp-validator';

export class CDPContract {
  static async openPosition(
    asset: string,
    collateralAmount: bigint,
    mintedAmount: bigint,
    params: SystemParams,
    lucid: LucidEvolution,
    assetRef?: OutRef,
    priceOracleRef?: OutRef,
    interestOracleRef?: OutRef,
    cdpCreatorRef?: OutRef,
    collectorRef?: OutRef,
  ): Promise<TxBuilder> {
    const [pkh, skh] = await addrDetails(lucid);
    const now = Date.now();
    const assetOut = await (assetRef
      ? IAssetHelpers.findIAssetByRef(assetRef, params, lucid)
      : IAssetHelpers.findIAssetByName(asset, params, lucid));

    // Fail if delisted asset
    if ('getOnChainPrice' in assetOut.datum.price)
      return Promise.reject('Trying to open CDP against delisted asset');

    const oracleAsset = assetOut.datum.price as AssetClass;
    const oracleOut = priceOracleRef
      ? (await lucid.utxosByOutRef([priceOracleRef]))[0]
      : await lucid.utxoByUnit(
          oracleAsset[0].unCurrencySymbol +
            fromText(oracleAsset[1].unTokenName),
        );
    if (!oracleOut.datum) return Promise.reject('Price Oracle datum not found');
    const oracleDatum = PriceOracleContract.decodePriceOracleDatum(
      oracleOut.datum,
    );

    const interestOracleAsset = assetOut.datum.interestOracle;
    const interestOracleOut = interestOracleRef
      ? (await lucid.utxosByOutRef([interestOracleRef]))[0]
      : await lucid.utxoByUnit(
          interestOracleAsset[0].unCurrencySymbol +
            fromText(interestOracleAsset[1].unTokenName),
        );
    if (!interestOracleOut.datum)
      return Promise.reject('Interest Oracle datum not found');
    const interestOracleDatum =
      InterestOracleContract.decodeInterestOracleDatum(interestOracleOut.datum);

    const cdpCreatorOut = getRandomElement(
      cdpCreatorRef
        ? await lucid.utxosByOutRef([cdpCreatorRef])
        : await lucid.utxosAtWithUnit(
            CDPCreatorContract.address(params.cdpCreatorParams, lucid),
            params.cdpCreatorParams.cdpCreatorNft[0].unCurrencySymbol +
              fromText(params.cdpCreatorParams.cdpCreatorNft[1].unTokenName),
          ),
    );
    const cdpCreatorRedeemer = CDPCreatorContract.redeemer(
      pkh,
      mintedAmount,
      collateralAmount,
      BigInt(now),
    );
    const cdpCreatorScriptRefUtxo = await CDPCreatorContract.scriptRef(
      params.scriptReferences,
      lucid,
    );

    const cdpAddress = CDPContract.address(params.cdpParams, lucid, skh);
    const cdpToken =
      params.cdpParams.cdpAuthToken[0].unCurrencySymbol +
      fromText(params.cdpParams.cdpAuthToken[1].unTokenName);

    const cdpValue: Assets = {
      lovelace: collateralAmount,
    };
    cdpValue[cdpToken] = 1n;
    const newSnapshot = InterestOracleContract.calculateUnitaryInterestSinceOracleLastUpdated(
      BigInt(now),
      interestOracleDatum,
    ) + interestOracleDatum.unitaryInterest;
    const cdpDatum = CDPContract.datum(pkh, asset, mintedAmount, {
      type: 'ActiveCDPInterestTracking',
      last_settled: BigInt(now),
      unitary_interest_snapshot: newSnapshot
    });
    
    const assetToken =
      params.cdpParams.cdpAssetSymbol.unCurrencySymbol + fromText(asset);
    const cdpTokenMintValue: Assets = {};
    cdpTokenMintValue[cdpToken] = 1n;
    const iassetTokenMintValue: Assets = {};
    iassetTokenMintValue[assetToken] = BigInt(mintedAmount);

    const cdpAuthTokenScriptRefUtxo = await CDPContract.cdpAuthTokenRef(
      params.scriptReferences,
      lucid,
    );
    const iAssetTokenScriptRefUtxo = await CDPContract.assetTokenRef(
      params.scriptReferences,
      lucid,
    );

    const debtMintingFee = calculateFeeFromPercentage(
      BigInt(assetOut.datum.debtMintingFeePercentage.getOnChainInt),
      (mintedAmount * oracleDatum.price) / 1_000_000n,
    );

    // Oracle timestamp - 20s (length of a slot)
    const cappedValidateTo = oracleDatum.expiration - 20_001n;
    const timeValidFrom = now - 1_000;
    const timeValidTo_ = now + params.cdpCreatorParams.biasTime - 1_000;
    const timeValidTo =
      cappedValidateTo <= timeValidFrom
        ? timeValidTo_
        : Math.min(timeValidTo_, Number(cappedValidateTo));

    const tx = lucid
      .newTx()
      .collectFrom([cdpCreatorOut], Data.to(cdpCreatorRedeemer))
      .readFrom([cdpCreatorScriptRefUtxo])
      .pay.ToContract(
        cdpAddress,
        { kind: 'inline', value: Data.to(cdpDatum) },
        cdpValue,
      )
      .pay.ToContract(
        cdpCreatorOut.address,
        { kind: 'inline', value: cdpCreatorOut.datum },
        cdpCreatorOut.assets,
      )
      .readFrom([oracleOut, interestOracleOut, assetOut.utxo])
      .mintAssets(cdpTokenMintValue, Data.to(new Constr(0, [])))
      .readFrom([cdpAuthTokenScriptRefUtxo])
      .mintAssets(iassetTokenMintValue, Data.to(new Constr(0, [])))
      .readFrom([iAssetTokenScriptRefUtxo])
      .addSignerKey(pkh.hash)
      .validFrom(Number(now - 60_000))
      .validTo(Number(timeValidTo));

      if (debtMintingFee > 0) {
        await CollectorContract.feeTx(
          debtMintingFee,
          lucid,
          params,
          tx,
          collectorRef,
        );
      }
    return tx;
  }

  static async deposit(
    cdpRef: OutRef,
    amount: bigint,
    params: SystemParams,
    lucid: LucidEvolution,
    assetRef?: OutRef,
    priceOracleRef?: OutRef,
    interestOracleRef?: OutRef,
    collectorRef?: OutRef,
    govRef?: OutRef,
    treasuryRef?: OutRef,
  ): Promise<TxBuilder> {
    return CDPContract.adjust(
      cdpRef,
      amount,
      0n,
      params,
      lucid,
      assetRef,
      priceOracleRef,
      interestOracleRef,
      collectorRef,
      govRef,
      treasuryRef,
    );
  }

  static async withdraw(
    cdpRef: OutRef,
    amount: bigint,
    params: SystemParams,
    lucid: LucidEvolution,
    assetRef?: OutRef,
    priceOracleRef?: OutRef,
    interestOracleRef?: OutRef,
    collectorRef?: OutRef,
    govRef?: OutRef,
    treasuryRef?: OutRef,
  ): Promise<TxBuilder> {
    return CDPContract.adjust(
      cdpRef,
      -amount,
      0n,
      params,
      lucid,
      assetRef,
      priceOracleRef,
      interestOracleRef,
      collectorRef,
      govRef,
      treasuryRef,
    );
  }

  static async mint(
    cdpRef: OutRef,
    amount: bigint,
    params: SystemParams,
    lucid: LucidEvolution,
    assetRef?: OutRef,
    priceOracleRef?: OutRef,
    interestOracleRef?: OutRef,
    collectorRef?: OutRef,
    govRef?: OutRef,
    treasuryRef?: OutRef,
  ): Promise<TxBuilder> {
    return CDPContract.adjust(
      cdpRef,
      0n,
      amount,
      params,
      lucid,
      assetRef,
      priceOracleRef,
      interestOracleRef,
      collectorRef,
      govRef,
      treasuryRef,
    );
  }

  static async burn(
    cdpRef: OutRef,
    amount: bigint,
    params: SystemParams,
    lucid: LucidEvolution,
    assetRef?: OutRef,
    priceOracleRef?: OutRef,
    interestOracleRef?: OutRef,
    collectorRef?: OutRef,
    govRef?: OutRef,
    treasuryRef?: OutRef,
  ): Promise<TxBuilder> {
    return CDPContract.adjust(
      cdpRef,
      0n,
      -amount,
      params,
      lucid,
      assetRef,
      priceOracleRef,
      interestOracleRef,
      collectorRef,
      govRef,
      treasuryRef,
    );
  }

  static async adjust(
    cdpRef: OutRef,
    collateralAmount: bigint,
    mintAmount: bigint,
    params: SystemParams,
    lucid: LucidEvolution,
    assetRef?: OutRef,
    priceOracleRef?: OutRef,
    interestOracleRef?: OutRef,
    collectorRef?: OutRef,
    govRef?: OutRef,
    treasuryRef?: OutRef,
  ): Promise<TxBuilder> {
    // Find Pkh, Skh
    const [pkh, skh] = await addrDetails(lucid);
    const now = Date.now();

    // Fail if no pkh
    if (!pkh)
      return Promise.reject(
        'Unable to determine the pub key hash of the wallet',
      );

    // Find Outputs: iAsset Output, CDP Output, Gov Output
    const cdp = (await lucid.utxosByOutRef([cdpRef]))[0];
    if (!cdp.datum) throw 'Unable to find CDP Datum';
    const cdpDatum = CDPContract.decodeCdpDatum(cdp.datum);
    if (cdpDatum.type !== 'CDP') throw 'Invalid CDP Datum';
    const iAsset = await (assetRef
      ? IAssetHelpers.findIAssetByRef(assetRef, params, lucid)
      : IAssetHelpers.findIAssetByName(cdpDatum.asset, params, lucid));

    const gov = govRef
      ? (await lucid.utxosByOutRef([govRef]))[0]
      : await lucid.utxoByUnit(
          params.govParams.govNFT[0].unCurrencySymbol +
            fromText(params.govParams.govNFT[1].unTokenName),
        );
    // const [iAsset, cdp, gov] = await lucid.utxosByOutRef([
    //   dIAssetTokenRef,
    //   dCDPTokenRef,
    //   dGovTokenRef,
    // ]);
    if (!gov.datum) throw 'Unable to find Gov Datum';
    const govData = GovContract.decodeGovDatum(gov.datum);
    if (!govData) throw 'No Governance datum found';
    const cdpScriptRefUtxo = await CDPContract.scriptRef(
      params.scriptReferences,
      lucid,
    );
    const cdpAssets = Object.assign({}, cdp.assets);
    cdpAssets['lovelace'] = cdp.assets['lovelace'] + collateralAmount;

    const interestOracleAsset = iAsset.datum.interestOracle;
    const interestOracleOut = interestOracleRef
      ? (await lucid.utxosByOutRef([interestOracleRef]))[0]
      : await lucid.utxoByUnit(
          interestOracleAsset[0].unCurrencySymbol +
            fromText(interestOracleAsset[1].unTokenName),
        );
    if (!interestOracleOut.datum)
      return Promise.reject('Interest Oracle datum not found');
    const interestOracleDatum =
      InterestOracleContract.decodeInterestOracleDatum(interestOracleOut.datum);

    let tx = lucid
      .newTx()
      .collectFrom([cdp], Data.to(new Constr(0, [BigInt(now), mintAmount, collateralAmount])))
      .readFrom([iAsset.utxo, gov, cdpScriptRefUtxo])
      .addSignerKey(pkh.hash);
    if (!cdp.datum) throw 'Unable to find CDP Datum';
    let cdpD = CDPContract.decodeCdpDatum(cdp.datum);
    if (!cdpD || cdpD.type !== 'CDP') throw 'Invalid CDP Datum';

    if (cdpD.fees.type !== 'ActiveCDPInterestTracking')
      throw 'Invalid CDP Fees';
    
    const newSnapshot = InterestOracleContract.calculateUnitaryInterestSinceOracleLastUpdated(
      BigInt(now),
      interestOracleDatum,
    ) + interestOracleDatum.unitaryInterest;
    
    const cdpD_: CDP = { 
      ...cdpD, 
      mintedAmount: cdpD.mintedAmount + mintAmount, 
      fees: { 
        type: 'ActiveCDPInterestTracking', 
        last_settled: BigInt(now), 
        unitary_interest_snapshot: newSnapshot
      } 
    };

    tx.pay.ToContract(
      cdp.address,
      {
        kind: 'inline',
        value: CDPContract.encodeCdpDatum(cdpD_),
      },
      cdpAssets,
    );

    // Find Oracle Ref Input
    const oracleAsset = iAsset.datum.price as AssetClass;
    const oracleRefInput = priceOracleRef
      ? (await lucid.utxosByOutRef([priceOracleRef]))[0]
      : await lucid.utxoByUnit(
          oracleAsset[0].unCurrencySymbol +
            fromText(oracleAsset[1].unTokenName),
        );

    // Fail if delisted asset
    if (!oracleRefInput.datum) return Promise.reject('Invalid oracle input');
    const od = PriceOracleContract.decodePriceOracleDatum(oracleRefInput.datum);
    if (!od) return Promise.reject('Invalid oracle input');

    // TODO: Sanity check: oacle expiration
    // Oracle timestamp - 20s (length of a slot)
    // Oracle timestamp - 20s (length of a slot)
    const cappedValidateTo = od.expiration - 20_001n;
    const timeValidFrom = now - 1_000;
    const timeValidTo_ = now + params.cdpCreatorParams.biasTime - 1_000;
    const timeValidTo =
      cappedValidateTo <= timeValidFrom
        ? timeValidTo_
        : Math.min(timeValidTo_, Number(cappedValidateTo));
    tx
      .readFrom([oracleRefInput])
      .validFrom(Number(timeValidFrom))
      .validTo(Number(timeValidTo));

    let fee = 0n;
    if (collateralAmount < 0) {
      fee += calculateFeeFromPercentage(
        govData.protocolParams.collateralFeePercentage,
        collateralAmount,
      );
    }

    if (mintAmount > 0) {
      fee += calculateFeeFromPercentage(
        iAsset.datum.debtMintingFeePercentage.getOnChainInt,
        (mintAmount * od.price) / 1_000_000n,
      );
    }

    // Interest payment
    
    const interestPaymentAsset = InterestOracleContract.calculateAccruedInterest(
      BigInt(now),
      cdpD.fees.unitary_interest_snapshot,
      cdpD.mintedAmount,
      cdpD.fees.last_settled,
      interestOracleDatum,
    );
    const interestPayment = (interestPaymentAsset * od.price) / 1_000_000n;
    const interestCollectorPayment = calculateFeeFromPercentage(
      iAsset.datum.interestCollectorPortionPercentage.getOnChainInt,
      interestPayment,
    );
    const interestTreasuryPayment = interestPayment - interestCollectorPayment;
    console.log(interestPayment, interestCollectorPayment, interestTreasuryPayment);
    if (interestTreasuryPayment > 0) {
      await TreasuryContract.feeTx(interestTreasuryPayment, lucid, params, tx, treasuryRef);
    }

    fee += interestCollectorPayment;
    tx.readFrom([interestOracleOut]);

    if (mintAmount !== 0n) {
      const iAssetTokenScriptRefUtxo = await CDPContract.assetTokenRef(
        params.scriptReferences,
        lucid,
      );
      const iassetToken =
        params.cdpParams.cdpAssetSymbol.unCurrencySymbol + fromText(cdpD.asset);
      const mintValue = {} as Assets;
      mintValue[iassetToken] = mintAmount;

      tx.readFrom([iAssetTokenScriptRefUtxo]).mintAssets(
        mintValue,
        Data.to(new Constr(0, [])),
      );
    }

    if (fee > 0n) {
      await CollectorContract.feeTx(fee, lucid, params, tx, collectorRef)
    }

    return tx;
  }

  static async close(
    cdpRef: OutRef,
    params: SystemParams,
    lucid: LucidEvolution,
    assetRef?: OutRef,
    priceOracleRef?: OutRef,
    interestOracleRef?: OutRef,
    collectorRef?: OutRef,
    govRef?: OutRef,
    treasuryRef?: OutRef,
  ): Promise<TxBuilder> {
    // Find Pkh, Skh
    const [pkh, skh] = await addrDetails(lucid);
    const now = Date.now();

    // Fail if no pkh
    if (!pkh)
      return Promise.reject(
        'Unable to determine the pub key hash of the wallet',
      );

    // Find Outputs: iAsset Output, CDP Output, Gov Output
    const cdp = (await lucid.utxosByOutRef([cdpRef]))[0];
    if (!cdp.datum) throw 'Unable to find CDP Datum';
    const cdpDatum = CDPContract.decodeCdpDatum(cdp.datum);
    if (cdpDatum.type !== 'CDP') throw 'Invalid CDP Datum';
    const iAsset = await (assetRef
      ? IAssetHelpers.findIAssetByRef(assetRef, params, lucid)
      : IAssetHelpers.findIAssetByName(cdpDatum.asset, params, lucid));

    const gov = govRef
      ? (await lucid.utxosByOutRef([govRef]))[0]
      : await lucid.utxoByUnit(
          params.govParams.govNFT[0].unCurrencySymbol +
            fromText(params.govParams.govNFT[1].unTokenName),
        );
        
    if (!gov.datum) throw 'Unable to find Gov Datum';
    const govData = GovContract.decodeGovDatum(gov.datum);
    if (!govData) throw 'No Governance datum found';
    const cdpScriptRefUtxo = await CDPContract.scriptRef(
      params.scriptReferences,
      lucid,
    );

    const interestOracleAsset = iAsset.datum.interestOracle;
    const interestOracleOut = interestOracleRef
      ? (await lucid.utxosByOutRef([interestOracleRef]))[0]
      : await lucid.utxoByUnit(
          interestOracleAsset[0].unCurrencySymbol +
            fromText(interestOracleAsset[1].unTokenName),
        );
    if (!interestOracleOut.datum)
      return Promise.reject('Interest Oracle datum not found');
    const interestOracleDatum =
      InterestOracleContract.decodeInterestOracleDatum(interestOracleOut.datum);

    let tx = lucid
      .newTx()
      .collectFrom([cdp], Data.to(new Constr(1, [BigInt(now)])))
      .readFrom([iAsset.utxo, gov, cdpScriptRefUtxo])
      .addSignerKey(pkh.hash);
    if (!cdp.datum) throw 'Unable to find CDP Datum';
    let cdpD = CDPContract.decodeCdpDatum(cdp.datum);
    if (!cdpD || cdpD.type !== 'CDP') throw 'Invalid CDP Datum';

    if (cdpD.fees.type !== 'ActiveCDPInterestTracking')
      throw 'Invalid CDP Fees';

    // Find Oracle Ref Input
    const oracleAsset = iAsset.datum.price as AssetClass;
    const oracleRefInput = priceOracleRef
      ? (await lucid.utxosByOutRef([priceOracleRef]))[0]
      : await lucid.utxoByUnit(
          oracleAsset[0].unCurrencySymbol +
            fromText(oracleAsset[1].unTokenName),
        );

    // Fail if delisted asset
    if (!oracleRefInput.datum) return Promise.reject('Invalid oracle input');
    const od = PriceOracleContract.decodePriceOracleDatum(oracleRefInput.datum);
    if (!od) return Promise.reject('Invalid oracle input');

    // TODO: Sanity check: oacle expiration
    // Oracle timestamp - 20s (length of a slot)
    // Oracle timestamp - 20s (length of a slot)
    const cappedValidateTo = od.expiration - 20_001n;
    const timeValidFrom = now - 1_000;
    const timeValidTo_ = now + params.cdpCreatorParams.biasTime - 1_000;
    const timeValidTo =
      cappedValidateTo <= timeValidFrom
        ? timeValidTo_
        : Math.min(timeValidTo_, Number(cappedValidateTo));
    tx
      .readFrom([oracleRefInput])
      .validFrom(Number(timeValidFrom))
      .validTo(Number(timeValidTo));

    let fee = 0n;

    // Interest payment
    const interestPaymentAsset = InterestOracleContract.calculateAccruedInterest(
      BigInt(now),
      cdpD.fees.unitary_interest_snapshot,
      cdpD.mintedAmount,
      cdpD.fees.last_settled,
      interestOracleDatum,
    );
    const interestPayment = (interestPaymentAsset * od.price) / 1_000_000n;
    const interestCollectorPayment = calculateFeeFromPercentage(
      iAsset.datum.interestCollectorPortionPercentage.getOnChainInt,
      interestPayment,
    );
    const interestTreasuryPayment = interestPayment - interestCollectorPayment;
    console.log(interestPayment, interestCollectorPayment, interestTreasuryPayment);
    if (interestTreasuryPayment > 0) {
      await TreasuryContract.feeTx(interestTreasuryPayment, lucid, params, tx, treasuryRef);
    }

    fee += interestCollectorPayment;
    tx.readFrom([interestOracleOut]);

    const iAssetTokenScriptRefUtxo = await CDPContract.assetTokenRef(
      params.scriptReferences,
      lucid,
    );
    const iassetToken =
      params.cdpParams.cdpAssetSymbol.unCurrencySymbol + fromText(cdpD.asset);
    const assetBurnValue = {} as Assets;
    assetBurnValue[iassetToken] = -BigInt(cdpD.mintedAmount);
    const cdpTokenBurnValue = {} as Assets;
    cdpTokenBurnValue[params.cdpParams.cdpAuthToken[0].unCurrencySymbol + fromText(params.cdpParams.cdpAuthToken[1].unTokenName)] = -1n;
    const cdpAuthTokenScriptRefUtxo = await CDPContract.cdpAuthTokenRef(
      params.scriptReferences,
      lucid,
    );
    tx.readFrom([iAssetTokenScriptRefUtxo]).mintAssets(
      assetBurnValue,
      Data.to(new Constr(0, [])),
    ).readFrom([cdpAuthTokenScriptRefUtxo]).mintAssets(
      cdpTokenBurnValue,
      Data.to(new Constr(0, [])),
    );

    if (fee > 0n) {
      await CollectorContract.feeTx(fee, lucid, params, tx, collectorRef)
    }

    return tx;
  }

  static decodeCdpDatum(datum: string): CDPDatum {
    const cdpDatum = Data.from(datum) as any;
    if (cdpDatum.index == 1 && cdpDatum.fields[0].index == 0) {
      const iasset = cdpDatum.fields[0].fields;
      return {
        type: 'IAsset',
        name: toText(iasset[0]),
        price:
          iasset[1].index === 0
            ? { getOnChainInt: iasset[1].fields[0] }
            : [
                { unCurrencySymbol: iasset[1].fields[0].fields[0].fields[0] },
                {
                  unTokenName: toText(iasset[1].fields[0].fields[0].fields[1]),
                },
              ],
        interestOracle: [
          { unCurrencySymbol: iasset[2].fields[0] },
          { unTokenName: toText(iasset[2].fields[1]) },
        ],
        redemptionRatioPercentage: { getOnChainInt: iasset[3].fields[0] },
        maintenanceRatioPercentage: { getOnChainInt: iasset[4].fields[0] },
        liquidationRatioPercentage: { getOnChainInt: iasset[5].fields[0] },
        debtMintingFeePercentage: { getOnChainInt: iasset[6].fields[0] },
        liquidationProcessingFeePercentage: {
          getOnChainInt: iasset[7].fields[0],
        },
        stabilityPoolWithdrawalFeePercentage: {
          getOnChainInt: iasset[8].fields[0],
        },
        redemptionReimbursementPercentage: {
          getOnChainInt: iasset[9].fields[0],
        },
        redemptionProcessingFeePercentage: {
          getOnChainInt: iasset[10].fields[0],
        },
        interestCollectorPortionPercentage: {
          getOnChainInt: iasset[11].fields[0],
        },
        firstAsset: iasset[12].index === 1,
        nextAsset:
          iasset[13].index === 0 ? toText(iasset[13].fields[0]) : undefined,
      };
    } else if (cdpDatum.index == 0 && cdpDatum.fields[0].index == 0) {
      const cdp = cdpDatum.fields[0].fields;
      return {
        type: 'CDP',
        owner: cdp[0].fields[0],
        asset: toText(cdp[1]),
        mintedAmount: cdp[2],
        fees:
          cdp[3].index === 0
            ? {
                type: 'ActiveCDPInterestTracking',
                last_settled: cdp[3].fields[0],
                unitary_interest_snapshot: cdp[3].fields[1],
              }
            : {
                type: 'FrozenCDPAccumulatedFees',
                lovelaces_treasury: cdp[3].fields[0],
                lovelaces_indy_stakers: cdp[3].fields[1],
              },
      };
    }

    throw 'Invalid CDP Datum provided';
  }

  static encodeCdpDatum(datum: CDPDatum): string {
    if (datum.type === 'CDP') {
      return Data.to(
        new Constr(0, [
          new Constr(0, [
            datum.owner ? new Constr(0, [datum.owner]) : new Constr(1, []),
            fromText(datum.asset),
            BigInt(datum.mintedAmount),
            datum.fees.type === 'ActiveCDPInterestTracking'
              ? new Constr(0, [
                  datum.fees.last_settled,
                  datum.fees.unitary_interest_snapshot,
                ])
              : new Constr(1, [
                  datum.fees.lovelaces_treasury,
                  datum.fees.lovelaces_indy_stakers,
                ]),
          ]),
        ]),
      );
    } else if (datum.type === 'IAsset') {
      return Data.to(
        new Constr(1, [
          new Constr(0, [
            fromText(datum.name),
            'getOnChainInt' in datum.price
              ? new Constr(0, [
                  new Constr(0, [BigInt(datum.price.getOnChainInt)]),
                ])
              : new Constr(1, [
                  new Constr(0, [
                    new Constr(0, [
                      datum.price[0].unCurrencySymbol,
                      fromText(datum.price[1].unTokenName),
                    ]),
                  ]),
                ]),
            new Constr(0, [
              datum.interestOracle[0].unCurrencySymbol,
              fromText(datum.interestOracle[1].unTokenName),
            ]),
            new Constr(0, [
              BigInt(datum.redemptionRatioPercentage.getOnChainInt),
            ]),
            new Constr(0, [
              BigInt(datum.maintenanceRatioPercentage.getOnChainInt),
            ]),
            new Constr(0, [
              BigInt(datum.liquidationRatioPercentage.getOnChainInt),
            ]),
            new Constr(0, [
              BigInt(datum.debtMintingFeePercentage.getOnChainInt),
            ]),
            new Constr(0, [
              BigInt(datum.liquidationProcessingFeePercentage.getOnChainInt),
            ]),
            new Constr(0, [
              BigInt(datum.stabilityPoolWithdrawalFeePercentage.getOnChainInt),
            ]),
            new Constr(0, [
              BigInt(datum.redemptionReimbursementPercentage.getOnChainInt),
            ]),
            new Constr(0, [
              BigInt(datum.redemptionProcessingFeePercentage.getOnChainInt),
            ]),
            new Constr(0, [
              BigInt(datum.interestCollectorPortionPercentage.getOnChainInt),
            ]),
            datum.firstAsset ? new Constr(1, []) : new Constr(0, []),
            datum.nextAsset
              ? new Constr(0, [fromText(datum.nextAsset)])
              : new Constr(1, []),
          ]),
        ]),
      );
    }

    throw 'Invalid CDP Datum provided';
  }

  static datum(
    hash: Credential,
    asset: string,
    mintedAmount: bigint,
    fees: CDPFees,
  ): Constr<Data> {
    return new Constr(0, [
      new Constr(0, [
        new Constr(0, [hash.hash]),
        fromText(asset),
        BigInt(mintedAmount),
        fees.type === 'ActiveCDPInterestTracking'
          ? new Constr(0, [
              BigInt(fees.last_settled),
              BigInt(fees.unitary_interest_snapshot),
            ])
          : new Constr(0, [
              BigInt(fees.lovelaces_treasury),
              BigInt(fees.lovelaces_indy_stakers),
            ]),
      ]),
    ]);
  }

  static validator(params: CdpParams): SpendingValidator {
    return {
      type: _cdpValidator.type,
      script: applyParamsToScript(_cdpValidator.cborHex, [
        new Constr(0, [
          new Constr(0, [
            params.cdpAuthToken[0].unCurrencySymbol,
            fromText(params.cdpAuthToken[1].unTokenName),
          ]),
          params.cdpAssetSymbol.unCurrencySymbol,
          new Constr(0, [
            params.iAssetAuthToken[0].unCurrencySymbol,
            fromText(params.iAssetAuthToken[1].unTokenName),
          ]),
          new Constr(0, [
            params.stabilityPoolAuthToken[0].unCurrencySymbol,
            fromText(params.stabilityPoolAuthToken[1].unTokenName),
          ]),
          new Constr(0, [
            params.versionRecordToken[0].unCurrencySymbol,
            fromText(params.versionRecordToken[1].unTokenName),
          ]),
          new Constr(0, [
            params.upgradeToken[0].unCurrencySymbol,
            fromText(params.upgradeToken[1].unTokenName),
          ]),
          params.collectorValHash,
          params.spValHash,
          new Constr(0, [
            params.govNFT[0].unCurrencySymbol,
            fromText(params.govNFT[1].unTokenName),
          ]),
          BigInt(params.minCollateralInLovelace),
          BigInt(params.partialRedemptionExtraFeeLovelace),
          BigInt(params.biasTime),
          params.treasuryValHash,
        ]),
      ]),
    };
  }

  static validatorHash(params: CdpParams): string {
    return validatorToScriptHash(CDPContract.validator(params));
  }

  static address(
    cdpParams: CdpParams,
    lucid: LucidEvolution,
    skh?: Credential,
  ) {
    const network = lucid.config().network;
    if (!network) {
      throw new Error('Network configuration is undefined');
    }
    return validatorToAddress(network, CDPContract.validator(cdpParams), skh);
  }

  static scriptRef(
    params: ScriptReferences,
    lucid: LucidEvolution,
  ): Promise<UTxO> {
    return scriptRef(params.cdpValidatorRef, lucid);
  }

  static cdpAuthTokenRef(
    params: ScriptReferences,
    lucid: LucidEvolution,
  ): Promise<UTxO> {
    return scriptRef(params.authTokenPolicies.cdpAuthTokenRef, lucid);
  }

  static assetTokenRef(
    params: ScriptReferences,
    lucid: LucidEvolution,
  ): Promise<UTxO> {
    return scriptRef(params.iAssetTokenPolicyRef, lucid);
  }

  static assetAuthTokenRef(
    params: ScriptReferences,
    lucid: LucidEvolution,
  ): Promise<UTxO> {
    return scriptRef(params.authTokenPolicies.iAssetTokenRef, lucid);
  }
}
