package cat

// Native Kenwood CAT — TS-590, TS-890, TS-2000 and the many rigs that speak the
// same dialect (Elecraft K3/K4, and the "Kenwood/Elecraft" setting on Flex and
// SunSDR). Plain ASCII, every command terminated by ';', same shape as Yaesu but
// a different vocabulary.
//
// The dialect is already proven in this repository from the other side:
// internal/catemu ANSWERS these commands, pretending to be a TS-2000 so an ACOM
// amplifier follows OpsLog. The frame layouts here and there are the same ones.
//
// Commands used:
//
//	FA;            → FA00014025000;  VFO A frequency, ELEVEN digits, Hz
//	FB;            → FB00014030000;  VFO B frequency
//	IF;            → 38-char status frame: frequency, RX/TX, mode, VFO, split —
//	                 the whole operating state in ONE round trip, which is why it
//	                 is the poll rather than asking four separate questions.
//	MD;            → MD3;            mode (1=LSB 2=USB 3=CW 4=FM 5=AM 6=FSK
//	                 7=CW-R 9=FSK-R)
//	FR0;/FR1;                        receive VFO — 0 = A, 1 = B
//	FT0;/FT1;                        transmit VFO (split = the two differ)
//	TX;/RX;                          key / unkey
//	ID;            → ID020;          model number
//	AI0;                             silence unsolicited status reports
//
// Why not OmniRig: the same reason the Yaesu backend exists. Every Kenwood
// fault reported through OmniRig came from its interpretation layer rather than
// from the radio, and the rig file decides what a "Freq" property means.

import (
	"bytes"
	"errors"
	"fmt"
	"net"
	"strconv"
	"strings"
	"sync"
	"time"

	"go.bug.st/serial"
)

// Kenwood is the native backend. One serial port, one mutex: a command and its
// reply are never interleaved with another exchange.
type Kenwood struct {
	portName string
	baud     int
	// host is "address:port" for a serial link reached over the network — a
	// ser2net daemon, an Ethernet-serial adapter, a Raspberry Pi in the shack.
	//
	// This is NOT Kenwood's own network protocol. A TS-890 or TS-990 speaks
	// KNS/ARCP over its Ethernet socket, with a session and authentication, and
	// that is a different piece of work needing one of those radios to confirm
	// it. What this covers is the same CAT byte stream over a socket instead of
	// a wire, which is how most operators actually put a rig on the network.
	host    string
	digital string // mode name logged for data (FT8 by default)
	// dataMode chooses what CAT mode a DATA/digital mode (FT8, PSK…) sets, because
	// no single Kenwood mode digit is right for every rig: "usb" → USB (the SSB-data
	// base, default), "data" → the DATA mode digit MD6 (Elecraft K3/K4), "keep" →
	// leave the rig's current mode untouched (safest for a TS-590SG/TS-990S whose
	// data mode is a USB modifier the operator sets on the rig). See SetMode.
	dataMode string
	// elecraft marks a K3/K4 (the "Elecraft" backend). It reuses this Kenwood-dialect
	// client but, being an Elecraft, always drives digital modes to DATA A (MD6+DT0)
	// — the sub-mode FT8 audio needs — instead of going through the dataMode option.
	elecraft bool

	mu   sync.Mutex
	port serial.Port

	// dialPort, when set, replaces serial.Open. It exists so the backend can be
	// driven against internal/catemu — which already SPEAKS this dialect to
	// satisfy an ACOM amplifier — without a radio, a COM port or a null-modem
	// pair. Written for a Kenwood backend nobody here owns a rig to test.
	dialPort func() (serial.Port, error)

	model     string
	curFreq   int64
	curRXFreq int64
	curVFO    string // "A" or "B"
	// Split is confirmed with FR;/FT; because IF's split bit is empty on some
	// Kenwood-dialect rigs — but that check is THROTTLED so it doesn't run every
	// poll: the two extra commands tripled the poll time on a slow K3 and made the
	// frequency (read from IF at the top of the poll) lag by seconds. Between
	// checks the last result stands.
	splitCheckAt   time.Time
	splitCached    bool
	splitVFOCached string
	keyWPM         int // CW keyer speed, for pacing the KY buffer (see kenwood_cw.go)
	// Commands this rig answered "?;" to — asked once, then never again.
	unsupported map[string]bool
	// noLatch suspends that memory while a refusal is expected to be about
	// timing rather than capability (see ask).
	noLatch bool

	// Panel state — the K3/K4 control panel, see kenwood_panel.go. Read on the
	// same serialised link as everything else, on a slow beat for the settings
	// and every poll for the meters.
	panel KenwoodTXState
	// The icon/status word, for working out which bit says "ATU in line" — see
	// probeIcons. Kept so only CHANGES are logged.
	lastIcons    string
	iconProbes   int
	panelCycle   int
	panelLoaded  bool
	metersLogged int
	powerPeak    meterPeak
	swrPeak      meterPeak

	// rx holds bytes read but not yet consumed, ACROSS calls to ask.
	//
	// It has to survive: a rig answers faster than we ask, so one Read often
	// returns a whole reply plus the start of the next frame. When this buffer
	// was local to ask, everything after the matched frame was dropped — half a
	// frame included — and the link desynchronised permanently: every ask then
	// found the PREVIOUS command's answer and timed out waiting for its own.
	// That is the "discarding \" 000000000010000000;\" while waiting for IF"
	// a TS-480 reported, followed by connected=false for ever.
	rx []byte

	// heard is whatever arrived during Connect that was not a reply we wanted.
	// Kept only to put it in the error message: "not answering" and "answering
	// something unreadable" are different faults with different fixes.
	heard string

	// lowerLines deasserts DTR and RTS after opening the port.
	//
	// Neither default is safe for everyone, which is why this is a setting and
	// not a decision. Windows raises both lines on open: an interface that reads
	// them as PTT then keys the rig for as long as OpsLog is running — reported
	// as FT8 output power wandering, and gone the moment OpsLog was closed.
	// Lowering them instead silences the radios whose USB-serial interface needs
	// RTS asserted to transmit at all — a TS-990 that opened cleanly and answered
	// nothing. Off by default: that is how this backend behaved for its whole
	// life before the question came up.
	lowerLines bool

	// tx tracks whether we currently hold PTT (SetPTT true). A Kenwood/Elecraft rig
	// answers "?;" to a status poll (IF;) WHILE TRANSMITTING; OpsLog used to read
	// that as "the rig doesn't support IF;", latch it off and drop the whole CAT
	// link — which tore down the shared-CAT PTT that WSJT-X / JTDX key through, so a
	// K3 keyed but the logger's transmission fell apart. While PTT is held we skip
	// the wire poll and hand back the last good state (lastState) instead. txAt caps
	// the skip so a missed SetPTT(false) can't freeze the state for ever.
	tx        bool
	txAt      time.Time
	lastState RigState
	// ifRejects counts consecutive "?;" answers to IF;. See State().
	ifRejects int
}

// errRigRejected marks a "?;" — the rig understood the frame and declined it.
// Distinct from a serial fault on purpose: one is "ask again in a moment", the
// other is "the link is gone", and collapsing them is what dropped CAT sharing.
var errRigRejected = errors.New("rejected by rig")

// ifRejectGrace is how many consecutive "?;" answers to IF; are ridden out
// before the link is called dead.
//
// A Kenwood answers "?;" while it is busy — the tail of a transmission, a menu
// open on the front panel. The TS-590SG does it for a moment after RX;, which is
// exactly when the poll resumes: WSJT-X keyed through shared CAT, dropped PTT,
// and the very next IF; came back "?;". One rejected poll then tore down the
// whole link, WSJT-X lost the rig, and Hamlib went on to send an uninitialised
// frequency (2^63) that OpsLog rightly refused — an alarming error message whose
// real cause was three lines earlier in the log.
//
// Three at 250 ms is under a second of tolerance: long enough for the rig to
// finish whatever it was doing, far too short to hide an unplugged cable.
const ifRejectGrace = 3

// SetLowerLines chooses whether DTR and RTS are deasserted on connect. Set
// before Connect.
func (k *Kenwood) SetLowerLines(v bool) {
	k.mu.Lock()
	k.lowerLines = v
	k.mu.Unlock()
}

// where names the link for a message, so an error does not read "COM @ 0 baud"
// after a network connect.
func (k *Kenwood) where() string {
	if k.host != "" {
		return k.host
	}
	return k.portName
}

// NewKenwoodTCP builds a backend that reaches the rig over a socket instead of
// a COM port (ser2net and friends).
func NewKenwoodTCP(hostPort, digital string) *Kenwood {
	k := NewKenwood("", 0, digital)
	k.host = strings.TrimSpace(hostPort)
	return k
}

func NewKenwood(portName string, baud int, digital string) *Kenwood {
	if baud <= 0 {
		baud = 9600 // TS-590 factory default; TS-890 ships at 115200
	}
	if strings.TrimSpace(digital) == "" {
		digital = "FT8"
	}
	return &Kenwood{portName: strings.TrimSpace(portName), baud: baud, digital: digital, curVFO: "A"}
}

func (k *Kenwood) Name() string { return "kenwood" }

func (k *Kenwood) Connect() error {
	k.mu.Lock()
	defer k.mu.Unlock()
	if k.portName == "" && k.host == "" {
		return fmt.Errorf("kenwood: no serial port or network address configured")
	}
	// Close any handle still held before opening another: Connect runs again on
	// every reconnect, and Windows opens a serial port exclusively, so a leaked
	// handle makes the next open fail with "port busy" (the fault found in the
	// Yaesu backend — same shape here).
	if k.port != nil {
		_ = k.port.Close()
		k.port = nil
	}
	p, err := k.openPort()
	if err != nil {
		if k.host != "" {
			return fmt.Errorf("kenwood: connect %s: %w", k.host, err)
		}
		return fmt.Errorf("kenwood: open %s @ %d baud: %w%s", k.portName, k.baud, err, busyHint(k.portName, err))
	}
	p.SetReadTimeout(300 * time.Millisecond)
	k.port = p
	k.unsupported = map[string]bool{}

	// Silence unsolicited status reports: they interleave with our
	// request/response pairs and make a reply impossible to attribute. We poll.
	_ = k.write("AI0;")

	// Start from silence. A reconnect inherits whatever the rig said last —
	// unsolicited frames sent before AI0 landed, the tail of an answer nobody
	// read — and one stale frame is enough to leave every ask one reply behind
	// its question for the rest of the session.
	k.rx = nil
	k.drain(250 * time.Millisecond)
	k.heard = ""

	answered := false
	if id, err := k.ask("ID;"); err == nil && strings.HasPrefix(id, "ID") {
		answered = true
		code := strings.TrimSuffix(strings.TrimPrefix(id, "ID"), ";")
		if name, ok := kenwoodModels[code]; ok {
			k.model = name
		} else {
			k.model = "Kenwood (" + code + ")"
			debugLog.Printf("kenwood: unknown model id %q — add it to kenwoodModels", code)
		}
	}
	// IF is the command everything else depends on, so it is also the honest
	// test of whether a radio is really there.
	if r, err := k.ask("IF;"); err == nil && strings.HasPrefix(r, "IF") {
		answered = true
	}
	if !answered {
		k.model = ""
		if k.heard == "" && len(k.rx) > 0 {
			k.heard = string(k.rx) // an unterminated fragment is evidence too
		}
		// Distinguish silence from noise. "The rig is not answering" sent an
		// operator checking the power switch and the baud rate on a radio that was
		// visibly talking — its frames were arriving, they just did not match what
		// was asked (wrong baud garbles them; an interface echoing our own
		// commands back produces the same). Say which of the two it is, and quote
		// what came back, because that is the fact that decides where to look.
		if seen := k.heard; seen != "" {
			// Over a NETWORK link, baud means nothing and saying "check the baud
			// rate" sends the operator to a setting that cannot be the cause.
			// The usual mistake there is a different protocol on the other end:
			// Hamlib's rigctld (4532) answers text, so it looks alive and never
			// replies to ID;. OpsLog itself serves that port under "Share CAT",
			// which makes it an easy number to reach for.
			if k.host != "" {
				return fmt.Errorf("kenwood: %s is sending data but no reply to ID; or IF; — got %q. That end is not speaking the radio's CAT: point this at a serial-over-network bridge or the radio's own raw CAT port, not at Hamlib rigctld (4532)", k.host, seen)
			}
			return fmt.Errorf("kenwood: %s is sending data but no reply to ID; or IF; — got %q. Check the baud rate (set to %d here) and that nothing else is echoing the port", k.where(), seen, k.baud)
		}
		if k.host != "" {
			return fmt.Errorf("kenwood: %s accepted the connection but the rig sent nothing — check that the serial bridge points at the radio and that the radio is switched on", k.host)
		}
		return fmt.Errorf("kenwood: %s opened but the rig sent nothing — check that it is switched on and set to %d baud", k.portName, k.baud)
	}
	// Name what was actually connected to. A log reading "connected on  @ 0 baud"
	// after a network connect is the kind of line that sends someone hunting a
	// serial fault that does not exist.
	if k.host != "" {
		debugLog.Printf("kenwood: connected to %s (network serial bridge), model=%q", k.host, k.model)
	} else {
		debugLog.Printf("kenwood: connected on %s @ %d baud, model=%q", k.portName, k.baud, k.model)
	}
	return nil
}

func (k *Kenwood) Disconnect() {
	k.mu.Lock()
	defer k.mu.Unlock()
	if k.port != nil {
		_ = k.port.Close()
		k.port = nil
	}
}

// ReadState polls the rig. IF carries frequency, mode, VFO, split and TX state
// in one frame; the other VFO is only asked for when split is actually on, so
// the common simplex case costs a single round trip.
func (k *Kenwood) ReadState() (RigState, error) {
	k.mu.Lock()
	defer k.mu.Unlock()
	if k.port == nil {
		return RigState{}, fmt.Errorf("kenwood: not connected")
	}
	// While transmitting, don't poll: the rig returns "?;" to IF; during TX, and
	// treating that as a fault dropped the shared CAT link the digital-mode
	// software keys through. Hand back the last known state. The 30 s cap resyncs
	// if a SetPTT(false) was somehow missed, so a stuck TX can't freeze state.
	if k.tx && !k.txAt.IsZero() && time.Since(k.txAt) < 30*time.Second {
		s := k.lastState
		s.Connected = true
		// The panel still has to be told the radio is transmitting, and the
		// transmit meters still have to be read — this early return used to skip
		// both. The panel therefore showed MOX unlit while the rig was keyed, so
		// pressing it again sent ANOTHER transmit command instead of unkeying,
		// and the K3 stayed in TX. The power and SWR bars never moved either,
		// for the same reason: the only moment they mean anything is the one
		// moment they were not being read.
		k.panel.Transmitting = true
		k.readTXMeters()
		return s, nil
	}
	raw, err := k.ask("IF;")
	if err != nil {
		// A "?;" is the rig saying "busy", not "gone". Ride out a few and keep
		// serving the last known state, so the CAT link the digital software keys
		// through survives the moment after a transmission. A serial fault is NOT
		// covered: that returns a different error and drops through at once.
		if errors.Is(err, errRigRejected) && k.lastState.Connected && k.ifRejects < ifRejectGrace {
			k.ifRejects++
			debugLog.Printf("kenwood: IF; rejected (%d/%d) — rig busy, keeping the link", k.ifRejects, ifRejectGrace)
			s := k.lastState
			s.Connected = true
			return s, nil
		}
		k.ifRejects = 0
		return RigState{}, err
	}
	k.ifRejects = 0
	f, ok := parseKenwoodIF(raw)
	if !ok {
		return RigState{}, fmt.Errorf("kenwood: unparsable IF frame %q", raw)
	}

	s := RigState{Connected: true, Backend: "kenwood"}
	k.curVFO = f.VFO
	s.Vfo = f.VFO
	// Both branches of SetMode that emit MD6 identify a K3/K4: the Elecraft
	// backend, and the "MD6" data-mode option that exists for it. Decode the
	// read-back the same way, or OpsLog contradicts the mode it just set.
	s.Mode = kenwoodModeToADIF(f.Mode, k.digital, k.elecraft || k.dataMode == "data")
	s.Rig = k.model

	// IF reports the frequency of the VFO in USE (what the operator hears).
	rx := f.FreqHz
	tx := rx

	// IF's split bit is not filled in by every rig that speaks this dialect —
	// reported on a Flex through its Kenwood CAT emulation, where the frequency
	// reads perfectly and split never appears. So ask the question directly as
	// well: split IS "the transmit VFO differs from the receive VFO", which is
	// what FR/FT answer, and it is the same rule the Yaesu backend settled on.
	//
	// A rig that rejects FR/FT answers "?;" once and is never asked again, so
	// this costs two short commands per poll only where it actually works.
	split := f.Split
	if !split {
		// Confirm with FR;/FT; only once a second, not every poll. Running the two
		// extra commands each cycle tripled the poll time on a slow K3, so the
		// frequency — already read from IF above — only surfaced every couple of
		// seconds. Between checks the last FR/FT result stands.
		if time.Since(k.splitCheckAt) >= time.Second {
			k.splitCheckAt = time.Now()
			k.splitCached, k.splitVFOCached = false, ""
			rxv, rxOK := k.askVFO("FR;")
			txv, txOK := k.askVFO("FT;")
			if rxOK && txOK && rxv != txv {
				k.splitCached = true
				// Trust FR over IF for which VFO is in use: they were asked in the
				// same breath, and a rig that leaves the split bit empty may be just
				// as vague about the VFO field. The block below picks the TRANSMIT
				// VFO as "the other one" from f.VFO, so leaving them disagreeing
				// would read the transmit frequency off the wrong dial.
				if rxv == "A" || rxv == "B" {
					k.splitVFOCached = rxv
				}
			}
		}
		if k.splitCached {
			split = true
			if k.splitVFOCached != "" {
				k.curVFO, s.Vfo, f.VFO = k.splitVFOCached, k.splitVFOCached, k.splitVFOCached
			}
		}
	}
	f.Split = split

	if f.Split {
		// The OTHER VFO is the one IF did not report. Read it rather than assume,
		// and fall back to simplex if it cannot be read: a wrong TX frequency is
		// written into the log, which is worse than showing no split at all.
		other := "FB;"
		if f.VFO == "B" {
			other = "FA;"
		}
		var otherHz int64
		if r, err := k.ask(other); err == nil {
			if hz, ok := parseKenwoodFreq(r, strings.TrimSuffix(other, ";")); ok && hz > 0 && hz != rx {
				otherHz = hz
			}
		}
		if otherHz > 0 {
			// WHICH of the two is the transmit frequency depends on whether the rig
			// is transmitting RIGHT NOW.
			//
			// IF reports the VFO "in use", and in split that is the RECEIVE VFO on
			// receive and the TRANSMIT VFO on transmit. The code took it as the
			// receive VFO always, so the moment the operator keyed up the two
			// frequencies swapped: correct on receive, reversed on transmit, which
			// is exactly how it was reported from a TS-590 on USB.
			//
			// It matters beyond the display. FreqHz is what a QSO is logged on, and
			// a contact made in split would have gone into the log on the DX's
			// frequency instead of the operator's.
			if f.TX {
				tx, rx = rx, otherHz
			} else {
				tx = otherHz
			}
		}
	}
	if tx != rx {
		s.FreqHz = tx // ADIF: FREQ is the TRANSMIT frequency
		s.RxFreqHz = rx
		s.Split = true
	} else {
		s.FreqHz = rx
	}
	k.curFreq = s.FreqHz
	if s.Split {
		k.curRXFreq = s.RxFreqHz
	}
	k.lastState = s // cache for the transmit window, where we can't poll
	// The panel rides on the same poll and the same held mutex: its own reader
	// would have to take turns on the serial port, and the K3 is slow enough
	// that two readers taking turns is what makes a dial lag.
	// f.TX, not k.tx: the radio is the one that knows it is transmitting. k.tx
	// only records that OPSLOG keyed it, so a carrier raised with the front-panel
	// PTT, a footswitch or the mic button left the panel showing a receiver — and
	// the transmit meters, which are read only while transmitting, were never
	// read at all for anyone who keys the radio by hand.
	k.readPanel(s.Mode, s.Split, s.FreqHz, f.TX)
	return s, nil
}

// SetFrequency tunes the VFO the operator is actually on — writing FA blind is
// what makes a display disagree with the radio when they are on B.
func (k *Kenwood) SetFrequency(hz int64) error {
	k.mu.Lock()
	defer k.mu.Unlock()
	if k.port == nil {
		return fmt.Errorf("kenwood: not connected")
	}
	if hz <= 0 || hz > 99_999_999_999 {
		return fmt.Errorf("kenwood: frequency %d out of the 11-digit CAT range", hz)
	}
	cmd := "FA"
	if k.curVFO == "B" {
		cmd = "FB"
	}
	if err := k.write(fmt.Sprintf("%s%011d;", cmd, hz)); err != nil {
		return err
	}
	// Remember what we just commanded.
	//
	// While PTT is held the poll is skipped and State() hands back lastState — the
	// rig answers "?;" to IF; mid-transmission, and reading that as a fault used
	// to drop the whole link. But a frequency SET during that window then went
	// unrecorded, so the cache kept describing the dial as it was before.
	//
	// WSJT-X's "Fake It" is exactly that sequence: move the dial, key, transmit,
	// and afterwards put it back. Polling during the over, it was told the rig was
	// still on the receive frequency — so there was nothing to put back, and the
	// dial stayed on the transmit frequency for good. Every following over
	// started from there, which is the drift that was reported.
	//
	// Only simplex is updated here. Under split, FreqHz means the transmit
	// frequency while this write lands on whichever VFO the operator is on, and
	// guessing which side moved would be worse than a stale value the next poll
	// corrects on its own.
	if !k.lastState.Split {
		k.curFreq = hz
		k.lastState.FreqHz = hz
	}
	return nil
}

func (k *Kenwood) SetMode(mode string) error {
	k.mu.Lock()
	defer k.mu.Unlock()
	if k.port == nil {
		return fmt.Errorf("kenwood: not connected")
	}
	// Honour the operator's DATA-mode preference for a digital mode. No single
	// mode digit fits every rig, so the operator picks: keep the rig's mode, use
	// MD6 (K3/K4 DATA), or fall through to the default USB from kenwoodModeDigit.
	if isKenwoodDataMode(mode) {
		// An Elecraft always uses DATA A for a soundcard digital mode — that's what
		// the "Elecraft" backend means, so the dataMode option doesn't apply.
		if k.elecraft {
			if err := k.write("MD6;"); err != nil {
				return err
			}
			return k.write("DT0;")
		}
		switch k.dataMode {
		case "keep":
			return nil // leave whatever data mode the operator set on the rig
		case "data":
			// Elecraft K3/K4 DATA mode (MD6) PLUS the DATA-A submode (DT0). MD6 alone
			// can leave the rig in an FSK/PSK data submode (from a prior RTTY/PSK
			// session), where FT8 keys the transmitter but the rear sound-card audio
			// never modulates — "transmits but nothing comes out". DT0 forces DATA A,
			// the audio submode FT8 needs. This is the Elecraft path (the option is
			// labelled K3/K4); a real K3/K4 answers DT with nothing. A plain Kenwood
			// has no DT command and would "?;" it — but that's a misconfiguration
			// (pick USB there), and IF is never latched so the link self-recovers.
			if err := k.write("MD6;"); err != nil {
				return err
			}
			return k.write("DT0;")
		}
	}
	d := kenwoodModeDigit(mode, k.curFreq)
	if d == 0 {
		return fmt.Errorf("kenwood: no CAT mode for %q", mode)
	}
	return k.write(fmt.Sprintf("MD%c;", d))
}

// SetDataMode configures how a DATA/digital mode is set: "usb" (default), "data"
// (MD6, Elecraft K3/K4) or "keep" (don't change the rig's mode).
func (k *Kenwood) SetDataMode(m string) {
	k.mu.Lock()
	k.dataMode = strings.ToLower(strings.TrimSpace(m))
	k.mu.Unlock()
}

// SetElecraft marks this client as driving a K3/K4 (the "Elecraft" backend), so
// digital modes always land in DATA A (MD6+DT0). Set before Start.
func (k *Kenwood) SetElecraft(v bool) {
	k.mu.Lock()
	k.elecraft = v
	k.mu.Unlock()
}

// isKenwoodDataMode reports whether a mode name is a soundcard/data mode (FT8,
// PSK, JT…) rather than a voice/CW/RTTY mode the rig sets natively.
func isKenwoodDataMode(mode string) bool {
	switch strings.ToUpper(strings.TrimSpace(mode)) {
	case "", "LSB", "USB", "SSB", "CW", "CW-R", "CWR", "FM", "NFM", "AM",
		"RTTY", "FSK", "RTTY-R", "FSK-R":
		return false
	}
	return true
}

// SetSplit arms or clears split, and when arming puts txHz on the transmit VFO.
//
// Both halves in one call on purpose. WSJT-X sends "split on, VFO B" and "VFO B
// to 14075300" as two commands, and honouring only the first is worse than
// honouring neither: split would arm on whatever VFO B happened to hold, so the
// operator transmits somewhere they never chose while the software reports
// exactly what they asked for. Nothing is armed here until the frequency is on
// the dial.
//
// FR selects the receive VFO, FT the transmit one — the same pair the poll loop
// already reads to detect split, so this writes what State() knows how to read.
func (k *Kenwood) SetSplit(on bool, txHz int64) error {
	k.mu.Lock()
	defer k.mu.Unlock()
	if k.port == nil {
		return fmt.Errorf("kenwood: not connected")
	}
	if !on {
		// Transmit follows receive again. FR is left alone: which VFO the operator
		// listens on is theirs to choose, and clearing split should not move them.
		return k.write("FT0;")
	}
	if txHz <= 0 || txHz > 99_999_999_999 {
		return fmt.Errorf("kenwood: split TX frequency %d out of the 11-digit CAT range", txHz)
	}
	// The transmit dial FIRST, then arm. Arming first would transmit on the old
	// contents of VFO B for however long the next command takes to arrive — brief,
	// but on the wrong frequency, and this runs the instant before a transmission.
	if err := k.write(fmt.Sprintf("FB%011d;", txHz)); err != nil {
		return err
	}
	if err := k.write("FR0;"); err != nil { // receive on A
		return err
	}
	return k.write("FT1;") // transmit on B
}

func (k *Kenwood) SetPTT(on bool) error {
	k.mu.Lock()
	defer k.mu.Unlock()
	if k.port == nil {
		return fmt.Errorf("kenwood: not connected")
	}
	k.tx = on
	if on {
		k.txAt = time.Now()
		return k.write("TX;")
	}
	return k.write("RX;")
}

// SetPTTData keys the transmitter on the DATA input: TX1 on a TS-590, which is
// ACC2/USB rather than the front microphone. The radio's own manual is explicit
// that the parameter chooses the input — "0: SEND (normal transmission using
// the MIC input), 1: DATA SEND (ACC2/USB input)" — so a voice keyer playing
// into the rig's USB codec has to say TX1 or it transmits dead air while the
// radio listens to a microphone nobody is speaking into.
//
// Unkeying is the same RX either way; there is no data-flavoured stop.
func (k *Kenwood) SetPTTData(on bool) error {
	k.mu.Lock()
	defer k.mu.Unlock()
	if k.port == nil {
		return fmt.Errorf("kenwood: not connected")
	}
	k.tx = on
	if on {
		k.txAt = time.Now()
		return k.write("TX1;")
	}
	return k.write("RX;")
}

func (k *Kenwood) write(cmd string) error {
	if k.port == nil {
		return fmt.Errorf("kenwood: not connected")
	}
	traceText("kenwood", "TX", cmd)
	_, err := k.port.Write([]byte(cmd))
	return err
}

// drain reads and throws away whatever the rig has already sent, until it stays
// quiet for one read timeout or the budget runs out.
func (k *Kenwood) drain(budget time.Duration) {
	if k.port == nil {
		return
	}
	tmp := make([]byte, 256)
	deadline := time.Now().Add(budget)
	for time.Now().Before(deadline) {
		n, err := k.port.Read(tmp)
		if err != nil || n == 0 {
			return // an error here is not interesting: we are throwing this away
		}
		traceText("kenwood", "RX-drop", string(tmp[:n]))
	}
}

// ask sends a query and returns the reply belonging to THAT command. Anything
// else on the wire is discarded: a stray frame parsed as a frequency reads as
// "lost the rig" to the Manager, which then reconnects — the CAT link dropping
// for no reason (found the hard way on the Yaesu backend).
func (k *Kenwood) ask(cmd string) (string, error) {
	want := cmdPrefix(cmd)
	if k.unsupported[want] {
		return "", fmt.Errorf("kenwood: %s is not supported by this rig", want)
	}
	if err := k.write(cmd); err != nil {
		return "", err
	}
	tmp := make([]byte, 64)
	deadline := time.Now().Add(600 * time.Millisecond)
	for {
		// Consume whatever is already buffered BEFORE reading more: the answer
		// may have arrived attached to the previous one.
		for {
			i := bytes.IndexByte(k.rx, ';')
			if i < 0 {
				break
			}
			frame := string(k.rx[:i+1])
			k.rx = k.rx[i+1:]
			traceText("kenwood", "RX", frame)
			if frame == "?;" {
				// Sentinel-wrapped so the poll loop can tell "the rig said no" apart
				// from a serial fault. The two look identical as plain errors and must
				// not be handled the same way: one means try again in a moment, the
				// other means the link is gone.
				// IF; and ID; are universal on Kenwood/Elecraft — a "?;" to them is a
				// transient "busy" (typically mid-transmit, or a menu open on the rig),
				// NOT "unsupported". Latching them off would blind the poll loop for
				// good and read as "lost the rig". Only remember the OPTIONAL commands
				// (FR/FT/…) so the poll loop stops paying a 600 ms timeout for those.
				// While TRANSMITTING the rig refuses a great deal that it answers
				// perfectly well on receive, so a "?;" then says nothing about what
				// the radio supports. Latching it would silence a meter for the rest
				// of the session on the strength of one badly timed question — and
				// the meters are read precisely while transmitting.
				if want != "IF" && want != "ID" && !k.noLatch {
					k.unsupported[want] = true
					debugLog.Printf("kenwood: this rig does not support %q — not asking again", cmd)
				}
				return "", fmt.Errorf("kenwood: %s rejected: %w", want, errRigRejected)
			}
			if strings.HasPrefix(frame, want) {
				return frame, nil
			}
			debugLog.Printf("kenwood: discarding %q while waiting for %s", frame, want)
			// Remember the first unexpected frame: if the whole handshake fails, this
			// is what tells the operator the radio was talking after all.
			if k.heard == "" {
				k.heard = frame
			}
		}
		if !time.Now().Before(deadline) {
			return "", fmt.Errorf("kenwood: timeout answering %q", cmd)
		}
		n, err := k.port.Read(tmp)
		if err != nil {
			return "", err
		}
		if n > 0 {
			k.rx = append(k.rx, tmp[:n]...)
		}
		// n == 0 is a read timeout, not silence for good: the rig may still be
		// composing its answer.
	}
}

// ── Frame parsing ──────────────────────────────────────────────────────────

// kenwoodIF is what the 38-character IF status frame carries.
type kenwoodIF struct {
	FreqHz int64
	Mode   byte
	VFO    string // "A" or "B"
	Split  bool
	// TX is parsed even though RigState has no PTT field: it is one character of
	// the same frame, and having it here means a future "transmitting" indicator
	// costs no extra round trip.
	TX bool
}

// parseKenwoodIF reads the TS-2000/TS-590 status frame. Its layout — the same
// one internal/catemu emits — is:
//
//	IF | freq(11) | step(4) | RIT(±5) | RIT/XIT/bank(3) | mem(2) | rx-tx(1) |
//	mode(1) | VFO(1) | scan(1) | split(1) | tone(1) | tone#(2) | shift(1) | ;
//
// Fields are read by POSITION, so the length is checked first: a short frame
// means a truncated read, and indexing into it would panic or, worse, silently
// yield a wrong frequency.
func parseKenwoodIF(reply string) (kenwoodIF, bool) {
	r := strings.TrimSpace(reply)
	if !strings.HasPrefix(r, "IF") || len(r) < 38 {
		return kenwoodIF{}, false
	}
	hz, err := strconv.ParseInt(strings.TrimSpace(r[2:13]), 10, 64)
	if err != nil || hz <= 0 {
		return kenwoodIF{}, false
	}
	out := kenwoodIF{FreqHz: hz, Mode: r[29], VFO: "A", TX: r[28] == '1', Split: r[32] == '1'}
	if r[30] == '1' {
		out.VFO = "B"
	}
	return out, true
}

// parseKenwoodFreq reads an FA/FB reply — ELEVEN digits on Kenwood, where Yaesu
// uses nine. The prefix is checked so an FB reply is never accepted as FA.
func parseKenwoodFreq(reply, prefix string) (int64, bool) {
	r := strings.TrimSpace(reply)
	if !strings.HasPrefix(r, prefix) {
		return 0, false
	}
	digits := strings.TrimSuffix(strings.TrimPrefix(r, prefix), ";")
	if digits == "" {
		return 0, false
	}
	hz, err := strconv.ParseInt(digits, 10, 64)
	if err != nil || hz <= 0 {
		return 0, false
	}
	return hz, true
}

// ── Modes ──────────────────────────────────────────────────────────────────

// kenwoodModeDigit maps an ADIF mode to the Kenwood digit. The sideband follows
// the frequency by worldwide convention — a backend that puts USB on 40 m makes
// every SSB QSO in the log wrong.
func kenwoodModeDigit(mode string, hz int64) byte {
	m := strings.ToUpper(strings.TrimSpace(mode))
	switch m {
	case "":
		return 0
	case "LSB":
		return '1'
	case "USB":
		return '2'
	case "CW":
		return '3'
	case "CW-R", "CWR":
		return '7'
	case "FM":
		return '4'
	case "AM":
		return '5'
	case "RTTY", "FSK":
		return '6'
	case "RTTY-R", "FSK-R":
		return '9'
	case "SSB":
		if hz > 0 && hz < 10_000_000 {
			return '1'
		}
		return '2'
	}
	// Any other digital mode (FT8, PSK, JT…) → USB on every band. There is NO single
	// mode digit that is right for both families: a K3 wants DATA (MD6), a TS-590SG
	// wants USB-DATA (MD2 + its DATA modifier), and MD6 on the TS-590SG is FSK/RTTY
	// — wrong for FT8. USB is the safe common base (the reversed-sideband "DATA REV"
	// only came from the old LSB-below-10-MHz rule). A rig-specific DATA mode is a
	// follow-up that needs the model, not a blind default.
	return '2'
}

// kenwoodModeToADIF turns the rig's mode digit into what the log records.
// Digital modes are indistinguishable from SSB over CAT — the rig only knows
// it is on USB — so the operator's configured digital mode is used, exactly as
// the other backends do.
//
// md6IsData says whether MD6 means DATA on THIS rig — true for an Elecraft
// (the K3/K4 backend, and the "MD6" data-mode option that targets it), false
// for a plain Kenwood. See below.
func kenwoodModeToADIF(d byte, digital string, md6IsData bool) string {
	switch d {
	case '1':
		return "LSB"
	case '2':
		return "USB"
	case '3', '7':
		return "CW"
	case '4':
		return "FM"
	case '5':
		return "AM"
	case '6', '9':
		// One digit, two meanings. On a Kenwood MD6/MD9 is FSK/FSK-R; on an
		// Elecraft it is DATA / DATA-REV — and DATA is precisely what SetMode
		// puts a K3/K4 into for FT8. Reading it straight back as RTTY meant a K3
		// running FT8 reported RTTY: the QSO was logged on the wrong mode, and
		// the shared CAT link told WSJT-X/JTDX the rig sat in RTTY while they had
		// asked for a data mode.
		if md6IsData {
			return digital
		}
		return "RTTY"
	}
	return ""
}

// kenwoodModels maps the ID reply to a name for the status bar. An unknown code
// is shown as-is rather than refused: the model name is cosmetic, and a rig that
// answers everything else must not be rejected over it.
var kenwoodModels = map[string]string{
	"017": "TS-570",
	"019": "TS-2000",
	"020": "TS-480",
	"021": "TS-590S",
	// 022 reported by a real TS-990S in the field. Kenwood's documentation gives
	// 024 for that radio, so both are kept: the observed value wins where they
	// disagree, and neither maps to anything else.
	"022": "TS-990S",
	"023": "TS-590SG",
	"024": "TS-990S",
	"025": "TS-890S",
}

// openPort opens the serial link, or whatever dialPort provides in a test.
func (k *Kenwood) openPort() (serial.Port, error) {
	if k.dialPort != nil {
		return k.dialPort()
	}
	if k.host != "" {
		c, err := net.DialTimeout("tcp", k.host, 5*time.Second)
		if err != nil {
			return nil, err
		}
		return &tcpSerial{conn: c}, nil
	}
	p, err := serial.Open(k.portName, &serial.Mode{BaudRate: k.baud})
	if err != nil {
		return nil, err
	}
	// The modem lines are only touched when the operator asks for it. See
	// lowerLines: both defaults break somebody's station.
	if k.lowerLines {
		_ = p.SetDTR(false)
		_ = p.SetRTS(false)
	}
	return p, nil
}

// tcpSerial presents a TCP connection as a serial.Port, so the backend has one
// code path whether the rig is on a wire or on the network.
//
// The modem-control methods are no-ops rather than errors: a bridge has no DTR
// to raise, and failing them would break a caller that sets them defensively.
type tcpSerial struct{ conn net.Conn }

func (t *tcpSerial) Read(p []byte) (int, error) {
	n, err := t.conn.Read(p)
	// A read deadline expiring is this transport's "no data yet", exactly what a
	// serial read timeout means to the caller — not a dead link. Reporting it as
	// an error would make ask() abandon a rig that is merely thinking.
	if err != nil {
		var ne net.Error
		if errors.As(err, &ne) && ne.Timeout() {
			return n, nil
		}
	}
	return n, err
}
func (t *tcpSerial) Write(p []byte) (int, error) { return t.conn.Write(p) }
func (t *tcpSerial) Close() error                { return t.conn.Close() }
func (t *tcpSerial) SetReadTimeout(d time.Duration) error {
	if d <= 0 {
		return t.conn.SetReadDeadline(time.Time{})
	}
	return t.conn.SetReadDeadline(time.Now().Add(d))
}
func (t *tcpSerial) SetMode(*serial.Mode) error { return nil }
func (t *tcpSerial) Drain() error               { return nil }
func (t *tcpSerial) ResetInputBuffer() error    { return nil }
func (t *tcpSerial) ResetOutputBuffer() error   { return nil }
func (t *tcpSerial) SetDTR(bool) error          { return nil }
func (t *tcpSerial) SetRTS(bool) error          { return nil }
func (t *tcpSerial) GetModemStatusBits() (*serial.ModemStatusBits, error) {
	return &serial.ModemStatusBits{}, nil
}
func (t *tcpSerial) Break(time.Duration) error { return nil }

// askVFO asks FR; or FT; and returns "A" or "B".
//
// The reply is FR0; / FR1; — the digit right after the two-letter command.
// Anything else (a rig that answers with more fields, or not at all) returns
// false, and the caller keeps whatever IF said rather than inventing a split.
func (k *Kenwood) askVFO(cmd string) (string, bool) {
	r, err := k.ask(cmd)
	if err != nil {
		return "", false
	}
	want := cmdPrefix(cmd)
	body := strings.TrimSuffix(strings.TrimPrefix(r, want), ";")
	if body == "" {
		return "", false
	}
	switch body[0] {
	case '0':
		return "A", true
	case '1':
		return "B", true
	}
	// 2 is "sub receiver" on a TS-2000 — real, but not a VFO we track. Saying
	// nothing is better than mapping it onto A or B and reporting a split that
	// does not exist.
	return "", false
}
