package udp

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"strings"

	"hamlog/internal/applog"
)

// WSJT-X Highlight Callsign (13) and Replay (7) — the two halves of making the
// Band Activity window log-aware.
//
// Highlight paints a callsign in the decoding application's own window with the
// colours OpsLog chooses — new DXCC, new band, a watchlist member — the way
// JTAlert does. Replay asks a freshly-discovered instance to resend the decodes
// it already has on screen, so the FT decodes panel starts full instead of
// empty until the next period.

const (
	wsjtMsgReplay    = 7
	wsjtMsgHighlight = 13
)

// RGB is one highlight colour. A nil *RGB means "invalid QColor", which is the
// protocol's way of saying "remove the highlight".
type RGB struct{ R, G, B uint8 }

// writeQColor serializes a QColor as QDataStream does: a spec byte (1 = RGB,
// 0 = invalid) followed by five 16-bit channels (alpha, red, green, blue, pad),
// each 8-bit value doubled into 16 bits the way Qt stores them.
func writeQColor(b *bytes.Buffer, c *RGB) {
	if c == nil {
		b.WriteByte(0) // invalid — clears the highlight
		for i := 0; i < 5; i++ {
			_ = binary.Write(b, binary.BigEndian, uint16(0))
		}
		return
	}
	b.WriteByte(1) // spec = RGB
	wide := func(v uint8) uint16 { return uint16(v) * 0x101 }
	_ = binary.Write(b, binary.BigEndian, uint16(0xFFFF)) // alpha, opaque
	_ = binary.Write(b, binary.BigEndian, wide(c.R))
	_ = binary.Write(b, binary.BigEndian, wide(c.G))
	_ = binary.Write(b, binary.BigEndian, wide(c.B))
	_ = binary.Write(b, binary.BigEndian, uint16(0)) // pad
}

// EncodeHighlight builds a Highlight Callsign datagram. bg/fg nil = invalid
// colour; both nil clears the callsign's highlight.
func EncodeHighlight(programID, callsign string, bg, fg *RGB, lastPeriodOnly bool) []byte {
	var b bytes.Buffer
	_ = binary.Write(&b, binary.BigEndian, uint32(wsjtMagic))
	_ = binary.Write(&b, binary.BigEndian, uint32(2))
	_ = binary.Write(&b, binary.BigEndian, uint32(wsjtMsgHighlight))
	writeQString(&b, programID)
	writeQString(&b, callsign)
	writeQColor(&b, bg)
	writeQColor(&b, fg)
	var last uint8
	if lastPeriodOnly {
		last = 1
	}
	_ = binary.Write(&b, binary.BigEndian, last)
	return b.Bytes()
}

// EncodeReplay builds a Replay datagram — "resend what your window holds".
func EncodeReplay(programID string) []byte {
	var b bytes.Buffer
	_ = binary.Write(&b, binary.BigEndian, uint32(wsjtMagic))
	_ = binary.Write(&b, binary.BigEndian, uint32(2))
	_ = binary.Write(&b, binary.BigEndian, uint32(wsjtMsgReplay))
	writeQString(&b, programID)
	return b.Bytes()
}

// sendToInstance routes a raw datagram to the application that owns programID,
// the same way SendReply does: to the address its packets actually arrive from.
func (m *Manager) sendToInstance(programID string, pkt []byte, what string) error {
	if strings.TrimSpace(programID) == "" {
		return fmt.Errorf("no application id")
	}
	m.mu.Lock()
	servers := make([]*Server, 0, len(m.inbound))
	for _, s := range m.inbound {
		servers = append(servers, s)
	}
	m.mu.Unlock()
	for _, s := range servers {
		conn, addr := s.replyTarget(programID)
		if conn == nil || addr == nil {
			continue
		}
		if _, err := conn.WriteToUDP(pkt, addr); err != nil {
			return fmt.Errorf("send %s to %s at %s: %w", what, programID, addr, err)
		}
		return nil
	}
	return fmt.Errorf("no packet has arrived from %q yet", programID)
}

// SendHighlight paints (or clears) one callsign in the given instance.
func (m *Manager) SendHighlight(programID, callsign string, bg, fg *RGB, lastPeriodOnly bool) error {
	return m.sendToInstance(programID, EncodeHighlight(programID, callsign, bg, fg, lastPeriodOnly), "highlight")
}

// SendClearHighlights removes every highlighting instruction OpsLog installed
// in the instance. "CLEARALL!" is the protocol's own magic callsign for it.
func (m *Manager) SendClearHighlights(programID string) error {
	return m.sendToInstance(programID, EncodeHighlight(programID, "CLEARALL!", nil, nil, false), "clear-highlights")
}

// SendReplay asks the instance to resend its on-screen decodes.
func (m *Manager) SendReplay(programID string) error {
	err := m.sendToInstance(programID, EncodeReplay(programID), "replay")
	if err == nil {
		applog.Printf("udp: replay requested from %q — its existing decodes will arrive marked not-new", programID)
	}
	return err
}

// Instances lists every program id a packet has arrived from, for "clear the
// highlights everywhere" and the startup replay.
func (m *Manager) Instances() []string {
	m.mu.Lock()
	servers := make([]*Server, 0, len(m.inbound))
	for _, s := range m.inbound {
		servers = append(servers, s)
	}
	m.mu.Unlock()
	seen := map[string]struct{}{}
	var out []string
	for _, s := range servers {
		s.mu.Lock()
		for id := range s.lastFrom {
			if _, dup := seen[id]; !dup {
				seen[id] = struct{}{}
				out = append(out, id)
			}
		}
		s.mu.Unlock()
	}
	return out
}
