"""In-app front end for ``tools/gradiometer_parser.py``.

The dialog lets a user of the packaged executable compute gradiometer
difference channels from a fields-only recording without needing a Python
installation; it calls the very same functions the standalone script uses.
"""

from __future__ import annotations

from pathlib import Path

from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtWidgets import (
    QCheckBox,
    QComboBox,
    QDialog,
    QFileDialog,
    QGridLayout,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

from tools.gradiometer_parser import (
    compute_differences,
    list_sensors,
    parse_pair,
)


class _ParseWorker(QThread):
    """Run one parse off the GUI thread so large recordings do not freeze it."""

    succeeded = pyqtSignal(int, int, str)
    failed = pyqtSignal(str)

    def __init__(
        self,
        input_path: Path,
        pairs: list[tuple[str, str]],
        output_path: Path,
        include_fields: bool,
        parent=None,
    ) -> None:
        super().__init__(parent)
        self._input = input_path
        self._pairs = pairs
        self._output = output_path
        self._include_fields = include_fields

    def run(self) -> None:
        try:
            written, skipped = compute_differences(
                self._input,
                self._pairs,
                self._output,
                include_fields=self._include_fields,
            )
        except Exception as exc:
            self.failed.emit(str(exc))
            return
        self.succeeded.emit(written, skipped, str(self._output))


class ParseRecordingDialog(QDialog):
    """Choose a recording, pick sensor pairs, and write their differences."""

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self.setObjectName("parseDialog")
        self.setWindowTitle("Parse Recording — Gradiometer Differences")
        self.setModal(False)
        self.setWindowFlags(Qt.Window)
        self.setMinimumWidth(720)
        self._worker: _ParseWorker | None = None

        layout = QVBoxLayout(self)
        layout.setContentsMargins(14, 12, 14, 12)
        layout.setSpacing(9)

        title = QLabel("PARSE RECORDING")
        title.setObjectName("plotTitle")
        layout.addWidget(title)

        intro = QLabel(
            "Compute gradiometer X − Y channels from a recorded CSV. The "
            "recording only needs the sensor field columns; every pair is "
            "an exact millisecond-timestamp match."
        )
        intro.setObjectName("controlHint")
        intro.setWordWrap(True)
        layout.addWidget(intro)

        grid = QGridLayout()
        grid.setHorizontalSpacing(9)
        grid.setVerticalSpacing(8)

        input_label = QLabel("RECORDING")
        input_label.setObjectName("fieldLabel")
        grid.addWidget(input_label, 0, 0)

        self.input_edit = QLineEdit()
        self.input_edit.setReadOnly(True)
        self.input_edit.setPlaceholderText("Choose a recorded CSV file…")
        grid.addWidget(self.input_edit, 0, 1)

        browse_input_button = QPushButton("BROWSE")
        browse_input_button.setObjectName("secondaryButton")
        browse_input_button.clicked.connect(self._browse_input)
        grid.addWidget(browse_input_button, 0, 2)

        sensors_label = QLabel("SENSORS")
        sensors_label.setObjectName("fieldLabel")
        grid.addWidget(sensors_label, 1, 0)

        self.sensors_label = QLabel("Choose a recording to see its sensors.")
        self.sensors_label.setObjectName("controlHint")
        self.sensors_label.setWordWrap(True)
        grid.addWidget(self.sensors_label, 1, 1, 1, 2)

        pairs_label = QLabel("PAIRS")
        pairs_label.setObjectName("fieldLabel")
        grid.addWidget(pairs_label, 2, 0)

        pair_row = QHBoxLayout()
        pair_row.setSpacing(7)
        self.first_combo = QComboBox()
        self.first_combo.setMinimumWidth(95)
        pair_row.addWidget(self.first_combo)
        minus = QLabel("−")
        minus.setObjectName("syncSectionTitle")
        pair_row.addWidget(minus)
        self.second_combo = QComboBox()
        self.second_combo.setMinimumWidth(95)
        pair_row.addWidget(self.second_combo)

        self.add_pair_button = QPushButton("ADD PAIR")
        self.add_pair_button.setObjectName("deviceButton")
        self.add_pair_button.clicked.connect(self._add_pair)
        pair_row.addWidget(self.add_pair_button)

        self.pairs_edit = QLineEdit()
        self.pairs_edit.setPlaceholderText("e.g. A-B A-C")
        self.pairs_edit.setToolTip(
            "Space-separated pairs; each channel is first − second. Use "
            "the selectors and ADD PAIR, or type pairs directly."
        )
        pair_row.addWidget(self.pairs_edit, 1)
        grid.addLayout(pair_row, 2, 1, 1, 2)

        output_label = QLabel("OUTPUT")
        output_label.setObjectName("fieldLabel")
        grid.addWidget(output_label, 3, 0)

        self.output_edit = QLineEdit()
        self.output_edit.setPlaceholderText("Output CSV path…")
        grid.addWidget(self.output_edit, 3, 1)

        browse_output_button = QPushButton("BROWSE")
        browse_output_button.setObjectName("secondaryButton")
        browse_output_button.clicked.connect(self._browse_output)
        grid.addWidget(browse_output_button, 3, 2)
        grid.setColumnStretch(1, 1)
        layout.addLayout(grid)

        action_row = QHBoxLayout()
        action_row.setSpacing(9)
        self.include_fields_checkbox = QCheckBox("INCLUDE SENSOR FIELDS")
        self.include_fields_checkbox.setToolTip(
            "Also copy the sensor field columns used by the pairs into the "
            "output file."
        )
        action_row.addWidget(self.include_fields_checkbox)

        self.status_label = QLabel("Choose a recording to begin.")
        self.status_label.setObjectName("controlHint")
        self.status_label.setWordWrap(True)
        action_row.addWidget(self.status_label, 1)

        self.parse_button = QPushButton("PARSE")
        self.parse_button.setObjectName("connectButton")
        self.parse_button.setEnabled(False)
        self.parse_button.clicked.connect(self._parse)
        action_row.addWidget(self.parse_button)

        close_button = QPushButton("CLOSE")
        close_button.setObjectName("secondaryButton")
        close_button.clicked.connect(self.close)
        action_row.addWidget(close_button)
        layout.addLayout(action_row)

    # ------------------------------------------------------------------ input
    def _browse_input(self) -> None:
        selected_path, _filter = QFileDialog.getOpenFileName(
            self,
            "Choose a recorded QTFM CSV",
            "",
            "CSV files (*.csv)",
        )
        if selected_path:
            self.load_recording(selected_path)

    def load_recording(self, path: str) -> bool:
        """Inspect a recording and prepare the pair selectors for it."""
        input_path = Path(path)
        try:
            sensors = list_sensors(input_path)
        except (OSError, ValueError, StopIteration) as exc:
            self.status_label.setText(f"Could not read {input_path.name}: {exc}")
            return False
        if not sensors:
            self.status_label.setText(
                f"{input_path.name} contains no sensor field columns."
            )
            return False

        self.input_edit.setText(str(input_path))
        self.sensors_label.setText(
            "  •  ".join(
                f"Sensor {key} (S/N {serial})" for key, serial in sensors
            )
        )

        keys = [key for key, _serial in sensors]
        for combo, default_index in (
            (self.first_combo, 0),
            (self.second_combo, min(1, len(keys) - 1)),
        ):
            combo.blockSignals(True)
            combo.clear()
            for key in keys:
                combo.addItem(f"Sensor {key}", key)
            combo.setCurrentIndex(default_index)
            combo.blockSignals(False)

        if not self.pairs_edit.text().strip() and len(keys) >= 2:
            self.pairs_edit.setText(f"{keys[0]}-{keys[1]}")
        base = input_path.with_suffix("")
        self.output_edit.setText(f"{base}_differences.csv")
        self.parse_button.setEnabled(True)
        self.status_label.setText(
            f"{len(keys)} sensors found. Choose pairs, then PARSE."
        )
        return True

    def _add_pair(self) -> None:
        first = self.first_combo.currentData()
        second = self.second_combo.currentData()
        if not isinstance(first, str) or not isinstance(second, str):
            return
        if first == second:
            self.status_label.setText("A pair needs two different sensors.")
            return
        existing = self.pairs_edit.text().split()
        pair_text = f"{first}-{second}"
        if pair_text not in existing:
            existing.append(pair_text)
            self.pairs_edit.setText(" ".join(existing))

    def _browse_output(self) -> None:
        selected_path, _filter = QFileDialog.getSaveFileName(
            self,
            "Save the difference CSV",
            self.output_edit.text(),
            "CSV files (*.csv)",
        )
        if selected_path:
            self.output_edit.setText(selected_path)

    # ------------------------------------------------------------------ parse
    def _parse(self) -> None:
        input_text = self.input_edit.text().strip()
        if not input_text:
            self.status_label.setText("Choose a recording first.")
            return
        pair_texts = self.pairs_edit.text().split()
        if not pair_texts:
            self.status_label.setText("Add at least one pair, e.g. A-B.")
            return
        try:
            pairs = [parse_pair(text) for text in pair_texts]
        except ValueError as exc:
            self.status_label.setText(str(exc))
            return
        output_text = self.output_edit.text().strip()
        if not output_text:
            self.status_label.setText("Choose an output file.")
            return
        if Path(output_text) == Path(input_text):
            self.status_label.setText(
                "The output file must differ from the recording."
            )
            return

        self.parse_button.setEnabled(False)
        self.status_label.setText("Parsing…")
        self._worker = _ParseWorker(
            Path(input_text),
            pairs,
            Path(output_text),
            self.include_fields_checkbox.isChecked(),
            self,
        )
        self._worker.succeeded.connect(self._on_parse_succeeded)
        self._worker.failed.connect(self._on_parse_failed)
        self._worker.finished.connect(self._worker.deleteLater)
        self._worker.start()

    def _on_parse_succeeded(
        self,
        written: int,
        skipped: int,
        output_path: str,
    ) -> None:
        message = f"Wrote {written:,} rows to {Path(output_path).name}"
        if skipped:
            message += f"  •  skipped {skipped:,} unreadable rows"
        self.status_label.setText(message)
        self.parse_button.setEnabled(True)
        self._worker = None

    def _on_parse_failed(self, message: str) -> None:
        self.status_label.setText(message)
        self.parse_button.setEnabled(True)
        self._worker = None
