package netaddr

import (
	
	
	
	
	
	
	
	
	
	

	
)

// TransportAddr is a network-level address at which an endpoint may be reached.
// It is one of [RelayAddr], [IPAddr], or [CustomAddr].
//
// The interface is closed: only the implementations in this package satisfy it.
type TransportAddr interface {
	// Network returns the transport kind: "relay", "ip", or "custom".
	Network() string
	// String renders the address in its "kind:value" form, e.g. "ip:127.0.0.1:9".
	String() string
	// Compare returns -1, 0, or +1 ordering this address against other. The
	// order matches the Rust reference's derived Ord on the TransportAddr enum:
	// by kind first (relay < ip < custom), then by value (relay URLs by their
	// normalized string, IP addresses numerically, custom by id then data).
	Compare(other TransportAddr) int
	isTransportAddr()
}

// transportKind is the kind ordinal used as the primary ordering key, matching
// the Rust enum variant order: Relay(0) < Ip(1) < Custom(2).
func transportKind( TransportAddr) int {
	switch .(type) {
	case RelayAddr:
		return 0
	case IPAddr:
		return 1
	case CustomAddr:
		return 2
	default:
		return 3
	}
}

// RelayAddr is a [TransportAddr] reachable via a relay server.
type RelayAddr struct{ URL RelayURL }

// IPAddr is a [TransportAddr] reachable at an IP socket address.
type IPAddr struct{ Addr netip.AddrPort }

// CustomAddr is a custom transport address: a freely-chosen u64 transport id
// plus opaque, unvalidated address data.
//
// A registry of well-known transport ids is at
// https://github.com/n0-computer/iroh/blob/main/TRANSPORTS.md.
//
// CustomAddr mirrors upstream iroh's experimental custom-transport address
// surface, which upstream excludes from its stability guarantees. The Go API
// below follows this module's normal compatibility policy, but the
// endpoint-ticket wire encoding of a CustomAddr may change to track upstream
// without a major go-iroh version bump.
//
// String encoding ([CustomAddr.String], [ParseCustomAddr]): "<id>_<data>" where
// <id> is the transport ID as lowercase hex (no "0x", no leading zeros) and
// <data> is the address bytes as lowercase hex.
//
// Binary encoding ([CustomAddr.MarshalBinary], [CustomAddr.UnmarshalBinary]):
// 8-byte little-endian u64 ID followed by the raw data bytes (minimum 8 bytes).
//
// CustomAddr mirrors upstream iroh's experimental custom-transport address
// surface, which upstream excludes from its stability guarantees. The Go API
// below follows this module's normal compatibility policy, but the
// endpoint-ticket wire encoding of a CustomAddr may change to track upstream
// without a major go-iroh version bump.
type CustomAddr struct {
	id   uint64
	data []byte
}

func (RelayAddr) ()  {}
func (IPAddr) ()     {}
func (CustomAddr) () {}

func (RelayAddr) () string  { return "relay" }
func (IPAddr) () string     { return "ip" }
func (CustomAddr) () string { return "custom" }

func ( RelayAddr) () string  { return "relay:" + .URL.String() }
func ( IPAddr) () string     { return "ip:" + .Addr.String() }
func ( CustomAddr) () string { return .customString() }

// MarshalText implements encoding.TextMarshaler using the string encoding
// described on [TransportAddr].
func ( RelayAddr) () ([]byte, error) {
	return []byte(.String()), nil
}

// UnmarshalText implements encoding.TextUnmarshaler using the string encoding
// described on [TransportAddr].
func ( *RelayAddr) ( []byte) error {
	,  := ParseTransportAddr(string())
	if  != nil {
		return 
	}
	,  := .(RelayAddr)
	if ! {
		return fmt.Errorf("transport address %q: got %T, want RelayAddr", , )
	}
	* = 
	return nil
}

// MarshalText implements encoding.TextMarshaler using the string encoding
// described on [TransportAddr].
func ( IPAddr) () ([]byte, error) {
	return []byte(.String()), nil
}

// UnmarshalText implements encoding.TextUnmarshaler using the string encoding
// described on [TransportAddr].
func ( *IPAddr) ( []byte) error {
	,  := ParseTransportAddr(string())
	if  != nil {
		return 
	}
	,  := .(IPAddr)
	if ! {
		return fmt.Errorf("transport address %q: got %T, want IPAddr", , )
	}
	* = 
	return nil
}

// Compare orders relay addresses by their normalized URL string.
func ( RelayAddr) ( TransportAddr) int {
	if ,  := .(RelayAddr);  {
		return .URL.Compare(.URL)
	}
	return cmp.Compare(transportKind(), transportKind())
}

// Compare orders IP addresses numerically (by [netip.AddrPort.Compare]).
func ( IPAddr) ( TransportAddr) int {
	if ,  := .(IPAddr);  {
		return .Addr.Compare(.Addr)
	}
	return cmp.Compare(transportKind(), transportKind())
}

// Compare orders custom addresses by numeric transport id, then by data bytes.
func ( CustomAddr) ( TransportAddr) int {
	if ,  := .(CustomAddr);  {
		if  := cmp.Compare(.id, .id);  != 0 {
			return 
		}
		return bytes.Compare(.data, .data)
	}
	return cmp.Compare(transportKind(), transportKind())
}

// NewCustomAddr creates a CustomAddr from a transport ID and raw address data.
// The data is copied.
func ( uint64,  []byte) CustomAddr {
	return CustomAddr{id: , data: slices.Clone()}
}

// ID returns the transport ID.
func ( CustomAddr) () uint64 { return .id }

// Data returns the opaque address data.
func ( CustomAddr) () []byte { return slices.Clone(.data) }

func ( CustomAddr) () string {
	return strconv.FormatUint(.id, 16) + "_" + hex.EncodeToString(.data)
}

// CustomAddr parse/encode errors.
var (
	ErrCustomAddrMissingSeparator = errors.New("missing '_' separator")
	ErrCustomAddrInvalidID        = errors.New("invalid ID")
	ErrCustomAddrInvalidData      = errors.New("invalid data")
	ErrCustomAddrTooShort         = errors.New("data too short")
)

// ParseCustomAddr parses a CustomAddr from its "<id>_<data>" string form.
// It also accepts the "custom:" prefix used by [ParseTransportAddr].
func ( string) (CustomAddr, error) {
	 = strings.TrimPrefix(, "custom:")
	, ,  := strings.Cut(, "_")
	if ! {
		return CustomAddr{}, ErrCustomAddrMissingSeparator
	}
	,  := strconv.ParseUint(, 16, 64)
	if  != nil {
		return CustomAddr{}, ErrCustomAddrInvalidID
	}
	,  := hex.DecodeString()
	if  != nil {
		return CustomAddr{}, ErrCustomAddrInvalidData
	}
	return NewCustomAddr(, ), nil
}

// MarshalText implements encoding.TextMarshaler using the string encoding
// described on [CustomAddr].
func ( CustomAddr) () ([]byte, error) {
	return []byte(.String()), nil
}

// UnmarshalText implements encoding.TextUnmarshaler using the string encoding
// described on [CustomAddr].
func ( *CustomAddr) ( []byte) error {
	,  := ParseCustomAddr(string())
	if  != nil {
		return 
	}
	* = 
	return nil
}

// MarshalBinary implements encoding.BinaryMarshaler using the binary encoding
// described on [CustomAddr].
func ( CustomAddr) () ([]byte, error) {
	 := make([]byte, 8+len(.data))
	binary.LittleEndian.PutUint64([:8], .id)
	copy([8:], .data)
	return , nil
}

// UnmarshalBinary implements encoding.BinaryUnmarshaler using the binary
// encoding described on [CustomAddr].
func ( *CustomAddr) ( []byte) error {
	if len() < 8 {
		return ErrCustomAddrTooShort
	}
	.id = binary.LittleEndian.Uint64([:8])
	.data = slices.Clone([8:])
	return nil
}

// ParseTransportAddr parses a TransportAddr from its "kind:value" string form.
func ( string) (TransportAddr, error) {
	, ,  := strings.Cut(, ":")
	if ! {
		return ParseCustomAddr()
	}
	switch  {
	case "relay":
		,  := ParseRelayURL()
		if  != nil {
			return nil, 
		}
		return RelayAddr{URL: }, nil
	case "ip":
		,  := netip.ParseAddrPort()
		if  != nil {
			return nil, fmt.Errorf("transport address %q: %w", , )
		}
		return IPAddr{Addr: }, nil
	case "custom":
		return ParseCustomAddr()
	default:
		return nil, fmt.Errorf("transport address %q: unknown kind %q", , )
	}
}

// EndpointAddr combines an endpoint's [key.EndpointID] with the network-level
// addresses at which it may be reached.
//
// To establish a connection both the key.EndpointID and at least one path (a relay
// URL or a direct IP address) are needed; an EndpointAddr with no addresses is
// still usable together with an address-lookup service.
type EndpointAddr struct {
	// ID is the endpoint's identifier.
	ID key.EndpointID
	// addrs is the sorted, deduplicated set of transport addresses.
	addrs []TransportAddr
}

// NewEndpointAddr creates an EndpointAddr with the given id and transport
// addresses. Addresses are deduplicated and sorted.
func ( key.EndpointID,  ...TransportAddr) EndpointAddr {
	 := EndpointAddr{ID: }
	return .WithAddrs(...)
}

// WithRelayURL returns a copy of a with the given relay URL added.
func ( EndpointAddr) ( RelayURL) EndpointAddr {
	return .WithAddrs(RelayAddr{URL: })
}

// WithIP returns a copy of a with the given IP address added.
func ( EndpointAddr) ( netip.AddrPort) EndpointAddr {
	return .WithAddrs(IPAddr{Addr: })
}

// WithAddrs returns a copy of a with the given addresses added. The result's
// address set is sorted and deduplicated.
func ( EndpointAddr) ( ...TransportAddr) EndpointAddr {
	 := append(slices.Clone(.addrs), ...)
	 = sortDedupAddrs()
	return EndpointAddr{ID: .ID, addrs: }
}

// Addrs returns the sorted, deduplicated transport addresses.
func ( EndpointAddr) () []TransportAddr { return slices.Clone(.addrs) }

// IsEmpty reports whether only the key.EndpointID is present.
func ( EndpointAddr) () bool { return len(.addrs) == 0 }

// String returns a diagnostic string for a.
func ( EndpointAddr) () string {
	var  strings.Builder
	.WriteString("EndpointAddr{id:")
	.WriteString(.ID.String())
	.WriteString(", addrs:[")
	for ,  := range .addrs {
		if  > 0 {
			.WriteString(", ")
		}
		.WriteString(.String())
	}
	.WriteString("]}")
	return .String()
}

// IPAddrs returns the IP socket addresses of this endpoint.
func ( EndpointAddr) () []netip.AddrPort {
	var  []netip.AddrPort
	for ,  := range .addrs {
		if ,  := .(IPAddr);  {
			 = append(, .Addr)
		}
	}
	return 
}

// RelayURLs returns the relay URLs of this endpoint. In practice this is
// expected to be zero or one home relay.
func ( EndpointAddr) () []RelayURL {
	var  []RelayURL
	for ,  := range .addrs {
		if ,  := .(RelayAddr);  {
			 = append(, .URL)
		}
	}
	return 
}

func sortDedupAddrs( []TransportAddr) []TransportAddr {
	slices.SortFunc(, TransportAddr.Compare)
	return slices.CompactFunc(, func(,  TransportAddr) bool {
		return .Compare() == 0
	})
}