"""Preprocessing-set selection and sequential capture workflow mixin."""

from __future__ import annotations

from python_app.gui.preprocess_dialog import PreprocessDialog
from python_app.hardware_full.kamil_adc_service import KamilAdcService
from python_app.orchestration.preprocess_assets import (
    PREPROCESS_ASSET_SPECS,
    VISIBLE_PREPROCESS_ASSET_KEYS,
    preprocess_asset_channel,
    preprocess_asset_display_name,
)
from python_app.workflows.kamil_adc_neutral_preprocess import build_kamil_adc_neutral_s21_sets
from python_app.workflows.multi_radar_capture_workflow import (
    MultiRadarCaptureBatch,
    MultiRadarSequentialCaptureSession,
)
from python_app.workflows.radar_config_variants import scan_radar_config_variants
from python_app.workflows.reference_workflow import capture_reference_set
from python_app.workflows.sequential_capture_workflow import SequentialCaptureSession


TMP_REFERENCE_SET_NAME = "tmp_reference"


class AppWindowPreprocessMixin:
    """Handles preprocess set management and sequential capture workflows."""

    @staticmethod
    def _capture_log_entries_for_session(session: SequentialCaptureSession | MultiRadarSequentialCaptureSession) -> list[str]:
        """Build capture-log rows from current session traces."""
        display_name = preprocess_asset_display_name(session.kind)
        total_count = session.state().total_count
        if isinstance(session, MultiRadarSequentialCaptureSession):
            return [
                f"{display_name}: {index}/{total_count} | "
                f"input={batch.combo.input} output={batch.combo.output} | "
                f"radar_configs={session.radar_variant_count()}"
                for index, batch in enumerate(session.captured_batches(), start=1)
            ]

        entries: list[str] = []
        for index, trace in enumerate(session.captured_traces(), start=1):
            entries.append(
                f"{display_name}: {index}/{total_count} | "
                f"input={trace.combo.input_pos} output={trace.combo.output_pos}"
            )
        return entries

    def _available_preprocess_sets_for_radar_key(self, radar_key: str) -> dict[str, list[str]]:
        """Load available preprocess-set names for one radar key."""
        return {
            key: self._store.list_sets(PREPROCESS_ASSET_SPECS[key].set_kind, radar_key)
            for key in VISIBLE_PREPROCESS_ASSET_KEYS
        }

    def _reset_preprocess_selection_after_radar_key_change(self) -> None:
        """Clear selected preprocess sets when radar-key-defining settings change."""
        try:
            radar_key = self._radar_key_from_ui()
        except Exception:
            return

        previous_radar_key = getattr(self, "_selected_preprocess_radar_key", radar_key)
        if radar_key == previous_radar_key:
            return

        self._selected_preprocess_radar_key = radar_key
        cleared = {
            key: value
            for key, value in self._selected_preprocess_sets.items()
            if value
        }
        if not cleared:
            if self._preprocess_dialog is not None:
                self._refresh_sets()
            return

        self._selected_preprocess_sets = {
            key: ""
            for key in VISIBLE_PREPROCESS_ASSET_KEYS
        }
        self._refresh_preprocess_summary_labels()

        if self._preprocess_dialog is not None:
            self._preprocess_dialog.set_selected_sets(self._selected_preprocess_sets, emit_signal=False)
            self._refresh_sets()

        cleared_summary = ", ".join(
            f"{preprocess_asset_display_name(key)}={value}"
            for key, value in cleared.items()
        )
        self._log(
            "Preprocess selection reset after radar settings changed: "
            f"{previous_radar_key} -> {radar_key}; cleared {cleared_summary}"
        )

    def _open_preprocess_panel(self) -> None:
        """Open preprocessing dialog and refresh available sets."""
        dialog = self._ensure_preprocess_dialog()
        try:
            self._refresh_sets()
            self._update_capture_dialog_state()
        except Exception as exc:  # noqa: BLE001
            self._show_exception("Failed to open preprocessing panel", exc)
            return

        dialog.showMaximized()
        dialog.raise_()
        dialog.activateWindow()

    def _capture_tmp_reference(self) -> None:
        """Capture, save, and select a temporary S21 reference with current sweep settings."""
        if self._capture_session is not None:
            self._show_error(
                "Cannot capture tmp reference during active capture sequence",
                details=self._capture_state_details(),
            )
            return

        pipeline_was_running = self._supervisor.is_running()
        pipeline_was_paused = False

        try:
            if pipeline_was_running:
                self._log("Pipeline paused for tmp reference capture")
                self._stop_run()
                pipeline_was_paused = True

            config = self._build_config()
            radar_key = self._radar_key(config)
            self._log(
                "Tmp S21 Reference capture started: "
                f"set={TMP_REFERENCE_SET_NAME}, radar_key={radar_key}"
            )

            radar_key, collection = capture_reference_set(config, TMP_REFERENCE_SET_NAME, self._store)
            self._selected_preprocess_sets["s21_reference"] = TMP_REFERENCE_SET_NAME
            self._selected_preprocess_radar_key = radar_key
            self._processor_run_signature = None
            self._history_run_signature = None
            if self._supervisor.is_processor_running():
                self._stop_all_processes()
            self._reset_runtime_history()
            self._refresh_preprocess_summary_labels()
            if collection.traces:
                self._draw_single_trace(
                    collection.traces[-1],
                    title="Tmp S21 Reference last trace",
                    channel="s21",
                )
            if self._preprocess_dialog is not None:
                self._refresh_sets()
                self._preprocess_dialog.set_status(
                    f"S21 Reference set saved: {TMP_REFERENCE_SET_NAME} ({len(collection.traces)} traces)"
                )

            self._log(
                "Tmp S21 Reference captured and selected: "
                f"set={TMP_REFERENCE_SET_NAME}, key={radar_key}, traces={len(collection.traces)}; "
                "runtime history reset"
            )
        except Exception as exc:  # noqa: BLE001
            self._show_exception("Failed to capture tmp reference", exc)
        finally:
            if pipeline_was_paused:
                self._start_run()

    def _ensure_preprocess_dialog(self) -> PreprocessDialog:
        """Create preprocessing dialog lazily and wire its signals once."""
        if self._preprocess_dialog is not None:
            return self._preprocess_dialog

        dialog = PreprocessDialog(self)
        self._preprocess_dialog = dialog
        dialog.set_set_name(self._preprocess_set_name)
        dialog.set_radar_config_dir(self._preprocess_radar_config_dir)
        dialog.set_use_all_radar_configs(self._preprocess_use_all_radar_configs)
        dialog.set_selected_sets(self._selected_preprocess_sets, emit_signal=False)
        dialog.refresh_requested.connect(self._refresh_sets)
        dialog.selection_changed.connect(self._on_preprocess_selection_changed)
        dialog.radar_config_dir_changed.connect(self._on_preprocess_radar_config_inputs_changed)
        dialog.multi_radar_option_changed.connect(self._on_preprocess_radar_config_inputs_changed)
        dialog.start_sequence_requested.connect(self._start_capture_sequence)
        dialog.capture_next_requested.connect(self._capture_next_combo)
        dialog.capture_all_requested.connect(self._capture_all_remaining)
        dialog.undo_last_requested.connect(self._undo_last_capture)
        dialog.finalize_sequence_requested.connect(self._finalize_capture_sequence)
        dialog.abort_sequence_requested.connect(self._abort_capture_sequence)
        dialog.create_kamil_adc_neutral_sets_requested.connect(self._create_kamil_adc_neutral_sets)
        dialog.set_kamil_adc_neutral_sets_visible(self._defaults_config.is_kamil_adc)
        dialog.set_radar_config_summary(
            directory_path=self._preprocess_radar_scan_summary.directory_path,
            json_file_count=self._preprocess_radar_scan_summary.json_file_count,
            valid_variant_count=self._preprocess_radar_scan_summary.valid_variant_count,
            skipped_file_count=self._preprocess_radar_scan_summary.skipped_file_count,
            duplicate_variant_count=self._preprocess_radar_scan_summary.duplicate_variant_count,
        )
        self._update_capture_dialog_state()
        return dialog

    def _on_preprocess_radar_config_inputs_changed(self) -> None:
        """Persist preprocess radar-config directory options and refresh the scan result."""
        dialog = self._ensure_preprocess_dialog()
        self._preprocess_radar_config_dir = dialog.radar_config_dir()
        self._preprocess_use_all_radar_configs = dialog.use_all_radar_configs()
        self._refresh_sets()

    def _on_preprocess_selection_changed(self) -> None:
        """Persist selected preprocessing set names from dialog."""
        dialog = self._ensure_preprocess_dialog()
        previous_selection = dict(self._selected_preprocess_sets)
        self._selected_preprocess_sets = dialog.selection_snapshot()
        self._refresh_preprocess_summary_labels()
        changes = []
        for key in VISIBLE_PREPROCESS_ASSET_KEYS:
            previous_value = previous_selection.get(key, "")
            current_value = self._selected_preprocess_sets.get(key, "")
            if previous_value == current_value:
                continue
            changes.append(
                f"{preprocess_asset_display_name(key)}: "
                f"{previous_value or '<not selected>'} -> {current_value or '<not selected>'}"
            )
        if changes:
            self._log("Preprocess selection changed: " + "; ".join(changes))

    def _refresh_preprocess_summary_labels(self) -> None:
        """Update compact summary labels in the main window."""
        for key in VISIBLE_PREPROCESS_ASSET_KEYS:
            self._selected_preprocess_labels[key].setText(self._selected_preprocess_sets.get(key, "") or "<not selected>")

    def _refresh_sets(self) -> None:
        """Refresh preprocess set lists for current radar key."""
        radar_key = self._radar_key_from_ui()
        self._selected_preprocess_radar_key = radar_key
        dialog = self._ensure_preprocess_dialog()
        self._preprocess_radar_config_dir = dialog.radar_config_dir()
        self._preprocess_use_all_radar_configs = dialog.use_all_radar_configs()

        available_sets = self._available_preprocess_sets_for_radar_key(radar_key)
        dialog.set_available_sets(available_sets)
        self._refresh_preprocess_radar_variants()

        unavailable_selections: list[str] = []
        for key, names in available_sets.items():
            current_value = self._selected_preprocess_sets.get(key, "")
            if not current_value or current_value in names:
                continue
            unavailable_selections.append(
                f"{preprocess_asset_display_name(key)}: "
                f"{current_value} (not available for current radar key)"
            )

        dialog.set_selected_sets(self._selected_preprocess_sets)
        self._refresh_preprocess_summary_labels()
        available_counts = ", ".join(
            f"{preprocess_asset_display_name(key)}={len(names)}"
            for key, names in available_sets.items()
        )
        dialog.set_kamil_adc_neutral_sets_visible(self._defaults_config.is_kamil_adc)
        self._log(f"Preprocess set lists refreshed: radar_key={radar_key}, {available_counts}")
        if unavailable_selections:
            self._log_warning(
                "Some selected preprocess sets are not currently available for this radar key.",
                details="\n".join(unavailable_selections),
            )

    def _refresh_preprocess_radar_variants(self) -> None:
        """Reload valid radar sweep variants from the preprocess dialog directory."""
        config = self._build_config()
        variants, summary = scan_radar_config_variants(
            self._preprocess_radar_config_dir,
            base_config=config,
        )
        self._preprocess_radar_variants = variants
        self._preprocess_radar_scan_summary = summary

        if self._preprocess_dialog is not None:
            self._preprocess_dialog.set_radar_config_summary(
                directory_path=summary.directory_path,
                json_file_count=summary.json_file_count,
                valid_variant_count=summary.valid_variant_count,
                skipped_file_count=summary.skipped_file_count,
                duplicate_variant_count=summary.duplicate_variant_count,
            )

        if summary.issues:
            self._log_warning(
                "Some radar config JSON files were skipped during preprocess scan.",
                details="\n".join(summary.issues),
                once_key=f"preprocess_radar_config_scan_{summary.directory_path}",
            )

    def _start_capture_sequence(self, kind: str) -> None:
        """Start sequential capture session for requested preprocess asset."""
        if self._capture_session is not None:
            self._show_error("Another capture sequence is already active", details=self._capture_state_details())
            return

        dialog = self._ensure_preprocess_dialog()
        set_name = dialog.set_name()
        if not set_name:
            self._show_error("Set name is required")
            return

        was_running = self._supervisor.is_running()
        if was_running:
            self._log("Pipeline paused for exclusive hardware capture")
            self._stop_run()

        self._resume_pipeline_after_capture = was_running

        try:
            config = self._build_config()
            display_name = preprocess_asset_display_name(kind)
            if self._preprocess_use_all_radar_configs:
                session = self._build_multi_radar_capture_session(config=config, kind=kind, set_name=set_name)
                radar_summary = (
                    f"radar_variants={session.radar_variant_count()}, "
                    f"combos={session.state().total_count}"
                )
            else:
                session = self._build_single_radar_capture_session(config=config, kind=kind, set_name=set_name)
                radar_summary = (
                    f"radar_key={self._radar_key(config)}, "
                    f"combos={session.state().total_count}"
                )

            session.open()
            self._capture_session = session

            dialog.clear_capture_log()
            dialog.reset_preview()
            dialog.set_status(f"{display_name} sequence started")
            self._clear_trace_plots()
            self._update_capture_dialog_state()
            self._log(
                f"{display_name} sequence started: set={set_name}, {radar_summary}"
            )
        except Exception as exc:  # noqa: BLE001
            self._cleanup_capture_session()
            self._show_exception(f"Failed to start {kind} sequence", exc)
            self._resume_pipeline_if_needed()

    def _create_kamil_adc_neutral_sets(self) -> None:
        """Save neutral S21 calibration/reference sets for the current Kamil ADC settings."""
        if self._capture_session is not None:
            self._show_error(
                "Cannot create neutral sets during active capture sequence",
                details=self._capture_state_details(),
            )
            return

        dialog = self._ensure_preprocess_dialog()
        set_name = dialog.set_name()
        if not set_name:
            self._show_error("Set name is required")
            return

        pipeline_was_paused = False
        try:
            config = self._build_config()
            if not config.is_kamil_adc:
                self._show_error("Neutral S21 sets are available only for kamil_adc")
                return

            radar_key = self._radar_key(config)
            duplicate_assets = [
                preprocess_asset_display_name(key)
                for key in ("s21_calibration", "s21_reference")
                if set_name in self._store.list_sets(PREPROCESS_ASSET_SPECS[key].set_kind, radar_key)
            ]
            if duplicate_assets:
                raise RuntimeError(
                    f"Set '{set_name}' already exists for: " + ", ".join(duplicate_assets)
                )

            if self._supervisor.is_running():
                self._log("Pipeline paused for Kamil ADC neutral-set creation")
                self._stop_run()
                pipeline_was_paused = True

            point_count = self._read_kamil_adc_point_count(config)
            calibration, reference = build_kamil_adc_neutral_s21_sets(config, point_count)
            self._store.save_set("s21_calibration", radar_key, set_name, calibration)
            self._store.save_set("s21_reference", radar_key, set_name, reference)

            self._selected_preprocess_sets["s21_calibration"] = set_name
            self._selected_preprocess_sets["s21_reference"] = set_name
            self._selected_preprocess_radar_key = radar_key
            self._processor_run_signature = None
            self._history_run_signature = None
            self._reset_runtime_history()
            self._refresh_sets()
            dialog.set_status(
                f"Neutral S21 sets saved: {set_name} ({len(calibration.traces)} combos, {point_count} points)"
            )
            self._log(
                "Kamil ADC neutral S21 sets saved: "
                f"set={set_name}, radar_key={radar_key}, combos={len(calibration.traces)}, points={point_count}"
            )
        except Exception as exc:  # noqa: BLE001
            self._show_exception("Failed to create Kamil ADC neutral sets", exc)
        finally:
            if pipeline_was_paused:
                self._start_run()

    def _read_kamil_adc_point_count(self, config) -> int:
        """Read one Kamil ADC sweep and return its actual point count."""
        dialog = self._ensure_preprocess_dialog()
        dialog.set_status("Reading one Kamil ADC sweep to detect point count...")
        self._log("Reading one Kamil ADC sweep to detect neutral-set point count")

        radar = KamilAdcService(config)
        try:
            radar.open()
            radar.configure(config.radar.sweep)
            sweep = radar.acquire()
        finally:
            radar.close()

        point_count = int(sweep.x.size)
        if point_count <= 0:
            raise RuntimeError("Kamil ADC returned an empty sweep while detecting point count")
        return point_count

    def _build_single_radar_capture_session(
        self,
        *,
        config,
        kind: str,
        set_name: str,
    ) -> SequentialCaptureSession:
        """Validate and create the existing single-radar capture session."""
        radar_key = self._radar_key(config)
        existing_sets = self._store.list_sets(PREPROCESS_ASSET_SPECS[kind].set_kind, radar_key)
        display_name = preprocess_asset_display_name(kind)
        if set_name in existing_sets:
            raise RuntimeError(f"Set '{set_name}' already exists for {display_name} and cannot be overwritten")
        return SequentialCaptureSession(config=config, kind=kind, set_name=set_name)

    def _build_multi_radar_capture_session(
        self,
        *,
        config,
        kind: str,
        set_name: str,
    ) -> MultiRadarSequentialCaptureSession:
        """Validate and create the multi-radar capture session."""
        self._refresh_preprocess_radar_variants()
        if not self._preprocess_radar_variants:
            raise RuntimeError("No valid radar config variants were found in the selected directory")

        display_name = preprocess_asset_display_name(kind)
        duplicate_set_keys: list[str] = []
        for variant in self._preprocess_radar_variants:
            existing_sets = self._store.list_sets(PREPROCESS_ASSET_SPECS[kind].set_kind, variant.radar_key)
            if set_name in existing_sets:
                duplicate_set_keys.append(f"{variant.display_name} -> {variant.radar_key}")
        if duplicate_set_keys:
            raise RuntimeError(
                f"Set '{set_name}' already exists for {display_name} in these radar variants:\n"
                + "\n".join(duplicate_set_keys)
            )

        return MultiRadarSequentialCaptureSession(
            base_config=config,
            kind=kind,
            set_name=set_name,
            radar_variants=self._preprocess_radar_variants,
        )

    def _capture_next_combo(self) -> None:
        """Capture next combo in active sequential capture session."""
        session = self._capture_session
        if session is None:
            self._show_error("No active capture sequence")
            return

        try:
            capture_result = session.capture_current_combo()
        except Exception as exc:  # noqa: BLE001
            self._on_capture_combo_failed(session, exc)
            return
        self._record_preprocess_capture(session, capture_result)

    def _capture_all_remaining(self) -> None:
        """Capture all remaining combos for the active preprocess session."""
        session = self._capture_session
        if session is None:
            self._show_error("No active capture sequence")
            return
        if session.is_complete():
            self._show_error(
                "Capture sequence is already complete",
                details=self._capture_state_details(),
            )
            return

        display_name = preprocess_asset_display_name(session.kind)
        dialog = self._ensure_preprocess_dialog()
        dialog.set_status(f"{display_name} batch capture started")
        self._log(
            f"{display_name} batch capture started: remaining="
            f"{session.state().total_count - session.state().captured_count}"
        )
        while not session.is_complete():
            try:
                capture_result = session.capture_current_combo()
            except Exception as exc:  # noqa: BLE001
                self._on_capture_combo_failed(session, exc)
                return
            self._record_preprocess_capture(session, capture_result)

    def _on_capture_combo_failed(
        self,
        session: SequentialCaptureSession | MultiRadarSequentialCaptureSession,
        exc: BaseException,
    ) -> None:
        """Report a failed combo capture while preserving the session and prior captures."""
        state = session.state()
        combo = state.current_combo
        combo_text = (
            f"input={combo.input}, output={combo.output}" if combo is not None else "<unknown>"
        )
        self._show_exception(
            f"Failed to capture combo {combo_text}; previous captures kept, retry when ready",
            exc,
        )
        display_name = preprocess_asset_display_name(session.kind)
        dialog = self._ensure_preprocess_dialog()
        dialog.set_status(
            f"{display_name} capture failed at {combo_text}: "
            f"{state.captured_count}/{state.total_count} kept, ready to retry"
        )
        self._update_capture_dialog_state()

    def _record_preprocess_capture(
        self,
        session: SequentialCaptureSession | MultiRadarSequentialCaptureSession,
        capture_result,
    ) -> None:
        """Update UI, preview, and logs after one successful preprocess capture."""
        dialog = self._ensure_preprocess_dialog()
        state = session.state()
        display_name = preprocess_asset_display_name(session.kind)
        channel = preprocess_asset_channel(session.kind)
        if isinstance(session, MultiRadarSequentialCaptureSession):
            assert isinstance(capture_result, MultiRadarCaptureBatch)
            trace = capture_result.display_trace
            input_pos = capture_result.combo.input
            output_pos = capture_result.combo.output
            extra_details = f" | radar_configs={len(capture_result.traces)}"
        else:
            trace = capture_result
            input_pos = trace.combo.input_pos
            output_pos = trace.combo.output_pos
            extra_details = ""

        dialog.append_capture_log_entry(
            kind=display_name,
            captured_count=state.captured_count,
            total_count=state.total_count,
            input_pos=input_pos,
            output_pos=output_pos,
            extra_details=extra_details,
        )
        dialog.draw_last_trace(trace, title=f"{display_name} captured", channel=channel)
        self._draw_single_trace(trace, title=f"{display_name} last trace", channel=channel)
        self._log(
            f"{display_name} capture: {state.captured_count}/{state.total_count} | "
            f"input={input_pos} output={output_pos}{extra_details}"
        )

        self._update_capture_dialog_state()
        if session.is_complete():
            dialog.set_status(f"{display_name} sequence complete. Review captures or save the set.")
            self._log(
                f"{display_name} sequence capture complete: "
                f"{state.captured_count}/{state.total_count}; waiting for save or undo"
            )

    def _undo_last_capture(self) -> None:
        """Remove the most recently captured combo and rewind capture cursor."""
        session = self._capture_session
        if session is None:
            self._show_error("No active capture sequence")
            return

        dialog = self._ensure_preprocess_dialog()
        try:
            removed_capture = session.undo_last_capture()
            state = session.state()
            display_name = preprocess_asset_display_name(session.kind)
            channel = preprocess_asset_channel(session.kind)
            if isinstance(session, MultiRadarSequentialCaptureSession):
                assert isinstance(removed_capture, MultiRadarCaptureBatch)
                removed_input = removed_capture.combo.input
                removed_output = removed_capture.combo.output
                extra_details = f" | radar_configs={len(removed_capture.traces)}"
            else:
                removed_input = removed_capture.combo.input_pos
                removed_output = removed_capture.combo.output_pos
                extra_details = ""

            dialog.set_capture_log_entries(self._capture_log_entries_for_session(session))
            last_trace = session.last_captured_trace()
            if last_trace is None:
                dialog.reset_preview()
                dialog.set_status(f"{display_name} last capture removed. No captured combos remain.")
                self._clear_trace_plots()
            else:
                dialog.draw_last_trace(last_trace, title=f"{display_name} last trace", channel=channel)
                dialog.set_status(f"{display_name} last capture removed. Ready to recapture.")
                self._draw_single_trace(last_trace, title=f"{display_name} last trace", channel=channel)

            self._update_capture_dialog_state()
            self._log(
                f"{display_name} undo last capture: removed input={removed_input} "
                f"output={removed_output}{extra_details}; remaining={state.captured_count}/{state.total_count}"
            )
        except Exception as exc:  # noqa: BLE001
            self._show_exception("Failed to undo last preprocess capture", exc)

    def _finalize_capture_sequence(self) -> None:
        """Persist completed capture session into preprocess-set storage."""
        session = self._capture_session
        if session is None:
            self._show_error("No active capture sequence")
            return
        if not session.is_complete():
            self._show_error(
                "Capture sequence is not complete",
                details=(
                    f"Captured {session.state().captured_count}/{session.state().total_count} combos. "
                    "Finish the remaining captures before saving."
                ),
            )
            return

        dialog = self._ensure_preprocess_dialog()
        try:
            set_name = session.set_name
            kind = session.kind
            display_name = preprocess_asset_display_name(kind)
            if isinstance(session, MultiRadarSequentialCaptureSession):
                saved_sets = session.finalize(self._store)
            else:
                radar_key, collection = session.finalize(self._store)
            self._cleanup_capture_session()

            self._selected_preprocess_sets[kind] = set_name
            self._refresh_sets()
            if isinstance(session, MultiRadarSequentialCaptureSession):
                assert isinstance(saved_sets, list)
                dialog.set_status(
                    f"{display_name} set saved: {set_name} ({len(saved_sets)} radar variants)"
                )
                saved_summary = ", ".join(
                    f"{saved.display_name}:{saved.trace_count}"
                    for saved in saved_sets
                )
                self._log(
                    f"{display_name} sequence completed and saved: set={set_name}, "
                    f"radar_variants={len(saved_sets)} [{saved_summary}]"
                )
            else:
                dialog.set_status(f"{display_name} set saved: {set_name} ({len(collection.traces)} traces)")
                self._log(f"{display_name} sequence completed and saved: set={set_name}, key={radar_key}")
            self._resume_pipeline_if_needed()
        except Exception as exc:  # noqa: BLE001
            self._show_exception("Failed to save preprocess set", exc)

    def _abort_capture_sequence(self, *, resume_pipeline: bool = True) -> None:
        """Abort active capture session and optionally resume pipeline."""
        if self._capture_session is None:
            return

        display_name = preprocess_asset_display_name(self._capture_session.kind)
        self._cleanup_capture_session()
        dialog = self._ensure_preprocess_dialog()
        dialog.set_status(f"{display_name} sequence aborted")
        self._log(f"{display_name} sequence aborted")
        if resume_pipeline:
            self._resume_pipeline_if_needed()

    def _update_capture_dialog_state(self) -> None:
        """Sync dialog state widgets with active capture session."""
        if self._preprocess_dialog is None:
            return

        if self._capture_session is None:
            self._preprocess_dialog.set_capture_state(
                kind=None,
                captured_count=0,
                total_count=0,
                next_input=None,
                next_output=None,
                can_undo=False,
                can_finalize=False,
                can_capture_all=False,
                variant_count=1,
            )
            return

        state = self._capture_session.state()
        next_input = None
        next_output = None
        if state.current_combo is not None:
            next_input = state.current_combo.input
            next_output = state.current_combo.output

        self._preprocess_dialog.set_capture_state(
            kind=state.kind,
            captured_count=state.captured_count,
            total_count=state.total_count,
            next_input=next_input,
            next_output=next_output,
            can_undo=state.can_undo,
            can_finalize=state.is_complete,
            can_capture_all=(
                state.supports_batch_capture
                and not state.is_complete
                and state.current_combo is not None
            ),
            variant_count=state.variant_count,
        )

    def _cleanup_capture_session(self) -> None:
        """Close and clear current capture session object."""
        if self._capture_session is not None:
            self._capture_session.close()
            self._capture_session = None
        self._update_capture_dialog_state()

    def _resume_pipeline_if_needed(self) -> None:
        """Resume acquisition pipeline if it was paused for capture session."""
        should_resume = self._resume_pipeline_after_capture
        self._resume_pipeline_after_capture = False
        if not should_resume:
            return

        try:
            self._start_run()
        except Exception as exc:  # noqa: BLE001
            self._show_exception("Failed to resume pipeline after capture", exc)
