"""
Sequence Interpreter tab - ported from seq_interp/src/gui/main_window.py.

Converts the standalone QMainWindow into a self-contained QWidget that can
be embedded as a tab inside LFMRIWindow.  All interpretation, export, and
visualisation functionality is preserved unchanged.
"""
from __future__ import annotations

import logging
import os
from datetime import datetime

from PySide6.QtCore import Qt, QSize, Signal
from PySide6.QtGui import QFont, QColor
from PySide6.QtWidgets import (
    QApplication, QWidget, QSplitter, QVBoxLayout, QHBoxLayout,
    QGroupBox, QFormLayout, QLabel, QListWidget, QListWidgetItem,
    QFrame, QPushButton, QProgressBar, QComboBox, QCheckBox, QSpinBox,
    QMessageBox, QScrollArea, QSizePolicy, QFileDialog,
)

# ADC dynamic-range codes  →  (code, label shown in UI)
_ADC_RANGES: list[tuple[int, str]] = [
    (0,  "10 мВ"),
    (1,  "20 мВ"),
    (2,  "50 мВ"),
    (3,  "100 мВ"),
    (4,  "200 мВ"),
    (5,  "500 мВ"),
    (6,  "1 В"),
    (7,  "2 В"),
    (8,  "5 В"),
    (9,  "10 В"),
    (10, "20 В"),
    (11, "50 В"),
]
# Default per-channel ranges are read from hw_config.json at runtime

from src.gui.tr_widgets import TrGroupBox, TrPushButton
from src.gui.adapters import (
    build_block_rows, seq_metadata, validate_timing, find_block_at_time,
)
from src.gui.block_table import BlockTable
from src.gui.controls_panel import DelayControlsPanel
from src.gui.plot_panel import PlotPanel
from src.gui.preview_panel import PreviewPanel
from src.gui.scheme_panel import SchemePanel, system_is_dark
from src.gui.workers import (
    SeqInterpHttpWorker, ArtifactDownloadWorker,
)
from src.clients.seq_interp_client import SeqInterpClient
from src import i18n, theme

# -- loading-state style maps (light / dark) -----------------------------------

_STATE_LIGHT: dict[str, tuple[str, str]] = {
    "idle":     ("#757575", "o"),
    "selected": ("#1565c0", "o"),
    "loading":  ("#e65100", "Refresh"),
    "loaded":   ("#2e7d32", "OK"),
    "failed":   ("#c62828", "ERR"),
}

_STATE_DARK: dict[str, tuple[str, str]] = {
    "idle":     ("#9e9e9e", "o"),
    "selected": ("#64b5f6", "o"),
    "loading":  ("#ff9800", "Refresh"),
    "loaded":   ("#81c784", "OK"),
    "failed":   ("#ef9a9a", "ERR"),
}


class SeqInterpTab(QWidget):
    """Full sequence interpreter panel, embeddable as a QTabWidget tab."""

    # Emitted after a successful interpretation so other tabs can react.
    seq_loaded = Signal(str)
    # Emitted after export with the info dict for Scanner tab.
    ready_for_scan = Signal(dict)

    def __init__(
        self,
        hw_config_path: str | None = None,
        output_dir: str | None = None,
        seq_interp_url: str = "http://localhost:7475",
        parent: QWidget | None = None,
    ) -> None:
        super().__init__(parent)

        # -- application state ------------------------------------------
        self._seq_path: str | None = None
        self._hw_config_path: str | None = hw_config_path
        self._output_dir: str | None = output_dir
        self._block_rows: list = []
        self._worker = None
        self._task_id: str | None = None   # last completed service task (for Export)
        self._pending_table_select: int | None = None
        self._post_info: dict | None = None

        # HardwareConstraints holds only the default values shown in the delay
        # controls; the actual interpretation runs in the service. The controls'
        # get_overrides() is sent to the service per run.
        from src.hardware.constraints import HardwareConstraints
        self._hw = HardwareConstraints(json_path=hw_config_path)

        # -- seq_interp HTTP client (used when service is available) ----
        self._seq_interp_client = SeqInterpClient(seq_interp_url)
        self._use_service: bool = False   # set to True after successful healthcheck

        # -- build UI ---------------------------------------------------
        root_layout = QVBoxLayout(self)
        root_layout.setContentsMargins(0, 0, 0, 0)
        root_layout.setSpacing(0)
        root_layout.addWidget(self._build_button_bar())
        root_layout.addWidget(self._build_seq_status_bar())
        root_layout.addWidget(self._build_main_splitter(), stretch=1)
        root_layout.addWidget(self._build_tab_status_bar())

        self._setup_logging()

    # ================================================================== #
    #  Public API (called by LFMRIWindow / FidTab)                        #
    # ================================================================== #

    def set_hw_config(self, path: str) -> None:
        self._hw_config_path = path
        self._log(f"HW config: {path}")

    def set_output_dir(self, path: str) -> None:
        self._output_dir = path
        self._log(f"Output dir: {path}")

    def load_seq_file(self, path: str) -> None:
        """Pre-load a sequence path (e.g. generated by FidTab or opened via menu)."""
        self._seq_path = path
        name = os.path.basename(path)
        self._set_seq_state("selected", name)
        self._btn_run.setEnabled(True)
        self._log(f"Sequence loaded: {path}")

    def apply_theme(self) -> None:
        p = theme.palette()
        bar_css = f"background: {p['surface']}; border-bottom: 1px solid {p['border2']};"
        self._button_bar.setStyleSheet(f"#ButtonBar {{ {bar_css} }}")
        self._seq_status_bar.setStyleSheet(f"#SeqStatus {{ {bar_css} }}")
        self._seq_status.setStyleSheet(f"color: {p['text_dim']};")
        if hasattr(self, "_plots"):
            self._plots.apply_theme()

    def retranslate_ui(self) -> None:
        # All static buttons/groups auto-translate via TrPushButton/TrGroupBox.
        # Only _btn_blocks toggles between two keys based on state.
        blocks_visible = self._table_container.isVisible()
        self._btn_blocks.setText(i18n.tr("blocks_open") if blocks_visible else i18n.tr("blocks_closed"))

    # ================================================================== #
    #  Button bar                                                          #
    # ================================================================== #

    def _build_button_bar(self) -> QWidget:
        bar = QWidget()
        bar.setObjectName("ButtonBar")
        p = theme.palette()
        bar.setStyleSheet(
            f"#ButtonBar {{ background: {p['surface']}; border-bottom: 1px solid {p['border2']}; }}"
        )
        lay = QHBoxLayout(bar)
        lay.setContentsMargins(6, 4, 6, 4)
        lay.setSpacing(4)

        def sep() -> QFrame:
            f = QFrame()
            f.setFrameShape(QFrame.VLine)
            f.setFrameShadow(QFrame.Sunken)
            f.setFixedWidth(2)
            return f

        def btn(key: str, tip: str, slot=None, enabled: bool = True) -> TrPushButton:
            b = TrPushButton(key)
            b.setToolTip(tip)
            b.setEnabled(enabled)
            b.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
            if slot:
                b.clicked.connect(slot)
            lay.addWidget(b)
            return b

        self._btn_load_seq  = btn("btn_load_seq",    "Open Pulseq .seq file",             self._load_seq)
        self._btn_load_hw   = btn("btn_hw_config",   "Load hardware constraints JSON",     self._load_hw_config)
        self._btn_out_dir   = btn("btn_out_dir",     "Choose output directory",            self._choose_output_dir)
        lay.addWidget(sep())
        self._btn_run       = btn("btn_run",         "Run interpretation pipeline",        self._run,    enabled=False)
        self._btn_export    = btn("btn_export",      "Export all artifacts to output dir", self._export, enabled=False)
        lay.addWidget(sep())
        self._btn_fit       = btn("btn_fit_all",     "Fit all plots to data",              lambda: self._plots.fit_all())
        # _btn_blocks toggles text — keep as plain QPushButton, retranslate_ui handles it
        self._btn_blocks    = QPushButton(i18n.tr("blocks_closed"))
        self._btn_blocks.setToolTip("Toggle block table open/closed")
        self._btn_blocks.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
        self._btn_blocks.clicked.connect(self._toggle_table)
        lay.addWidget(self._btn_blocks)
        lay.addWidget(sep())
        self._btn_send_scan = btn(
            "btn_send_scanner",
            "Send exported sequence info to Scanner tab",
            self._send_to_scanner,
            enabled=False,
        )
        self._btn_send_scan.setVisible(False)

        lay.addStretch()

        self._progress = QProgressBar()
        self._progress.setRange(0, 0)
        self._progress.setFixedWidth(120)
        self._progress.setVisible(False)
        lay.addWidget(self._progress)

        self._button_bar = bar
        return bar


    def _build_seq_status_bar(self) -> QWidget:
        bar = QWidget()
        bar.setObjectName("SeqStatus")
        p = theme.palette()
        bar.setStyleSheet(
            f"#SeqStatus {{ background: {p['surface']}; border-bottom: 1px solid {p['border2']}; }}"
        )
        lay = QHBoxLayout(bar)
        lay.setContentsMargins(8, 2, 8, 2)
        self._seq_status = QLabel(f"  o  {i18n.tr('no_file_selected')}")
        self._seq_status.setFont(QFont("Arial", 9))
        self._seq_status.setStyleSheet(f"color: {theme.palette()['text_dim']};")
        lay.addWidget(self._seq_status)
        lay.addStretch()
        self._seq_status_bar = bar
        return bar

    # ================================================================== #
    #  Main three-panel splitter                                           #
    # ================================================================== #

    def _build_main_splitter(self) -> QSplitter:
        root = QSplitter(Qt.Horizontal)

        root.addWidget(self._build_left_panel())
        root.addWidget(self._build_centre_panel())

        self._preview = PreviewPanel()
        self._preview.setMinimumWidth(260)
        self._preview.setMaximumWidth(480)
        root.addWidget(self._preview)

        root.setSizes([280, 1040, 280])

        self._plots.blockClicked.connect(self._on_block_from_plot)
        self._plots.timeHovered.connect(self._on_hover)
        self._table.blockSelected.connect(self._on_block_from_table)
        self._table.blockDoubleClicked.connect(self._plots.zoom_to_block)
        self._scheme.blockClicked.connect(self._on_block_from_scheme)
        self._controls.rerun.connect(self._rerun)
        self._controls.reloadConfig.connect(self._reload_hw_config)
        self._preview.post_json_edited.connect(self._on_post_json_edited)

        return root

    def _build_left_panel(self) -> QScrollArea:
        left = QWidget()
        left.setMinimumWidth(220)
        left.setMaximumWidth(360)
        lay = QVBoxLayout(left)
        lay.setContentsMargins(4, 4, 4, 4)
        lay.setSpacing(6)

        self._meta_grp = TrGroupBox("grp_seq_metadata")
        meta_form = QFormLayout(self._meta_grp)
        meta_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
        self._meta_labels: dict[str, QLabel] = {}
        for key in [
            "Total blocks (orig)", "RF blocks", "ADC blocks", "Grad blocks",
            "RF raster (uss)", "Grad raster (uss)", "ADC raster (ns)",
            "Block raster (uss)", "RF delay (ns)", "TR delay (ns)",
            "Start delay (uss)", "Min block dur (ns)", "Gamma (MHz/T)", "RF scale",
        ]:
            lbl = QLabel("-")
            lbl.setFont(QFont("Courier New", 9))
            meta_form.addRow(QLabel(f"{key}:"), lbl)
            self._meta_labels[key] = lbl
        lay.addWidget(self._meta_grp)

        self._controls = DelayControlsPanel()
        self._controls.load_from_hw(self._hw)   # populate with default values
        lay.addWidget(self._controls)

        # Warnings are shown in PreviewPanel (right panel)
        self._warn_list = QListWidget()   # kept for _refresh_warnings compatibility

        # -- Collapsible ADC section (channels + averaging) -------------------
        lay.addWidget(self._build_adc_section())

        lay.addStretch()

        scroll = QScrollArea()
        scroll.setWidget(left)
        scroll.setWidgetResizable(True)
        scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        return scroll

    def _build_adc_section(self) -> QWidget:
        """
        Collapsible section for per-channel ADC ranges + averaging.
        Click the header button to expand / collapse.
        """
        container = QWidget()
        vlay = QVBoxLayout(container)
        vlay.setContentsMargins(0, 0, 0, 0)
        vlay.setSpacing(0)

        # -- Header button (acts as toggle) -----------------------------------
        self._adc_toggle_btn = QPushButton("▶  АЦП / Усреднение")
        self._adc_toggle_btn.setCheckable(True)
        self._adc_toggle_btn.setChecked(True)          # expanded by default
        self._adc_toggle_btn.setFlat(True)
        self._adc_toggle_btn.setStyleSheet(
            "QPushButton { text-align: left; padding: 4px 6px;"
            " font-weight: bold; font-size: 11px; border: none;"
            " background: transparent; }"
            "QPushButton:hover { background: rgba(255,255,255,0.04); }"
        )
        self._adc_toggle_btn.toggled.connect(self._on_adc_section_toggled)
        vlay.addWidget(self._adc_toggle_btn)

        # -- Content widget ---------------------------------------------------
        self._adc_content = QWidget()
        content_lay = QVBoxLayout(self._adc_content)
        content_lay.setContentsMargins(6, 2, 6, 6)
        content_lay.setSpacing(6)

        # Per-channel range form
        self._ch_range_form   = QFormLayout()
        self._ch_range_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
        self._ch_range_form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
        self._ch_range_form.setSpacing(4)
        self._ch_range_combos: list[QComboBox] = []
        content_lay.addLayout(self._ch_range_form)

        # Separator line
        sep = QFrame()
        sep.setFrameShape(QFrame.HLine)
        sep.setFrameShadow(QFrame.Sunken)
        content_lay.addWidget(sep)

        # Averaging row
        avg_row = QWidget()
        avg_lay = QHBoxLayout(avg_row)
        avg_lay.setContentsMargins(0, 0, 0, 0)
        avg_lay.setSpacing(6)

        self._avg_check = QCheckBox("Усреднение")
        self._avg_check.setToolTip("Включить усреднение (iadc.averaging)")
        self._avg_check.setChecked(False)
        self._avg_check.toggled.connect(self._on_avg_toggled)
        avg_lay.addWidget(self._avg_check)

        self._avg_spin = QSpinBox()
        self._avg_spin.setRange(1, 9999)
        self._avg_spin.setValue(1)
        self._avg_spin.setToolTip("Количество усреднений")
        self._avg_spin.setEnabled(False)
        self._avg_spin.setFixedWidth(56)
        self._avg_spin.setButtonSymbols(QSpinBox.NoButtons)
        self._avg_spin.valueChanged.connect(self._on_avg_value_changed)
        avg_lay.addWidget(self._avg_spin)
        avg_lay.addStretch()
        content_lay.addWidget(avg_row)

        vlay.addWidget(self._adc_content)

        # Initialise channels from hw_config
        self._rebuild_ch_range_combos(self._read_hw_config_channel_ranges())
        # Set initial arrow
        self._on_adc_section_toggled(True)

        return container

    def _on_adc_section_toggled(self, expanded: bool) -> None:
        self._adc_content.setVisible(expanded)
        self._adc_toggle_btn.setText(
            ("▼" if expanded else "▶") + "  АЦП / Усреднение"
        )

    def _build_centre_panel(self) -> QSplitter:
        vsplit = QSplitter(Qt.Vertical)
        self._centre_vsplit = vsplit

        self._scheme = SchemePanel()
        vsplit.addWidget(self._scheme)

        self._plots = PlotPanel()
        vsplit.addWidget(self._plots)

        self._table_container = QWidget()
        tc_lay = QVBoxLayout(self._table_container)
        tc_lay.setContentsMargins(0, 0, 0, 0)
        self._table = BlockTable()
        tc_lay.addWidget(self._table)
        self._table_container.setVisible(False)
        vsplit.addWidget(self._table_container)

        vsplit.setSizes([64, 700, 0])
        vsplit.setCollapsible(2, True)
        return vsplit

    # ================================================================== #
    #  Tab-level status bar (replaces QMainWindow.statusBar())             #
    # ================================================================== #

    def _build_tab_status_bar(self) -> QFrame:
        bar = QFrame()
        bar.setFrameShape(QFrame.StyledPanel)
        bar.setFixedHeight(22)
        lay = QHBoxLayout(bar)
        lay.setContentsMargins(6, 0, 6, 0)
        self._status_lbl = QLabel(i18n.tr("status_ready"))
        self._status_lbl.setFont(QFont("Arial", 8))
        lay.addWidget(self._status_lbl)
        lay.addStretch()
        return bar

    def _setup_logging(self) -> None:
        # __file__ is src/tabs/seq_interp_tab.py
        # parents[0] = tabs/, [1] = src/, [2] = lf_mri_gui/  -> log there
        log_dir = os.path.join(
            os.path.dirname(os.path.dirname(
                os.path.dirname(os.path.abspath(__file__))
            )),
            "log",
        )
        os.makedirs(log_dir, exist_ok=True)
        ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
        handler = logging.FileHandler(
            os.path.join(log_dir, f"gui_{ts}.log"), encoding="utf-8"
        )
        handler.setFormatter(
            logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
        )
        logging.getLogger().addHandler(handler)
        logging.getLogger().setLevel(logging.INFO)

    # ================================================================== #
    #  File / directory actions                                            #
    # ================================================================== #

    def _load_seq(self) -> None:
        path, _ = QFileDialog.getOpenFileName(
            self, "Open Pulseq sequence", "",
            "Pulseq files (*.seq);;All files (*)"
        )
        if not path:
            return
        self._seq_path = path
        name = os.path.basename(path)
        self._set_seq_state("selected", name)
        self._btn_run.setEnabled(True)
        self._log(f"Sequence selected: {path}")

    def _load_hw_config(self) -> None:
        path, _ = QFileDialog.getOpenFileName(
            self, "Open HW config", "",
            "JSON files (*.json);;All files (*)"
        )
        if not path:
            return
        self._hw_config_path = path
        self._log(f"HW config: {path}")

    def _choose_output_dir(self) -> None:
        path = QFileDialog.getExistingDirectory(self, "Choose output directory")
        if path:
            self._output_dir = path
            self._log(f"Output dir: {path}")

    def _reload_hw_config(self) -> None:
        if self._hw and self._hw_config_path:
            self._hw.load_from_json(self._hw_config_path)
            self._controls.load_from_hw(self._hw)
            self._log("HW config reloaded from file")
        elif self._hw:
            from src.hardware.constraints import HardwareConstraints
            self._hw = HardwareConstraints()
            self._controls.load_from_hw(self._hw)
            self._log("HW config reset to class defaults")

    # ================================================================== #
    #  Table toggle                                                        #
    # ================================================================== #

    def _toggle_table(self) -> None:
        visible = not self._table_container.isVisible()
        self._table_container.setVisible(visible)
        self._btn_blocks.setText(i18n.tr("blocks_open") if visible else i18n.tr("blocks_closed"))
        if visible:
            sizes = self._centre_vsplit.sizes()
            if sizes[2] < 100:
                target  = 200
                plots_h = max(100, sizes[1] - target)
                self._centre_vsplit.setSizes([sizes[0], plots_h, target])
            if self._pending_table_select is not None:
                self._table.select_by_sync_index(self._pending_table_select)
                self._pending_table_select = None

    # ================================================================== #
    #  Run / rerun / export                                                #
    # ================================================================== #

    def _run(self) -> None:
        if not self._seq_path:
            return
        name = os.path.basename(self._seq_path)
        self._set_seq_state("loading", name)

        # Pure client: interpretation always runs in the seq_interp service.
        if not self._seq_interp_client.healthcheck():
            self._set_seq_state("failed", name, "service unavailable")
            self._on_worker_error(
                "seq_interp service is unavailable. Start the service and retry."
            )
            return

        self._start_busy("Sending to seq_interp service...")
        overrides = self._controls.get_overrides() if self._controls else {}
        self._worker = SeqInterpHttpWorker(
            self._seq_interp_client, self._seq_path, hw_overrides=overrides
        )
        self._worker.progress.connect(self._log)
        self._worker.finished.connect(self._on_http_interp_finished)
        self._worker.error.connect(self._on_worker_error)
        self._worker.start()

    def _rerun(self) -> None:
        # Re-interpret via the service with the current control values.
        self._run()

    def _export(self) -> None:
        if not self._task_id:
            QMessageBox.information(
                self, "Export", "Run an interpretation first."
            )
            return
        out = self._output_dir
        if not out:
            out = QFileDialog.getExistingDirectory(self, "Choose output directory")
            if not out:
                return
            self._output_dir = out
        self._start_busy("Downloading artifacts from service...")
        self._worker = ArtifactDownloadWorker(
            self._seq_interp_client, self._task_id, out
        )
        self._worker.progress.connect(self._log)
        self._worker.finished.connect(self._on_export_finished)
        self._worker.error.connect(self._on_worker_error)
        self._worker.start()

    # ================================================================== #
    #  Worker callbacks                                                    #
    # ================================================================== #

    def _on_http_interp_finished(self, result: dict) -> None:
        """Handle result from SeqInterpHttpWorker (service mode)."""
        self._stop_busy()
        try:
            post_info = result.get("post_json", {})
            self._post_info = post_info.get("info", post_info)
            self._sync_combo_from_post_info()
            self._patch_adc_range(self._post_info)
            self._patch_averaging(self._post_info)
            xml_text = result.get("xml_text", "")
            import json as _json
            post_text = _json.dumps({"info": self._post_info}, indent=2, default=str)
            self._preview.set_xml_text(xml_text)
            self._preview.set_post_json_text(post_text)

            # ── Plots ──────────────────────────────────────────────────────
            waveforms = result.get("waveforms", {})
            if waveforms:
                self._plots.plot_from_waveforms(waveforms)

            # ── Scheme + block table ────────────────────────────────────────
            blocks_slim = result.get("blocks", [])
            if blocks_slim and waveforms.get("blocks_duration"):
                seq_like  = {"blocks": blocks_slim}
                sync_like = {k: waveforms[k]
                             for k in ("blocks_duration", "gate_adc",
                                       "gate_rf", "gate_tr_switch")
                             if k in waveforms}
                self._block_rows = build_block_rows(seq_like, sync_like)
                self._table.load_rows(self._block_rows)
                self._scheme.load_rows(self._block_rows)

            # ── Metadata labels ─────────────────────────────────────────────
            meta = result.get("metadata", {})
            _meta_map = {
                "Total blocks (orig)": meta.get("block_count",      "-"),
                "RF blocks":           meta.get("adc_count",        "-"),
                "ADC blocks":          meta.get("adc_count",        "-"),
                "RF raster (uss)":     meta.get("rf_raster_us",     "-"),
                "Grad raster (uss)":   meta.get("grad_raster_us",   "-"),
            }
            for key, val in _meta_map.items():
                if key in self._meta_labels:
                    self._meta_labels[key].setText(str(val))

            name = os.path.basename(self._seq_path or "")
            parts = [
                f"{meta.get('block_count', '?')} blocks",
                f"{meta.get('sync_block_count', '?')} sync",
                f"{meta.get('total_duration_ms', '?')} ms",
            ]
            self._set_seq_state("loaded", name, "  |  ".join(str(p) for p in parts))
            self._status_lbl.setText(
                f"{meta.get('block_count','?')} input blocks -> "
                f"{meta.get('sync_block_count','?')} sync blocks"
            )
            self._task_id = result.get("task_id")
            self._act_enabled(run=True, export=bool(self._task_id))
            if self._post_info:
                self._btn_send_scan.setVisible(True)
                self._btn_send_scan.setEnabled(True)
            if self._seq_path:
                self.seq_loaded.emit(self._seq_path)
            self._log(f"Service interpretation complete. "
                      f"Output: {result.get('output_dir', '-')}")

        except Exception as exc:
            self._on_worker_error(f"Result parse error: {exc}")

    def _on_export_finished(self, output_dir: str) -> None:
        self._stop_busy()
        self._log(f"Export complete -> {output_dir}")
        QMessageBox.information(
            self, "Export complete", f"Artifacts downloaded to:\n{output_dir}"
        )

    # -- ADC per-channel range helpers -------------------------------------

    def _read_hw_config_channel_ranges(self) -> list[int]:
        """Read channel_ranges from hw_config.json; fall back to [8, 1, 8]."""
        fallback = [8, 1, 8]
        if not self._hw_config_path:
            return fallback
        try:
            import json as _j
            with open(self._hw_config_path, encoding="utf-8") as f:
                cfg = _j.load(f)
            ranges = cfg.get("iadc", {}).get("channel_ranges", fallback)
            return [int(r) for r in ranges] if ranges else fallback
        except Exception:
            return fallback

    def _make_ch_combo(self, code: int) -> QComboBox:
        """Create a single channel range combo preset to `code`."""
        cb = QComboBox()
        for c, label in _ADC_RANGES:
            cb.addItem(label, userData=c)
        idx = next((i for i, (c, _) in enumerate(_ADC_RANGES) if c == code), 0)
        cb.setCurrentIndex(idx)
        cb.currentIndexChanged.connect(self._on_adc_range_changed)
        return cb

    def _rebuild_ch_range_combos(self, ranges: list[int]) -> None:
        """Recreate per-channel combo widgets in the collapsible ADC form."""
        while self._ch_range_form.rowCount() > 0:
            self._ch_range_form.removeRow(0)
        self._ch_range_combos.clear()
        for idx, code in enumerate(ranges):
            cb = self._make_ch_combo(code)
            cb.setToolTip(f"Динамический диапазон канала {idx}")
            self._ch_range_form.addRow(str(idx), cb)
            self._ch_range_combos.append(cb)

    def _selected_adc_ranges(self) -> list[int]:
        """Return list of selected codes, one per channel."""
        return [cb.currentData() for cb in self._ch_range_combos]

    def _patch_adc_range(self, info: dict) -> None:
        """Write per-channel ranges into info['iadc']['channel_ranges']."""
        iadc = info.get("iadc")
        if not isinstance(iadc, dict):
            return
        selected = self._selected_adc_ranges()
        current  = iadc.get("channel_ranges", [])
        # If channel count changed, extend/trim to match actual iadc
        n = len(current) if current else len(selected)
        if len(selected) >= n:
            iadc["channel_ranges"] = selected[:n]
        else:
            # More channels in iadc than combos — fill extra with last selection
            iadc["channel_ranges"] = selected + [selected[-1]] * (n - len(selected))

    def _sync_combo_from_post_info(self) -> None:
        """Rebuild or update per-channel combos to match post_info channel_ranges."""
        if not self._post_info:
            return
        iadc   = self._post_info.get("iadc", {})
        ranges = iadc.get("channel_ranges", [])
        if not ranges:
            return
        codes = [int(r) for r in ranges]
        if len(codes) != len(self._ch_range_combos):
            # Channel count changed — rebuild
            self._rebuild_ch_range_combos(codes)
        else:
            for cb, code in zip(self._ch_range_combos, codes):
                idx = next((i for i, (c, _) in enumerate(_ADC_RANGES) if c == code), 0)
                cb.blockSignals(True)
                cb.setCurrentIndex(idx)
                cb.blockSignals(False)

    def _on_adc_range_changed(self) -> None:
        if self._post_info:
            self._patch_adc_range(self._post_info)
            self._refresh_post_json_preview()

    # -- Averaging helpers --------------------------------------------------

    def _selected_averaging(self) -> int:
        """Return the averaging value to write: 1 if disabled, N if enabled."""
        return self._avg_spin.value() if self._avg_check.isChecked() else 1

    def _patch_averaging(self, info: dict) -> None:
        """Write averaging into info['iadc']['averaging'] in-place."""
        iadc = info.get("iadc")
        if isinstance(iadc, dict):
            iadc["averaging"] = self._selected_averaging()

    def _on_avg_toggled(self, checked: bool) -> None:
        self._avg_spin.setEnabled(checked)
        if self._post_info:
            self._patch_averaging(self._post_info)
            self._refresh_post_json_preview()

    def _on_avg_value_changed(self) -> None:
        if self._avg_check.isChecked() and self._post_info:
            self._patch_averaging(self._post_info)
            self._refresh_post_json_preview()

    def _refresh_post_json_preview(self) -> None:
        """Push current _post_info into the preview panel POST JSON tab."""
        if self._post_info:
            import json as _j
            self._preview.set_post_json_text(
                _j.dumps({"info": self._post_info}, indent=2, ensure_ascii=False)
            )

    def _on_post_json_edited(self, data: dict) -> None:
        """
        Called when the user edits POST JSON in the preview panel and clicks Apply.
        Updates _post_info and syncs the ADC-range / averaging controls.
        """
        # data may be {"info": {...}} or the flat info dict directly
        info = data.get("info", data) if isinstance(data, dict) else {}
        if not isinstance(info, dict):
            return
        self._post_info = info
        self._sync_combo_from_post_info()
        # Sync averaging spinbox
        avg = info.get("iadc", {}).get("averaging", 1)
        if avg and avg > 1:
            self._avg_check.blockSignals(True)
            self._avg_check.setChecked(True)
            self._avg_check.blockSignals(False)
            self._avg_spin.blockSignals(True)
            self._avg_spin.setValue(avg)
            self._avg_spin.blockSignals(False)
            self._avg_spin.setEnabled(True)
        else:
            self._avg_check.blockSignals(True)
            self._avg_check.setChecked(False)
            self._avg_check.blockSignals(False)
            self._avg_spin.setEnabled(False)
        self._btn_send_scan.setVisible(True)
        self._btn_send_scan.setEnabled(True)

    def _send_to_scanner(self) -> None:
        if self._post_info:
            self._patch_adc_range(self._post_info)
            self._patch_averaging(self._post_info)
            self.ready_for_scan.emit(self._post_info)

    def _on_worker_error(self, msg: str) -> None:
        name = os.path.basename(self._seq_path or "")
        self._set_seq_state("failed", name, msg[:80])
        self._stop_busy()
        self._log(f"ERROR: {msg}", error=True)
        self._preview.add_error(msg)
        QMessageBox.critical(self, "Error", msg)

    # ================================================================== #
    #  Results display                                                     #
    # ================================================================== #

    def _refresh_warnings(self, warnings: list[str]) -> None:
        self._warn_list.clear()
        warn_color = QColor("#ff9800" if system_is_dark() else "#e65100")
        for w in warnings:
            item = QListWidgetItem(f"WARN  {w}")
            item.setForeground(warn_color)
            self._warn_list.addItem(item)

    # ================================================================== #
    #  Block selection sync                                                #
    # ================================================================== #

    def _on_block_from_plot(self, sync_index: int) -> None:
        self._select_block(sync_index, source="plot")

    def _on_block_from_table(self, sync_index: int) -> None:
        self._select_block(sync_index, source="table")

    def _on_block_from_scheme(self, sync_index: int) -> None:
        self._select_block(sync_index, source="scheme")

    def _select_block(self, sync_index: int, source: str) -> None:
        row = self._table.row_for_sync_index(sync_index)
        self._preview.show_block_details(row)
        self._scheme.select_block(sync_index)

        if source != "plot":
            self._plots.highlight_block(sync_index)

        if self._table_container.isVisible():
            if source != "table":
                self._table.select_by_sync_index(sync_index)
        else:
            self._pending_table_select = sync_index

    # ================================================================== #
    #  Status / log helpers                                                #
    # ================================================================== #

    def _set_seq_state(self, state: str, name: str = "",
                       detail: str = "") -> None:
        _state_map = _STATE_DARK if system_is_dark() else _STATE_LIGHT
        color, icon = _state_map.get(state, ("#9e9e9e", "o"))
        text = f"  {icon}  {name}" if name else f"  {icon}  {i18n.tr('no_file_selected')}"
        if detail:
            text += f"  -  {detail}"
        self._seq_status.setStyleSheet(f"color: {color}; font-weight: bold;")
        self._seq_status.setText(text)

    def _on_hover(self, t_s: float, channel: str, value: float) -> None:
        block = find_block_at_time(t_s, self._block_rows) if self._block_rows else None
        blk = f"  block #{block.sync_index} [{block.block_type}]" if block else ""
        self._status_lbl.setText(
            f"t = {t_s * 1e6:.4f} uss   {channel} = {value:.4g}{blk}"
        )

    def _log(self, msg: str, error: bool = False) -> None:
        self._preview.append_log(msg, error=error)
        if error:
            logging.error(msg)
        else:
            logging.info(msg)

    def _start_busy(self, tip: str) -> None:
        self._progress.setVisible(True)
        self._status_lbl.setText(tip)
        self._act_enabled(run=False, export=False)

    def _stop_busy(self) -> None:
        self._progress.setVisible(False)

    def _act_enabled(self, run: bool, export: bool) -> None:
        self._btn_run.setEnabled(run and bool(self._seq_path))
        self._btn_export.setEnabled(export and self._task_id is not None)
