package alby

import (
	"context"
	"encoding/json"
	"fmt"
	"sync"
	"time"

	"git.devvul.com/asara/gologger"
	"git.devvul.com/asara/well-goknown/config"
	"github.com/nbd-wtf/go-nostr"
	"github.com/nbd-wtf/go-nostr/nip04"
)

// check if event is valid
func checkEvent(n string) bool {
	var zapEvent ZapEvent
	l := gologger.Get(config.GetConfig().LogLevel).With().Caller().Logger()

	err := json.Unmarshal([]byte(n), &zapEvent)
	if err != nil {
		l.Debug().Msgf("unable to unmarshal nwc value: %s", err.Error())
		return false
	}

	if err != nil {
		l.Debug().Msgf("unable to read tags from nostr request: %s", err.Error())
		return false
	}

	evt := nostr.Event{
		ID:        zapEvent.Id,
		PubKey:    zapEvent.Pubkey,
		CreatedAt: zapEvent.CreatedAt,
		Kind:      zapEvent.Kind,
		Tags:      zapEvent.Tags,
		Content:   zapEvent.Content,
		Sig:       zapEvent.Signature,
	}

	ok, err := evt.CheckSignature()
	if !ok {
		l.Debug().Msgf("event is invalid", err.Error())
		return false
	}
	return true
}

// background task to return a receipt when the payment is paid
func watchForReceipt(nEvent string, secret NWCSecret, invoice string) {
	var zapEvent ZapEvent
	l := gologger.Get(config.GetConfig().LogLevel).With().Caller().Logger()
	_, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	ok := checkEvent(nEvent)
	if !ok {
		l.Debug().Msgf("nostr event is not valid")
		return
	}

	err := json.Unmarshal([]byte(nEvent), &zapEvent)
	if err != nil {
		l.Debug().Msgf("unable to unmarshal nwc value: %s", err.Error())
		return
	}

	ticker := time.NewTicker(30 * time.Second)
	quit := make(chan struct{})

	go func() {
		defer ticker.Stop()
		for {
			select {
			case <-quit:
				return
			case _ = <-ticker.C:
				paid, failed, result := checkInvoicePaid(invoice, secret, nEvent)
				if failed {
					close(quit)
					return
				}
				if paid {
					sendReceipt(secret, result, nEvent)
					close(quit)
					return
				}
			}
		}
		defer close(quit)
	}()
}

func checkInvoicePaid(checkInvoice string, secret NWCSecret, nEvent string) (bool, bool, LookupInvoiceResponse) {
	l := gologger.Get(config.GetConfig().LogLevel).With().Caller().Logger()
	invoiceParams := LookupInvoiceParams{
		Invoice: checkInvoice,
	}

	invoice := LookupInvoice{
		Method: "lookup_invoice",
		Params: invoiceParams,
	}

	invoiceJson, err := json.Marshal(invoice)
	if err != nil {
		l.Debug().Msgf("unable to marshal invoice: %s", err.Error())
		return false, true, LookupInvoiceResponse{}
	}

	// generate nip-04 shared secret
	sharedSecret, err := nip04.ComputeSharedSecret(secret.AppPubkey, secret.Secret)
	if err != nil {
		l.Debug().Msgf("unable to marshal invoice: %s", err.Error())
		return false, true, LookupInvoiceResponse{}
	}

	// create the encrypted content payload
	encryptedContent, err := nip04.Encrypt(string(invoiceJson), sharedSecret)
	if err != nil {
		l.Debug().Msgf("unable to marshal invoice: %s", err.Error())
		return false, true, LookupInvoiceResponse{}
	}

	recipient := nostr.Tag{"p", secret.AppPubkey}
	nwcEv := nostr.Event{
		PubKey:    secret.AppPubkey,
		CreatedAt: nostr.Now(),
		Kind:      nostr.KindNWCWalletRequest,
		Tags:      nostr.Tags{recipient},
		Content:   encryptedContent,
	}

	// sign the message with the app token
	nwcEv.Sign(secret.Secret)

	var filters nostr.Filters
	t := make(map[string][]string)
	t["e"] = []string{nwcEv.GetID()}
	filters = []nostr.Filter{
		{
			Kinds: []int{
				nostr.KindNWCWalletResponse,
			},
			Tags: t,
		},
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	relay, err := nostr.RelayConnect(ctx, secret.Relay)
	subCtx, subCancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer subCancel()

	// subscribe to the filter
	sub, err := relay.Subscribe(subCtx, filters)
	if err != nil {
		l.Debug().Msgf("unable to connect to relay: %s", err.Error())
		return false, false, LookupInvoiceResponse{}
	}

	var wg sync.WaitGroup
	wg.Add(1)

	// watch for the invoice
	evs := make([]nostr.Event, 0)
	go func() {
		defer wg.Done()
		for {
			select {
			case ev, ok := <-sub.Events:
				if !ok {
					l.Debug().Msgf("subscription events channel is closed")
					return
				}
				if ev.Kind != 0 {
					evs = append(evs, *ev)
				}
				if len(evs) > 0 {
					return
				}
			case <-sub.EndOfStoredEvents:
				l.Trace().Msgf("end of stored events received")
			case <-ctx.Done():
				l.Debug().Msgf("subscription context cancelled or done: %v", ctx.Err())
				return
			}
		}
	}()

	// publish the invoice request
	if err := relay.Publish(ctx, nwcEv); err != nil {
		l.Debug().Msgf("unable to publish event: %s", err.Error())
		return false, false, LookupInvoiceResponse{}
	}
	// wait for the invoice to get returned
	wg.Wait()

	// decrypt the invoice
	response, err := nip04.Decrypt(evs[0].Content, sharedSecret)
	resStruct := LookupInvoiceResponse{}
	err = json.Unmarshal([]byte(response), &resStruct)
	if err != nil {
		l.Debug().Msgf("unable to unmarshal invoice response: %s", err.Error())
		return false, true, LookupInvoiceResponse{}
	}

	if settled := resStruct.Result.isSettled(); settled {
		return true, false, resStruct
	}

	if expired := resStruct.Result.isExpired(); expired {
		return false, true, LookupInvoiceResponse{}
	}

	return false, false, LookupInvoiceResponse{}
}

func sendReceipt(secret NWCSecret, result LookupInvoiceResponse, nEvent string) {
	l := gologger.Get(config.GetConfig().LogLevel).With().Caller().Logger()
	var zapRequestEvent ZapEvent
	err := json.Unmarshal([]byte(nEvent), &zapRequestEvent)
	if err != nil {
		return
	}

	zapReceipt := nostr.Event{
		PubKey:    secret.ClientPubkey,
		CreatedAt: result.Result.SettledAt,
		Kind:      nostr.KindNWCWalletResponse,
		Tags:      zapRequestEvent.Tags,
		Content:   "",
	}

	// add context to zapReceipt
	sender := nostr.Tag{"P", zapRequestEvent.Pubkey}
	bolt11 := nostr.Tag{"bolt11", result.Result.Invoice}
	preimage := nostr.Tag{"preimage", result.Result.Preimage}
	description := nostr.Tag{"description", nEvent}

	zapReceipt.Tags = zapReceipt.Tags.AppendUnique(sender)
	zapReceipt.Tags = zapReceipt.Tags.AppendUnique(bolt11)
	zapReceipt.Tags = zapReceipt.Tags.AppendUnique(preimage)
	zapReceipt.Tags = zapReceipt.Tags.AppendUnique(description)

	// remove unneeded values from tags
	zapReceipt.Tags = zapReceipt.Tags.FilterOut([]string{"relays"})
	zapReceipt.Tags = zapReceipt.Tags.FilterOut([]string{"alt"})

	// sign the receipt
	zapReceipt.Sign(secret.Secret)

	// send it to the listed relays
	ctx := context.Background()
	relayTag := zapRequestEvent.Tags.GetFirst([]string{"relays"})

	var report []string

	for idx, url := range *relayTag {
		if idx == 0 {
			continue
		}
		relay, err := nostr.RelayConnect(ctx, url)
		if err != nil {
			report = append(report, fmt.Sprintf("error: unable to connect to relay (%s): %s", url, err.Error()))
			return
		}
		if err := relay.Publish(ctx, zapReceipt); err != nil {
			report = append(report, fmt.Sprintf("error: unable to connect to relay (%s): %s", url, err.Error()))
			return
		}
		report = append(report, fmt.Sprintf("success: sent receipt to %s", url))
	}
	l.Debug().Msgf("receipt report: %v", report)
	return
}