import {
  negateAssets,
  noAdaValue,
} from '@3rd-eye-labs/cardano-offchain-common';
import {
  addAssets,
  Assets,
  credentialToAddress,
  getAddressDetails,
  LucidEvolution,
  paymentCredentialOf,
  UTxO,
} from '@lucid-evolution/lucid';
import { array as A, function as F } from 'fp-ts';
import { zipWith } from 'fp-ts/lib/Array';
import { runAndAwaitTxBuilder } from '../test-helpers';

/**
 * In case passing in address with stake credential, the non stake credential address
 * is used as well for collecting inputs but cannot increase its value.
 */
export async function getValueChangeAtAddressAfterAction<T>(
  lucid: LucidEvolution,
  address: string,
  action: () => Promise<T>,
): Promise<[T, Assets]> {
  const hasStakeCred = getAddressDetails(address).stakeCredential != null;

  const valBefore = A.reduce<UTxO, Assets>({}, (acc, utxo) =>
    addAssets(acc, utxo.assets),
  )(await lucid.utxosAt(address));

  const [res, nonStakeCredValChange] = hasStakeCred
    ? await getValueChangeAtAddressAfterAction(
        lucid,
        credentialToAddress(
          lucid.config().network!,
          paymentCredentialOf(address),
        ),
        action,
      )
    : [await action(), {}];

  if (
    hasStakeCred &&
    !Object.entries(nonStakeCredValChange).every(([_, amt]) => amt <= 0n)
  ) {
    throw new Error("Can't add value to the address without stake credential");
  }

  const valAfter = A.reduce<UTxO, Assets>({}, (acc, utxo) =>
    addAssets(acc, utxo.assets),
  )(await lucid.utxosAt(address));

  return [
    res,
    addAssets(valAfter, negateAssets(valBefore), nonStakeCredValChange),
  ];
}

export async function getValueChangeAtAddressesAfterAction<T>(
  lucid: LucidEvolution,
  addresses: string[],
  action: () => Promise<T>,
): Promise<[T, Assets[]]> {
  const valsBefore = [];

  for (const address of addresses) {
    const utxos = await lucid.utxosAt(address);
    valsBefore.push(
      F.pipe(
        utxos,
        A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets)),
      ),
    );
  }

  const res = await action();

  const valsAfter = [];

  for (const address of addresses) {
    const utxos = await lucid.utxosAt(address);
    valsAfter.push(
      F.pipe(
        utxos,
        A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets)),
      ),
    );
  }

  return [
    res,
    zipWith(valsBefore, valsAfter, (valBefore, valAfter) =>
      addAssets(valAfter, negateAssets(valBefore)),
    ),
  ];
}

export async function getNewUtxosAtAddressAfterAction<T>(
  lucid: LucidEvolution,
  address: string,
  action: () => Promise<T>,
): Promise<[T, UTxO[]]> {
  const utxosBefore = await lucid.utxosAt(address);

  const res = await action();

  const utxosAfter = await lucid.utxosAt(address);

  return [
    res,
    utxosAfter.filter(
      (utxo) =>
        utxosBefore.filter(
          (oldUtxo) =>
            utxo.txHash === oldUtxo.txHash &&
            utxo.outputIndex === oldUtxo.outputIndex,
        ).length === 0,
    ),
  ];
}

export async function totalValueAtAddress(
  lucid: LucidEvolution,
  addr: string,
): Promise<Assets> {
  const utxos = await lucid.utxosAt(addr);

  return F.pipe(
    utxos,
    A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets)),
  );
}

export async function sendValueTo(
  destinationAddr: string,
  value: Assets,
  lucid: LucidEvolution,
): Promise<void> {
  await runAndAwaitTxBuilder(
    lucid,
    lucid.newTx().pay.ToAddress(destinationAddr, value),
  );
}

/**
 * For all the non-ADA assets in current wallet, create a separate UTXo with each of them.
 */
export async function fragmentWallet(lucid: LucidEvolution): Promise<void> {
  const currentAddr = await lucid.wallet().address();

  const totalVal = await totalValueAtAddress(lucid, currentAddr);

  const otherAssets = Object.keys(noAdaValue(totalVal));

  if (otherAssets.length === 0) return;

  const tx = lucid.newTx();

  for (const asset of otherAssets) {
    tx.pay.ToAddress(currentAddr, {
      [asset]: totalVal[asset],
    });
  }

  await runAndAwaitTxBuilder(lucid, tx);
}
