package netboot

import (
	"errors"
	"fmt"
	"net"
	"os"
	"strings"
	"syscall"
	"time"

	"github.com/insomniacslk/dhcp/dhcpv4"
	"github.com/insomniacslk/dhcp/dhcpv6"
	"github.com/jsimonetti/rtnetlink"
	"github.com/jsimonetti/rtnetlink/rtnl"
	"github.com/mdlayher/netlink"
)

// AddrConf holds a single IP address configuration for a NIC
type AddrConf struct {
	IPNet             net.IPNet
	PreferredLifetime time.Duration
	ValidLifetime     time.Duration
}

// NetConf holds multiple IP configuration for a NIC, and DNS configuration
type NetConf struct {
	Addresses     []AddrConf
	DNSServers    []net.IP
	DNSSearchList []string
	Routers       []net.IP
	NTPServers    []net.IP
}

// GetNetConfFromPacketv6 extracts network configuration information from a DHCPv6
// Reply packet and returns a populated NetConf structure
func GetNetConfFromPacketv6(d *dhcpv6.Message) (*NetConf, error) {
	iana := d.Options.OneIANA()
	if iana == nil {
		return nil, errors.New("no option IA NA found")
	}
	netconf := NetConf{}

	for _, iaaddr := range iana.Options.Addresses() {
		netconf.Addresses = append(netconf.Addresses, AddrConf{
			IPNet: net.IPNet{
				IP: iaaddr.IPv6Addr,

				// This mask tells Linux which addresses we know to be
				// "on-link" (i.e., reachable on this interface without
				// having to talk to a router).
				//
				// Since DHCPv6 does not give us that information, we
				// have to assume that no addresses are on-link. To do
				// that, we use /128. (See also RFC 5942 Section 5,
				// "Observed Incorrect Implementation Behavior".)
				Mask: net.CIDRMask(128, 128),
			},
			PreferredLifetime: iaaddr.PreferredLifetime,
			ValidLifetime:     iaaddr.ValidLifetime,
		})
	}
	// get DNS configuration
	netconf.DNSServers = d.Options.DNS()

	// get domain search list
	domains := d.Options.DomainSearchList()
	if domains != nil {
		netconf.DNSSearchList = domains.Labels
	}

	// get NTP servers
	netconf.NTPServers = d.Options.NTPServers()

	return &netconf, nil
}

// GetNetConfFromPacketv4 extracts network configuration information from a DHCPv4
// Reply packet and returns a populated NetConf structure
func GetNetConfFromPacketv4(d *dhcpv4.DHCPv4) (*NetConf, error) {
	// extract the address from the DHCPv4 address
	ipAddr := d.YourIPAddr
	if ipAddr == nil || ipAddr.Equal(net.IPv4zero) {
		return nil, errors.New("ip address is null (0.0.0.0)")
	}
	netconf := NetConf{}

	// get the subnet mask from OptionSubnetMask. If the netmask is not defined
	// in the packet, an error is returned
	netmask := d.SubnetMask()
	if netmask == nil {
		return nil, errors.New("no netmask option in response packet")
	}
	ones, _ := netmask.Size()
	if ones == 0 {
		return nil, errors.New("netmask extracted from OptSubnetMask options is null")
	}

	// netconf struct requires a valid lifetime to be specified. ValidLifetime is a dhcpv6
	// concept, the closest mapping in dhcpv4 world is "IP Address Lease Time". If the lease
	// time option is nil, we set it to 0
	leaseTime := d.IPAddressLeaseTime(0)

	netconf.Addresses = append(netconf.Addresses, AddrConf{
		IPNet: net.IPNet{
			IP:   ipAddr,
			Mask: netmask,
		},
		PreferredLifetime: 0,
		ValidLifetime:     leaseTime,
	})

	// get DNS configuration
	netconf.DNSServers = d.DNS()

	// get domain search list
	dnsSearchList := d.DomainSearch()
	if dnsSearchList != nil {
		if len(dnsSearchList.Labels) == 0 {
			return nil, errors.New("dns search list is empty")
		}
		netconf.DNSSearchList = dnsSearchList.Labels
	}

	// get default gateway (empty is perfectly fine)
	netconf.Routers = d.Router()

	// get NTP servers
	netconf.NTPServers = d.NTPServers()

	return &netconf, nil
}

// IfUp brings up an interface by name, and waits for it to come up until a timeout expires
func IfUp(ifname string, timeout time.Duration) (_ *net.Interface, err error) {
	start := time.Now()
	rt, err := rtnl.Dial(nil)
	if err != nil {
		return nil, err
	}
	defer func() {
		if cerr := rt.Close(); cerr != nil {
			err = cerr
		}
	}()
	for time.Since(start) < timeout {
		iface, err := net.InterfaceByName(ifname)
		if err != nil {
			return nil, err
		}
		// If the interface is up, return. According to kernel documentation OperState may
		// be either Up or Unknown:
		//   Interface is in RFC2863 operational state UP or UNKNOWN. This is for
		//   backward compatibility, routing daemons, dhcp clients can use this
		//   flag to determine whether they should use the interface.
		// Source: https://www.kernel.org/doc/Documentation/networking/operstates.txt
		operState, err := getOperState(iface.Index)
		if err != nil {
			return nil, err
		}
		if operState == rtnetlink.OperStateUp || operState == rtnetlink.OperStateUnknown {
			// XXX despite the OperUp state, upon the first attempt I
			// consistently get a "cannot assign requested address" error. Need
			// to investigate more.
			time.Sleep(time.Second)
			return iface, nil
		}
		// otherwise try to bring it up
		if err := rt.LinkUp(iface); err != nil {
			return nil, fmt.Errorf("interface %q: %v can't bring it up: %v", ifname, iface, err)
		}
		time.Sleep(10 * time.Millisecond)
	}

	return nil, fmt.Errorf("timed out while waiting for %s to come up", ifname)
}

// ConfigureInterface configures a network interface with the configuration held by a
// NetConf structure
func ConfigureInterface(ifname string, netconf *NetConf) (err error) {
	iface, err := net.InterfaceByName(ifname)
	if err != nil {
		return err
	}
	rt, err := rtnl.Dial(nil)
	if err != nil {
		return err
	}
	defer func() {
		if cerr := rt.Close(); err != nil {
			err = cerr
		}
	}()
	// configure interfaces
	for _, addr := range netconf.Addresses {
		if err := rt.AddrAdd(iface, &addr.IPNet); err != nil {
			return fmt.Errorf("cannot configure %s on %s: %v", ifname, addr.IPNet, err)
		}
	}
	// configure /etc/resolv.conf
	resolvconf := ""
	for _, ns := range netconf.DNSServers {
		resolvconf += fmt.Sprintf("nameserver %s\n", ns)
	}
	if len(netconf.DNSSearchList) > 0 {
		resolvconf += fmt.Sprintf("search %s\n", strings.Join(netconf.DNSSearchList, " "))
	}
	if err = os.WriteFile("/etc/resolv.conf", []byte(resolvconf), 0644); err != nil {
		return fmt.Errorf("could not write resolv.conf file %v", err)
	}

	// FIXME wut? No IPv6 here?
	// add default route information for v4 space. only one default route is allowed
	// so ignore the others if there are multiple ones
	if len(netconf.Routers) > 0 {
		// if there is a default v4 route, remove it, as we want to add the one we just got during
		// the dhcp transaction. if the route is not present, which is the final state we want,
		// an error is returned so ignore it
		dst := net.IPNet{
			IP:   net.IPv4zero,
			Mask: net.CIDRMask(0, 32),
		}
		// Remove a possible default route (dst 0.0.0.0) to the L2 domain (gw: 0.0.0.0), which is what
		// a client would want to add before initiating the DHCP transaction in order not to fail with
		// ENETUNREACH. If this default route has a specific metric assigned, it doesn't get removed.
		// The code doesn't remove any other default route (i.e. gw != 0.0.0.0).
		if err := rt.RouteDel(iface, net.IPNet{IP: net.IPv4zero}); err != nil {
			switch err := err.(type) {
			case *netlink.OpError:
				// ignore the error if it's -EEXIST or -ESRCH
				if !os.IsExist(err.Err) && err.Err != syscall.ESRCH {
					return fmt.Errorf("could not delete default route on interface %s: %v", ifname, err)
				}
			default:
				return fmt.Errorf("could not delete default route on interface %s: %v", ifname, err)
			}
		}

		src := netconf.Addresses[0].IPNet
		// TODO handle the remaining Routers if more than one
		if err := rt.RouteAdd(iface, dst, netconf.Routers[0], rtnl.WithRouteSrc(&src)); err != nil {
			return fmt.Errorf("could not add gateway %s for src %s dst %s to interface %s: %v", netconf.Routers[0], src, dst, ifname, err)
		}
	}

	return nil
}
