package tendrils

import (
	"context"
	"encoding/binary"
	"log"
	"net"
	"strings"
	"time"
)

func (t *Tendrils) listenBMD(ctx context.Context, iface net.Interface) {
	go t.discoverATEMs(ctx, iface)
}

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

	conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: srcIP, Port: 0})
	if err != nil {
		return
	}
	defer conn.Close()

	ticker := time.NewTicker(30 * time.Second)
	defer ticker.Stop()

	go t.atemDiscoveryLoop(ctx, conn, broadcast, iface.Name)

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

func (t *Tendrils) atemDiscoveryLoop(ctx context.Context, conn *net.UDPConn, broadcast net.IP, ifaceName string) {
	// Send hello to broadcast
	hello := []byte{
		0x10, 0x14,
		0x00, 0x00,
		0x00, 0x00,
		0x00, 0x00,
		0x00, 0x00,
		0x00, 0x00,
		0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	}
	conn.WriteToUDP(hello, &net.UDPAddr{IP: broadcast, Port: 9910})

	if t.DebugBMD {
		log.Printf("[bmd] %s: sent atem discovery to %s", ifaceName, broadcast)
	}

	// Collect responses and initiate sessions
	sessions := map[string]*atemSession{}
	buf := make([]byte, 2048)

	deadline := time.Now().Add(5 * time.Second)
	for time.Now().Before(deadline) {
		select {
		case <-ctx.Done():
			return
		default:
		}

		conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
		n, src, err := conn.ReadFromUDP(buf)
		if err != nil {
			continue
		}

		if n < 12 {
			continue
		}

		ipKey := src.IP.String()
		sess := sessions[ipKey]
		if sess == nil {
			sess = &atemSession{ip: src.IP}
			sessions[ipKey] = sess
		}

		t.handleATEMPacket(conn, src, buf[:n], sess, ifaceName)

		if sess.productName != "" && !sess.updated {
			sess.updated = true
			if t.DebugBMD {
				log.Printf("[bmd] %s: atem %s at %s", ifaceName, sess.productName, src.IP)
			}
			t.nodes.Update(nil, nil, []net.IP{src.IP}, "", sess.productName, "bmd")
		}
	}

	// Update any ATEMs we found but couldn't get name for
	for _, sess := range sessions {
		if !sess.updated {
			if t.DebugBMD {
				log.Printf("[bmd] %s: atem (unknown) at %s", ifaceName, sess.ip)
			}
			t.nodes.Update(nil, nil, []net.IP{sess.ip}, "", "atem", "bmd")
		}
	}
}

type atemSession struct {
	ip          net.IP
	sessionID   uint16
	remoteSeq   uint16
	productName string
	updated     bool
}

func (t *Tendrils) handleATEMPacket(conn *net.UDPConn, src *net.UDPAddr, data []byte, sess *atemSession, ifaceName string) {
	flags := data[0] >> 3
	length := int(binary.BigEndian.Uint16(data[0:2]) & 0x07FF)

	if length > len(data) {
		return
	}

	sessionID := binary.BigEndian.Uint16(data[2:4])
	remoteSeq := binary.BigEndian.Uint16(data[10:12])

	// Hello response - extract session ID
	if flags&0x02 != 0 {
		sess.sessionID = sessionID
		// Send ACK
		ack := make([]byte, 12)
		ack[0] = 0x80
		ack[1] = 0x0c
		binary.BigEndian.PutUint16(ack[2:4], sessionID)
		binary.BigEndian.PutUint16(ack[4:6], remoteSeq)
		conn.WriteToUDP(ack, src)
	}

	// ACK request - send ACK
	if flags&0x01 != 0 {
		sess.remoteSeq = remoteSeq
		ack := make([]byte, 12)
		ack[0] = 0x80
		ack[1] = 0x0c
		binary.BigEndian.PutUint16(ack[2:4], sessionID)
		binary.BigEndian.PutUint16(ack[4:6], remoteSeq)
		conn.WriteToUDP(ack, src)
	}

	// Parse commands in payload
	if length > 12 {
		t.parseATEMCommands(data[12:length], sess)
	}
}

func (t *Tendrils) parseATEMCommands(data []byte, sess *atemSession) {
	offset := 0
	for offset+8 <= len(data) {
		cmdLen := int(binary.BigEndian.Uint16(data[offset : offset+2]))
		if cmdLen < 8 || offset+cmdLen > len(data) {
			break
		}

		cmdName := string(data[offset+4 : offset+8])

		if cmdName == "_pin" && cmdLen > 8 {
			// Product Information Name
			nameData := data[offset+8 : offset+cmdLen]
			nullIdx := 0
			for i, b := range nameData {
				if b == 0 {
					nullIdx = i
					break
				}
			}
			if nullIdx > 0 {
				sess.productName = strings.TrimSpace(string(nameData[:nullIdx]))
			}
		}

		offset += cmdLen
	}
}

func (t *Tendrils) probeBMDDevice(ip net.IP) {
	if name := t.probeHyperDeck(ip); name != "" {
		t.nodes.Update(nil, nil, []net.IP{ip}, "", name, "bmd")
		return
	}
	if name := t.probeVideoHub(ip); name != "" {
		t.nodes.Update(nil, nil, []net.IP{ip}, "", name, "bmd")
		return
	}
}

func (t *Tendrils) probeHyperDeck(ip net.IP) string {
	conn, err := net.DialTimeout("tcp", ip.String()+":9993", 500*time.Millisecond)
	if err != nil {
		return ""
	}
	defer conn.Close()

	conn.SetReadDeadline(time.Now().Add(1 * time.Second))
	buf := make([]byte, 1024)
	n, err := conn.Read(buf)
	if err != nil || n == 0 {
		return ""
	}

	response := string(buf[:n])
	for _, line := range strings.Split(response, "\r\n") {
		if strings.HasPrefix(line, "model: ") {
			model := strings.TrimPrefix(line, "model: ")
			if t.DebugBMD {
				log.Printf("[bmd] hyperdeck %s at %s", model, ip)
			}
			return model
		}
	}
	return ""
}

func (t *Tendrils) probeVideoHub(ip net.IP) string {
	conn, err := net.DialTimeout("tcp", ip.String()+":9990", 500*time.Millisecond)
	if err != nil {
		return ""
	}
	defer conn.Close()

	conn.SetReadDeadline(time.Now().Add(1 * time.Second))
	buf := make([]byte, 1024)
	n, err := conn.Read(buf)
	if err != nil || n == 0 {
		return ""
	}

	response := string(buf[:n])
	for _, line := range strings.Split(response, "\n") {
		if strings.HasPrefix(line, "Model name: ") {
			model := strings.TrimSpace(strings.TrimPrefix(line, "Model name: "))
			if t.DebugBMD {
				log.Printf("[bmd] videohub %s at %s", model, ip)
			}
			return model
		}
	}
	return ""
}
