package main

import (
	"encoding/json"
	"log"
	"net/http"

	"github.com/drjones/quantum-arcade/pkg/lnurl"
)

// LNURL-withdraw endpoints.
//
// Two of these are called by the player's wallet, not by the arcade's own
// client, so they follow the LNURL specification's shapes rather than this
// project's conventions: a bare JSON object with a status field, and no
// bearer token, because a wallet has none. The single-use k1 is the
// authorisation.

func (s *server) routesLNURL(mux *http.ServeMux) {
	// Called by the arcade's own client, authenticated as normal.
	mux.HandleFunc("POST /api/withdraw/code", s.handleWithdrawCode)

	// Called by the player's wallet after scanning. No session exists here.
	mux.HandleFunc("GET /lnurl/withdraw", s.handleLNURLTerms)
	mux.HandleFunc("GET /lnurl/withdraw/callback", s.handleLNURLCallback)
}

// handleWithdrawCode issues a scannable cash-out code.
//
// The funds are debited when the code is issued, not when it is redeemed. A
// code is an authorisation to pull an exact amount, and leaving the balance
// spendable while a code is outstanding would let a player cash out and then
// bet the same sats before the wallet claims them.
func (s *server) handleWithdrawCode(w http.ResponseWriter, r *http.Request) {
	if s.ln == nil || s.lnurl == nil {
		writeErr(w, http.StatusServiceUnavailable, "lightning not configured")
		return
	}
	accountID, _, ok := s.account(r)
	if !ok {
		writeErr(w, http.StatusUnauthorized, "sign in first")
		return
	}

	var req struct {
		AmountSat int64 `json:"amount_sats"`
	}
	body, err := readBody(r)
	if err != nil {
		writeErr(w, http.StatusBadRequest, "could not read request")
		return
	}
	if err := json.Unmarshal(body, &req); err != nil || req.AmountSat < 1 {
		writeErr(w, http.StatusBadRequest, "amount_sats required (>=1)")
		return
	}
	amountMsat := req.AmountSat * 1000

	// Take the money now. If the code is never scanned, the sweep below
	// returns it.
	if _, err := s.ledger.Withdraw(r.Context(), accountID, amountMsat); err != nil {
		writeErr(w, http.StatusBadRequest, err.Error())
		return
	}

	code, k1, err := s.lnurl.Issue(accountID, amountMsat)
	if err != nil {
		// Could not issue: give the money straight back rather than holding it
		// against a code that does not exist.
		if _, rerr := s.ledger.Deposit(r.Context(), accountID, amountMsat); rerr != nil {
			log.Printf("lnurl: CRITICAL: debited %d msat from %d but could not "+
				"issue a code or refund: %v / %v", amountMsat, accountID, err, rerr)
		}
		writeErr(w, http.StatusInternalServerError, err.Error())
		return
	}

	bal, _ := s.ledger.Balance(r.Context(), accountID)
	writeJSON(w, http.StatusOK, map[string]any{
		"lnurl":        code,
		"k1":           k1,
		"amount_sats":  req.AmountSat,
		"balance_msat": bal,
		"expires_in":   int(lnurl.TokenTTL.Seconds()),
	})
}

// handleLNURLTerms is what the wallet fetches after scanning.
func (s *server) handleLNURLTerms(w http.ResponseWriter, r *http.Request) {
	if s.lnurl == nil {
		writeJSON(w, http.StatusOK, lnurl.Fail("lightning not configured"))
		return
	}
	terms, err := s.lnurl.Describe(r.URL.Query().Get("k1"))
	if err != nil {
		// LNURL errors are 200s with a status field; a wallet shows the reason
		// to the player, whereas an HTTP error shows nothing useful.
		writeJSON(w, http.StatusOK, lnurl.Fail(err.Error()))
		return
	}
	writeJSON(w, http.StatusOK, terms)
}

// handleLNURLCallback is where the wallet delivers its invoice.
func (s *server) handleLNURLCallback(w http.ResponseWriter, r *http.Request) {
	if s.ln == nil || s.lnurl == nil {
		writeJSON(w, http.StatusOK, lnurl.Fail("lightning not configured"))
		return
	}

	k1 := r.URL.Query().Get("k1")
	invoice := r.URL.Query().Get("pr")
	if k1 == "" || invoice == "" {
		writeJSON(w, http.StatusOK, lnurl.Fail("k1 and pr are required"))
		return
	}

	// Consume the token before paying. A token left valid after a successful
	// payment could be replayed for the same amount again.
	token, err := s.lnurl.Redeem(r.Context(), k1)
	if err != nil {
		writeJSON(w, http.StatusOK, lnurl.Fail(err.Error()))
		return
	}

	// The balance was already debited when the code was issued, so this queues
	// the payment against funds the arcade is holding rather than the player's
	// balance. Crediting first and withdrawing again would double-charge.
	if _, err := s.ln.PayHeld(r.Context(), token.AccountID, invoice, token.AmountMsat); err != nil {
		// Payment failed: hand the authorisation back so the player can retry
		// rather than losing the cash-out silently.
		s.lnurl.Restore(token)
		log.Printf("lnurl: payment for account %d failed: %v", token.AccountID, err)
		writeJSON(w, http.StatusOK, lnurl.Fail("payment failed; try scanning again"))
		return
	}

	writeJSON(w, http.StatusOK, lnurl.OK())
}
