package alby

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"strconv"
	"sync"
	"time"

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

var (
	DB        *sqlx.DB
	qsDecoder = schema.NewDecoder()
)

type AlbyApp struct {
	Id        int32  `json:"id"`
	Name      string `json:"name"`
	AppPubkey string `json:"appPubkey"`
}

type AlbyApps []AlbyApp

type lnurlp struct {
	Status         string `json:"status"`
	Tag            string `json:"tag"`
	CommentAllowed int32  `json:"commentAllowed"`
	Callback       string `json:"callback"`
	MinSendable    int64  `json:"minSendable"`
	MaxSendable    int64  `json:"maxSendable"`
	Metadata       string `json:"metadata"`
	AllowsNostr    bool   `json:"allowsNostr"`
	NostrPubkey    string `json:"nostrPubkey"`
}

type lnurlpError struct {
	Status string `json:"status"`
	Reason string `json:"reason"`
}

type ZapEvent struct {
	Id        string          `json:"id"`
	Pubkey    string          `json:"pubkey"`
	CreatedAt nostr.Timestamp `json:"created_at"`
	Kind      int             `json:"kind"`
	Tags      nostr.Tags      `json:"tags"`
	Content   string          `json:"content"`
	Signature string          `json:"sig"`
}

type NWCReq struct {
	Nostr   string `json:"nostr"`
	Amount  string `json:"amount"`
	Comment string `json:"comment"`
	LNUrl   string `json:"lnurl"`
}

type NWCSecret struct {
	Name         string
	Domain       string
	Wallet       string
	ClientPubkey string
	AppPubkey    string
	Relay        string
	Secret       string
}

func (s *NWCSecret) decodeSecret() {
	l := gologger.Get(config.GetConfig().LogLevel).With().Caller().Logger()
	u, err := url.Parse(s.Wallet)
	if err != nil {
		l.Error().Msgf("failed")
	}
	s.AppPubkey = u.Host
	q, _ := url.ParseQuery(u.RawQuery)
	s.Relay = q.Get("relay")
	s.Secret = q.Get("secret")

}

type LookupInvoiceParams struct {
	Invoice string `json:"invoice"`
}

type LookupInvoice struct {
	Method string              `json:"method"`
	Params LookupInvoiceParams `json:"params"`
}

type LookupInvoiceResponseResult struct {
	Type            string          `json:"type"`
	State           string          `json:"state"`
	Invoice         string          `json:"invoice"`
	Description     string          `json:"description"`
	DescriptionHash string          `json:"description_hash"`
	Preimage        string          `json:"preimage"`
	PaymentHash     string          `json:"payment_hash"`
	Amount          int64           `json:"amount"`
	FeesPaid        int64           `json:"fees_paid"`
	CreatedAt       nostr.Timestamp `json:"created_at"`
	ExpiresAt       nostr.Timestamp `json:"expires_at"`
	SettledAt       nostr.Timestamp `json:"settled_at"`
	Metadata        string          `json:"metadata"`
}

func (s *LookupInvoiceResponseResult) isExpired() bool {
	if time.Now().Unix() > s.ExpiresAt.Time().Unix() {
		return true
	}
	return false
}

func (s *LookupInvoiceResponseResult) isSettled() bool {
	if s.SettledAt.Time().Unix() != 0 {
		return true
	}
	return false
}

type LookupInvoiceResponse struct {
	Result     LookupInvoiceResponseResult `json:"result"`
	ResultType string                      `json:"result_type"`
}

type MakeInvoiceParams struct {
	Amount          int64  `json:"amount"`
	Description     string `json:"description"`
	DescriptionHash string `json:"description_hash"`
	Expiry          int32  `json:"expiry"`
}

type MakeInvoice struct {
	Method string            `json:"method"`
	Params MakeInvoiceParams `json:"params"`
}

type MakeInvoiceResponseResult struct {
	Type            string `json:"type"`
	Invoice         string `json:"invoice"`
	Description     string `json:"description"`
	DescriptionHash string `json:"description_hash"`
	Preimage        string `json:"preimage"`
	PaymentHash     string `json:"payment_hash"`
	Amount          int64  `json:"amount"`
	FeesPaid        int64  `json:"fees_paid"`
	CreatedAt       int64  `json:"created_at"`
	ExpiresAt       int64  `json:"expires_at"`
	SettledAt       int64  `json:"settled_at"`
	Metadata        string `json:"metadata"`
}

type MakeInvoiceResponse struct {
	Result     MakeInvoiceResponseResult `json:"result"`
	ResultType string                    `json:"result_type"`
}

type InvoiceResponse struct {
	Invoice string `json:"pr"`
	Routes  []int  `json:"routes"`
}

func GetLnurlp(w http.ResponseWriter, r *http.Request) {
	l := gologger.Get(config.GetConfig().LogLevel).With().Caller().Logger()
	albyAdmin := config.GetConfig().AlbyAdminAuth

	// setup response type
	w.Header().Set("Content-Type", "application/json")

	// normalize domain
	domain, _, err := net.SplitHostPort(r.Host)
	if err != nil {
		domain = r.Host
	}

	name := r.PathValue("name")

	// get all alby apps
	client := http.Client{}
	req, err := http.NewRequest("GET", "https://alby.devvul.com/api/apps", nil)
	if err != nil {
		l.Error().Msgf("unable to generate alby request for %s@%s: %s", name, domain, err.Error())
		lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "unknown error"}
		retError, _ := json.Marshal(lnurlpReturnError)
		w.WriteHeader(http.StatusNotFound)
		w.Write(retError)
		return
	}

	req.Header = http.Header{"Authorization": {fmt.Sprintf("Bearer %s", albyAdmin)}}
	resp, err := client.Do(req)
	defer resp.Body.Close()

	var albyApps AlbyApps
	err = json.NewDecoder(resp.Body).Decode(&albyApps)
	if err != nil {
		l.Error().Msgf("unable to unmarshal alby request for %s@%s: %s", name, domain, err.Error())
		lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "unknown error"}
		retError, _ := json.Marshal(lnurlpReturnError)
		w.WriteHeader(http.StatusNotFound)
		w.Write(retError)
		return
	}

	// check if user exists
	var npk string
	for _, element := range albyApps {
		if element.Name == name {
			npk = element.AppPubkey
		}
	}

	if len(npk) == 0 {
		l.Debug().Msgf("user doesn't exist in alby %s@%s", name, domain)
		lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "user does not exist"}
		retError, _ := json.Marshal(lnurlpReturnError)
		w.WriteHeader(http.StatusNotFound)
		w.Write(retError)
		return
	}

	// get server pubkey
	var secret NWCSecret
	err = DB.QueryRow("SELECT name, domain, wallet, pubkey FROM lnwallets WHERE name=$1 AND domain=$2", name, domain).
		Scan(&secret.Name, &secret.Domain, &secret.Wallet, &secret.ClientPubkey)
	if err != nil {
		l.Debug().Msgf("user doesn't exist in alby %s@%s: %s", name, domain, err.Error())
		lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "user does not exist"}
		retError, _ := json.Marshal(lnurlpReturnError)
		w.WriteHeader(http.StatusNotFound)
		w.Write(retError)
		return
	}
	secret.decodeSecret()

	lnurlpReturn := &lnurlp{
		Status:         "OK",
		Tag:            "payRequest",
		CommentAllowed: 255,
		Callback:       fmt.Sprintf("https://%s/.well-known/lnurlp/%s/callback", domain, name),
		MinSendable:    1000,
		MaxSendable:    10000000,
		Metadata:       fmt.Sprintf("[[\"text/plain\", \"ln address payment to %s on the devvul server\"],[\"text/identifier\", \"%s@%s\"]]", name, name, domain),
		AllowsNostr:    true,
		NostrPubkey:    secret.ClientPubkey,
	}

	ret, err := json.Marshal(lnurlpReturn)
	if err != nil {
		l.Error().Msgf("unable to marshal json for %s@%s: %s", name, domain, err.Error())
		lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "User not found"}
		retError, _ := json.Marshal(lnurlpReturnError)
		w.WriteHeader(http.StatusNotFound)
		w.Write(retError)
		return
	}

	l.Debug().Msgf("returning pay request callback for %s@%s", name, domain)
	w.WriteHeader(http.StatusOK)
	w.Write(ret)
	return
}

func GetLnurlpCallback(w http.ResponseWriter, r *http.Request) {
	l := gologger.Get(config.GetConfig().LogLevel).With().Caller().Logger()
	var nwc NWCReq
	var zapEvent ZapEvent

	// normalize domain
	domain, _, err := net.SplitHostPort(r.Host)
	if err != nil {
		domain = r.Host
	}
	name := r.PathValue("name")

	err = qsDecoder.Decode(&nwc, r.URL.Query())
	if err != nil {
		l.Error().Msgf("unable to marshal json for %s@%s: %s", name, domain, err.Error())
	}

	// get NWC secret
	var secret NWCSecret
	err = DB.QueryRow("SELECT name, domain, wallet, pubkey FROM lnwallets WHERE name=$1 AND domain=$2", name, domain).
		Scan(&secret.Name, &secret.Domain, &secret.Wallet, &secret.ClientPubkey)
	if err != nil {
		l.Debug().Msgf("user doesn't exist in alby %s@%s: %s", name, domain, err.Error())
		lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "user does not exist"}
		retError, _ := json.Marshal(lnurlpReturnError)
		w.WriteHeader(http.StatusNotFound)
		w.Write(retError)
		return
	}
	secret.decodeSecret()

	// if there is a nostr payload unmarshal it
	if nwc.Nostr != "" {
		err = json.Unmarshal([]byte(nwc.Nostr), &zapEvent)
		if err != nil {
			l.Debug().Msgf("unable to unmarshal nwc value: %s", err.Error())
			lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "unable to connect to relay"}
			retError, _ := json.Marshal(lnurlpReturnError)
			w.WriteHeader(http.StatusNotFound)
			w.Write(retError)
			return
		}
		ok := checkEvent(nwc.Nostr)
		if !ok {
			l.Debug().Msgf("nostr event is not valid", err.Error())
			lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "check your request and try again"}
			retError, _ := json.Marshal(lnurlpReturnError)
			w.WriteHeader(http.StatusNotFound)
			w.Write(retError)
			return
		}
	}

	// connect to the relay
	relayCtx, relayCancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer relayCancel()
	relay, err := nostr.RelayConnect(relayCtx, secret.Relay)
	if err != nil {
		l.Debug().Msgf("unable to connect to relay: %s", err.Error())
		lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "unable to connect to relay"}
		retError, _ := json.Marshal(lnurlpReturnError)
		w.WriteHeader(http.StatusNotFound)
		w.Write(retError)
		return
	}

	// create make_invoice request
	amt, err := strconv.ParseInt(nwc.Amount, 10, 64)
	if err != nil {
		l.Debug().Msgf("unable to parse amount: %s", err.Error())
		lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "unable to create an invoice"}
		retError, _ := json.Marshal(lnurlpReturnError)
		w.WriteHeader(http.StatusNotFound)
		w.Write(retError)
		return
	}

	var hash string
	if nwc.Nostr != "" {
		sha := sha256.Sum256([]byte(nwc.Nostr))
		hash = hex.EncodeToString(sha[:])
	}
	if nwc.Nostr == "" {
		hash = ""
	}

	invoiceParams := MakeInvoiceParams{
		Amount:          amt,
		Description:     nwc.Nostr,
		DescriptionHash: hash,
		Expiry:          300,
	}

	invoice := MakeInvoice{
		Method: "make_invoice",
		Params: invoiceParams,
	}

	invoiceJson, err := json.Marshal(invoice)
	if err != nil {
		l.Debug().Msgf("unable to marshal invoice: %s", err.Error())
		lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "unable to create an invoice"}
		retError, _ := json.Marshal(lnurlpReturnError)
		w.WriteHeader(http.StatusNotFound)
		w.Write(retError)
		return
	}

	// 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())
		lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "unable to create an invoice"}
		retError, _ := json.Marshal(lnurlpReturnError)
		w.WriteHeader(http.StatusNotFound)
		w.Write(retError)
		return
	}

	// 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())
		lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "unable to create an invoice"}
		retError, _ := json.Marshal(lnurlpReturnError)
		w.WriteHeader(http.StatusNotFound)
		w.Write(retError)
		return
	}

	recipient := nostr.Tag{"p", secret.AppPubkey}
	nwcEv := nostr.Event{
		PubKey:    secret.ClientPubkey,
		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,
		},
	}

	subCtx, subCancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer subCancel()
	sub, err := relay.Subscribe(subCtx, filters)
	if err != nil {
		l.Debug().Msgf("unable to connect to relay: %s", err.Error())
		lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "unable to connect to relay"}
		retError, _ := json.Marshal(lnurlpReturnError)
		w.WriteHeader(http.StatusNotFound)
		w.Write(retError)
		return
	}

	var wg sync.WaitGroup
	wg.Add(1)

	// watch for the invoice
	evs := make([]nostr.Event, 0, 1)
	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 <-subCtx.Done():
				l.Debug().Msgf("subscription context cancelled or done: %v", subCtx.Err())
				return
			}
		}
	}()

	// publish the invoice request
	if err := relay.Publish(relayCtx, nwcEv); err != nil {
		l.Debug().Msgf("unable to marshal invoice: %s", err.Error())
		lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "unable to create an invoice"}
		retError, _ := json.Marshal(lnurlpReturnError)
		w.WriteHeader(http.StatusNotFound)
		w.Write(retError)
		return
	}
	// wait for the invoice to get returned
	wg.Wait()

	// decrypt the invoice
	response, err := nip04.Decrypt(evs[0].Content, sharedSecret)
	resStruct := MakeInvoiceResponse{}
	err = json.Unmarshal([]byte(response), &resStruct)
	if err != nil {
		l.Debug().Msgf("unable to create invoice: %s", err.Error())
		lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "unable to connect to relay"}
		retError, _ := json.Marshal(lnurlpReturnError)
		w.WriteHeader(http.StatusNotFound)
		w.Write(retError)
		return
	}

	retStruct := InvoiceResponse{
		Invoice: resStruct.Result.Invoice,
		Routes:  []int{},
	}

	// return the invoice to the requester
	ret, err := json.Marshal(retStruct)
	if err != nil {
		l.Error().Msgf("unable to marshal json for invoice: %s", err.Error())
		lnurlpReturnError := &lnurlpError{Status: "ERROR", Reason: "unable to create invoice"}
		retError, _ := json.Marshal(lnurlpReturnError)
		w.WriteHeader(http.StatusNotFound)
		w.Write(retError)
		return
	}
	if nwc.Nostr != "" {
		l.Debug().Msgf("starting background job for invoice")
		go watchForReceipt(nwc.Nostr, secret, retStruct.Invoice)
	}

	l.Info().Msg("returning lnurl-p payload")
	w.WriteHeader(http.StatusOK)
	w.Write(ret)
}