/**
 * @file Hub.cpp
 */

#include "Hub.h"
#include "Theme.h"

#include <QDateTime>
#include <algorithm>

using namespace StreamHubClient;

namespace shq {

Hub::Hub(QObject* parent) : QObject(parent) {
    connect(&ws_, &WsClient::connectedChanged, this, &Hub::onConnected);
    connect(&ws_, &WsClient::textReceived,     this, &Hub::onText);
    connect(&ws_, &WsClient::binaryReceived,   this, &Hub::onBinary);
}

void Hub::start(const QString& host, uint16_t port) {
    ws_.connectTo(host, port);
}

void Hub::reconnect(const QString& host, uint16_t port) {
    ws_.reconnectTo(host, port);
}

/* ── connection ──────────────────────────────────────────────────────────── */

void Hub::onConnected(bool connected) {
    if (connected) {
        sendGetSources();
        sendGetStats();
        sendHistoryInfo();
    }
    Q_EMIT connectedChanged(connected);
}

/* ── dispatch ────────────────────────────────────────────────────────────── */

void Hub::onText(const QString& jsonQ) {
    const std::string json = jsonQ.toStdString();
    const std::string type = ParseType(json);
    if      (type == "sources")          { onSources(json); }
    else if (type == "config")           { onConfig(json); }
    else if (type == "stats")            { onStats(json); }
    else if (type == "triggerState")     { onTriggerState(json); }
    else if (type == "zoom")             { onZoom(json); }
    else if (type == "historyZoom")      { onHistoryZoom(json); }
    else if (type == "historyInfo")      { onHistoryInfo(json); }
    else if (type == "maxPointsUpdated") { onMaxPointsUpdated(json); }
    /* pong: ignore */
}

void Hub::onBinary(const QByteArray& bytes) {
    if (bytes.isEmpty()) { return; }
    const uint8_t* data = reinterpret_cast<const uint8_t*>(bytes.constData());
    const size_t   len  = static_cast<size_t>(bytes.size());

    if (data[0] == 2u) {                /* trigger capture frame */
        CaptureFrame cf;
        if (ParseCaptureFrame(data, len, cf)) {
            capture_    = std::move(cf);
            hasCapture_ = true;
            trigger_.status      = "triggered";
            trigger_.trigTime    = capture_.trigTime;
            trigger_.hasTrigTime = true;
            Q_EMIT captureReceived();
        }
        return;
    }

    DataFrame frame;                    /* v1 live push */
    if (!ParseBinaryFrame(data, len, frame)) { return; }
    int srcIdx = findSource(frame.sourceId);
    if (srcIdx < 0) { return; }
    Source& src = sources_[srcIdx];
    for (const auto& fs : frame.signals) {
        int sigIdx = findSignal(src, fs.name);
        if (sigIdx < 0) { continue; }
        size_t n = std::min(fs.t.size(), fs.v.size());
        auto& buf = src.signals[sigIdx].buf;
        for (size_t i = 0; i < n; i++) { buf.push(fs.t[i], fs.v[i]); }
    }
}

/* ── JSON handlers (port of App.cpp) ─────────────────────────────────────── */

void Hub::onSources(const std::string& json) {
    std::vector<SourceInfo> infos;
    ParseSources(json, infos);
    for (const auto& info : infos) {
        int idx = findSource(info.id);
        if (idx < 0) {
            Source s;
            s.id = info.id; s.label = info.label; s.addr = info.addr;
            s.port = info.port; s.state = info.state;
            sources_.push_back(std::move(s));
        } else {
            sources_[idx].state = info.state;
            sources_[idx].label = info.label;
        }
    }
    sources_.erase(std::remove_if(sources_.begin(), sources_.end(),
        [&infos](const Source& s) {
            for (const auto& i : infos) { if (i.id == s.id) { return false; } }
            return true;
        }), sources_.end());
    /* Request config for any source not yet configured. */
    for (auto& s : sources_) {
        if (!s.configured) { sendGetConfig(s.id); }
    }
    Q_EMIT sourcesChanged();
}

void Hub::onConfig(const std::string& json) {
    std::string sourceId;
    int publishMode = 0;
    std::vector<SignalMeta> metas;
    if (!ParseConfig(json, sourceId, publishMode, metas)) { return; }
    int idx = findSource(sourceId);
    if (idx < 0) { return; }
    Source& src = sources_[idx];
    src.configured  = true;
    src.publishMode = publishMode;
    for (size_t m = 0; m < metas.size(); m++) {
        int si = findSignal(src, metas[m].name);
        if (si < 0) {
            SignalView sig;
            sig.meta  = metas[m];
            sig.color = tracePalette(static_cast<int>(src.signals.size()));
            src.signals.push_back(std::move(sig));
        } else {
            src.signals[si].meta = metas[m];
        }
    }
    Q_EMIT configChanged(QString::fromStdString(sourceId));
}

void Hub::onStats(const std::string& json) {
    std::vector<std::pair<std::string, SourceStats>> statsVec;
    ParseStats(json, statsVec);
    for (const auto& kv : statsVec) {
        int idx = findSource(kv.first);
        if (idx >= 0) {
            sources_[idx].stats = kv.second;
            sources_[idx].state = kv.second.state;
        }
    }
    Q_EMIT statsChanged();
}

void Hub::onTriggerState(const std::string& json) {
    TriggerStateMsg msg;
    if (!ParseTriggerState(json, msg)) { return; }
    trigger_.status  = msg.state;
    trigger_.stopped = msg.stopped;
    if (msg.hasTrigTime) {
        trigger_.trigTime    = msg.trigTime;
        trigger_.hasTrigTime = true;
    }
    if (msg.state == "idle") { trigger_.hasTrigTime = false; }
    Q_EMIT triggerStateChanged();
}

static void routeZoom(PlotZoomCache* caches, const ZoomResponse& resp,
                      int& outPlot) {
    outPlot = -1;
    for (int i = 0; i < kMaxPlotSlots; i++) {
        auto& zc = caches[i];
        if (zc.pending && zc.reqId == resp.reqId) {
            double dataT0 = 1e300, dataT1 = -1e300;
            bool anyData = false;
            for (const auto& zs : resp.signals) {
                for (double tv : zs.t) {
                    if (tv < dataT0) { dataT0 = tv; }
                    if (tv > dataT1) { dataT1 = tv; }
                    anyData = true;
                }
            }
            if (anyData) {
                zc.pts = resp.signals;
                zc.t0 = dataT0; zc.t1 = dataT1; zc.valid = true;
            }
            zc.pending = false;
            outPlot = i;
            return;
        }
    }
}

void Hub::onZoom(const std::string& json) {
    ZoomResponse resp;
    if (!ParseZoom(json, resp)) { return; }
    int plot = -1;
    routeZoom(zoomCache_, resp, plot);
    if (plot >= 0) { Q_EMIT zoomReceived(plot); }
}

void Hub::onHistoryZoom(const std::string& json) {
    ZoomResponse resp;
    if (!ParseZoom(json, resp)) { return; }
    int plot = -1;
    routeZoom(histZoomCache_, resp, plot);
    if (plot >= 0) { Q_EMIT historyZoomReceived(plot); }
}

void Hub::onHistoryInfo(const std::string& json) {
    ParseHistoryInfo(json, historyInfo_);
    Q_EMIT historyInfoChanged();
}

void Hub::onMaxPointsUpdated(const std::string& json) {
    uint32_t mp = 0;
    ParseMaxPointsUpdated(json, mp);
    if (mp >= 2) {
        maxPoints_ = mp;
        for (auto& src : sources_) {
            for (auto& sig : src.signals) { sig.buf.setCapacity(mp); }
        }
        Q_EMIT maxPointsChanged(mp);
    }
}

/* ── helpers ─────────────────────────────────────────────────────────────── */

int Hub::findSource(const std::string& id) const {
    for (int i = 0; i < static_cast<int>(sources_.size()); i++) {
        if (sources_[i].id == id) { return i; }
    }
    return -1;
}

int Hub::findSignal(const Source& src, const std::string& name) const {
    for (int i = 0; i < static_cast<int>(src.signals.size()); i++) {
        if (src.signals[i].meta.name == name) { return i; }
    }
    return -1;
}

std::string Hub::slotKey(const PlotAssignment& a) const {
    if (a.sourceIdx < 0 || a.sourceIdx >= static_cast<int>(sources_.size())) {
        return std::string();
    }
    const Source& src = sources_[a.sourceIdx];
    if (a.signalIdx < 0 || a.signalIdx >= static_cast<int>(src.signals.size())) {
        return std::string();
    }
    return src.id + ":" + src.signals[a.signalIdx].meta.name;
}

/* ── commands ────────────────────────────────────────────────────────────── */

void Hub::sendGetSources() { ws_.sendText(BuildGetSources()); }
void Hub::sendGetStats()   { ws_.sendText(BuildGetStats()); }
void Hub::sendGetConfig(const std::string& id) { ws_.sendText(BuildGetConfig(id)); }
void Hub::sendHistoryInfo(){ ws_.sendText(BuildHistoryInfo()); }

void Hub::sendAddSource(const std::string& label, const std::string& addr,
                        const std::string& mcast, uint16_t dataPort) {
    ws_.sendText(BuildAddSource(label, addr, mcast, dataPort));
}
void Hub::sendRemoveSource(const std::string& id) {
    ws_.sendText(BuildRemoveSource(id));
}
void Hub::sendSaveSources() { ws_.sendText(BuildSaveSources()); }
void Hub::sendSetMaxPoints(uint32_t n) { ws_.sendText(BuildSetMaxPoints(n)); }

void Hub::sendSetTrigger(const std::string& key, const std::string& edge,
                         double threshold, double windowSec, double prePercent,
                         const std::string& mode) {
    ws_.sendText(BuildSetTrigger(key, edge, threshold, windowSec, prePercent, mode));
}
void Hub::sendArm()             { ws_.sendText(BuildArm()); }
void Hub::sendDisarm()          { ws_.sendText(BuildDisarm()); }
void Hub::sendRearm()           { ws_.sendText(BuildRearm()); }
void Hub::sendTrigStop(bool s)  { ws_.sendText(BuildTrigStop(s)); }

void Hub::requestZoom(int plotIdx, double t0, double t1, const std::string& csv) {
    if (plotIdx < 0 || plotIdx >= kMaxPlotSlots) { return; }
    if (!ws_.isConnected() || csv.empty()) { return; }
    auto& zc = zoomCache_[plotIdx];
    zc.reqId = nextZoomReqId_++;
    zc.reqT0 = t0; zc.reqT1 = t1; zc.pending = true;
    ws_.sendText(BuildZoom(zc.reqId, t0, t1, 2400, csv));
}

void Hub::requestHistoryZoom(int plotIdx, double t0, double t1,
                             const std::string& csv) {
    if (plotIdx < 0 || plotIdx >= kMaxPlotSlots) { return; }
    if (!ws_.isConnected() || csv.empty()) { return; }
    auto& hc = histZoomCache_[plotIdx];
    hc.reqId = nextZoomReqId_++;
    hc.reqT0 = t0; hc.reqT1 = t1; hc.pending = true;
    ws_.sendText(BuildHistoryZoom(hc.reqId, t0, t1, 2400, csv));
}

} /* namespace shq */
