package main

// Wiring the custom outbound UDP/URL rows to the moments that fire them.
//
// One function per trigger, each building the fields its template can use. The
// field SET is per trigger and that is the point: a rotation has no frequency, a
// lookup has no report. The Settings panel lists what each one offers, because a
// template that quietly renders an empty string is the worst kind of wrong.

import (
	"fmt"
	"strconv"
	"strings"
	"sync"

	"hamlog/internal/cat"
	"hamlog/internal/integrations/udp"
	"hamlog/internal/lookup"
	"hamlog/internal/qso"
)

// lastTriggerBand remembers the rig's band so a change can be told from a
// frequency moving inside one. Guarded because the CAT callback and a settings
// reload can both reach it.
var (
	lastTriggerBandMu sync.Mutex
	lastTriggerBand   string
)

// udpTriggerQSOLogged fires after a contact is written to the log.
func (a *App) udpTriggerQSOLogged(q qso.QSO) {
	if a.udp == nil {
		return
	}
	f := map[string]string{
		"call":    q.Callsign,
		"band":    q.Band,
		"band_m":  bandMetres(q.Band),
		"mode":    q.Mode,
		"grid":    q.Grid,
		"name":    q.Name,
		"country": q.Country,
		"rst_s":   q.RSTSent,
		"rst_r":   q.RSTRcvd,
		"comment": q.Comment,
		"date":    q.QSODate.UTC().Format("20060102"),
		"time":    q.QSODate.UTC().Format("150405"),
	}
	if q.FreqHz != nil && *q.FreqHz > 0 {
		f["freq_hz"] = strconv.FormatInt(*q.FreqHz, 10)
		f["freq_mhz"] = fmt.Sprintf("%.6f", float64(*q.FreqHz)/1e6)
	}
	if q.DXCC != nil {
		f["dxcc"] = strconv.Itoa(*q.DXCC)
	}
	a.udp.EmitTrigger(udp.TriggerQSOLogged, f)
}

// udpTriggerRotator fires whenever the antenna is told to turn — the header's
// SP and LP buttons, the compass, a click on a spot.
//
// Hooked to the COMMAND rather than to a button: one place, and it stays true
// when the interface changes. path is "SP", "LP" or "" when the caller did not
// say which.
func (a *App) udpTriggerRotator(az, el int, path string) {
	if a.udp == nil {
		return
	}
	f := map[string]string{
		"az":   strconv.Itoa(az),
		"path": path,
	}
	if el >= 0 {
		f["el"] = strconv.Itoa(el)
	}
	a.udp.EmitTrigger(udp.TriggerRotatorGo, f)
}

// udpTriggerLookup fires after a callbook lookup returns.
func (a *App) udpTriggerLookup(r *lookup.Result) {
	if a.udp == nil || r == nil {
		return
	}
	a.udp.EmitTrigger(udp.TriggerLookupDone, map[string]string{
		"call":    r.Callsign,
		"name":    r.Name,
		"grid":    r.Grid,
		"country": r.Country,
		"qth":     r.QTH,
		"state":   r.State,
		"dxcc":    strconv.Itoa(r.DXCC),
	})
}

// udpTriggerBandChange fires when the RIG changes band — not the entry form: an
// antenna switch follows the radio, not what is being typed.
//
// Called from the CAT state callback, which runs on every user-relevant change,
// so the comparison against the last band is what makes this rare rather than
// continuous. No damping needed beyond that.
func (a *App) udpTriggerBandChange(s cat.RigState) {
	if a.udp == nil {
		return
	}
	if !s.Connected {
		return
	}
	a.emitBandChangeTrigger(s.Band, s.Mode, s.FreqHz)
}

// noteBandChange normalises a band and reports whether it is a NEW one.
//
// The state is shared by both sources on purpose. The rig reports a band change
// and so does the entry strip, and on a station that has both, one QSY produces
// both — an antenna switch must be commanded once, not twice.
func noteBandChange(raw string) (string, bool) {
	band := strings.ToLower(strings.TrimSpace(raw))
	if band == "" {
		return "", false
	}
	lastTriggerBandMu.Lock()
	defer lastTriggerBandMu.Unlock()
	if band == lastTriggerBand {
		return band, false
	}
	lastTriggerBand = band
	return band, true
}

// emitBandChangeTrigger fires the band-change rows, once per NEW band.
//
// Split out of the rig-state path because the rig is not the only thing that
// changes band: a station with no CAT link changes it in the QSO entry strip,
// and that is just as much a band change to the antenna switch on the other end
// of the message. The de-duplication is shared, so the two sources cannot
// double-fire between them.
func (a *App) emitBandChangeTrigger(rawBand, mode string, freqHz int64) {
	if a.udp == nil {
		return
	}
	band, changed := noteBandChange(rawBand)
	if !changed {
		return
	}
	f := map[string]string{
		"band":   band,
		"band_m": bandMetres(band),
		"mode":   mode,
	}
	if freqHz > 0 {
		f["freq_hz"] = strconv.FormatInt(freqHz, 10)
		f["freq_mhz"] = fmt.Sprintf("%.6f", float64(freqHz)/1e6)
	}
	a.udp.EmitTrigger(udp.TriggerBandChange, f)
}

// bandMetres turns "20m" into "20" — an antenna switch usually wants the number
// and nothing else, and asking an operator to strip it in a template would be
// one more thing to get wrong.
func bandMetres(band string) string {
	b := strings.ToLower(strings.TrimSpace(band))
	b = strings.TrimSuffix(b, "m")
	b = strings.TrimSuffix(b, "c") // "70cm" → "70c" → "70"
	if b == "" {
		return ""
	}
	for _, r := range b {
		if r < '0' || r > '9' {
			if r != '.' {
				return ""
			}
		}
	}
	return b
}
