#pragma once

#include <QObject>
#include <QString>
#include <QStringList>
#include <QMap>
#include <QTimer>

#include "core/backends/SliceDelta.h"

namespace AetherSDR {

// A "slice" in SmartSDR terminology is an independent receive channel.
// Each slice has its own frequency, mode, filter, and audio settings.
class SliceModel : public QObject {
    Q_OBJECT

    Q_PROPERTY(int    sliceId    READ sliceId)
    Q_PROPERTY(double frequency  READ frequency  WRITE setFrequency  NOTIFY frequencyChanged)
    Q_PROPERTY(QString mode      READ mode       WRITE setMode       NOTIFY modeChanged)
    Q_PROPERTY(int filterLow     READ filterLow  NOTIFY filterChanged)
    Q_PROPERTY(int filterHigh    READ filterHigh NOTIFY filterChanged)
    Q_PROPERTY(bool active       READ isActive   NOTIFY activeChanged)
    Q_PROPERTY(bool txSlice      READ isTxSlice  NOTIFY txSliceChanged)

public:
    explicit SliceModel(int id, QObject* parent = nullptr);
    ~SliceModel() override;

    // Getters
    int     sliceId()    const { return m_id; }
    // Per-client display letter from the radio's `index_letter` status field.
    // In Multi-Flex sessions this is the letter as the radio assigns it to
    // *this* client (e.g. the second client's first slice is "A" even when
    // its global sliceId is 2).  Falls back to `'A' + sliceId` if the field
    // hasn't arrived yet (early in status, or older firmware).
    QString letter()     const { return m_letter.isEmpty()
                                     ? QString(QChar('A' + m_id))
                                     : m_letter; }
    QString panId()      const { return m_panId; }       // e.g. "0x40000000"
    double  frequency()  const { return m_frequency; }   // MHz
    QString mode()       const { return m_mode; }
    QStringList modeList() const { return m_modeList; }
    int     filterLow()  const { return m_filterLow; }   // Hz offset
    int     filterHigh() const { return m_filterHigh; }
    // Monotonic counter bumped ONLY by setFilterWidth() (operator preset/drag),
    // never by applyAdaptiveFilter() or status echoes. Lets the adaptive engine
    // recognise a genuine manual filter edit without value-guessing. RFC #3878.
    quint64 userFilterEpoch() const { return m_userFilterEpoch; }
    // Getters — Adaptive RX filter (client-side ESSB auto-fit; RFC #3878)
    bool    adaptiveFilterEnabled() const { return m_adaptiveFilterEnabled; }
    int     adaptiveMinLowCut()     const { return m_adaptiveMinLowCut; }  // Hz
    int     adaptiveMaxHighCut()    const { return m_adaptiveMaxHighCut; } // Hz
    int     adaptiveMinSnr()        const { return m_adaptiveMinSnr; }   // 0=Sensitive,1=Normal,2=Strong
    int     adaptiveResponse()      const { return m_adaptiveResponse; } // 0=Fast,1=Normal,2=Slow
    int     adaptiveSplatter()      const { return m_adaptiveSplatter; } // 0=Tight,1=Normal,2=Wide
    bool    adaptiveHetReject()     const { return m_adaptiveHetReject; } // opt-in edge-het cut
    bool    adaptiveActive()        const { return m_adaptiveActive; }
    bool    isActive()   const { return m_active; }
    bool    isTxSlice()  const { return m_txSlice; }
    float   rfGain()     const { return m_rfGain; }
    float   audioGain()  const { return m_externalReceiveAudioReplacement
                                      ? m_externalReceiveAudioGain
                                      : m_audioGain; }
    float   flexAudioGain() const { return m_audioGain; }
    int     audioPan()   const { return m_externalReceiveAudioReplacement
                                      ? m_externalReceiveAudioPan
                                      : m_audioPan; }
    int     flexAudioPan() const { return m_audioPan; }

    // Getters — RX DSP state
    QString rxAntenna()   const { return m_rxAntenna; }
    QString txAntenna()   const { return m_txAntenna; }
    QStringList rxAntennaList() const { return m_rxAntennaList; }
    QStringList txAntennaList() const { return m_txAntennaList; }
    bool    isLocked()    const { return m_locked; }
    bool    qskOn()       const { return m_qsk; }
    bool    nbOn()        const { return m_nb; }
    bool    nrOn()        const { return m_nr; }
    bool    anfOn()       const { return m_anf; }
    bool    nrlOn()       const { return m_nrl; }
    bool    nrsOn()       const { return m_nrs; }
    bool    rnnOn()       const { return m_rnn; }
    bool    nrfOn()       const { return m_nrf; }
    bool    anflOn()      const { return m_anfl; }
    bool    anftOn()      const { return m_anft; }
    // The radio's own single in-passband notch (RadioCapabilities::
    // hasManualNotch). mnLevel() is a POSITION, 0..100 across the passband,
    // not a frequency and not a depth.
    bool    mnOn()        const { return m_mn; }
    bool    apfOn()       const { return m_apf; }
    int     apfLevel()    const { return m_apfLevel; }
    int     nbLevel()     const { return m_nbLevel; }
    int     nrLevel()     const { return m_nrLevel; }
    int     anfLevel()    const { return m_anfLevel; }
    int     nrlLevel()    const { return m_nrlLevel; }
    int     nrsLevel()    const { return m_nrsLevel; }
    int     nrfLevel()    const { return m_nrfLevel; }
    int     anflLevel()   const { return m_anflLevel; }
    int     mnLevel()     const { return m_mnLevel; }
    QString agcMode()      const { return m_agcMode; }
    QString flexAgcMode()  const { return m_agcMode; }
    QString receiveAgcMode() const { return m_externalReceiveAudioReplacement
                                          ? m_externalReceiveAgcMode
                                          : m_agcMode; }
    int     agcThreshold() const { return m_agcThreshold; }
    int     flexAgcThreshold() const { return m_agcThreshold; }
    int     receiveAgcThreshold() const { return m_externalReceiveAudioReplacement
                                              ? m_externalReceiveAgcThreshold
                                              : m_agcThreshold; }
    int     agcOffLevel()  const { return m_agcOffLevel; }
    int     flexAgcOffLevel() const { return m_agcOffLevel; }
    int     receiveAgcOffLevel() const { return m_externalReceiveAudioReplacement
                                             ? m_externalReceiveAgcOffLevel
                                             : m_agcOffLevel; }
    bool    audioMute()   const { return m_externalReceiveAudioReplacement
                                      ? m_externalReceiveAudioMute
                                      : m_audioMute; }
    bool    flexAudioMute() const { return m_audioMute; }
    bool    squelchOn()   const { return m_squelchOn; }
    bool    flexSquelchOn() const { return m_squelchOn; }
    bool    receiveSquelchOn() const { return m_externalReceiveAudioReplacement
                                           ? m_externalReceiveSquelchOn
                                           : m_squelchOn; }
    bool    externalReceiveAutoSquelchOn() const
    {
        return m_externalReceiveAutoSquelch;
    }
    int     squelchLevel()const { return m_squelchLevel; }
    int     flexSquelchLevel() const { return m_squelchLevel; }
    int     receiveSquelchLevel() const { return m_externalReceiveAudioReplacement
                                              ? m_externalReceiveSquelchLevel
                                              : m_squelchLevel; }
    // Last manual-mode threshold the operator chose for THIS slice,
    // independent of squelchLevel() (which Auto mode also overwrites with
    // its own computed threshold each update). RxApplet restores Manual
    // mode's slider from this when it reattaches to a slice, so switching
    // the active slice doesn't pull in another slice's threshold (#3326).
    int     manualSquelchLevel() const { return m_manualSquelchLevel; }
    void    setManualSquelchLevel(int level) { m_manualSquelchLevel = qBound(0, level, 100); }
    // Whether a radio-echoed squelch_level for this slice should be taken as
    // the operator's manual choice.  Driven by whichever surface owns the
    // slice's SQL mode (RxApplet), and true only while that mode is Manual:
    //   Manual — the echo is a genuine manual level (the operator's own
    //            edit, another Multi-Flex client, or session restore) and
    //            must update the manual memory.
    //   Auto   — the level is algorithm-computed and re-pushed every tick.
    //   Off    — the mode push sends sqlManualLevel(), but nothing keeps a
    //            disabled squelch's level pinned, so an echo here is not a
    //            threshold the operator chose either.
    // Adopting the last two would silently overwrite the threshold the
    // operator actually chose (#4592) — the same silent-overwrite class
    // #3326 fixed, reached via the status-echo path rather than a direct
    // client write.  Defaults true so a slice with no surface attached (a
    // non-active VFO flag, a slice reclaimed from a previous session) still
    // tracks genuine manual changes — the leak #4592 part 1 set out to close.
    void    setSquelchEchoIsManual(bool isManual) { m_squelchEchoIsManual = isManual; }
    bool    ritOn()       const { return m_ritOn; }
    int     ritFreq()     const { return m_ritFreq; }
    bool    xitOn()       const { return m_xitOn; }
    int     xitFreq()     const { return m_xitFreq; }
    int     stepHz()      const { return m_stepHz; }
    // HOST-BANK MEMORY RECALL ONLY — do not call this on a radio that owns its
    // slots. Step size is radio-authoritative (AGENTS.md, Principle II): on a
    // Flex it arrives as `slice` status and the client must never assert it, or
    // the two fight on reconnect. On a backend with no command plane there is no
    // radio opinion to defer to, the host bank owns the channel, and a recalled
    // step would otherwise never take because the wire command that normally
    // round-trips it is dropped. Named for its one caller so the exception stays
    // visible; see RadioModel::recallLocalMemory().
    void    applyRecalledStepHz(int hz);
    QVector<int> stepList() const { return m_stepList; }
    int     daxChannel()  const { return m_daxChannel; }
    int     rttyMark()        const { return m_rttyMark; }
    int     rttyMarkDefault() const { return m_rttyMarkDefault; }
    int     rttyShift()   const { return m_rttyShift; }
    int     diglOffset()  const { return m_diglOffset; }
    int     diguOffset()  const { return m_diguOffset; }

    // Record/playback state (radio-managed)
    bool    recordOn()    const { return m_recordOn; }
    bool    playOn()      const { return m_playOn; }
    bool    playEnabled() const { return m_playEnabled; }

    // Getters — FM duplex/repeater
    QString fmToneMode()          const { return m_fmToneMode; }
    QString fmToneValue()         const { return m_fmToneValue; }
    QString repeaterOffsetDir()   const { return m_repeaterOffsetDir; }
    double  fmRepeaterOffsetFreq()const { return m_fmRepeaterOffsetFreq; }
    double  txOffsetFreq()        const { return m_txOffsetFreq; }
    int     fmDeviation()         const { return m_fmDeviation; }

    // Setters (emit signals AND send radio commands)
    void setFrequency(double mhz);           // slice tune autopan=0 — no recenter
    void tuneAndRecenter(double mhz);      // slice tune — recenters pan (band changes)
    void setMode(const QString& mode);
    void setFilterWidth(int low, int high);
    // Adaptive RX filter (client-side; the toggle/bounds send no radio
    // command — the engine drives the passband via applyAdaptiveFilter()).
    void setAdaptiveFilterEnabled(bool on);
    void setAdaptiveMinLowCut(int hz);
    void setAdaptiveMaxHighCut(int hz);
    void setAdaptiveMinSnr(int level);     // 0=Sensitive,1=Normal,2=Strong
    void setAdaptiveResponse(int level);   // 0=Fast,1=Normal,2=Slow
    void setAdaptiveSplatter(int level);   // 0=Tight,1=Normal,2=Wide
    void setAdaptiveHetReject(bool on);    // opt-in edge-het cut
    void setAdaptiveActive(bool on);
    // Engine-driven passband write: same wire command as setFilterWidth, but
    // a distinct entry point so the engine can recognise its own writes (vs
    // a user preset/drag) when tracking the manual baseline. RFC #3878.
    void applyAdaptiveFilter(int low, int high);
    void setAudioGain(float gain);
    void setRfGain(float gain);
    void setAudioPan(int pan);
    void setAudioMute(bool mute);
    void setExternalReceiveAudioReplacementMute(bool active,
                                                bool restoreMute = false);
    // A FLEX band-stack recall persists the slice's current audio_mute value.
    // Temporarily restore the pre-replacement Flex mute before the band command
    // so the KiwiSDR suppression mute is never written into the outgoing slot.
    // The external receive presentation remains active throughout.
    void prepareExternalReceiveAudioReplacementBandRecall(bool restoreMute);
    void setExternalReceiveAutoSquelch(bool on);
    bool externalReceiveReplacementActive() const
    {
        return m_externalReceiveAudioReplacement;
    }
    void setDiversity(bool on);
    bool diversity() const { return m_diversity; }
    bool isDiversityChild() const { return m_diversityChild; }
    bool isDiversityParent() const { return m_diversityParent; }
    int  diversityIndex() const { return m_diversityIndex; }
    bool escEnabled() const { return m_escEnabled; }
    float escGain() const { return m_escGain; }
    float escPhaseShift() const { return m_escPhaseShift; }
    void setEscEnabled(bool on);
    void setEscGain(float gain);
    void setEscPhaseShift(float deg);
    void setRxAntenna(const QString& ant);
    void setTxAntenna(const QString& ant);
    void setLocked(bool locked);
    void notifyTuneBlockedByLock();

    // Centralized 500ms "LOCKED" visual-feedback gate (see #2983).
    // Widgets that render a sustained LOCKED overlay connect to
    // lockedFeedbackActiveChanged() and read isLockedFeedbackActive() in
    // their repaint paths. Widgets that only need a one-shot reaction to a
    // blocked tune attempt (e.g. cancel direct-entry, status-bar message)
    // continue to use tuneBlockedByLock().
    bool isLockedFeedbackActive() const { return m_lockedFeedbackActive; }
    static constexpr int kLockedFeedbackMs = 500;
    void setQsk(bool on);
    void setNb(bool on);
    void setNr(bool on);
    void setAnf(bool on);
    void setNrl(bool on);
    void setNrs(bool on);
    void setRnn(bool on);
    void setNrf(bool on);
    void setAnfl(bool on);
    void setAnft(bool on);
    void setMn(bool on);
    void setApf(bool on);
    void setApfLevel(int v);
    void setNbLevel(int v);
    void setNrLevel(int v);
    void setAnfLevel(int v);
    void setNrlLevel(int v);
    void setNrsLevel(int v);
    void setNrfLevel(int v);
    void setAnflLevel(int v);
    void setMnLevel(int v);
    void setAgcMode(const QString& mode);
    void setAgcThreshold(int value);
    void setAgcOffLevel(int value);
    void setSquelch(bool on, int level);
    // For genuine operator-driven manual squelch input only (a VFO flag's
    // own SQL controls, a controller-mapped squelch knob) — setSquelch()
    // plus recording the level as the operator's manual choice, in one
    // call so no caller can push a manual level and forget the second half
    // (#4592). Algorithm-driven writes (Auto mode) must keep calling plain
    // setSquelch() — routing them here would silently overwrite the
    // operator's last manual choice with the auto-computed value.
    void setManualSquelch(bool on, int level);
    void setRit(bool on, int hz);
    void setXit(bool on, int hz);
    void setDaxChannel(int ch);
    void setRttyMark(int hz);
    void setRttyShift(int hz);
    // Called by RadioModel when the radio's rtty_mark_default changes.
    void setRttyMarkDefault(int hz) { m_rttyMarkDefault = hz; }
    void setDiglOffset(int hz);
    void setDiguOffset(int hz);
    void setTxSlice(bool on);
    void setActive(bool on);
    void setRecordOn(bool on);
    void setPlayOn(bool on);

    // Setters — FM duplex/repeater
    void setFmToneMode(const QString& mode);
    void setFmToneValue(const QString& value);
    void setRepeaterOffsetDir(const QString& dir);
    void setFmRepeaterOffsetFreq(double mhz);
    void setTxOffsetFreq(double mhz);
    void setFmDeviation(int hz);

    // Apply a normalized, typed slice delta from the backend
    // (IRadioBackend::sliceChanged). Vendor-neutral fields only — the Flex wire
    // decode lives in FlexBackend::decodeSliceStatus. Applies only the fields the
    // delta has engaged (present-only). (aetherd RFC 2.3.)
    void applyChanges(const SliceDelta& delta);

    // Force a re-emit of letterChanged() with the current letter — used
    // when a global display preference (e.g. AppSettings
    // SliceLetterDisplay) changes so widgets repaint without us having
    // to know which ones they are.  See #2606.
    void emitLetterRefresh();

    // Drain pending outgoing commands (called by RadioModel to send them)
    QStringList drainPendingCommands();

signals:
    void letterChanged(const QString& newLetter);
    void frequencyChanged(double mhz);
    // Emitted after a local setter has issued a frequency command. Unlike
    // frequencyChanged, radio-status application does not emit this signal.
    void frequencyCommandIssued(double mhz);
    // Filter change originating from the OPERATOR, not from radio status.
    // filterChanged() fires for both, so it must not be used to drive a command
    // back at the radio; that would echo the radio's own state as a request
    // (Principle II). Mirrors frequencyCommandIssued.
    void filterCommandIssued(int lowHz, int highHz);
    // Operator-issued AGC change. Distinct from agcModeChanged/
    // agcThresholdChanged, which ALSO fire when radio status is applied —
    // driving a command off those would echo the radio's own state back at it
    // as a request (Principle II). Emitted only from setAgcMode()/
    // setAgcThreshold(), and always carries BOTH values because a backend
    // configuring a DSP AGC needs the pair to act on either.
    void agcCommandIssued(const QString& mode, int thresholdDb);

    // RECEIVE DSP THE RADIO RUNS. Same contract as the signals above: emitted
    // only by the operator-facing setters, never by status application, so a
    // radio's own echo can never come back as a fresh command (Principle II).
    //
    // These exist because every one of these controls used to emit FlexRadio
    // wire text and nothing else. On a Flex that string IS the command; on any
    // other backend it was discarded, and there was no seam verb for a backend
    // to implement instead — so declaring hasRadioSideDsp bought nothing and
    // the control moved while the radio never heard about it.
    //
    // Enable and level travel TOGETHER because a radio that has a level
    // register generally needs both to make either meaningful, and because the
    // two arriving separately is how a toggle lands before the level it implies.
    void noiseReductionCommandIssued(bool on, int level);
    void noiseBlankerCommandIssued(bool on, int level);
    void autoNotchCommandIssued(bool on);
    // Enable and position together — see IRadioBackend::setSliceManualNotch
    // for why turning the notch on without placing it is not enough.
    void manualNotchCommandIssued(bool on, int position);
    void squelchCommandIssued(bool on, int level);
    // Receive and transmit incremental tuning.
    void ritCommandIssued(bool on, int hz);
    void xitCommandIssued(bool on, int hz);
    // Operator-issued per-slice AUDIO changes, same discipline as the three
    // above: audioMuteChanged/audioGainChanged/audioPanChanged also fire when
    // radio status is applied, so driving a command off those would echo the
    // radio's own state back as a request (Principle II).
    //
    // These exist because a Flex mixes its slices ON THE RADIO and these
    // controls are wire commands to it, while a host-mixing backend (HL2)
    // demodulates every receiver here and has to apply them in its own mixer.
    // Without them the operator's mute moved the model and the fader, and the
    // audio kept playing.
    void audioMuteCommandIssued(bool mute);
    void audioGainCommandIssued(int gainPercent);
    void audioPanCommandIssued(int panPercent);      // 0=left, 50=centre, 100=right
    // Operator asked for THIS slice to own transmit. A radio with one
    // transmitter and several receivers has to move it rather than set a flag.
    void txSliceCommandIssued();
    // Operator selected THIS slice as the one the shared controls act on.
    // Separate from activeChanged, which also fires when radio status is
    // applied — driving a command off that would echo the radio's own state
    // back as a request (Principle II).
    void activeSliceCommandIssued();
    void panIdChanged(const QString& panId);
    void modeChanged(const QString& mode);
    void filterChanged(int low, int high);
    void adaptiveFilterEnabledChanged(bool on);
    void adaptiveMinLowCutChanged(int hz);
    void adaptiveMaxHighCutChanged(int hz);
    void adaptiveMinSnrChanged(int level);
    void adaptiveResponseChanged(int level);
    void adaptiveSplatterChanged(int level);
    void adaptiveHetRejectChanged(bool on);
    void adaptiveActiveChanged(bool on);
    void activeChanged(bool active);
    void txSliceChanged(bool tx);
    void audioGainChanged(float gain);
    void audioPanChanged(int pan);
    void rxAntennaChanged(const QString& ant);
    void txAntennaChanged(const QString& ant);
    void rxAntennaListChanged(const QStringList& ants);
    void txAntennaListChanged(const QStringList& ants);
    void lockedChanged(bool locked);
    void tuneBlockedByLock();
    void lockedFeedbackActiveChanged(bool active);
    void qskChanged(bool on);
    void nbChanged(bool on);
    void nrChanged(bool on);
    void anfChanged(bool on);
    void nrlChanged(bool on);
    void nrsChanged(bool on);
    void rnnChanged(bool on);
    void nrfChanged(bool on);
    void anflChanged(bool on);
    void anftChanged(bool on);
    void mnChanged(bool on);
    void apfChanged(bool on);
    void apfLevelChanged(int v);
    void nbLevelChanged(int v);
    void nrLevelChanged(int v);
    void anfLevelChanged(int v);
    void nrlLevelChanged(int v);
    void nrsLevelChanged(int v);
    void nrfLevelChanged(int v);
    void anflLevelChanged(int v);
    void mnLevelChanged(int v);
    void agcModeChanged(const QString& mode);
    void agcThresholdChanged(int value);
    void agcOffLevelChanged(int value);
    void audioMuteChanged(bool mute);
    void diversityChanged(bool on);
    void escEnabledChanged(bool on);
    void escGainChanged(float gain);
    void escPhaseShiftChanged(float deg);
    void rfGainChanged(float gain);
    void externalReceiveAgcModeChanged(const QString& mode);
    void externalReceiveAgcThresholdChanged(int value);
    void externalReceiveAgcOffLevelChanged(int value);
    void externalReceiveAutoSquelchChanged(bool on);
    void squelchChanged(bool on, int level);
    void externalReceiveSquelchChanged(bool on, int level);
    void stepChanged(int hz, const QVector<int>& stepList);
    void ritChanged(bool on, int hz);
    void xitChanged(bool on, int hz);
    void daxChannelChanged(int ch);
    void rttyMarkChanged(int hz);
    void rttyShiftChanged(int hz);
    void diglOffsetChanged(int hz);
    void diguOffsetChanged(int hz);

    // FM duplex/repeater signals
    void fmToneModeChanged(const QString& mode);
    void fmToneValueChanged(const QString& value);
    void repeaterOffsetDirChanged(const QString& dir);
    void fmRepeaterOffsetFreqChanged(double mhz);
    void txOffsetFreqChanged(double mhz);
    void fmDeviationChanged(int hz);

    void modeListChanged(const QStringList& modes);
    void recordOnChanged(bool on);
    void playOnChanged(bool on);
    void playEnabledChanged(bool enabled);
    void commandReady(const QString& cmd);  // ready to send to radio
    // aetherd RFC 2.3 encode template: express intent instead of building the
    // wire string. RadioModel routes this to FlexBackend::setSliceMode, whose
    // output goes through the TX-inhibit-guarded slice sink. (The other slice
    // commands still use commandReady until they convert.)
    void modeChangeRequested(const QString& mode);
    void digitalVoiceSliceDisplaced(int sliceId, const QString& previousMode);

public:
    // Filter polarity families (#3434): the single mode→family mapping every
    // polarity decision uses. Public/static so capture paths (memories) can
    // mirror back to the wire form without duplicating the mode list.
    static bool filterPolarityUsbFamily(const QString& mode);
    static bool filterPolarityLsbFamily(const QString& mode);
    // Modes whose passband must straddle the carrier (AM/SAM/DSB/DRM/FM...).
    static bool filterCarrierStraddlingFamily(const QString& mode);

private:
    // Sign-guarded, idempotent (lo,hi)→(-hi,-lo) mirror of the stored filter
    // when its polarity is wrong for m_mode; true if it changed anything.
    bool normalizeFilterPolarity();

    int     m_id{0};
    QString m_letter;          // per-client display letter from `index_letter`
    QString m_panId;           // panadapter assignment (e.g. "0x40000000")
    double  m_frequency{0.0};
    QString m_mode{"USB"};
    QString m_modeBeforeDigitalVoice;
    QStringList m_modeList;
    int     m_filterLow{-1500};
    int     m_filterHigh{1500};
    quint64 m_userFilterEpoch{0};   // bumped only by setFilterWidth() (RFC #3878)
    // Adaptive RX filter — client-side config + runtime state (RFC #3878).
    // The filter edges themselves stay radio-authoritative (never persisted);
    // only enabled + the two bounds are persisted by the GUI.
    bool    m_adaptiveFilterEnabled{false};
    int     m_adaptiveMinLowCut{0};      // Hz, one of {0,50,100,200}
    int     m_adaptiveMaxHighCut{4000};  // Hz, one of {3000,3500,4000,6000}
    int     m_adaptiveMinSnr{1};         // 0=Sensitive,1=Normal,2=Strong
    int     m_adaptiveResponse{1};       // 0=Fast,1=Normal,2=Slow
    int     m_adaptiveSplatter{1};       // 0=Tight,1=Normal,2=Wide
    bool    m_adaptiveHetReject{false};  // opt-in edge-het cut
    bool    m_adaptiveActive{false};     // a confident live fit is applied
    bool    m_active{false};
    bool    m_txSlice{false};
    float   m_rfGain{0.0f};
    float   m_audioGain{50.0f};
    int     m_audioPan{50};

    // Slice control state
    QString m_rxAntenna{"ANT1"};
    QString m_txAntenna{"ANT1"};
    QStringList m_rxAntennaList;
    QStringList m_txAntennaList;
    bool    m_locked{false};
    bool    m_qsk{false};
    bool    m_audioMute{false};
    bool    m_externalReceiveAudioReplacement{false};
    bool    m_externalReceiveFlexAudioSuppressed{false};
    bool    m_externalReceiveAudioMute{false};
    float   m_externalReceiveAudioGain{70.0f};
    int     m_externalReceiveAudioPan{50};
    QString m_externalReceiveAgcMode{"med"};
    int     m_externalReceiveAgcThreshold{-100};
    int     m_externalReceiveAgcOffLevel{50};
    bool    m_externalReceiveAutoSquelch{false};
    bool    m_externalReceiveSquelchOn{false};
    // KiwiSDR does not report an initial squelch threshold. Start at the
    // lowest manual UI level so first-enable cannot unexpectedly close audio.
    int     m_externalReceiveSquelchLevel{0};
    bool    m_diversity{false};
    bool    m_diversityChild{false};
    bool    m_diversityParent{false};
    int     m_diversityIndex{-1};
    bool    m_escEnabled{false};
    float   m_escGain{1.0f};
    float   m_escPhaseShift{0.0f};
    bool    m_nb{false};
    bool    m_nr{false};
    bool    m_anf{false};
    bool    m_nrl{false};
    bool    m_nrs{false};
    bool    m_rnn{false};
    bool    m_nrf{false};
    bool    m_anfl{false};
    bool    m_anft{false};
    bool    m_mn{false};
    bool    m_apf{false};
    int     m_apfLevel{50};
    int     m_nbLevel{50};
    int     m_nrLevel{50};
    int     m_anfLevel{50};
    int     m_nrlLevel{50};
    int     m_nrsLevel{50};
    int     m_nrfLevel{50};
    int     m_anflLevel{50};
    // Mid-passband, so a notch enabled before the slider is touched lands
    // somewhere the operator can see and drag, not at an edge.
    int     m_mnLevel{50};
    QString m_agcMode{"med"};
    int     m_agcThreshold{65};
    int     m_agcOffLevel{10};
    bool    m_squelchOn{false};
    int     m_squelchLevel{20};
    int     m_manualSquelchLevel{20};
    bool    m_squelchEchoIsManual{true};
    int     m_stepHz{100};
    QVector<int> m_stepList;
    bool    m_ritOn{false};
    int     m_ritFreq{0};
    bool    m_xitOn{false};
    int     m_xitFreq{0};
    int     m_daxChannel{0};
    int     m_rttyMark{2125};
    int     m_rttyMarkDefault{2125};
    bool    m_rttyMarkUserOverride{false};
    // Flex firmware's `profile global` snapshot does not persist
    // speex_nr_level — on recall the radio reports the firmware default of
    // 50, even when the user set a different value before saving. Cache the
    // user's explicit choice so applyChanges() can re-push it when the radio
    // comes back at 50 with no user-initiated change to that value.
    int     m_nrsLevelUser{50};
    bool    m_nrsLevelUserOverride{false};
    int     m_rttyShift{170};
    int     m_diglOffset{2210};
    int     m_diguOffset{1500};

    // FM duplex/repeater state
    QString m_fmToneMode{"off"};
    QString m_fmToneValue{"100.0"};
    QString m_repeaterOffsetDir{"simplex"};
    double  m_fmRepeaterOffsetFreq{0.0};
    double  m_txOffsetFreq{0.0};
    int     m_fmDeviation{5000};

    // Record/playback
    bool    m_recordOn{false};
    bool    m_playOn{false};
    bool    m_playEnabled{false};

    // Centralized LOCKED feedback gate — single source of truth for
    // widgets rendering the "LOCKED" overlay after a blocked tune (#2983).
    QTimer  m_lockedFeedbackTimer;
    bool    m_lockedFeedbackActive{false};

    void setLockedFeedbackActive(bool on);

    void sendCommand(const QString& cmd);

    QStringList m_pendingCommands;
};

} // namespace AetherSDR
