// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package ice

import (
	"context"
	"errors"
	"fmt"
	"hash/crc32"
	"io"
	"net"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/pion/stun/v3"
)

type candidateBase struct {
	id            string
	networkType   NetworkType
	candidateType CandidateType

	component      uint16
	address        string
	port           int
	relatedAddress *CandidateRelatedAddress
	tcpType        TCPType

	resolvedAddr net.Addr

	lastSent     atomic.Int64
	lastReceived atomic.Int64
	conn         net.PacketConn

	currAgent *Agent
	closeCh   chan struct{}
	closedCh  chan struct{}

	foundationOverride string
	priorityOverride   uint32

	relayLocalPreference uint16

	remoteCandidateCaches sync.Map // map[AddrPort]Candidate
	isLocationTracked     bool
	extensions            []CandidateExtension
}

// Save a time reference to calculate monotonic time for candidate last sent/received.
// nolint: gochecknoglobals
var timeRef = time.Now()

// getMonoNanos returns the monotonic nanoseconds of a time t since timeRef.
func getMonoNanos(t time.Time) int64 {
	return t.Sub(timeRef).Nanoseconds()
}

// getMonoTime returns a time.Time based on monotonic nanos since timeRef.
func getMonoTime(nanos int64) time.Time {
	return timeRef.Add(time.Duration(nanos))
}

// Done implements context.Context.
func (c *candidateBase) Done() <-chan struct{} {
	return c.closeCh
}

// Err implements context.Context.
func (c *candidateBase) Err() error {
	select {
	case <-c.closedCh:
		return ErrRunCanceled
	default:
		return nil
	}
}

// Deadline implements context.Context.
func (c *candidateBase) Deadline() (deadline time.Time, ok bool) {
	return time.Time{}, false
}

// Value implements context.Context.
func (c *candidateBase) Value(any) any {
	return nil
}

// setWriteDeadline is used by upper layers to push write deadlines down to the
// underlying packet connection.
func (c *candidateBase) setWriteDeadline(t time.Time) error {
	if c.conn == nil {
		return nil
	}

	return c.conn.SetWriteDeadline(t)
}

// ID returns Candidate ID.
func (c *candidateBase) ID() string {
	return c.id
}

func (c *candidateBase) Foundation() string {
	if c.foundationOverride != "" {
		return c.foundationOverride
	}

	return fmt.Sprintf("%d", crc32.ChecksumIEEE([]byte(c.Type().String()+c.address+c.networkType.String())))
}

// Address returns Candidate Address.
func (c *candidateBase) Address() string {
	return c.address
}

// Port returns Candidate Port.
func (c *candidateBase) Port() int {
	return c.port
}

// Type returns candidate type.
func (c *candidateBase) Type() CandidateType {
	return c.candidateType
}

// NetworkType returns candidate NetworkType.
func (c *candidateBase) NetworkType() NetworkType {
	return c.networkType
}

// Component returns candidate component.
func (c *candidateBase) Component() uint16 {
	return c.component
}

func (c *candidateBase) SetComponent(component uint16) {
	c.component = component
}

// LocalPreference returns the local preference for this candidate.
func (c *candidateBase) LocalPreference() uint16 { //nolint:cyclop
	if c.candidateType == CandidateTypeRelay {
		return c.relayLocalPreference
	}

	if c.NetworkType().IsTCP() {
		// RFC 6544, section 4.2
		//
		// In Section 4.1.2.1 of [RFC5245], a recommended formula for UDP ICE
		// candidate prioritization is defined.  For TCP candidates, the same
		// formula and candidate type preferences SHOULD be used, and the
		// RECOMMENDED type preferences for the new candidate types defined in
		// this document (see Section 5) are 105 for NAT-assisted candidates and
		// 75 for UDP-tunneled candidates.
		//
		// (...)
		//
		// With TCP candidates, the local preference part of the recommended
		// priority formula is updated to also include the directionality
		// (active, passive, or simultaneous-open) of the TCP connection.  The
		// RECOMMENDED local preference is then defined as:
		//
		//     local preference = (2^13) * direction-pref + other-pref
		//
		// The direction-pref MUST be between 0 and 7 (both inclusive), with 7
		// being the most preferred.  The other-pref MUST be between 0 and 8191
		// (both inclusive), with 8191 being the most preferred.  It is
		// RECOMMENDED that the host, UDP-tunneled, and relayed TCP candidates
		// have the direction-pref assigned as follows: 6 for active, 4 for
		// passive, and 2 for S-O.  For the NAT-assisted and server reflexive
		// candidates, the RECOMMENDED values are: 6 for S-O, 4 for active, and
		// 2 for passive.
		//
		// (...)
		//
		// If any two candidates have the same type-preference and direction-
		// pref, they MUST have a unique other-pref.  With this specification,
		// this usually only happens with multi-homed hosts, in which case
		// other-pref is the preference for the particular IP address from which
		// the candidate was obtained.  When there is only a single IP address,
		// this value SHOULD be set to the maximum allowed value (8191).
		var otherPref uint16 = 8191

		directionPref := func() uint16 {
			switch c.Type() {
			case CandidateTypeHost, CandidateTypeRelay:
				switch c.tcpType {
				case TCPTypeActive:
					return 6
				case TCPTypePassive:
					return 4
				case TCPTypeSimultaneousOpen:
					return 2
				case TCPTypeUnspecified:
					return 0
				}
			case CandidateTypePeerReflexive, CandidateTypeServerReflexive:
				switch c.tcpType {
				case TCPTypeSimultaneousOpen:
					return 6
				case TCPTypeActive:
					return 4
				case TCPTypePassive:
					return 2
				case TCPTypeUnspecified:
					return 0
				}
			case CandidateTypeUnspecified:
				return 0
			}

			return 0
		}()

		return (1<<13)*directionPref + otherPref
	}

	return defaultLocalPreference
}

// RelatedAddress returns *CandidateRelatedAddress.
func (c *candidateBase) RelatedAddress() *CandidateRelatedAddress {
	return c.relatedAddress
}

func (c *candidateBase) TCPType() TCPType {
	return c.tcpType
}

// start runs the candidate using the provided connection.
func (c *candidateBase) start(a *Agent, conn net.PacketConn, initializedCh <-chan struct{}) {
	if c.conn != nil {
		c.agent().log.Warn("Can't start already started candidateBase")

		return
	}
	c.currAgent = a
	c.conn = conn
	c.closeCh = make(chan struct{})
	c.closedCh = make(chan struct{})

	go c.recvLoop(initializedCh)
}

var bufferPool = sync.Pool{ // nolint:gochecknoglobals
	New: func() any {
		buf := make([]byte, receiveMTU)

		return &buf
	},
}

func (c *candidateBase) recvLoop(initializedCh <-chan struct{}) {
	agent := c.agent()

	defer close(c.closedCh)

	select {
	case <-initializedCh:
	case <-c.closeCh:
		return
	}

	bufPtr, ok := bufferPool.Get().(*[]byte)
	if !ok {
		return
	}
	defer bufferPool.Put(bufPtr)
	buf := *bufPtr

	for {
		n, srcAddr, err := c.conn.ReadFrom(buf)
		if err != nil {
			if !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
				agent.log.Warnf("Failed to read from candidate %s: %v", c, err)
			}

			return
		}

		c.handleInboundPacket(buf[:n], srcAddr)
	}
}

func (c *candidateBase) validateSTUNTrafficCache(addr net.Addr) bool {
	if candidate, ok := c.remoteCandidateCaches.Load(toAddrPort(addr)); ok {
		remoteCandidate, ok := candidate.(Candidate)
		if !ok {
			return false
		}

		remoteCandidate.seen(false)

		return true
	}

	return false
}

func (c *candidateBase) addRemoteCandidateCache(candidate Candidate, srcAddr net.Addr) {
	if c.validateSTUNTrafficCache(srcAddr) {
		return
	}
	c.remoteCandidateCaches.Store(toAddrPort(srcAddr), candidate)
}

func (c *candidateBase) replaceRemoteCandidateCacheValues(oldRemote, newRemote Candidate) {
	c.remoteCandidateCaches.Range(func(key, value any) bool {
		candidate, ok := value.(Candidate)
		if ok && candidate == oldRemote {
			c.remoteCandidateCaches.Store(key, newRemote)
		}

		return true
	})
}

func (c *candidateBase) handleInboundPacket(buf []byte, srcAddr net.Addr) {
	agent := c.agent()

	if stun.IsMessage(buf) {
		msg := &stun.Message{
			Raw: make([]byte, len(buf)),
		}

		// Explicitly copy raw buffer so Message can own the memory.
		copy(msg.Raw, buf)

		if err := msg.Decode(); err != nil {
			agent.log.Warnf("Failed to handle decode ICE from %s to %s: %v", c.addr(), srcAddr, err)

			return
		}

		if err := agent.loop.Run(c, func(_ context.Context) {
			// nolint: contextcheck
			agent.handleInbound(msg, c, srcAddr)
		}); err != nil {
			agent.log.Warnf("Failed to handle message: %v", err)
		}

		return
	}

	if !c.validateSTUNTrafficCache(srcAddr) {
		remoteCandidate, valid := agent.validateNonSTUNTraffic(c, srcAddr) //nolint:contextcheck
		if !valid {
			agent.log.Warnf("Discarded message from %s, not a valid remote candidate", c.addr())

			return
		}
		c.addRemoteCandidateCache(remoteCandidate, srcAddr)
	}

	// Note: This will return packetio.ErrFull if the buffer ever manages to fill up.
	n, err := agent.buf.Write(buf)
	if err != nil {
		agent.log.Warnf("Failed to write packet: %s", err)

		return
	}

	// Add received application bytes to the currently selected candidate pair.
	if n > 0 {
		if sp := agent.getSelectedPair(); sp != nil {
			sp.UpdatePacketReceived(n)
		}
	}
}

// close stops the recvLoop.
func (c *candidateBase) close() error {
	// If conn has never been started will be nil
	if c.Done() == nil {
		return nil
	}

	// Assert that conn has not already been closed
	select {
	case <-c.Done():
		return nil
	default:
	}

	var firstErr error

	// Unblock recvLoop
	close(c.closeCh)
	if err := c.conn.SetDeadline(time.Now()); err != nil {
		firstErr = err
	}

	// Close the conn
	if err := c.conn.Close(); err != nil && firstErr == nil {
		firstErr = err
	}

	if firstErr != nil {
		return firstErr
	}

	// Wait until the recvLoop is closed
	<-c.closedCh

	return nil
}

func (c *candidateBase) writeTo(raw []byte, dst Candidate) (int, error) {
	n, err := c.conn.WriteTo(raw, dst.addr())
	if err != nil {
		// If the connection is closed, we should return the error
		if errors.Is(err, io.ErrClosedPipe) {
			return n, err
		}
		c.agent().log.Infof("Failed to send packet: %v", err)

		return n, nil
	}
	c.seen(true)

	return n, nil
}

// TypePreference returns the type preference for this candidate.
func (c *candidateBase) TypePreference() uint16 {
	pref := c.Type().Preference()
	if pref == 0 {
		return 0
	}

	if c.NetworkType().IsTCP() {
		var tcpPriorityOffset uint16 = defaultTCPPriorityOffset
		if c.agent() != nil {
			tcpPriorityOffset = c.agent().tcpPriorityOffset
		}

		pref -= tcpPriorityOffset
	}

	return pref
}

// Priority computes the priority for this ICE Candidate
// See: https://www.rfc-editor.org/rfc/rfc8445#section-5.1.2.1
func (c *candidateBase) Priority() uint32 {
	if c.priorityOverride != 0 {
		return c.priorityOverride
	}

	// The local preference MUST be an integer from 0 (lowest preference) to
	// 65535 (highest preference) inclusive.  When there is only a single IP
	// address, this value SHOULD be set to 65535.  If there are multiple
	// candidates for a particular component for a particular data stream
	// that have the same type, the local preference MUST be unique for each
	// one.

	return (1<<24)*uint32(c.TypePreference()) +
		(1<<8)*uint32(c.LocalPreference()) +
		(1<<0)*uint32(256-c.Component())
}

// transportAddressEqual checks if the transport address (IP, Port, NetworkType, TCPType) is equal to another
// candidate.
func (c *candidateBase) transportAddressEqual(other Candidate) bool {
	if c.addr() != other.addr() {
		if c.addr() == nil || other.addr() == nil {
			return false
		}
		if !addrEqual(c.addr(), other.addr()) {
			return false
		}
	}

	return c.NetworkType() == other.NetworkType() &&
		c.Address() == other.Address() &&
		c.Port() == other.Port() &&
		c.TCPType() == other.TCPType()
}

// Equal is used to compare two candidateBases.
func (c *candidateBase) Equal(other Candidate) bool {
	return c.transportAddressEqual(other) &&
		c.Type() == other.Type() &&
		c.RelatedAddress().Equal(other.RelatedAddress())
}

// DeepEqual is same as Equal but also compares the extensions.
func (c *candidateBase) DeepEqual(other Candidate) bool {
	return c.Equal(other) && c.extensionsEqual(other.Extensions())
}

// String makes the candidateBase printable.
func (c *candidateBase) String() string {
	return fmt.Sprintf(
		"%s %s %s%s (resolved: %v)",
		c.NetworkType(),
		c.Type(),
		net.JoinHostPort(c.Address(), strconv.Itoa(c.Port())),
		c.relatedAddress,
		c.resolvedAddr,
	)
}

// LastReceived returns a time.Time indicating the last time
// this candidate was received.
func (c *candidateBase) LastReceived() time.Time {
	if lastReceived := c.lastReceived.Load(); lastReceived != 0 {
		return getMonoTime(lastReceived)
	}

	return time.Time{}
}

func (c *candidateBase) setLastReceived(t time.Time) {
	c.lastReceived.Store(getMonoNanos(t))
}

// LastSent returns a time.Time indicating the last time
// this candidate was sent.
func (c *candidateBase) LastSent() time.Time {
	if lastSent := c.lastSent.Load(); lastSent != 0 {
		return getMonoTime(lastSent)
	}

	return time.Time{}
}

func (c *candidateBase) setLastSent(t time.Time) {
	c.lastSent.Store(getMonoNanos(t))
}

func (c *candidateBase) seen(outbound bool) {
	if outbound {
		c.setLastSent(time.Now())
	} else {
		c.setLastReceived(time.Now())
	}
}

func (c *candidateBase) addr() net.Addr {
	return c.resolvedAddr
}

func (c *candidateBase) filterForLocationTracking() bool {
	return c.isLocationTracked
}

func (c *candidateBase) agent() *Agent {
	return c.currAgent
}

func (c *candidateBase) context() context.Context {
	return c
}

func (c *candidateBase) copy() (Candidate, error) {
	return UnmarshalCandidate(c.Marshal())
}

func removeZoneIDFromAddress(addr string) string {
	if before, _, ok := strings.Cut(addr, "%"); ok {
		return before
	}

	return addr
}

// Marshal returns the string representation of the ICECandidate.
func (c *candidateBase) Marshal() string {
	val := c.Foundation()
	if val == " " {
		val = ""
	}

	val = fmt.Sprintf("%s %d %s %d %s %d typ %s",
		val,
		c.Component(),
		c.NetworkType().NetworkShort(),
		c.Priority(),
		removeZoneIDFromAddress(c.Address()),
		c.Port(),
		c.Type())

	if r := c.RelatedAddress(); r != nil && r.Address != "" && r.Port != 0 {
		val = fmt.Sprintf("%s raddr %s rport %d",
			val,
			r.Address,
			r.Port)
	}

	extensions := c.marshalExtensions()

	if extensions != "" {
		val = fmt.Sprintf("%s %s", val, extensions)
	}

	return val
}

// CandidateExtension represents a single candidate extension
// as defined in https://tools.ietf.org/html/rfc5245#section-15.1
// .
type CandidateExtension struct {
	Key   string
	Value string
}

func (c *candidateBase) Extensions() []CandidateExtension {
	tcpType := c.TCPType()
	hasTCPType := 0
	if tcpType != TCPTypeUnspecified {
		hasTCPType = 1
	}

	extensions := make([]CandidateExtension, len(c.extensions)+hasTCPType)
	// We store the TCPType in c.tcpType, but we need to return it as an extension.
	if hasTCPType == 1 {
		extensions[0] = CandidateExtension{
			Key:   "tcptype",
			Value: tcpType.String(),
		}
	}

	copy(extensions[hasTCPType:], c.extensions)

	return extensions
}

// Get returns the value of the given key if it exists.
func (c *candidateBase) GetExtension(key string) (CandidateExtension, bool) {
	extension := CandidateExtension{Key: key}

	for i := range c.extensions {
		if c.extensions[i].Key == key {
			extension.Value = c.extensions[i].Value

			return extension, true
		}
	}

	// TCPType was manually set.
	if key == "tcptype" && c.TCPType() != TCPTypeUnspecified { //nolint:goconst
		extension.Value = c.TCPType().String()

		return extension, true
	}

	return extension, false
}

func (c *candidateBase) AddExtension(ext CandidateExtension) error {
	if ext.Key == "tcptype" {
		tcpType := NewTCPType(ext.Value)
		if tcpType == TCPTypeUnspecified {
			return fmt.Errorf("%w: invalid or unsupported TCPtype %s", errParseTCPType, ext.Value)
		}

		c.tcpType = tcpType

		return nil
	}

	if ext.Key == "" {
		return fmt.Errorf("%w: key is empty", errParseExtension)
	}

	// per spec, Extensions aren't explicitly unique, we only set the first one.
	// If the exteion is set multiple times.
	for i := range c.extensions {
		if c.extensions[i].Key == ext.Key {
			c.extensions[i] = ext

			return nil
		}
	}

	c.extensions = append(c.extensions, ext)

	return nil
}

func (c *candidateBase) RemoveExtension(key string) (ok bool) {
	if key == "tcptype" {
		c.tcpType = TCPTypeUnspecified
		ok = true
	}

	for i := range c.extensions {
		if c.extensions[i].Key == key {
			c.extensions = append(c.extensions[:i], c.extensions[i+1:]...)
			ok = true

			break
		}
	}

	return ok
}

// marshalExtensions returns the string representation of the candidate extensions.
func (c *candidateBase) marshalExtensions() string {
	value := ""
	exts := c.Extensions()

	for i := range exts {
		if value != "" {
			value += " "
		}

		value += exts[i].Key + " " + exts[i].Value
	}

	return value
}

// Equal returns true if the candidate extensions are equal.
func (c *candidateBase) extensionsEqual(other []CandidateExtension) bool {
	freq1 := make(map[CandidateExtension]int)
	freq2 := make(map[CandidateExtension]int)

	if len(c.extensions) != len(other) {
		return false
	}

	if len(c.extensions) == 0 {
		return true
	}

	if len(c.extensions) == 1 {
		return c.extensions[0] == other[0]
	}

	for i := range c.extensions {
		freq1[c.extensions[i]]++
		freq2[other[i]]++
	}

	for k, v := range freq1 {
		if freq2[k] != v {
			return false
		}
	}

	return true
}

func (c *candidateBase) setExtensions(extensions []CandidateExtension) {
	c.extensions = extensions
}

// UnmarshalCandidate Parses a candidate from a string
// https://datatracker.ietf.org/doc/html/rfc5245#section-15.1
func UnmarshalCandidate(raw string) (Candidate, error) { //nolint:cyclop
	// Handle candidates with the "candidate:" prefix as defined in RFC 5245 section 15.1.
	raw = strings.TrimPrefix(raw, "candidate:")

	pos := 0
	// foundation ( 1*32ice-char ) But we allow for empty foundation,
	foundation, pos, err := readCandidateCharToken(raw, pos, 32)
	if err != nil {
		return nil, fmt.Errorf("%w: %v in %s", errParseFoundation, err, raw) //nolint:errorlint // we wrap the error
	}

	// Empty foundation, not RFC 8445 compliant but seen in the wild
	if foundation == "" {
		foundation = " "
	}

	if pos >= len(raw) {
		return nil, fmt.Errorf("%w: expected component in %s", errAttributeTooShortICECandidate, raw)
	}

	// component-id ( 1*5DIGIT )
	component, pos, err := readCandidateDigitToken(raw, pos, 5)
	if err != nil {
		return nil, fmt.Errorf("%w: %v in %s", errParseComponent, err, raw) //nolint:errorlint // we wrap the error
	}

	if pos >= len(raw) {
		return nil, fmt.Errorf("%w: expected transport in %s", errAttributeTooShortICECandidate, raw)
	}

	// transport ( "UDP" / transport-extension ; from RFC 3261 ) SP
	protocol, pos := readCandidateStringToken(raw, pos)

	if pos >= len(raw) {
		return nil, fmt.Errorf("%w: expected priority in %s", errAttributeTooShortICECandidate, raw)
	}

	// priority ( 1*10DIGIT ) SP
	priority, pos, err := readCandidateDigitToken(raw, pos, 10)
	if err != nil {
		return nil, fmt.Errorf("%w: %v in %s", errParsePriority, err, raw) //nolint:errorlint // we wrap the error
	}

	if pos >= len(raw) {
		return nil, fmt.Errorf("%w: expected address in %s", errAttributeTooShortICECandidate, raw)
	}

	// connection-address SP     ;from RFC 4566
	address, pos := readCandidateStringToken(raw, pos)

	// Remove IPv6 ZoneID: https://github.com/pion/ice/pull/704
	address = removeZoneIDFromAddress(address)

	if pos >= len(raw) {
		return nil, fmt.Errorf("%w: expected port in %s", errAttributeTooShortICECandidate, raw)
	}

	// port from RFC 4566
	port, pos, err := readCandidatePort(raw, pos)
	if err != nil {
		return nil, fmt.Errorf("%w: %v in %s", errParsePort, err, raw) //nolint:errorlint // we wrap the error
	}

	// "typ" SP
	typeKey, pos := readCandidateStringToken(raw, pos)
	if typeKey != "typ" {
		return nil, fmt.Errorf("%w (%s)", ErrUnknownCandidateTyp, typeKey)
	}

	if pos >= len(raw) {
		return nil, fmt.Errorf("%w: expected candidate type in %s", errAttributeTooShortICECandidate, raw)
	}

	// SP cand-type ("host" / "srflx" / "prflx" / "relay")
	typ, pos := readCandidateStringToken(raw, pos)

	raddr, rport, pos, err := tryReadRelativeAddrs(raw, pos)
	if err != nil {
		return nil, err
	}

	tcpType := TCPTypeUnspecified
	var extensions []CandidateExtension
	var tcpTypeRaw string

	if pos < len(raw) {
		extensions, tcpTypeRaw, err = unmarshalCandidateExtensions(raw[pos:])
		if err != nil {
			return nil, fmt.Errorf("%w: %v", errParseExtension, err) //nolint:errorlint // we wrap the error
		}

		if tcpTypeRaw != "" {
			tcpType = NewTCPType(tcpTypeRaw)
			if tcpType == TCPTypeUnspecified {
				return nil, fmt.Errorf("%w: invalid or unsupported TCPtype %s", errParseTCPType, tcpTypeRaw)
			}
		}
	}

	// this code is ugly because we can't break backwards compatibility
	// with the old way of parsing candidates
	switch typ {
	case "host":
		candidate, err := NewCandidateHost(&CandidateHostConfig{
			"",
			protocol,
			address,
			port,
			uint16(component), //nolint:gosec // G115 no overflow we read 5 digits
			uint32(priority),  //nolint:gosec // G115 no overflow we read 5 digits
			foundation,
			tcpType,
			false,
		})
		if err != nil {
			return nil, err
		}

		candidate.setExtensions(extensions)

		return candidate, nil
	case "srflx":
		candidate, err := NewCandidateServerReflexive(&CandidateServerReflexiveConfig{
			"",
			protocol,
			address,
			port,
			uint16(component), //nolint:gosec // G115 no overflow we read 5 digits
			uint32(priority),  //nolint:gosec // G115 no overflow we read 5 digits
			foundation,
			raddr,
			rport,
		})
		if err != nil {
			return nil, err
		}

		candidate.setExtensions(extensions)

		return candidate, nil
	case "prflx":
		candidate, err := NewCandidatePeerReflexive(&CandidatePeerReflexiveConfig{
			"",
			protocol,
			address,
			port,
			uint16(component), //nolint:gosec // G115 no overflow we read 5 digits
			uint32(priority),  //nolint:gosec // G115 no overflow we read 5 digits
			foundation,
			raddr,
			rport,
		})
		if err != nil {
			return nil, err
		}

		candidate.setExtensions(extensions)

		return candidate, nil
	case "relay":
		candidate, err := NewCandidateRelay(&CandidateRelayConfig{
			"",
			protocol,
			address,
			port,
			uint16(component), //nolint:gosec // G115 no overflow we read 5 digits
			uint32(priority),  //nolint:gosec // G115 no overflow we read 5 digits
			foundation,
			raddr,
			rport,
			"",
			nil,
		})
		if err != nil {
			return nil, err
		}

		candidate.setExtensions(extensions)

		return candidate, nil
	default:
		return nil, fmt.Errorf("%w (%s)", ErrUnknownCandidateTyp, typ)
	}
}

// Read an ice-char token from the raw string
// ice-char = ALPHA / DIGIT / "+" / "/"
// stop reading when a space is encountered or the end of the string.
func readCandidateCharToken(raw string, start int, limit int) (string, int, error) { //nolint:cyclop
	for i, char := range raw[start:] {
		if char == 0x20 { // SP
			return raw[start : start+i], start + i + 1, nil
		}

		if i == limit {
			//nolint: err113 // handled by caller
			return "", 0, fmt.Errorf("token too long: %s expected 1x%d", raw[start:start+i], limit)
		}

		if (char < 'A' || char > 'Z') &&
			(char < 'a' || char > 'z') &&
			(char < '0' || char > '9') &&
			char != '+' && char != '/' {
			return "", 0, fmt.Errorf("invalid ice-char token: %c", char) //nolint: err113 // handled by caller
		}
	}

	return raw[start:], len(raw), nil
}

// Read an ice string token from the raw string until a space is encountered
// Or the end of the string, we imply that ice string are UTF-8 encoded.
func readCandidateStringToken(raw string, start int) (string, int) {
	for i, char := range raw[start:] {
		if char == 0x20 { // SP
			return raw[start : start+i], start + i + 1
		}
	}

	return raw[start:], len(raw)
}

// Read a digit token from the raw string
// stop reading when a space is encountered or the end of the string.
func readCandidateDigitToken(raw string, start, limit int) (int, int, error) {
	var val int
	for i, char := range raw[start:] {
		if char == 0x20 { // SP
			return val, start + i + 1, nil
		}

		if i == limit {
			//nolint: err113 // handled by caller
			return 0, 0, fmt.Errorf("token too long: %s expected 1x%d", raw[start:start+i], limit)
		}

		if char < '0' || char > '9' {
			return 0, 0, fmt.Errorf("invalid digit token: %c", char) //nolint: err113 // handled by caller
		}

		val = val*10 + int(char-'0')
	}

	return val, len(raw), nil
}

// Read and validate RFC 4566 port from the raw string.
func readCandidatePort(raw string, start int) (int, int, error) {
	port, pos, err := readCandidateDigitToken(raw, start, 5)
	if err != nil {
		return 0, 0, err
	}

	if port > 65535 {
		return 0, 0, fmt.Errorf("invalid RFC 4566 port %d", port) //nolint: err113 // handled by caller
	}

	return port, pos, nil
}

// Read a byte-string token from the raw string
// As defined in RFC 4566  1*(%x01-09/%x0B-0C/%x0E-FF) ;any byte except NUL, CR, or LF
// we imply that extensions byte-string are UTF-8 encoded.
func readCandidateByteString(raw string, start int) (string, int, error) {
	for i, char := range raw[start:] {
		if char == 0x20 { // SP
			return raw[start : start+i], start + i + 1, nil
		}

		// 1*(%x01-09/%x0B-0C/%x0E-FF)
		if (char < 0x01 || char > 0x09) &&
			(char < 0x0B || char > 0x0C) &&
			(char < 0x0E || char > 0xFF) {
			return "", 0, fmt.Errorf("invalid byte-string character: %c", char) //nolint: err113 // handled by caller
		}
	}

	return raw[start:], len(raw), nil
}

// Read and validate raddr and rport from the raw string
// [SP rel-addr] [SP rel-port]
// defined in https://datatracker.ietf.org/doc/html/rfc5245#section-15.1
// .
func tryReadRelativeAddrs(raw string, start int) (raddr string, rport, pos int, err error) {
	key, pos := readCandidateStringToken(raw, start)

	if key != "raddr" {
		return "", 0, start, nil
	}

	if pos >= len(raw) {
		return "", 0, 0, fmt.Errorf("%w: expected raddr value in %s", errParseRelatedAddr, raw)
	}

	raddr, pos = readCandidateStringToken(raw, pos)

	if pos >= len(raw) {
		return "", 0, 0, fmt.Errorf("%w: expected rport in %s", errParseRelatedAddr, raw)
	}

	key, pos = readCandidateStringToken(raw, pos)
	if key != "rport" {
		return "", 0, 0, fmt.Errorf("%w: expected rport in %s", errParseRelatedAddr, raw)
	}

	if pos >= len(raw) {
		return "", 0, 0, fmt.Errorf("%w: expected rport value in %s", errParseRelatedAddr, raw)
	}

	rport, pos, err = readCandidatePort(raw, pos)
	if err != nil {
		return "", 0, 0, fmt.Errorf("%w: %v", errParseRelatedAddr, err) //nolint:errorlint // we wrap the error
	}

	return raddr, rport, pos, nil
}

// UnmarshalCandidateExtensions parses the candidate extensions from the raw string.
// *(SP extension-att-name SP extension-att-value)
// Where extension-att-name, and extension-att-value are byte-strings
// as defined in https://tools.ietf.org/html/rfc5245#section-15.1
func unmarshalCandidateExtensions(raw string) (extensions []CandidateExtension, rawTCPTypeRaw string, err error) {
	extensions = make([]CandidateExtension, 0)

	if raw == "" {
		return extensions, "", nil
	}

	if raw[0] == 0x20 { // SP
		return extensions, "", fmt.Errorf("%w: unexpected space %s", errParseExtension, raw)
	}

	for i := 0; i < len(raw); {
		key, next, err := readCandidateByteString(raw, i)
		if err != nil {
			return extensions, "", fmt.Errorf(
				"%w: failed to read key %v", errParseExtension, err, //nolint: errorlint // we wrap the error
			)
		}
		i = next

		// while not spec-compliant, we allow for empty values, as seen in the wild
		var value string
		if i < len(raw) {
			value, next, err = readCandidateByteString(raw, i)
			if err != nil {
				return extensions, "", fmt.Errorf(
					"%w: failed to read value %v", errParseExtension, err, //nolint: errorlint // we are wrapping the error
				)
			}
			i = next
		}

		if key == "tcptype" {
			rawTCPTypeRaw = value

			continue
		}

		extensions = append(extensions, CandidateExtension{key, value})
	}

	return extensions, rawTCPTypeRaw, nil
}
