import type { Proof } from "@cashu/cashu-ts";

import type { NDKCashuToken, NDKEvent } from "@nostr-dev-kit/ndk";
import createDebug from "debug";
import { EventEmitter } from "tseep";
import { NDKCashuQuote } from "./quote";
import type { NDKCashuWallet } from "./wallet/index.js";
import { createInTxEvent } from "./wallet/txs.js";

const d = createDebug("ndk-wallet:cashu:deposit");

function randomMint(wallet: NDKCashuWallet) {
    const mints = wallet.mints;
    const mint = mints[Math.floor(Math.random() * mints.length)];
    return mint;
}

export class NDKCashuDeposit extends EventEmitter<{
    success: (token: NDKCashuToken) => void;
    error: (error: string) => void;
}> {
    public mint: string;
    public amount: number;
    public quoteId: string | undefined;
    private wallet: NDKCashuWallet;
    public checkTimeout: NodeJS.Timeout | undefined;
    public checkIntervalLength = 2500;
    public finalized = false;

    private quoteEvent?: NDKEvent;

    constructor(wallet: NDKCashuWallet, amount: number, mint?: string) {
        super();
        this.wallet = wallet;
        this.mint = mint || randomMint(wallet);
        this.amount = amount;
    }

    static fromQuoteEvent(wallet: NDKCashuWallet, quote: NDKCashuQuote) {
        if (!quote.amount) throw new Error("quote has no amount");
        if (!quote.mint) throw new Error("quote has no mint");
        const deposit = new NDKCashuDeposit(wallet, quote.amount, quote.mint);

        deposit.quoteId = quote.quoteId;
        return deposit;
    }

    /**
     * Creates a quote ID and start monitoring for payment.
     *
     * Once a payment is received, the deposit will emit a "success" event.
     *
     * @param pollTime - time in milliseconds between checks
     * @returns
     */
    async start(pollTime = 2500) {
        const cashuWallet = await this.wallet.getCashuWallet(this.mint);
        const quote = await cashuWallet.createMintQuote(this.amount);
        d("created quote %s for %d %s", quote.quote, this.amount, this.mint);

        this.quoteId = quote.quote;

        // register deposit with monitor
        this.wallet.depositMonitor.addDeposit(this);

        setTimeout(this.check.bind(this, pollTime), pollTime);
        this.createQuoteEvent(quote.quote, quote.request).then((event) => (this.quoteEvent = event));

        return quote.request;
    }

    /**
     * This generates a 7374 event containing the quote ID
     * with an optional expiration set to the bolt11 expiry (if there is one)
     */
    private async createQuoteEvent(quoteId: string, bolt11: string) {
        const { ndk } = this.wallet;

        const quoteEvent = new NDKCashuQuote(ndk);
        quoteEvent.quoteId = quoteId;
        quoteEvent.mint = this.mint;
        quoteEvent.amount = this.amount;
        quoteEvent.wallet = this.wallet;
        quoteEvent.invoice = bolt11;

        try {
            await quoteEvent.save();
            d("saved quote on event %s", quoteEvent.rawEvent());
        } catch (e: any) {
            d("error saving quote on event %s", e.relayErrors);
        }

        return quoteEvent;
    }

    private async runCheck() {
        if (!this.finalized) await this.finalize();
        if (!this.finalized) this.delayCheck();
    }

    private delayCheck() {
        setTimeout(() => {
            this.runCheck();
            this.checkIntervalLength += 500;
        }, this.checkIntervalLength);
    }

    /**
     * Check if the deposit has been finalized.
     * @param timeout A timeout in milliseconds to wait before giving up.
     */
    async check(timeout?: number) {
        this.runCheck();

        if (timeout) {
            setTimeout(() => {
                clearTimeout(this.checkTimeout);
            }, timeout);
        }
    }

    async finalize() {
        if (!this.quoteId) throw new Error("No quoteId set.");

        let proofs: Proof[];

        try {
            d("Checking for minting status of %s", this.quoteId);
            const cashuWallet = await this.wallet.getCashuWallet(this.mint);
            const proofsWeHave = await this.wallet.state.getProofs({ mint: this.mint });
            proofs = await cashuWallet.mintProofs(this.amount, this.quoteId, {
                proofsWeHave,
            });
            if (proofs.length === 0) return;
        } catch (e: any) {
            if (e.message.match(/not paid/i)) return;

            if (e.message.match(/already issued/i)) {
                d("Mint is saying the quote has already been issued, destroying quote event: %s", e.message);
                this.destroyQuoteEvent();
                this.finalized = true;
                return;
            }

            if (e.message.match(/rate limit/i)) {
                d("Mint seems to be rate limiting, lowering check interval");
                this.checkIntervalLength += 5000;
                return;
            }

            d(e.message);
            return;
        }

        try {
            this.finalized = true;

            const updateRes = await this.wallet.state.update(
                {
                    store: proofs,
                    mint: this.mint,
                },
                "Deposit",
            );

            const tokenEvent = updateRes.created;
            if (!tokenEvent) throw new Error("no token event created");

            createInTxEvent(
                this.wallet.ndk,
                proofs,
                this.mint,
                updateRes,
                { description: "Deposit" },
                this.wallet.relaySet,
            );

            this.emit("success", tokenEvent);

            // delete the quote event if it exists
            this.destroyQuoteEvent();
        } catch (e: any) {
            this.emit("error", e.message);
            console.error(e);
        }
    }

    private async destroyQuoteEvent() {
        if (!this.quoteEvent) return;
        const deleteEvent = await this.quoteEvent.delete(undefined, false);
        deleteEvent.publish(this.wallet.relaySet);
    }
}
