import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet';
import { u8 } from '@solana/buffer-layout';
import { ComputeBudgetProgram, MessageV0, VersionedTransaction, } from '@solana/web3.js';
import { COMPUTE_BUDGET_PROGRAM_ID } from '../constants';
import { awaitTransactionSignatureConfirmation } from '@blockworks-foundation/mangolana/lib/transactions';
import { tryStringify } from '../utils';
export async function sendTransaction(provider, ixs, alts, opts = {}) {
    const connection = provider.connection;
    const latestBlockhash = await fetchLatestBlockHash(provider, opts);
    const payer = provider.wallet;
    //
    // setComputeUnitLimit, hard code to a higher minimum, this is needed so that we dont fail simple UI interactions
    //
    // https://github.com/solana-labs/solana-web3.js/blob/master/packages/library-legacy/src/programs/compute-budget.ts#L202
    const computeUnitLimitIxFound = ixs.some((ix) => ix.programId.equals(COMPUTE_BUDGET_PROGRAM_ID) &&
        u8().decode(ix.data.subarray(0, 1)) == 2);
    if (!computeUnitLimitIxFound) {
        const totalUserIntendedIxs = ixs.filter((ix) => !ix.programId.equals(COMPUTE_BUDGET_PROGRAM_ID)).length;
        const requestCu = Math.min(totalUserIntendedIxs * 300_000, 1_600_000);
        ixs = [
            ComputeBudgetProgram.setComputeUnitLimit({
                units: requestCu,
            }),
            ...ixs,
        ];
    }
    //
    // setComputeUnitPrice
    //
    if (opts.prioritizationFee) {
        ixs = [createComputeBudgetIx(opts.prioritizationFee), ...ixs];
    }
    const message = MessageV0.compile({
        payerKey: provider.wallet.publicKey,
        instructions: ixs,
        recentBlockhash: latestBlockhash.blockhash,
        addressLookupTableAccounts: alts,
    });
    let vtx = new VersionedTransaction(message);
    if (opts?.additionalSigners?.length) {
        vtx.sign([...opts?.additionalSigners]);
    }
    if (typeof payer.signTransaction === 'function' &&
        !(payer instanceof NodeWallet || payer.constructor.name == 'NodeWallet')) {
        vtx = (await payer.signTransaction(vtx));
    }
    else {
        // Maybe this path is only correct for NodeWallet?
        vtx.sign([payer.payer]);
    }
    // if configured, send the transaction using multiple connections
    let signature;
    if (opts?.multipleConnections?.length ?? 0 > 0) {
        const allConnections = [connection, ...opts.multipleConnections];
        signature = await Promise.any(allConnections.map((c) => {
            return c.sendRawTransaction(vtx.serialize(), {
                skipPreflight: true, // mergedOpts.skipPreflight,
            });
        }));
    }
    else {
        signature = await connection.sendRawTransaction(vtx.serialize(), {
            skipPreflight: true, // mergedOpts.skipPreflight,
        });
    }
    if (opts.postSendTxCallback) {
        try {
            opts.postSendTxCallback({
                txid: signature,
                txSignatureBlockHash: latestBlockhash,
                instructions: vtx,
            });
        }
        catch (e) {
            console.warn(`postSendTxCallback error ${e}`);
        }
    }
    if (!opts.confirmInBackground) {
        return await confirmTransaction(connection, opts, latestBlockhash, signature);
    }
    else {
        confirmTransaction(connection, opts, latestBlockhash, signature);
        return { signature };
    }
}
const confirmTransaction = async (connection, opts = {}, latestBlockhash, signature) => {
    let status;
    const allConnections = [connection];
    if (opts.multipleConnections && opts.multipleConnections.length) {
        allConnections.push(...opts.multipleConnections);
    }
    const abortController = new AbortController();
    try {
        if (latestBlockhash.blockhash != null &&
            latestBlockhash.lastValidBlockHeight != null) {
            status = await Promise.any(allConnections.map((c) => awaitTransactionSignatureConfirmation({
                txid: signature,
                confirmLevel: 'processed',
                connection: c,
                timeoutStrategy: {
                    block: latestBlockhash,
                },
                abortSignal: abortController.signal,
            })));
        }
        else {
            status = await Promise.any(allConnections.map((c) => awaitTransactionSignatureConfirmation({
                txid: signature,
                confirmLevel: 'processed',
                connection: c,
                timeoutStrategy: {
                    timeout: 90,
                },
                abortSignal: abortController.signal,
            })));
        }
        abortController.abort();
        const signatureResult = status.value;
        if (signatureResult.err) {
            console.warn('Tx status: ', status);
            throw new MangoError({
                txid: signature,
                message: `${JSON.stringify(status)}`,
            });
        }
        if (opts.postTxConfirmationCallback) {
            try {
                opts.postTxConfirmationCallback({
                    txid: signature,
                    txSignatureBlockHash: latestBlockhash,
                });
            }
            catch (e) {
                console.warn(`postTxConfirmationCallback error ${e}`);
            }
        }
        return { signature, slot: status.context.slot, ...signatureResult };
    }
    catch (e) {
        abortController.abort();
        if (e instanceof AggregateError) {
            for (const individualError of e.errors) {
                const stringifiedError = tryStringify(individualError);
                throw new MangoError({
                    txid: signature,
                    message: `${stringifiedError
                        ? stringifiedError
                        : individualError
                            ? individualError
                            : 'Unknown error'}`,
                });
            }
        }
        if (isErrorWithSignatureResult(e)) {
            const stringifiedError = tryStringify(e?.value?.err);
            throw new MangoError({
                txid: signature,
                message: `${stringifiedError ? stringifiedError : e?.value?.err}`,
            });
        }
        const stringifiedError = tryStringify(e);
        throw new MangoError({
            txid: signature,
            message: `${stringifiedError ? stringifiedError : e}`,
        });
    }
};
export async function fetchLatestBlockHash(provider, opts = {}) {
    if (opts.latestBlockhash) {
        return opts.latestBlockhash;
    }
    const commitment = opts.preflightCommitment ??
        provider.opts.preflightCommitment ??
        'finalized';
    const blockhashRequest = await provider.connection.getLatestBlockhashAndContext(commitment);
    return {
        slot: blockhashRequest.context.slot,
        lastValidBlockHeight: blockhashRequest.value.lastValidBlockHeight,
        blockhash: blockhashRequest.value.blockhash,
    };
}
export const createComputeBudgetIx = (microLamports) => {
    const computeBudgetIx = ComputeBudgetProgram.setComputeUnitPrice({
        microLamports,
    });
    return computeBudgetIx;
};
export class MangoError extends Error {
    message;
    txid;
    constructor({ txid, message }) {
        super();
        this.message = message;
        this.txid = txid;
    }
}
function isErrorWithSignatureResult(err) {
    return err && typeof err.value !== 'undefined';
}
