// Package normalize is a set of tools for cleaning up URL s and formatting
// nostr OK and CLOSED messages.
package normalize

import (
	"bytes"
	"errors"
	"fmt"
	"net/url"

	"lol.mleku.dev/chk"
	"lol.mleku.dev/log"
	"next.orly.dev/pkg/encoders/ints"
	"next.orly.dev/pkg/utils/constraints"
)

var (
	hp    = bytes.HasPrefix
	WS    = []byte("ws://")
	WSS   = []byte("wss://")
	HTTP  = []byte("http://")
	HTTPS = []byte("https://")
)

// URL normalizes the URL
//
// - Adds wss:// to addresses without a port, or with 443 that have no protocol
// prefix
//
// - Adds ws:// to addresses with any other port
//
// - Converts http/s to ws/s
func URL[V constraints.Bytes](v V) (b []byte) {
	u := []byte(v)
	if len(u) == 0 {
		return nil
	}
	u = bytes.TrimSpace(u)
	u = bytes.ToLower(u)
	// if the address has a port number, we can probably assume it is insecure
	// websocket as most public or production relays have a domain name and a
	// well-known port 80 or 443 and thus no port number.
	//
	// if a protocol prefix is present, we assume it is already complete.
	// Converting http/s to websocket-equivalent will be done later anyway.
	if bytes.Contains(u, []byte(":")) &&
		!(hp(u, HTTP) || hp(u, HTTPS) || hp(u, WS) || hp(u, WSS)) {

		split := bytes.Split(u, []byte(":"))
		if len(split) != 2 {
			log.D.F("Error: more than one ':' in URL: '%s'", u)
			// this is a malformed URL if it has more than one ":", return empty
			// since this function does not return an error explicitly.
			return
		}
		p := ints.New(0)
		_, err := p.Unmarshal(split[1])
		if chk.E(err) {
			log.D.F("Error normalizing URL '%s': %s", u, err)
			// again, without an error, we must return nil
			return
		}
		if p.Uint64() > 65535 {
			log.D.F(
				"Port on address %d: greater than maximum 65535",
				p.Uint64(),
			)
			return
		}
		// if the port is explicitly set to 443 we assume it is wss:// and drop
		// the port.
		if p.Uint16() == 443 {
			u = append(WSS, split[0]...)
		} else {
			u = append(WSS, u...)
		}
	}

	// if the prefix isn't specified as http/s or websocket, assume secure
	// websocket and add wss prefix (this is the most common).
	if !(hp(u, HTTP) || hp(u, HTTPS) || hp(u, WS) || hp(u, WSS)) {
		u = append(WSS, u...)
	}
	var err error
	var p *url.URL
	if p, err = url.Parse(string(u)); chk.E(err) {
		return
	}
	// convert http/s to ws/s
	switch p.Scheme {
	case "https":
		p.Scheme = "wss"
	case "http":
		p.Scheme = "ws"
	}
	// remove trailing path slash
	p.Path = string(bytes.TrimRight([]byte(p.Path), "/"))
	return []byte(p.String())
}

// Msg constructs a properly formatted message with a machine-readable prefix
// for OK and CLOSED envelopes.
func Msg(prefix Reason, format string, params ...any) []byte {
	if len(prefix) < 1 {
		prefix = Error
	}
	return []byte(fmt.Sprintf(prefix.S()+": "+format, params...))
}

// MsgString constructs a properly formatted message with a machine-readable prefix
// for OK and CLOSED envelopes.
func MsgString(prefix Reason, format string, params ...any) string {
	if len(prefix) < 1 {
		prefix = Error
	}
	return fmt.Sprintf(prefix.S()+": "+format, params...)
}

// Reason is the machine-readable prefix before the colon in an OK or CLOSED
// envelope message. Below are the most common kinds that are mentioned in
// NIP-01.
type Reason []byte

var (
	AuthRequired = Reason("auth-required")
	PoW          = Reason("pow")
	Duplicate    = Reason("duplicate")
	Blocked      = Reason("blocked")
	RateLimited  = Reason("rate-limited")
	Invalid      = Reason("invalid")
	Error        = Reason("error")
	Unsupported  = Reason("unsupported")
	Restricted   = Reason("restricted")
)

// S returns the Reason as a string
func (r Reason) S() string { return string(r) }

// B returns the Reason as a byte slice.
func (r Reason) B() []byte { return r }

// IsPrefix returns whether a text contains the same Reason prefix.
func (r Reason) IsPrefix(reason []byte) bool {
	return bytes.HasPrefix(
		reason, r.B(),
	)
}

// F allows creation of a full Reason text with a printf style format.
func (r Reason) F(format string, params ...any) []byte {
	return Msg(
		r, format, params...,
	)
}

// Errorf allows creation of a full Reason text with a printf style as an error.
func (r Reason) Errorf(format string, params ...any) (err error) {
	return errors.New(
		MsgString(
			r, format, params...,
		),
	)
}
