"""Configuration and live-processing binding mixin for the main window."""

from __future__ import annotations

from python_app.gui.runtime.history import remove_last_aligned_histories
from python_app.hardware_full.librevna_service import LibreVnaService
from python_app.models.run_config_model import ComboModel, RunConfigModel
from python_app.orchestration.config_writer import parse_combos_from_text
from python_app.orchestration.live_processing_config import ProcessingLiveConfig
from python_app.storage.npz_store import radar_key_from_config


class AppWindowConfigMixin:
    """Builds runtime config models from current UI state."""

    def _save_current_config(self) -> None:
        """Persist currently selected GUI settings into root run_config.json."""
        try:
            config = self._build_config()
            self._config_writer.write(config, self._defaults_config_path)
            self._defaults_config = config.clone()
            self._log(f"Current config saved: {self._defaults_config_path}")
        except Exception as exc:  # noqa: BLE001
            self._show_error(f"Failed to save current config: {exc}")

    def _build_config(self) -> RunConfigModel:
        """Build `RunConfigModel` from current GUI widget values."""
        config = self._defaults_config.clone()

        config.radar.serial = self._serial_input.text().strip()
        config.radar.driver_mode = self._radar_mode.currentText()

        config.radar.sweep.start_hz = float(self._start_hz_input.text().strip())
        config.radar.sweep.stop_hz = float(self._stop_hz_input.text().strip())
        config.radar.sweep.points = int(self._points_input.text().strip())
        config.radar.sweep.if_bandwidth_hz = float(self._ifbw_input.text().strip())
        config.radar.sweep.power_dbm = float(self._power_input.text().strip())

        config.input_switch.driver_mode = self._input_mode.currentText()
        config.output_switch.driver_mode = self._output_mode.currentText()

        config.input_switch.driver = self._input_driver.currentText()
        config.output_switch.driver = self._output_driver.currentText()
        config.input_switch.radar_port = 2
        config.output_switch.radar_port = 1

        config.input_switch.positions = int(self._input_positions.text().strip())
        config.output_switch.positions = int(self._output_positions.text().strip())

        config.input_switch.gpio_chip = self._input_gpio_chip.text().strip()
        config.input_switch.pin_a = int(self._input_pin_a.text().strip())
        config.input_switch.pin_b = int(self._input_pin_b.text().strip())
        config.input_switch.invert_logic = self._input_invert_logic.currentText() == "true"

        config.output_switch.gpio_chip = self._output_gpio_chip.text().strip()
        config.output_switch.pin_a = int(self._output_pin_a.text().strip())
        config.output_switch.pin_b = int(self._output_pin_b.text().strip())
        config.output_switch.invert_logic = self._output_invert_logic.currentText() == "true"

        config.runtime.settling_ms = int(self._settling_ms.text().strip())
        config.runtime.processing_live_config_path = str(self._live_config_writer.path)

        combo_text = self._combos_text.text()
        config.combos = parse_combos_from_text(combo_text)
        config.ensure_combos()
        if self._switches_are_effectively_static(config):
            config.combos = [ComboModel(input=0, output=0)]

        config.preprocess.calibration_set = self._selected_calibration_set
        config.preprocess.reference_set = self._selected_reference_set
        return config

    def _radar_key(self, config: RunConfigModel) -> str:
        """Build radar key used by calibration/reference storage lookup."""
        return radar_key_from_config(
            model_name=config.radar.model,
            serial=config.radar.serial,
            sweep_start_hz=config.radar.sweep.start_hz,
            sweep_stop_hz=config.radar.sweep.stop_hz,
            sweep_points=config.radar.sweep.points,
            ifbw_hz=config.radar.sweep.if_bandwidth_hz,
            power_dbm=config.radar.sweep.power_dbm,
        )

    def _live_processing_config(self, *, history_command: str = "none") -> ProcessingLiveConfig:
        """Build live processing config from current processing widgets."""
        self._sync_bscan_frequency_limits_with_radar()
        return ProcessingLiveConfig(
            processor_mode=self._processing_mode.currentText(),
            gain_db=float(self._processing_gain_db.value()),
            phase_deg=float(self._processing_phase_deg.value()),
            bscan_axis=self._bscan_axis.currentText(),
            bscan_cut_m=float(self._bscan_cut_m.value()),
            bscan_max_depth_m=float(self._bscan_max_depth_m.value()),
            bscan_gain=float(self._bscan_gain.value()),
            bscan_start_freq_mhz=float(self._bscan_start_freq_mhz.value()),
            bscan_stop_freq_mhz=float(self._bscan_stop_freq_mhz.value()),
            history_command_seq=int(self._history_command_seq),
            history_command=str(history_command),
        )

    def _write_live_processing_config(self, *, history_command: str = "none", bump_history_seq: bool = False) -> None:
        """Persist current live processing config to runtime JSON file."""
        if bump_history_seq:
            self._history_command_seq += 1
        self._live_config_writer.write(self._live_processing_config(history_command=history_command))

    def _on_processing_live_settings_changed(self, *_args) -> None:
        """Handle live-processing setting changes and trigger redraw when needed."""
        try:
            self._write_live_processing_config()
            if self._processing_mode.currentText() == "bscan":
                self._drain_results_until_quiet(timeout_s=0.25, poll_s=0.01)
                self._sync_bscan_history_from_results()
                self._draw_bscan_heatmap_from_history()
            elif self._result_history:
                self._draw_results(self._result_history[-1])
        except Exception as exc:  # noqa: BLE001
            self._show_error(f"Failed to update live processing settings: {exc}")

    def _on_processing_mode_changed(self, mode: str) -> None:
        """Switch processing parameter page and refresh corresponding visualization."""
        mode_to_page = {
            "pass_through": 0,
            "bscan": 1,
        }
        self._set_plot_mode(mode)
        self._processing_mode_pages.setCurrentIndex(mode_to_page.get(mode, 0))
        current_page = self._processing_mode_pages.currentWidget()
        if current_page is not None:
            self._processing_mode_pages.setFixedHeight(current_page.sizeHint().height())
        self._processing_mode_pages.updateGeometry()
        self._on_processing_live_settings_changed()

    def _on_bscan_clear_history_clicked(self) -> None:
        """Permanently clear all runtime histories, ring backlogs, and B-scan cache."""
        self._apply_bscan_history_deletion(remove_last_only=False)

    def _on_bscan_remove_last_sweep_clicked(self) -> None:
        """Permanently delete the latest sweep from runtime histories and rings."""
        self._apply_bscan_history_deletion(remove_last_only=True)

    def _apply_bscan_history_deletion(self, *, remove_last_only: bool) -> None:
        """Apply destructive B-scan history deletion via C++ processor history commands."""
        resume_acquisition = self._supervisor.is_running()
        history_command = "remove_last" if remove_last_only else "clear_all"
        action = "last sweep removed" if remove_last_only else "history fully cleared"
        dropped_results = 0

        try:
            if resume_acquisition:
                self._stop_run()

            if self._result_reader is not None:
                dropped_results = self._result_reader.drop_all()

            if remove_last_only:
                retained_raw, retained_pre, retained_result = remove_last_aligned_histories(
                    list(self._raw_history),
                    list(self._pre_history),
                    list(self._result_history),
                )
            else:
                retained_raw = []
                retained_pre = []
                retained_result = []
            self._replace_runtime_history(
                retained_raw=retained_raw,
                retained_pre=retained_pre,
                retained_result=retained_result,
            )
            self._clear_bscan_plot_history()

            self._write_live_processing_config(history_command=history_command, bump_history_seq=True)

            if self._supervisor.is_processor_running():
                self._drain_results_until_quiet(timeout_s=0.35, poll_s=0.01)

            self._update_history_indicator()
            self._redraw_after_history_deletion()
            if resume_acquisition:
                self._start_run()
            self._log(f"B-scan {action}; dropped pending results={dropped_results}")
        except Exception as exc:  # noqa: BLE001
            self._show_error(f"Failed to delete B-scan history: {exc}")

    def _redraw_after_history_deletion(self) -> None:
        """Refresh plot immediately after destructive history deletion."""
        if self._processing_mode.currentText() == "bscan":
            if self._result_history:
                self._sync_bscan_history_from_results()
            if not self._draw_bscan_heatmap_from_history():
                self._bscan_plot.clear()
            return
        if self._result_history:
            self._draw_results(self._result_history[-1])
            return
        self._bscan_plot.clear()
        self._clear_trace_plots()

    def _on_radar_identity_changed(self, *_args) -> None:
        """Refresh device limits when radar identity/mode changes."""
        if self._radar_mode.currentText() != "native":
            self._apply_radar_limits_to_ui(None)
            return
        changed = self._refresh_radar_limits_from_device()
        if changed:
            self._on_processing_live_settings_changed()

    def _on_radar_sweep_limits_changed(self) -> None:
        """Clamp B-scan frequency bounds after sweep start/stop edits."""
        if self._sync_bscan_frequency_limits_with_radar():
            self._on_processing_live_settings_changed()

    def _refresh_radar_limits_from_device(self) -> bool:
        """Query native LibreVNA limits and apply them to GUI fields."""
        serial = self._serial_input.text().strip()
        radar_service = LibreVnaService(serial=serial or None)
        if not radar_service.driver_available:
            self._fallback_to_mock_mode("LibreVNA Python driver is not available for device limits query")
            return False

        try:
            limits = radar_service.read_device_limits()
        except Exception as exc:  # noqa: BLE001
            self._fallback_to_mock_mode(f"Failed to query LibreVNA limits: {exc}")
            return False

        return self._apply_radar_limits_to_ui(limits)

    def _fallback_to_mock_mode(self, reason: str) -> None:
        """Fallback to mock mode when native limits cannot be queried."""
        self._log(f"{reason}; switched radar mode to mock")
        if self._radar_mode.currentText() != "mock":
            was_blocked = self._radar_mode.blockSignals(True)
            self._radar_mode.setCurrentText("mock")
            self._radar_mode.blockSignals(was_blocked)
        self._apply_radar_limits_to_ui(None)

    def _apply_radar_limits_to_ui(self, limits: dict[str, float | int] | None) -> bool:
        """Apply optional radar limits and clamp dependent GUI fields."""
        if limits is None:
            self._radar_limits = None
            self._radar_start_label.setText("Start Hz")
            self._radar_stop_label.setText("Stop Hz")
            self._radar_points_label.setText("Points")
            self._radar_ifbw_label.setText("IF BW Hz")
            self._radar_power_label.setText("Stimulus Power dBm")
            if self._radar_mode.currentText() == "native":
                self._radar_limits_hint.setText("Device limits unavailable in native mode (device not connected).")
            else:
                self._radar_limits_hint.setText("Mock mode: device limits are not applied.")
            self._power_input.setToolTip("Device power limits are available only in native mode.")
            return False

        min_freq_hz = float(limits["min_frequency_hz"])
        max_freq_hz = float(limits["max_frequency_hz"])
        min_ifbw_hz = float(limits["min_ifbw_hz"])
        max_ifbw_hz = float(limits["max_ifbw_hz"])
        max_points = int(limits["max_points"])
        min_power_dbm = float(limits["min_power_dbm"])
        max_power_dbm = float(limits["max_power_dbm"])

        self._radar_limits = limits

        self._radar_start_label.setText(f"Start Hz ({min_freq_hz:g}..{max_freq_hz:g})")
        self._radar_stop_label.setText(f"Stop Hz ({min_freq_hz:g}..{max_freq_hz:g})")
        self._radar_points_label.setText(f"Points (1..{max_points:d})")
        self._radar_ifbw_label.setText(f"IF BW Hz ({min_ifbw_hz:g}..{max_ifbw_hz:g})")
        self._radar_power_label.setText(f"Stimulus Power dBm ({min_power_dbm:g}..{max_power_dbm:g})")
        self._radar_limits_hint.setText(
            f"Limits: Freq {min_freq_hz:g}..{max_freq_hz:g} Hz, Points 1..{max_points:d}, "
            f"IF BW {min_ifbw_hz:g}..{max_ifbw_hz:g} Hz, Power {min_power_dbm:g}..{max_power_dbm:g} dBm."
        )

        changed = False
        prev_start = self._start_hz_input.text().strip()
        prev_stop = self._stop_hz_input.text().strip()
        prev_points = self._points_input.text().strip()
        prev_ifbw = self._ifbw_input.text().strip()
        prev_power = self._power_input.text().strip()

        start_hz = self._clamp_line_edit_float(self._start_hz_input, min_freq_hz, max_freq_hz)
        stop_hz = self._clamp_line_edit_float(self._stop_hz_input, min_freq_hz, max_freq_hz)
        if start_hz > stop_hz:
            stop_hz = start_hz
            self._stop_hz_input.setText(f"{stop_hz:g}")
            changed = True

        points = self._clamp_line_edit_int(self._points_input, 1, max_points)
        ifbw = self._clamp_line_edit_float(self._ifbw_input, min_ifbw_hz, max_ifbw_hz)
        power = self._clamp_line_edit_float(self._power_input, min_power_dbm, max_power_dbm)
        self._power_input.setToolTip(f"Device range: {min_power_dbm:g}..{max_power_dbm:g} dBm")

        changed = (
            changed
            or prev_start != self._start_hz_input.text().strip()
            or prev_stop != self._stop_hz_input.text().strip()
            or prev_points != self._points_input.text().strip()
            or prev_ifbw != self._ifbw_input.text().strip()
            or prev_power != self._power_input.text().strip()
        )

        self._sync_bscan_frequency_limits_with_radar()
        return changed

    @staticmethod
    def _clamp_line_edit_float(widget, min_value: float, max_value: float) -> float:
        """Clamp float line-edit value to inclusive range and rewrite widget text."""
        try:
            value = float(widget.text().strip())
        except ValueError:
            value = min_value
        value = min(max(value, min_value), max_value)
        widget.setText(f"{value:g}")
        return value

    @staticmethod
    def _clamp_line_edit_int(widget, min_value: int, max_value: int) -> int:
        """Clamp integer line-edit value to inclusive range and rewrite widget text."""
        try:
            value = int(float(widget.text().strip()))
        except ValueError:
            value = min_value
        value = min(max(value, min_value), max_value)
        widget.setText(str(value))
        return value

    def _sync_bscan_frequency_limits_with_radar(self) -> bool:
        """Synchronize B-scan start/stop MHz widget ranges with radar sweep bounds."""
        required_widgets = (
            "_start_hz_input",
            "_stop_hz_input",
            "_bscan_start_freq_mhz",
            "_bscan_stop_freq_mhz",
        )
        if not all(hasattr(self, widget_name) for widget_name in required_widgets):
            # Processing callbacks can fire while UI groups are still being built.
            return False

        try:
            radar_start_hz = float(self._start_hz_input.text().strip())
            radar_stop_hz = float(self._stop_hz_input.text().strip())
        except ValueError:
            return False

        radar_min_mhz = min(radar_start_hz, radar_stop_hz) / 1_000_000.0
        radar_max_mhz = max(radar_start_hz, radar_stop_hz) / 1_000_000.0

        changed = False
        for widget in (self._bscan_start_freq_mhz, self._bscan_stop_freq_mhz):
            if widget.minimum() != radar_min_mhz or widget.maximum() != radar_max_mhz:
                changed = True
            widget.blockSignals(True)
            widget.setRange(radar_min_mhz, radar_max_mhz)
            widget.blockSignals(False)

        clamped_start_mhz = min(max(self._bscan_start_freq_mhz.value(), radar_min_mhz), radar_max_mhz)
        clamped_stop_mhz = min(max(self._bscan_stop_freq_mhz.value(), radar_min_mhz), radar_max_mhz)
        if clamped_start_mhz != self._bscan_start_freq_mhz.value():
            changed = True
            self._bscan_start_freq_mhz.blockSignals(True)
            self._bscan_start_freq_mhz.setValue(clamped_start_mhz)
            self._bscan_start_freq_mhz.blockSignals(False)
        if clamped_stop_mhz != self._bscan_stop_freq_mhz.value():
            changed = True
            self._bscan_stop_freq_mhz.blockSignals(True)
            self._bscan_stop_freq_mhz.setValue(clamped_stop_mhz)
            self._bscan_stop_freq_mhz.blockSignals(False)

        return changed

    @staticmethod
    def _switches_are_effectively_static(config: RunConfigModel) -> bool:
        """Return `True` when switch setup effectively yields one fixed combo."""
        has_single_position = config.input_switch.positions <= 1 and config.output_switch.positions <= 1
        both_mock = config.input_switch.driver_mode == "mock" and config.output_switch.driver_mode == "mock"
        return has_single_position or both_mock
