package netlink

import (
	"context"
	"syscall"
	"unsafe"
)

// RouteConn represents a NETLINK_ROUTE socket.
type RouteConn struct{ conn *Conn }

// Close closes the underlying socket.
func (c *RouteConn) Close() error { return c.conn.Close() }

// DialRoute returns the address of a newly connected [RouteConn].
func DialRoute(rcvbuf int64) (*RouteConn, error) {
	c, err := Dial(syscall.NETLINK_ROUTE, 0, rcvbuf)
	if err != nil {
		return nil, err
	}
	return &RouteConn{c}, nil
}

// rtnlConsume consumes a message from rtnetlink.
func (c *RouteConn) rtnlConsume(resp []syscall.NetlinkMessage) error {
	for i := range resp {
		if err := c.conn.checkReply(&resp[i].Header); err != nil {
			return err
		}

		switch resp[i].Header.Type {
		case syscall.NLMSG_DONE:
			return Complete{}

		case syscall.NLMSG_ERROR:
			if e := As[syscall.NlMsgerr](resp[i].Data); e != nil {
				if e.Error == 0 {
					return Complete{}
				}
				return syscall.Errno(-e.Error)
			}
			return syscall.EBADE
		}
	}
	return nil
}

// InAddr is equivalent to struct in_addr.
type InAddr [4]byte

// RtAttrMsg holds syscall.RtAttr alongside its payload.
type RtAttrMsg[D any] struct {
	syscall.RtAttr
	Data D
}

// populate populates the Len field of the embedded syscall.RtAttr.
func (attr *RtAttrMsg[M]) populate() {
	attr.Len = syscall.SizeofRtAttr + uint16(unsafe.Sizeof(attr.Data))
}

// writeIfAddrmsg writes an ifaddrmsg structure to conn.
func (c *RouteConn) writeIfAddrmsg(
	typ, flags uint16,
	msg *syscall.IfAddrmsg,
	attrs ...RtAttrMsg[InAddr],
) bool {
	c.conn.typ, c.conn.flags = typ, syscall.NLM_F_REQUEST|syscall.NLM_F_ACK|flags
	if !add(c.conn, msg) {
		return false
	}
	for _, attr := range attrs {
		attr.populate()
		if !add(c.conn, &attr) {
			return false
		}
	}
	return true
}

// SendIfAddrmsg sends an ifaddrmsg structure to rtnetlink.
func (c *RouteConn) SendIfAddrmsg(
	ctx context.Context,
	typ, flags uint16,
	msg *syscall.IfAddrmsg,
	attrs ...RtAttrMsg[InAddr],
) error {
	if !c.writeIfAddrmsg(typ, flags, msg, attrs...) {
		return syscall.ENOMEM
	}
	return c.conn.Roundtrip(ctx, c.rtnlConsume)
}

// writeNewaddrLo writes a RTM_NEWADDR message for the loopback address.
func (c *RouteConn) writeNewaddrLo(lo uint32) bool {
	return c.writeIfAddrmsg(
		syscall.RTM_NEWADDR,
		syscall.NLM_F_CREATE|syscall.NLM_F_EXCL,
		&syscall.IfAddrmsg{
			Family:    syscall.AF_INET,
			Prefixlen: 8,
			Flags:     syscall.IFA_F_PERMANENT,
			Scope:     syscall.RT_SCOPE_HOST,
			Index:     lo,
		},
		RtAttrMsg[InAddr]{syscall.RtAttr{
			Type: syscall.IFA_LOCAL,
		}, InAddr{127, 0, 0, 1}},
		RtAttrMsg[InAddr]{syscall.RtAttr{
			Type: syscall.IFA_ADDRESS,
		}, InAddr{127, 0, 0, 1}},
	)
}

// SendNewaddrLo sends a RTM_NEWADDR message for the loopback address to the kernel.
func (c *RouteConn) SendNewaddrLo(ctx context.Context, lo uint32) error {
	if !c.writeNewaddrLo(lo) {
		return syscall.ENOMEM
	}
	return c.conn.Roundtrip(ctx, c.rtnlConsume)
}

// writeIfInfomsg writes an ifinfomsg structure to conn.
func (c *RouteConn) writeIfInfomsg(
	typ, flags uint16,
	msg *syscall.IfInfomsg,
) bool {
	c.conn.typ, c.conn.flags = typ, syscall.NLM_F_REQUEST|syscall.NLM_F_ACK|flags
	return add(c.conn, msg)
}

// SendIfInfomsg sends an ifinfomsg structure to rtnetlink.
func (c *RouteConn) SendIfInfomsg(
	ctx context.Context,
	typ, flags uint16,
	msg *syscall.IfInfomsg,
) error {
	if !c.writeIfInfomsg(typ, flags, msg) {
		return syscall.ENOMEM
	}
	return c.conn.Roundtrip(ctx, c.rtnlConsume)
}
