package tendrils

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

	"github.com/miekg/dns"
)

const (
	mdnsAddr = "224.0.0.251:5353"
)

func isSkaarhojService(s string) bool {
	return strings.Contains(s, "_skaarhoj._tcp")
}

func extractSkaarhojName(s string) string {
	idx := strings.Index(s, "._skaarhoj._tcp")
	if idx <= 0 {
		return ""
	}
	return strings.ReplaceAll(s[:idx], "\\", "")
}

func isNetaudioService(s string) bool {
	return strings.Contains(s, "_netaudio-cmc._udp") || strings.Contains(s, "_netaudio-arc._udp") || strings.Contains(s, "_netaudio-chan._udp")
}

func extractNetaudioName(s string) string {
	for _, suffix := range []string{"._netaudio-cmc._udp", "._netaudio-arc._udp"} {
		idx := strings.Index(s, suffix)
		if idx > 0 {
			return strings.ReplaceAll(s[:idx], "\\", "")
		}
	}
	return ""
}

func extractNetaudioChanDevice(s string) string {
	idx := strings.Index(s, "._netaudio-chan._udp")
	if idx <= 0 {
		return ""
	}
	name := strings.ReplaceAll(s[:idx], "\\", "")
	if atIdx := strings.Index(name, "@"); atIdx >= 0 {
		return name[atIdx+1:]
	}
	return ""
}

func (t *Tendrils) listenMDNS(ctx context.Context, iface net.Interface) {
	addr, err := net.ResolveUDPAddr("udp4", mdnsAddr)
	if err != nil {
		log.Printf("[ERROR] failed to resolve mdns address: %v", err)
		return
	}

	conn, err := net.ListenMulticastUDP("udp4", &iface, addr)
	if err != nil {
		log.Printf("[ERROR] failed to listen mdns on %s: %v", iface.Name, err)
		return
	}
	defer conn.Close()

	go t.runMDNSQuerier(ctx, iface, conn)

	buf := make([]byte, 65536)
	for {
		select {
		case <-ctx.Done():
			return
		default:
		}

		conn.SetReadDeadline(time.Now().Add(1 * time.Second))
		n, src, err := conn.ReadFromUDP(buf)
		if err != nil {
			if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
				continue
			}
			continue
		}

		t.handleMDNSPacket(iface.Name, src.IP, buf[:n])
	}
}

func (t *Tendrils) handleMDNSPacket(ifaceName string, srcIP net.IP, data []byte) {
	msg := new(dns.Msg)
	if err := msg.Unpack(data); err != nil {
		return
	}

	if msg.Response {
		t.processMDNSResponse(ifaceName, srcIP, msg)
	}
}

func (t *Tendrils) processMDNSResponse(ifaceName string, srcIP net.IP, msg *dns.Msg) {
	allRecords := append(msg.Answer, msg.Extra...)

	if t.DebugMDNS {
		for _, rr := range allRecords {
			log.Printf("[mdns] %s: record %s", ifaceName, rr.String())
		}
	}

	aRecords := map[string]net.IP{}
	srvTargets := map[string]string{}
	skaarhojNames := map[string]bool{}
	netaudioNames := map[string]bool{}

	for _, rr := range allRecords {
		switch r := rr.(type) {
		case *dns.A:
			name := strings.TrimSuffix(r.Hdr.Name, ".")
			aRecords[name] = r.A
		case *dns.AAAA:
			continue
		case *dns.PTR:
			if isSkaarhojService(r.Ptr) {
				name := extractSkaarhojName(r.Ptr)
				if name != "" {
					skaarhojNames[name] = true
				}
			}
		case *dns.SRV:
			target := strings.TrimSuffix(r.Target, ".")
			if isSkaarhojService(r.Hdr.Name) {
				name := extractSkaarhojName(r.Hdr.Name)
				if name != "" {
					skaarhojNames[name] = true
					if target != "" {
						srvTargets[name] = target
					}
				}
			}
			if isNetaudioService(r.Hdr.Name) {
				name := extractNetaudioName(r.Hdr.Name)
				if name == "" {
					name = extractNetaudioChanDevice(r.Hdr.Name)
				}
				if name != "" {
					netaudioNames[name] = true
					if target != "" {
						srvTargets[name] = target
					}
				}
			}
		}
	}

	for name := range skaarhojNames {
		var ip net.IP
		if target, ok := srvTargets[name]; ok {
			ip = aRecords[target]
		}
		if ip == nil {
			ip = srcIP
		}
		if t.DebugMDNS {
			log.Printf("[mdns] %s: skaarhoj %s -> %s", ifaceName, name, ip)
		}
		t.nodes.Update(nil, nil, []net.IP{ip}, "", name, "skaarhoj")
	}

	for name := range netaudioNames {
		var ip net.IP
		var targetHostname string
		if target, ok := srvTargets[name]; ok {
			ip = aRecords[target]
			targetHostname = strings.TrimSuffix(target, ".local")
		}
		if ip == nil {
			ip = srcIP
		}
		if t.DebugMDNS {
			log.Printf("[mdns] %s: netaudio %s -> %s (target %s)", ifaceName, name, ip, targetHostname)
		}
		t.nodes.Update(nil, nil, []net.IP{ip}, "", name, "mdns-srv")
		if targetHostname != "" && targetHostname != name {
			t.nodes.Update(nil, nil, []net.IP{ip}, "", targetHostname, "mdns-srv")
		}
	}

	if len(skaarhojNames) == 0 && len(netaudioNames) == 0 {
		for aName, ip := range aRecords {
			hostname := strings.TrimSuffix(aName, ".local")
			if hostname != "" && hostname != aName && !strings.Contains(hostname, "in-addr") && !strings.Contains(hostname, "ip6.arpa") {
				if t.DebugMDNS {
					log.Printf("[mdns] %s: %s -> %s", ifaceName, ip, hostname)
				}
				t.nodes.Update(nil, nil, []net.IP{ip}, "", hostname, "mdns")
			}
		}
	}
}

var mdnsServices = []string{
	"_services._dns-sd._udp.local.",
	"_skaarhoj._tcp.local.",
	"_smb._tcp.local.",
	"_airplay._tcp.local.",
	"_qlab._tcp.local.",
	"_blackmagic._tcp.local.",
	"_hyperdeck_ctrl._tcp.local.",
	"_netaudio-cmc._udp.local.",
	"_netaudio-arc._udp.local.",
	"_netaudio-chan._udp.local.",
}

func (t *Tendrils) runMDNSQuerier(ctx context.Context, iface net.Interface, conn *net.UDPConn) {
	ticker := time.NewTicker(30 * time.Second)
	defer ticker.Stop()

	t.sendMDNSQueries(iface.Name, conn)

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			t.sendMDNSQueries(iface.Name, conn)
		}
	}
}

var mdnsQueryOffset int

func (t *Tendrils) sendMDNSQueries(ifaceName string, conn *net.UDPConn) {
	dest := &net.UDPAddr{IP: net.IPv4(224, 0, 0, 251), Port: 5353}

	n := len(mdnsServices)
	for i := 0; i < n; i++ {
		service := mdnsServices[(i+mdnsQueryOffset)%n]
		msg := new(dns.Msg)
		msg.SetQuestion(service, dns.TypePTR)
		msg.RecursionDesired = false

		data, err := msg.Pack()
		if err != nil {
			continue
		}

		conn.WriteToUDP(data, dest)
	}
	mdnsQueryOffset++

	if t.DebugMDNS {
		log.Printf("[mdns] %s: sent queries for %d services", ifaceName, len(mdnsServices))
	}
}
