#include "mainwindow.h"
#include "plotwidget.h"
#include "sidebar.h"
#include "stats_panel.h"
#include "trigger_toolbar.h"

#include <QApplication>
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QFormLayout>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QSpinBox>
#include <QStatusBar>
#include <QTextStream>
#include <QTimer>
#include <QToolBar>
#include <QVBoxLayout>
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <set>

// ── Dark palette ──────────────────────────────────────────────────────

static void applyDarkPalette(QApplication& app)
{
    QPalette p;
    p.setColor(QPalette::Window,          QColor(28, 28, 28));
    p.setColor(QPalette::WindowText,      QColor(220, 220, 220));
    p.setColor(QPalette::Base,            QColor(18, 18, 18));
    p.setColor(QPalette::AlternateBase,   QColor(36, 36, 36));
    p.setColor(QPalette::ToolTipBase,     QColor(50, 50, 50));
    p.setColor(QPalette::ToolTipText,     QColor(220, 220, 220));
    p.setColor(QPalette::Text,            QColor(220, 220, 220));
    p.setColor(QPalette::Button,          QColor(45, 45, 45));
    p.setColor(QPalette::ButtonText,      QColor(220, 220, 220));
    p.setColor(QPalette::BrightText,      Qt::red);
    p.setColor(QPalette::Highlight,       QColor(42, 130, 218));
    p.setColor(QPalette::HighlightedText, Qt::black);
    app.setPalette(p);
    app.setStyle("Fusion");
}

// ── MainWindow ────────────────────────────────────────────────────────

MainWindow::MainWindow(const AppConfig& cfg, QWidget* parent)
    : QMainWindow(parent)
    , cfg_(cfg)
{
    setWindowTitle("DAQ Viewer");
    resize(cfg_.display.width, cfg_.display.height);

    if (auto* a = qobject_cast<QApplication*>(qApp))
        applyDarkPalette(*a);

    applyStylesheet();

    // Create central area first so statsPanel_ exists before buildToolbar() connects to it
    centralArea_ = new QWidget(this);
    auto* centralLay = new QVBoxLayout(centralArea_);
    centralLay->setSpacing(0);
    centralLay->setContentsMargins(0,0,0,0);
    plotGrid_ = new QWidget(centralArea_);
    centralLay->addWidget(plotGrid_, 1);
    statsPanel_ = new StatsPanel(centralArea_);
    centralLay->addWidget(statsPanel_, 0);
    setCentralWidget(centralArea_);

    buildToolbar();

    // Left dock: signal tree + Add Source button
    sidebar_ = new Sidebar(this);
    addDockWidget(Qt::LeftDockWidgetArea, sidebar_);
    connect(sidebar_, &Sidebar::addSourceRequested, this, &MainWindow::onAddSource);

    // Bottom dock: trigger panel (hidden by default)
    triggerPanel_ = new TriggerToolBar(this);
    addDockWidget(Qt::BottomDockWidgetArea, triggerPanel_);
    triggerPanel_->hide();
    connect(triggerPanel_, &TriggerToolBar::armRequested,
            this, &MainWindow::onTriggerArm);
    connect(triggerPanel_, &TriggerToolBar::disarmRequested,
            this, &MainWindow::onTriggerDisarm);

    // Status bar
    statsLabel_ = new QLabel(this);
    statusBar()->addWidget(statsLabel_);

    // Trigger feed buffers
    trigFeedT_.resize(kTriggerFeedPts);
    trigFeedV_.resize(kTriggerFeedPts);

    // Timer
    timer_ = new QTimer(this);
    connect(timer_, &QTimer::timeout, this, &MainWindow::onTimer);
    timer_->start(1000 / cfg_.display.fps);

    for (const auto& sc : cfg_.sources)
        addSource(sc.label, sc.host, sc.port,
                  sc.multicast_group, sc.data_port);

    applyLayout(Layout::L1x1);
}

MainWindow::~MainWindow()
{
    timer_->stop();
    for (auto& src : sources_) src->stop();
}

// ── QSS Stylesheet ────────────────────────────────────────────────────

void MainWindow::applyStylesheet()
{
    qApp->setStyleSheet(R"QSS(
QWidget {
    font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
    font-size: 12px;
}
QToolBar {
    background-color: #2a2a2a;
    border-bottom: 1px solid #383838;
    spacing: 3px;
    padding: 2px 4px;
}
QToolBar::separator {
    width: 1px; background: #444; margin: 3px 2px;
}
QPushButton {
    background-color: #3c3c3c;
    border: 1px solid #555;
    border-radius: 4px;
    padding: 4px 10px;
    color: #dcdcdc;
    min-height: 20px;
}
QPushButton:hover { background-color: #4a4a4a; border-color: #2a82da; }
QPushButton:pressed { background-color: #1e6ab5; }
QPushButton:checked { background-color: #2a82da; border-color: #1a62ba; color: #fff; }
QPushButton:disabled { color: #666; border-color: #444; background-color: #2d2d2d; }

QPushButton#addSourceBtn {
    background: transparent;
    border: none;
    border-top: 1px solid #383838;
    border-radius: 0;
    color: #888;
    font-size: 11px;
    padding: 5px;
    text-align: center;
}
QPushButton#addSourceBtn:hover { background-color: #242424; color: #2a82da; }

QPushButton#statsPanelToggle {
    background: transparent;
    border: none;
    color: #aaa;
    font-size: 11px;
    padding: 2px 6px;
    text-align: left;
}
QPushButton#statsPanelToggle:hover { color: #ddd; }

QWidget#statsPanelHeader {
    background-color: #252525;
    border-top: 1px solid #383838;
}

QComboBox {
    background-color: #3c3c3c;
    border: 1px solid #555;
    border-radius: 4px;
    padding: 3px 6px;
    color: #dcdcdc;
    min-width: 70px;
    min-height: 20px;
}
QComboBox:hover { border-color: #2a82da; }
QComboBox::drop-down { border: none; width: 20px; }
QComboBox QAbstractItemView {
    background-color: #2d2d2d;
    border: 1px solid #555;
    selection-background-color: #2a82da;
    selection-color: #fff;
    outline: none;
}
QSpinBox, QDoubleSpinBox {
    background-color: #3c3c3c;
    border: 1px solid #555;
    border-radius: 4px;
    padding: 3px 4px;
    color: #dcdcdc;
    min-height: 20px;
}
QSpinBox:hover, QDoubleSpinBox:hover { border-color: #2a82da; }
QSpinBox::up-button, QDoubleSpinBox::up-button,
QSpinBox::down-button, QDoubleSpinBox::down-button {
    background-color: #4a4a4a; border: none;
    border-left: 1px solid #555; width: 16px;
}
QLineEdit {
    background-color: #2d2d2d;
    border: 1px solid #555;
    border-radius: 4px;
    padding: 3px 6px;
    color: #dcdcdc;
    min-height: 20px;
}
QLineEdit:focus { border-color: #2a82da; }
QTreeWidget {
    background-color: #1c1c1c;
    alternate-background-color: #242424;
    border: none;
    color: #dcdcdc;
    outline: none;
}
QTreeWidget::item { padding: 2px 0; }
QTreeWidget::item:hover { background-color: #2d3748; }
QTreeWidget::item:selected { background-color: #2a82da; color: #fff; }
QTreeWidget::branch { background: transparent; }
QTableWidget {
    background-color: #1c1c1c;
    alternate-background-color: #242424;
    gridline-color: #333;
    border: none;
    color: #dcdcdc;
}
QTableWidget::item:selected { background-color: #2a82da; color: #fff; }
QHeaderView::section {
    background-color: #2a2a2a;
    border: 1px solid #383838;
    padding: 3px 5px;
    color: #aaa;
    font-weight: bold;
    font-size: 11px;
}
QScrollBar:vertical {
    background: #1c1c1c; width: 8px; margin: 0;
}
QScrollBar::handle:vertical {
    background: #4a4a4a; border-radius: 4px; min-height: 24px;
}
QScrollBar::handle:vertical:hover { background: #666; }
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }
QScrollBar:horizontal {
    background: #1c1c1c; height: 8px; margin: 0;
}
QScrollBar::handle:horizontal {
    background: #4a4a4a; border-radius: 4px; min-width: 24px;
}
QScrollBar::handle:horizontal:hover { background: #666; }
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0; }
QDockWidget { color: #dcdcdc; }
QDockWidget::title {
    background-color: #252525;
    padding: 4px 6px;
    border-bottom: 1px solid #383838;
    font-weight: bold;
    color: #aaa;
    text-align: left;
}
QDockWidget::close-button, QDockWidget::float-button {
    background: transparent; border: none; padding: 2px;
}
QDockWidget::close-button:hover, QDockWidget::float-button:hover {
    background: #444; border-radius: 3px;
}
QStatusBar {
    background-color: #252525;
    border-top: 1px solid #383838;
    color: #999;
    font-size: 11px;
}
QGroupBox {
    border: 1px solid #3a3a3a;
    border-radius: 4px;
    margin-top: 8px;
    padding-top: 4px;
    color: #888;
    font-size: 11px;
    font-weight: bold;
}
QGroupBox::title {
    subcontrol-origin: margin; left: 8px; padding: 0 4px;
}
QMenu {
    background-color: #2d2d2d; border: 1px solid #555; color: #dcdcdc;
}
QMenu::item:selected { background-color: #2a82da; color: #fff; }
QMenu::separator { height: 1px; background: #3a3a3a; }
QToolTip {
    background-color: #2d2d2d; border: 1px solid #555;
    color: #dcdcdc; padding: 3px 6px;
}
)QSS");
}

// ── Sources ───────────────────────────────────────────────────────────

void MainWindow::addSource(const std::string& label,
                            const std::string& host, uint16_t port,
                            const std::string& multicast_group, uint16_t data_port)
{
    auto src = std::make_shared<Source>(label, host, port, multicast_group, data_port);
    src->start();
    int idx = static_cast<int>(sources_.size());
    sources_.push_back(src);
    sidebar_->addSource(src, idx);
}

// ── Toolbar ───────────────────────────────────────────────────────────

void MainWindow::buildToolbar()
{
    auto* tb = addToolBar("Controls");
    tb->setMovable(false);

    pauseBtn_ = new QPushButton("Pause", this);
    pauseBtn_->setCheckable(true);
    connect(pauseBtn_, &QPushButton::toggled, this, &MainWindow::onPauseToggled);
    tb->addWidget(pauseBtn_);

    tb->addSeparator();

    tb->addWidget(new QLabel("Window:", this));
    windowCombo_ = new QComboBox(this);
    windowCombo_->addItems({"1 s","2 s","5 s","10 s","30 s","60 s","2 min","5 min"});
    windowCombo_->setCurrentIndex(2);
    connect(windowCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged),
            [this](int idx) {
                static const double secs[] = {1,2,5,10,30,60,120,300};
                if (idx >= 0 && idx < 8) {
                    windowSec_ = secs[idx];
                    // Reset user zoom when window changes so new range applies
                    for (auto* p : plots_) p->resetZoom();
                    for (auto* p : plots_)
                        p->refresh(viewTMax_, windowSec_, paused_);
                }
            });
    tb->addWidget(windowCombo_);

    tb->addSeparator();

    layoutPicker_ = new LayoutPicker(this);
    connect(layoutPicker_, &LayoutPicker::layoutSelected,
            this, &MainWindow::onLayoutChanged);
    tb->addWidget(layoutPicker_);

    tb->addSeparator();

    auto* statsToggle = new QPushButton("Stats", this);
    statsToggle->setCheckable(true);
    connect(statsToggle, &QPushButton::toggled,
            statsPanel_, [this](bool on) { statsPanel_->setExpanded(on); });
    tb->addWidget(statsToggle);

    auto* trigBtn = new QPushButton("Trigger", this);
    trigBtn->setCheckable(true);
    connect(trigBtn, &QPushButton::toggled, [this](bool on) {
        triggerPanel_->setVisible(on);
        if (on) triggerPanel_->refreshSources(sources_);
    });
    tb->addWidget(trigBtn);

    tb->addSeparator();

    // A/B cursors — Ctrl+Left/Right click on any plot to place
    cursorABtn_ = new QPushButton("A", this);
    cursorABtn_->setCheckable(true);
    cursorABtn_->setToolTip("Cursor A — Ctrl+Left click to place");
    cursorABtn_->setStyleSheet("QPushButton:checked { background-color: #b8a000; color: #fff; }");
    connect(cursorABtn_, &QPushButton::toggled, [this](bool on) {
        cursorAActive_ = on;
        if (!on) {
            cursorATime_ = 0.0;
            deltaLabel_->setText("ΔT: --");
        }
        for (auto* pw : plots_)
            pw->refreshCursors(cursorATime_, cursorBTime_, cursorAActive_, cursorBActive_);
    });
    tb->addWidget(cursorABtn_);

    cursorBBtn_ = new QPushButton("B", this);
    cursorBBtn_->setCheckable(true);
    cursorBBtn_->setToolTip("Cursor B — Ctrl+Right click to place");
    cursorBBtn_->setStyleSheet("QPushButton:checked { background-color: #008b8b; color: #fff; }");
    connect(cursorBBtn_, &QPushButton::toggled, [this](bool on) {
        cursorBActive_ = on;
        if (!on) {
            cursorBTime_ = 0.0;
            deltaLabel_->setText("ΔT: --");
        }
        for (auto* pw : plots_)
            pw->refreshCursors(cursorATime_, cursorBTime_, cursorAActive_, cursorBActive_);
    });
    tb->addWidget(cursorBBtn_);

    deltaLabel_ = new QLabel("ΔT: --", this);
    deltaLabel_->setStyleSheet("color: #aaa; font-size: 11px; padding: 0 6px;");
    tb->addWidget(deltaLabel_);

    tb->addSeparator();

    // Zoom history controls — visible only when zoomed
    backBtn_ = new QPushButton("← Back", this);
    backBtn_->setToolTip("Go back one zoom level");
    backBtn_->setVisible(false);
    connect(backBtn_, &QPushButton::clicked, this, &MainWindow::onZoomBack);
    tb->addWidget(backBtn_);

    autoBtn_ = new QPushButton("◉ Live", this);
    autoBtn_->setToolTip("Return to live rolling window");
    autoBtn_->setStyleSheet("QPushButton { color: #6cf; }");
    autoBtn_->setVisible(false);
    connect(autoBtn_, &QPushButton::clicked, this, &MainWindow::onAutoZoom);
    tb->addWidget(autoBtn_);

    tb->addSeparator();

    exportBtn_ = new QPushButton("↓ CSV", this);
    exportBtn_->setToolTip("Export visible data to CSV");
    connect(exportBtn_, &QPushButton::clicked, this, &MainWindow::onExportCsv);
    tb->addWidget(exportBtn_);
}

// ── Plot grid ─────────────────────────────────────────────────────────

void MainWindow::buildPlotGrid()
{
    delete plotGrid_->layout();
    for (auto* p : plots_) delete p;
    plots_.clear();
}

void MainWindow::applyLayout(Layout lay)
{
    layout_ = lay;

    std::vector<PlotWidget::SignalId> saved;
    for (auto* p : plots_)
        for (auto& id : p->getSignals())
            saved.push_back(id);

    buildPlotGrid();

    int rows = 1, cols = 1;
    switch (lay) {
    case Layout::L1x1: rows=1; cols=1; break;
    case Layout::L1x2: rows=1; cols=2; break;
    case Layout::L1x3: rows=1; cols=3; break;
    case Layout::L1x4: rows=1; cols=4; break;
    case Layout::L2x1: rows=2; cols=1; break;
    case Layout::L2x2: rows=2; cols=2; break;
    case Layout::L3x1: rows=3; cols=1; break;
    case Layout::L4x1: rows=4; cols=1; break;
    }

    auto* grid = new QGridLayout(plotGrid_);
    grid->setSpacing(4);
    grid->setContentsMargins(4,4,4,4);

    int id = 0;
    for (int r = 0; r < rows; ++r) {
        for (int c = 0; c < cols; ++c) {
            auto* pw = new PlotWidget(id++, plotGrid_);
            grid->addWidget(pw, r, c);
            plots_.push_back(pw);
            connect(pw, &PlotWidget::signalDropped,
                    [this, pw](int srcIdx, int sigIdx) {
                        if (srcIdx < 0 || srcIdx >= static_cast<int>(sources_.size())) return;
                        pw->addSignal(sources_[srcIdx], sigIdx);
                    });
        }
    }

    if (!plots_.empty())
        for (auto& id : saved)
            plots_[0]->addSignal(id.src, id.sigIdx);

    // Reset zoom history when layout changes
    zoomHistory_.clear();
    zoomed_ = false;
    if (autoBtn_) { autoBtn_->setVisible(false); backBtn_->setVisible(false); }

    // Wire cross-plot X sync + zoom history for all new plots
    for (auto* pw : plots_) {
        connect(pw, &PlotWidget::xRangeChanged, [this, pw](double lo, double hi) {
            // Push current zoom state to history before updating
            if (!zoomed_) {
                // Transition rolling → zoomed: push rolling sentinel
                zoomHistory_.push_back({-1.0, -1.0});
                zoomed_ = true;
                autoBtn_->setVisible(true);
                backBtn_->setVisible(true);
            } else if (!plots_.empty()) {
                // Already zoomed: push the current range (from before this update)
                auto [pl, ph] = plots_[0]->currentXRange();
                // Avoid duplicate entries from drag-during-zoom
                if (zoomHistory_.empty() || zoomHistory_.back() != std::make_pair(pl, ph))
                    zoomHistory_.push_back({pl, ph});
            }
            if (zoomHistory_.size() > 20)
                zoomHistory_.erase(zoomHistory_.begin());
            // Sync other plots
            for (auto* other : plots_)
                if (other != pw) other->syncXRange(lo, hi);
        });
        // Cursor placement: update global state and push to all plots
        connect(pw, &PlotWidget::cursorAPlaced, this, &MainWindow::onCursorAPlaced);
        connect(pw, &PlotWidget::cursorBPlaced, this, &MainWindow::onCursorBPlaced);

        // Push current cursor state to newly created plot
        pw->refreshCursors(cursorATime_, cursorBTime_, cursorAActive_, cursorBActive_);
    }
}

// ── Timer ─────────────────────────────────────────────────────────────

void MainWindow::onTimer()
{
    using clock = std::chrono::system_clock;
    double now = std::chrono::duration<double>(
        clock::now().time_since_epoch()).count();

    if (!paused_) {
        double latest = 0.0;
        for (auto& src : sources_) {
            std::lock_guard<std::mutex> lk(src->signals_mutex);
            for (auto& ss : src->signal_states) {
                if (!ss.buffer) continue;
                double t = ss.buffer->latest_time();
                if (t > latest) latest = t;
            }
        }
        viewTMax_ = (latest > 0.0) ? latest : now;
    }

    // Feed trigger engine
    if (trigger_.state() == TriggerEngine::State::Armed && triggerSrc_) {
        std::shared_ptr<SignalBuffer> tbuf;
        {
            std::lock_guard<std::mutex> lk(triggerSrc_->signals_mutex);
            if (triggerSigIdx_ < static_cast<int>(triggerSrc_->signal_states.size()))
                tbuf = triggerSrc_->signal_states[triggerSigIdx_].buffer;
        }
        if (tbuf) {
            size_t head = tbuf->total_written();
            if (head > triggerLastHead_) {
                size_t newPts = std::min(head - triggerLastHead_, kTriggerFeedPts);
                size_t n = tbuf->read_latest(newPts, trigFeedT_.data(), trigFeedV_.data());
                if (trigger_.feed_new(trigFeedT_.data(), trigFeedV_.data(), n)) {
                    // Triggered: pause and freeze the view at [pre..post] window.
                    // Use QSignalBlocker so setting the button's checked state does
                    // not re-enter onPauseToggled and call onAutoZoom().
                    paused_ = true;
                    {
                        QSignalBlocker blocker(pauseBtn_);
                        pauseBtn_->setChecked(true);
                        pauseBtn_->setText("Resume");
                    }
                    triggerPanel_->setStatus("Fired");

                    double trigT   = trigger_.trigger_time();
                    double preSec  = triggerPanel_->preSec();
                    double postSec = triggerPanel_->postSec();
                    for (auto* p : plots_)
                        p->setViewRange(trigT - preSec, trigT + postSec);
                }
                triggerLastHead_ = head;
            }
        }
    }

    for (auto* p : plots_)
        p->refresh(viewTMax_, windowSec_, paused_);

    sidebar_->refresh();
    statsPanel_->refresh(sources_);
    refreshStatus();
}

void MainWindow::refreshStatus()
{
    uint64_t totalRx  = 0;
    size_t   connected = 0;
    for (auto& src : sources_) {
        if (src->connected()) ++connected;
        totalRx += src->stats().packets_rx;
    }
    statsLabel_->setText(
        QString("Sources: %1/%2  |  Packets RX: %3")
            .arg(connected).arg(sources_.size()).arg(totalRx));
}

// ── Slots ─────────────────────────────────────────────────────────────

void MainWindow::onPauseToggled(bool paused)
{
    paused_ = paused;
    pauseBtn_->setText(paused ? "Resume" : "Pause");
    if (!paused) {
        onAutoZoom();  // return to live rolling when resuming
    }
}

void MainWindow::onWindowSecChanged(double secs)
{
    windowSec_ = secs;
}

void MainWindow::onLayoutChanged(int index)
{
    static const Layout layouts[] = {
        Layout::L1x1, Layout::L1x2, Layout::L1x3, Layout::L1x4,
        Layout::L2x1, Layout::L2x2, Layout::L3x1, Layout::L4x1
    };
    if (index >= 0 && index < 8)
        applyLayout(layouts[index]);
}

void MainWindow::onAddSource()
{
    QDialog dlg(this);
    dlg.setWindowTitle("Add Source");
    auto* form       = new QFormLayout(&dlg);
    auto* labelEdit  = new QLineEdit("Source", &dlg);
    auto* hostEdit   = new QLineEdit("localhost", &dlg);
    auto* portSpin   = new QSpinBox(&dlg);
    portSpin->setRange(1, 65535);
    portSpin->setValue(44500);
    // Multicast (optional)
    auto* mcastEdit  = new QLineEdit(&dlg);
    mcastEdit->setPlaceholderText("e.g. 239.0.0.1  (blank = unicast)");
    auto* dataPortSpin = new QSpinBox(&dlg);
    dataPortSpin->setRange(0, 65535);
    dataPortSpin->setValue(0);
    dataPortSpin->setSpecialValueText("port + 1");
    form->addRow("Label:", labelEdit);
    form->addRow("Host:", hostEdit);
    form->addRow("Port:", portSpin);
    form->addRow(new QLabel("─── Multicast (optional) ───", &dlg));
    form->addRow("Multicast Group:", mcastEdit);
    form->addRow("Data Port:", dataPortSpin);
    auto* btns = new QDialogButtonBox(
        QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg);
    connect(btns, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
    connect(btns, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
    form->addRow(btns);
    if (dlg.exec() == QDialog::Accepted) {
        addSource(labelEdit->text().toStdString(),
                  hostEdit->text().toStdString(),
                  static_cast<uint16_t>(portSpin->value()),
                  mcastEdit->text().toStdString(),
                  static_cast<uint16_t>(dataPortSpin->value()));
        if (triggerPanel_->isVisible())
            triggerPanel_->refreshSources(sources_);
    }
}

void MainWindow::onTriggerArm()
{
    auto [srcIdx, sigIdx] = triggerPanel_->selectedSignal();
    if (srcIdx < 0 || srcIdx >= static_cast<int>(sources_.size())) {
        triggerPanel_->setStatus("No signal");
        return;
    }
    triggerSrc_    = sources_[srcIdx];
    triggerSigIdx_ = sigIdx;

    {
        std::lock_guard<std::mutex> lk(triggerSrc_->signals_mutex);
        if (triggerSigIdx_ < static_cast<int>(triggerSrc_->signal_states.size())) {
            auto& buf = triggerSrc_->signal_states[triggerSigIdx_].buffer;
            triggerLastHead_ = buf ? buf->total_written() : 0;
        }
    }

    trigger_.configure(
        triggerPanel_->edge(),
        triggerPanel_->threshold(),
        triggerPanel_->mode(),
        triggerPanel_->preSec(),
        triggerPanel_->postSec());
    trigger_.arm();
    triggerPanel_->setStatus("Armed");
}

void MainWindow::onTriggerDisarm()
{
    trigger_.disarm();
    triggerSrc_.reset();
    triggerPanel_->setStatus("Idle");

    // Pause live scrolling so any data visible at disarm time is preserved.
    // Use QSignalBlocker to avoid re-entering onPauseToggled → onAutoZoom,
    // which would reset the zoom and snap back to the live window.
    if (!paused_) {
        paused_ = true;
        QSignalBlocker blocker(pauseBtn_);
        pauseBtn_->setChecked(true);
        pauseBtn_->setText("Resume");
    }
}

// ── A/B cursor slots (C) ──────────────────────────────────────────────

void MainWindow::onCursorAPlaced(double t)
{
    if (!cursorAActive_) return;
    cursorATime_ = t;
    if (cursorAActive_ && cursorBActive_)
        deltaLabel_->setText(
            QString("ΔT: %1 ms").arg((cursorBTime_ - cursorATime_) * 1000.0, 0, 'f', 3));
    for (auto* pw : plots_)
        pw->refreshCursors(cursorATime_, cursorBTime_, cursorAActive_, cursorBActive_);
}

void MainWindow::onCursorBPlaced(double t)
{
    if (!cursorBActive_) return;
    cursorBTime_ = t;
    if (cursorAActive_ && cursorBActive_)
        deltaLabel_->setText(
            QString("ΔT: %1 ms").arg((cursorBTime_ - cursorATime_) * 1000.0, 0, 'f', 3));
    for (auto* pw : plots_)
        pw->refreshCursors(cursorATime_, cursorBTime_, cursorAActive_, cursorBActive_);
}

// ── Zoom history ──────────────────────────────────────────────────────

void MainWindow::onAutoZoom()
{
    zoomHistory_.clear();
    zoomed_ = false;
    autoBtn_->setVisible(false);
    backBtn_->setVisible(false);
    for (auto* p : plots_) p->resetZoom();
}

void MainWindow::onZoomBack()
{
    if (zoomHistory_.empty()) { onAutoZoom(); return; }

    auto prev = zoomHistory_.back();
    zoomHistory_.pop_back();

    if (prev.first < 0.0) {
        // Sentinel: go back to rolling
        onAutoZoom();
    } else {
        // Apply the previous zoom range to all plots
        for (auto* p : plots_) p->applyZoomRange(prev.first, prev.second);
    }

    if (zoomHistory_.empty()) {
        autoBtn_->setVisible(false);
        backBtn_->setVisible(false);
        zoomed_ = false;
    }
}

// ── CSV Export ────────────────────────────────────────────────────────

void MainWindow::onExportCsv()
{
    if (plots_.empty()) return;

    QString fname = QFileDialog::getSaveFileName(
        this, "Export CSV", QString(), "CSV files (*.csv);;All files (*)");
    if (fname.isEmpty()) return;

    // Collect unique signals across all plots (deduplicated by src+idx)
    struct SigRef { std::shared_ptr<Source> src; int sigIdx; std::string name; };
    std::vector<SigRef> sigs;
    std::set<std::pair<void*, int>> seen;
    for (auto* pw : plots_) {
        for (auto& sid : pw->getSignals()) {
            auto key = std::make_pair((void*)sid.src.get(), sid.sigIdx);
            if (seen.count(key)) continue;
            seen.insert(key);
            std::string name;
            {
                std::lock_guard<std::mutex> lk(sid.src->signals_mutex);
                if (sid.sigIdx < (int)sid.src->signal_states.size())
                    name = sid.src->signal_states[sid.sigIdx].def.name;
            }
            sigs.push_back({sid.src, sid.sigIdx, name});
        }
    }
    if (sigs.empty()) return;

    // Determine export time range from first plot's current range
    auto [tMin, tMax] = plots_[0]->currentXRange();
    if (!zoomed_) {
        tMax = viewTMax_;
        tMin = tMax - windowSec_;
    }

    // Read buffer data for each signal
    const size_t kMaxPts = 200'000;
    std::vector<std::vector<double>> allT(sigs.size()), allV(sigs.size());
    for (size_t si = 0; si < sigs.size(); ++si) {
        allT[si].resize(kMaxPts); allV[si].resize(kMaxPts);
        size_t n = 0;
        {
            std::lock_guard<std::mutex> lk(sigs[si].src->signals_mutex);
            if (sigs[si].sigIdx < (int)sigs[si].src->signal_states.size()) {
                auto& ss = sigs[si].src->signal_states[sigs[si].sigIdx];
                if (ss.buffer)
                    n = ss.buffer->read_window(tMin, tMax,
                                               allT[si].data(), allV[si].data(), kMaxPts);
            }
        }
        allT[si].resize(n); allV[si].resize(n);
    }

    // Find master (densest) signal
    int master = 0;
    for (int i = 1; i < (int)sigs.size(); ++i)
        if (allT[i].size() > allT[master].size()) master = i;
    if (allT[master].empty()) return;

    QFile file(fname);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) return;
    QTextStream out(&file);

    // Header
    out << "time_s";
    for (auto& s : sigs)
        out << "," << QString::fromStdString(s.name);
    out << "\n";

    // Rows aligned to master time axis; other signals nearest-neighbor
    for (size_t row = 0; row < allT[master].size(); ++row) {
        double t = allT[master][row];
        out << QString::number(t, 'f', 9);
        for (int ci = 0; ci < (int)sigs.size(); ++ci) {
            out << ",";
            if (ci == master) {
                out << QString::number(allV[master][row], 'g', 8);
            } else if (allT[ci].empty()) {
                // no data
            } else {
                // Binary search for nearest time
                auto& ts = allT[ci];
                auto it = std::lower_bound(ts.begin(), ts.end(), t);
                if (it == ts.end()) --it;
                else if (it != ts.begin()) {
                    auto prev = std::prev(it);
                    if (std::abs(*prev - t) < std::abs(*it - t)) it = prev;
                }
                size_t idx = static_cast<size_t>(it - ts.begin());
                out << QString::number(allV[ci][idx], 'g', 8);
            }
        }
        out << "\n";
    }

    statusBar()->showMessage(
        QString("Exported %1 rows to %2").arg(allT[master].size()).arg(fname), 5000);
}
