#pragma once
#ifdef HAVE_WEBSOCKETS

#include "TciProtocol.h"
#include "TciRoutingState.h"
#include "TciTrxMap.h"
#include "IcomTciUnkeySettle.h"

#include <QObject>
#include <QPointer>
#include <QElapsedTimer>
#include <QHash>
#include <QJsonObject>
#include <QList>
#include <QMap>
#include <QSet>
#include <QString>
#include <QVector>
#include <functional>
#include <memory>
#include <optional>

class QWebSocketServer;
class QWebSocket;
class QTimer;

namespace AetherSDR {

class RadioModel;
class AudioEngine;
class SliceModel;
class Resampler;

// Read-only snapshot of one connected TCI client, surfaced to the Radio
// Setup → TCI tab. TCI has no client-identity handshake, so a client is
// only ever known by its network endpoint plus the stream subscriptions
// it has requested.
struct TciClientInfo {
    QString peerAddress;
    quint16 peerPort{0};
    bool    audio{false};
    int     audioReceiver{-1};   // -1 = all receivers
    bool    iq{false};
    bool    rxSensors{false};
    bool    txSensors{false};
};

// TCI WebSocket server — exposes radio state and audio over the TCI protocol.
// Phase 1: text commands (VFO, mode, filter, TX, RIT/XIT, CW, spots)
// Phase 2: binary RX/TX audio streaming
class TciServer : public QObject {
    Q_OBJECT
    friend class TciServerReviewTest;
    friend class Hl2TciSignalingTest;

public:
    explicit TciServer(RadioModel* model, QObject* parent = nullptr);
    ~TciServer() override;

    bool start(quint16 port = 50001);
    void stop();

    bool isRunning() const;
    quint16 port() const;
    int clientCount() const { return m_clients.size(); }

    // Automation-only diagnostics. This never changes protocol or radio state;
    // it projects the routing state machine and deferred work into JSON for the
    // local automation bridge's `tci routes` action.
    QJsonObject routingSnapshot() const;

    // (ownsDaxChannel() and the cross-consumer peeking it existed for were
    // replaced by per-consumer holds in PanadapterStream's centralized DAX
    // channel manager — see acquireDaxChannel/releaseDaxChannel. #3305)

    // Snapshot of all currently connected clients (endpoint + subscriptions).
    // Cheap to call; intended for the Radio Setup → TCI tab on demand and
    // whenever clientsChanged() fires.
    QVector<TciClientInfo> connectedClients() const;

    void setAudioEngine(AudioEngine* audio) { m_audio = audio; }

    // Broadcast a master-volume change to all connected TCI clients. Called
    // by MainWindow whenever the GUI master volume slider moves so remote
    // controllers (e.g. aether_pad) stay in sync. Idempotent — clients
    // re-applying the value they just sent is harmless.
    void broadcastMasterVolume(int pct);

    // TCI TX gain (0.0–1.0). Applied to outbound TX audio from WSJT-X/JTDX
    // before the radio.  Decoupled from DaxTxGain (#1627) — the DAX bridge
    // and TCI maintain independent gain settings.  Persists to TciTxGain.
    void setTxGain(float gain);
    float txGain() const { return m_txGain; }

    // TCI TX overflow handling.  After gain, samples whose magnitude
    // exceeds full-scale (±1.0) are handled per this mode:
    //   Clip     — saturating clamp to ±1.0 (legacy default, defensive)
    //   NaNGuard — pass-through; only zero NaN/Inf (preserves bit-exactness
    //              for legitimate digital-mode tones at the cost of letting
    //              malformed >1.0 clients through)
    //   Measure  — pure bypass; count clip events but never mutate samples
    // Persists to TciTxOverflowMode (0/1/2).
    enum class OverflowMode : int { Clip = 0, NaNGuard = 1, Measure = 2 };
    void setOverflowMode(int mode);
    int overflowMode() const { return static_cast<int>(m_overflowMode); }

    // Per-channel TCI RX gain (0.0–1.0), applied to outbound DAX audio before
    // resampling and sending to TCI clients.  Decoupled from DaxRxGain<n> so
    // DAX bridge and TCI maintain independent per-channel gains.
    // Channel is 1-based (1–4).  Persists to TciRxGain<channel>.
    void setRxChannelGain(int channel, float gain);
    float rxChannelGain(int channel) const;

    // Wire slice signals for state change broadcasts
    void wireSlice(int trx, SliceModel* slice);
    void wireSpotModel();
    void notifySpotClicked(int spotIndex, SliceModel* slice = nullptr);
    void rearmDaxForProfileLoad();

public slots:
    // RX audio from DAX pipeline (float32 stereo, 24 kHz)
    void onDaxAudioReady(int channel, const QByteArray& pcm);
    // IQ data from DAX IQ stream (big-endian float32 I/Q pairs)
    void onIqDataReady(int channel, const QByteArray& rawPayload, int sampleRate);
    // Waterfall row from PanadapterStream — forwarded to spectrum_event subscribers
    void onWaterfallRowReady(quint32 streamId, const QVector<float>& binsDbm,
                             double lowMhz, double highMhz,
                             quint32 timecode, qint64 emittedNs);
    // A DAX channel's radio-side stream went away — drop its channel→TRX routing
    // cache entry so a re-registration re-resolves cleanly (#3669/#3766). Bound
    // to PanadapterStream::daxStreamUnregistered via the MainWindow stream-sink
    // helper so it survives a backend/family swap (#4448 F6).
    void onDaxStreamUnregistered(int channel, quint32 streamId);

signals:
    void clientCountChanged(int count);
    // Fired whenever the client list or any client's subscriptions change
    // (connect, disconnect, audio start/stop). The TCI tab repopulates on
    // this signal.
    void clientsChanged();
    // Raw TCI text traffic for the embedded monitor. direction is
    // "rx" (received from a client) or "tx" (broadcast to clients).
    // One emission per logical message. Binary audio/IQ frames are
    // never emitted; high-rate text broadcasts like rx_smeter ARE
    // emitted but can be muted per-command via the monitor's
    // suppression list to keep the stream readable.
    void tciMessage(const QString& direction, const QString& text);
    void rxLevel(int channel, float rms);  // 1-based channel, RMS of TCI-gained RX audio
    void txLevel(float rms);                // RMS of post-gain TCI TX audio
    // Emitted when a TCI client sends `volume:N;` (master volume SET).
    // MainWindow handles it by calling the same path as the title bar
    // master volume slider — m_audio->setRxVolume() (or lineout when PC
    // audio is off) plus persistence to AppSettings.
    void masterVolumeRequested(int pct);

private slots:
    void onNewConnection();
    void onClientDisconnected();
    void onTextMessage(const QString& msg);
    void onBinaryMessage(const QByteArray& data);
    void broadcastStatus();

private:
    struct ClientState;

    // Rate-limited drive:/tune_drive: relay (#4161). queue* is the signal
    // entry point; broadcast* does the de-duped send.
    void queuePowerBroadcast();
    void broadcastPower();

    void sendInitBurst(QWebSocket* client);
    // Diagnostic: log + send a text reply to one client (per-command echoes
    // bypass the central dispatch log, so route them here for visibility).
    void replyText(QWebSocket* ws, const QString& msg);
    void broadcastSpotClicked(const QString& callsign, long long frequencyHz,
                              int trx, int channel);
    void broadcastSliceFrequencies(SliceModel* slice);
    void publishActiveTrx();
    SliceModel* sliceForPanId(const QString& panId) const;
    void broadcast(const QString& msg);
    void broadcastBinary(const QByteArray& data);
    SliceModel* sliceForTrx(int trx) const;
    // No first-slice fallback — for paths that key the radio (#4547).
    SliceModel* sliceForTrxStrict(int trx) const;
    // The receiver a client is actually operating: its declared audio_start
    // receiver when it has one, else the trx it put on the wire (#4547).
    int effectiveTrx(QWebSocket* client, int requestedTrx) const;
    QVector<TciSliceEndpoint> routingEndpoints() const;
    // Diagnostics helpers for the PTT routing decision log.
    static const char* txRouteOwnerName(TciRoutingState::TxRouteOwner owner);
    // "<sliceId>(trx<n>)", "<sliceId>(gone)" for a slice that is no longer
    // live, "none" for a negative id. The wire speaks trx and the router
    // speaks slice ids; the log has to state both or it cannot be read
    // against a client transcript.
    QString sliceTag(int sliceId) const;
    void handleVfoRequest(QWebSocket* client, const TciProtocol::VfoRequest& request);
    void handleSplitRequest(QWebSocket* client, const TciProtocol::SplitRequest& request);
    void handleTrxRequest(QWebSocket* client, const TciProtocol::TrxRequest& request);
    void tuneSliceAndConfirm(
        QWebSocket* client, int trx, int channel, int sliceId, long long frequencyHz);
    void promoteTxSliceAndContinue(int sliceId, std::function<void(bool)> continuation);
    void createTxSliceForVfoB(QWebSocket* client,
        const TciProtocol::VfoRequest& request,
        SliceModel* rxSlice,
        const QString& routeConfirmation = {},
        bool splitOnly = false);
    void reportVfoBRouteFailure(QWebSocket* client,
        const TciProtocol::VfoRequest& request,
        const QString& reason,
        bool rejectSplit);
    // True when the connected backend runs the modulator/demodulator in this
    // process (HL2) instead of inside the radio — hence has no DAX data plane.
    bool hostModulatingBackend() const;
    void prepareTxAudio();
    void startTxChrono(QWebSocket* client, int trx);
    void notePttRequest(const TciProtocol::TrxRequest& request);
    void notePttOutcome(const QString& outcome);
    void beginIcomUnkeySettle();
    void finishIcomUnkeySettle(quint64 generation);
    void stopTxChrono();
    void requestTciPttOff();
    void abortTciPtt();
    quint64 beginRouteTransition();
    void finishRouteTransition(quint64 generation);
    void drainDeferredRoutingAndPtt();
    void onRadioTransmittingChanged(bool transmitting);
    void onRadioTransmitConfirmed(bool transmitting);
    void broadcastActualTxState(bool transmitting);
    void teardownTciRoute();
    void sendTxChronoFrame(QWebSocket* client);
    void logTxAudioSummary(const char* reason);
    ClientState* clientStateFor(QWebSocket* socket);
    void noteClientTextTx(QWebSocket* socket, const QString& message);
    void sendClientText(QWebSocket* socket, const QString& message);
    void noteClientSocketError(QWebSocket* socket, int error);
    QJsonObject disconnectSnapshot(const ClientState& client,
                                   const QWebSocket* socket) const;

    // Build a TCI binary audio frame (64-byte header + float32 samples)
    static QByteArray buildAudioFrame(int receiver, int type,
                                      int sampleRate, int channels,
                                      const float* samples, int sampleCount);

    struct ClientState {
        QWebSocket*  socket{nullptr};
        TciProtocol* protocol{nullptr};
        bool         audioEnabled{false};   // client sent AUDIO_START
        int          audioReceiver{-1};     // -1 = all receivers, otherwise TCI TRX
        int          audioSampleRate{48000}; // requested output rate (48kHz for WSJT-X compat)
        int          audioChannels{2};       // 1=mono, 2=stereo
        int          audioFormat{3};         // 0=int16, 3=float32
        // Per-DAX-channel resamplers.  A single shared r8brain instance would
        // carry filter state from slice A into slice B, causing audible
        // crosstalk (#1806).  Each channel gets its own stateful instance,
        // lazily created in onDaxAudioReady() and deleted/recreated whenever
        // the client changes its audio_samplerate.  No entry (or nullptr) for
        // a channel means 24 kHz pass-through (no resampling needed).
        QHash<int, Resampler*> resamplers;
        // Per-DAX-channel accumulation buffers. Concatenating multi-channel
        // packets into a shared buffer would interleave audio from different
        // slices and destroy the resampler output, so each channel maintains
        // its own staging area. QHash over QMap: channel count is tiny (1-4)
        // and we never iterate in key order.
        QHash<int, QByteArray> rxAccumBuf;
        bool         rxSensorsEnabled{false};
        bool         txSensorsEnabled{false};
        bool         iqEnabled{false};       // client sent IQ_START
        int          iqChannel{0};           // TCI TRX → DAX IQ channel (0-based)
        bool         spectrumEnabled{false}; // client sent spectrum_event:on;
        // Payload-free lifecycle telemetry retained across disconnect. Command
        // names are stored without their arguments, so diagnostics can identify
        // the failing TCI layer without retaining frequencies or operator text.
        qint64       connectedAtMs{-1};
        qint64       lastTextRxAtMs{-1};
        qint64       lastTextTxAtMs{-1};
        qint64       lastSocketErrorAtMs{-1};
        QString      lastRxCommand;
        QString      lastTxCommand;
        int          lastSocketError{-1};
        QString      lastSocketErrorString;
    };

    // Minimum frames to accumulate before flushing to r8brain.
    // ~21ms at 24kHz — large enough for clean resampling, small enough
    // for acceptable latency in digital modes.
    static constexpr int kAccumMinFrames = 512;

    void ensureDaxForTci();
    void releaseDaxForTci();
    void scheduleDaxRelease();   // debounced releaseDaxForTci — cancel on reconnect
    void cancelDaxRelease();

    QPointer<RadioModel> m_model;  // QPointer auto-clears when RadioModel is destroyed (#2385)
    AudioEngine*      m_audio{nullptr};
    QWebSocketServer* m_server{nullptr};
    QList<ClientState> m_clients;
    QSet<int>         m_tciDaxSlices;   // slice IDs where we auto-assigned DAX (#1331)
    int               m_activeTrx{-1};  // TRX holding GUI focus; -1 = not yet observed (#4160)
    // The focused slice by identity. trx is positional and shifts when an
    // earlier slice is removed, so the pointer is what survives renumbering;
    // QPointer clears if the slice is destroyed (#4160).
    QPointer<SliceModel> m_activeSlice;
    QString           m_activeLetter;   // focused slice's display letter (#4160)
    QMap<int, int>     m_channelTrx;            // DAX channel → last-resolved TCI TRX (routing cache, #3669)
    QHash<QString, long long> m_lastDdsCenterHz; // panId → last broadcast dds center, gates zoom-only re-emits (#3910)
    TciRoutingState m_routingState;
    // #4567: stable sliceId→trx receiver bindings. Acquired on sliceAdded,
    // released 500 ms after a genuine slice close (recreates reclaim their
    // number), cleared on disconnect. Injected into every TciProtocol.
    TciTrxMap m_trxMap;
    struct PendingVfoBCreate
    {
        QPointer<QWebSocket> client;
        TciProtocol::VfoRequest request;
        int rxSliceId { -1 };
        QString routeConfirmation;
        bool splitOnly { false };
        quint64 transitionGeneration { 0 };
    };
    std::optional<PendingVfoBCreate> m_pendingVfoBCreate;
    struct PendingTrxRequest
    {
        QPointer<QWebSocket> client;
        TciProtocol::TrxRequest request;
    };
    std::optional<PendingTrxRequest> m_pendingTrxRequest;
    struct PendingRouteCommand
    {
        enum class Kind {
            Vfo,
            Split,
        };
        Kind kind { Kind::Vfo };
        QPointer<QWebSocket> client;
        TciProtocol::VfoRequest vfo;
        TciProtocol::SplitRequest split;
    };
    QList<PendingRouteCommand> m_pendingRouteCommands;
    bool m_routeTransitionInFlight { false };
    quint64 m_routeTransitionGeneration { 0 };
    QString m_lastRouteError;
    QTimer*           m_meterTimer{nullptr};  // 200ms status broadcast
    QTimer*           m_daxReleaseTimer{nullptr}; // debounced DAX RX teardown
    // Rate limiter for drive:/tune_drive: (#4161). A power-slider drag steps
    // the value ~40 times a second and each step is a separate radio command,
    // so relaying every one floods remote clients. Leading edge is sent
    // immediately (a client's own SET still echoes promptly); further changes
    // inside the window collapse to one trailing send of the latest value.
    QTimer*           m_powerRateTimer{nullptr};
    bool              m_drivePending{false};      // rfPowerChanged since last flush
    bool              m_tuneDrivePending{false};  // tunePowerChanged since last flush
    int               m_lastDriveSent{-1};
    int               m_lastTuneDriveSent{-1};
    // Last resolved TX-slice trx, used to label drive:/tune_drive: when a
    // band-change slice recreation momentarily leaves no slice marked TX.
    int               m_lastTxTrx{0};
    QTimer*           m_txChronoTimer{nullptr}; // TX_CHRONO frame cadence
    QWebSocket*       m_txChronoClient{nullptr};
    QPointer<QWebSocket> m_tciPttClient;
    int m_tciPttTrx { 0 };
    bool m_tciPttWantsAudio { false };
    bool m_tciPttRequestedOn { false };
    bool m_tciPttConfirmedOn { false };
    bool m_tciPttCancelPending { false };
    quint64 m_tciPttGeneration { 0 };
    // Icom publishes an optimistic local unkey edge before CI-V readback. Hold
    // the TCI presentation in a short bounded settle window, but complete it
    // only from the backend's accepted CI-V PTT-off confirmation. No reply
    // retains ownership and republishes keyed at expiry.
    bool m_tciPttUnkeyReported { false };
    IcomTciUnkeySettle m_icomUnkeySettle;
    quint64 m_tciPttUnkeySettleCount { 0 };
    quint64 m_tciPttSuppressedRekeyCount { 0 };
    quint64 m_tciPttUnkeySettleTimeoutCount { 0 };
    // Payload-free TCI ingress/confirmation telemetry. These counters make
    // `tci routes` answer whether a WSJT-X key request reached this process,
    // survived routing/preflight, and was confirmed by radio-authoritative
    // state without enabling the full TCI wire trace.
    QElapsedTimer m_tciPttTelemetryClock;
    quint64 m_tciPttRequestCount { 0 };
    quint64 m_tciPttOnRequestCount { 0 };
    quint64 m_tciPttOffRequestCount { 0 };
    quint64 m_tciPttAcceptedOnCount { 0 };
    quint64 m_tciPttConfirmedOnCount { 0 };
    quint64 m_tciPttConfirmationTimeoutCount { 0 };
    qint64 m_tciPttLastRequestAtMs { -1 };
    qint64 m_tciPttLastAcceptedAtMs { -1 };
    qint64 m_tciPttLastConfirmedAtMs { -1 };
    qint64 m_tciPttLastOutcomeAtMs { -1 };
    bool m_tciPttLastRequestedOn { false };
    QString m_tciPttLastOutcome { QStringLiteral("none") };
    QJsonObject m_lastDisconnect;
    qint64 m_lastDisconnectAtMs { -1 };
    bool m_txAudioPrepared { false };
    int               m_txChronoTrx{0};
    std::unique_ptr<Resampler> m_txResampler; // 48kHz→24kHz TX downsampler
    QElapsedTimer     m_txChronoClock;
    QElapsedTimer     m_txChronoSessionClock;
    qint64            m_txChronoAccumNs{0};
    qint64            m_txChronoRequestedFrames{0};
    bool              m_txUseRadioRoute{true};
    float             m_txGain{1.0f};
    OverflowMode      m_overflowMode{OverflowMode::Clip};
    float             m_rxChannelGain[8]{1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f};
    qint64            m_txAudioBlocks{0};
    qint64            m_txInputFrames{0};
    qint64            m_txOutputFrames{0};
    qint64            m_txClipSamples{0};
    qint64            m_txAudioSampleCount{0};
    double            m_txAudioSumSq{0.0};
    float             m_txAudioPeak{0.0f};
    bool              m_txSawDuplicatedStereo{false};
    QElapsedTimer     m_rxAudioLogTimer;
    qint64            m_rxAudioPackets{0};
    qint64            m_rxAudioFramesSent{0};
    bool m_lastRadioTx { false };
    float             m_cachedSLevel[8]{-130,-130,-130,-130,-130,-130,-130,-130};
    float             m_cachedFwdPower{0};
    float             m_cachedSwr{1.0f};
    float             m_cachedMicLevel{-50.0f};
    float             m_cachedAlc{0.0f};       // SW-ALC peak, dBFS
};

} // namespace AetherSDR

#endif // HAVE_WEBSOCKETS
