//go:build unix

package ping

import (
	"net"
	"net/netip"
	"os"
	"runtime"
	"syscall"

	"github.com/sagernet/sing/common/bufio"
	"github.com/sagernet/sing/common/control"
	E "github.com/sagernet/sing/common/exceptions"
	M "github.com/sagernet/sing/common/metadata"

	"golang.org/x/sys/unix"
)

func connect(privileged bool, controlFunc control.Func, destination netip.Addr) (net.Conn, error) {
	var (
		network string
		fd      int
		err     error
	)
	if destination.Is4() {
		network = "ip4" // like std's netFD.ctrlNetwork
		if !privileged {
			fd, err = unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_ICMP)
		} else {
			fd, err = unix.Socket(unix.AF_INET, unix.SOCK_RAW, unix.IPPROTO_ICMP)
		}
	} else {
		network = "ip6" // like std's netFD.ctrlNetwork
		if !privileged {
			fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_ICMPV6)
		} else {
			fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_RAW, unix.IPPROTO_ICMPV6)
		}
	}
	if err != nil {
		return nil, E.Cause(err, "socket()")
	}
	file := os.NewFile(uintptr(fd), "datagram-oriented icmp")
	defer file.Close()
	if controlFunc != nil {
		var syscallConn syscall.RawConn
		syscallConn, err = file.SyscallConn()
		if err != nil {
			return nil, err
		}
		err = controlFunc(network, destination.String(), syscallConn)
		if err != nil {
			return nil, err
		}
	}
	if destination.Is4() && (runtime.GOOS == "linux" || runtime.GOOS == "android") {
		//err = unix.SetsockoptInt(fd, unix.IPPROTO_IP, unix.IP_RECVTOS, 1)
		//if err != nil {
		//	return nil, err
		//}
		err = unix.SetsockoptInt(fd, unix.IPPROTO_IP, unix.IP_RECVTTL, 1)
		if err != nil {
			return nil, E.Cause(err, "setsockopt()")
		}
	}
	if destination.Is6() {
		err = unix.SetsockoptInt(fd, unix.IPPROTO_IPV6, unix.IPV6_RECVHOPLIMIT, 1)
		if err != nil {
			return nil, E.Cause(err, "setsockopt()")
		}
		err = unix.SetsockoptInt(fd, unix.IPPROTO_IPV6, unix.IPV6_RECVTCLASS, 1)
		if err != nil {
			return nil, E.Cause(err, "setsockopt()")
		}
	}

	var bindAddress netip.Addr
	if !destination.Is6() {
		bindAddress = netip.AddrFrom4([4]byte{})
	} else {
		bindAddress = netip.AddrFrom16([16]byte{})
	}

	err = unix.Bind(fd, M.AddrPortToSockaddr(netip.AddrPortFrom(bindAddress, 0)))
	if err != nil {
		return nil, err
	}

	useUnconnected := (runtime.GOOS == "darwin" && !privileged) ||
		((runtime.GOOS == "linux" || runtime.GOOS == "android") && privileged)
	if useUnconnected {
		// A connected ICMP socket only receives messages whose source is the connected peer,
		// so the Time Exceeded replies that transit routers send for traceroute never reach it.
		// Additionally, on macOS NetworkExtension, writing to a connected socket returns EPIPE.
		var packetConn net.PacketConn
		packetConn, err = net.FilePacketConn(file)
		if err != nil {
			return nil, err
		}
		var writeTarget net.Addr
		if privileged {
			writeTarget = M.SocksaddrFrom(destination, 0).IPAddr()
		} else {
			writeTarget = M.SocksaddrFrom(destination, 0).UDPAddr()
		}
		return bufio.NewBindPacketConn(packetConn, writeTarget), nil
	}
	err = unix.Connect(fd, M.AddrPortToSockaddr(netip.AddrPortFrom(destination, 0)))
	if err != nil {
		return nil, err
	}
	var conn net.Conn
	conn, err = net.FileConn(file)
	if err != nil {
		return nil, err
	}
	return conn, nil
}
