import {
  fromText,
  LucidEvolution,
  TxBuilder,
  Data,
  UTxO,
  fromHex,
  toHex,
  addAssets,
  OutRef,
  slotToUnixTime,
  credentialToAddress,
} from '@lucid-evolution/lucid';
import {
  fromSysParamsCredential,
  fromSystemParamsAsset,
  fromSystemParamsScriptRef,
  SystemParams,
} from '../../types/system-params';
import { addrDetails, getInlineDatumOrThrow } from '../../utils/lucid-utils';
import {
  BASE_MAX_TX_FEE,
  createProcessRequestAccountRedeemer,
  findRelevantE2s2sIdxs,
  initSpState,
  liquidationHelper,
  partitionEpochToScaleToSums,
  updateAccount,
  updatePoolStateWhenWithdrawalFee,
} from './helpers';
import { calculateFeeFromRatio } from '../../utils/indigo-helpers';
import {
  AccountAction,
  AccountContent,
  fromSPInteger,
  mkSPInteger,
  parseAccountDatumOrThrow,
  parseStabilityPoolDatumOrThrow,
  serialiseActionReturnDatum,
  serialiseStabilityPoolDatum,
  serialiseStabilityPoolRedeemer,
  spAdd,
  spSub,
  spZeroNegatives,
} from './types-new';
import {
  adaAssetClass,
  addressFromBech32,
  addressToBech32,
  AssetClass,
  assetClassValueOf,
  estimateUtxoMinLovelace,
  lovelacesAmt,
  matchSingle,
  mkAssetsOf,
  mkLovelacesOf,
  negateAssets,
} from '@3rd-eye-labs/cardano-offchain-common';
import { parseIAssetDatumOrThrow } from '../iasset/types';
import { match, P } from 'ts-pattern';
import { bigintMax, zeroNegatives } from '../../utils/bigint-utils';

export async function requestSpAccountCreation(
  assetAscii: string,
  amount: bigint,
  sysParams: SystemParams,
  lucid: LucidEvolution,
): Promise<TxBuilder> {
  const [pkh, _skh] = await addrDetails(lucid);

  const iasset = fromHex(fromText(assetAscii));
  const iassetAssetClass = {
    currencySymbol: fromHex(
      sysParams.stabilityPoolParams.assetSymbol.unCurrencySymbol,
    ),
    tokenName: iasset,
  };

  const datum: AccountContent = {
    owner: fromHex(pkh.hash),
    iasset: iasset,
    state: { ...initSpState, depositVal: mkSPInteger(amount) },
    assetSums: [],
    request: 'Create',
    lastRequestProcessingTime: 0n,
  };

  return lucid
    .newTx()
    .pay.ToContract(
      credentialToAddress(
        lucid.config().network!,
        {
          hash: sysParams.validatorHashes.stabilityPoolHash,
          type: 'Script',
        },
        sysParams.stabilityPoolParams.stakeCredential != null
          ? fromSysParamsCredential(
              sysParams.stabilityPoolParams.stakeCredential,
            )
          : undefined,
      ),
      {
        kind: 'inline',
        value: serialiseStabilityPoolDatum({ Account: datum }),
      },
      addAssets(
        mkAssetsOf(iassetAssetClass, amount),
        mkLovelacesOf(
          // TODO: Calculate a more accurate amount to just cover costs.
          // This should cover the account creation fee,
          // the minimum ADA for the account UTxO and the transaction fee.
          BigInt(sysParams.stabilityPoolParams.accountCreateFeeLovelaces) +
            3_000_000n,
        ),
      ),
    )
    .addSignerKey(pkh.hash);
}

export async function requestSpAccountAdjustment(
  amount: bigint,
  accountUtxo: UTxO,
  sysParams: SystemParams,
  lucid: LucidEvolution,
): Promise<TxBuilder> {
  const myAddress = await lucid.wallet().address();

  const stabilityPoolScriptRef = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.stabilityPoolValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single stability pool Ref Script UTXO'),
  );

  const oldAccountDatum: AccountContent = parseAccountDatumOrThrow(
    getInlineDatumOrThrow(accountUtxo),
  );

  const newAccountDatum: AccountContent = {
    ...oldAccountDatum,
    request: {
      Adjust: {
        amount: amount,
        outputAddress: addressFromBech32(myAddress),
      },
    },
  };

  return lucid
    .newTx()
    .readFrom([stabilityPoolScriptRef])
    .collectFrom(
      [accountUtxo],
      serialiseStabilityPoolRedeemer({
        RequestAction: {
          Adjust: {
            amount: amount,
            outputAddress: addressFromBech32(myAddress),
          },
        },
      }),
    )
    .pay.ToContract(
      accountUtxo.address,
      {
        kind: 'inline',
        value: serialiseStabilityPoolDatum({
          Account: newAccountDatum,
        }),
      },
      addAssets(
        mkLovelacesOf(
          // TODO: Calculate a more accurate amount to just cover costs.
          // This should cover the minimum ADA for 2 UTxOs (account and
          // rewards withdrawal) and the transaction fee.
          5_000_000n,
        ),
        mkAssetsOf(
          fromSystemParamsAsset(sysParams.stabilityPoolParams.accountToken),
          1n,
        ),
        amount > 0n
          ? mkAssetsOf(
              {
                currencySymbol: fromHex(
                  sysParams.stabilityPoolParams.assetSymbol.unCurrencySymbol,
                ),
                tokenName: oldAccountDatum.iasset,
              },
              amount,
            )
          : {},
      ),
    )
    .addSignerKey(toHex(oldAccountDatum.owner));
}

export async function requestSpAccountClosure(
  accountUtxo: UTxO,
  sysParams: SystemParams,
  lucid: LucidEvolution,
  maxTxFee: bigint = BASE_MAX_TX_FEE,
): Promise<TxBuilder> {
  const myAddress = await lucid.wallet().address();

  const stabilityPoolScriptRef = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.stabilityPoolValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single stability pool Ref Script UTXO'),
  );

  const request: AccountAction = {
    Close: {
      outputAddress: addressFromBech32(myAddress),
      maxTxFee: maxTxFee,
    },
  };
  const oldAccountDatum: AccountContent = parseAccountDatumOrThrow(
    getInlineDatumOrThrow(accountUtxo),
  );
  const newAccountDatum: AccountContent = {
    ...oldAccountDatum,
    request: request,
  };

  return lucid
    .newTx()
    .readFrom([stabilityPoolScriptRef])
    .collectFrom(
      [accountUtxo],
      serialiseStabilityPoolRedeemer({ RequestAction: request }),
    )
    .pay.ToContract(
      accountUtxo.address,
      {
        kind: 'inline',
        value: serialiseStabilityPoolDatum({ Account: newAccountDatum }),
      },
      addAssets(
        mkLovelacesOf(
          // TODO: Calculate a more accurate amount to just cover costs.
          // This should cover the minimum ADA for the rewards UTxO and the transaction fee.
          3_000_000n,
        ),
        mkAssetsOf(
          fromSystemParamsAsset(sysParams.stabilityPoolParams.accountToken),
          1n,
        ),
      ),
    )
    .addSignerKey(toHex(oldAccountDatum.owner));
}

async function processSpRequestAuxiliary(
  stabilityPoolUtxo: UTxO,
  accountUtxo: UTxO,
  iAssetUtxo: UTxO,
  /**
   * For performance provide only the ones related to the pool's iAsset.
   */
  allE2s2sSnapshotOrefs: OutRef[],
  sysParams: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
  txFee: bigint,
): Promise<TxBuilder> {
  const network = lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, currentSlot));

  const stabilityPoolScriptRef = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.stabilityPoolValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single stability pool Ref Script UTXO'),
  );

  const accountDatum = parseAccountDatumOrThrow(
    getInlineDatumOrThrow(accountUtxo),
  );

  const stabilityPoolDatum = parseStabilityPoolDatumOrThrow(
    getInlineDatumOrThrow(stabilityPoolUtxo),
  );

  const baseRefInputs = [iAssetUtxo, stabilityPoolScriptRef];

  const validFrom = slotToUnixTime(network, currentSlot - 1);
  const tx = lucid
    .newTx()
    .validFrom(validFrom)
    .validTo(validFrom + sysParams.stabilityPoolParams.accountProcessingBiasMs)
    .readFrom(baseRefInputs)
    .collectFrom([stabilityPoolUtxo], {
      kind: 'selected',
      inputs: [stabilityPoolUtxo, accountUtxo],
      makeRedeemer: (indices) =>
        serialiseStabilityPoolRedeemer({
          ProcessRequestPool: {
            poolInputIdx: indices[0],
            accountInputIdx: indices[1],
          },
        }),
    });
  if (!accountDatum.request) throw new Error('Account Request is null');

  const iassetAssetClass: AssetClass = {
    currencySymbol: fromHex(
      sysParams.stabilityPoolParams.assetSymbol.unCurrencySymbol,
    ),
    tokenName: stabilityPoolDatum.iasset,
  };

  await match(accountDatum.request)
    .with('Create', async (_) => {
      const accountTokenScriptRef = matchSingle(
        await lucid.utxosByOutRef([
          fromSystemParamsScriptRef(
            sysParams.scriptReferences.authTokenPolicies.accountTokenRef,
          ),
        ]),
        (_) =>
          new Error('Expected a single cdp auth token policy Ref Script UTXO'),
      );

      const requestDepositAmt = assetClassValueOf(
        accountUtxo.assets,
        iassetAssetClass,
      );

      const poolAddr = credentialToAddress(
        lucid.config().network!,
        {
          hash: sysParams.validatorHashes.stabilityPoolHash,
          type: 'Script',
        },
        sysParams.stabilityPoolParams.stakeCredential != null
          ? fromSysParamsCredential(
              sysParams.stabilityPoolParams.stakeCredential,
            )
          : undefined,
      );

      const accountOutputAdaAmt =
        lovelacesAmt(accountUtxo.assets) -
        BigInt(sysParams.stabilityPoolParams.accountCreateFeeLovelaces) -
        txFee;

      const accountTokenVal = mkAssetsOf(
        fromSystemParamsAsset(sysParams.stabilityPoolParams.accountToken),
        1n,
      );

      const accountOutputDatum = serialiseStabilityPoolDatum({
        Account: {
          owner: accountDatum.owner,
          iasset: stabilityPoolDatum.iasset,
          state: {
            ...stabilityPoolDatum.state,
            depositVal: mkSPInteger(requestDepositAmt),
          },
          assetSums: stabilityPoolDatum.assetStates.map(([key, val]) => [
            key,
            val.currentSumVal,
          ]),
          request: null,
          lastRequestProcessingTime: currentTime,
        },
      });

      const accountOutMinUtxoLovelace = estimateUtxoMinLovelace(
        lucid.config().protocolParameters!,
        poolAddr,
        accountTokenVal,
        { InlineDatum: { datum: accountOutputDatum } },
      );

      // This is to prevent spending ADA from the wallet submitting the request processing Tx.
      if (accountOutputAdaAmt < accountOutMinUtxoLovelace) {
        throw new Error("Request doesn't have enough ADA to be processed.");
      }

      tx.readFrom([accountTokenScriptRef])
        .collectFrom([accountUtxo], {
          kind: 'selected',
          inputs: [stabilityPoolUtxo, accountUtxo],
          makeRedeemer: (indices) =>
            serialiseStabilityPoolRedeemer({
              ProcessRequestAccount: {
                poolInputIdx: indices[0],
                accountInputIdx: indices[1],
                e2s2sIdxs: [],
                currentTime: currentTime,
              },
            }),
        })
        .mintAssets(accountTokenVal, Data.void())
        .pay.ToContract(
          poolAddr,
          {
            kind: 'inline',
            value: serialiseStabilityPoolDatum({
              StabilityPool: liquidationHelper(
                {
                  ...stabilityPoolDatum,
                  state: {
                    ...stabilityPoolDatum.state,
                    depositVal: spAdd(
                      stabilityPoolDatum.state.depositVal,
                      mkSPInteger(requestDepositAmt),
                    ),
                  },
                },
                adaAssetClass,
                0n,
                BigInt(sysParams.stabilityPoolParams.accountCreateFeeLovelaces),
              ),
            }),
          },
          addAssets(
            stabilityPoolUtxo.assets,
            mkAssetsOf(iassetAssetClass, requestDepositAmt),
            mkLovelacesOf(
              BigInt(sysParams.stabilityPoolParams.accountCreateFeeLovelaces),
            ),
          ),
        )
        .pay.ToContract(
          poolAddr,
          {
            kind: 'inline',
            value: accountOutputDatum,
          },
          addAssets(accountTokenVal, mkLovelacesOf(accountOutputAdaAmt)),
        )
        .setMinFee(txFee);
    })
    .with({ Adjust: P.select() }, async (adjustContent) => {
      const iassetDatum = parseIAssetDatumOrThrow(
        getInlineDatumOrThrow(iAssetUtxo),
      );

      const accountDepositChange = adjustContent.amount;
      const outputAddress = addressToBech32(
        adjustContent.outputAddress,
        lucid.config().network!,
      );

      const e2s2sIdxs = await findRelevantE2s2sIdxs(
        lucid,
        stabilityPoolDatum,
        accountDatum.state,
        allE2s2sSnapshotOrefs,
      );

      const { updatedAccountContent, reward } = updateAccount(
        stabilityPoolDatum,
        accountDatum,
        e2s2sIdxs,
      );

      const isDepositOrRewardWithdrawal = accountDepositChange >= 0;

      const accountBalanceChange = isDepositOrRewardWithdrawal
        ? accountDepositChange
        : bigintMax(
            accountDepositChange,
            -fromSPInteger(updatedAccountContent.state.depositVal),
          );

      const newPoolDepositExcludingFee = spZeroNegatives(
        spAdd(
          stabilityPoolDatum.state.depositVal,
          mkSPInteger(accountBalanceChange),
        ),
      );

      const withdrawalFeeAmt =
        isDepositOrRewardWithdrawal || newPoolDepositExcludingFee.value === 0n
          ? 0n
          : calculateFeeFromRatio(
              iassetDatum.stabilityPoolWithdrawalFeeRatio,
              -accountBalanceChange,
            );

      const newPoolState = updatePoolStateWhenWithdrawalFee(withdrawalFeeAmt, {
        ...stabilityPoolDatum.state,
        depositVal: newPoolDepositExcludingFee,
      });

      const { e2s2sRefInputs, mkProcessRequestAccountRedeemerContent } =
        createProcessRequestAccountRedeemer(
          e2s2sIdxs,
          baseRefInputs,
          currentTime,
        );

      if (e2s2sRefInputs.length > 0) {
        tx.readFrom(e2s2sRefInputs);
      }

      tx.collectFrom([accountUtxo], {
        kind: 'selected',
        inputs: [stabilityPoolUtxo, accountUtxo],
        makeRedeemer: (indices) => {
          return serialiseStabilityPoolRedeemer({
            ProcessRequestAccount: mkProcessRequestAccountRedeemerContent(
              indices[0],
              indices[1],
            ),
          });
        },
      }).pay.ToContract(
        stabilityPoolUtxo.address,
        {
          kind: 'inline',
          value: serialiseStabilityPoolDatum({
            StabilityPool: {
              ...stabilityPoolDatum,
              state: newPoolState,
            },
          }),
        },
        addAssets(
          stabilityPoolUtxo.assets,
          negateAssets(reward),
          mkAssetsOf(iassetAssetClass, accountBalanceChange + withdrawalFeeAmt),
        ),
      );

      const theoreticalOwnerOutputVal = addAssets(
        reward,
        isDepositOrRewardWithdrawal
          ? {}
          : mkAssetsOf(
              iassetAssetClass,
              -accountBalanceChange - withdrawalFeeAmt,
            ),
      );
      const ownerOutputDat = serialiseActionReturnDatum({
        IndigoStabilityPoolAccountAdjustment: {
          txHash: fromHex(accountUtxo.txHash),
          outputIndex: BigInt(accountUtxo.outputIndex),
        },
      });

      const ownerOutputMinUtxoLovelace = estimateUtxoMinLovelace(
        lucid.config().protocolParameters!,
        outputAddress,
        theoreticalOwnerOutputVal,
        { InlineDatum: { datum: ownerOutputDat } },
      );

      // Add extra lovelaces only when needed to reach minUTxo lovelaces.
      const extraOwnerOutputLovelacesAmt =
        lovelacesAmt(theoreticalOwnerOutputVal) >= ownerOutputMinUtxoLovelace
          ? 0n
          : ownerOutputMinUtxoLovelace -
            lovelacesAmt(theoreticalOwnerOutputVal);

      const actualOwnerOutputVal = addAssets(
        theoreticalOwnerOutputVal,
        mkLovelacesOf(extraOwnerOutputLovelacesAmt),
      );

      const accountOutputDat = serialiseStabilityPoolDatum({
        Account: {
          ...updatedAccountContent,
          state: {
            ...updatedAccountContent.state,
            depositVal: spAdd(
              updatedAccountContent.state.depositVal,
              mkSPInteger(accountBalanceChange),
            ),
          },
          request: null,
          lastRequestProcessingTime: currentTime,
        },
      });

      const accountOutputValWithoutAda = mkAssetsOf(
        fromSystemParamsAsset(sysParams.stabilityPoolParams.accountToken),
        1n,
      );

      const accountOutMinUtxoLovelace = estimateUtxoMinLovelace(
        lucid.config().protocolParameters!,
        stabilityPoolUtxo.address,
        accountOutputValWithoutAda,
        { InlineDatum: { datum: accountOutputDat } },
      );

      const accountOutputAdaAmt =
        lovelacesAmt(accountUtxo.assets) - extraOwnerOutputLovelacesAmt - txFee;

      // This is to prevent spending ADA from the wallet submitting the request processing Tx.
      if (accountOutputAdaAmt < accountOutMinUtxoLovelace) {
        throw new Error("Account doesn't have enough ADA to be processed.");
      }

      tx.pay
        .ToContract(
          stabilityPoolUtxo.address,
          {
            kind: 'inline',
            value: accountOutputDat,
          },
          addAssets(
            accountOutputValWithoutAda,
            mkLovelacesOf(accountOutputAdaAmt),
          ),
        )
        .pay.ToAddressWithData(
          outputAddress,
          {
            kind: 'inline',
            value: ownerOutputDat,
          },
          actualOwnerOutputVal,
        )
        .setMinFee(txFee);
    })
    .with({ Close: P.select() }, async (closeContent) => {
      if (txFee > closeContent.maxTxFee) {
        throw new Error("Account doesn't allow current transaction fee.");
      }

      const accountTokenScriptRef = matchSingle(
        await lucid.utxosByOutRef([
          fromSystemParamsScriptRef(
            sysParams.scriptReferences.authTokenPolicies.accountTokenRef,
          ),
        ]),
        (_) =>
          new Error('Expected a single cdp auth token policy Ref Script UTXO'),
      );

      const iassetDatum = parseIAssetDatumOrThrow(
        getInlineDatumOrThrow(iAssetUtxo),
      );

      const outputAddress = addressToBech32(
        closeContent.outputAddress,
        lucid.config().network!,
      );

      const e2s2sIdxs = await findRelevantE2s2sIdxs(
        lucid,
        stabilityPoolDatum,
        accountDatum.state,
        allE2s2sSnapshotOrefs,
      );

      const { updatedAccountContent, reward } = updateAccount(
        stabilityPoolDatum,
        accountDatum,
        e2s2sIdxs,
      );

      const newPoolDepositExcludingFee = spZeroNegatives(
        spSub(
          stabilityPoolDatum.state.depositVal,
          updatedAccountContent.state.depositVal,
        ),
      );

      const withdrawnAmt = zeroNegatives(
        fromSPInteger(updatedAccountContent.state.depositVal),
      );

      const withdrawalFeeAmt =
        newPoolDepositExcludingFee.value === 0n
          ? 0n
          : calculateFeeFromRatio(
              iassetDatum.stabilityPoolWithdrawalFeeRatio,
              withdrawnAmt,
            );

      const newPoolState = updatePoolStateWhenWithdrawalFee(withdrawalFeeAmt, {
        ...stabilityPoolDatum.state,
        depositVal: newPoolDepositExcludingFee,
      });

      const { e2s2sRefInputs, mkProcessRequestAccountRedeemerContent } =
        createProcessRequestAccountRedeemer(
          e2s2sIdxs,
          [...baseRefInputs, accountTokenScriptRef],
          currentTime,
        );

      if (e2s2sRefInputs.length > 0) {
        tx.readFrom(e2s2sRefInputs);
      }

      tx.readFrom([accountTokenScriptRef])
        .collectFrom([accountUtxo], {
          kind: 'selected',
          inputs: [stabilityPoolUtxo, accountUtxo],
          makeRedeemer: (indices) => {
            return serialiseStabilityPoolRedeemer({
              ProcessRequestAccount: mkProcessRequestAccountRedeemerContent(
                indices[0],
                indices[1],
              ),
            });
          },
        })
        .mintAssets(
          mkAssetsOf(
            fromSystemParamsAsset(sysParams.stabilityPoolParams.accountToken),
            -1n,
          ),
          Data.void(),
        )
        .pay.ToContract(
          stabilityPoolUtxo.address,
          {
            kind: 'inline',
            value: serialiseStabilityPoolDatum({
              StabilityPool: {
                ...stabilityPoolDatum,
                state: newPoolState,
              },
            }),
          },
          addAssets(
            stabilityPoolUtxo.assets,
            negateAssets(reward),
            mkAssetsOf(iassetAssetClass, -withdrawnAmt + withdrawalFeeAmt),
          ),
        )
        .pay.ToAddressWithData(
          outputAddress,
          {
            kind: 'inline',
            value: serialiseActionReturnDatum({
              IndigoStabilityPoolAccountClosure: {
                txHash: fromHex(accountUtxo.txHash),
                outputIndex: BigInt(accountUtxo.outputIndex),
              },
            }),
          },
          addAssets(
            mkLovelacesOf(lovelacesAmt(accountUtxo.assets) - txFee),
            reward,
            mkAssetsOf(iassetAssetClass, withdrawnAmt - withdrawalFeeAmt),
          ),
        )
        .setMinFee(txFee);
    })
    .exhaustive();

  return tx;
}

export async function processSpRequest(
  stabilityPoolUtxo: UTxO,
  accountUtxo: UTxO,
  iAssetUtxo: UTxO,
  /**
   * For performance provide only the ones related to the pool's iAsset.
   */
  allE2s2sSnapshotOrefs: OutRef[],
  sysParams: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
): Promise<TxBuilder> {
  const draftTx = processSpRequestAuxiliary(
    stabilityPoolUtxo,
    accountUtxo,
    iAssetUtxo,
    allE2s2sSnapshotOrefs,
    sysParams,
    lucid,
    currentSlot,
    // Placeholder transation fee
    1n,
  );

  const fee = (await (await draftTx).complete()).toTransaction().body().fee();

  return processSpRequestAuxiliary(
    stabilityPoolUtxo,
    accountUtxo,
    iAssetUtxo,
    allE2s2sSnapshotOrefs,
    sysParams,
    lucid,
    currentSlot,
    fee,
  );
}

export async function createE2s2sSnapshots(
  stabilityPoolOref: OutRef,
  sysParams: SystemParams,
  lucid: LucidEvolution,
): Promise<TxBuilder> {
  const stabilityPoolRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.stabilityPoolValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single stability pool Ref Script UTXO'),
  );
  const snapshotE2s2sPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.authTokenPolicies
          .snapshotEpochToScaleToSumTokenRef,
      ),
    ]),
    (_) => new Error('Expected a single snapshot e2s2s policy Ref Script UTXO'),
  );

  const spUtxo = matchSingle(
    await lucid.utxosByOutRef([stabilityPoolOref]),
    (_) => new Error('Expected a single stability pool UTXO'),
  );
  const spDatum = parseStabilityPoolDatumOrThrow(getInlineDatumOrThrow(spUtxo));

  const [newSnapshotDatums, newAssetStates] =
    partitionEpochToScaleToSums(spDatum);

  if (newSnapshotDatums.length === 0) {
    throw new Error('There has to be a snapshot being created.');
  }

  const snapshotAc = fromSystemParamsAsset(
    sysParams.stabilityPoolParams.snapshotEpochToScaleToSumToken,
  );

  const tx = lucid
    .newTx()
    .readFrom([stabilityPoolRefScriptUtxo, snapshotE2s2sPolicyRefScriptUtxo])
    .collectFrom(
      [spUtxo],
      serialiseStabilityPoolRedeemer('RecordEpochToScaleToSum'),
    )
    .mintAssets(
      mkAssetsOf(snapshotAc, BigInt(newSnapshotDatums.length)),
      Data.void(),
    )
    .pay.ToContract(
      spUtxo.address,
      {
        kind: 'inline',
        value: serialiseStabilityPoolDatum({
          StabilityPool: { ...spDatum, assetStates: newAssetStates },
        }),
      },
      spUtxo.assets,
    );

  for (const newDatum of newSnapshotDatums) {
    tx.pay.ToContract(
      spUtxo.address,
      {
        kind: 'inline',
        value: serialiseStabilityPoolDatum({
          SnapshotEpochToScaleToSum: newDatum,
        }),
      },
      mkAssetsOf(snapshotAc, 1n),
    );
  }

  return tx;
}

export async function annulRequest(
  accountUtxo: UTxO,
  params: SystemParams,
  lucid: LucidEvolution,
): Promise<TxBuilder> {
  const stabilityPoolRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        params.scriptReferences.stabilityPoolValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single stability pool Ref Script UTXO'),
  );
  const oldAccountDatum: AccountContent = parseAccountDatumOrThrow(
    getInlineDatumOrThrow(accountUtxo),
  );

  const tx = lucid
    .newTx()
    .readFrom([stabilityPoolRefScriptUtxo])
    .collectFrom([accountUtxo], serialiseStabilityPoolRedeemer('AnnulRequest'))
    .addSignerKey(toHex(oldAccountDatum.owner));

  if (oldAccountDatum.request !== 'Create') {
    tx.pay.ToContract(
      accountUtxo.address,
      {
        kind: 'inline',
        value: serialiseStabilityPoolDatum({
          Account: {
            ...oldAccountDatum,
            request: null,
          },
        }),
      },
      mkAssetsOf(
        fromSystemParamsAsset(params.stabilityPoolParams.accountToken),
        1n,
      ),
    );
  }

  return tx;
}
