package gvisorstack

import (
	"context"
	"fmt"

	"gvisor.dev/gvisor/pkg/tcpip"
	"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
	"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
	"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"

	"github.com/v2fly/v2ray-core/v5/common/dualStack/fusedPacketConn"
	"github.com/v2fly/v2ray-core/v5/common/net"
)

// DialTCP will create a connection to the given destination, using the stack.
// Machine Generated
func (w *WrappedStack) DialTCP(ctx context.Context, remoteAddress net.Destination) (net.Conn, error) {
	if w == nil || w.stack == nil {
		return nil, fmt.Errorf("gvisor stack not initialized")
	}

	if remoteAddress.Network != net.Network_TCP {
		return nil, fmt.Errorf("destination is not tcp: %v", remoteAddress.Network)
	}

	// Resolve address to IP if necessary.
	var ipBytes []byte
	switch remoteAddress.Address.Family() {
	case net.AddressFamilyIPv4:
		ipBytes = remoteAddress.Address.IP().To4()
	case net.AddressFamilyIPv6:
		ipBytes = remoteAddress.Address.IP().To16()
	case net.AddressFamilyDomain:
		// Do not resolve domain names here. Return explicit error.
		return nil, fmt.Errorf("domain address not supported for gVisor dial: %s", remoteAddress.Address.String())
	default:
		return nil, fmt.Errorf("unsupported address family: %v", remoteAddress.Address.Family())
	}

	if ipBytes == nil {
		return nil, fmt.Errorf("failed to obtain IP bytes for %v", remoteAddress.Address)
	}

	// Choose network protocol number based on IP length.
	netProto := ipv4.ProtocolNumber
	if len(ipBytes) == 16 {
		netProto = ipv6.ProtocolNumber
	}

	remote := tcpip.FullAddress{
		Addr: tcpip.AddrFromSlice(ipBytes),
		Port: uint16(remoteAddress.Port),
	}

	// Use gonet dialer to create a TCP connection on the in-memory stack.
	conn, err := gonet.DialContextTCP(ctx, w.stack, remote, netProto)
	if err != nil {
		return nil, err
	}
	return conn, nil
}

// ListenUDP will create a connection to the given destination, using the stack.
// Machine Generated
func (w *WrappedStack) ListenUDP(ctx context.Context, localAddress net.Destination) (net.PacketConn, error) {
	// allow ctx to be accepted by the function signature
	_ = ctx

	if w == nil || w.stack == nil {
		return nil, fmt.Errorf("gvisor stack not initialized")
	}

	if localAddress.Network != net.Network_UDP {
		return nil, fmt.Errorf("destination is not udp: %v", localAddress.Network)
	}

	// Determine local address bytes.
	var ipBytes []byte
	specified := false
	if localAddress.Address == nil {
		// If address is nil, treat as unspecified (zero) address.
		specified = false
	} else {
		switch localAddress.Address.Family() {
		case net.AddressFamilyIPv4:
			specified = true
			ipBytes = localAddress.Address.IP().To4()
		case net.AddressFamilyIPv6:
			specified = true
			ipBytes = localAddress.Address.IP().To16()
		case net.AddressFamilyDomain:
			// Listening on a domain name is not supported.
			return nil, fmt.Errorf("listening on domain address not supported: %s", localAddress.Address.String())
		default:
			// If unspecified (zero) address, allow kernel (stack) to choose.
			specified = false
		}
	}

	var laddr *tcpip.FullAddress
	if specified {
		if ipBytes == nil {
			return nil, fmt.Errorf("failed to obtain IP bytes for %v", localAddress.Address)
		}
		netProto := ipv4.ProtocolNumber
		if len(ipBytes) == 16 {
			netProto = ipv6.ProtocolNumber
		}
		l := tcpip.FullAddress{Addr: tcpip.AddrFromSlice(ipBytes), Port: uint16(localAddress.Port)}
		laddr = &l
		// Create UDP endpoint bound to local address.
		udpConn, err := gonet.DialUDP(w.stack, laddr, nil, netProto)
		if err != nil {
			return nil, err
		}
		return udpConn, nil
	}

	if w.config.DualStackUdp {
		udpConn4, err := gonet.DialUDP(w.stack, nil, nil, ipv4.ProtocolNumber)
		if err != nil {
			return nil, fmt.Errorf("failed to create IPv4 UDP conn for dual stack: %w", err)
		}
		udpConn6, err := gonet.DialUDP(w.stack, nil, nil, ipv6.ProtocolNumber)
		if err != nil {
			udpConn4.Close()
			return nil, fmt.Errorf("failed to create IPv6 UDP conn for dual stack: %w", err)
		}
		preferIPv6 := w.config.GetPreferIpv6ForUdp()
		return fusedPacketConn.NewFusedPacketConn(udpConn4, udpConn6, int(w.config.Mtu), preferIPv6), nil
	}

	// If not specified, let the stack choose the local address (pass nil laddr).
	// Default network selection honors PreferIpv6ForUdp if configured.
	defaultNet := ipv4.ProtocolNumber
	if w.config != nil && w.config.GetPreferIpv6ForUdp() {
		defaultNet = ipv6.ProtocolNumber
	}
	udpConn, err := gonet.DialUDP(w.stack, nil, nil, defaultNet)
	if err != nil {
		return nil, err
	}
	return udpConn, nil
}
