package dnsutil

import (
	"strings"
	"time"

	"codeberg.org/miekg/dns/internal/dnsstring"
)

// This is copied to zdnsutil.go in the main package to also have access to these functions and not have an
// import cycle. See dnsutil_generate.go.
//
// This file SHOULD NOT import dns things, as that leads to the impossibility to use it from svcb/ and/or
// deleg/.

// Labels returns the number of labels in the name s.
func Labels(s string) (labels int) {
	if s == "." {
		return
	}
	off := 0
	end := false
	for {
		off, end = Next(s, off)
		labels++
		if end {
			return
		}
	}
}

// Next returns the index of the start of the next label in the string s starting at offset. A negative offset
// will cause a panic. The bool end is true when the end of the string has been reached. Also see [Prev].
func Next(s string, offset int) (i int, end bool) {
	if s == "" {
		return 0, true
	}
	for i = offset; i < len(s)-1; i++ {
		if s[i] != '.' {
			continue
		}
		return i + 1, false
	}
	return i + 1, true
}

// Prev returns the index of the label when starting from the right and jumping n labels to the left.
// The bool start is true when the start of the string has been overshot. Also see [Next].
func Prev(s string, n int) (i int, start bool) {
	if s == "" {
		return 0, true
	}
	if n == 0 {
		return len(s), false
	}

	l := len(s) - 1
	if s[l] == '.' {
		l--
	}

	for ; l >= 0 && n > 0; l-- {
		if s[l] != '.' {
			continue
		}
		n--
		if n == 0 {
			return l + 1, false
		}
	}

	return 0, n > 1
}

// Fqdn return the fully qualified domain name from s. If s is already fully qualified, it behaves as the
// identity function.
func Fqdn(s string) string {
	if IsFqdn(s) {
		return s
	}
	return s + "."
}

// IsFqdn checks if a domain name is fully qualified. As this library doesn't support escapes in names, this
// simply calls [strings.HasSuffix].
func IsFqdn(s string) bool { return strings.HasSuffix(s, ".") }

// Canonical returns the domain name in canonical form. A name in canonical form is lowercase and fully qualified.
// Only US-ASCII letters are affected. See Section 6.2 in RFC 4034.
func Canonical(s string) string {
	return strings.Map(func(r rune) rune {
		if r >= 'A' && r <= 'Z' {
			r += 'a' - 'A'
		}
		return r
	}, Fqdn(s))
}

// IsName checks if s is a valid domain name.  A non fully qualified domain name is considered valid.
// Note that this function is extremely liberal; almost any string is a valid domain name as the DNS is 8 bit
// protocol. It checks if each label fits in 63 characters and that the entire name will fit into the 255
// octet wire-format limit.
func IsName(s string) bool {
	// XXX: The logic in this function was copied from pack.Name and should be kept in sync with that function.
	const lenmsg = 256
	ls := len(s)

	if ls == 1 && s[0] == '.' {
		return true
	}

	var (
		off   int
		begin int
	)
	for begin < ls {
		i := strings.IndexByte(s[begin:], '.')
		if i == -1 {
			break
		}
		i += begin

		labelLen := i - begin
		// top two bits of length must be clear and two dots back to back is not legal
		if labelLen == 0 || labelLen >= 1<<6 {
			return false
		}
		// off can already (we're in a loop) be bigger than lenmsg this happens when a name isn't fully qualified
		off += 1 + labelLen
		if off > lenmsg {
			return false
		}
		begin = i + 1
	}
	return true
}

// compareLabel compares a and b while ignoring case. It returns 0 when equal, -1 when a is smaller than b,
// and +1 when a is greater then b. This ends up a compareLabel in the dns package too as generated by
// dnsutil_generate.go.
func compareLabel(a, b string) int {
	la, lb := len(a), len(b)
	for i := range min(la, lb) {
		ai := a[i]
		bi := b[i]
		if ai >= 'A' && ai <= 'Z' {
			ai |= 'a' - 'A'
		}
		if bi >= 'A' && bi <= 'Z' {
			bi |= 'a' - 'A'
		}
		if ai < bi {
			return -1
		}
		if ai > bi {
			return +1
		}
	}
	if la < lb {
		return -1
	}
	if la > lb {
		return +1
	}
	return 0
}

// TimeToString translates the RRSIG's incep. and expir. times to the
// string representation used when printing the record. It takes serial arithmetic (RFC 1982) into account.
func TimeToString(t uint32) string {
	mod := max((int64(t)-time.Now().Unix())/maxSerialIncrement-1, 0)
	ti := time.Unix(int64(t)-mod*maxSerialIncrement, 0).UTC()
	return ti.Format("20060102150405")
}

// StringToTime translates the RRSIG's incep. and expir. times from string values like "20110403154150" to an 32 bit integer.
// It takes serial arithmetic (RFC 1982) into account.
func StringToTime(s string) (uint32, error) {
	t, err := dnsstring.ToTime(s)
	if err != nil {
		return 0, err
	}
	mod := max(t/maxSerialIncrement-1, 0)
	return uint32(t - mod*maxSerialIncrement), nil
}

// Absolute takes the name and origin and appends the origin to the name. This takes the RFC 1035 presentation
// format into account, i.e. "@" means the origin in a name.
// If s is not a valid domain name, the empty string is returned. If the origin is needed to be appended,
// but is empty the empty string is also returned.
func Absolute(s, origin string) string {
	if s == "@" {
		return origin
	}
	if IsName(s) == false { // done to make the conversion via dnsutil_generate.go work, instead of !IsName(s)
		return ""
	}
	if IsFqdn(s) {
		return s
	}
	if origin == "" {
		return ""
	}
	if origin == "." {
		return s + origin
	}
	return s + "." + origin
}

// maxSerialIncrement is the maximum difference between two serial numbers. See RFC 1982.
const maxSerialIncrement = 2147483647
