// Package rigctld shares OpsLog's CAT link with other programs.
//
// A native CAT backend owns the rig's serial port, and Windows gives a COM port
// to ONE process. So the moment OpsLog talks to the radio directly, WSJT-X,
// MSHV or JTDX can no longer reach it — the cost of dropping OmniRig, which was
// itself a sharing layer.
//
// The answer is the one wfview uses: OpsLog becomes the server. It speaks the
// Hamlib "net rigctl" protocol, which WSJT-X, JTDX, MSHV, Log4OM and CQRLOG all
// support natively (rig model "Hamlib NET rigctl", host:4532) with no driver to
// install. The other program asks us, and we relay to whichever backend is
// connected — OmniRig, Flex, Icom, TCI or Yaesu alike.
//
// ── The protocol ──────────────────────────────────────────────────────────
// Line-based ASCII. A lowercase letter reads, its uppercase counterpart writes,
// and long names are prefixed with a backslash. A write answers "RPRT 0" for
// success or "RPRT -n" for an error; a read answers the value(s), one per line.
//
//	f            → 14074000          get_freq
//	F 14074000   → RPRT 0            set_freq
//	m            → USB\n2400         get_mode (mode + passband)
//	M USB 2400   → RPRT 0            set_mode
//	t / T 1      → 0                 get/set PTT
//	s            → 0\nVFOB           get_split_vfo
//	v            → VFOA              get_vfo
//	\dump_state  → capability block  asked once by WSJT-X at connect
//
// WSJT-X will not proceed past connect without a well-formed dump_state, which
// is why that block is written out in full rather than stubbed.
package rigctld

import (
	"bufio"
	"fmt"
	"hamlog/internal/cat"
	"net"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

// Rig is what the server needs from OpsLog's CAT manager. An interface, so this
// package stays testable without a radio and without importing internal/cat.
type Rig interface {
	// Freq is the frequency of the CURRENT VFO — where the operator is
	// listening. Hamlib's "f" means the VFO in use, not the transmit one; the
	// split TX frequency is a separate question, asked with "i". Answering "f"
	// with the TX frequency invites a client to write it back as the dial.
	Freq() int64          // current (RX) frequency in Hz, 0 if unknown
	Mode() string         // ADIF mode (SSB, CW, FT8…)
	Split() (bool, int64) // split on?, and the other VFO's frequency
	SetFreq(hz int64) error
	SetMode(mode string) error
	SetPTT(on bool) error
	// SetSplit arms or clears split and places the transmit frequency. Returns an
	// error on a rig that cannot: a refusal the client can report is worth far
	// more than a success it has no way to check.
	SetSplit(on bool, txHz int64) error
}

type Server struct {
	port int
	rig  Rig
	log  func(string, ...any)

	mu     sync.Mutex
	ln     net.Listener
	conns  map[net.Conn]struct{}
	closed bool

	// accepted counts every connection this listener has taken. Only selfTest
	// reads it, to tell "a client reached us" from "a client reached someone
	// else on our port".
	accepted atomic.Int64

	// ptt mirrors the last PTT state a client commanded via set_ptt. WSJT-X/JTDX
	// poll get_ptt DURING transmit to confirm the rig is keyed; if get_ptt reads
	// RX they conclude PTT failed and abort the over after a second or two. We
	// don't read PTT back from every backend, so echo what the client last set —
	// always consistent with its own command, and enough to satisfy the check.
	ptt atomic.Bool
	// pttKnown says ptt reflects a state we actually commanded, so a repeat can
	// be told from the very first call — where the radio's state is unknown and
	// the command must go through.
	pttKnown atomic.Bool
	// splitWanted remembers a set_split_vfo that arrived before the frequency it
	// needs, so the pair can be honoured in the order the client sends them.
	splitWanted atomic.Bool
}

func New(port int, rig Rig, logf func(string, ...any)) *Server {
	if port <= 0 || port > 65535 {
		port = 4532 // the rigctld default every client pre-fills
	}
	if logf == nil {
		logf = func(string, ...any) {}
	}
	return &Server{port: port, rig: rig, log: logf, conns: map[net.Conn]struct{}{}}
}

func (s *Server) Start() error {
	s.mu.Lock()
	if s.ln != nil {
		s.mu.Unlock()
		return nil // already listening
	}
	s.closed = false
	s.mu.Unlock()

	ln, err := net.Listen("tcp", fmt.Sprintf(":%d", s.port))
	if err != nil {
		return fmt.Errorf("rigctld: listen on %d: %w", s.port, err)
	}
	s.mu.Lock()
	s.ln = ln
	s.mu.Unlock()
	s.log("rigctld: sharing CAT on port %d (Hamlib NET rigctl)", s.port)

	go func() {
		for {
			c, err := ln.Accept()
			if err != nil {
				s.mu.Lock()
				closed := s.closed
				s.mu.Unlock()
				if !closed {
					s.log("rigctld: accept failed: %v", err)
				}
				return
			}
			s.accepted.Add(1)
			s.mu.Lock()
			s.conns[c] = struct{}{}
			s.mu.Unlock()
			go s.serve(c)
		}
	}()
	go s.selfTest()
	return nil
}

// selfTest checks that a client connecting to 127.0.0.1:<port> actually reaches
// THIS listener, and says so in the log when it does not.
//
// Binding successfully is not the same as being reachable. OpsLog listens on
// 0.0.0.0, and Windows lets a second program bind the SAME port on the specific
// address 127.0.0.1. Connections to localhost then go to the MORE SPECIFIC
// listener — the other program — while ours sits there having logged "sharing
// CAT on port 4532" and never seeing a single client.
//
// Seen in the field with Nexus, which starts its own rigctld on 127.0.0.1:4532
// and connects to it. Neither program reports anything wrong; the operator gets
// a CAT timeout from a daemon with no radio behind it, and OpsLog's log is
// silent because nothing ever arrived. Three exchanges went into finding that,
// so it is worth one line at startup.
//
// The counter can only be raised by our own accept loop, so a real client
// arriving during the probe makes this pass, never fail wrongly.
func (s *Server) selfTest() {
	before := s.accepted.Load()
	addr := fmt.Sprintf("127.0.0.1:%d", s.port)
	c, err := net.DialTimeout("tcp", addr, 2*time.Second)
	if err != nil {
		s.log("rigctld: WARNING — could not reach our own CAT port at %s (%v); "+
			"clients on this PC will not find OpsLog", addr, err)
		return
	}
	defer c.Close()
	// Give the accept loop a moment: the dial returns as soon as the handshake
	// completes, which can be marginally before Accept hands the connection over.
	for i := 0; i < 20; i++ {
		if s.accepted.Load() > before {
			return // it reached us — nothing to say
		}
		time.Sleep(50 * time.Millisecond)
	}
	s.log("rigctld: WARNING — another program is already answering on %s. "+
		"It will receive the CAT connections meant for OpsLog, which will look "+
		"like a timeout in that program and silence here. Close it, or move "+
		"OpsLog's shared CAT to a different port.", addr)
}

// releasePTT drops the transmitter when whoever was holding it goes away.
//
// Nothing else will. The Kenwood/Elecraft backend deliberately suspends its
// wire poll while PTT is held (a K3 answers "?;" to IF; during transmit), so a
// client that crashes, is killed, or simply has its socket closed under it
// leaves the rig keyed with nobody watching — and closing the socket is exactly
// what Stop() does on every settings save. Seen in the field: a K3 sat in
// transmit for 29 s, until the CAT link happened to be rebuilt.
//
// Keyed reports whether a client currently has the rig transmitting.
//
// Asked before the server is torn down: pulling the socket out from under
// WSJT-X mid-over is the one moment that costs a QSO — and, on the log that
// prompted this, sometimes the whole application.
func (s *Server) Keyed() bool { return s.ptt.Load() }

// Swap makes this once-only, so the Stop() path and the per-connection defer it
// triggers can both call it without double-unkeying.
func (s *Server) releasePTT(why string) {
	if !s.ptt.Swap(false) {
		return
	}
	s.log("rigctld: %s while the rig was keyed — dropping PTT", why)
	if err := s.rig.SetPTT(false); err != nil {
		s.log("rigctld: emergency unkey failed: %v", err)
	}
}

func (s *Server) Stop() {
	// Before anything is torn down: reloadCAT calls us BEFORE it restarts the CAT
	// backend, so the rig is still reachable here and an unkey still lands.
	s.releasePTT("CAT sharing stopped")

	s.mu.Lock()
	s.closed = true
	ln := s.ln
	s.ln = nil
	conns := make([]net.Conn, 0, len(s.conns))
	for c := range s.conns {
		conns = append(conns, c)
	}
	s.conns = map[net.Conn]struct{}{}
	s.mu.Unlock()

	if ln != nil {
		_ = ln.Close()
	}
	// Close the live sessions too. Leaving them open would keep a client happily
	// talking to a server the operator has switched off.
	for _, c := range conns {
		_ = c.Close()
	}
}

func (s *Server) serve(c net.Conn) {
	defer func() {
		s.mu.Lock()
		delete(s.conns, c)
		s.mu.Unlock()
		_ = c.Close()
		// A client that walks away mid-over must not leave the rig transmitting.
		s.releasePTT(fmt.Sprintf("client %s left", c.RemoteAddr()))
	}()
	s.log("rigctld: client connected from %s", c.RemoteAddr())
	// The HANDSHAKE is always traced — the first few commands are where a
	// client decides to stay or hang up, and a connect that lasted 50 ms left
	// nothing in the log to say which answer it disliked. Steady-state polling
	// stays behind the CAT trace switch.
	traced := 0
	r := bufio.NewReader(c)
	w := bufio.NewWriter(c)
	for {
		// No deadline: WSJT-X polls every few seconds but a client may legitimately
		// sit idle between band changes, and dropping it would look like a fault.
		line, err := r.ReadString('\n')
		if err != nil {
			s.log("rigctld: client %s disconnected", c.RemoteAddr())
			return
		}
		req := strings.TrimSpace(line)
		started := time.Now()
		resp, quit := s.handle(req)
		// A command the radio took a visible time to accept is worth a line of its
		// own, always — not behind the trace switch below.
		//
		// This is the shape every "WSJT-X takes ten seconds to change band" report
		// has: the client is waiting on us, we are waiting on the rig, and the log
		// showed neither. Which command, and how long, is the whole diagnosis —
		// and one second is already far outside anything a healthy link does.
		if took := time.Since(started); took > time.Second && req != "" {
			s.log("rigctld: %q took %s — the radio was slow to answer, the client waited that long", req, took.Round(10*time.Millisecond))
		}
		// The whole exchange, when tracing is on.
		//
		// Only PTT transitions were ever recorded, so when JTDX aborted a
		// transmission mid-frame the log showed the abort and not the command
		// that preceded it — the one thing needed to tell whether OpsLog answered
		// something the client could not accept. Behind the same switch as the CAT
		// wire trace: this is one line per poll and would drown an ordinary log.
		if req != "" && (traced < 6 || cat.CIVTraceEnabled()) {
			traced++
			s.log("rigctld: %s → %q ⇒ %q", c.RemoteAddr(), req, strings.TrimRight(resp, "\r\n"))
		}
		if resp != "" {
			if _, err := w.WriteString(resp); err != nil {
				return
			}
			if err := w.Flush(); err != nil {
				return
			}
		}
		if quit {
			return
		}
	}
}

// handle answers one command line. Pure apart from the Rig calls, so the whole
// protocol is testable with a fake rig.
func (s *Server) handle(line string) (resp string, quit bool) {
	if line == "" {
		return "", false
	}
	// Extended mode: clients may prefix a command with '+' or '-' to ask for a
	// verbose reply. We answer in the plain format, which every client also
	// accepts, so the prefix is simply stripped.
	line = strings.TrimLeft(line, "+-")
	fields := strings.Fields(line)
	if len(fields) == 0 {
		return "", false
	}
	cmd, args := fields[0], stripVFOArg(fields[1:])

	switch cmd {
	case "\\dump_state", "dump_state":
		return dumpState, false
	case "\\chk_vfo", "chk_vfo":
		// "is VFO mode on?" — we answer for one VFO at a time, so: no.
		return "CHKVFO 0\n", false
	case "\\get_powerstat", "get_powerstat":
		return "1\n", false

	// Three commands a client may issue as a matter of course. Refusing them with
	// RPRT -11 is allowed, and a tolerant client carries on — but nothing obliges
	// it to, and Nexus sends all three around every transmit. Answering costs
	// nothing and removes them as suspects when something really is wrong.
	case "\\get_lock_mode", "get_lock_mode":
		// Truthful: OpsLog never locks the dial against its own clients.
		return "0\n", false
	case "\\set_lock_mode", "set_lock_mode":
		// Accepted and ignored, like set_vfo below: there is no lock to set, and
		// failing here would abort a client's whole transmit sequence over a
		// setting that has no effect either way.
		return rprt(0), false
	case "\\stop_morse", "stop_morse":
		// Nothing is queued here — CW over CAT is keyed by the rig's own keyer
		// through the backend, not buffered in this server. "Stopped" is therefore
		// accurate rather than polite.
		return rprt(0), false
	case "q", "Q", "\\quit":
		return "", true

	case "f", "\\get_freq":
		return fmt.Sprintf("%d\n", s.rig.Freq()), false
	case "F", "\\set_freq":
		if len(args) < 1 {
			return rprt(-1), false
		}
		hz, err := parseFreq(args[0])
		if err != nil {
			// Logged with the RAW line: a client that phrases a command in a
			// dialect we don't accept shows only "Invalid parameter" on its side,
			// which says nothing about what it actually sent.
			s.log("rigctld: cannot read a frequency from %q — client dialect not handled", line)
			return rprt(-1), false
		}
		if err := s.rig.SetFreq(hz); err != nil {
			s.log("rigctld: set_freq %d failed: %v", hz, err)
			return rprt(-9), false
		}
		return rprt(0), false

	case "m", "\\get_mode":
		// Passband width is required by the protocol. We do not read the rig's
		// filter, and a made-up number is harmless here: clients use it to display
		// a bandwidth, never to decide anything.
		return fmt.Sprintf("%s\n%d\n", adifToHamlib(s.rig.Mode()), passbandFor(s.rig.Mode())), false
	case "M", "\\set_mode":
		if len(args) < 1 {
			return rprt(-1), false
		}
		// Only touch the radio on a CHANGE, the same rule set_ptt above follows.
		//
		// WSJT-X restates the mode on every band change and after every transmit,
		// almost always the mode the rig is already in. On a native backend that
		// is not free: an Icom set_mode is the mode frame, the data-mode frame and
		// a readback to check the rig honoured them, each a round trip — over a
		// remote CI-V link that is seconds, spent to arrive where we already were,
		// with the client blocked on the answer the whole time.
		//
		// Compared in the CLIENT's own vocabulary — what "m" would report against
		// what it just asked for — so nothing it can observe changes. A rig whose
		// mode we do not know yet (empty) is never assumed.
		if cur := s.rig.Mode(); cur != "" && adifToHamlib(cur) == adifToHamlib(hamlibToADIF(args[0])) {
			return rprt(0), false
		}
		if err := s.rig.SetMode(hamlibToADIF(args[0])); err != nil {
			s.log("rigctld: set_mode %q failed: %v", args[0], err)
			return rprt(-9), false
		}
		return rprt(0), false

	case "t", "\\get_ptt":
		// Echo the last commanded PTT state. WSJT-X/JTDX poll this WHILE
		// transmitting to confirm the rig is keyed; answering a blanket "0" (RX)
		// made them decide PTT had failed and abort the over after ~1-2 s.
		if s.ptt.Load() {
			return "1\n", false
		}
		return "0\n", false
	case "T", "\\set_ptt":
		if len(args) < 1 {
			return rprt(-1), false
		}
		on := args[0] != "0"
		// Only touch the radio on a CHANGE.
		//
		// A client is free to restate PTT as often as it likes, and one does:
		// Nexus sends set_ptt 0 about sixteen times a second, so the Flex was
		// getting "xmit 0" every 60 ms forever. Worse than wasteful — its own
		// "xmit 1" landed between two of them and was overwritten in the same
		// millisecond, so the radio never stayed keyed and the operator saw a
		// transmit request that simply did nothing.
		//
		// Repeating a state is not a request to change it. The first call always
		// goes through, since we cannot know how the radio was left.
		if s.pttKnown.Load() && s.ptt.Load() == on {
			return rprt(0), false
		}
		if err := s.rig.SetPTT(on); err != nil {
			s.log("rigctld: set_ptt %v failed: %v", on, err)
			return rprt(-9), false
		}
		s.ptt.Store(on)
		s.pttKnown.Store(true)
		s.log("rigctld: PTT %s", map[bool]string{true: "ON", false: "off"}[on])
		return rprt(0), false

	case "v", "\\get_vfo":
		return "VFOA\n", false
	case "V", "\\set_vfo":
		// Accepted and ignored: OpsLog follows the rig's own VFO selection, and
		// answering an error here makes WSJT-X abandon the connection entirely.
		return rprt(0), false

	case "s", "\\get_split_vfo":
		on, _ := s.rig.Split()
		n := 0
		if on {
			n = 1
		}
		return fmt.Sprintf("%d\nVFOB\n", n), false
	case "S", "\\set_split_vfo":
		// "S <0|1> <VFO>". The VFO argument is ignored: which dial transmits is the
		// rig's own business, and every backend here puts it on the second one.
		//
		// This used to answer RPRT 0 and do NOTHING. WSJT-X in "Split Operating:
		// Rig" sends this and set_split_freq, believed both, and transmitted on the
		// RECEIVE frequency — on a pileup, straight onto the DX, while the software
		// showed exactly what the operator had asked for. A lie that leaves no
		// trace anywhere is the worst kind of bug, so it now works or says so.
		if len(args) < 1 {
			return rprt(-1), false
		}
		if args[0] != "0" {
			// Arming needs a frequency, and WSJT-X sends set_split_freq AFTER this.
			// Remember the request and let that command do the work: alone, this
			// would arm split on whatever the transmit VFO happens to hold.
			s.splitWanted.Store(true)
			return rprt(0), false
		}
		s.splitWanted.Store(false)
		// Already simplex? Then there is nothing to do and the request is
		// satisfied. Reporting a failure here is what broke JTDX in "Fake It":
		// Fake It uses no split, JTDX still sends "S 0" to be sure, and a backend
		// that cannot SET split answered an error to a request that was already
		// true. JTDX read that as rig control failing and abandoned the
		// transmission a second into the frame.
		//
		// A refusal is only honest when something actually needed doing.
		if on, _ := s.rig.Split(); !on {
			return rprt(0), false
		}
		if err := s.rig.SetSplit(false, 0); err != nil {
			s.log("rigctld: split off failed: %v", err)
			return rprt(-9), false
		}
		s.log("rigctld: split off")
		return rprt(0), false
	case "i", "\\get_split_freq":
		_, tx := s.rig.Split()
		if tx <= 0 {
			tx = s.rig.Freq()
		}
		return fmt.Sprintf("%d\n", tx), false
	case "I", "\\set_split_freq":
		if len(args) < 1 {
			return rprt(-1), false
		}
		// Hamlib sends a float ("14075300.000000"), so parse as one.
		hz, err := strconv.ParseFloat(args[0], 64)
		if err != nil || hz <= 0 {
			return rprt(-1), false
		}
		if err := s.rig.SetSplit(true, int64(hz)); err != nil {
			s.log("rigctld: split TX %.0f Hz failed: %v", hz, err)
			return rprt(-9), false
		}
		s.splitWanted.Store(true)
		s.log("rigctld: split ON, TX %.0f Hz", hz)
		return rprt(0), false

	case "X", "\\set_split_mode":
		// "X <mode> <passband>" — the mode of the TRANSMIT VFO.
		//
		// Accepted rather than refused. WSJT-X and JTDX send it as part of their
		// normal setup even in "Fake It", where there is no split and therefore no
		// second VFO to give a mode to; answering "not implemented" made JTDX give
		// up on rig control mid-transmission.
		//
		// The mode is applied when there IS a split — the transmit VFO is a real
		// one then. Without split the request has no target and succeeding is the
		// honest answer: the transmit VFO already has that mode, because it is the
		// same VFO.
		if len(args) < 1 {
			return rprt(-1), false
		}
		if on, _ := s.rig.Split(); !on {
			return rprt(0), false
		}
		if err := s.rig.SetMode(args[0]); err != nil {
			s.log("rigctld: split mode %q failed: %v", args[0], err)
			return rprt(-9), false
		}
		return rprt(0), false
	case "x", "\\get_split_mode":
		// Mirrors X: the transmit VFO's mode and passband. Without split that is
		// simply the current mode.
		return s.rig.Mode() + "\n2400\n", false

	default:
		// A frame ending in ';' is not a rigctl command at all — it is raw rig
		// dialect (Kenwood/Elecraft/Yaesu), which means the client is configured
		// with a RIG MODEL pointing at this port instead of "Hamlib NET rigctl".
		//
		// Worth naming, because the symptom hides the cause completely: we answer
		// RPRT -11 like any unknown command, but that reply has no ';' to terminate
		// on, so the client's parser waits and then reports "reply incomplete, got
		// nothing". The operator sees a timeout and concludes the CAT share is
		// broken, when it is a one-line setting in the other program.
		if strings.HasSuffix(line, ";") && !strings.Contains(line, " ") {
			s.log("rigctld: %q is a raw rig command, not rigctl — the client is set to a RIG MODEL; "+
				"it must be set to \"Hamlib NET rigctl\" (rig 2) at this address", line)
			return rprt(-11), false
		}
		// RPRT -11 is "command not implemented". Answering something is essential:
		// a client waiting on a silent socket hangs rather than degrading.
		s.log("rigctld: unimplemented command %q", line)
		return rprt(-11), false
	}
}

func rprt(code int) string { return fmt.Sprintf("RPRT %d\n", code) }

// stripVFOArg drops a leading VFO name from a command's arguments.
//
// Hamlib has two dialects. In the plain one a client sends "F 14074000"; in VFO
// mode it names the target first — "F VFOA 14074000". MSHV uses the first and
// worked immediately; JTDX uses the second, so the frequency landed in the
// argument slot where a VFO was expected, the parse failed, and JTDX showed
// "Hamlib error: Invalid parameter while setting frequency" (our RPRT -1).
//
// Accepting both costs nothing here: OpsLog follows the rig's own VFO
// selection, so the name carries no information we act on — dropping it is not
// losing anything, and refusing it locks out a whole family of clients.
func stripVFOArg(args []string) []string {
	if len(args) == 0 {
		return args
	}
	switch strings.ToUpper(args[0]) {
	case "VFOA", "VFOB", "VFOC", "VFO", "CURRVFO", "CURR", "MAIN", "SUB", "MEM", "A", "B":
		return args[1:]
	}
	return args
}

// parseFreq accepts both the integer Hz and the "14074000.000000" form clients
// send interchangeably.
func parseFreq(s string) (int64, error) {
	s = strings.TrimSpace(s)
	if i := strings.IndexByte(s, '.'); i >= 0 {
		s = s[:i]
	}
	hz, err := strconv.ParseInt(s, 10, 64)
	if err != nil || hz <= 0 {
		return 0, fmt.Errorf("rigctld: bad frequency %q", s)
	}
	return hz, nil
}

// adifToHamlib maps our mode vocabulary to Hamlib's. Every digital sub-mode
// becomes PKTUSB: that is what a client expects to see when the rig is in DATA,
// and it is what WSJT-X sets when it takes control.
func adifToHamlib(mode string) string {
	switch strings.ToUpper(strings.TrimSpace(mode)) {
	case "SSB", "USB":
		return "USB"
	case "LSB":
		return "LSB"
	case "CW":
		return "CW"
	case "AM":
		return "AM"
	case "FM":
		return "FM"
	case "RTTY":
		return "RTTY"
	case "":
		return "USB"
	default:
		return "PKTUSB"
	}
}

// hamlibToADIF is the reverse. PKTUSB/PKTLSB/DATA become "DATA": the CAT backend
// then applies the operator's configured digital mode, so a client switching the
// rig to data does not silently relabel their QSOs as FT8 when they run JS8.
func hamlibToADIF(mode string) string {
	switch strings.ToUpper(strings.TrimSpace(mode)) {
	case "USB":
		return "USB"
	case "LSB":
		return "LSB"
	case "CW", "CWR":
		return "CW"
	case "AM":
		return "AM"
	case "FM", "FMN", "WFM":
		return "FM"
	case "RTTY", "RTTYR":
		return "RTTY"
	case "PKTUSB", "PKTLSB", "PKTFM", "DATA", "DIGU", "DIGL":
		return "DATA"
	default:
		return strings.ToUpper(strings.TrimSpace(mode))
	}
}

func passbandFor(mode string) int {
	switch adifToHamlib(mode) {
	case "CW":
		return 500
	case "RTTY", "PKTUSB":
		return 3000
	case "AM":
		return 6000
	case "FM":
		return 15000
	default:
		return 2400
	}
}

// dumpState is the capability block Hamlib clients read once at connect. WSJT-X
// refuses to go further without it, and parses it positionally — the field
// ORDER is the contract, so this is kept as one literal rather than assembled.
//
// It declares protocol version 0, a generic rig, and one 150 kHz–1500 MHz range
// with the common modes. The numbers are deliberately permissive: they say what
// a client may ASK for, and OpsLog's backend refuses anything the radio cannot
// really do.
const dumpState = `0
1
2
150000.000000 1500000000.000000 0x1ff -1 -1 0x10000003 0x3
0 0 0 0 0 0 0
150000.000000 1500000000.000000 0x1ff -1 -1 0x10000003 0x3
0 0 0 0 0 0 0
0 0
0 0
0x1ff 1
0x1ff 0
0 0
0x1e 2400
0x2 500
0x1 8000
0x1 2400
0x20 15000
0x20 8000
0x40 230000
0 0
9990
9990
10000
0
10
10 20 30
0x3effffff
0x3effffff
0x7fffffff
0x7fffffff
0x7fffffff
0x7fffffff
`

// dialTimeout is only used by tests, kept here so the value is one place.
const dialTimeout = 2 * time.Second
