package mdns

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"strings"
	"syscall"
	"time"

	"github.com/AlexxIT/go2rtc/pkg/xnet"
	"github.com/miekg/dns" // awesome library for parsing mDNS records
)

const (
	ServiceDNSSD = "_services._dns-sd._udp.local."
	ServiceHAP   = "_hap._tcp.local." // HomeKit Accessory Protocol
)

type ServiceEntry struct {
	Name string            `json:"name,omitempty"`
	IP   net.IP            `json:"ip,omitempty"`
	Port uint16            `json:"port,omitempty"`
	Info map[string]string `json:"info,omitempty"`
}

func (e *ServiceEntry) String() string {
	b, err := json.Marshal(e)
	if err != nil {
		return err.Error()
	}
	return string(b)
}

func (e *ServiceEntry) TXT() []string {
	var txt []string
	for k, v := range e.Info {
		txt = append(txt, k+"="+v)
	}
	return txt
}

func (e *ServiceEntry) Complete() bool {
	return e.IP != nil && e.Port > 0 && e.Info != nil
}

func (e *ServiceEntry) Addr() string {
	return fmt.Sprintf("%s:%d", e.IP, e.Port)
}

func (e *ServiceEntry) Host(service string) string {
	return e.name() + "." + strings.TrimRight(service, ".")
}

func (e *ServiceEntry) name() string {
	b := []byte(e.Name)
	for i, c := range b {
		if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' {
			continue
		}
		b[i] = '-'
	}
	return string(b)
}

var MulticastAddr = &net.UDPAddr{
	IP:   net.IP{224, 0, 0, 251},
	Port: 5353,
}

const sendTimeout = time.Millisecond * 505
const respTimeout = time.Second * 3

// BasicDiscovery - default golang Multicast UDP listener.
// Does not work well with multiple interfaces.
func BasicDiscovery(service string, onentry func(*ServiceEntry) bool) error {
	conn, err := net.ListenMulticastUDP("udp4", nil, MulticastAddr)
	if err != nil {
		return err
	}

	b := Browser{
		Service:     service,
		Addr:        MulticastAddr,
		Recv:        conn,
		Sends:       []net.PacketConn{conn},
		RecvTimeout: respTimeout,
		SendTimeout: sendTimeout,
	}

	defer b.Close()

	return b.Browse(onentry)
}

// Discovery - better discovery version. Works well with multiple interfaces.
func Discovery(service string, onentry func(*ServiceEntry) bool) error {
	b := Browser{
		Service:     service,
		Addr:        MulticastAddr,
		RecvTimeout: respTimeout,
		SendTimeout: sendTimeout,
	}

	if err := b.ListenMulticastUDP(); err != nil {
		return err
	}

	defer b.Close()

	return b.Browse(onentry)
}

// Query - direct Discovery request on device IP-address. Works even over VPN.
func Query(host, service string) (entry *ServiceEntry, err error) {
	conn, err := net.ListenPacket("udp4", ":0") // shouldn't use ":5353"
	if err != nil {
		return
	}

	br := Browser{
		Service: service,
		Addr: &net.UDPAddr{
			IP:   net.ParseIP(host),
			Port: 5353,
		},
		Recv:        conn,
		Sends:       []net.PacketConn{conn},
		SendTimeout: time.Millisecond * 255,
		RecvTimeout: time.Second,
	}

	defer br.Close()

	err = br.Browse(func(en *ServiceEntry) bool {
		entry = en
		return true
	})

	return
}

// QueryOrDiscovery - useful if we know previous device host and want
// to update port or any other information. Will work even over VPN.
func QueryOrDiscovery(host, service string, onentry func(*ServiceEntry) bool) error {
	entry, _ := Query(host, service)
	if entry != nil && onentry(entry) {
		return nil
	}

	return Discovery(service, onentry)
}

type Browser struct {
	Service string

	Addr  net.Addr
	Nets  []*net.IPNet
	Recv  net.PacketConn
	Sends []net.PacketConn

	RecvTimeout time.Duration
	SendTimeout time.Duration
}

// ListenMulticastUDP - creates multiple senders socket (each for IP4 interface).
// And one receiver with multicast membership for each sender.
// Receiver will get multicast responses on senders requests.
func (b *Browser) ListenMulticastUDP() error {
	// 1. Collect IPv4 interfaces
	nets, err := xnet.IPNets(func(ip net.IP) bool {
		return !xnet.Docker.Contains(ip)
	})
	if err != nil {
		return err
	}

	// 2. Create senders
	lc1 := net.ListenConfig{
		Control: func(network, address string, c syscall.RawConn) error {
			return c.Control(func(fd uintptr) {
				// 1. Allow multicast UDP to listen concurrently across multiple listeners
				_ = SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
			})
		},
	}

	ctx := context.Background()

	for _, ipn := range nets {
		conn, err := lc1.ListenPacket(ctx, "udp4", ipn.IP.String()+":5353") // same port important
		if err != nil {
			continue
		}
		b.Nets = append(b.Nets, ipn)
		b.Sends = append(b.Sends, conn)
	}

	if b.Sends == nil {
		return errors.New("no interfaces for listen")
	}

	// 3. Create receiver
	lc2 := net.ListenConfig{
		Control: func(network, address string, c syscall.RawConn) error {
			return c.Control(func(fd uintptr) {
				// 1. Allow multicast UDP to listen concurrently across multiple listeners
				_ = SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)

				// 2. Disable loop responses
				_ = SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_MULTICAST_LOOP, 0)

				// 3. Allow receive multicast responses on all this addresses
				mreq := &syscall.IPMreq{
					Multiaddr: [4]byte{224, 0, 0, 251},
				}
				_ = SetsockoptIPMreq(fd, syscall.IPPROTO_IP, syscall.IP_ADD_MEMBERSHIP, mreq)

				for _, send := range b.Sends {
					addr := send.LocalAddr().(*net.UDPAddr)
					mreq.Interface = [4]byte(addr.IP.To4())
					_ = SetsockoptIPMreq(fd, syscall.IPPROTO_IP, syscall.IP_ADD_MEMBERSHIP, mreq)
				}
			})
		},
	}

	b.Recv, err = lc2.ListenPacket(ctx, "udp4", ":5353")

	return err
}

func (b *Browser) Browse(onentry func(*ServiceEntry) bool) error {
	msg := &dns.Msg{
		Question: []dns.Question{
			{Name: b.Service, Qtype: dns.TypePTR, Qclass: dns.ClassINET},
		},
	}

	query, err := msg.Pack()
	if err != nil {
		return err
	}

	if err = b.Recv.SetDeadline(time.Now().Add(b.RecvTimeout)); err != nil {
		return err
	}

	go func() {
		for {
			for _, send := range b.Sends {
				if _, err := send.WriteTo(query, b.Addr); err != nil {
					return
				}
			}
			time.Sleep(b.SendTimeout)
		}
	}()

	processed := map[string]struct{}{"": {}}

	b2 := make([]byte, 1500)
	for {
		// in the Hass docker network can receive same msg from different address
		n, addr, err := b.Recv.ReadFrom(b2)
		if err != nil {
			break
		}

		if err = msg.Unpack(b2[:n]); err != nil {
			continue
		}

		ptr := GetPTR(msg, b.Service)

		if _, ok := processed[ptr]; ok {
			continue
		}

		ip := addr.(*net.UDPAddr).IP

		for _, entry := range NewServiceEntries(msg, ip) {
			if onentry(entry) {
				return nil
			}
		}

		processed[ptr] = struct{}{}
	}

	return nil
}

func (b *Browser) Close() error {
	if b.Recv != nil {
		_ = b.Recv.Close()
	}
	for _, send := range b.Sends {
		_ = send.Close()
	}
	return nil
}

func GetPTR(msg *dns.Msg, service string) string {
	for _, record := range msg.Answer {
		if ptr, ok := record.(*dns.PTR); ok && ptr.Hdr.Name == service {
			return ptr.Ptr
		}
	}
	return ""
}

func NewServiceEntries(msg *dns.Msg, ip net.IP) (entries []*ServiceEntry) {
	records := make([]dns.RR, 0, len(msg.Answer)+len(msg.Ns)+len(msg.Extra))
	records = append(records, msg.Answer...)
	records = append(records, msg.Ns...)
	records = append(records, msg.Extra...)

	// PTR ptr=SomeName._hap._tcp.local. hdr=_hap._tcp.local.
	// TXT txt=...                       hdr=SomeName._hap._tcp.local.
	// SRV target=SomeName.local.        hdr=SomeName._hap._tcp.local.
	// A   a=192.168.1.123               hdr=SomeName.local.

	for _, record := range records {
		ptr, ok := record.(*dns.PTR)
		if !ok {
			continue
		}

		entry := &ServiceEntry{}

		if i := strings.IndexByte(ptr.Ptr, '.'); i > 0 {
			entry.Name = strings.ReplaceAll(ptr.Ptr[:i], `\ `, " ")
		}

		var txt *dns.TXT
		var srv *dns.SRV
		var a *dns.A

		for _, record = range records {
			if txt, ok = record.(*dns.TXT); ok && txt.Hdr.Name == ptr.Ptr {
				entry.Info = make(map[string]string, len(txt.Txt))
				for _, s := range txt.Txt {
					k, v, _ := strings.Cut(s, "=")
					entry.Info[k] = v
				}
				break
			}
		}

		for _, record = range records {
			if srv, ok = record.(*dns.SRV); ok && srv.Hdr.Name == ptr.Ptr {
				entry.Port = srv.Port

				for _, record = range records {
					if a, ok = record.(*dns.A); ok && a.Hdr.Name == srv.Target {
						// device can send multiple IP addresses (ex. Homebridge)
						// use first IP from the list or same IP from sender
						if entry.IP == nil || ip.Equal(a.A) {
							entry.IP = a.A
						}
					}
				}
				break
			}
		}

		entries = append(entries, entry)
	}

	return
}
