package espradio

import (
	"errors"
	"net/netip"
	"time"

	"github.com/soypat/lneto"
	"github.com/soypat/lneto/dhcp/dhcpv4"
	"github.com/soypat/lneto/ethernet"
	"github.com/soypat/lneto/ipv4"
	"github.com/soypat/lneto/x/xnet"
)

var (
	errDHCPInvalidSubnet = errors.New("dhcp server: invalid subnet")
	errDHCPNoStaticAddr  = errors.New("dhcp server: stack has no static IPv4 address")
)

// Stack wraps an lneto async network stack on top of a NetDev (EthernetDevice).
type Stack struct {
	s       xnet.StackAsync
	dev     *NetDev
	rxtxBuf []byte
	dhcpSrv dhcpv4.Server
}

// StackConfig configures the lneto-based network stack.
type StackConfig struct {
	StaticAddress netip.Addr
	StaticSubnet  netip.Prefix
	DNSServer     netip.Addr
	NTPServer     netip.Addr
	RandSeed      int64
	Hostname      string
	MaxTCPPorts   int
	MaxUDPPorts   int
	PassivePeers  int
	// AcceptBroadcast4 enables reception of IPv4 broadcast packets.
	// Must be true when running a DHCP server (AP mode).
	AcceptBroadcast4 bool
}

// DHCPConfig configures DHCP address acquisition.
type DHCPConfig struct {
	RequestedAddr netip.Addr
}

// NewStack creates a new lneto-based TCP/IP stack on top of the given NetDev.
// The NetDev must already be started (WiFi joined, StartNetDev called).
func NewStack(dev *NetDev, cfg StackConfig) (*Stack, error) {
	if cfg.Hostname == "" {
		return nil, errors.New("empty hostname")
	}
	mac, err := dev.HardwareAddr6()
	if err != nil {
		return nil, err
	}

	stack := &Stack{dev: dev}
	const MTU = MaxFrameSize - ethernet.MaxOverheadSize + 4 // CRC not included:+4
	xcfg := xnet.StackConfig{
		DNSServer:           cfg.DNSServer,
		NTPServer:           cfg.NTPServer,
		Hostname:            cfg.Hostname,
		MaxActiveTCPPorts:   uint16(cfg.MaxTCPPorts),
		MaxActiveUDPPorts:   uint16(cfg.MaxUDPPorts),
		RandSeed:            time.Now().UnixNano() ^ cfg.RandSeed,
		HardwareAddress:     mac,
		MTU:                 MTU,
		PassivePeers:        cfg.PassivePeers,
		AcceptIPv4Broadcast: cfg.AcceptBroadcast4,
	}
	if cfg.StaticAddress.IsValid() && cfg.StaticAddress.Is4() {
		xcfg.StaticAddress4 = cfg.StaticAddress.As4()
	}
	err = stack.s.Reset(xcfg)
	if err != nil {
		return nil, err
	}
	switch {
	case cfg.StaticSubnet.IsValid():
		addr := cfg.StaticSubnet.Addr()
		if addr.Is4() {
			stack.s.SetSubnet4(addr.As4(), uint8(cfg.StaticSubnet.Bits()))
		}
	case cfg.StaticAddress.IsValid() && cfg.StaticAddress.Is4():
		// Default: derive a /24 subnet from the static address so passive
		// ARP learning works without an explicit subnet.
		stack.s.SetSubnet4(cfg.StaticAddress.As4(), 24)
	}
	dev.SetEthRecvHandler(func(pkt []byte) error {
		return stack.s.IngressEthernet(pkt)
	})
	stack.rxtxBuf = make([]byte, MTU+ethernet.MaxOverheadSize)
	return stack, nil
}

// LnetoStack returns the underlying lneto async stack for advanced use.
func (stack *Stack) LnetoStack() *xnet.StackAsync {
	return &stack.s
}

// Hostname returns the hostname configured on the stack.
func (stack *Stack) Hostname() string {
	return stack.s.Hostname()
}

// RecvAndSend polls the device for received frames and sends any pending
// outgoing frames. Returns the number of bytes sent and received.
func (stack *Stack) RecvAndSend() (send, recv int, err error) {
	recv, errrecv := stack.dev.EthPoll(stack.rxtxBuf)
	if pcapdebug && recv > 0 {
		printPacket("IN", stack.rxtxBuf[:recv])
	}
	send, err = stack.s.EgressEthernet(stack.rxtxBuf)
	if err != nil {
		return send, recv, err
	} else if errrecv != nil {
		err = errrecv
	}
	if send == 0 {
		return send, recv, err
	}
	if pcapdebug {
		printPacket("OUT", stack.rxtxBuf[:send])
	}

	err = stack.dev.SendEthFrame(stack.rxtxBuf[:send])
	return send, recv, err
}

// SetupWithDHCP performs DHCPv4 to obtain an IP address and configures the
// stack with the results. Blocks until complete or timeout.
func (stack *Stack) SetupWithDHCP(cfg DHCPConfig) (*xnet.DHCPResults, error) {
	var reqaddr [4]byte
	if cfg.RequestedAddr.IsValid() {
		if !cfg.RequestedAddr.Is4() {
			return nil, errors.New("IPv6 DHCP unsupported")
		}
		reqaddr = cfg.RequestedAddr.As4()
	}

	lstack := stack.LnetoStack()
	rstack := lstack.StackRetrying(lneto.BackoffStrategy(func(_ uint) time.Duration {
		return 50 * time.Millisecond
	}))

	dhcpResults, err := rstack.DoDHCPv4(reqaddr, 3*time.Second, 3)
	if err != nil {
		return dhcpResults, err
	}
	err = lstack.AssimilateDHCPResults(dhcpResults)
	if err != nil {
		return dhcpResults, err
	}

	gatewayHW, err := rstack.DoResolveHardwareAddress6(dhcpResults.Router, 500*time.Millisecond, 4)
	if err != nil {
		return dhcpResults, err
	}
	lstack.SetGatewayHardwareAddr(gatewayHW)
	return dhcpResults, nil
}

// SetupWithDHCPServer starts a DHCPv4 server on the stack, assigning addresses
// from the given subnet. The stack's own address must already be set (via
// StackConfig.StaticAddress). subnet should cover the AP's address, e.g.
// netip.MustParsePrefix("192.168.4.0/24").
func (stack *Stack) SetupWithDHCPServer(subnet netip.Prefix) error {
	if !subnet.IsValid() || !subnet.Addr().Is4() {
		return errDHCPInvalidSubnet
	}
	lstack := stack.LnetoStack()
	serverAddr := lstack.Addr4()
	if serverAddr == [4]byte{} {
		return errDHCPNoStaticAddr
	}
	if !subnet.Contains(netip.AddrFrom4(serverAddr)) {
		return errDHCPInvalidSubnet
	}
	err := stack.dhcpSrv.Configure(dhcpv4.ServerConfig{
		ServerAddr: serverAddr,
		Gateway:    serverAddr,
		Subnet:     ipv4.PrefixFromNetip(subnet),
	})
	if err != nil {
		return err
	}
	// Pass zero raddr so lneto's UDP source-IP filter accepts packets from any
	// source (0.0.0.0 disables the filter; required for DHCP clients that have
	// no IP yet when sending Discovers/Requests).
	return lstack.RegisterUDP4(&stack.dhcpSrv, [4]byte{}, dhcpv4.DefaultClientPort)
}
