"""Live-processing bindings and runtime reactions for the main window."""

from __future__ import annotations

from PyQt6.QtCore import QSignalBlocker

from python_app.gui.runtime.constraints import validate_processing_mode_constraints
from python_app.orchestration.live_processing_config import ProcessingLiveConfig


class AppWindowLiveProcessingMixin:
    """Handle live processing updates, redraws, and locator republishing."""

    @staticmethod
    def _is_gpr_processing_mode(mode: str) -> bool:
        """Return whether a mode uses GPR result payloads and plot surface."""
        return mode in {"gpr", "legacy_gpr"}

    def _validate_processing_mode_selection(self, mode: str) -> None:
        """Validate requested processing mode against current stable/live GUI state."""
        validate_processing_mode_constraints(
            mode,
            self._build_config(),
            self._live_processing_config(),
        )

    def _live_processing_config(
        self,
        *,
        history_command: str = "none",
        reprocess_current_result: bool = True,
    ) -> ProcessingLiveConfig:
        """Build live processing config from current processing widgets."""
        self._sync_bscan_frequency_limits_with_radar()
        self._sync_gpr_frequency_limits_with_radar()
        mode = self._processing_mode.currentText()
        if mode == "legacy_gpr":
            gpr_input_positions_text = self._legacy_gpr_input_positions_input.text()
            gpr_output_positions_text = self._legacy_gpr_output_positions_input.text()
            gpr_min_depth_m = float(self._legacy_gpr_min_depth_m.value())
            gpr_max_depth_m = float(self._legacy_gpr_max_depth_m.value())
            gpr_start_freq_mhz = float(self._legacy_gpr_start_freq_mhz.value())
            gpr_stop_freq_mhz = float(self._legacy_gpr_stop_freq_mhz.value())
            gpr_background_enabled = bool(self._legacy_gpr_background_subtract_enabled.isChecked())
            gpr_background_mean_count = int(self._legacy_gpr_background_mean_count.value())
        else:
            gpr_input_positions_text = self._gpr_input_positions_input.text()
            gpr_output_positions_text = self._gpr_output_positions_input.text()
            gpr_min_depth_m = float(self._gpr_min_depth_m.value())
            gpr_max_depth_m = float(self._gpr_max_depth_m.value())
            gpr_start_freq_mhz = float(self._gpr_start_freq_mhz.value())
            gpr_stop_freq_mhz = float(self._gpr_stop_freq_mhz.value())
            gpr_background_enabled = bool(self._gpr_background_subtract_enabled.isChecked())
            gpr_background_mean_count = int(self._gpr_background_mean_count.value())

        y_min_db = float(self._pass_through_y_min_db.value())
        y_max_db = float(self._pass_through_y_max_db.value())
        return ProcessingLiveConfig(
            processor_mode=mode,
            pass_through_channel="s21",
            pass_through_fixed_y_enabled=bool(self._pass_through_fixed_y_enabled.isChecked()),
            pass_through_y_min_db=min(y_min_db, y_max_db),
            pass_through_y_max_db=max(y_min_db, y_max_db),
            bscan_axis=self._bscan_axis.currentText(),
            bscan_channel="s21",
            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()),
            legacy_gpr_mode=self._legacy_gpr_config_mode.currentText(),
            gpr_input_positions=self._parse_csv_int_list(gpr_input_positions_text),
            gpr_output_positions=self._parse_csv_int_list(gpr_output_positions_text),
            gpr_min_depth_m=gpr_min_depth_m,
            gpr_max_depth_m=gpr_max_depth_m,
            gpr_range_comp_power=float(self._gpr_range_comp_power.value()),
            gpr_angle_comp_power=float(self._gpr_angle_comp_power.value()),
            gpr_comp_power=float(self._legacy_gpr_comp_power.value()),
            gpr_score_mode=self._gpr_score_mode.currentText(),
            gpr_max_detected_objects_to_draw=int(self._gpr_max_detected_objects_to_draw.value()),
            gpr_draw_top_m_objects=int(self._gpr_draw_top_m_objects.value()),
            gpr_speed_m_s=float(self._legacy_gpr_speed_m_s.value()),
            gpr_look_angle_deg=float(self._legacy_gpr_look_angle_deg.value()),
            gpr_snr_thresh=float(self._legacy_gpr_snr_thresh.value()),
            gpr_snr_comp_max=float(self._legacy_gpr_snr_comp_max.value()),
            gpr_start_freq_mhz=gpr_start_freq_mhz,
            gpr_stop_freq_mhz=gpr_stop_freq_mhz,
            gpr_background_subtract_enabled=gpr_background_enabled,
            gpr_background_mean_count=gpr_background_mean_count,
            gpr_remove_sidelobe_objects_enabled=bool(self._gpr_remove_sidelobe_objects_enabled.isChecked()),
            gpr_imaging_plane_y_m=float(self._gpr_imaging_plane_y_m.value()),
            reprocess_current_result=bool(reprocess_current_result),
            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,
        reprocess_current_result: bool = True,
    ) -> 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,
                reprocess_current_result=reprocess_current_result,
            )
        )

    def _on_processing_live_settings_changed(self, *_args) -> None:
        """Handle live-processing setting changes and trigger redraw when needed."""
        try:
            current_mode = self._processing_mode.currentText()
            if self._is_gpr_processing_mode(current_mode):
                self._drain_results_until_quiet(timeout_s=0.05, poll_s=0.005)

            self._write_live_processing_config()
            if current_mode == "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._is_gpr_processing_mode(current_mode):
                latest = self._drain_results_until_quiet(timeout_s=0.8, poll_s=0.02)
                collection = latest
                if collection is None and self._result_history:
                    collection = self._result_history[-1]
                if collection is not None:
                    if not self._draw_results(collection):
                        self._clear_gpr_plot()
                else:
                    self._clear_gpr_plot()
            elif self._result_history:
                self._draw_results(self._result_history[-1])
            else:
                self._clear_trace_plots()
        except Exception as exc:  # noqa: BLE001
            self._show_exception("Failed to update live processing settings", exc)

    def _on_gpr_visual_settings_changed(self, *_args) -> None:
        """Redraw current GPR result using updated GUI-only render settings."""
        if not self._is_gpr_processing_mode(self._processing_mode.currentText()):
            return
        try:
            if self._result_history and self._draw_results(self._result_history[-1]):
                return
            self._clear_gpr_plot()
        except Exception as exc:  # noqa: BLE001
            self._show_exception("Failed to update GPR render settings", exc)

    def _on_gpr_locator_threshold_changed(self, *_args) -> None:
        """Redraw GPR view and republish locator snapshot after threshold changes."""
        self._on_gpr_visual_settings_changed()
        if not self._is_gpr_processing_mode(self._processing_mode.currentText()):
            return
        try:
            self._publish_locator_snapshot_from_latest_result()
        except Exception as exc:  # noqa: BLE001
            self._show_exception("Failed to update locator GPR threshold", exc)

    def _on_gpr_locator_window_changed(self, *_args) -> None:
        """Redraw GPR view and republish locator snapshot after visible X/Z changes."""
        self._on_gpr_visual_settings_changed()
        if not self._is_gpr_processing_mode(self._processing_mode.currentText()):
            return
        try:
            self._publish_locator_snapshot_from_latest_result()
        except Exception as exc:  # noqa: BLE001
            self._show_exception("Failed to update locator GPR window", exc)

    def _set_processing_mode_page(self, mode: str) -> None:
        """Show the parameter page for `mode` without changing runtime state."""
        mode_to_page = {
            "pass_through": 0,
            "bscan": 1,
            "gpr": 2,
            "legacy_gpr": 3,
        }
        self._processing_mode_pages.setCurrentIndex(mode_to_page.get(mode, 0))
        self._gpr_common_page.setVisible(self._is_gpr_processing_mode(mode))
        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()

    def _on_processing_mode_changed(self, mode: str) -> None:
        """Switch processing parameter page and refresh corresponding visualization."""
        previous_mode = getattr(self, "_active_processing_mode", "pass_through")
        if mode != previous_mode:
            try:
                self._validate_processing_mode_selection(mode)
            except Exception as exc:  # noqa: BLE001
                with QSignalBlocker(self._processing_mode):
                    self._set_combo_current_text(self._processing_mode, previous_mode)
                self._show_error(
                    f"Cannot switch processing mode to {mode}",
                    details=str(exc) or type(exc).__name__,
                )
                return

        self._active_processing_mode = mode
        self._set_plot_mode(mode)
        self._set_processing_mode_page(mode)
        self._on_processing_live_settings_changed()
        if self._is_gpr_processing_mode(mode):
            self._publish_locator_snapshot_from_latest_result()
        elif self._is_gpr_processing_mode(previous_mode) and self._locator_service is not None:
            self._locator_service.publish_empty()
        if mode == "pass_through":
            self._log(
                "Processing mode selected: pass_through "
                f"(show_magnitude={self._show_magnitude_checkbox.isChecked()}, "
                f"show_phase={self._show_phase_checkbox.isChecked()}, "
                f"combos={self._pass_through_combo_filter_input.text().strip() or '<all>'}, "
                f"fixed_y={self._pass_through_fixed_y_enabled.isChecked()}, "
                f"y_range={self._pass_through_y_min_db.value():g}..{self._pass_through_y_max_db.value():g} dB)"
            )
        elif mode == "bscan":
            self._log(
                "Processing mode selected: bscan "
                f"(axis={self._bscan_axis.currentText()}, "
                f"cut={self._bscan_cut_m.value():g} m, "
                f"max_depth={self._bscan_max_depth_m.value():g} m, "
                f"gain={self._bscan_gain.value():g}, "
                f"freq={self._bscan_start_freq_mhz.value():g}..{self._bscan_stop_freq_mhz.value():g} MHz, "
                f"subtract_mean_ascan={self._bscan_subtract_mean_ascan.isChecked()})"
            )
        elif mode == "gpr":
            self._log(
                "Processing mode selected: gpr "
                f"(inputs={self._gpr_input_positions_input.text().strip() or '<all>'}, "
                f"outputs={self._gpr_output_positions_input.text().strip() or '<all>'}, "
                f"depth={self._gpr_min_depth_m.value():g}..{self._gpr_max_depth_m.value():g} m, "
                f"freq={self._gpr_start_freq_mhz.value():g}..{self._gpr_stop_freq_mhz.value():g} MHz, "
                f"range_comp={self._gpr_range_comp_power.value():g}, "
                f"angle_comp={self._gpr_angle_comp_power.value():g}, "
                f"score_mode={self._gpr_score_mode.currentText()}, "
                f"background_subtract={self._gpr_background_subtract_enabled.isChecked()}, "
                f"mean_count={self._gpr_background_mean_count.value()}, "
                f"remove_sidelobes={self._gpr_remove_sidelobe_objects_enabled.isChecked()}, "
                f"imaging_plane_y={self._gpr_imaging_plane_y_m.value():g} m, "
                f"render_mode={self._gpr_render_mode.currentText()}, "
                f"min_score={self._gpr_min_visible_score.value():g}, "
                f"max_draw={self._gpr_max_detected_objects_to_draw.value()}, "
                f"draw_top={self._gpr_draw_top_m_objects.value()})"
            )
        elif mode == "legacy_gpr":
            self._log(
                "Processing mode selected: legacy_gpr "
                f"(mode={self._legacy_gpr_config_mode.currentText()}, "
                f"inputs={self._legacy_gpr_input_positions_input.text().strip() or '<all>'}, "
                f"outputs={self._legacy_gpr_output_positions_input.text().strip() or '<all>'}, "
                f"depth={self._legacy_gpr_min_depth_m.value():g}..{self._legacy_gpr_max_depth_m.value():g} m, "
                f"freq={self._legacy_gpr_start_freq_mhz.value():g}..{self._legacy_gpr_stop_freq_mhz.value():g} MHz, "
                f"comp={self._legacy_gpr_comp_power.value():g}, "
                f"snr={self._legacy_gpr_snr_thresh.value():g}, "
                f"snr_comp_max={self._legacy_gpr_snr_comp_max.value():g}, "
                f"speed={self._legacy_gpr_speed_m_s.value():g} m/s, "
                f"look_angle={self._legacy_gpr_look_angle_deg.value():g} deg, "
                f"background_subtract={self._legacy_gpr_background_subtract_enabled.isChecked()}, "
                f"mean_count={self._legacy_gpr_background_mean_count.value()}, "
                f"render_mode={self._legacy_gpr_render_mode.currentText()}, "
                f"min_pairs={self._legacy_gpr_min_visible_pair_count.value()})"
            )

    def _clear_history_mode_caches(self) -> None:
        """Drop cached render state for pass-through, B-scan, and GPR views."""
        self._bscan_history_floor_collection_id = 0
        self._clear_bscan_plot_history()
        if hasattr(self, "_bscan_plot"):
            self._bscan_plot.clear()
            self._configure_bscan_plot_axes()
        self._clear_trace_plots()
        if hasattr(self, "_gpr_plot"):
            self._clear_gpr_plot()

    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()
                self._configure_bscan_plot_axes()
            return
        if self._is_gpr_processing_mode(self._processing_mode.currentText()):
            if self._result_history and self._draw_results(self._result_history[-1]):
                return
            self._clear_gpr_plot()
            return
        if self._result_history:
            self._draw_results(self._result_history[-1])
            return
        self._bscan_plot.clear()
        self._configure_bscan_plot_axes()
        self._clear_trace_plots()
