//go:build linux || freebsd

package osl

import (
	"errors"
	"fmt"
	"net"
	"slices"

	"github.com/moby/moby/v2/daemon/libnetwork/types"
	"github.com/vishvananda/netlink"
)

// Gateway returns the IPv4 gateway for the sandbox.
func (n *Namespace) Gateway() net.IP {
	n.mu.Lock()
	defer n.mu.Unlock()

	return n.gw
}

// GatewayIPv6 returns the IPv6 gateway for the sandbox.
func (n *Namespace) GatewayIPv6() net.IP {
	n.mu.Lock()
	defer n.mu.Unlock()

	return n.gwv6
}

// StaticRoutes returns additional static routes for the sandbox. Note that
// directly connected routes are stored on the particular interface they
// refer to.
func (n *Namespace) StaticRoutes() []*types.StaticRoute {
	n.mu.Lock()
	defer n.mu.Unlock()

	routes := make([]*types.StaticRoute, len(n.staticRoutes))
	for i, route := range n.staticRoutes {
		routes[i] = route.Copy()
	}

	return routes
}

// SetGateway sets the default IPv4 gateway for the sandbox. It is a no-op
// if the given gateway is empty.
func (n *Namespace) SetGateway(gw net.IP) error {
	if len(gw) == 0 {
		return nil
	}

	if err := n.programGateway(gw, true); err != nil {
		return err
	}
	n.mu.Lock()
	n.gw = gw
	n.mu.Unlock()
	return nil
}

// UnsetGateway the previously set default IPv4 gateway in the sandbox.
// It is a no-op if no gateway was set.
func (n *Namespace) UnsetGateway() error {
	gw := n.Gateway()
	if len(gw) == 0 {
		return nil
	}

	if err := n.programGateway(gw, false); err != nil {
		return err
	}
	n.mu.Lock()
	n.gw = net.IP{}
	n.mu.Unlock()
	return nil
}

func (n *Namespace) programGateway(gw net.IP, isAdd bool) error {
	gwRoutes, err := n.nlHandle.RouteGet(gw)
	if err != nil {
		return fmt.Errorf("route for the gateway %s could not be found: %v", gw, err)
	}

	var linkIndex int
	for _, gwRoute := range gwRoutes {
		if gwRoute.Gw == nil {
			linkIndex = gwRoute.LinkIndex
			break
		}
	}

	if linkIndex == 0 {
		return fmt.Errorf("direct route for the gateway %s could not be found", gw)
	}

	if isAdd {
		return n.nlHandle.RouteAdd(&netlink.Route{
			Scope:     netlink.SCOPE_UNIVERSE,
			LinkIndex: linkIndex,
			Gw:        gw,
		})
	}

	return n.nlHandle.RouteDel(&netlink.Route{
		Scope:     netlink.SCOPE_UNIVERSE,
		LinkIndex: linkIndex,
		Gw:        gw,
	})
}

// Program a route in to the namespace routing table.
func (n *Namespace) programRoute(dest *net.IPNet, nh net.IP) error {
	gwRoutes, err := n.nlHandle.RouteGet(nh)
	if err != nil {
		return fmt.Errorf("route for the next hop %s could not be found: %v", nh, err)
	}

	return n.nlHandle.RouteAdd(&netlink.Route{
		Scope:     netlink.SCOPE_UNIVERSE,
		LinkIndex: gwRoutes[0].LinkIndex,
		Gw:        nh,
		Dst:       dest,
	})
}

// Delete a route from the namespace routing table.
func (n *Namespace) removeRoute(dest *net.IPNet, nh net.IP) error {
	gwRoutes, err := n.nlHandle.RouteGet(nh)
	if err != nil {
		return fmt.Errorf("route for the next hop could not be found: %v", err)
	}

	return n.nlHandle.RouteDel(&netlink.Route{
		Scope:     netlink.SCOPE_UNIVERSE,
		LinkIndex: gwRoutes[0].LinkIndex,
		Gw:        nh,
		Dst:       dest,
	})
}

// SetGatewayIPv6 sets the default IPv6 gateway for the sandbox. It is a no-op
// if the given gateway is empty.
func (n *Namespace) SetGatewayIPv6(gwv6 net.IP) error {
	if len(gwv6) == 0 {
		return nil
	}

	if err := n.programGateway(gwv6, true); err != nil {
		return err
	}

	n.mu.Lock()
	n.gwv6 = gwv6
	n.mu.Unlock()
	return nil
}

// UnsetGatewayIPv6 unsets the previously set default IPv6 gateway in the sandbox.
// It is a no-op if no gateway was set.
func (n *Namespace) UnsetGatewayIPv6() error {
	gwv6 := n.GatewayIPv6()
	if len(gwv6) == 0 {
		return nil
	}

	if err := n.programGateway(gwv6, false); err != nil {
		return err
	}

	n.mu.Lock()
	n.gwv6 = net.IP{}
	n.mu.Unlock()
	return nil
}

// AddStaticRoute adds a static route to the sandbox.
func (n *Namespace) AddStaticRoute(r *types.StaticRoute) error {
	if err := n.programRoute(r.Destination, r.NextHop); err != nil {
		return err
	}

	n.mu.Lock()
	n.staticRoutes = append(n.staticRoutes, r)
	n.mu.Unlock()
	return nil
}

// RemoveStaticRoute removes a static route from the sandbox.
func (n *Namespace) RemoveStaticRoute(r *types.StaticRoute) error {
	if err := n.removeRoute(r.Destination, r.NextHop); err != nil {
		return err
	}

	n.mu.Lock()
	lastIndex := len(n.staticRoutes) - 1
	for i, v := range n.staticRoutes {
		if v == r {
			// Overwrite the route we're removing with the last element
			n.staticRoutes[i] = n.staticRoutes[lastIndex]
			// Shorten the slice to trim the extra element
			n.staticRoutes = n.staticRoutes[:lastIndex]
			break
		}
	}
	n.mu.Unlock()
	return nil
}

// SetDefaultRouteIPv4 sets up a connected route to 0.0.0.0 via the Interface
// with srcName, if that Interface has a route to 0.0.0.0. Otherwise, it
// returns an error.
func (n *Namespace) SetDefaultRouteIPv4(srcName string) error {
	if err := n.setDefaultRoute(srcName, func(ipNet *net.IPNet) bool {
		return ipNet.IP.IsUnspecified() && ipNet.IP.To4() != nil
	}); err != nil {
		return fmt.Errorf("setting IPv4 default route to interface with srcName '%s': %w", srcName, err)
	}

	n.mu.Lock()
	n.defRoute4SrcName = srcName
	n.mu.Unlock()
	return nil
}

// SetDefaultRouteIPv6 sets up a connected route to [::] via the Interface
// with srcName, if that Interface has a route to [::]. Otherwise, it
// returns an error.
func (n *Namespace) SetDefaultRouteIPv6(srcName string) error {
	if err := n.setDefaultRoute(srcName, func(ipNet *net.IPNet) bool {
		return ipNet.IP.IsUnspecified() && ipNet.IP.To4() == nil
	}); err != nil {
		return fmt.Errorf("setting IPv6 default route to interface with srcName '%s': %w", srcName, err)
	}

	n.mu.Lock()
	n.defRoute6SrcName = srcName
	n.mu.Unlock()
	return nil
}

func (n *Namespace) setDefaultRoute(srcName string, routeMatcher func(*net.IPNet) bool) error {
	iface := n.InterfaceBySrcName(srcName)
	if iface == nil {
		return errors.New("no interface")
	}

	ridx := slices.IndexFunc(iface.routes, routeMatcher)
	if ridx == -1 {
		return errors.New("no default route")
	}

	link, err := n.nlHandle.LinkByName(iface.dstName)
	if err != nil {
		return fmt.Errorf("no link src:%s dst:%s", srcName, iface.dstName)
	}

	if err := n.nlHandle.RouteAdd(&netlink.Route{
		Scope:     netlink.SCOPE_LINK,
		LinkIndex: link.Attrs().Index,
		Dst:       iface.routes[ridx],
	}); err != nil {
		return err
	}
	return nil
}

// UnsetDefaultRouteIPv4 unsets the previously set default IPv4 default route
// in the sandbox. It is a no-op if no gateway was set.
func (n *Namespace) UnsetDefaultRouteIPv4() error {
	n.mu.Lock()
	srcName := n.defRoute4SrcName
	n.mu.Unlock()

	if err := n.unsetDefaultRoute(srcName, func(ipNet *net.IPNet) bool {
		return ipNet.IP.IsUnspecified() && ipNet.IP.To4() != nil
	}); err != nil {
		return fmt.Errorf("removing IPv4 default route to interface with srcName '%s': %w", srcName, err)
	}

	n.mu.Lock()
	n.defRoute4SrcName = ""
	n.mu.Unlock()
	return nil
}

// UnsetDefaultRouteIPv6 unsets the previously set default IPv6 default route
// in the sandbox. It is a no-op if no gateway was set.
func (n *Namespace) UnsetDefaultRouteIPv6() error {
	n.mu.Lock()
	srcName := n.defRoute6SrcName
	n.mu.Unlock()

	if err := n.unsetDefaultRoute(srcName, func(ipNet *net.IPNet) bool {
		return ipNet.IP.IsUnspecified() && ipNet.IP.To4() == nil
	}); err != nil {
		return fmt.Errorf("removing IPv6 default route to interface with srcName '%s': %w", srcName, err)
	}

	n.mu.Lock()
	n.defRoute6SrcName = ""
	n.mu.Unlock()
	return nil
}

func (n *Namespace) unsetDefaultRoute(srcName string, routeMatcher func(*net.IPNet) bool) error {
	if srcName == "" {
		return nil
	}

	iface := n.InterfaceBySrcName(srcName)
	if iface == nil {
		return nil
	}

	ridx := slices.IndexFunc(iface.routes, routeMatcher)
	if ridx == -1 {
		return errors.New("no default route")
	}

	link, err := n.nlHandle.LinkByName(iface.dstName)
	if err != nil {
		return errors.New("no link")
	}

	return n.nlHandle.RouteDel(&netlink.Route{
		Scope:     netlink.SCOPE_LINK,
		LinkIndex: link.Attrs().Index,
		Dst:       iface.routes[ridx],
	})
}
