import {
  addAssets,
  Data,
  fromHex,
  LucidEvolution,
  OutRef,
  sortUTxOs,
  toHex,
  TxBuilder,
  UTxO,
} from '@lucid-evolution/lucid';
import {
  parseStableswapOrderDatumOrThrow,
  serialiseStableswapOrderRedeemer,
  StableswapOrderDatum,
} from '../../src/contracts/stableswap/types-new';
import {
  fromSystemParamsScriptRef,
  SystemParams,
  treasuryFeeTx,
} from '../../src';
import {
  addressToBech32,
  assetClassToUnit,
  getInlineDatumOrThrow,
  lovelacesAmt,
  matchSingle,
  mkAssetsOf,
  mkLovelacesOf,
} from '@3rd-eye-labs/cardano-offchain-common';
import { isEmpty } from 'fp-ts/lib/Array';
import { array as A, function as F } from 'fp-ts';
import { calculateFeeFromRatio } from '../../src/utils/indigo-helpers';
import {
  parseStableswapPoolDatumOrThrow,
  serialiseCdpRedeemer,
  serialiseStableswapPoolDatum,
} from '../../src/contracts/cdp/types-new';
import { createDestinationDatum } from '../../src/contracts/stableswap/helpers';
import {
  rationalDiv,
  rationalFloor,
  rationalFromInt,
  rationalMul,
} from '../../src/types/rational';

type StableswapOrderInfo = {
  utxo: UTxO;
  datum: StableswapOrderDatum;
  suppliedIasset: bigint;
  suppliedCollateralAsset: bigint;
};
export type MutatedBatchProcessStableswapOrdersType = {
  type: 'exceed-max-execution-fee';
  maxExecutionFee: bigint;
};
export async function mutatedBatchProcessStableswapOrders(
  stableswapOrderOrefs: OutRef[],
  stableswapPoolOref: OutRef,
  treasuryOref: OutRef,
  sysParams: SystemParams,
  lucid: LucidEvolution,
  type: MutatedBatchProcessStableswapOrdersType,
): Promise<TxBuilder> {
  const stableswapScriptRefUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.stableswapValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single Stableswap Ref Script UTXO'),
  );

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

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

  if (isEmpty(stableswapOrderOrefs)) {
    throw new Error('At least one order must be provided.');
  }

  const stableswapOrderUtxos = await lucid.utxosByOutRef(stableswapOrderOrefs);

  const sortedStableswapOrderUtxos = sortUTxOs(
    stableswapOrderUtxos,
    'Canonical',
  );

  if (sortedStableswapOrderUtxos.length !== stableswapOrderOrefs.length) {
    throw new Error('Expected certain number of orders');
  }

  const mainOrderUtxo = sortedStableswapOrderUtxos[0];

  const mainOrderDatum = parseStableswapOrderDatumOrThrow(
    getInlineDatumOrThrow(mainOrderUtxo),
  );

  const iassetAc = {
    currencySymbol: fromHex(
      sysParams.stableswapParams.iassetSymbol.unCurrencySymbol,
    ),
    tokenName: mainOrderDatum.iasset,
  };

  const collateralAc = mainOrderDatum.collateralAsset;

  const ordersInfo: StableswapOrderInfo[] = sortedStableswapOrderUtxos.map(
    (orderUtxo) => {
      const orderDatum = parseStableswapOrderDatumOrThrow(
        getInlineDatumOrThrow(orderUtxo),
      );

      if (
        toHex(orderDatum.iasset) != toHex(mainOrderDatum.iasset) ||
        toHex(orderDatum.collateralAsset.currencySymbol) !=
          toHex(mainOrderDatum.collateralAsset.currencySymbol) ||
        toHex(orderDatum.collateralAsset.tokenName) !=
          toHex(mainOrderDatum.collateralAsset.tokenName)
      ) {
        throw new Error('Wrong batch of orders');
      }

      const suppliedIasset = orderUtxo.assets[assetClassToUnit(iassetAc)] ?? 0n;
      const suppliedCollateralAsset =
        orderUtxo.assets[assetClassToUnit(collateralAc)] ?? 0n;

      if (
        (suppliedIasset != 0n && suppliedCollateralAsset != 0n) ||
        (suppliedIasset == 0n && suppliedCollateralAsset == 0n)
      ) {
        throw new Error(
          'An order must supply either iAsset or collateral asset',
        );
      }

      return {
        utxo: orderUtxo,
        datum: orderDatum,
        suppliedIasset: suppliedIasset,
        suppliedCollateralAsset: suppliedCollateralAsset,
      };
    },
  );

  const totalSuppliedAssets = F.pipe(
    ordersInfo,
    A.reduce<StableswapOrderInfo, [bigint, bigint]>(
      [0n, 0n],
      (acc, orderInfo) => [
        acc[0] + orderInfo.suppliedIasset,
        acc[1] + orderInfo.suppliedCollateralAsset,
      ],
    ),
  );

  const totalSuppliedIasset = totalSuppliedAssets[0];

  const totalSuppliedCollateralAsset = totalSuppliedAssets[1];

  const stableswapPoolUtxo = matchSingle(
    await lucid.utxosByOutRef([stableswapPoolOref]),
    (_) => new Error('Expected a single cdp UTXO'),
  );

  const stableswapPoolDatum = parseStableswapPoolDatumOrThrow(
    getInlineDatumOrThrow(stableswapPoolUtxo),
  );

  const redemptionFee = calculateFeeFromRatio(
    stableswapPoolDatum.redemptionFeeRatio,
    totalSuppliedIasset,
  );

  const totalEffectiveSuppliedIasset = totalSuppliedIasset - redemptionFee;

  const collateralAmtChangePool =
    totalSuppliedCollateralAsset -
    rationalFloor(
      rationalMul(
        rationalFromInt(totalEffectiveSuppliedIasset),
        stableswapPoolDatum.collateralToIassetRatio,
      ),
    );

  const amountToMint =
    rationalFloor(
      rationalDiv(
        rationalFromInt(totalSuppliedCollateralAsset),
        stableswapPoolDatum.collateralToIassetRatio,
      ),
    ) - totalEffectiveSuppliedIasset;

  const mintingFee = calculateFeeFromRatio(
    stableswapPoolDatum.mintingFeeRatio,
    amountToMint,
  );

  const tx = lucid
    .newTx()
    .readFrom([
      stableswapScriptRefUtxo,
      cdpScriptRefUtxo,
      iAssetTokenPolicyRefScriptUtxo,
    ])
    .collectFrom([stableswapPoolUtxo], {
      kind: 'selected',
      makeRedeemer: (inputIndices) =>
        serialiseCdpRedeemer({
          Stableswap: {
            forwardingInputIndex: inputIndices[0],
          },
        }),
      inputs: [mainOrderUtxo],
    })
    .pay.ToContract(
      stableswapPoolUtxo.address,
      {
        kind: 'inline',
        value: serialiseStableswapPoolDatum(stableswapPoolDatum),
      },
      collateralAmtChangePool != 0n
        ? addAssets(
            stableswapPoolUtxo.assets,
            mkAssetsOf(collateralAc, collateralAmtChangePool),
          )
        : stableswapPoolUtxo.assets,
    );

  const fee = mintingFee + redemptionFee;

  if (fee > 0) {
    await treasuryFeeTx(
      iassetAc,
      fee,
      0n,
      lucid,
      sysParams,
      tx,
      stableswapPoolOref,
      treasuryOref,
    );
  }

  if (amountToMint != 0n) {
    tx.mintAssets(mkAssetsOf(iassetAc, amountToMint), Data.void());
  }

  F.pipe(
    ordersInfo,
    A.reduce<StableswapOrderInfo, TxBuilder>(tx, (acc, orderInfo) => {
      const iassetSupplied =
        orderInfo.utxo.assets[assetClassToUnit(iassetAc)] ?? 0n;

      const collateralAssetSupplied =
        orderInfo.utxo.assets[assetClassToUnit(collateralAc)] ?? 0n;

      const isMinting = iassetSupplied > 0n ? false : true;

      const iAssetEquivalentSupplied = isMinting
        ? rationalFloor(
            rationalDiv(
              rationalFromInt(collateralAssetSupplied),
              stableswapPoolDatum.collateralToIassetRatio,
            ),
          )
        : iassetSupplied;

      const effectiveiAssetEquivalentSupplied = isMinting
        ? iAssetEquivalentSupplied -
          calculateFeeFromRatio(
            stableswapPoolDatum.mintingFeeRatio,
            iAssetEquivalentSupplied,
          )
        : iAssetEquivalentSupplied -
          calculateFeeFromRatio(
            stableswapPoolDatum.redemptionFeeRatio,
            iAssetEquivalentSupplied,
          );

      const amountToReceive = isMinting
        ? effectiveiAssetEquivalentSupplied
        : rationalFloor(
            rationalMul(
              rationalFromInt(effectiveiAssetEquivalentSupplied),
              stableswapPoolDatum.collateralToIassetRatio,
            ),
          );

      const maxExecutionFee =
        type.type === 'exceed-max-execution-fee'
          ? type.maxExecutionFee
          : orderInfo.datum.maxExecutionFee;

      return acc
        .collectFrom(
          [orderInfo.utxo],
          orderInfo.utxo == mainOrderUtxo
            ? serialiseStableswapOrderRedeemer('BatchProcessStableswapOrders')
            : {
                kind: 'selected',
                makeRedeemer: (inputIndices: bigint[]) => {
                  return serialiseStableswapOrderRedeemer({
                    BatchAuxiliary: {
                      ownInputIndex: inputIndices[0],
                      mainOrderInputIndex: inputIndices[1],
                    },
                  });
                },
                inputs: [orderInfo.utxo, mainOrderUtxo],
              },
        )
        .pay.ToAddressWithData(
          addressToBech32(orderInfo.datum.destination, lucid.config().network!),
          {
            kind: 'inline',
            value: createDestinationDatum(
              orderInfo.datum.destinationInlineDatum ?? null,
              orderInfo.utxo,
            ),
          },
          addAssets(
            // Currently, we always take the max execution fee from the order utxo.
            // This can be improved so that we take the actual execution fee.
            mkLovelacesOf(
              lovelacesAmt(orderInfo.utxo.assets) - maxExecutionFee,
            ),
            isMinting
              ? mkAssetsOf(iassetAc, amountToReceive)
              : mkAssetsOf(collateralAc, amountToReceive),
          ),
        );
    }),
  );
  return tx;
}
