// Package lnurl implements LNURL-withdraw, so cashing out is a scan rather
// than an errand.
//
// Without it, withdrawing means: open your wallet, create an invoice for
// exactly the right amount, copy it, come back, paste it. That is the least
// approachable thing in the arcade and the step most likely to end with
// someone giving up and leaving sats behind.
//
// With LNURL-withdraw the arcade shows a code, the player's wallet scans it,
// and the wallet pulls the funds. The player never types an amount or handles
// an invoice.
package lnurl

import (
	"fmt"
	"strings"
)

const charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"

// bech32Polymod is the checksum function from BIP-173.
func bech32Polymod(values []byte) uint32 {
	gen := []uint32{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}
	chk := uint32(1)
	for _, v := range values {
		top := chk >> 25
		chk = (chk&0x1ffffff)<<5 ^ uint32(v)
		for i := 0; i < 5; i++ {
			if (top>>uint(i))&1 == 1 {
				chk ^= gen[i]
			}
		}
	}
	return chk
}

func hrpExpand(hrp string) []byte {
	out := make([]byte, 0, len(hrp)*2+1)
	for _, c := range hrp {
		out = append(out, byte(c)>>5)
	}
	out = append(out, 0)
	for _, c := range hrp {
		out = append(out, byte(c)&31)
	}
	return out
}

func createChecksum(hrp string, data []byte) []byte {
	values := append(hrpExpand(hrp), data...)
	values = append(values, 0, 0, 0, 0, 0, 0)
	polymod := bech32Polymod(values) ^ 1
	out := make([]byte, 6)
	for i := 0; i < 6; i++ {
		out[i] = byte(polymod>>uint(5*(5-i))) & 31
	}
	return out
}

func verifyChecksum(hrp string, data []byte) bool {
	return bech32Polymod(append(hrpExpand(hrp), data...)) == 1
}

// convertBits regroups a byte stream between bit widths, which is how bech32
// packs 8-bit data into 5-bit symbols.
func convertBits(data []byte, from, to uint, pad bool) ([]byte, error) {
	var acc uint32
	var bits uint
	maxv := uint32(1)<<to - 1
	var out []byte

	for _, b := range data {
		if from == 8 && b>>from != 0 {
			return nil, fmt.Errorf("lnurl: byte %d exceeds %d bits", b, from)
		}
		acc = acc<<from | uint32(b)
		bits += from
		for bits >= to {
			bits -= to
			out = append(out, byte(acc>>bits)&byte(maxv))
		}
	}
	if pad {
		if bits > 0 {
			out = append(out, byte(acc<<(to-bits))&byte(maxv))
		}
	} else if bits >= from || byte(acc<<(to-bits))&byte(maxv) != 0 {
		return nil, fmt.Errorf("lnurl: invalid padding")
	}
	return out, nil
}

// Encode renders data as a bech32 string under the given human-readable part.
func Encode(hrp string, data []byte) (string, error) {
	converted, err := convertBits(data, 8, 5, true)
	if err != nil {
		return "", err
	}
	combined := append(converted, createChecksum(hrp, converted)...)

	var sb strings.Builder
	sb.WriteString(hrp)
	sb.WriteByte('1')
	for _, c := range combined {
		if int(c) >= len(charset) {
			return "", fmt.Errorf("lnurl: symbol %d out of range", c)
		}
		sb.WriteByte(charset[c])
	}
	return sb.String(), nil
}

// Decode parses a bech32 string back into its human-readable part and data.
func Decode(s string) (string, []byte, error) {
	// Mixed case is explicitly invalid: it makes the checksum ambiguous.
	lower, upper := strings.ToLower(s), strings.ToUpper(s)
	if s != lower && s != upper {
		return "", nil, fmt.Errorf("lnurl: mixed case")
	}
	s = lower

	pos := strings.LastIndex(s, "1")
	if pos < 1 || pos+7 > len(s) {
		return "", nil, fmt.Errorf("lnurl: no separator or too short")
	}
	hrp := s[:pos]

	data := make([]byte, 0, len(s)-pos-1)
	for _, c := range s[pos+1:] {
		idx := strings.IndexRune(charset, c)
		if idx < 0 {
			return "", nil, fmt.Errorf("lnurl: character %q not in charset", c)
		}
		data = append(data, byte(idx))
	}
	if !verifyChecksum(hrp, data) {
		return "", nil, fmt.Errorf("lnurl: bad checksum")
	}

	converted, err := convertBits(data[:len(data)-6], 5, 8, false)
	if err != nil {
		return "", nil, err
	}
	return hrp, converted, nil
}

// EncodeURL renders a URL as an LNURL string.
//
// Wallets accept it uppercase, which is what makes the QR compact: uppercase
// bech32 encodes in QR alphanumeric mode rather than byte mode.
func EncodeURL(url string) (string, error) {
	s, err := Encode("lnurl", []byte(url))
	if err != nil {
		return "", err
	}
	return strings.ToUpper(s), nil
}

// DecodeURL parses an LNURL back into the URL it carries.
func DecodeURL(s string) (string, error) {
	hrp, data, err := Decode(s)
	if err != nil {
		return "", err
	}
	if hrp != "lnurl" {
		return "", fmt.Errorf("lnurl: unexpected prefix %q", hrp)
	}
	return string(data), nil
}
