import {
  addAssets,
  Address,
  credentialToAddress,
  Data,
  fromHex,
  fromText,
  LucidEvolution,
  OutRef,
  paymentCredentialOf,
  sortUTxOs,
  toHex,
  TxBuilder,
  UTxO,
} from '@lucid-evolution/lucid';
import {
  fromSystemParamsScriptRef,
  SystemParams,
} from '../../types/system-params';
import { estimateUtxoMinLovelace } from '../../utils/lucid-utils';
import {
  parseStableswapOrderDatumOrThrow,
  serialiseStableswapOrderDatum,
  serialiseStableswapOrderRedeemer,
  StableswapOrderDatum,
} from './types-new';
import {
  AssetClass,
  addressFromBech32,
  addressToBech32,
  getInlineDatumOrThrow,
  matchSingle,
  mkAssetsOf,
  mkLovelacesOf,
  lovelacesAmt,
  isSameOutRef,
  assetClassValueOf,
} from '@3rd-eye-labs/cardano-offchain-common';
import {
  parseStableswapPoolDatumOrThrow,
  serialiseCdpRedeemer,
  serialiseStableswapPoolDatum,
  StableswapPoolContent,
} from '../cdp/types-new';
import { calculateFeeFromRatio } from '../../utils/indigo-helpers';
import { array as A, function as F } from 'fp-ts';
import { isEmpty } from 'fp-ts/lib/Array';
import { BASE_MAX_EXECUTION_FEE, createDestinationDatum } from './helpers';
import * as Core from '@evolution-sdk/evolution';
import { treasuryFeeTx } from '../treasury/transactions';
import {
  Rational,
  rationalDiv,
  rationalFloor,
  rationalFromInt,
  rationalMul,
} from '../../types/rational';

type StableswapInfo = {
  suppliedCollateralAsset: bigint;
  suppliedIasset: bigint;
  owedCollateralAsset: bigint;
  owedIasset: bigint;
  mintingFee: bigint;
  redemptionFee: bigint;
};

type StableswapOrderInfo = {
  utxo: UTxO;
  datum: StableswapOrderDatum;
  swapInfo: StableswapInfo;
};

export async function createStableswapOrder(
  iasset: string,
  collateralAsset: AssetClass,
  amount: bigint,
  minting: boolean,
  poolDatum: StableswapPoolContent,
  params: SystemParams,
  lucid: LucidEvolution,
  destinationAddress?: Address,
  destinationInlineDatum?: Core.Data.Data,
  maxExecutionFee: bigint = BASE_MAX_EXECUTION_FEE,
  additionalLovelaces: bigint = 0n,
  maxFeeRatio?: Rational,
): Promise<TxBuilder> {
  const myAddress = await lucid.wallet().address();

  const pkh = paymentCredentialOf(myAddress);

  const datum: StableswapOrderDatum = {
    iasset: fromHex(fromText(iasset)),
    collateralAsset: collateralAsset,
    owner: fromHex(pkh.hash),
    destination: addressFromBech32(destinationAddress ?? myAddress),
    destinationInlineDatum: destinationInlineDatum ?? null,
    maxExecutionFee: maxExecutionFee,
    maxFeeRatio:
      maxFeeRatio ??
      (minting ? poolDatum.mintingFeeRatio : poolDatum.redemptionFeeRatio),
  };

  const assetsToSwap = minting
    ? mkAssetsOf(collateralAsset, amount)
    : mkAssetsOf(
        {
          currencySymbol: fromHex(
            params.stableswapParams.iassetSymbol.unCurrencySymbol,
          ),
          tokenName: fromHex(fromText(iasset)),
        },
        amount,
      );

  // This is an approximation of the amount of lovelace that will be needed to pay for the output.
  const expectedOutputLovelaces = estimateUtxoMinLovelace(
    lucid.config().protocolParameters!,
    myAddress,
    assetsToSwap,
    {
      kind: 'inline',
      value: createDestinationDatum(destinationInlineDatum ?? null, {
        txHash:
          '0000000000000000000000000000000000000000000000000000000000000000',
        outputIndex: 0,
      }),
    },
  );

  const roundedExpectedOutputLovelaces =
    (expectedOutputLovelaces / 1_000_000n + 1n) * 1_000_000n;

  return lucid.newTx().pay.ToContract(
    credentialToAddress(lucid.config().network!, {
      hash: params.validatorHashes.stableswapHash,
      type: 'Script',
    }),
    {
      kind: 'inline',
      value: serialiseStableswapOrderDatum(datum),
    },
    addAssets(
      assetsToSwap,
      mkLovelacesOf(
        roundedExpectedOutputLovelaces + maxExecutionFee + additionalLovelaces,
      ),
    ),
  );
}

export async function cancelStableswapOrder(
  stableswapOrderOref: OutRef,
  sysParams: SystemParams,
  lucid: LucidEvolution,
): Promise<TxBuilder> {
  const stableswapScriptRefUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.stableswapValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single Stableswap Ref Script UTXO'),
  );

  const stableswapOrderUtxo = matchSingle(
    await lucid.utxosByOutRef([stableswapOrderOref]),
    (_) => new Error('Expected a single Stableswap Order UTXO.'),
  );

  const stableswapOrderDatum = parseStableswapOrderDatumOrThrow(
    getInlineDatumOrThrow(stableswapOrderUtxo),
  );

  return lucid
    .newTx()
    .readFrom([stableswapScriptRefUtxo])
    .collectFrom(
      [stableswapOrderUtxo],
      serialiseStableswapOrderRedeemer('CancelStableswapOrder'),
    )
    .addSignerKey(toHex(stableswapOrderDatum.owner));
}

export async function batchProcessStableswapOrders(
  stableswapOrderOrefs: OutRef[],
  stableswapPoolOref: OutRef,
  treasuryOref: OutRef,
  sysParams: SystemParams,
  lucid: LucidEvolution,
): 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 stableswapPoolUtxo = matchSingle(
    await lucid.utxosByOutRef([stableswapPoolOref]),
    (_) => new Error('Expected a single cdp UTXO'),
  );

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

  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 = assetClassValueOf(orderUtxo.assets, iassetAc);
      const suppliedCollateralAsset = assetClassValueOf(
        orderUtxo.assets,
        collateralAc,
      );

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

      const isMinting = suppliedCollateralAsset > 0n;

      const isOneToOne =
        stableswapPoolDatum.collateralToIassetRatio.numerator ===
        stableswapPoolDatum.collateralToIassetRatio.denominator;

      if (isMinting) {
        // Mint order with one to one ratio case.
        if (isOneToOne) {
          const fee = calculateFeeFromRatio(
            stableswapPoolDatum.mintingFeeRatio,
            suppliedCollateralAsset,
          );

          return {
            utxo: orderUtxo,
            datum: orderDatum,
            swapInfo: {
              suppliedCollateralAsset: suppliedCollateralAsset,
              suppliedIasset: 0n,
              owedCollateralAsset: 0n,
              owedIasset: suppliedCollateralAsset - fee,
              mintingFee: fee,
              redemptionFee: 0n,
            },
          };
          // Mint order with any ratio case.
        } else {
          const iAssetConversion = rationalFloor(
            rationalDiv(
              rationalFromInt(suppliedCollateralAsset),
              stableswapPoolDatum.collateralToIassetRatio,
            ),
          );

          const attemptedNormalizedCollateral = rationalFloor(
            rationalMul(
              rationalFromInt(iAssetConversion),
              stableswapPoolDatum.collateralToIassetRatio,
            ),
          );

          const normalizedCollateralSupplied =
            rationalFloor(
              rationalDiv(
                rationalFromInt(attemptedNormalizedCollateral),
                stableswapPoolDatum.collateralToIassetRatio,
              ),
            ) != iAssetConversion
              ? suppliedCollateralAsset
              : attemptedNormalizedCollateral;

          const fee = calculateFeeFromRatio(
            stableswapPoolDatum.mintingFeeRatio,
            iAssetConversion,
          );

          return {
            utxo: orderUtxo,
            datum: orderDatum,
            swapInfo: {
              suppliedCollateralAsset: normalizedCollateralSupplied,
              suppliedIasset: 0n,
              owedCollateralAsset: 0n,
              owedIasset: iAssetConversion - fee,
              mintingFee: fee,
              redemptionFee: 0n,
            },
          };
        }
        // Redeem order case
      } else {
        const fee = calculateFeeFromRatio(
          stableswapPoolDatum.redemptionFeeRatio,
          suppliedIasset,
        );

        const effectiveSuppliedIasset = suppliedIasset - fee;

        // Redeem order with one to one ratio case
        if (isOneToOne) {
          return {
            utxo: orderUtxo,
            datum: orderDatum,
            swapInfo: {
              suppliedCollateralAsset: 0n,
              suppliedIasset: effectiveSuppliedIasset,
              owedCollateralAsset: effectiveSuppliedIasset,
              owedIasset: 0n,
              mintingFee: 0n,
              redemptionFee: fee,
            },
          };
          // Redeem order with any ratio case
        } else {
          const collateralConversion = rationalFloor(
            rationalMul(
              rationalFromInt(effectiveSuppliedIasset),
              stableswapPoolDatum.collateralToIassetRatio,
            ),
          );

          const attemptedNormalizedEffectiveIasset = rationalFloor(
            rationalDiv(
              rationalFromInt(collateralConversion),
              stableswapPoolDatum.collateralToIassetRatio,
            ),
          );

          const normalizedEffectiveIasset =
            rationalFloor(
              rationalMul(
                rationalFromInt(attemptedNormalizedEffectiveIasset),
                stableswapPoolDatum.collateralToIassetRatio,
              ),
            ) != collateralConversion
              ? effectiveSuppliedIasset
              : attemptedNormalizedEffectiveIasset;

          return {
            utxo: orderUtxo,
            datum: orderDatum,
            swapInfo: {
              suppliedCollateralAsset: 0n,
              suppliedIasset: normalizedEffectiveIasset,
              owedCollateralAsset: rationalFloor(
                rationalMul(
                  rationalFromInt(effectiveSuppliedIasset),
                  stableswapPoolDatum.collateralToIassetRatio,
                ),
              ),
              owedIasset: 0n,
              mintingFee: 0n,
              redemptionFee: fee,
            },
          };
        }
      }
    },
  );

  const totalSwapInfo = F.pipe(
    ordersInfo,
    A.reduce<StableswapOrderInfo, StableswapInfo>(
      {
        suppliedCollateralAsset: 0n,
        suppliedIasset: 0n,
        owedCollateralAsset: 0n,
        owedIasset: 0n,
        mintingFee: 0n,
        redemptionFee: 0n,
      },
      (acc, orderInfo) => {
        return {
          suppliedCollateralAsset:
            acc.suppliedCollateralAsset +
            orderInfo.swapInfo.suppliedCollateralAsset,
          suppliedIasset:
            acc.suppliedIasset + orderInfo.swapInfo.suppliedIasset,
          owedCollateralAsset:
            acc.owedCollateralAsset + orderInfo.swapInfo.owedCollateralAsset,
          owedIasset: acc.owedIasset + orderInfo.swapInfo.owedIasset,
          mintingFee: acc.mintingFee + orderInfo.swapInfo.mintingFee,
          redemptionFee: acc.redemptionFee + orderInfo.swapInfo.redemptionFee,
        };
      },
    ),
  );

  const collateralAmtChangePool =
    totalSwapInfo.suppliedCollateralAsset - totalSwapInfo.owedCollateralAsset;

  const amountToMint =
    totalSwapInfo.owedIasset -
    totalSwapInfo.suppliedIasset +
    totalSwapInfo.mintingFee;

  const fee = totalSwapInfo.mintingFee + totalSwapInfo.redemptionFee;

  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,
    )
    // This has to be added as otherwise there is the following error:
    // TxBuilderError: { Complete: RedeemerBuilder: Coin selection had to be updated
    // after building redeemers, possibly leading to incorrect indices. Try setting
    // a minimum fee of 1761019 lovelaces. }
    // Trying to set it as low as possible to reduce costs.
    .setMinFee(stableswapOrderOrefs.length > 1 ? 1_498_875n : 1_030_000n);

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

  F.pipe(
    ordersInfo,
    A.reduce<StableswapOrderInfo, TxBuilder>(tx, (acc, orderInfo) => {
      return acc
        .collectFrom(
          [orderInfo.utxo],
          isSameOutRef(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) -
                orderInfo.datum.maxExecutionFee,
            ),
            orderInfo.swapInfo.owedIasset > 0
              ? mkAssetsOf(iassetAc, orderInfo.swapInfo.owedIasset)
              : mkAssetsOf(
                  collateralAc,
                  orderInfo.swapInfo.owedCollateralAsset,
                ),
          ),
        );
    }),
  );
  if (fee > 0) {
    await treasuryFeeTx(
      iassetAc,
      fee,
      0n,
      lucid,
      sysParams,
      tx,
      stableswapPoolOref,
      treasuryOref,
    );
  }
  return tx;
}

export async function updateStableswapPoolFees(
  stableswapPoolOutRef: OutRef,
  stableswapFeeOutRef: OutRef,
  newMintingFeeRatio: Rational | null,
  newRedemptionFeeRatio: Rational | null,
  sysParams: SystemParams,
  lucid: LucidEvolution,
): 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 stableswapPool = matchSingle(
    await lucid.utxosByOutRef([stableswapPoolOutRef]),
    (_) => new Error('Expected a single Stableswap Pool UTXO.'),
  );

  const stableswapFeeUtxo = matchSingle(
    await lucid.utxosByOutRef([stableswapFeeOutRef]),
    (_) => new Error('Expected a single Stableswap Fee UTXO.'),
  );

  const stableswapPoolDatum = parseStableswapPoolDatumOrThrow(
    getInlineDatumOrThrow(stableswapPool),
  );
  const newStableswapPoolDatum = {
    ...stableswapPoolDatum,
    mintingFeeRatio: newMintingFeeRatio ?? stableswapPoolDatum.mintingFeeRatio,
    redemptionFeeRatio:
      newRedemptionFeeRatio ?? stableswapPoolDatum.redemptionFeeRatio,
  };

  if (!stableswapPoolDatum.feeManager) {
    throw new Error('Stableswap pool fee manager is not set');
  }

  return lucid
    .newTx()
    .readFrom([stableswapScriptRefUtxo, cdpScriptRefUtxo])
    .collectFrom([stableswapPool], {
      kind: 'selected',
      makeRedeemer: (inputIndices) =>
        serialiseCdpRedeemer({
          Stableswap: {
            forwardingInputIndex: inputIndices[0],
          },
        }),
      inputs: [stableswapFeeUtxo],
    })
    .collectFrom(
      [stableswapFeeUtxo],
      serialiseStableswapOrderRedeemer('UpdateFees'),
    )
    .pay.ToContract(
      stableswapPool.address,
      {
        kind: 'inline',
        value: serialiseStableswapPoolDatum(newStableswapPoolDatum),
      },
      stableswapPool.assets,
    )
    .addSignerKey(toHex(stableswapPoolDatum.feeManager))
    .setMinFee(1038402n);
}
