package socket

import (
	
	
	
	
	
	
	

	
)

// Transports multiplexes the magic socket's network paths: a direct-IP
// transport plus optional relay and custom transports. It is the Go analog of
// the Rust Transports struct (iroh/src/socket/transports.rs:47).
//
// The IP transport is nil for relay-only endpoints. The relay transport is
// present when the endpoint has relays configured; otherwise relay-addressed
// sends are blackholed (reported as success so quic-go's loss recovery
// retransmits). Custom transports are present only when callers configure them.
type Transports struct {
	ip     *IpTransport
	relay  *RelayTransport
	custom []*customTransport
}

// MagicConn is the single net.PacketConn handed to a quic-go Transport. It
// presents every iroh network path — direct IP, relay, custom — as one UDP
// socket, mapping non-IP paths to synthetic IPv6 ULAs so quic-go can address
// them. It is the Go analog of the Rust `impl AsyncUdpSocket for Transport`
// (iroh/src/socket/transports.rs:1067).
//
// MagicConn satisfies net.PacketConn. It deliberately does not satisfy
// quic-go's OOBCapablePacketConn: GRO and ECN receive metadata do not
// generalize across relay and custom transports. On Linux it exposes a narrower
// send-message method so qng can use GSO for direct IP destinations and split
// the same write for other transports. Correctness does not depend on it.
//
// Create one with [NewMagicConn] and start it with [MagicConn.Serve]. The zero
// value is not usable.
type MagicConn struct {
	sock       *Socket
	transports *Transports
	udp        *net.UDPConn
	localAddr  net.Addr

	recvCh chan recvBatch

	readDeadline  deadline
	writeDeadline deadline

	recvAddrs map[netip.AddrPort]*net.UDPAddr
	metrics   Metrics

	endpointMu     sync.RWMutex
	endpointSender func(key.EndpointID, []byte) bool
}

// NewMagicConn returns a MagicConn whose sole transport is an [IpTransport]
// bound to udp. sock holds the mapped-address tables shared with the transports.
// Start the receive loop with [MagicConn.Serve] before handing the MagicConn to
// a quic-go Transport.
func ( *Socket,  *net.UDPConn) *MagicConn {
	return NewMagicConnWithRelay(, , nil)
}

// NewMagicConnWithRelay returns a MagicConn with an IP transport over udp and,
// if actor is non-nil, a relay transport driven by it. Datagrams received from
// relays surface through [MagicConn.ReadFrom] as a [RelayMappedAddr]; sends to a
// relay mapped address route to the actor. Start the receive loops with
// [MagicConn.Serve].
func ( *Socket,  *net.UDPConn,  *RelayActor) *MagicConn {
	return NewMagicConnWithTransports(, , )
}

// NewMagicConnWithTransports returns a MagicConn with direct IP, optional relay,
// and optional custom transports.
func ( *Socket,  *net.UDPConn,  *RelayActor,  ...CustomTransport) *MagicConn {
	return newMagicConn(, , , ...)
}

// NewMagicConnRelayOnly returns a MagicConn with no direct-IP transport. Relay
// and custom transports are still available. Start the receive loops with
// [MagicConn.Serve].
func ( *Socket,  *RelayActor,  ...CustomTransport) *MagicConn {
	return newMagicConn(, nil, , ...)
}

func newMagicConn( *Socket,  *net.UDPConn,  *RelayActor,  ...CustomTransport) *MagicConn {
	 := make(chan recvBatch, 4)
	 := &Transports{}
	var  net.Addr
	if  != nil {
		.ip = NewIpTransport(, )
		 = .LocalAddr()
	} else {
		 = mappedUDPAddr(NewRelayMappedAddr().Addr())
	}
	if  != nil {
		.relay = NewRelayTransport(, , )
	}
	for ,  := range  {
		if  != nil {
			.custom = append(.custom, newCustomTransport(, ))
		}
	}
	 := &MagicConn{
		sock:       ,
		transports: ,
		udp:        ,
		localAddr:  ,
		recvCh:     ,
		recvAddrs:  make(map[netip.AddrPort]*net.UDPAddr),
	}
	.readDeadline.init()
	.writeDeadline.init()
	if  != nil {
		.setMetrics(&.metrics)
	}
	return 
}

// Relay returns the relay transport, or nil if no relay actor was configured.
func ( *MagicConn) () *RelayTransport { return .transports.relay }

// SetEndpointSender sets the callback used for endpoint-id mapped addresses.
// The callback should route p through the remote endpoint's actor and report
// whether it accepted the datagram. A nil callback restores blackhole behavior.
func ( *MagicConn) ( func(key.EndpointID, []byte) bool) {
	.endpointMu.Lock()
	.endpointSender = 
	.endpointMu.Unlock()
}

// Serve runs the magic socket's receive loops until ctx is cancelled or the
// underlying socket is closed. It blocks; run it in its own goroutine.
func ( *MagicConn) ( context.Context) {
	if .transports.ip == nil {
		for ,  := range .transports.custom {
			go .Serve()
		}
		if .transports.relay != nil {
			.transports.relay.Serve()
			return
		}
		<-.Done()
		return
	}
	if .transports.relay != nil {
		go .transports.relay.Serve()
	}
	for ,  := range .transports.custom {
		go .Serve()
	}
	.transports.ip.Serve()
}

// ReadFrom delivers the next datagram from any transport into p, returning its
// length and the net.Addr quic-go should associate with the path it arrived on.
// For IP paths that addr is the real remote IP; for relay and custom paths it is
// the synthetic mapped IPv6 ULA (port 12345). It implements net.PacketConn.
func ( *MagicConn) ( []byte) (int, net.Addr, error) {
	for {
		select {
		case  := <-.recvCh:
			,  := .recvBatchAddr()
			if ! {
				.release()
				// Unknown relay/custom source: cannot present a stable path to
				// quic-go. Drop and keep reading.
				continue
			}
			.recordRecv(.recvAddr())
			 := copy(, .data)
			.release()
			return , , nil
		case <-.readDeadline.wait():
			return 0, nil, timeoutError{}
		}
	}
}

func ( *MagicConn) ( recvBatch) (net.Addr, bool) {
	if .ip.IsValid() {
		 := .udpAddr(.ip)
		return , true
	}
	,  := .recvAddr(.info)
	return , 
}

// Metrics returns a point-in-time copy of magic-socket counters.
func ( *MagicConn) () MetricsSnapshot {
	if  == nil {
		return MetricsSnapshot{}
	}
	return .metrics.snapshot()
}

// MetricsSet returns the shared magic-socket counter set.
func ( *MagicConn) () *Metrics {
	if  == nil {
		return nil
	}
	return &.metrics
}

// RecordRelayHomeChange increments the relay-home change counter.
func ( *MagicConn) () {
	if  != nil {
		.metrics.relayHomeChange.Add(1)
	}
}

// recvAddr maps a received datagram's RecvInfo to the net.Addr quic-go sees: the
// real IP for an IP path, or the synthetic mapped IPv6 ULA for a relay or custom
// path. It mirrors the Rust recv rewrite in process_datagrams
// (iroh/src/socket.rs:596).
func ( *MagicConn) ( RecvInfo) (net.Addr, bool) {
	switch .Remote.kind {
	case AddrIP:
		,  := .Remote.IP()
		 := .udpAddr()
		return , true
	case AddrRelay:
		, ,  := .Remote.Relay()
		 := .sock.RelayMappedAddrFor(, ).AddrPort()
		 := .udpAddr()
		return , true
	case AddrCustom:
		,  := .Remote.Custom()
		 := .sock.CustomMappedAddrFor().AddrPort()
		 := .udpAddr()
		return , true
	default:
		return nil, false
	}
}

func ( *MagicConn) ( netip.AddrPort) *net.UDPAddr {
	 = canonicalAddrPort()
	if ,  := .recvAddrs[];  {
		return 
	}
	 := udpAddrFromAddrPort()
	.recvAddrs[] = 
	return 
}

// mappedUDPAddr wraps a mapped IPv6 ULA as a *net.UDPAddr at the fixed dummy
// port quic-go uses to address the path.
func mappedUDPAddr( netip.Addr) *net.UDPAddr {
	return udpAddrFromAddrPort(netip.AddrPortFrom(, mappedPort))
}

// WriteTo routes p to the transport addressed by addr and reports success.
//
// addr is classified by [Classify]: a real IP routes to the IP transport; the
// EndpointID, relay, and custom mapped ULAs route to their transports. A send to
// a path with no live transport, an unknown mapped address, or a closed socket
// is blackholed — WriteTo still returns (len(p), nil). quic-go observes the send
// as successful and its loss recovery retransmits the lost datagram, matching
// the Rust Sender::poll_send blackhole invariant
// (iroh/src/socket/transports.rs:1176).
func ( *MagicConn) ( []byte,  net.Addr) (int, error) {
	if .sock.IsClosed() {
		return len(), nil
	}
	if ,  := .(*net.UDPAddr);  {
		 := .AddrPort()
		if isDefinitelyIP(.Addr()) || Classify(.Addr()) == KindIP {
			if .transports.ip == nil {
				.metrics.blackholed.Add(1)
				return len(), nil
			}
			if ,  := .transports.ip.send(, );  == nil {
				.recordIPSent()
			} else {
				.metrics.blackholed.Add(1)
			}
			return len(), nil
		}
	}
	,  := addrPort()
	if ! {
		return len(), nil
	}
	switch Classify(.Addr()) {
	case KindIP:
		.sendAddr(IPAddr(), )
		return len(), nil
	case KindEndpointID:
		if ,  := .sock.LookupEndpointID(EndpointIDMappedAddrFromAddr(.Addr()));  {
			.endpointMu.RLock()
			 := .endpointSender
			.endpointMu.RUnlock()
			if  != nil {
				if (, ) {
					.metrics.endpointIDSent.Add(1)
				} else {
					.metrics.blackholed.Add(1)
				}
			} else {
				.metrics.blackholed.Add(1)
			}
		} else {
			.metrics.blackholed.Add(1)
		}
		return len(), nil
	case KindRelay:
		if ,  := relayAddrForMapped(.sock, .Addr());  {
			.sendAddr(, )
		} else {
			.metrics.blackholed.Add(1)
		}
		return len(), nil
	case KindCustom:
		if ,  := .sock.LookupCustom(CustomMappedAddr{a: .Addr()});  {
			.sendAddr(CustomAddr(), )
		} else {
			.metrics.blackholed.Add(1)
		}
		return len(), nil
	default:
		.metrics.blackholed.Add(1)
		return len(), nil
	}
}

func isDefinitelyIP( netip.Addr) bool {
	if !.Is6() {
		return true
	}
	return .As16()[0] != 0xfd
}

// relayAddrForMapped returns the relay Addr for mapped.
func relayAddrForMapped( *Socket,  netip.Addr) (Addr, bool) {
	if ,  := .LookupRelay(RelayMappedAddrFromAddr());  {
		return RelayAddr(.URL, .EID), true
	}
	return Addr{}, false
}

// sendAddr routes p to one concrete transport address. It reports whether the
// datagram was accepted by a transport. Errors are loss, not socket failures.
func ( *MagicConn) ( Addr,  []byte) bool {
	switch .Kind() {
	case AddrIP:
		,  := .IP()
		if !.IsValid() || .Port() == 0 {
			.metrics.blackholed.Add(1)
			return false
		}
		if .transports.ip == nil {
			.metrics.blackholed.Add(1)
			return false
		}
		,  := .transports.ip.send(, )
		if  == nil {
			.recordIPSent()
			return true
		}
		.metrics.blackholed.Add(1)
		return false
	case AddrRelay:
		if .transports.relay == nil {
			.metrics.blackholed.Add(1)
			return false
		}
		, ,  := .Relay()
		 := .sock.RelayMappedAddrFor(, )
		if .transports.relay.Send(, ) {
			.metrics.relaySent.Add(1)
			return true
		}
		.metrics.blackholed.Add(1)
		return false
	case AddrCustom:
		,  := .Custom()
		for ,  := range .transports.custom {
			if .Send(, nil, ) {
				.metrics.customSent.Add(1)
				return true
			}
		}
		.metrics.blackholed.Add(1)
		return false
	default:
		.metrics.blackholed.Add(1)
		return false
	}
}

// SendAddr routes p to one concrete magic-socket transport address. It is used
// by RemoteStateActor endpoint-id fanout.
func ( *MagicConn) ( Addr,  []byte) bool {
	if .sock.IsClosed() {
		.metrics.blackholed.Add(1)
		return false
	}
	return .sendAddr(, )
}

func ( *MagicConn) ( Addr) {
	.metrics.recvDatagrams.Add(1)
	switch .Kind() {
	case AddrIP:
		,  := .IP()
		if .Addr().Is4() {
			.metrics.ipv4Recv.Add(1)
		} else {
			.metrics.ipv6Recv.Add(1)
		}
	case AddrRelay:
		.metrics.relayRecv.Add(1)
	case AddrCustom:
		.metrics.customRecv.Add(1)
	}
}

func ( *MagicConn) ( netip.AddrPort) {
	if .Addr().Is4() {
		.metrics.ipv4Sent.Add(1)
	} else {
		.metrics.ipv6Sent.Add(1)
	}
}

// LocalAddr returns the bound local address of the underlying UDP socket. It
// implements net.PacketConn.
func ( *MagicConn) () net.Addr { return .localAddr }

// Close releases the magic socket. It marks the shared [Socket] closed and
// closes the underlying UDP socket, which ends the receive loop. It implements
// net.PacketConn.
func ( *MagicConn) () error {
	.sock.Close()
	.readDeadline.set(time.Unix(0, 1))
	if .udp == nil {
		return nil
	}
	return .udp.Close()
}

// SetDeadline sets both the read and write deadlines. It implements
// net.PacketConn.
func ( *MagicConn) ( time.Time) error {
	.readDeadline.set()
	if .udp == nil {
		return nil
	}
	return .udp.SetWriteDeadline()
}

// SetReadDeadline sets the deadline for future ReadFrom calls. It implements
// net.PacketConn.
func ( *MagicConn) ( time.Time) error {
	.readDeadline.set()
	return nil
}

// SetWriteDeadline sets the deadline for future WriteTo calls. Writes go
// straight to the underlying socket, so the deadline is applied there. It
// implements net.PacketConn.
func ( *MagicConn) ( time.Time) error {
	if .udp == nil {
		return nil
	}
	return .udp.SetWriteDeadline()
}

// SyscallConn returns the underlying UDP socket's raw connection. quic-go uses
// it to size the kernel receive buffer and to set the Don't Fragment bit on the
// direct-IP path. Exposing it does not make MagicConn an OOBCapablePacketConn.
// On Linux qng combines it with MagicConn's send-message method for send-side
// GSO only.
func ( *MagicConn) () (syscall.RawConn, error) {
	if .udp == nil {
		return nil, errors.ErrUnsupported
	}
	return .udp.SyscallConn()
}

// SetReadBuffer sets the kernel receive buffer size on the underlying UDP
// socket. quic-go calls it to raise the buffer to its desired size.
func ( *MagicConn) ( int) error {
	if .udp == nil {
		return nil
	}
	return .udp.SetReadBuffer()
}

// SetWriteBuffer sets the kernel send buffer size on the underlying UDP socket.
func ( *MagicConn) ( int) error {
	if .udp == nil {
		return nil
	}
	return .udp.SetWriteBuffer()
}

var _ net.PacketConn = (*MagicConn)(nil)