#pragma once

#include <QByteArray>
#include <QObject>
#include <QString>
#include <QStringList>
#include <QVariant>
#include <QVariantMap>

#include "core/backends/AmpDelta.h"
#include "core/backends/GpsDelta.h"
#include "core/backends/MemoryDelta.h"
#include "core/backends/MeterDef.h"
#include "core/backends/ProfileDelta.h"
#include "core/backends/RadioCapabilities.h"
#include "core/backends/RadioDelta.h"
#include "core/backends/SliceDelta.h"
#include "core/backends/TransmitDelta.h"
#include "core/backends/TunerDelta.h"

namespace AetherSDR {

// Neutral, family-agnostic connect descriptor. Core fields cover the common
// case; vendor-specific parameters (SmartLink token, Kiwi endpoint path, …)
// ride in `params` so the interface never grows a per-vendor connect signature.
struct RadioConnectRequest {
    QString host;
    quint16 port = 0;
    QString serial;         // when a family identifies radios by serial
    QVariantMap params;     // family-specific extras (namespaced by the backend)
};

// The radio-facing seam of the engine (aetherd RFC §5.5). Everything that
// speaks a vendor wire protocol lives *behind* this interface, inside
// libaethercore; RadioModel and the (future) protocol see only this. The
// SmartSDR stack becomes the first implementor (FlexBackend, step 2.2) with
// ZERO behavior change; other radio families are added as further implementors
// without touching any client.
//
// Design decisions (RFC §5.5 open questions, resolved 2026-07-05):
//   Q2 — RadioModel keeps owning its sub-models (SliceModel, MeterModel, …);
//        the backend DRIVES them by emitting the signals below, which
//        RadioModel connects to. The backend does not own UI-facing model
//        objects. (Minimal churn; the models already communicate via signals.)
//   Q3 — ONE interface, not an RX-only/TX-capable type split. A receive-only
//        family reports capabilities().canTransmit == false and implements
//        setKeying() as a guarded no-op; the engine TX guard (RFC §6, above
//        this seam) denies keying when canTransmit is false.
//
// DSP location is invisible here (RFC §5.5): a backend whose hardware
// demodulates and computes FFTs is a thin protocol decoder; one that ships raw
// samples owns an engine-side DSP chain — either way it emits the same
// normalized signals, so no consumer can tell the difference.
//
// This is the CORE seed. It carries the lifecycle, capability, canonical-verb,
// and extension surface; it grows one method at a time as the touchpoint
// burndown (docs/architecture/aetherd-touchpoints.md) converts each gui→engine
// touchpoint into a protocol/backend verb. Do NOT dump all 140 touchpoints
// here at once.
class IRadioBackend : public QObject {
    Q_OBJECT

public:
    explicit IRadioBackend(QObject* parent = nullptr) : QObject(parent) {}
    ~IRadioBackend() override = default;

    // ---- identity & capability (feeds the protocol `welcome`, §4.1) ----
    virtual RadioCapabilities capabilities() const = 0;

    // ---- connection lifecycle ----
    virtual void connectRadio(const RadioConnectRequest& request) = 0;
    virtual void disconnectRadio() = 0;
    virtual bool isConnected() const = 0;

    // ---- intents DOWN: canonical core-profile verbs (grow per burndown) ----
    // The backend translates each to its vendor wire protocol.
    virtual void setSliceFrequency(int sliceId, double hz) = 0;
    virtual void setSliceMode(int sliceId, const QString& mode) = 0;
    virtual void setSliceFilter(int sliceId, int lowHz, int highHz) = 0;

    // TX keying intent. The decision to allow keying is made ABOVE this seam by
    // the engine guard (RFC §6, single-holder lock + capability check); the
    // backend only translates an already-authorized intent to its mechanism
    // (command verb, in-stream bit, hardware line). A backend whose
    // capabilities().canTransmit is false implements this as a no-op.
    virtual void setKeying(bool key) = 0;

    // ---- vendor extensions (namespaced, capability-advertised) ----
    // Vendor-specific verbs that are NOT part of the core profile. Clients
    // discover available namespaces via capabilities().extensionNamespaces.
    //
    // Fire-and-forget like every other DOWN verb: the result of a real device
    // command (ATU tune, amp state, …) arrives later on the wire, so it comes
    // back asynchronously via extensionResult(requestId, …) / extensionError.
    // The caller (above the seam) mints requestId and correlates the reply; a
    // requestId of 0 means "no reply expected". A synchronous QVariant return
    // would have to block or fabricate a local value against an async backend.
    virtual void invokeExtension(const QString& ns, const QString& verb,
                                 quint64 requestId, const QVariant& arg = {}) = 0;

signals:
    // ---- connection state UP ----
    void connected();
    void disconnected();
    void connectionError(const QString& reason);
    void capabilitiesChanged();

    // ---- vendor-extension replies UP (correlate to invokeExtension) ----
    // The async result of an invokeExtension() call, keyed by the caller's
    // requestId. A backend that completes locally may emit this synchronously;
    // one speaking a wire protocol emits it when the device answers.
    void extensionResult(quint64 requestId, const QVariant& result);
    void extensionError(quint64 requestId, const QString& reason);

    // ---- normalized model state UP (RadioModel connects these to its
    //      sub-models; Q2) — the key/value shape mirrors the models' fields ----
    // Normalized slice-status delta (aetherd RFC 2.3). Typed + compiler-checked:
    // the backend populates only the fields the wire reported, the model applies
    // exactly those. Replaces the prior stringly-keyed QVariantMap payload.
    void sliceChanged(int sliceId, const SliceDelta& delta);
    void sliceRemoved(int sliceId);
    void meterUpdate(const QString& meterId, double value);

    // Normalized transmit-status delta (aetherd RFC 2.3 — TransmitModel
    // touchpoint). Typed + compiler-checked; the backend populates only the
    // fields the wire reported (across the transmit / interlock / ATU / APD /
    // APD-sampler status planes) and RadioModel drives the TransmitModel.
    void transmitChanged(const TransmitDelta& delta);

    // Normalized power-amplifier status delta (aetherd 2.4 — AmpModel decode
    // split, #4094). Typed + present-only; the backend translates the SmartSDR
    // "amplifier" wire and AmpModel applies the state machine. Command/encode is
    // the neutral AmpModel::operateRequested intent, translated back to the wire
    // by invokeExtension("flex", "amp.operate", …) (#4094).
    void amplifierChanged(const AmpDelta& delta);

    // Normalized antenna-tuner status delta (aetherd 2.4 — TunerModel decode
    // split, #4092). Typed + present-only; the backend translates the SmartSDR
    // "atu"/"amplifier"(TunerGeniusXL) wire, TunerModel applies the change-gated
    // state. Command/encode is TunerModel's neutral operate/bypass/autotune
    // intents, translated by invokeExtension("flex", "tuner.*", …) (#4092).
    void tunerChanged(const TunerDelta& delta);

    // Normalized radio-global status delta (aetherd RFC 2.3 — RadioModel
    // residual). Typed + compiler-checked; the backend populates only the fields
    // the wire reported and RadioModel applies them + its own orchestration.
    void radioChanged(const RadioDelta& delta);

    // Normalized GPS status delta (aetherd RFC 2.3 — RadioModel residual). The
    // backend tokenizes the vendor GPS status line into a present-only GpsDelta;
    // RadioModel applies it and emits gpsStatusChanged.
    void gpsChanged(const GpsDelta& delta);

    // Normalized memory-slot status (aetherd RFC 2.3 — RadioModel residual),
    // keyed by slot index. The backend decodes the vendor memory-status kv-set;
    // RadioModel applies it to MemoryEntry (text sanitisation is a model
    // concern) or drops the slot when delta.removed is set.
    void memoryChanged(const MemoryDelta& delta);

    // Normalized profile status (aetherd RFC 2.3 — RadioModel residual). The
    // backend parses the vendor "profile <type> …" status (list/current + the
    // database importing/exporting flags); RadioModel routes it to TransmitModel
    // tx/mic profiles, the global-profile list, or the import/export flags.
    void profileChanged(const ProfileDelta& delta);

    // Meter definition catalog (aetherd RFC 2.3 — MeterModel touchpoint). The
    // backend decodes the vendor meter-status wire format into a typed MeterDef;
    // RadioModel hands it straight to MeterModel::defineMeter(). Fields the wire
    // did not report keep their MeterDef defaults (present-only on the decode
    // side). The meter *values* stream on the data plane (VITA-49), separate.
    // (#4070: typed payload — replaces the prior stringly-keyed QVariantMap.)
    void meterDefined(const MeterDef& def);
    void meterRemoved(int index);
    // Panadapter core display state (universal — every family has a pan center
    // and span). The backend decodes it from vendor status; RadioModel drives
    // the PanadapterModel. panId is the pan's identifier (opaque to the model).
    // (aetherd RFC 2.3 — first converted touchpoint; the template the other
    // universal pan fields + the other mixed models follow.)
    void panCenterBandwidthChanged(const QString& panId,
                                   double centerMhz, double bandwidthMhz);

    // Panadapter display level range (universal — the Y-axis geometry that
    // pairs with center/bandwidth's X-axis). Unlike center/bandwidth, dBm is
    // signed, so the "unchanged" sentinel for an omitted field is NaN, not a
    // negative value — the backend carries NaN for whichever of min/max the
    // wire did not report. (aetherd RFC 2.3 — second converted universal pan
    // field, following the center/bandwidth template.)
    void panRangeChanged(const QString& panId, double minDbm, double maxDbm);

    // Panadapter RF gain (universal — every family has an RX gain control; the
    // range/step are family-specific and reported via RadioCapabilities). The
    // backend decodes it from vendor status; RadioModel drives the pan.
    void panRfGainChanged(const QString& panId, int gain);

    // Panadapter antenna selection (universal). Two signals because the wire may
    // report the selected RX antenna and the available list independently.
    void panRxAntennaChanged(const QString& panId, const QString& antenna);
    void panAntennaListChanged(const QString& panId, const QStringList& antennas);

    // Waterfall line duration in ms (universal display timing). Decoded from the
    // waterfall-status plane; RadioModel drives the pan's waterfall model state.
    void panWaterfallLineDurationChanged(const QString& panId, int ms);

    // Vendor-specific status data that is NOT part of the core profile — the
    // namespaced *extension* channel (aetherd RFC §5.5). A client that doesn't
    // understand `ns` ignores it; `kind` names the event within the namespace
    // and `fields` carries only the keys the wire actually reported. This is the
    // status counterpart to invokeExtension's request/reply.
    void extensionStatus(const QString& ns, const QString& kind,
                         const QVariantMap& fields);

    // ---- data plane UP (RFC §4.2) ----
    // Declared here so backends have a normalized outlet for spectrum/waterfall/
    // audio; the concrete zero-copy/binary frame formats are step-4 work. Until
    // then a backend may relay the existing in-tree frame types.
    void spectrumFrameReady(int panId, const QByteArray& frame);
    void waterfallRowReady(int panId, const QByteArray& row);
    void audioFrameReady(const QByteArray& pcm);
};

}  // namespace AetherSDR
