package engine

import (
	"bufio"
	"net"
	"os"
	"os/exec"
	"strings"
	"sync"
	"time"

	"github.com/mdlayher/netlink"
	"golang.org/x/sys/unix"
)

const (
	ifaceCacheTTL = 30 * time.Second
	arpCacheTTL   = 10 * time.Second
	ndpCacheTTL   = 10 * time.Second
)

type sourceMACResolver struct {
	mu sync.RWMutex

	lastIfaceRefresh time.Time
	ifaceByIP        map[string]net.HardwareAddr

	lastARPRefresh time.Time
	arpByIP        map[string]net.HardwareAddr

	lastNDPRefresh time.Time
	ndpByIP        map[string]net.HardwareAddr
}

func newSourceMACResolver() *sourceMACResolver {
	return &sourceMACResolver{
		ifaceByIP: make(map[string]net.HardwareAddr),
		arpByIP:   make(map[string]net.HardwareAddr),
		ndpByIP:   make(map[string]net.HardwareAddr),
	}
}

func (r *sourceMACResolver) Resolve(ip net.IP) net.HardwareAddr {
	if ip == nil {
		return nil
	}
	ipKey := ip.String()
	if ipKey == "" {
		return nil
	}

	now := time.Now()
	r.mu.RLock()
	ifaceRefreshDue := now.Sub(r.lastIfaceRefresh) > ifaceCacheTTL
	arpRefreshDue := now.Sub(r.lastARPRefresh) > arpCacheTTL
	ndpRefreshDue := now.Sub(r.lastNDPRefresh) > ndpCacheTTL
	if mac := r.ifaceByIP[ipKey]; len(mac) != 0 {
		out := append(net.HardwareAddr(nil), mac...)
		r.mu.RUnlock()
		return out
	}
	if mac := r.arpByIP[ipKey]; len(mac) != 0 && !arpRefreshDue {
		out := append(net.HardwareAddr(nil), mac...)
		r.mu.RUnlock()
		return out
	}
	if mac := r.ndpByIP[ipKey]; len(mac) != 0 && !ndpRefreshDue {
		out := append(net.HardwareAddr(nil), mac...)
		r.mu.RUnlock()
		return out
	}
	r.mu.RUnlock()

	if ifaceRefreshDue {
		r.refreshIfaceCache(now)
	}
	if arpRefreshDue {
		r.refreshARPCache(now)
	}
	if ndpRefreshDue {
		r.refreshNDPCache(now)
	}

	r.mu.RLock()
	defer r.mu.RUnlock()
	if mac := r.ifaceByIP[ipKey]; len(mac) != 0 {
		return append(net.HardwareAddr(nil), mac...)
	}
	if mac := r.arpByIP[ipKey]; len(mac) != 0 {
		return append(net.HardwareAddr(nil), mac...)
	}
	if mac := r.ndpByIP[ipKey]; len(mac) != 0 {
		return append(net.HardwareAddr(nil), mac...)
	}

	// On-demand IPv6 neighbor lookup via route-netlink as a last fast path.
	if ip.To4() == nil {
		if mac, ok := lookupNeighborMACNetlink(ip); ok {
			out := append(net.HardwareAddr(nil), mac...)
			r.mu.Lock()
			r.ndpByIP[ipKey] = append(net.HardwareAddr(nil), mac...)
			r.mu.Unlock()
			return out
		}
	}
	return nil
}

func (r *sourceMACResolver) refreshIfaceCache(now time.Time) {
	interfaces, err := net.Interfaces()
	if err != nil {
		return
	}

	m := make(map[string]net.HardwareAddr)
	for _, iface := range interfaces {
		if len(iface.HardwareAddr) == 0 {
			continue
		}
		addrs, err := iface.Addrs()
		if err != nil {
			continue
		}
		for _, addr := range addrs {
			ipNet, ok := addr.(*net.IPNet)
			if !ok || ipNet.IP == nil {
				continue
			}
			m[ipNet.IP.String()] = append(net.HardwareAddr(nil), iface.HardwareAddr...)
		}
	}

	r.mu.Lock()
	r.ifaceByIP = m
	r.lastIfaceRefresh = now
	r.mu.Unlock()
}

func (r *sourceMACResolver) refreshARPCache(now time.Time) {
	f, err := os.Open("/proc/net/arp")
	if err != nil {
		return
	}
	defer f.Close()

	m := make(map[string]net.HardwareAddr)
	scanner := bufio.NewScanner(f)
	lineNo := 0
	for scanner.Scan() {
		lineNo++
		if lineNo == 1 {
			continue // header
		}
		fields := strings.Fields(scanner.Text())
		if len(fields) < 4 {
			continue
		}
		ipStr := fields[0]
		hwAddr := fields[3]
		if hwAddr == "00:00:00:00:00:00" {
			continue
		}
		mac, err := net.ParseMAC(hwAddr)
		if err != nil {
			continue
		}
		m[ipStr] = append(net.HardwareAddr(nil), mac...)
	}
	if err := scanner.Err(); err != nil {
		return
	}

	r.mu.Lock()
	r.arpByIP = m
	r.lastARPRefresh = now
	r.mu.Unlock()
}

func (r *sourceMACResolver) refreshNDPCache(now time.Time) {
	m, ok := readNeighborCacheFile("/proc/net/ndisc_cache")
	if !ok || len(m) == 0 {
		// Fallback for environments without /proc/net/ndisc_cache.
		m = readIPv6NeighNetlink()
	}
	if len(m) == 0 {
		// Last-resort fallback for environments where route-netlink dumps are restricted.
		m = readIPv6NeighCommand()
	}
	r.mu.Lock()
	r.ndpByIP = m
	r.lastNDPRefresh = now
	r.mu.Unlock()
}

func readNeighborCacheFile(path string) (map[string]net.HardwareAddr, bool) {
	f, err := os.Open(path)
	if err != nil {
		return nil, false
	}
	defer f.Close()

	m := make(map[string]net.HardwareAddr)
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		ip, mac, ok := parseNeighborLine(scanner.Text())
		if !ok {
			continue
		}
		m[ip] = mac
	}
	if err := scanner.Err(); err != nil {
		return nil, false
	}
	return m, true
}

func readIPv6NeighNetlink() map[string]net.HardwareAddr {
	const (
		ndMsgLen  = 12
		ndaDst    = 1
		ndaLLAddr = 2
	)

	m := make(map[string]net.HardwareAddr)
	conn, err := netlink.Dial(unix.NETLINK_ROUTE, nil)
	if err != nil {
		return m
	}
	defer conn.Close()

	req := make([]byte, ndMsgLen)
	req[0] = unix.AF_INET6
	msgs, err := conn.Execute(netlink.Message{
		Header: netlink.Header{
			Type:  unix.RTM_GETNEIGH,
			Flags: netlink.Request | netlink.Dump,
		},
		Data: req,
	})
	if err != nil {
		return m
	}

	for _, msg := range msgs {
		if msg.Header.Type != unix.RTM_NEWNEIGH || len(msg.Data) < ndMsgLen || msg.Data[0] != unix.AF_INET6 {
			continue
		}
		attrs, err := netlink.UnmarshalAttributes(msg.Data[ndMsgLen:])
		if err != nil {
			continue
		}
		var ipStr string
		var mac net.HardwareAddr
		for _, a := range attrs {
			switch a.Type {
			case ndaDst:
				if len(a.Data) == net.IPv6len {
					ipStr = net.IP(append([]byte(nil), a.Data...)).String()
				}
			case ndaLLAddr:
				if len(a.Data) >= 6 {
					candidate := append(net.HardwareAddr(nil), a.Data...)
					if candidate.String() != "00:00:00:00:00:00" {
						mac = candidate
					}
				}
			}
		}
		if ipStr != "" && len(mac) != 0 {
			m[ipStr] = mac
		}
	}

	return m
}

func lookupNeighborMACNetlink(target net.IP) (net.HardwareAddr, bool) {
	const (
		ndMsgLen  = 12
		ndaDst    = 1
		ndaLLAddr = 2
	)
	if target == nil || target.To4() != nil {
		return nil, false
	}
	target16 := target.To16()
	if target16 == nil {
		return nil, false
	}

	conn, err := netlink.Dial(unix.NETLINK_ROUTE, nil)
	if err != nil {
		return nil, false
	}
	defer conn.Close()

	req := make([]byte, ndMsgLen)
	req[0] = unix.AF_INET6
	msgs, err := conn.Execute(netlink.Message{
		Header: netlink.Header{
			Type:  unix.RTM_GETNEIGH,
			Flags: netlink.Request | netlink.Dump,
		},
		Data: req,
	})
	if err != nil {
		return nil, false
	}

	for _, msg := range msgs {
		if msg.Header.Type != unix.RTM_NEWNEIGH || len(msg.Data) < ndMsgLen || msg.Data[0] != unix.AF_INET6 {
			continue
		}
		attrs, err := netlink.UnmarshalAttributes(msg.Data[ndMsgLen:])
		if err != nil {
			continue
		}
		var dstIP net.IP
		var mac net.HardwareAddr
		for _, a := range attrs {
			switch a.Type {
			case ndaDst:
				if len(a.Data) == net.IPv6len {
					dstIP = net.IP(append([]byte(nil), a.Data...))
				}
			case ndaLLAddr:
				if len(a.Data) >= 6 {
					candidate := append(net.HardwareAddr(nil), a.Data...)
					if candidate.String() != "00:00:00:00:00:00" {
						mac = candidate
					}
				}
			}
		}
		if dstIP != nil && mac != nil && dstIP.Equal(target16) {
			return mac, true
		}
	}

	return nil, false
}

func readIPv6NeighCommand() map[string]net.HardwareAddr {
	commands := [][]string{
		{"ip", "-6", "neigh", "show"},
	}
	m := make(map[string]net.HardwareAddr)
	for _, cmd := range commands {
		out, err := exec.Command(cmd[0], cmd[1:]...).Output()
		if err != nil || len(out) == 0 {
			continue
		}
		for _, line := range strings.Split(string(out), "\n") {
			ip, mac, ok := parseNeighborLine(line)
			if !ok {
				continue
			}
			m[ip] = mac
		}
		if len(m) != 0 {
			return m
		}
	}
	return m
}

func parseNeighborLine(line string) (string, net.HardwareAddr, bool) {
	fields := strings.Fields(line)
	if len(fields) < 2 {
		return "", nil, false
	}
	var ipStr string
	var mac net.HardwareAddr
	for _, f := range fields {
		if ip := net.ParseIP(f); ip != nil {
			ipStr = ip.String()
			continue
		}
		if parsedMAC, err := net.ParseMAC(f); err == nil {
			// Ignore unresolved/bogus entries.
			if parsedMAC.String() != "00:00:00:00:00:00" {
				mac = append(net.HardwareAddr(nil), parsedMAC...)
			}
		}
	}
	if ipStr == "" || len(mac) == 0 {
		return "", nil, false
	}
	return ipStr, mac, true
}
