package tendrils

import (
	"context"
	"fmt"
	"log"
	"net"
	"sort"
	"strings"
	"time"

	"github.com/fvbommel/sortorder"
	"github.com/gopatchy/artnet"
)

type artnetHandler struct {
	t         *Tendrils
	discovery *artnet.Discovery
}

func (h *artnetHandler) HandleDMX(src *net.UDPAddr, pkt *artnet.DMXPacket) {}

func (h *artnetHandler) HandlePoll(src *net.UDPAddr, pkt *artnet.PollPacket) {
	h.discovery.HandlePoll(src)
}

func (h *artnetHandler) HandlePollReply(src *net.UDPAddr, pkt *artnet.PollReplyPacket) {
	h.discovery.HandlePollReply(src, pkt)
}

func (t *Tendrils) startArtNet(ctx context.Context, iface net.Interface) {
	srcIP, broadcast := getInterfaceIPv4(iface)
	if srcIP == nil || broadcast == nil {
		return
	}

	sender, err := artnet.NewInterfaceSender(iface.Name)
	if err != nil {
		log.Printf("[ERROR] failed to create artnet sender for %s: %v", iface.Name, err)
		return
	}

	discovery := artnet.NewDiscovery(sender, srcIP, broadcast, iface.HardwareAddr, "", "", nil, nil)
	discovery.SetOnChange(func(node *artnet.Node) {
		t.handleArtNetNode(node)
	})

	handler := &artnetHandler{t: t, discovery: discovery}

	receiver, err := artnet.NewInterfaceReceiver(iface.Name, handler)
	if err != nil {
		log.Printf("[ERROR] failed to create artnet receiver for %s: %v", iface.Name, err)
		sender.Close()
		return
	}

	discovery.SetReceiver(receiver)
	receiver.Start()
	discovery.Start()

	<-ctx.Done()

	discovery.Stop()
	receiver.Stop()
	sender.Close()
}

func (t *Tendrils) handleArtNetNode(node *artnet.Node) {
	ip := node.IP
	mac := node.MAC
	shortName := node.ShortName
	longName := node.LongName

	var inputs, outputs []int
	for _, u := range node.Inputs {
		inputs = append(inputs, int(u))
	}
	for _, u := range node.Outputs {
		outputs = append(outputs, int(u))
	}

	if t.DebugArtNet {
		log.Printf("[artnet] %s %s short=%q long=%q in=%v out=%v", ip, mac, shortName, longName, inputs, outputs)
	}

	name := longName
	if name == "" {
		name = shortName
	}
	if name != "" {
		t.nodes.Update(nil, mac, []net.IP{ip}, "", name, "artnet")
	}

	n := t.nodes.GetByIP(ip)
	if n == nil && mac != nil {
		n = t.nodes.GetByMAC(mac)
	}
	if n == nil && name != "" {
		n = t.nodes.GetOrCreateByName(name)
	}
	if n != nil {
		t.nodes.UpdateArtNet(n, inputs, outputs)
	}
	t.NotifyUpdate()
}

func (n *Nodes) UpdateArtNet(node *Node, inputs, outputs []int) {
	n.mu.Lock()
	defer n.mu.Unlock()

	if node.ArtNetInputs == nil {
		node.ArtNetInputs = ArtNetUniverseSet{}
	}
	if node.ArtNetOutputs == nil {
		node.ArtNetOutputs = ArtNetUniverseSet{}
	}

	for _, u := range inputs {
		node.ArtNetInputs.Add(ArtNetUniverse(u))
	}
	for _, u := range outputs {
		node.ArtNetOutputs.Add(ArtNetUniverse(u))
	}
}

func (n *Nodes) expireArtNet() {
	for _, node := range n.nodes {
		if node.ArtNetInputs != nil {
			node.ArtNetInputs.Expire(60 * time.Second)
		}
		if node.ArtNetOutputs != nil {
			node.ArtNetOutputs.Expire(60 * time.Second)
		}
	}
}

func (n *Nodes) mergeArtNet(keep, merge *Node) {
	if merge.ArtNetInputs != nil {
		if keep.ArtNetInputs == nil {
			keep.ArtNetInputs = ArtNetUniverseSet{}
		}
		for u, lastSeen := range merge.ArtNetInputs {
			if existing, ok := keep.ArtNetInputs[u]; !ok || lastSeen.After(existing) {
				keep.ArtNetInputs[u] = lastSeen
			}
		}
	}
	if merge.ArtNetOutputs != nil {
		if keep.ArtNetOutputs == nil {
			keep.ArtNetOutputs = ArtNetUniverseSet{}
		}
		for u, lastSeen := range merge.ArtNetOutputs {
			if existing, ok := keep.ArtNetOutputs[u]; !ok || lastSeen.After(existing) {
				keep.ArtNetOutputs[u] = lastSeen
			}
		}
	}
}

func (n *Nodes) logArtNet() {
	inputUniverses := map[ArtNetUniverse][]string{}
	outputUniverses := map[ArtNetUniverse][]string{}

	for _, node := range n.nodes {
		if len(node.ArtNetInputs) == 0 && len(node.ArtNetOutputs) == 0 {
			continue
		}
		name := node.DisplayName()
		if name == "" {
			name = "??"
		}
		for u := range node.ArtNetInputs {
			inputUniverses[u] = append(inputUniverses[u], name)
		}
		for u := range node.ArtNetOutputs {
			outputUniverses[u] = append(outputUniverses[u], name)
		}
	}

	if len(inputUniverses) == 0 && len(outputUniverses) == 0 {
		return
	}

	seen := map[ArtNetUniverse]bool{}
	var allUniverses []ArtNetUniverse
	for u := range inputUniverses {
		if !seen[u] {
			allUniverses = append(allUniverses, u)
			seen[u] = true
		}
	}
	for u := range outputUniverses {
		if !seen[u] {
			allUniverses = append(allUniverses, u)
			seen[u] = true
		}
	}
	sort.Slice(allUniverses, func(i, j int) bool { return allUniverses[i] < allUniverses[j] })

	log.Printf("[sigusr1] ================ %d artnet universes ================", len(allUniverses))
	for _, u := range allUniverses {
		ins := inputUniverses[u]
		outs := outputUniverses[u]
		var parts []string
		if len(ins) > 0 {
			sort.Slice(ins, func(i, j int) bool { return sortorder.NaturalLess(ins[i], ins[j]) })
			parts = append(parts, fmt.Sprintf("in: %s", strings.Join(ins, ", ")))
		}
		if len(outs) > 0 {
			sort.Slice(outs, func(i, j int) bool { return sortorder.NaturalLess(outs[i], outs[j]) })
			parts = append(parts, fmt.Sprintf("out: %s", strings.Join(outs, ", ")))
		}
		log.Printf("[sigusr1]   artnet:%d (%s) %s", u, u.String(), strings.Join(parts, "; "))
	}
}
