package tlsrpt

import (
	"fmt"
	"net/url"
	"strings"
)

// Extension is an additional key/value pair for a TLSRPT record.
type Extension struct {
	Key   string
	Value string
}

// Record is a parsed TLSRPT record, to be served under "_smtp._tls.<domain>".
//
// Example:
//
//	v=TLSRPTv1; rua=mailto:tlsrpt@mox.example;
type Record struct {
	Version    string     // "TLSRPTv1", for "v=".
	RUAs       [][]string // Aggregate reporting URI, for "rua=". "rua=" can occur multiple times, each can be a list. Must be URL-encoded strings, with ",", "!" and ";" encoded.
	Extensions []Extension
}

// String returns a string or use as a TLSRPT DNS TXT record.
func (r Record) String() string {
	b := &strings.Builder{}
	fmt.Fprint(b, "v="+r.Version)
	for _, rua := range r.RUAs {
		fmt.Fprint(b, "; rua="+strings.Join(rua, ","))
	}
	for _, p := range r.Extensions {
		fmt.Fprint(b, "; "+p.Key+"="+p.Value)
	}
	return b.String()
}

type parseErr string

func (e parseErr) Error() string {
	return string(e)
}

var _ error = parseErr("")

// ParseRecord parses a TLSRPT record.
func ParseRecord(txt string) (record *Record, istlsrpt bool, err error) {
	defer func() {
		x := recover()
		if x == nil {
			return
		}
		if xerr, ok := x.(parseErr); ok {
			record = nil
			err = fmt.Errorf("%w: %s", ErrRecordSyntax, xerr)
			return
		}
		panic(x)
	}()

	p := newParser(txt)

	record = &Record{
		Version: "TLSRPTv1",
	}

	p.xtake("v=TLSRPTv1")
	p.xdelim()
	istlsrpt = true
	for {
		k := p.xkey()
		p.xtake("=")
		// note: duplicates are allowed.
		switch k {
		case "rua":
			record.RUAs = append(record.RUAs, p.xruas())
		default:
			v := p.xvalue()
			record.Extensions = append(record.Extensions, Extension{k, v})
		}
		if !p.delim() || p.empty() {
			break
		}
	}
	if !p.empty() {
		p.xerrorf("leftover chars")
	}
	if record.RUAs == nil {
		p.xerrorf("missing rua")
	}
	return
}

type parser struct {
	s string
	o int
}

func newParser(s string) *parser {
	return &parser{s: s}
}

func (p *parser) xerrorf(format string, args ...any) {
	msg := fmt.Sprintf(format, args...)
	if p.o < len(p.s) {
		msg += fmt.Sprintf(" (remain %q)", p.s[p.o:])
	}
	panic(parseErr(msg))
}

func (p *parser) xtake(s string) string {
	if !p.prefix(s) {
		p.xerrorf("expected %q", s)
	}
	p.o += len(s)
	return s
}

func (p *parser) xdelim() {
	if !p.delim() {
		p.xerrorf("expected semicolon")
	}
}

func (p *parser) xtaken(n int) string {
	r := p.s[p.o : p.o+n]
	p.o += n
	return r
}

func (p *parser) prefix(s string) bool {
	return strings.HasPrefix(p.s[p.o:], s)
}

func (p *parser) take(s string) bool {
	if p.prefix(s) {
		p.o += len(s)
		return true
	}
	return false
}

func (p *parser) xtakefn1(fn func(rune, int) bool) string {
	for i, b := range p.s[p.o:] {
		if !fn(b, i) {
			if i == 0 {
				p.xerrorf("expected at least one char")
			}
			return p.xtaken(i)
		}
	}
	if p.empty() {
		p.xerrorf("expected at least 1 char")
	}
	return p.xtaken(len(p.s) - p.o)
}

// ../rfc/8460:368
func (p *parser) xkey() string {
	return p.xtakefn1(func(b rune, i int) bool {
		return i < 32 && (b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || (i > 0 && b == '_' || b == '-' || b == '.'))
	})
}

// ../rfc/8460:371
func (p *parser) xvalue() string {
	return p.xtakefn1(func(b rune, i int) bool {
		return b > ' ' && b < 0x7f && b != '=' && b != ';'
	})
}

// ../rfc/8460:399
func (p *parser) delim() bool {
	o := p.o
	e := len(p.s)
	for o < e && (p.s[o] == ' ' || p.s[o] == '\t') {
		o++
	}
	if o >= e || p.s[o] != ';' {
		return false
	}
	o++
	for o < e && (p.s[o] == ' ' || p.s[o] == '\t') {
		o++
	}
	p.o = o
	return true
}

func (p *parser) empty() bool {
	return p.o >= len(p.s)
}

func (p *parser) wsp() {
	for p.o < len(p.s) && (p.s[p.o] == ' ' || p.s[p.o] == '\t') {
		p.o++
	}
}

// ../rfc/8460:358
func (p *parser) xruas() []string {
	l := []string{p.xuri()}
	p.wsp()
	for p.take(",") {
		p.wsp()
		l = append(l, p.xuri())
		p.wsp()
	}
	return l
}

// ../rfc/8460:360
func (p *parser) xuri() string {
	v := p.xtakefn1(func(b rune, i int) bool {
		return b != ',' && b != '!' && b != ' ' && b != '\t' && b != ';'
	})
	u, err := url.Parse(v)
	if err != nil {
		p.xerrorf("parsing uri %q: %s", v, err)
	}
	if u.Scheme == "" {
		p.xerrorf("missing scheme in uri")
	}
	return v
}
