"""Trace plot rendering helpers for pass-through and single-trace views."""

from __future__ import annotations

from PyQt6.QtCore import Qt
import numpy as np
import pyqtgraph as pg

from python_app.models.dataset_model import ResultCollection, TraceData
from python_app.models.run_config_model import parse_combos_from_text


class AppWindowTracePlotMixin:
    """Renders pass-through trace plots on stacked magnitude/phase widgets."""

    def _show_magnitude_curves(self) -> bool:
        """Return whether magnitude curves should be rendered."""
        return self._show_magnitude_checkbox.isChecked()

    def _show_phase_curves(self) -> bool:
        """Return whether phase curves should be rendered."""
        return self._show_phase_checkbox.isChecked()

    def _pass_through_fixed_y_range(self) -> tuple[bool, float, float]:
        """Return normalized magnitude Y-range override for pass-through mode."""
        y_min = float(self._pass_through_y_min_db.value())
        y_max = float(self._pass_through_y_max_db.value())
        return bool(self._pass_through_fixed_y_enabled.isChecked()), min(y_min, y_max), max(y_min, y_max)

    def _pass_through_combo_filter(self) -> set[tuple[int, int]] | None:
        """Return selected pass-through switch combos, or `None` when all are visible."""
        text = self._pass_through_combo_filter_input.text().strip()
        if not text:
            return None
        try:
            return {
                (int(combo.input), int(combo.output))
                for combo in parse_combos_from_text(text)
            }
        except Exception as exc:  # noqa: BLE001
            self._log_warning(
                "Invalid pass-through switch-combo filter.",
                details=f"{exc}\nExpected format: input:output,input:output",
                once_key=f"pass_through_combo_filter_invalid_{text}",
            )
            return set()

    def _configure_pass_through_magnitude_axis(self, plot: pg.PlotWidget) -> None:
        """Apply pass-through magnitude-axis autorange or fixed Y window."""
        fixed_y_enabled, y_min, y_max = self._pass_through_fixed_y_range()
        view_box = plot.getViewBox()
        view_box.invertY(False)
        view_box.enableAutoRange(x=True, y=not fixed_y_enabled)
        if fixed_y_enabled:
            plot.setYRange(y_min, y_max, padding=0.0)

    def _on_trace_visibility_changed(self, *_args) -> None:
        """Redraw pass-through traces when magnitude/phase toggles changed."""
        if self._processing_mode.currentText() in {"bscan", "gpr", "legacy_gpr"}:
            return
        if self._result_history:
            self._draw_results(self._result_history[-1])
            return
        self._clear_trace_plots()

    def _clear_trace_plots(self) -> None:
        """Clear pass-through magnitude and phase plots."""
        self._trace_magnitude_plot.clear()
        self._trace_phase_plot.clear()
        self._clear_trace_legends()
        self._trace_magnitude_curves.clear()
        self._trace_phase_curves.clear()

    def _clear_trace_legends(self) -> None:
        """Remove trace plot legends to avoid stale combo-color mappings."""
        mag_legend = self._trace_magnitude_legend
        if mag_legend is not None:
            try:
                self._trace_magnitude_plot.getPlotItem().removeItem(mag_legend)
            except Exception as exc:  # noqa: BLE001
                self._log_warning(
                    f"Failed to remove pass-through magnitude legend: {type(exc).__name__}: {exc}",
                    once_key="plot_remove_magnitude_legend_failed",
                )
            self._trace_magnitude_legend = None
        self._trace_magnitude_legend_combo_keys.clear()

        phase_legend = self._trace_phase_legend
        if phase_legend is not None:
            try:
                self._trace_phase_plot.getPlotItem().removeItem(phase_legend)
            except Exception as exc:  # noqa: BLE001
                self._log_warning(
                    f"Failed to remove pass-through phase legend: {type(exc).__name__}: {exc}",
                    once_key="plot_remove_phase_legend_failed",
                )
            self._trace_phase_legend = None
        self._trace_phase_legend_combo_keys.clear()

    def _draw_trace_lines(self, collection: ResultCollection) -> bool:
        """Draw result payload traces as stacked magnitude/phase plots."""
        show_magnitude = self._show_magnitude_curves()
        show_phase = self._show_phase_curves()
        magnitude_plot = self._trace_magnitude_plot
        phase_plot = self._trace_phase_plot
        pass_through_channel = "S21"

        magnitude_plot.setVisible(show_magnitude)
        phase_plot.setVisible(show_phase)
        if not show_magnitude and not show_phase:
            self._clear_trace_plots()
            return False
        combo_filter = self._pass_through_combo_filter()

        if show_magnitude:
            mag_item = magnitude_plot.getPlotItem()
            self._configure_pass_through_magnitude_axis(magnitude_plot)
            mag_item.showAxis("left", show=True)
            mag_item.showAxis("bottom", show=not show_phase)
            magnitude_plot.setLabel("left", "Magnitude", units="dB")
            magnitude_plot.setTitle(f"Pass-Through {pass_through_channel}")
            if not show_phase:
                magnitude_plot.setLabel("bottom", "Frequency", units="Hz")

        if show_phase:
            phase_item = phase_plot.getPlotItem()
            phase_plot.getViewBox().invertY(False)
            phase_plot.getViewBox().enableAutoRange(x=True, y=False)
            phase_item.showAxis("left", show=True)
            phase_item.showAxis("bottom", show=True)
            phase_plot.setLabel("left", "Phase", units="deg")
            phase_plot.setLabel("bottom", "Frequency", units="Hz")
            phase_plot.setTitle(f"Pass-Through {pass_through_channel}")

        palette = [
            "#4cc9f0",
            "#f72585",
            "#b8f2e6",
            "#ffd166",
            "#90be6d",
            "#ff595e",
            "#6a4c93",
            "#1982c4",
        ]

        combo_colors: dict[tuple[int, int], str] = {}
        legend_source_magnitude: dict[tuple[int, int], pg.PlotCurveItem] = {}
        legend_source_phase: dict[tuple[int, int], pg.PlotCurveItem] = {}
        active_magnitude_keys: set[tuple[int, int, int, str]] = set()
        active_phase_keys: set[tuple[int, int, int, str]] = set()
        has_data = False
        x_min = np.inf
        x_max = -np.inf
        for block in collection.blocks:
            combo_key = (int(block.combo.input_pos), int(block.combo.output_pos))
            if combo_filter is not None and combo_key not in combo_filter:
                continue
            if combo_key not in combo_colors:
                combo_colors[combo_key] = palette[len(combo_colors) % len(palette)]
            color = combo_colors[combo_key]

            for payload_index, payload in enumerate(block.payloads):
                if payload.kind != 1 or payload.trace.size == 0:
                    continue
                if payload.frequency_hz.size == 0 or payload.frequency_hz.size != payload.trace.size:
                    continue
                curve_key = (
                    combo_key[0],
                    combo_key[1],
                    int(payload_index),
                    str(payload.processing_name),
                )

                local_x_min = float(np.min(payload.frequency_hz))
                local_x_max = float(np.max(payload.frequency_hz))
                x_min = min(x_min, local_x_min)
                x_max = max(x_max, local_x_max)

                if show_magnitude:
                    magnitude_values = 20.0 * np.log10(np.maximum(np.abs(payload.trace), 1e-12))
                    active_magnitude_keys.add(curve_key)
                    magnitude_curve = self._trace_magnitude_curves.get(curve_key)
                    if magnitude_curve is None:
                        magnitude_curve = pg.PlotCurveItem(pen=pg.mkPen(color, width=1.4))
                        self._trace_magnitude_curves[curve_key] = magnitude_curve
                        magnitude_plot.addItem(magnitude_curve)
                    else:
                        magnitude_curve.setPen(pg.mkPen(color, width=1.4))
                    magnitude_curve.setData(payload.frequency_hz, magnitude_values)
                    legend_source_magnitude.setdefault(combo_key, magnitude_curve)
                    has_data = True

                if show_phase:
                    active_phase_keys.add(curve_key)
                    phase_curve = self._trace_phase_curves.get(curve_key)
                    if phase_curve is None:
                        phase_curve = pg.PlotCurveItem(pen=pg.mkPen(color, width=1.2))
                        self._trace_phase_curves[curve_key] = phase_curve
                        phase_plot.addItem(phase_curve)
                    else:
                        phase_curve.setPen(pg.mkPen(color, width=1.2))
                    phase_x, phase_values = self._phase_display_arrays(payload.frequency_hz, payload.trace)
                    phase_curve.setData(phase_x, phase_values)
                    legend_source_phase.setdefault(combo_key, phase_curve)
                    has_data = True

        if show_magnitude:
            self._remove_inactive_trace_curves(
                plot=magnitude_plot,
                cache=self._trace_magnitude_curves,
                active_keys=active_magnitude_keys,
            )
        else:
            self._remove_all_trace_curves(plot=magnitude_plot, cache=self._trace_magnitude_curves)

        if show_phase:
            self._remove_inactive_trace_curves(
                plot=phase_plot,
                cache=self._trace_phase_curves,
                active_keys=active_phase_keys,
            )
        else:
            self._remove_all_trace_curves(plot=phase_plot, cache=self._trace_phase_curves)

        self._sync_trace_legends(
            show_magnitude=show_magnitude,
            show_phase=show_phase,
            magnitude_sources=legend_source_magnitude,
            phase_sources=legend_source_phase,
        )

        if has_data:
            if np.isfinite(x_min) and np.isfinite(x_max):
                if show_magnitude:
                    magnitude_plot.setXRange(x_min, x_max, padding=0.02)
                if show_phase:
                    phase_plot.setXRange(x_min, x_max, padding=0.02)
            if show_phase:
                phase_plot.setYRange(-180.0, 180.0, padding=0.02)
            if show_magnitude:
                self._configure_pass_through_magnitude_axis(magnitude_plot)
        return has_data

    @staticmethod
    def _remove_inactive_trace_curves(
        *,
        plot: pg.PlotWidget,
        cache: dict[tuple[int, int, int, str], pg.PlotCurveItem],
        active_keys: set[tuple[int, int, int, str]],
    ) -> None:
        """Delete curve items no longer present in latest result collection."""
        for key in list(cache.keys()):
            if key in active_keys:
                continue
            curve = cache.pop(key)
            plot.removeItem(curve)

    @staticmethod
    def _remove_all_trace_curves(
        *,
        plot: pg.PlotWidget,
        cache: dict[tuple[int, int, int, str], pg.PlotCurveItem],
    ) -> None:
        """Delete all cached curves from selected plot."""
        for curve in cache.values():
            plot.removeItem(curve)
        cache.clear()

    def _phase_display_arrays(self, frequency_hz: np.ndarray, trace: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
        """Return phase display arrays with decimation for faster rendering."""
        max_points = int(getattr(self, "_trace_phase_render_max_points", 1200))
        if max_points > 0 and trace.size > max_points:
            step = max(1, int(np.ceil(trace.size / max_points)))
            frequency_hz = frequency_hz[::step]
            trace = trace[::step]
        phase_values = np.arctan2(trace.imag, trace.real) * (180.0 / np.pi)
        return frequency_hz, phase_values

    def _sync_trace_legends(
        self,
        *,
        show_magnitude: bool,
        show_phase: bool,
        magnitude_sources: dict[tuple[int, int], pg.PlotCurveItem],
        phase_sources: dict[tuple[int, int], pg.PlotCurveItem],
    ) -> None:
        """Rebuild legends only when active combo set changes."""
        self._sync_single_trace_legend(
            show=show_magnitude,
            plot=self._trace_magnitude_plot,
            legend_attr="_trace_magnitude_legend",
            legend_keys_attr="_trace_magnitude_legend_combo_keys",
            sources=magnitude_sources,
        )
        self._sync_single_trace_legend(
            show=show_phase,
            plot=self._trace_phase_plot,
            legend_attr="_trace_phase_legend",
            legend_keys_attr="_trace_phase_legend_combo_keys",
            sources=phase_sources,
        )

    def _sync_single_trace_legend(
        self,
        *,
        show: bool,
        plot: pg.PlotWidget,
        legend_attr: str,
        legend_keys_attr: str,
        sources: dict[tuple[int, int], pg.PlotCurveItem],
    ) -> None:
        """Rebuild one legend from provided combo->curve mapping when needed."""
        legend = getattr(self, legend_attr)
        existing_keys = getattr(self, legend_keys_attr)
        active_keys = set(sources.keys())
        if not show or not active_keys:
            if legend is not None:
                try:
                    plot.getPlotItem().removeItem(legend)
                except Exception as exc:  # noqa: BLE001
                    self._log_warning(
                        f"Failed to clear plot legend: {type(exc).__name__}: {exc}",
                        once_key=f"{legend_attr}_clear_failed",
                    )
            setattr(self, legend_attr, None)
            existing_keys.clear()
            return

        if legend is not None and existing_keys == active_keys:
            return

        if legend is not None:
            try:
                plot.getPlotItem().removeItem(legend)
            except Exception as exc:  # noqa: BLE001
                self._log_warning(
                    f"Failed to replace plot legend: {type(exc).__name__}: {exc}",
                    once_key=f"{legend_attr}_replace_failed",
                )

        legend = plot.addLegend(offset=(8, 8))
        for combo_key in sorted(active_keys):
            curve = sources[combo_key]
            legend.addItem(curve, f"in{combo_key[0]}/out{combo_key[1]}")
        setattr(self, legend_attr, legend)
        existing_keys.clear()
        existing_keys.update(active_keys)

    def _draw_single_trace(self, trace: TraceData, title: str, *, channel: str = "s21") -> None:
        """Draw one trace on stacked magnitude/phase plots."""
        show_magnitude = self._show_magnitude_curves()
        show_phase = self._show_phase_curves()
        magnitude_plot = self._trace_magnitude_plot
        phase_plot = self._trace_phase_plot
        samples = trace.s11 if channel == "s11" else trace.s21

        magnitude_plot.setVisible(show_magnitude)
        phase_plot.setVisible(show_phase)
        self._clear_trace_plots()
        if not show_magnitude and not show_phase:
            return

        if show_magnitude:
            self._configure_pass_through_magnitude_axis(magnitude_plot)
            magnitude_plot.getPlotItem().showAxis("bottom", show=not show_phase)
            magnitude_plot.setLabel("left", "Magnitude", units="dB")
            magnitude_plot.setTitle(title)
            if not show_phase:
                magnitude_plot.setLabel("bottom", "Frequency", units="Hz")
        if show_phase:
            phase_plot.getViewBox().invertY(False)
            phase_plot.getViewBox().enableAutoRange(x=True, y=False)
            phase_plot.getPlotItem().showAxis("bottom", show=True)
            phase_plot.setLabel("left", "Phase", units="deg")
            phase_plot.setLabel("bottom", "Frequency", units="Hz")
            phase_plot.setTitle(title)

        if show_magnitude:
            magnitude_db = 20.0 * np.log10(np.maximum(np.abs(samples), 1e-12))
            magnitude_curve = pg.PlotCurveItem(
                trace.frequency_hz,
                magnitude_db,
                pen=pg.mkPen("#ffd166", width=1.8),
            )
            magnitude_plot.addItem(magnitude_curve)
            self._trace_magnitude_curves[
                (int(trace.combo.input_pos), int(trace.combo.output_pos), 0, "__single_trace__")
            ] = magnitude_curve

        if show_phase:
            phase_values = np.degrees(np.angle(samples))
            phase_curve = pg.PlotCurveItem(
                trace.frequency_hz,
                phase_values,
                pen=pg.mkPen("#80ed99", width=1.4, style=Qt.PenStyle.DashLine),
            )
            phase_plot.addItem(phase_curve)
            self._trace_phase_curves[
                (int(trace.combo.input_pos), int(trace.combo.output_pos), 0, "__single_trace__")
            ] = phase_curve
            phase_plot.setYRange(-180.0, 180.0, padding=0.02)

        if np.size(trace.frequency_hz) > 1:
            x_min = float(np.min(trace.frequency_hz))
            x_max = float(np.max(trace.frequency_hz))
            if show_magnitude:
                magnitude_plot.setXRange(x_min, x_max, padding=0.02)
                self._configure_pass_through_magnitude_axis(magnitude_plot)
            if show_phase:
                phase_plot.setXRange(x_min, x_max, padding=0.02)
