package kvm

import (
	"bytes"
	"fmt"
	"net"
	"os"
	"strings"
	"time"

	"os/exec"

	"github.com/hashicorp/go-envparse"
	"github.com/pion/mdns/v2"
	"golang.org/x/net/ipv4"
	"golang.org/x/net/ipv6"

	"github.com/vishvananda/netlink"
	"github.com/vishvananda/netlink/nl"
)

var mDNSConn *mdns.Conn

var networkState NetworkState

type NetworkState struct {
	Up   bool
	IPv4 string
	IPv6 string
	MAC  string

	checked bool
}

type LocalIpInfo struct {
	IPv4 string
	IPv6 string
	MAC  string
}

const (
	NetIfName     = "eth0"
	DHCPLeaseFile = "/run/udhcpc.%s.info"
)

// setDhcpClientState sends signals to udhcpc to change it's current mode
// of operation. Setting active to true will force udhcpc to renew the DHCP lease.
// Setting active to false will put udhcpc into idle mode.
func setDhcpClientState(active bool) {
	var signal string
	if active {
		signal = "-SIGUSR1"
	} else {
		signal = "-SIGUSR2"
	}

	cmd := exec.Command("/usr/bin/killall", signal, "udhcpc")
	if err := cmd.Run(); err != nil {
		logger.Warn().Err(err).Msg("network: setDhcpClientState: failed to change udhcpc state")
	}
}

func checkNetworkState() {
	iface, err := netlink.LinkByName(NetIfName)
	if err != nil {
		logger.Warn().Err(err).Str("interface", NetIfName).Msg("failed to get interface")
		return
	}

	newState := NetworkState{
		Up:  iface.Attrs().OperState == netlink.OperUp,
		MAC: iface.Attrs().HardwareAddr.String(),

		checked: true,
	}

	addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL)
	if err != nil {
		logger.Warn().Err(err).Str("interface", NetIfName).Msg("failed to get addresses")
	}

	// If the link is going down, put udhcpc into idle mode.
	// If the link is coming back up, activate udhcpc and force it to renew the lease.
	if newState.Up != networkState.Up {
		setDhcpClientState(newState.Up)
	}

	for _, addr := range addrs {
		if addr.IP.To4() != nil {
			if !newState.Up && networkState.Up {
				// If the network is going down, remove all IPv4 addresses from the interface.
				logger.Info().Str("address", addr.IP.String()).Msg("network: state transitioned to down, removing IPv4 address")
				err := netlink.AddrDel(iface, &addr)
				if err != nil {
					logger.Warn().Err(err).Str("address", addr.IP.String()).Msg("network: failed to delete address")
				}

				newState.IPv4 = "..."
			} else {
				newState.IPv4 = addr.IP.String()
			}
		} else if addr.IP.To16() != nil && newState.IPv6 == "" {
			newState.IPv6 = addr.IP.String()
		}
	}

	if newState != networkState {
		logger.Info().
			Interface("newState", newState).
			Interface("oldState", networkState).
			Msg("network state changed")

		// restart MDNS
		_ = startMDNS()
		networkState = newState
		requestDisplayUpdate()
	}
}

func startMDNS() error {
	// If server was previously running, stop it
	if mDNSConn != nil {
		logger.Info().Msg("stopping mDNS server")
		err := mDNSConn.Close()
		if err != nil {
			logger.Warn().Err(err).Msg("failed to stop mDNS server")
		}
	}

	// Start a new server
	hostname := "jetkvm.local"

	scopedLogger := logger.With().Str("hostname", hostname).Logger()
	scopedLogger.Info().Msg("starting mDNS server")

	addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4)
	if err != nil {
		return err
	}

	addr6, err := net.ResolveUDPAddr("udp6", mdns.DefaultAddressIPv6)
	if err != nil {
		return err
	}

	l4, err := net.ListenUDP("udp4", addr4)
	if err != nil {
		return err
	}

	l6, err := net.ListenUDP("udp6", addr6)
	if err != nil {
		return err
	}

	mDNSConn, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{
		LocalNames:    []string{hostname}, //TODO: make it configurable
		LoggerFactory: defaultLoggerFactory,
	})
	if err != nil {
		scopedLogger.Warn().Err(err).Msg("failed to start mDNS server")
		mDNSConn = nil
		return err
	}
	//defer server.Close()
	return nil
}

func getNTPServersFromDHCPInfo() ([]string, error) {
	buf, err := os.ReadFile(fmt.Sprintf(DHCPLeaseFile, NetIfName))
	if err != nil {
		// do not return error if file does not exist
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, fmt.Errorf("failed to load udhcpc info: %w", err)
	}

	// parse udhcpc info
	env, err := envparse.Parse(bytes.NewReader(buf))
	if err != nil {
		return nil, fmt.Errorf("failed to parse udhcpc info: %w", err)
	}

	val, ok := env["ntpsrv"]
	if !ok {
		return nil, nil
	}

	var servers []string

	for _, server := range strings.Fields(val) {
		if net.ParseIP(server) == nil {
			logger.Info().Str("server", server).Msg("invalid NTP server IP, ignoring")
		}
		servers = append(servers, server)
	}

	return servers, nil
}

func initNetwork() {
	ensureConfigLoaded()

	updates := make(chan netlink.LinkUpdate)
	done := make(chan struct{})

	if err := netlink.LinkSubscribe(updates, done); err != nil {
		logger.Warn().Err(err).Msg("failed to subscribe to link updates")
		return
	}

	go func() {
		waitCtrlClientConnected()
		checkNetworkState()
		ticker := time.NewTicker(1 * time.Second)
		defer ticker.Stop()

		for {
			select {
			case update := <-updates:
				if update.Link.Attrs().Name == NetIfName {
					logger.Info().Interface("update", update).Msg("link update")
					checkNetworkState()
				}
			case <-ticker.C:
				checkNetworkState()
			case <-done:
				return
			}
		}
	}()
	err := startMDNS()
	if err != nil {
		logger.Warn().Err(err).Msg("failed to run mDNS")
	}
}
