package clients

import (
	"bufio"
	"fmt"
	"os"
	"sort"
	"strconv"
	"strings"

	"alpine-router/config"
	"alpine-router/traffic"
)

const LeasesFile = "/var/lib/misc/dnsmasq.leases"

type Client struct {
	IP           string `json:"ip"`
	MAC          string `json:"mac"`
	Hostname     string `json:"hostname"`
	Interface    string `json:"interface"`
	LeaseExpires int64  `json:"lease_expires"`
	IsDHCP       bool   `json:"is_dhcp"`
	Online       bool   `json:"online"`
	TxBytes      uint64 `json:"tx_bytes"`
	RxBytes      uint64 `json:"rx_bytes"`
	LastActive   int64  `json:"last_active"`
	Known        bool   `json:"known"`
	Blocked      bool   `json:"blocked"`
	StaticIP     string `json:"static_ip"`
}

func GetAll() ([]Client, error) {
	leases, err := parseDNSMasqLeases()
	if err != nil {
		leases = map[string]*Client{}
	}

	arpEntries, err := parseARPTable()
	if err != nil {
		return nil, fmt.Errorf("arp: %w", err)
	}

	byIP := make(map[string]*Client, len(arpEntries))

	for ip, c := range arpEntries {
		byIP[ip] = c
	}

	for ip, lease := range leases {
		if c, exists := byIP[ip]; exists {
			c.IsDHCP = true
			c.LeaseExpires = lease.LeaseExpires
			if lease.Hostname != "" {
				c.Hostname = lease.Hostname
			}
			if c.MAC == "" {
				c.MAC = lease.MAC
			}
		} else {
			byIP[ip] = lease
		}
	}

	blockedByMAC := make(map[string]bool)

	cfg, cfgErr := config.Load()
	if cfgErr == nil && cfg != nil {
		knownByMAC := make(map[string]bool)
		for _, c := range byIP {
			if c.MAC != "" {
				knownByMAC[c.MAC] = true
			}
		}

		for _, kd := range cfg.KnownDevices {
			key := kd.IP
			found := false

			for ip, c := range byIP {
				if kd.MAC != "" && c.MAC == kd.MAC {
					c.Blocked = kd.Blocked
					c.StaticIP = kd.StaticIP
					if kd.Hostname != "" {
						c.Hostname = kd.Hostname
					}
					if kd.StaticIP != "" {
						c.IP = kd.StaticIP
					}
					found = true
					break
				}
				if kd.IP != "" && ip == kd.IP && (kd.MAC == "" || c.MAC == kd.MAC) {
					c.Blocked = kd.Blocked
					c.StaticIP = kd.StaticIP
					if kd.Hostname != "" {
						c.Hostname = kd.Hostname
					}
					if kd.StaticIP != "" {
						c.IP = kd.StaticIP
					}
					found = true
					break
				}
			}

			if !found && key != "" {
				knownByMACKey := kd.MAC
				if knownByMACKey != "" && knownByMAC[knownByMACKey] {
					continue
				}
				displayIP := kd.IP
				if kd.StaticIP != "" {
					displayIP = kd.StaticIP
				}
				byIP[key] = &Client{
					IP:       displayIP,
					MAC:      kd.MAC,
					Hostname: kd.Hostname,
					Known:    true,
					Blocked:  kd.Blocked,
					StaticIP: kd.StaticIP,
				}
			}

			if kd.Blocked && kd.MAC != "" {
				blockedByMAC[kd.MAC] = true
			}
		}
	}

	trafficAvailable := traffic.Available()
	for ip, c := range byIP {
		if c.Known && c.IP == "" {
			continue
		}
		if trafficAvailable {
			traffic.EnsureIPTracked(ip)
		}
		ts := traffic.Get(ip)
		c.TxBytes = ts.TxBytes
		c.RxBytes = ts.RxBytes
		if !ts.LastActive.IsZero() {
			c.LastActive = ts.LastActive.Unix()
		}
		if trafficAvailable {
			c.Online = traffic.IsOnline(ip)
		}
	}

	go syncKnownDevices(byIP)

	result := make([]Client, 0, len(byIP))
	for _, c := range byIP {
		result = append(result, *c)
	}

	sort.Slice(result, func(i, j int) bool {
		if result[i].Online != result[j].Online {
			return result[i].Online
		}
		if result[i].Known != result[j].Known {
			return result[i].Known
		}
		return ipLess(result[i].IP, result[j].IP)
	})

	return result, nil
}

func syncKnownDevices(byIP map[string]*Client) {
	cfg, err := config.Load()
	if err != nil {
		return
	}

	savedHostnames := make(map[string]string)
	savedBlocked := make(map[string]bool)
	savedStaticIPs := make(map[string]string)
	for _, kd := range cfg.KnownDevices {
		key := kd.MAC
		if key == "" {
			key = kd.IP
		}
		savedHostnames[key] = kd.Hostname
		if kd.Blocked {
			savedBlocked[key] = true
		}
		if kd.StaticIP != "" {
			savedStaticIPs[key] = kd.StaticIP
		}
	}

	var seen []config.KnownDevice
	for _, c := range byIP {
		if c.MAC != "" && c.IP != "" {
			key := c.MAC
			hostname := c.Hostname
			if saved, ok := savedHostnames[key]; ok {
				hostname = saved
			}
			kd := config.KnownDevice{
				IP:       c.IP,
				MAC:      c.MAC,
				Hostname: hostname,
			}
			if savedBlocked[key] {
				kd.Blocked = true
			}
			if sip, ok := savedStaticIPs[key]; ok {
				kd.StaticIP = sip
			}
			seen = append(seen, kd)
		}
	}
	_ = config.UpdateKnownDevices(seen)
}

func parseDNSMasqLeases() (map[string]*Client, error) {
	f, err := os.Open(LeasesFile)
	if err != nil {
		if os.IsNotExist(err) {
			return map[string]*Client{}, nil
		}
		return nil, err
	}
	defer f.Close()

	out := map[string]*Client{}
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		fields := strings.Fields(scanner.Text())
		if len(fields) < 4 {
			continue
		}
		exp, _ := strconv.ParseInt(fields[0], 10, 64)
		mac := fields[1]
		ip := fields[2]
		hostname := fields[3]
		if hostname == "*" {
			hostname = ""
		}
		out[ip] = &Client{
			IP:           ip,
			MAC:          mac,
			Hostname:     hostname,
			LeaseExpires: exp,
			IsDHCP:       true,
		}
	}
	return out, scanner.Err()
}

func parseARPTable() (map[string]*Client, error) {
	f, err := os.Open("/proc/net/arp")
	if err != nil {
		return nil, err
	}
	defer f.Close()

	out := map[string]*Client{}
	scanner := bufio.NewScanner(f)
	scanner.Scan()

	for scanner.Scan() {
		fields := strings.Fields(scanner.Text())
		if len(fields) < 6 {
			continue
		}
		ip := fields[0]
		flags := fields[2]
		mac := fields[3]
		iface := fields[5]

		if mac == "00:00:00:00:00:00" || iface == "lo" {
			continue
		}

		arpOnline := flags == "0x2" || flags == "0x6"

		out[ip] = &Client{
			IP:        ip,
			MAC:       mac,
			Interface: iface,
			Online:    arpOnline,
		}
	}
	return out, scanner.Err()
}

func ipLess(a, b string) bool {
	return ipToUint32(a) < ipToUint32(b)
}

func ipToUint32(ip string) uint32 {
	parts := strings.SplitN(ip, ".", 4)
	if len(parts) != 4 {
		return 0
	}
	var v uint32
	for _, p := range parts {
		n, _ := strconv.ParseUint(p, 10, 8)
		v = v<<8 | uint32(n)
	}
	return v
}