// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package dnsmessage

import (
	"slices"
)

// An SVCBResource is an SVCB Resource record.
type SVCBResource struct {
	Priority uint16
	Target   Name
	Params   []SVCParam // Must be in strict increasing order by Key.
}

func (r *SVCBResource) realType() Type {
	return TypeSVCB
}

// GoString implements fmt.GoStringer.GoString.
func (r *SVCBResource) GoString() string {
	b := []byte("dnsmessage.SVCBResource{" +
		"Priority: " + printUint16(r.Priority) + ", " +
		"Target: " + r.Target.GoString() + ", " +
		"Params: []dnsmessage.SVCParam{")
	if len(r.Params) > 0 {
		b = append(b, r.Params[0].GoString()...)
		for _, p := range r.Params[1:] {
			b = append(b, ", "+p.GoString()...)
		}
	}
	b = append(b, "}}"...)
	return string(b)
}

// An HTTPSResource is an HTTPS Resource record.
// It has the same format as the SVCB record.
type HTTPSResource struct {
	// Alias for SVCB resource record.
	SVCBResource
}

func (r *HTTPSResource) realType() Type {
	return TypeHTTPS
}

// GoString implements fmt.GoStringer.GoString.
func (r *HTTPSResource) GoString() string {
	return "dnsmessage.HTTPSResource{SVCBResource: " + r.SVCBResource.GoString() + "}"
}

// GetParam returns a parameter value by key.
func (r *SVCBResource) GetParam(key SVCParamKey) (value []byte, ok bool) {
	for i := range r.Params {
		if r.Params[i].Key == key {
			return r.Params[i].Value, true
		}
		if r.Params[i].Key > key {
			break
		}
	}
	return nil, false
}

// SetParam sets a parameter value by key.
// The Params list is kept sorted by key.
func (r *SVCBResource) SetParam(key SVCParamKey, value []byte) {
	i := 0
	for i < len(r.Params) {
		if r.Params[i].Key >= key {
			break
		}
		i++
	}

	if i < len(r.Params) && r.Params[i].Key == key {
		r.Params[i].Value = value
		return
	}

	r.Params = slices.Insert(r.Params, i, SVCParam{Key: key, Value: value})
}

// DeleteParam deletes a parameter by key.
// It returns true if the parameter was present.
func (r *SVCBResource) DeleteParam(key SVCParamKey) bool {
	for i := range r.Params {
		if r.Params[i].Key == key {
			r.Params = slices.Delete(r.Params, i, i+1)
			return true
		}
		if r.Params[i].Key > key {
			break
		}
	}
	return false
}

// A SVCParam is a service parameter.
type SVCParam struct {
	Key   SVCParamKey
	Value []byte
}

// GoString implements fmt.GoStringer.GoString.
func (p SVCParam) GoString() string {
	return "dnsmessage.SVCParam{" +
		"Key: " + p.Key.GoString() + ", " +
		"Value: []byte{" + printByteSlice(p.Value) + "}}"
}

// A SVCParamKey is a key for a service parameter.
type SVCParamKey uint16

// Values defined at https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml#dns-svcparamkeys.
const (
	SVCParamMandatory          SVCParamKey = 0
	SVCParamALPN               SVCParamKey = 1
	SVCParamNoDefaultALPN      SVCParamKey = 2
	SVCParamPort               SVCParamKey = 3
	SVCParamIPv4Hint           SVCParamKey = 4
	SVCParamECH                SVCParamKey = 5
	SVCParamIPv6Hint           SVCParamKey = 6
	SVCParamDOHPath            SVCParamKey = 7
	SVCParamOHTTP              SVCParamKey = 8
	SVCParamTLSSupportedGroups SVCParamKey = 9
)

var svcParamKeyNames = map[SVCParamKey]string{
	SVCParamMandatory:          "Mandatory",
	SVCParamALPN:               "ALPN",
	SVCParamNoDefaultALPN:      "NoDefaultALPN",
	SVCParamPort:               "Port",
	SVCParamIPv4Hint:           "IPv4Hint",
	SVCParamECH:                "ECH",
	SVCParamIPv6Hint:           "IPv6Hint",
	SVCParamDOHPath:            "DOHPath",
	SVCParamOHTTP:              "OHTTP",
	SVCParamTLSSupportedGroups: "TLSSupportedGroups",
}

// String implements fmt.Stringer.String.
func (k SVCParamKey) String() string {
	if n, ok := svcParamKeyNames[k]; ok {
		return n
	}
	return printUint16(uint16(k))
}

// GoString implements fmt.GoStringer.GoString.
func (k SVCParamKey) GoString() string {
	if n, ok := svcParamKeyNames[k]; ok {
		return "dnsmessage.SVCParam" + n
	}
	return printUint16(uint16(k))
}

func (r *SVCBResource) pack(msg []byte, _ map[string]uint16, _ int) ([]byte, error) {
	oldMsg := msg
	msg = packUint16(msg, r.Priority)
	// https://datatracker.ietf.org/doc/html/rfc3597#section-4 prohibits name
	// compression for RR types that are not "well-known".
	// https://datatracker.ietf.org/doc/html/rfc9460#section-2.2 explicitly states that
	// compression of the Target is prohibited, following RFC 3597.
	msg, err := r.Target.pack(msg, nil, 0)
	if err != nil {
		return oldMsg, &nestedError{"SVCBResource.Target", err}
	}
	for i, param := range r.Params {
		if i > 0 && param.Key <= r.Params[i-1].Key {
			return oldMsg, &nestedError{"SVCBResource.Params", errParamOutOfOrder}
		}
		if len(param.Value) > (1<<16)-1 {
			return oldMsg, &nestedError{"SVCBResource.Params", errTooLongSVCBValue}
		}
		msg = packUint16(msg, uint16(param.Key))
		msg = packUint16(msg, uint16(len(param.Value)))
		msg = append(msg, param.Value...)
	}
	return msg, nil
}

func unpackSVCBResource(msg []byte, off int, length uint16) (SVCBResource, error) {
	// Wire format reference: https://www.rfc-editor.org/rfc/rfc9460.html#section-2.2.
	r := SVCBResource{}
	paramsOff := off
	bodyEnd := off + int(length)

	if bodyEnd > len(msg) {
		return SVCBResource{}, errResourceLen
	}

	var err error
	if r.Priority, paramsOff, err = unpackUint16(msg, paramsOff); err != nil {
		return SVCBResource{}, &nestedError{"Priority", err}
	}

	if paramsOff, err = r.Target.unpack(msg, paramsOff); err != nil {
		return SVCBResource{}, &nestedError{"Target", err}
	}

	// Two-pass parsing to avoid allocations.
	// First, count the number of params.
	n := 0
	var totalValueLen uint16
	off = paramsOff
	var previousKey uint16
	for off < bodyEnd {
		var key, size uint16
		if key, off, err = unpackUint16(msg, off); err != nil {
			return SVCBResource{}, &nestedError{"Params key", err}
		}
		if n > 0 && key <= previousKey {
			// As per https://www.rfc-editor.org/rfc/rfc9460.html#section-2.2, clients MUST
			// consider the RR malformed if the SvcParamKeys are not in strictly increasing numeric order
			return SVCBResource{}, &nestedError{"Params", errParamOutOfOrder}
		}
		if size, off, err = unpackUint16(msg, off); err != nil {
			return SVCBResource{}, &nestedError{"Params value length", err}
		}
		if off+int(size) > bodyEnd {
			return SVCBResource{}, errResourceLen
		}
		previousKey = key
		totalValueLen += size
		off += int(size)
		n++
	}
	if off != bodyEnd {
		return SVCBResource{}, errResourceLen
	}

	// Second, fill in the params.
	r.Params = make([]SVCParam, n)
	// valuesBuf is used to hold all param values to reduce allocations.
	// Each param's Value slice will point into this buffer.
	valuesBuf := make([]byte, totalValueLen)
	off = paramsOff
	for i := 0; i < n; i++ {
		p := &r.Params[i]
		var key, size uint16
		if key, off, err = unpackUint16(msg, off); err != nil {
			return SVCBResource{}, &nestedError{"param key", err}
		}
		p.Key = SVCParamKey(key)
		if size, off, err = unpackUint16(msg, off); err != nil {
			return SVCBResource{}, &nestedError{"param length", err}
		}
		if len(msg[off:]) < int(size) {
			return SVCBResource{}, &nestedError{"param value", errCalcLen}
		}
		if copy(valuesBuf, msg[off:][:int(size)]) != int(size) {
			return SVCBResource{}, &nestedError{"param value", errCalcLen}
		}
		p.Value = valuesBuf[:size:size]
		valuesBuf = valuesBuf[size:]
		off += int(size)
	}

	return r, nil
}

// genericSVCBResource parses a single Resource Record compatible with SVCB.
func (p *Parser) genericSVCBResource(svcbType Type) (SVCBResource, error) {
	if !p.resHeaderValid || p.resHeaderType != svcbType {
		return SVCBResource{}, ErrNotStarted
	}
	r, err := unpackSVCBResource(p.msg, p.off, p.resHeaderLength)
	if err != nil {
		return SVCBResource{}, err
	}
	p.off += int(p.resHeaderLength)
	p.resHeaderValid = false
	p.index++
	return r, nil
}

// SVCBResource parses a single SVCBResource.
//
// One of the XXXHeader methods must have been called before calling this
// method.
func (p *Parser) SVCBResource() (SVCBResource, error) {
	return p.genericSVCBResource(TypeSVCB)
}

// HTTPSResource parses a single HTTPSResource.
//
// One of the XXXHeader methods must have been called before calling this
// method.
func (p *Parser) HTTPSResource() (HTTPSResource, error) {
	svcb, err := p.genericSVCBResource(TypeHTTPS)
	if err != nil {
		return HTTPSResource{}, err
	}
	return HTTPSResource{svcb}, nil
}

// genericSVCBResource is the generic implementation for adding SVCB-like resources.
func (b *Builder) genericSVCBResource(h ResourceHeader, r SVCBResource) error {
	if err := b.checkResourceSection(); err != nil {
		return err
	}
	msg, lenOff, err := h.pack(b.msg, b.compression, b.start)
	if err != nil {
		return &nestedError{"ResourceHeader", err}
	}
	preLen := len(msg)
	if msg, err = r.pack(msg, b.compression, b.start); err != nil {
		return &nestedError{"ResourceBody", err}
	}
	if err := h.fixLen(msg, lenOff, preLen); err != nil {
		return err
	}
	if err := b.incrementSectionCount(); err != nil {
		return err
	}
	b.msg = msg
	return nil
}

// SVCBResource adds a single SVCBResource.
func (b *Builder) SVCBResource(h ResourceHeader, r SVCBResource) error {
	h.Type = r.realType()
	return b.genericSVCBResource(h, r)
}

// HTTPSResource adds a single HTTPSResource.
func (b *Builder) HTTPSResource(h ResourceHeader, r HTTPSResource) error {
	h.Type = r.realType()
	return b.genericSVCBResource(h, r.SVCBResource)
}
