import { MeltQuoteState } from "@cashu/cashu-ts";
import type { NDKPaymentConfirmationLN } from "@nostr-dev-kit/ndk";
import { getBolt11Amount } from "../../../utils/ln.js";
import { consolidateMintTokens } from "../validate.js";
import { type WalletOperation, withProofReserve } from "../wallet/effect.js";
import { calculateFee } from "../wallet/fee.js";
import type { NDKCashuWallet } from "../wallet/index.js";

/**
 * Pay a Lightning Network invoice with a Cashu wallet.
 *
 * @param wallet - The Cashu wallet to use for the payment.
 * @param pr - The Lightning Network payment request (invoice) to pay.
 * @param { amount, unit } - The intended amount and unit to pay. -- Intended amount could be what we wanted to pay and the amount in the bolt11 might have a fee.
 * @returns A Promise that resolves to the payment preimage as a string if successful, or null if the payment fails.
 */
export async function payLn(
    wallet: NDKCashuWallet,
    pr: string,
    { amount, unit }: { amount?: number; unit?: string } = {},
): Promise<WalletOperation<NDKPaymentConfirmationLN> | null> {
    let invoiceAmount = getBolt11Amount(pr);
    if (!invoiceAmount) throw new Error("invoice amount is required");

    invoiceAmount = invoiceAmount / 1000; // msat

    if (amount && unit) {
        if (unit === "msat") {
            amount = amount / 1000;
        }
    }

    // we add three sats to the calculation as a guess for the fee
    const eligibleMints = wallet.getMintsWithBalance(invoiceAmount + 3);

    if (!eligibleMints.length) {
        return null;
    }

    for (const mint of eligibleMints) {
        try {
            const result = await executePayment(mint, pr, amount ?? invoiceAmount, wallet);
            if (result) {
                if (amount) {
                    result.fee = calculateFee(
                        amount,
                        result.proofsChange?.destroy ?? [],
                        result.proofsChange?.store ?? [],
                    );
                }
                return result;
            }
        } catch (error: any) {
            wallet.warn(`Failed to execute payment with min ${mint}: ${error}`);
        }
    }

    return null;
}

/**
 * Attempts to pay using a selected set of Cashu tokens.
 *
 * @param selection - The TokenSelection object containing the chosen proofs and quote for the payment.
 * @param pr - The Lightning Network payment request (invoice) to pay.
 * @param wallet - The NDKCashuPay wallet instance.
 * @param debug - The debug function for logging.
 * @returns A Promise that resolves to the payment preimage as a string if successful, or null if the payment fails.
 *
 * @throws Will throw an error if the payment fails due to network issues or other problems.
 *
 * This function performs the following steps:
 * 1. Creates a new CashuWallet instance for the specific mint.
 * 2. Attempts to pay the Lightning invoice using the selected proofs.
 * 3. If successful, it rolls over any change proofs.
 * 4. If the proofs are already spent, it rolls over the selection without change.
 * 5. Logs the process and any errors for debugging purposes.
 */
async function executePayment(
    mint: string,
    pr: string,
    amountWithoutFees: number,
    wallet: NDKCashuWallet,
): Promise<WalletOperation<NDKPaymentConfirmationLN> | null> {
    const cashuWallet = await wallet.getCashuWallet(mint);

    try {
        const meltQuote = await cashuWallet.createMeltQuote(pr);
        const amountToSend = meltQuote.amount + meltQuote.fee_reserve;

        const result = await withProofReserve<NDKPaymentConfirmationLN>(
            wallet,
            cashuWallet,
            mint,
            amountToSend,
            amountWithoutFees,
            async (proofsToUse, _allOurProofs) => {
                const meltResult = await cashuWallet.meltProofs(meltQuote, proofsToUse);

                if (meltResult.quote.state === MeltQuoteState.PAID) {
                    return {
                        result: {
                            preimage: meltResult.quote.payment_preimage ?? "",
                        },
                        change: meltResult.change,
                    };
                }

                return null;
            },
        );

        return result;
    } catch (e) {
        if (e instanceof Error) {
            if (e.message.match(/already spent/i)) {
                setTimeout(() => {
                    consolidateMintTokens(mint, wallet);
                }, 2500);
            } else {
                throw e;
            }
        }

        return null;
    }
}
