package udp

import (
	"fmt"
	"net"
	"strings"

	"hamlog/internal/applog"
)

// This file holds the outbound emitters: OpsLog → other programs over UDP.
// Formats are chosen per connection row (like the inbound parsers), so the user
// can point PstRotator, a second logger, an SDR, etc. at OpsLog.

// tensOfHz converts a frequency in Hz to the "tens of Hz" unit N1MM and
// PstRotator use for their frequency fields (e.g. 14 025 500 Hz → 1 402 550).
func tensOfHz(freqHz int64) int64 { return freqHz / 10 }

// BuildPstFreq builds the datagram PstRotatorAz expects for its "DXLog.net"
// tracker: <PST><FREQUENCY>{tens of Hz}</FREQUENCY></PST>. Verified by probing a
// live PstRotatorAz — it reads the value as tens of Hz (14025.5 kHz → 1402550 →
// displayed 3.5255 MHz for 3525.5 kHz, etc.).
func BuildPstFreq(freqHz int64) []byte {
	return []byte(fmt.Sprintf("<PST><FREQUENCY>%d</FREQUENCY></PST>", tensOfHz(freqHz)))
}

// BuildN1MMRadioInfo builds an N1MM Logger+ RadioInfo XML datagram. <Freq> and
// <TXFreq> are in tens of Hz. Consumed by PstRotator (as the "N1MM Logger"
// tracker) and many other programs. mode is passed through (CW/USB/LSB/…).
func BuildN1MMRadioInfo(station string, rxFreqHz, txFreqHz int64, mode, opCall string) []byte {
	if station == "" {
		station = "OPSLOG"
	}
	if txFreqHz == 0 {
		txFreqHz = rxFreqHz
	}
	var b strings.Builder
	b.WriteString(`<?xml version="1.0" encoding="utf-8"?>`)
	b.WriteString(`<RadioInfo>`)
	b.WriteString(`<StationName>` + xmlEsc(station) + `</StationName>`)
	b.WriteString(`<RadioNr>1</RadioNr>`)
	b.WriteString(fmt.Sprintf(`<Freq>%d</Freq>`, tensOfHz(rxFreqHz)))
	b.WriteString(fmt.Sprintf(`<TXFreq>%d</TXFreq>`, tensOfHz(txFreqHz)))
	b.WriteString(`<Mode>` + xmlEsc(mode) + `</Mode>`)
	b.WriteString(`<OpCall>` + xmlEsc(opCall) + `</OpCall>`)
	b.WriteString(`<IsRunning>True</IsRunning>`)
	b.WriteString(`<FocusEntry>0</FocusEntry>`)
	b.WriteString(`<Antenna>0</Antenna>`)
	b.WriteString(`<Rotors></Rotors>`)
	b.WriteString(`<FocusRadioNr>1</FocusRadioNr>`)
	b.WriteString(`<IsStereo>False</IsStereo>`)
	b.WriteString(`<ActiveRadioNr>1</ActiveRadioNr>`)
	b.WriteString(`</RadioInfo>`)
	return []byte(b.String())
}

func xmlEsc(s string) string {
	r := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;", `"`, "&quot;", "'", "&apos;")
	return r.Replace(s)
}

// RadioState is a snapshot the app pushes to EmitRadioState on freq/mode change.
type RadioState struct {
	StationName string
	OpCall      string
	RxFreqHz    int64 // operating/RX frequency
	TxFreqHz    int64 // TX frequency (may equal RX when not split)
	Mode        string
}

// EmitRadioState sends the current radio frequency/mode to every enabled
// outbound row whose format is frequency-based (PstRotator, N1MM RadioInfo).
// Best-effort: send errors are logged, never returned to the caller.
func (m *Manager) EmitRadioState(st RadioState) {
	for _, c := range m.Outbound(ServicePstFreq) {
		m.sendTo(c, BuildPstFreq(st.RxFreqHz))
	}
	for _, c := range m.Outbound(ServiceN1MMRadio) {
		m.sendTo(c, BuildN1MMRadioInfo(st.StationName, st.RxFreqHz, st.TxFreqHz, st.Mode, st.OpCall))
	}
}

// EmitLoggedADIF sends the ADIF of a just-logged QSO to every enabled outbound
// "ADIF message" row (db_updated) — lets a second logger or Cloudlog gateway
// pick up contacts as they're logged.
func (m *Manager) EmitLoggedADIF(adif string) {
	if strings.TrimSpace(adif) == "" {
		return
	}
	rows := m.Outbound(ServiceDBUpdated)
	if len(rows) == 0 {
		// Said once per session, not per QSO. An operator who configured a second
		// logger and sees nothing arrive needs to know the difference between "we
		// sent it" and "there was nothing to send to" — and the usual cause is a
		// row created as an inbound ADIF listener instead of an outbound message.
		m.noADIFOnce.Do(func() {
			applog.Printf("udp: a QSO was logged but no outbound \"ADIF message\" row is enabled — " +
				"nothing is forwarded to another logger")
		})
		return
	}
	for _, c := range rows {
		m.sendTo(c, []byte(adif))
	}
}

// EmitLoggedQSOWSJT announces a logged contact on the WSJT-X interface, for
// receivers that speak it rather than plain text — Logger32's additional UDP
// sockets among them.
//
// BOTH messages go out, "QSO Logged" (5) then "Logged ADIF" (12), because that
// is exactly what WSJT-X does for every contact. Which one a receiver takes is
// its own business: MacLoggerDX reads the structured one by default and offers
// the ADIF as an option, and sending only the ADIF is why a QSO reached
// Logger32's socket and never reached its log. Anything built for WSJT-X
// already sees both from the real thing, so neither is a surprise and nothing
// logs the contact twice.
func (m *Manager) EmitLoggedQSOWSJT(q LoggedQSO, adifRec string) {
	rows := m.Outbound(ServiceWSJTLog)
	if len(rows) == 0 {
		return
	}
	qsoPkt := BuildWSJTQSOLogged("OpsLog", q)
	var adifPkt []byte
	if strings.TrimSpace(adifRec) != "" {
		adifPkt = BuildWSJTLoggedADIF("OpsLog", adifRec)
	}
	for _, c := range rows {
		m.sendTo(c, qsoPkt)
		if adifPkt != nil {
			m.sendTo(c, adifPkt)
		}
	}
}

// sentPreview appends what was actually sent, for the rows where that is worth
// reading.
//
// Custom rows only, and deliberately: their payload is a template the operator
// wrote, and "sent 18 bytes" tells them nothing about whether the substitution
// came out as they meant. The ADIF and WSJT-X rows carry a whole record or a
// binary frame, and dumping either into the log on every QSO would bury it.
//
// The trigger is named too — a row that fires on the wrong moment looks exactly
// like a row that does not fire.
func sentPreview(c Config, payload []byte) string {
	if c.ServiceType != ServiceCustom {
		return ""
	}
	const maxShown = 200
	s := string(payload)
	if len(s) > maxShown {
		s = s[:maxShown] + "…"
	}
	// %q below already renders a carriage return as \r rather than breaking the
	// log line in two — and it is the line ending the operator chose, so it has
	// to be visible. Escaping it by hand first would double every backslash.
	if c.Trigger != "" {
		return fmt.Sprintf(" on %s: %q", c.Trigger, s)
	}
	return fmt.Sprintf(": %q", s)
}

// sendTo resolves the row's destination (host:port) and fires one datagram.
//
// A successful send is logged, not just a failure. UDP has no delivery report:
// when an operator says "I set up an ADIF message to Logger32 on port 2250 and
// nothing arrives", the only thing that separates "OpsLog never sent it" from
// "the other program did not take it" is a line saying we sent. Without one,
// both look identical from here — and the first is far more common, because a
// row created as an INBOUND ADIF listener rather than an OUTBOUND ADIF message
// matches nothing and emits nothing, in silence.
//
// Rate is not a concern: these fire on a QSO being logged or a frequency
// change, not per packet on a socket.
func (m *Manager) sendTo(c Config, payload []byte) {
	host := strings.TrimSpace(c.DestinationIP)
	if host == "" {
		host = "127.0.0.1"
	}
	dst := fmt.Sprintf("%s:%d", host, c.Port)
	if err := SendUDP(dst, payload); err != nil {
		applog.Printf("udp: [%s] outbound send to %s failed: %v", c.Name, dst, err)
		return
	}
	applog.Printf("udp: [%s] sent %d bytes to %s (%s)%s",
		c.Name, len(payload), dst, c.ServiceType, sentPreview(c, payload))
}

// RelayInbound re-sends one received datagram, unchanged, to every configured
// relay destination.
//
// Called with the RAW bytes as they arrived — before parsing, and whatever the
// parse made of them. A packet this build cannot decode is still a packet the
// application downstream may understand perfectly well, and a relay that only
// forwards what it understood is a relay that silently drops the fields it has
// not learned about yet.
//
// Loop guard: a destination that is one of our OWN inbound ports on a local
// address would come straight back in and be relayed again, for ever, at line
// rate. Such a row is skipped and said so once — quietly dropping it would look
// like a relay that does not work.
func (m *Manager) RelayInbound(pkt []byte, fromPort int) {
	if len(pkt) == 0 {
		return
	}
	rows := m.Outbound(ServiceWSJTRelay)
	if len(rows) == 0 {
		return
	}
	m.mu.Lock()
	ports := make(map[int]struct{}, len(m.inbound))
	for _, s := range m.inbound {
		ports[s.cfg.Port] = struct{}{}
	}
	m.mu.Unlock()

	for _, c := range rows {
		if isLoopback(c.DestinationIP) {
			if _, mine := ports[c.Port]; mine {
				m.relayLoopOnce(c)
				continue
			}
		}
		// Deliberately not sendTo: that logs a line per datagram, and this runs
		// on every decode of every period — it would bury the log within minutes.
		host := strings.TrimSpace(c.DestinationIP)
		if host == "" {
			host = "127.0.0.1"
		}
		dst := fmt.Sprintf("%s:%d", host, c.Port)
		if err := SendUDP(dst, pkt); err != nil {
			m.relayErrOnce(c, dst, err)
		}
	}
	_ = fromPort
}

// isLoopback reports whether a destination names this machine. Empty counts:
// sendTo defaults it to 127.0.0.1.
func isLoopback(host string) bool {
	h := strings.TrimSpace(host)
	if h == "" || h == "localhost" {
		return true
	}
	ip := net.ParseIP(h)
	return ip != nil && ip.IsLoopback()
}

// relayLoopOnce and relayErrOnce keep a repeating relay complaint to one line a
// session. Both fire per datagram otherwise, which on a busy band is hundreds a
// minute and makes the log useless for anything else.
func (m *Manager) relayLoopOnce(c Config) {
	m.relayWarnMu.Lock()
	defer m.relayWarnMu.Unlock()
	if m.relayWarned == nil {
		m.relayWarned = map[int64]bool{}
	}
	if m.relayWarned[c.ID] {
		return
	}
	m.relayWarned[c.ID] = true
	applog.Printf("udp: [%s] relay target %s:%d is one of OpsLog's OWN listening ports — "+
		"that would feed the stream back into itself for ever. Nothing is relayed on this row; "+
		"point it at the other application's port.", c.Name, c.DestinationIP, c.Port)
}

func (m *Manager) relayErrOnce(c Config, dst string, err error) {
	m.relayWarnMu.Lock()
	defer m.relayWarnMu.Unlock()
	if m.relayWarned == nil {
		m.relayWarned = map[int64]bool{}
	}
	if m.relayWarned[-c.ID-1] {
		return
	}
	m.relayWarned[-c.ID-1] = true
	applog.Printf("udp: [%s] relay to %s failed: %v (said once)", c.Name, dst, err)
}
