package cat

import (
	"errors"
	"fmt"
	"strings"

	"hamlog/internal/applog"
	"hamlog/internal/cat/civ"
)

// Satellite operation on an Icom.
//
// Two rigs in the range have a satellite mode of their own — the IC-9700 and
// the IC-9100 — and on those the right thing to do is ask the radio for it
// rather than build an imitation out of split. Their satellite mode pairs the
// MAIN band (the downlink) with the SUB band (the uplink), gives full duplex,
// and keeps the two dials linked the way the designers meant. Every other Icom
// has one receiver on one band: it can be tuned to the downlink, and that is
// the whole truth about what it can do on a cross-band satellite.
//
// UNTESTED ON HARDWARE. Built from the IC-9700 CI-V reference: 0x16 0x5A arms
// satellite mode, 0x07 0xD0 / 0xD1 select MAIN and SUB, and once a band is
// selected the ordinary 0x05 / 0x06 tune it. If an IC-9700 owner reports it
// misbehaving, the log lines below name every frame sent.

// ErrSatUplinkUnreachable says the downlink was tuned and the uplink was not,
// because the radio has no second receiver and the two are on different bands.
//
// A distinct error rather than a silent half-success: a tracker that quietly
// stops transmitting where the operator expects it to is worse than one that
// says it cannot. The caller reports it once, not once per Doppler step.
var ErrSatUplinkUnreachable = errors.New("cat: this radio has one receiver — the uplink is on another band and cannot be set")

// SetSatellite arms the rig's own satellite mode.
func (b *IcomSerial) SetSatellite(on bool) error {
	if !b.satNative {
		// Nothing to arm and nothing to break: the tuning path below does what
		// this radio can do without any mode change. Refusing here would deny an
		// operator the downlink, which is most of the value on a receive-heavy
		// pass.
		b.satOn = on
		return nil
	}
	if err := b.exec(civ.CmdSwitch, civ.SubSwSatellite, boolByte(on)); err != nil {
		return fmt.Errorf("icom: satellite mode %v refused: %w", on, err)
	}
	b.satOn = on
	applog.Printf("icom: satellite mode %v (%s)", on, b.model)
	if on {
		// Leave the radio pointing at MAIN. Everything else in OpsLog — the poll
		// loop, the logged frequency, the operator's dial — reads the selected
		// band, and on a satellite the band worth reading is the one carrying the
		// downlink.
		_ = b.exec(civ.CmdVFO, civ.SubVFOMain)
	}
	return nil
}

// TuneSatellite puts the receiver on downHz and the transmitter on upHz.
func (b *IcomSerial) TuneSatellite(downHz, upHz int64, downMode, upMode string) error {
	if downHz <= 0 {
		return fmt.Errorf("icom: no downlink frequency")
	}
	if !b.satNative {
		return b.tuneSatSingleBand(downHz, upHz, downMode, upMode)
	}
	// MAIN — the downlink.
	if err := b.exec(civ.CmdVFO, civ.SubVFOMain); err != nil {
		return fmt.Errorf("icom: could not select the main band: %w", err)
	}
	if err := b.SetFrequency(downHz); err != nil {
		return err
	}
	if err := b.satSetMode(downMode, downHz); err != nil {
		return err
	}
	// SUB — the uplink.
	if upHz > 0 {
		if err := b.exec(civ.CmdVFO, civ.SubVFOSub); err != nil {
			return fmt.Errorf("icom: could not select the sub band: %w", err)
		}
		uerr := b.execIdempotent(fmt.Sprintf("set uplink %d Hz", upHz),
			append([]byte{civ.CmdSetFreq}, civ.FreqToBCD(upHz)...)...)
		merr := b.satSetMode(upMode, upHz)
		// Back to MAIN whatever happened. A rig left pointing at SUB reports the
		// uplink as its frequency, and every band-dependent thing in OpsLog —
		// the log, the antenna, the amplifier — would follow the transmitter
		// onto the wrong band.
		if err := b.exec(civ.CmdVFO, civ.SubVFOMain); err != nil {
			applog.Printf("icom: could not return to the main band: %v", err)
		}
		if uerr != nil {
			return uerr
		}
		if merr != nil {
			return merr
		}
	}
	return nil
}

// satSetMode sets the mode of whichever band is currently selected. An empty
// mode leaves it alone — a linear transponder is worked in one mode for a whole
// pass, and re-sending it every second is traffic for nothing.
func (b *IcomSerial) satSetMode(mode string, freqHz int64) error {
	mode = strings.TrimSpace(mode)
	if mode == "" {
		return nil
	}
	// modeCode resolves "SSB" against the CURRENT dial to pick a sideband, which
	// is wrong here twice over: the dial may still be on the other band, and on
	// satellites USB is the convention on both sides whatever the frequency.
	code, data, err := b.modeCode(satSideband(mode))
	if err != nil {
		return err
	}
	return b.setModeBytes(mode, code, data)
}

// satSideband is the sideband convention above 30 MHz: USB, on both the uplink
// and the downlink, including the parts of a linear transponder that fall in
// what would be an LSB band on HF. The exceptions — AO-7's mode A downlink on
// 29 MHz among them — are still USB by convention, so there is no exception to
// make.
func satSideband(mode string) string {
	if strings.EqualFold(strings.TrimSpace(mode), "SSB") {
		return "USB"
	}
	return mode
}

// SatReceiveHz is where the receiver is now.
func (b *IcomSerial) SatReceiveHz() (int64, error) {
	if b.satNative {
		// The selected band is MAIN — see TuneSatellite, which always returns to
		// it — so the ordinary frequency read is the downlink.
		if err := b.exec(civ.CmdVFO, civ.SubVFOMain); err != nil {
			applog.Printf("icom: sat readback could not select main: %v", err)
		}
	}
	return b.readFreq()
}

// tuneSatSingleBand is every other Icom: one receiver, one band.
//
// The downlink is set, because that is what the operator is listening to. The
// uplink is set through split only when it is close enough to be on the same
// band — QO-100 behind transverters, AO-7's mode A — and otherwise reported as
// out of reach rather than quietly skipped.
func (b *IcomSerial) tuneSatSingleBand(downHz, upHz int64, downMode, _ string) error {
	if err := b.SetFrequency(downHz); err != nil {
		return err
	}
	if err := b.satSetMode(downMode, downHz); err != nil {
		return err
	}
	if upHz <= 0 {
		return nil
	}
	// One megahertz apart is the working definition of "the same band" here: it
	// covers a transponder's own passband and any sensible transverter pairing,
	// and excludes every real cross-band satellite (145 / 435 MHz).
	if abs64(upHz-downHz) > 1_000_000 {
		return ErrSatUplinkUnreachable
	}
	if err := b.exec(append([]byte{civ.CmdVfoFreq, civ.SubVfoUnselected}, civ.FreqToBCD(upHz)...)...); err != nil {
		return err
	}
	if !b.satOn {
		return nil
	}
	return b.exec(civ.CmdSplit, boolByte(true))
}

func abs64(v int64) int64 {
	if v < 0 {
		return -v
	}
	return v
}
