// Only build for (linux AND (amd64 OR arm64)) due to using
// linux-specific syscalls with uint64 for “unsigned long”:

// +build linux
// +build amd64 arm64

// dhcp is a minimal DHCP client for gokrazy.
package main

import (
	"bytes"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"os"
	"path/filepath"
	"strings"
	"syscall"
	"time"

	"github.com/gokrazy/gokrazy/internal/iface"
	"github.com/google/gopacket/layers"
	"github.com/mdlayher/raw"
	"github.com/rtr7/dhcp4"
	"golang.org/x/sys/unix"
)

var (
	defaultDst     = net.IP([]byte{0, 0, 0, 0})
	defaultNetmask = net.IPMask([]byte{0, 0, 0, 0})
)

type client struct {
	hostname     string
	hardwareAddr net.HardwareAddr
	generateXID  func() uint32
	conn         net.PacketConn
}

func (c *client) packet(xid uint32, opts []layers.DHCPOption) *layers.DHCPv4 {
	return &layers.DHCPv4{
		Operation:    layers.DHCPOpRequest,
		HardwareType: layers.LinkTypeEthernet,
		HardwareLen:  uint8(len(layers.EthernetBroadcast)),
		HardwareOpts: 0, // clients set this to zero (used by relay agents)
		Xid:          xid,
		Secs:         0, // TODO: fill in?
		Flags:        0, // we can receive IP packets via unicast
		ClientHWAddr: c.hardwareAddr,
		ServerName:   nil,
		File:         nil,
		Options:      opts,
	}
}

func (c *client) discover() (*layers.DHCPv4, error) {
	discover := c.packet(c.generateXID(), []layers.DHCPOption{
		dhcp4.MessageTypeOpt(layers.DHCPMsgTypeDiscover),
		dhcp4.HostnameOpt(c.hostname),
		dhcp4.ClientIDOpt(layers.LinkTypeEthernet, c.hardwareAddr),
		dhcp4.ParamsRequestOpt(
			layers.DHCPOptDNS,
			layers.DHCPOptRouter,
			layers.DHCPOptSubnetMask,
			layers.DHCPOptDomainName),
	})
	if err := dhcp4.Write(c.conn, discover); err != nil {
		return nil, err
	}

	// Look for DHCPOFFER packet (described in RFC2131 4.3.1):
	c.conn.SetReadDeadline(time.Now().Add(5 * time.Second))
	for {
		offer, err := dhcp4.Read(c.conn)
		if err != nil {
			return nil, err
		}
		if offer == nil {
			continue // not a DHCPv4 packet
		}
		if offer.Xid != discover.Xid {
			continue // broadcast reply for different DHCP transaction
		}
		if !dhcp4.HasMessageType(offer.Options, layers.DHCPMsgTypeOffer) {
			continue
		}
		return offer, nil
	}
}

func (c *client) request(last *layers.DHCPv4) (*layers.DHCPv4, error) {
	// Build a DHCPREQUEST packet:
	request := c.packet(last.Xid, append([]layers.DHCPOption{
		dhcp4.MessageTypeOpt(layers.DHCPMsgTypeRequest),
		dhcp4.RequestIPOpt(last.YourClientIP),
		dhcp4.HostnameOpt(c.hostname),
		dhcp4.ClientIDOpt(layers.LinkTypeEthernet, c.hardwareAddr),
		dhcp4.ParamsRequestOpt(
			layers.DHCPOptDNS,
			layers.DHCPOptRouter,
			layers.DHCPOptSubnetMask,
			layers.DHCPOptDomainName),
	}, dhcp4.ServerID(last.Options)...))
	if err := dhcp4.Write(c.conn, request); err != nil {
		return nil, err
	}

	c.conn.SetReadDeadline(time.Now().Add(10 * time.Second))
	for {
		// Look for DHCPACK packet (described in RFC2131 4.3.1):
		ack, err := dhcp4.Read(c.conn)
		if err != nil {
			return nil, err
		}
		if ack == nil {
			continue // not a DHCPv4 packet
		}
		if ack.Xid != request.Xid {
			continue // broadcast reply for different DHCP transaction
		}
		if !dhcp4.HasMessageType(ack.Options, layers.DHCPMsgTypeAck) {
			continue
		}
		return ack, nil
	}
}

func main() {
	log.SetFlags(log.LstdFlags | log.Lshortfile)
	var (
		ifname = flag.String(
			"interface",
			"eth0",
			"network interface to obtain a DHCP lease on")
	)
	flag.Parse()

	// NOTE: cannot gokrazy.WaitForClock() here, since the clock can only be
	// initialized once the network is up.

	var utsname unix.Utsname
	if err := unix.Uname(&utsname); err != nil {
		log.Fatal(err)
	}
	hostname := string(utsname.Nodename[:bytes.IndexByte(utsname.Nodename[:], 0)])

	intf, err := net.InterfaceByName(*ifname)
	if err != nil {
		log.Fatal(err)
	}

	cs, err := iface.NewConfigSocket(*ifname)
	if err != nil {
		log.Fatalf("config socket: %v", err)
	}
	defer cs.Close()

	// Ensure the interface is up so that we can send DHCP packets.
	if err := cs.Up(); err != nil {
		log.Fatal(err)
	}

	// Wait for up to 10 seconds for the link to indicate it has a
	// carrier.
	for i := 0; i < 10; i++ {
		b, err := ioutil.ReadFile(filepath.Join("/sys/class/net", *ifname, "carrier"))
		if err == nil && strings.TrimSpace(string(b)) == "1" {
			break
		}
		time.Sleep(1 * time.Second)
	}

	conn, err := raw.ListenPacket(intf, syscall.ETH_P_IP, &raw.Config{
		LinuxSockDGRAM: true,
	})
	if err != nil {
		log.Fatal(err)
	}

	c := &client{
		hostname:     hostname,
		hardwareAddr: intf.HardwareAddr,
		generateXID:  dhcp4.XIDGenerator(intf.HardwareAddr),
		conn:         conn,
	}
	offer, err := c.discover()
	if err != nil {
		log.Fatal(err)
	}
	last := offer
	for {
		last, err = c.request(last)
		if err != nil {
			log.Fatal(err)
		}

		lease := dhcp4.LeaseFromACK(last)

		// Log the received DHCPACK packet:
		details := []string{
			fmt.Sprintf("IP %v", lease.IP),
		}
		if len(lease.Netmask) > 0 {
			ipnet := net.IPNet{
				IP:   lease.IP,
				Mask: lease.Netmask,
			}
			details[0] = fmt.Sprintf("IP %v", ipnet.String())
		}
		if len(lease.Router) > 0 {
			details = append(details, fmt.Sprintf("router %v", lease.Router))
		}
		if len(lease.DNS) > 0 {
			details = append(details, fmt.Sprintf("DNS %v", lease.DNS))
		}
		if len(lease.Broadcast) > 0 {
			details = append(details, fmt.Sprintf("broadcast %v", lease.Broadcast))
		}

		log.Printf("DHCPACK: %v", strings.Join(details, ", "))

		// Apply the received settings:
		if err := cs.SetAddress(lease.IP); err != nil {
			log.Fatal(err)
		}
		if len(lease.Netmask) > 0 {
			if err := cs.SetNetmask(lease.Netmask); err != nil {
				log.Fatalf("setNetmask(%v): %v", lease.Netmask, err)
			}
		}
		if b := lease.Broadcast; len(b) > 0 {
			if err := cs.SetBroadcast(b); err != nil {
				log.Fatalf("setBroadcast(%v): %v", b, err)
			}
		}

		if err := cs.Up(); err != nil {
			log.Fatal(err)
		}

		if r := lease.Router; len(r) > 0 {
			if errno := cs.AddRoute(defaultDst, r, defaultNetmask); errno != 0 {
				if errno == syscall.EEXIST {
					if errno := cs.DelRoute(defaultDst, r, defaultNetmask); errno != 0 {
						log.Printf("delRoute(%v): %v", r, errno)
					}
					if errno := cs.AddRoute(defaultDst, r, defaultNetmask); errno != 0 {
						log.Fatalf("addRoute(%v): %v", r, errno)
					}
				} else {
					log.Fatalf("addRoute(%v): %v", r, errno)
				}
			}
		}

		if len(lease.DNS) > 0 {
			resolvConf := "/etc/resolv.conf"
			if dest, err := os.Readlink("/etc/resolv.conf"); err == nil && dest == "/tmp/resolv.conf" {
				resolvConf = "/tmp/resolv.conf"
			}
			// Get the symlink out of the way, if any.
			if err := os.Remove(resolvConf); err != nil && !os.IsNotExist(err) {
				log.Fatalf("resolv.conf: %v", err)
			}
			var lines []string
			if domain := lease.Domain; domain != "" {
				lines = append(lines, fmt.Sprintf("domain %s", domain))
				lines = append(lines, fmt.Sprintf("search %s", domain))
			}
			for _, ns := range lease.DNS {
				lines = append(lines, fmt.Sprintf("nameserver %v", ns))
			}
			if err := ioutil.WriteFile(resolvConf, []byte(strings.Join(lines, "\n")+"\n"), 0644); err != nil {
				log.Fatalf("resolv.conf: %v", err)
			}
		}

		// Notify init of new addresses
		p, _ := os.FindProcess(1)
		if err := p.Signal(syscall.SIGHUP); err != nil {
			log.Printf("send SIGHUP to init: %v", err)
		}

		time.Sleep(lease.RenewalTime)
	}
}
