package udp

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

	"hamlog/internal/applog"
)

// WSJT-X Reply (message type 4) — "answer this station".
//
// It is the same thing as double-clicking the line in WSJT-X's own Band
// Activity window: the application looks the decode up in its list, sets its
// transmit frequency to the caller's, fills the DX call and starts the exchange.
// Which is why OpsLog cannot do this by tuning the radio — on FT8 the whole band
// sits inside one passband, so moving the dial changes nothing about who gets
// answered. The decision belongs to the decoding application, and this is the
// only way to hand it over.
//
// The payload REPLAYS the decode being answered, and WSJT-X matches it against
// what it decoded. Every field has to come back exactly as it went out — which
// is why the parser now keeps the time, the delta time, the audio offset and the
// message text rather than only what the panadapter needed.
//
//	Reply    type 4
//	  id                utf8    the target application's own id
//	  time              quint32 ms since midnight, from the decode
//	  snr               qint32
//	  delta_time        double  seconds
//	  delta_frequency   quint32 audio offset in the passband, Hz
//	  mode              utf8
//	  message           utf8
//	  low_confidence    bool
//	  modifiers         quint8  keyboard modifiers (0 = a plain click)
const wsjtMsgReply = 4

// Reply is one "call this station" request, rebuilt from a decode.
type Reply struct {
	ProgramID     string // which application to talk to ("WSJT-X", "MSHV", "WSJT-X - 2")
	MsSinceMidnig uint32
	SNR           int32
	DeltaTime     float64
	DeltaFreqHz   uint32
	Mode          string
	Message       string
	LowConfidence bool
}

// writeQString writes a Qt QString/QUtf8: a big-endian int32 length then the
// bytes. An EMPTY string is length 0, not the -1 that means null — WSJT-X reads
// a null where it expects text as a malformed packet and drops the whole reply.
func writeQString(b *bytes.Buffer, s string) {
	_ = binary.Write(b, binary.BigEndian, int32(len(s)))
	b.WriteString(s)
}

// EncodeReply builds the datagram.
func EncodeReply(r Reply) []byte {
	var b bytes.Buffer
	_ = binary.Write(&b, binary.BigEndian, uint32(wsjtMagic))
	_ = binary.Write(&b, binary.BigEndian, uint32(2)) // schema 2 — the one every current sender speaks
	_ = binary.Write(&b, binary.BigEndian, uint32(wsjtMsgReply))
	writeQString(&b, r.ProgramID)
	_ = binary.Write(&b, binary.BigEndian, r.MsSinceMidnig)
	_ = binary.Write(&b, binary.BigEndian, r.SNR)
	_ = binary.Write(&b, binary.BigEndian, r.DeltaTime)
	_ = binary.Write(&b, binary.BigEndian, r.DeltaFreqHz)
	writeQString(&b, r.Mode)
	writeQString(&b, r.Message)
	var low uint8
	if r.LowConfidence {
		low = 1
	}
	_ = binary.Write(&b, binary.BigEndian, low)
	_ = binary.Write(&b, binary.BigEndian, uint8(0)) // modifiers: a plain click
	return b.Bytes()
}

// SendReply hands a Reply to the application that produced the decode.
//
// Routed by PROGRAM ID, not by listener: two receivers can share one multicast
// group, and answering a station heard on the 6 m instance by talking to the
// 20 m one would start a call on the wrong band. The id is what tells them
// apart, and the address the reply goes to is the one that instance's packets
// actually arrive from — a multicast listener must answer back to the sender,
// not to the group.
func (m *Manager) SendReply(r Reply) error {
	if strings.TrimSpace(r.ProgramID) == "" {
		return fmt.Errorf("no application id — cannot tell which receiver to answer with")
	}
	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(r.ProgramID)
		if conn == nil || addr == nil {
			continue
		}
		pkt := EncodeReply(r)
		if _, err := conn.WriteToUDP(pkt, addr); err != nil {
			return fmt.Errorf("send reply to %s at %s: %w", r.ProgramID, addr, err)
		}
		// Says whether the decode being answered was a CQ, because that is what
		// decides whether the far end will act on it at all: the protocol only
		// requires a Reply to be honoured for a CQ or QRZ. Without this the log
		// showed a reply going out and nothing happening, and the reason had to
		// be deduced from the message text by eye.
		kind := "not a CQ — the far end may ignore it"
		if strings.HasPrefix(strings.TrimSpace(strings.ToUpper(r.Message)), "CQ ") {
			kind = "CQ"
		}
		applog.Printf("udp: reply sent to %s at %s — %q (%s)", r.ProgramID, addr, r.Message, kind)
		return nil
	}
	return fmt.Errorf("no packet has arrived from %q yet — nothing to answer to", r.ProgramID)
}

// replyTarget returns this listener's socket and the address the given program
// last sent from, or nils when it has never been heard here.
func (s *Server) replyTarget(programID string) (*net.UDPConn, *net.UDPAddr) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.conn == nil || s.lastFrom == nil {
		return nil, nil
	}
	addr, ok := s.lastFrom[programID]
	if !ok {
		return nil, nil
	}
	return s.conn, addr
}
