//go:build linux

package bindtodevice

import (
	"fmt"
	"net"
	"syscall"

	"github.com/AdguardTeam/golibs/errors"
	"golang.org/x/sys/unix"
)

// setSockOptFunc is a function that sets a socket option on fd.
type setSockOptFunc func(fd int) (err error)

// newIntSetSockOptFunc returns an integer socket-option function with the given
// parameters.
func newIntSetSockOptFunc(name string, lvl, opt, val int) (o setSockOptFunc) {
	return func(fd int) (err error) {
		opErr := unix.SetsockoptInt(fd, lvl, opt, val)

		return errors.Annotate(opErr, "setting %s: %w", name)
	}
}

// newStringSetSockOptFunc returns a string socket-option function with the
// given parameters.
func newStringSetSockOptFunc(name string, lvl, opt int, val string) (o setSockOptFunc) {
	return func(fd int) (err error) {
		opErr := unix.SetsockoptString(fd, lvl, opt, val)

		return errors.Annotate(opErr, "setting %s: %w", name)
	}
}

// newListenConfig returns a [net.ListenConfig] that can bind to a network
// interface (device) by its name.  ctrlConf must not be nil.
func newListenConfig(devName string, ctrlConf *ControlConfig) (lc *net.ListenConfig) {
	return &net.ListenConfig{
		Control: func(network, address string, c syscall.RawConn) (err error) {
			return listenControlWithSO(ctrlConf, devName, network, address, c)
		},
	}
}

// listenControlWithSO is used as a [net.ListenConfig.Control] function to set
// additional socket options.
func listenControlWithSO(
	ctrlConf *ControlConfig,
	devName string,
	network string,
	_ string,
	c syscall.RawConn,
) (err error) {
	opts := []setSockOptFunc{
		newStringSetSockOptFunc("SO_BINDTODEVICE", unix.SOL_SOCKET, unix.SO_BINDTODEVICE, devName),
		// Use SO_REUSEADDR as well, which is not technically necessary, to
		// help with the situation of sockets hanging in CLOSE_WAIT for too
		// long.
		newIntSetSockOptFunc("SO_REUSEADDR", unix.SOL_SOCKET, unix.SO_REUSEADDR, 1),
		newIntSetSockOptFunc("SO_REUSEPORT", unix.SOL_SOCKET, unix.SO_REUSEPORT, 1),
	}

	switch network {
	case "tcp", "tcp4", "tcp6":
		// Socket options for TCP connection already set.  Go on.
	case "udp", "udp4", "udp6":
		opts = append(
			opts,
			newIntSetSockOptFunc("IP_RECVORIGDSTADDR", unix.IPPROTO_IP, unix.IP_RECVORIGDSTADDR, 1),
			newIntSetSockOptFunc("IP_FREEBIND", unix.IPPROTO_IP, unix.IP_FREEBIND, 1),
			newIntSetSockOptFunc(
				"IPV6_RECVORIGDSTADDR",
				unix.IPPROTO_IPV6,
				unix.IPV6_RECVORIGDSTADDR,
				1,
			),
			newIntSetSockOptFunc("IPV6_FREEBIND", unix.IPPROTO_IPV6, unix.IPV6_FREEBIND, 1),
		)
	default:
		return fmt.Errorf("bad network %q", network)
	}

	if ctrlConf.SndBufSize > 0 {
		opts = append(
			opts,
			newIntSetSockOptFunc("SO_SNDBUF", unix.SOL_SOCKET, unix.SO_SNDBUF, ctrlConf.SndBufSize),
		)
	}

	if ctrlConf.RcvBufSize > 0 {
		opts = append(
			opts,
			newIntSetSockOptFunc("SO_RCVBUF", unix.SOL_SOCKET, unix.SO_RCVBUF, ctrlConf.RcvBufSize),
		)
	}

	var opErr error
	err = c.Control(func(fd uintptr) {
		d := int(fd)
		for _, opt := range opts {
			opErr = opt(d)
			if opErr != nil {
				return
			}
		}
	})

	return errors.WithDeferred(opErr, err)
}

// msgUDPReader is an interface for types of connections that can read UDP
// messages.  See [*net.UDPConn].
type msgUDPReader interface {
	ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error)
}

// readPacketSession is a helper that reads a packet-session data from a UDP
// connection.
func readPacketSession(c msgUDPReader, body, oob []byte) (sess *packetSession, err error) {
	n, oobn, _, raddr, err := c.ReadMsgUDP(body, oob)
	if err != nil {
		return nil, fmt.Errorf("reading: %w", err)
	}

	var ctrlMsgs []unix.SocketControlMessage
	ctrlMsgs, err = unix.ParseSocketControlMessage(oob[:oobn])
	if err != nil {
		return nil, fmt.Errorf("parsing ctrl messages: %w", err)
	}

	if l := len(ctrlMsgs); l != 1 {
		return nil, fmt.Errorf("expected 1 ctrl message, got %d", l)
	}

	ctrlMsg := ctrlMsgs[0]
	origDstSockAddr, err := unix.ParseOrigDstAddr(&ctrlMsg)
	if err != nil {
		return nil, fmt.Errorf("parsing orig dst: %w", err)
	}

	origDstAddr, respOOB, err := sockAddrData(origDstSockAddr)
	if err != nil {
		return nil, err
	}

	sess = &packetSession{
		laddr:    origDstAddr,
		raddr:    raddr,
		readBody: body[:n],
		respOOB:  respOOB,
	}

	return sess, nil
}

// sockAddrData converts the provided socket address into a UDP address as well
// as encodes the response packet information.
func sockAddrData(sockAddr unix.Sockaddr) (origDstAddr *net.UDPAddr, respOOB []byte, err error) {
	switch sockAddr := sockAddr.(type) {
	case *unix.SockaddrInet4:
		origDstAddr = &net.UDPAddr{
			IP:   sockAddr.Addr[:],
			Port: sockAddr.Port,
		}

		// Set both addresses to make sure that users receive the correct source
		// IP address even when virtual interfaces are involved.
		pktInfo := &unix.Inet4Pktinfo{
			Addr:     sockAddr.Addr,
			Spec_dst: sockAddr.Addr,
		}

		respOOB = unix.PktInfo4(pktInfo)
	case *unix.SockaddrInet6:
		origDstAddr = &net.UDPAddr{
			IP:   sockAddr.Addr[:],
			Port: sockAddr.Port,
		}

		pktInfo := &unix.Inet6Pktinfo{
			Addr:    sockAddr.Addr,
			Ifindex: sockAddr.ZoneId,
		}

		respOOB = unix.PktInfo6(pktInfo)
	default:
		return nil, nil, fmt.Errorf("bad orig dst sockaddr type %T", sockAddr)
	}

	return origDstAddr, respOOB, nil
}
