//go:build dragonfly || freebsd || netbsd || openbsd || darwin

package networkmonitor

import (
	"context"
	"errors"
	"fmt"
	"syscall"
	"unsafe"

	log "github.com/sirupsen/logrus"
	"golang.org/x/net/route"
	"golang.org/x/sys/unix"

	"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
)

func prepareFd() (int, error) {
	return unix.Socket(syscall.AF_ROUTE, syscall.SOCK_RAW, syscall.AF_UNSPEC)
}

func routeCheck(ctx context.Context, fd int, nexthopv4, nexthopv6 systemops.Nexthop) error {
	for {
		// Wait until fd is readable or context is cancelled, to avoid a busy-loop
		// when the routing socket returns EAGAIN (e.g. immediately after wakeup).
		if err := waitReadable(ctx, fd); err != nil {
			return err
		}

		buf := make([]byte, 2048)
		n, err := unix.Read(fd, buf)
		if err != nil {
			if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) {
				continue
			}
			if errors.Is(err, unix.EBADF) || errors.Is(err, unix.EINVAL) {
				return fmt.Errorf("routing socket closed: %w", err)
			}
			return fmt.Errorf("read routing socket: %w", err)
		}

		if n < unix.SizeofRtMsghdr {
			log.Debugf("Network monitor: read from routing socket returned less than expected: %d bytes", n)
			continue
		}

		msg := (*unix.RtMsghdr)(unsafe.Pointer(&buf[0]))

		switch msg.Type {
		// handle route changes
		case unix.RTM_ADD, syscall.RTM_DELETE:
			route, flags, err := parseRouteMessage(buf[:n])
			if err != nil {
				log.Debugf("Network monitor: error parsing routing message: %v", err)
				continue
			}

			if route.Dst.Bits() != 0 {
				continue
			}

			intf := "<nil>"
			if route.Interface != nil {
				intf = route.Interface.Name
			}
			switch msg.Type {
			case unix.RTM_ADD:
				if systemops.IgnoreAddedDefaultRoute(flags) {
					log.Debugf("Network monitor: ignoring added default route via %s, interface %s, flags %#x", route.Gw, intf, flags)
					continue
				}
				log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf)
				return nil
			case unix.RTM_DELETE:
				if nexthopv4.Intf != nil && route.Gw.Compare(nexthopv4.IP) == 0 || nexthopv6.Intf != nil && route.Gw.Compare(nexthopv6.IP) == 0 {
					log.Infof("Network monitor: default route removed: via %s, interface %s", route.Gw, intf)
					return nil
				}
			}
		}
	}
}

func parseRouteMessage(buf []byte) (*systemops.Route, int, error) {
	msgs, err := route.ParseRIB(route.RIBTypeRoute, buf)
	if err != nil {
		return nil, 0, fmt.Errorf("parse RIB: %v", err)
	}

	if len(msgs) != 1 {
		return nil, 0, fmt.Errorf("unexpected RIB message msgs: %v", msgs)
	}

	msg, ok := msgs[0].(*route.RouteMessage)
	if !ok {
		return nil, 0, fmt.Errorf("unexpected RIB message type: %T", msgs[0])
	}

	r, err := systemops.MsgToRoute(msg)
	if err != nil {
		return nil, 0, err
	}
	return r, msg.Flags, nil
}

// waitReadable blocks until fd has data to read, or ctx is cancelled.
func waitReadable(ctx context.Context, fd int) error {
	var fdset unix.FdSet
	if fd < 0 || fd/unix.NFDBITS >= len(fdset.Bits) {
		return fmt.Errorf("fd %d out of range for FdSet", fd)
	}

	for {
		if err := ctx.Err(); err != nil {
			return err
		}

		fdset = unix.FdSet{}
		fdset.Set(fd)
		// Use a 1-second timeout so we can re-check ctx periodically.
		tv := unix.Timeval{Sec: 1}
		n, err := unix.Select(fd+1, &fdset, nil, nil, &tv)
		if err != nil {
			if errors.Is(err, unix.EINTR) {
				continue
			}
			return fmt.Errorf("select on routing socket: %w", err)
		}
		if n > 0 {
			return nil
		}
		// timeout — loop back and re-check ctx
	}
}
