package nip60

import (
	"context"
	"encoding/hex"
	"fmt"
	"slices"

	"fiatjaf.com/nostr"
	"fiatjaf.com/nostr/nip60/client"
	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/elnosh/gonuts/cashu"
	"github.com/elnosh/gonuts/cashu/nuts/nut02"
	"github.com/elnosh/gonuts/cashu/nuts/nut10"
	"github.com/elnosh/gonuts/cashu/nuts/nut11"
)

// SendOptions contains options for sending tokens
type SendOptions struct {
	SpecificSourceMint string
	P2PK               *btcec.PublicKey
	RefundTimelock     nostr.Timestamp
	Hashlock           [32]byte
}

func (opts SendOptions) asSpendingCondition(refund *btcec.PublicKey) *nut10.SpendingCondition {
	if opts.Hashlock != [32]byte{} {
		// when we have an HTLC condition:
		// (it can also include a P2PK and a timelock)
		tags := nut11.P2PKTags{
			NSigs:    1,
			Locktime: 0,
			Sigflag:  nut11.SIGINPUTS,
		}
		if opts.P2PK != nil {
			tags.Pubkeys = []*btcec.PublicKey{opts.P2PK}
		}
		if opts.RefundTimelock != 0 {
			tags.Refund = []*btcec.PublicKey{refund}
			tags.Locktime = int64(opts.RefundTimelock)
		}

		return &nut10.SpendingCondition{
			Kind: nut10.HTLC,
			Data: hex.EncodeToString(opts.Hashlock[:]),
			Tags: nut11.SerializeP2PKTags(tags),
		}
	} else if opts.P2PK != nil {
		// otherwise when it is just a P2PK condition with no hashlock
		// (may also have a timelock)

		tags := nut11.P2PKTags{
			NSigs:    1,
			Locktime: 0,
			Pubkeys:  []*btcec.PublicKey{opts.P2PK},
			Sigflag:  nut11.SIGINPUTS,
		}
		if opts.RefundTimelock != 0 {
			tags.Refund = []*btcec.PublicKey{refund}
			tags.Locktime = int64(opts.RefundTimelock)
		}

		return &nut10.SpendingCondition{
			Kind: nut10.P2PK,
			Data: hex.EncodeToString(opts.P2PK.SerializeCompressed()),
			Tags: nut11.SerializeP2PKTags(tags),
		}
	} else {
		return nil
	}
}

type chosenTokens struct {
	mint         string
	tokens       []Token
	tokenIndexes []int
	proofs       cashu.Proofs
	keysets      []nut02.Keyset
}

func (w *Wallet) saveChangeAndDeleteUsedTokens(
	ctx context.Context,
	mintURL string,
	changeProofs cashu.Proofs,
	usedTokenIndexes []int,
	he *HistoryEntry,
) error {
	// delete spent tokens and save our change
	updatedTokens := make([]Token, 0, len(w.Tokens))

	changeToken := Token{
		mintedAt: nostr.Now(),
		Mint:     mintURL,
		Proofs:   changeProofs,
		Deleted:  make([]nostr.ID, 0, len(usedTokenIndexes)),
		event:    &nostr.Event{},
	}

	for i, token := range w.Tokens {
		if slices.Contains(usedTokenIndexes, i) {
			if token.event != nil {
				token.Deleted = append(token.Deleted, token.event.ID)

				deleteEvent := nostr.Event{
					CreatedAt: nostr.Now(),
					Kind:      5,
					Tags:      nostr.Tags{{"e", token.event.ID.Hex()}, {"k", "7375"}},
				}
				w.kr.SignEvent(ctx, &deleteEvent)

				w.Lock()
				w.PublishUpdate(deleteEvent, &token, nil, nil, false)
				w.Unlock()

				// fill in the history deleted token
				he.TokenReferences = append(he.TokenReferences, TokenRef{
					EventID:  token.event.ID,
					Created:  false,
					IsNutzap: false,
				})
			}
		} else {
			updatedTokens = append(updatedTokens, token)
		}
	}

	if len(changeToken.Proofs) > 0 {
		if err := changeToken.toEvent(ctx, w.kr, changeToken.event); err != nil {
			return fmt.Errorf("failed to make change token: %w", err)
		}
		w.Lock()
		w.PublishUpdate(*changeToken.event, nil, nil, &changeToken, false)
		w.Unlock()

		// we don't have to lock tokensMu here because this function will always be called with that lock already held
		w.Tokens = append(updatedTokens, changeToken)

		// fill in the history created token
		he.TokenReferences = append(he.TokenReferences, TokenRef{
			EventID:  changeToken.event.ID,
			Created:  true,
			IsNutzap: false,
		})
	}

	return nil
}

func (w *Wallet) getProofsForSending(
	ctx context.Context,
	amount uint64,
	fromMint string,
	excludeMints []string,
) (chosenTokens, uint64, error) {
	byMint := make(map[string]chosenTokens)
	for t, token := range w.Tokens {
		if fromMint != "" && token.Mint != fromMint {
			continue
		}
		if slices.Contains(excludeMints, token.Mint) {
			continue
		}

		part, ok := byMint[token.Mint]
		if !ok {
			keysets, err := client.GetAllKeysets(ctx, token.Mint)
			if err != nil {
				return chosenTokens{}, 0, fmt.Errorf("failed to get %s keysets: %w", token.Mint, err)
			}
			part.keysets = keysets
			part.tokens = make([]Token, 0, 3)
			part.tokenIndexes = make([]int, 0, 3)
			part.proofs = make(cashu.Proofs, 0, 7)
			part.mint = token.Mint
		}

		part.tokens = append(part.tokens, token)
		part.tokenIndexes = append(part.tokenIndexes, t)
		part.proofs = append(part.proofs, token.Proofs...)
		if part.proofs.Amount() >= amount {
			// maybe we found it here
			fee := calculateFee(part.proofs, part.keysets)
			if part.proofs.Amount() >= (amount + fee) {
				// yes, we did
				return part, fee, nil
			}
		}

		byMint[token.Mint] = part
	}

	// if we got here it's because we didn't get enough proofs from the same mint
	return chosenTokens{}, 0, fmt.Errorf("not enough proofs found from the same mint")
}
