package nip60

import (
	"context"
	"fmt"
	"time"

	"fiatjaf.com/nostr/nip60/client"
	"github.com/elnosh/gonuts/cashu"
	"github.com/elnosh/gonuts/cashu/nuts/nut02"
	"github.com/elnosh/gonuts/cashu/nuts/nut04"
	"github.com/elnosh/gonuts/cashu/nuts/nut05"
	"github.com/elnosh/gonuts/cashu/nuts/nut10"
)

type lightningSwapStatus int

const (
	nothingCanBeDone = iota
	tryAnotherTargetMint
	storeTokenFromSourceMint
	manualActionRequired
)

// lightningMeltMint does the lightning dance of moving funds between mints
func lightningMeltMint(
	ctx context.Context,
	proofs cashu.Proofs,
	from string,
	fromKeysets []nut02.Keyset,
	to string,
) (cashu.Proofs, error, lightningSwapStatus) {
	// now we start the melt-mint process in multiple attempts
	invoicePct := uint64(99)
	proofsAmount := proofs.Amount()
	amount := proofsAmount * invoicePct / 100
	fee := uint64(calculateFee(proofs, fromKeysets))
	var meltQuote string
	var mintQuote string
	for range 10 {
		// request _mint_ quote to the 'to' mint -- this will generate an invoice
		mintResp, err := client.PostMintQuoteBolt11(ctx, to, nut04.PostMintQuoteBolt11Request{
			Amount: uint64(amount) - fee,
			Unit:   cashu.Sat.String(),
		})
		if err != nil {
			return nil, fmt.Errorf("error requesting mint quote from %s: %w", to, err), tryAnotherTargetMint
		}

		// request _melt_ quote from the 'from' mint
		// this melt will pay the invoice generated from the previous mint quote request
		meltResp, err := client.PostMeltQuoteBolt11(ctx, from, nut05.PostMeltQuoteBolt11Request{
			Request: mintResp.Request,
			Unit:    cashu.Sat.String(),
		})
		if err != nil {
			return nil, fmt.Errorf("error requesting melt quote from %s: %w", from, err), storeTokenFromSourceMint
		}

		// if amount in proofs is less than amount asked from mint in melt request,
		// lower the amount for mint request (because of lighting fees)
		if meltResp.Amount+meltResp.FeeReserve+fee > proofsAmount {
			invoicePct--
			amount = proofsAmount * invoicePct / 100
		} else {
			meltQuote = meltResp.Quote
			mintQuote = mintResp.Quote
			goto meltworked
		}
	}

	return nil, fmt.Errorf("stop trying to do the melt because the mint part is too expensive"), tryAnotherTargetMint

meltworked:
	// request from mint to _melt_ into paying the invoice
	delay := 200 * time.Millisecond
	// this request will block until the invoice is paid or it fails
	// (but the API also says it can return "pending" so we handle both)
	meltStatus, err := client.PostMeltBolt11(ctx, from, nut05.PostMeltBolt11Request{
		Quote:  meltQuote,
		Inputs: proofs,
	})
inspectmeltstatusresponse:
	if err != nil || meltStatus.State == nut05.Unpaid {
		return nil, fmt.Errorf("error melting token: %w", err), storeTokenFromSourceMint
	} else if meltStatus.State == nut05.Unknown {
		return nil,
			fmt.Errorf("we don't know what happened with the melt at %s: %v", from, meltStatus),
			manualActionRequired
	} else if meltStatus.State == nut05.Pending {
		for {
			time.Sleep(delay)
			delay *= 2
			meltStatus, err = client.GetMeltQuoteState(ctx, from, meltStatus.Quote)
			goto inspectmeltstatusresponse
		}
	}

	proofs, err = redeemMinted(ctx, to, mintQuote, amount, nil)
	if err != nil {
		return nil,
			fmt.Errorf("failed to redeem minted proofs at %s (after successfully melting at %s): %w", to, from, err),
			manualActionRequired
	}

	return proofs, nil, nothingCanBeDone
}

// redeemMinted just downloads proofs from a lightning invoice paid at some mint.
func redeemMinted(
	ctx context.Context,
	mint string,
	mintQuote string,
	mintAmount uint64,
	spendingCondition *nut10.SpendingCondition,
) (cashu.Proofs, error) {
	// source mint says it has paid the invoice, now check it against the target mint
	// check if the _mint_ invoice was paid
	mintQuoteStatusResp, err := client.GetMintQuoteState(ctx, mint, mintQuote)
	if err != nil {
		return nil, fmt.Errorf(
			"target failed to answer to our mint quote checks (%s): %w; a manual fix is needed",
			mintQuote, err,
		)
	}
	if mintQuoteStatusResp.State != nut04.Paid {
		return nil, fmt.Errorf("target says the invoice wasn't paid (mint quote %s)", mintQuote)
	}

	// since it got paid proceed to get proofs
	//

	// get active keyset of target mint
	keyset, err := client.GetActiveKeyset(ctx, mint)
	if err != nil {
		return nil, fmt.Errorf("failed to get keyset keys for %s: %w", mint, err)
	}

	// unblind the signatures from the promises and build the blinded messages
	keysetKeys, err := ParseKeysetKeys(keyset.Keys)
	if err != nil {
		return nil, fmt.Errorf("target mint %s sent us an invalid keyset: %w", mint, err)
	}
	split := cashu.AmountSplit(mintAmount)
	blindedMessages, secrets, rs, err := createBlindedMessages(split, keyset.Id, spendingCondition)
	if err != nil {
		return nil, fmt.Errorf("error creating blinded messages: %w", err)
	}

	// request mint to sign the blinded messages
	mintResponse, err := client.PostMintBolt11(ctx, mint, nut04.PostMintBolt11Request{
		Quote:   mintQuote,
		Outputs: blindedMessages,
	})
	if err != nil {
		return nil, fmt.Errorf("mint request: %w", err)
	}

	// finally turn those into proofs
	proofs, err := constructProofs(preparedOutputs{
		bm:      blindedMessages,
		secrets: secrets,
		rs:      rs,
	}, mintResponse.Signatures, keysetKeys)
	if err != nil {
		return nil, fmt.Errorf("error constructing proofs: %w", err)
	}

	return proofs, nil
}
