"""
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,
    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 В"),
]
_ADC_RANGE_DEFAULT_CODE = 2   # 50 мВ

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 (
    LoadInterpWorker, SyncOnlyWorker, ExportWorker, XmlPreviewWorker,
    SeqInterpHttpWorker,
)
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._seq_data: dict | None = None
        self._sync_data: dict | None = None
        self._hw = None
        self._block_rows: list = []
        self._worker = None
        self._xml_preview_worker = None
        self._pending_table_select: int | None = None
        self._post_info: dict | None = None

        # -- 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()

        # -- ADC dynamic range selector ----------------------------------------
        lay.addWidget(sep())
        _range_lbl = QLabel("АЦП диап.:")
        _range_lbl.setToolTip("Динамический диапазон АЦП (channel_ranges в iadc)")
        lay.addWidget(_range_lbl)

        self._adc_range_combo = QComboBox()
        self._adc_range_combo.setToolTip(
            "Динамический диапазон АЦП.\n"
            "Значение записывается в iadc.channel_ranges для всех каналов."
        )
        for code, label in _ADC_RANGES:
            self._adc_range_combo.addItem(label, userData=code)
        # set default
        default_idx = next(
            (i for i, (c, _) in enumerate(_ADC_RANGES) if c == _ADC_RANGE_DEFAULT_CODE),
            0,
        )
        self._adc_range_combo.setCurrentIndex(default_idx)
        self._adc_range_combo.currentIndexChanged.connect(self._on_adc_range_changed)
        lay.addWidget(self._adc_range_combo)

        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._scheme.blockClicked.connect(self._on_block_from_scheme)
        self._controls.rerun.connect(self._rerun)
        self._controls.reloadConfig.connect(self._reload_hw_config)

        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()
        lay.addWidget(self._controls)

        self._warn_grp = TrGroupBox("grp_warnings")
        warn_lay = QVBoxLayout(self._warn_grp)
        self._warn_list = QListWidget()
        self._warn_list.setFont(QFont("Arial", 9))
        self._warn_list.setMaximumHeight(110)
        self._warn_list.setStyleSheet(
            "QListWidget { color: #ddddff; background: #16162a; border: 1px solid #2a2a4a; } "
            "QListWidget::item { padding: 2px; }"
        )
        warn_lay.addWidget(self._warn_list)
        lay.addWidget(self._warn_grp)

        lay.addStretch()

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

    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)

        # Try HTTP service first; fall back to local pipeline if unavailable.
        if self._seq_interp_client.healthcheck():
            self._use_service = True
            self._start_busy("Sending to seq_interp service...")
            self._worker = SeqInterpHttpWorker(
                self._seq_interp_client, self._seq_path
            )
            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()
        else:
            self._use_service = False
            self._start_busy("Loading and interpreting (local)...")
            overrides = self._controls.get_overrides() if self._hw else {}
            self._worker = LoadInterpWorker(
                self._seq_path,
                hw_config_path=self._hw_config_path,
                hw_overrides=overrides,
            )
            self._worker.log_msg.connect(self._log)
            self._worker.finished.connect(self._on_interp_finished)
            self._worker.error.connect(self._on_worker_error)
            self._worker.start()

    def _rerun(self) -> None:
        if self._seq_data is None:
            self._run()
            return
        self._start_busy("Re-synchronizing...")
        self._worker = SyncOnlyWorker(
            self._seq_data,
            hw_config_path=self._hw_config_path,
            hw_overrides=self._controls.get_overrides(),
        )
        self._worker.log_msg.connect(self._log)
        self._worker.finished.connect(self._on_sync_finished)
        self._worker.error.connect(self._on_worker_error)
        self._worker.start()

    def _export(self) -> None:
        if self._seq_data is None or self._sync_data is None:
            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("Exporting artifacts...")
        self._worker = ExportWorker(
            self._seq_data, self._sync_data, self._hw, out
        )
        self._worker.log_msg.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()   # reflect hw_config range in combo
            self._patch_adc_range(self._post_info)  # then apply current selection
            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)

            meta = result.get("metadata", {})
            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._act_enabled(run=True, export=False)
            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_interp_finished(self, seq_data: dict, sync_data: dict, hw) -> None:
        self._seq_data  = seq_data
        self._sync_data = sync_data
        self._hw        = hw
        self._stop_busy()
        self._apply_results(seq_data, sync_data, hw)
        if self._seq_path:
            self.seq_loaded.emit(self._seq_path)

    def _on_sync_finished(self, sync_data: dict, hw) -> None:
        self._sync_data = sync_data
        self._hw        = hw
        self._stop_busy()
        self._apply_results(self._seq_data, sync_data, hw)

    def _on_export_finished(self, output_dir: str, xml_text: str,
                             post_text: str) -> None:
        self._stop_busy()
        self._preview.set_xml_text(xml_text)
        self._preview.set_post_json_text(post_text)
        self._log(f"Export complete -> {output_dir}")
        try:
            import json as _json
            payload = _json.loads(post_text)
            self._post_info = payload.get("info", payload)
            self._sync_combo_from_post_info()
            self._patch_adc_range(self._post_info)
        except Exception:
            self._post_info = None
        if self._post_info:
            self._btn_send_scan.setVisible(True)
            self._btn_send_scan.setEnabled(True)
        QMessageBox.information(
            self, "Export complete", f"Artifacts written to:\n{output_dir}"
        )

    # -- ADC range helpers -------------------------------------------------

    def _selected_adc_code(self) -> int:
        """Return the currently selected ADC range code (0–11)."""
        return self._adc_range_combo.currentData()

    def _patch_adc_range(self, info: dict) -> None:
        """
        Overwrite channel_ranges in info['iadc'] with the selected code
        for every channel.  Operates in-place.
        """
        iadc = info.get("iadc")
        if not isinstance(iadc, dict):
            return
        code = self._selected_adc_code()
        n = len(iadc.get("channel_ranges", [])) or 1
        iadc["channel_ranges"] = [code] * n

    def _sync_combo_from_post_info(self) -> None:
        """
        After loading post_info, update the combo to reflect the first
        channel_ranges value (if it corresponds to a known code).
        """
        if not self._post_info:
            return
        iadc = self._post_info.get("iadc", {})
        ranges = iadc.get("channel_ranges", [])
        if ranges:
            code = ranges[0]
            idx = next(
                (i for i, (c, _) in enumerate(_ADC_RANGES) if c == code),
                None,
            )
            if idx is not None:
                self._adc_range_combo.blockSignals(True)
                self._adc_range_combo.setCurrentIndex(idx)
                self._adc_range_combo.blockSignals(False)

    def _on_adc_range_changed(self) -> None:
        """Live-patch _post_info when the user changes the range selector."""
        if self._post_info:
            self._patch_adc_range(self._post_info)

    def _send_to_scanner(self) -> None:
        if self._post_info:
            self._patch_adc_range(self._post_info)   # ensure latest selection applied
            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 _apply_results(self, seq_data: dict, sync_data: dict, hw) -> None:
        meta = seq_metadata(seq_data, hw)
        for key, lbl in self._meta_labels.items():
            lbl.setText(str(meta.get(key, "-")))

        if not self._controls._defaults:
            self._controls.load_from_hw(hw)

        warnings = validate_timing(hw, seq_data, sync_data)
        self._refresh_warnings(warnings)
        self._preview.set_warnings(warnings)

        self._block_rows = build_block_rows(seq_data, sync_data)
        self._table.load_rows(self._block_rows)
        self._scheme.load_rows(self._block_rows)

        self._plots.plot_all(seq_data, sync_data)

        pw = XmlPreviewWorker(sync_data, hw)
        pw.finished.connect(
            lambda xml, post: (
                self._preview.set_xml_text(xml),
                self._preview.set_post_json_text(post),
            )
        )
        pw.error.connect(lambda e: self._log(f"XML preview: {e}", error=True))
        pw.start()
        self._xml_preview_worker = pw

        blocks = seq_data.get("blocks", [])
        total_s = sum(sync_data.get("blocks_duration", []))
        parts = [f"{len(blocks)} blocks"]
        if any("RF"   in b.get("type", []) for b in blocks):
            parts.append("RF")
        if any(b.get("has_adc") for b in blocks):
            parts.append("ADC")
        if any("GRAD" in b.get("type", []) for b in blocks):
            parts.append("Grad")
        if total_s >= 1e-3:
            parts.append(f"{total_s * 1e3:.2f} ms")
        else:
            parts.append(f"{total_s * 1e6:.1f} uss")
        name = os.path.basename(self._seq_path or "")
        self._set_seq_state("loaded", name, "  |  ".join(parts))

        self._act_enabled(run=True, export=True)
        self._status_lbl.setText(
            f"{len(blocks)} input blocks -> {sync_data['number_of_blocks']} sync blocks"
        )

    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._seq_data is not None)
