"""Dialog for preprocess set selection and sequential capture."""

from __future__ import annotations

from PyQt6.QtCore import QSignalBlocker, pyqtSignal
from PyQt6.QtWidgets import (
    QCheckBox,
    QComboBox,
    QDialog,
    QFileDialog,
    QFormLayout,
    QGridLayout,
    QGroupBox,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QPlainTextEdit,
    QPushButton,
    QScrollArea,
    QSpinBox,
    QVBoxLayout,
    QWidget,
)
import numpy as np
import pyqtgraph as pg

from python_app.models.dataset_model import TraceData
from python_app.orchestration.preprocess_assets import (
    PREPROCESS_ASSET_SPECS,
    VISIBLE_PREPROCESS_ASSET_KEYS,
    preprocess_asset_display_name,
)


class PreprocessDialog(QDialog):
    """Standalone dialog for preprocess set selection and capture workflows."""

    refresh_requested = pyqtSignal()
    selection_changed = pyqtSignal()
    radar_config_dir_changed = pyqtSignal()
    multi_radar_option_changed = pyqtSignal()
    median_sweep_count_changed = pyqtSignal()
    start_sequence_requested = pyqtSignal(str)
    capture_next_requested = pyqtSignal()
    capture_all_requested = pyqtSignal()
    undo_last_requested = pyqtSignal()
    finalize_sequence_requested = pyqtSignal()
    abort_sequence_requested = pyqtSignal()
    create_kamil_adc_neutral_sets_requested = pyqtSignal()

    def __init__(self, parent=None) -> None:
        """Initialize window metadata and compose dialog UI."""
        super().__init__(parent)
        self._set_combos: dict[str, QComboBox] = {}
        self._amplitude_plot: pg.PlotWidget | None = None
        self._phase_plot: pg.PlotWidget | None = None
        self._preview_placeholder: QLabel | None = None
        self._preview_host_layout: QHBoxLayout | None = None
        self._preview_plot_unavailable = False
        self._init_window()
        self._build_ui()

    def _init_window(self) -> None:
        """Set static window properties."""
        self.setWindowTitle("Preprocessing Setup")
        self.resize(1120, 820)

    def _build_ui(self) -> None:
        """Build root dialog layout and all sections."""
        root_layout = QVBoxLayout(self)

        scroll_area = QScrollArea(self)
        scroll_area.setWidgetResizable(True)

        content_widget = QWidget(scroll_area)
        content_layout = QVBoxLayout(content_widget)
        content_layout.addWidget(self._build_sets_group())
        content_layout.addWidget(self._build_sequence_group())
        self._build_status_line(content_layout)
        self._build_preview_plot(content_layout)
        content_layout.addStretch(1)

        scroll_area.setWidget(content_widget)
        root_layout.addWidget(scroll_area)

    def _build_sets_group(self) -> QGroupBox:
        """Build set-management controls used for preprocessing snapshots."""
        group = QGroupBox("Preprocess Sets", self)
        layout = QVBoxLayout(group)

        header_row = QHBoxLayout()
        self._set_name_input = QLineEdit("set_001", group)
        refresh_button = QPushButton("Refresh Sets", group)
        refresh_button.clicked.connect(self.refresh_requested.emit)
        self._kamil_adc_neutral_sets_button = QPushButton("Create Neutral S21 Sets", group)
        self._kamil_adc_neutral_sets_button.setToolTip(
            "Save S21 calibration=1 and S21 reference=0 for the current Kamil ADC settings."
        )
        self._kamil_adc_neutral_sets_button.clicked.connect(
            self.create_kamil_adc_neutral_sets_requested.emit
        )
        self._kamil_adc_neutral_sets_button.setVisible(False)
        header_row.addWidget(QLabel("Set name"))
        header_row.addWidget(self._set_name_input, stretch=1)
        header_row.addWidget(self._kamil_adc_neutral_sets_button)
        header_row.addWidget(refresh_button)
        layout.addLayout(header_row)
        layout.addLayout(self._build_median_sweep_row(group))
        layout.addWidget(self._build_radar_config_group(group))

        layout.addWidget(self._build_selector_group("S21", VISIBLE_PREPROCESS_ASSET_KEYS, group))
        return group

    def _build_median_sweep_row(self, parent: QGroupBox) -> QHBoxLayout:
        """Build per-combo median sweep-count selector for capture sessions."""
        row = QHBoxLayout()
        self._median_sweep_count_input = QSpinBox(parent)
        self._median_sweep_count_input.setRange(1, 99)
        self._median_sweep_count_input.setValue(5)
        self._median_sweep_count_input.setToolTip(
            "How many sweeps to capture for each combo. The per-frequency median of all "
            "sweeps is saved, suppressing single-sweep outliers (e.g. random phase glitches)."
        )
        self._median_sweep_count_input.valueChanged.connect(self.median_sweep_count_changed.emit)
        row.addWidget(QLabel("Sweeps per combo (median)"))
        row.addWidget(self._median_sweep_count_input)
        row.addStretch(1)
        return row

    def _build_radar_config_group(self, parent: QGroupBox) -> QGroupBox:
        """Build radar-config directory controls for multi-radar preprocessing."""
        group = QGroupBox("Radar Config Variants", parent)
        layout = QVBoxLayout(group)

        dir_row = QHBoxLayout()
        self._radar_config_dir_input = QLineEdit(group)
        self._radar_config_dir_input.setPlaceholderText("Directory with radar JSON configs")
        self._radar_config_dir_input.editingFinished.connect(self.radar_config_dir_changed.emit)
        browse_button = QPushButton("Browse...", group)
        browse_button.clicked.connect(self._choose_radar_config_dir)
        dir_row.addWidget(QLabel("Directory"))
        dir_row.addWidget(self._radar_config_dir_input, stretch=1)
        dir_row.addWidget(browse_button)
        layout.addLayout(dir_row)

        self._radar_config_summary_label = QLabel("No radar config directory selected.", group)
        self._radar_config_summary_label.setWordWrap(True)
        layout.addWidget(self._radar_config_summary_label)

        self._use_all_radar_configs_checkbox = QCheckBox(
            "Capture current combo for all found radar configs",
            group,
        )
        self._use_all_radar_configs_checkbox.toggled.connect(self.multi_radar_option_changed.emit)
        layout.addWidget(self._use_all_radar_configs_checkbox)
        return group

    def _build_selector_group(self, title: str, keys: tuple[str, ...], parent: QGroupBox) -> QGroupBox:
        """Build one selector subgroup for a channel family."""
        group = QGroupBox(title, parent)
        form = QFormLayout(group)
        form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)

        for key in keys:
            combo = QComboBox(group)
            combo.setPlaceholderText("<not selected>")
            combo.setCurrentIndex(-1)
            combo.currentTextChanged.connect(self._emit_selection_changed)
            self._set_combos[key] = combo
            form.addRow(self._asset_row_label(key), combo)
        return group

    def _build_sequence_group(self) -> QGroupBox:
        """Build sequential-capture controls and capture log."""
        group = QGroupBox("Sequential Capture (Fill Full N*M)", self)
        layout = QGridLayout(group)

        self._active_kind_label = QLabel("<none>", group)
        self._progress_label = QLabel("0 / 0", group)
        self._combo_label = QLabel("<none>", group)

        layout.addWidget(QLabel("Active type"), 0, 0)
        layout.addWidget(self._active_kind_label, 0, 1)
        layout.addWidget(QLabel("Progress"), 1, 0)
        layout.addWidget(self._progress_label, 1, 1)
        layout.addWidget(QLabel("Current combo"), 2, 0)
        layout.addWidget(self._combo_label, 2, 1)
        layout.addLayout(self._build_sequence_button_grid(group), 3, 0, 1, 2)
        layout.addLayout(self._build_sequence_action_row(group), 4, 0, 1, 2)

        self._capture_log = QPlainTextEdit(group)
        self._capture_log.setReadOnly(True)
        self._capture_log.setPlaceholderText("Capture history per combo")
        self._capture_log.setMinimumHeight(180)
        layout.addWidget(self._capture_log, 5, 0, 1, 2)
        return group

    def _build_sequence_button_grid(self, parent: QGroupBox) -> QGridLayout:
        """Build per-asset capture start buttons."""
        layout = QGridLayout()
        for index, key in enumerate(VISIBLE_PREPROCESS_ASSET_KEYS):
            button = QPushButton(f"Start {preprocess_asset_display_name(key)}", parent)
            button.clicked.connect(lambda _checked=False, asset_key=key: self.start_sequence_requested.emit(asset_key))
            layout.addWidget(button, index // 2, index % 2)
        return layout

    def _build_sequence_action_row(self, parent: QGroupBox) -> QHBoxLayout:
        """Build capture/abort row for active sequence control."""
        self._capture_next_button = QPushButton("Capture Current Combo", parent)
        self._capture_next_button.clicked.connect(self.capture_next_requested.emit)
        self._capture_next_button.setEnabled(False)

        self._capture_all_button = QPushButton("Capture All Remaining", parent)
        self._capture_all_button.clicked.connect(self.capture_all_requested.emit)
        self._capture_all_button.setEnabled(False)

        self._undo_last_button = QPushButton("Undo Last Capture", parent)
        self._undo_last_button.clicked.connect(self.undo_last_requested.emit)
        self._undo_last_button.setEnabled(False)

        self._save_sequence_button = QPushButton("Save Captured Set", parent)
        self._save_sequence_button.clicked.connect(self.finalize_sequence_requested.emit)
        self._save_sequence_button.setEnabled(False)

        self._abort_button = QPushButton("Abort Sequence", parent)
        self._abort_button.clicked.connect(self.abort_sequence_requested.emit)
        self._abort_button.setEnabled(False)

        layout = QHBoxLayout()
        layout.addWidget(self._capture_next_button)
        layout.addWidget(self._capture_all_button)
        layout.addWidget(self._undo_last_button)
        layout.addWidget(self._save_sequence_button)
        layout.addWidget(self._abort_button)
        layout.addStretch(1)
        return layout

    def _build_status_line(self, root_layout: QVBoxLayout) -> None:
        """Build one-line status output for dialog operations."""
        self._status_label = QLabel("Ready", self)
        root_layout.addWidget(self._status_label)

    def _build_preview_plot(self, root_layout: QVBoxLayout) -> None:
        """Build lazy preview host used after each successful capture.

        The host holds a side-by-side amplitude (left) and phase (right) plot
        pair, created lazily on first capture and replacing a placeholder
        label when the PyQtGraph build supports it.
        """
        host = QWidget(self)
        layout = QHBoxLayout(host)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(6)

        placeholder = QLabel("Preview will appear after the first successful capture.", host)
        placeholder.setWordWrap(True)
        placeholder.setMinimumHeight(320)
        layout.addWidget(placeholder)

        self._preview_host_layout = layout
        self._preview_placeholder = placeholder
        root_layout.addWidget(host)

    def set_name(self) -> str:
        """Return requested target set name."""
        return self._set_name_input.text().strip()

    def set_set_name(self, value: str) -> None:
        """Replace requested target set name."""
        self._set_name_input.setText(value)

    def radar_config_dir(self) -> str:
        """Return configured radar-config directory path."""
        return self._radar_config_dir_input.text().strip()

    def set_radar_config_dir(self, value: str) -> None:
        """Replace configured radar-config directory path."""
        self._radar_config_dir_input.setText(value)

    def use_all_radar_configs(self) -> bool:
        """Return whether multi-radar capture mode is enabled."""
        return bool(self._use_all_radar_configs_checkbox.isChecked())

    def set_use_all_radar_configs(self, enabled: bool) -> None:
        """Update multi-radar capture checkbox state."""
        self._use_all_radar_configs_checkbox.setChecked(bool(enabled))

    def median_sweep_count(self) -> int:
        """Return configured number of sweeps to median-combine per combo."""
        return max(1, int(self._median_sweep_count_input.value()))

    def set_median_sweep_count(self, count: int) -> None:
        """Update per-combo median sweep-count spinbox."""
        with QSignalBlocker(self._median_sweep_count_input):
            self._median_sweep_count_input.setValue(max(1, int(count)))

    def set_radar_config_summary(
        self,
        *,
        directory_path: str,
        json_file_count: int,
        valid_variant_count: int,
        skipped_file_count: int,
        duplicate_variant_count: int,
    ) -> None:
        """Update one-line radar-config scan summary."""
        if not directory_path:
            self._radar_config_summary_label.setText("No radar config directory selected.")
            return
        self._radar_config_summary_label.setText(
            f"Directory: {directory_path} | "
            f"json={json_file_count}, valid={valid_variant_count}, "
            f"skipped={skipped_file_count}, duplicates={duplicate_variant_count}"
        )

    def selection_snapshot(self) -> dict[str, str]:
        """Return currently selected set names keyed by preprocess asset key."""
        return {key: self._set_combos[key].currentText().strip() for key in VISIBLE_PREPROCESS_ASSET_KEYS}

    def clear_capture_log(self) -> None:
        """Clear capture history text box."""
        self._capture_log.clear()

    def set_capture_log_entries(self, entries: list[str]) -> None:
        """Replace capture history text with provided rows."""
        self._capture_log.setPlainText("\n".join(entries))

    def append_capture_log_entry(
        self,
        *,
        kind: str,
        captured_count: int,
        total_count: int,
        input_pos: int,
        output_pos: int,
        extra_details: str = "",
    ) -> None:
        """Append one capture progress row to dialog log."""
        self._capture_log.appendPlainText(
            f"{kind}: {captured_count}/{total_count} | "
            f"input={input_pos} output={output_pos}{extra_details}"
        )

    def set_capture_state(
        self,
        *,
        kind: str | None,
        captured_count: int,
        total_count: int,
        next_input: int | None,
        next_output: int | None,
        can_undo: bool,
        can_finalize: bool,
        can_capture_all: bool,
        variant_count: int = 1,
    ) -> None:
        """Update sequence progress/status widgets."""
        if kind is None:
            self._active_kind_label.setText("<none>")
            self._progress_label.setText("0 / 0")
            self._combo_label.setText("<none>")
            self._capture_next_button.setEnabled(False)
            self._capture_all_button.setEnabled(False)
            self._undo_last_button.setEnabled(False)
            self._save_sequence_button.setEnabled(False)
            self._abort_button.setEnabled(False)
            self._capture_all_button.setText("Capture All Remaining")
            return

        active_label = preprocess_asset_display_name(kind) if kind in PREPROCESS_ASSET_SPECS else kind
        self._active_kind_label.setText(active_label)
        self._progress_label.setText(f"{captured_count} / {total_count}")
        self._undo_last_button.setEnabled(bool(can_undo))
        self._save_sequence_button.setEnabled(bool(can_finalize))
        self._capture_all_button.setEnabled(bool(can_capture_all))
        self._abort_button.setEnabled(True)
        self._capture_all_button.setText("Capture All Remaining")
        if next_input is None or next_output is None:
            self._combo_label.setText("<complete>")
            self._capture_next_button.setEnabled(False)
        else:
            combo_text = f"input={next_input}, output={next_output}"
            if int(variant_count) > 1:
                combo_text += f" | radar configs={int(variant_count)}"
            self._combo_label.setText(combo_text)
            self._capture_next_button.setEnabled(True)

    def set_available_sets(self, available_sets: dict[str, list[str]]) -> None:
        """Replace combo-box choices for all preprocess assets."""
        for key in VISIBLE_PREPROCESS_ASSET_KEYS:
            combo = self._set_combos[key]
            with QSignalBlocker(combo):
                self._set_combo_items(combo, available_sets.get(key, []), combo.currentText().strip())

    def set_selected_sets(self, selected_sets: dict[str, str], *, emit_signal: bool = True) -> None:
        """Apply selected set names to all comboboxes and optionally emit update."""
        for key in VISIBLE_PREPROCESS_ASSET_KEYS:
            selected_value = selected_sets.get(key, "")
            combo = self._set_combos[key]
            with QSignalBlocker(combo):
                if not selected_value:
                    combo.setCurrentIndex(-1)
                    continue
                index = combo.findText(selected_value)
                if index < 0:
                    combo.addItem(selected_value)
                    index = combo.findText(selected_value)
                combo.setCurrentIndex(index)
        if emit_signal:
            self._emit_selection_changed()

    def set_status(self, message: str) -> None:
        """Set short human-readable status line."""
        self._status_label.setText(message)

    def set_kamil_adc_neutral_sets_visible(self, visible: bool) -> None:
        """Show Kamil ADC neutral-set shortcut only in the matching radar mode."""
        self._kamil_adc_neutral_sets_button.setVisible(bool(visible))
        self._kamil_adc_neutral_sets_button.setEnabled(bool(visible))

    def reset_preview(self) -> None:
        """Clear preview surfaces and restore default empty-state text when possible."""
        if self._amplitude_plot is not None:
            self._amplitude_plot.clear()
        if self._phase_plot is not None:
            self._phase_plot.clear()
        if self._preview_placeholder is not None:
            self._preview_placeholder.setText("Preview will appear after the first successful capture.")

    def draw_last_trace(self, trace: TraceData, title: str, *, channel: str) -> None:
        """Draw amplitude (dB) and wrapped phase (deg, ±180) panes for the latest capture."""
        samples = trace.s11 if channel == "s11" else trace.s21
        magnitude_db = 20.0 * np.log10(np.maximum(np.abs(samples), 1e-12))
        phase_deg = np.degrees(np.angle(samples))

        if self._ensure_preview_plots():
            assert self._amplitude_plot is not None
            assert self._phase_plot is not None
            self._amplitude_plot.clear()
            self._amplitude_plot.plot(
                trace.frequency_hz,
                magnitude_db,
                pen=pg.mkPen("#4cc9f0", width=1.8),
            )
            self._phase_plot.clear()
            self._phase_plot.plot(
                trace.frequency_hz,
                phase_deg,
                pen=pg.mkPen("#f48c06", width=1.8),
            )
        elif self._preview_placeholder is not None:
            self._preview_placeholder.setText(
                f"{title}\n"
                f"input={trace.combo.input}, output={trace.combo.output}, "
                f"points={trace.frequency_hz.size}\n"
                f"Preview plot is unavailable on this PyQtGraph/PyQt6 build."
            )

        combo = trace.combo
        self._status_label.setText(
            f"{title}: input={combo.input}, output={combo.output}, points={trace.frequency_hz.size}"
        )

    def _ensure_preview_plots(self) -> bool:
        """Create the amplitude+phase plot pair lazily on first successful capture."""
        if self._amplitude_plot is not None and self._phase_plot is not None:
            return True
        if self._preview_plot_unavailable:
            return False
        if self._preview_host_layout is None:
            return False

        try:
            amplitude_plot = self._build_preview_axis("Magnitude", "dB")
            phase_plot = self._build_preview_axis("Phase", "deg")
        except Exception:
            self._preview_plot_unavailable = True
            return False

        if self._preview_placeholder is not None:
            self._preview_host_layout.removeWidget(self._preview_placeholder)
            self._preview_placeholder.deleteLater()
            self._preview_placeholder = None

        self._amplitude_plot = amplitude_plot
        self._phase_plot = phase_plot
        self._preview_host_layout.addWidget(amplitude_plot, stretch=1)
        self._preview_host_layout.addWidget(phase_plot, stretch=1)
        return True

    @staticmethod
    def _build_preview_axis(left_label: str, left_units: str) -> "pg.PlotWidget":
        """Return one PlotWidget configured with the project's preview style."""
        plot = pg.PlotWidget(background="#101418", enableMenu=False)
        plot.showGrid(x=True, y=True, alpha=0.2)
        plot.setLabel("bottom", "Frequency", units="Hz")
        plot.setLabel("left", left_label, units=left_units)
        plot.setMinimumHeight(320)
        return plot

    def _emit_selection_changed(self) -> None:
        """Emit current selection snapshot change."""
        self.selection_changed.emit()

    def _choose_radar_config_dir(self) -> None:
        """Pick radar-config directory and notify controller code."""
        selected_dir = QFileDialog.getExistingDirectory(
            self,
            "Select Radar Config Directory",
            self.radar_config_dir(),
        )
        if not selected_dir:
            return
        self._radar_config_dir_input.setText(selected_dir)
        self.radar_config_dir_changed.emit()

    @staticmethod
    def _asset_row_label(key: str) -> str:
        """Return short selector label for one preprocess asset."""
        return preprocess_asset_display_name(key)

    @staticmethod
    def _set_combo_items(combo: QComboBox, names: list[str], current_text: str) -> None:
        """Replace combo contents and keep previous value when still available."""
        combo.clear()
        combo.addItems(names)
        if not current_text:
            combo.setCurrentIndex(-1)
            return
        index = combo.findText(current_text)
        if index < 0:
            combo.addItem(current_text)
            index = combo.findText(current_text)
        combo.setCurrentIndex(index)
