"""Standalone GUI utility for inspecting raw orchestrator ring output."""

from __future__ import annotations

import argparse
import ctypes
import ctypes.util
import json
import os
from pathlib import Path
import signal
import subprocess
import sys
import time

import numpy as np
from PyQt6.QtCore import QTimer
from PyQt6.QtWidgets import QApplication, QLabel, QMainWindow, QVBoxLayout, QWidget
import pyqtgraph as pg

PROJECT_ROOT = Path(__file__).resolve().parents[2]
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from python_app.hardware_full.single_radar_service import create_single_radar_service
from python_app.models.run_config_model import RunConfigModel
from python_app.orchestration.shm_reader import ShmRingReader


def _shm_unlink(name: str) -> None:
    """Best-effort unlink for POSIX shared-memory object."""
    libc_name = ctypes.util.find_library("c")
    if libc_name is None:
        return

    libc = ctypes.CDLL(libc_name, use_errno=True)
    libc.shm_unlink.argtypes = [ctypes.c_char_p]
    libc.shm_unlink.restype = ctypes.c_int

    result = libc.shm_unlink(name.encode("utf-8"))
    if result == 0:
        return

    err = ctypes.get_errno()
    if err != 2:  # ENOENT
        raise OSError(err, f"shm_unlink failed for {name}")


def _read_raw_ring_name(config_path: Path) -> str:
    """Extract raw ring name from run config."""
    config = json.loads(config_path.read_text(encoding="utf-8"))
    ring_name = config["rings"]["raw"]["name"]
    if not isinstance(ring_name, str) or not ring_name.startswith("/"):
        raise RuntimeError("Config rings.raw.name must be a POSIX shm name starting with '/'")
    return ring_name


def _read_native_summary(config_path: Path) -> str:
    """Build short summary of radar/switch driver modes."""
    config = json.loads(config_path.read_text(encoding="utf-8"))
    radar_mode = config["radar"]["driver_mode"]
    switches = config["switches"]
    if "port1" in switches and "port2" in switches:
        port1_mode = switches["port1"]["driver_mode"]
        port2_mode = switches["port2"]["driver_mode"]
    else:
        port1_mode = switches["output"]["driver_mode"]
        port2_mode = switches["input"]["driver_mode"]
    return f"radar={radar_mode}, port1={port1_mode}, port2={port2_mode}"


def _prepare_radar_if_needed(config_path: Path, *, strict: bool) -> str | None:
    """Preconfigure native radar through Python service when requested."""
    config_payload = json.loads(config_path.read_text(encoding="utf-8"))
    config = RunConfigModel.from_dict(config_payload)
    if config.radar.driver_mode != "native":
        return "Radar pre-configuration skipped (mock mode)."
    if config.is_multi_device:
        return "Radar pre-configuration skipped (multi-device producer config)."

    radar_service = create_single_radar_service(config)
    if not getattr(radar_service, "driver_available", True):
        message = "LibreVNA Python driver is unavailable: skipping pre-configuration"
        if strict:
            raise RuntimeError(message)
        return message

    try:
        radar_service.open()
        radar_service.configure(config.radar.sweep)
        return "Radar pre-configuration completed."
    except Exception as exc:
        message = f"Radar pre-configuration failed ({exc})"
        if strict:
            raise
        return f"{message}. Continuing with native C++ configuration."
    finally:
        radar_service.close()


class RawOrchestratorViewer(QMainWindow):
    """Qt window that runs sweep_orchestrator and plots raw collections."""

    def __init__(self, config_path: Path, reset_ring: bool, prepare_radar: bool, strict_prepare: bool) -> None:
        """Initialize viewer, optionally prepare radar, and start polling."""
        super().__init__()

        self._config_path = config_path
        self._raw_ring_name = _read_raw_ring_name(config_path)
        self._orchestrator_process: subprocess.Popen[str] | None = None
        self._raw_reader: ShmRingReader | None = None

        if reset_ring:
            _shm_unlink(self._raw_ring_name)

        self._build_ui()
        if prepare_radar:
            self._status.setText("Preparing radar...")
            prepare_status = _prepare_radar_if_needed(self._config_path, strict=strict_prepare)
            if prepare_status is not None:
                self._status.setText(prepare_status)
        self._start_orchestrator()
        self._open_reader_or_fail()

        self._timer = QTimer(self)
        self._timer.setInterval(60)
        self._timer.timeout.connect(self._poll)
        self._timer.start()

    def _build_ui(self) -> None:
        """Build viewer widgets and raw trace plot."""
        self.setWindowTitle("Raw Sweep Viewer (orchestrator)")
        root = QWidget(self)
        self.setCentralWidget(root)

        layout = QVBoxLayout(root)

        mode_summary = _read_native_summary(self._config_path)
        self._status = QLabel(f"Starting... ({mode_summary})")
        layout.addWidget(self._status)

        self._plot = pg.PlotWidget(background="#101418")
        self._plot.showGrid(x=True, y=True, alpha=0.2)
        self._plot.setLabel("bottom", "Frequency", units="Hz")
        self._plot.setLabel("left", "Magnitude", units="dB")
        layout.addWidget(self._plot)

        self.resize(1400, 900)

    def _start_orchestrator(self) -> None:
        """Start sweep orchestrator subprocess."""
        binary = PROJECT_ROOT / "build/bin/sweep_orchestrator"
        if not binary.exists():
            raise RuntimeError(f"Missing binary: {binary}. Build first with 'make -j4'")

        command = [str(binary), "--config", str(self._config_path)]
        self._orchestrator_process = subprocess.Popen(
            command,
            cwd=PROJECT_ROOT,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )

    def _open_reader_or_fail(self) -> None:
        """Wait for raw ring readiness and open `ShmRingReader`."""
        if self._orchestrator_process is None:
            raise RuntimeError("Orchestrator process is not started")

        shm_path = Path("/dev/shm") / self._raw_ring_name[1:]
        deadline = time.monotonic() + 5.0
        last_reader_error: str | None = None

        while time.monotonic() < deadline:
            return_code = self._orchestrator_process.poll()
            if return_code is not None:
                details = self._read_process_output(self._orchestrator_process)
                raise RuntimeError(f"sweep_orchestrator exited with code {return_code}: {details}")

            if shm_path.exists():
                try:
                    self._raw_reader = ShmRingReader(self._raw_ring_name)
                    self._status.setText(f"Running: ring={self._raw_ring_name}")
                    return
                except RuntimeError as exc:
                    last_reader_error = str(exc)

            time.sleep(0.05)

        if last_reader_error is not None:
            raise RuntimeError(
                f"Timed out waiting for raw ring header readiness: {self._raw_ring_name}; "
                f"last error: {last_reader_error}"
            )
        raise RuntimeError(f"Timed out waiting for raw ring file: {shm_path}")

    def _poll(self) -> None:
        """Poll subprocess state and draw latest available raw collection."""
        if self._orchestrator_process is None:
            return

        return_code = self._orchestrator_process.poll()
        if return_code is not None:
            details = self._read_process_output(self._orchestrator_process)
            self._status.setText(f"Error: sweep_orchestrator exited ({return_code})")
            raise RuntimeError(f"sweep_orchestrator exited with code {return_code}: {details}")

        if self._raw_reader is None:
            return

        latest = None
        for _ in range(16):
            collection = self._raw_reader.pop_raw_collection()
            if collection is None:
                break
            latest = collection

        if latest is not None:
            self._draw_collection(latest)

    def _draw_collection(self, collection) -> None:
        """Render all traces from one raw collection."""
        self._plot.clear()
        palette = [
            "#4cc9f0",
            "#f72585",
            "#b8f2e6",
            "#ffd166",
            "#90be6d",
            "#ff595e",
            "#6a4c93",
            "#1982c4",
            "#ff9f1c",
            "#2ec4b6",
            "#e71d36",
            "#a0c4ff",
        ]

        for idx, trace in enumerate(collection.traces):
            magnitude_db = 20.0 * np.log10(np.maximum(np.abs(trace.s21), 1e-12))
            label = f"input={trace.combo.input_pos}, output={trace.combo.output_pos}"
            self._plot.plot(
                trace.frequency_hz,
                magnitude_db,
                pen=pg.mkPen(palette[idx % len(palette)], width=1.6),
                name=label,
            )

        self._status.setText(
            f"Running: collection_id={collection.collection_id}, "
            f"traces={len(collection.traces)}, ring={self._raw_ring_name}"
        )

    @staticmethod
    def _read_process_output(process: subprocess.Popen[str]) -> str:
        """Collect process stdout/stderr text for diagnostics."""
        stdout = ""
        stderr = ""
        if process.stdout is not None:
            stdout = process.stdout.read().strip()
        if process.stderr is not None:
            stderr = process.stderr.read().strip()

        if stderr and stdout:
            return f"{stderr}\nstdout:\n{stdout}"
        if stderr:
            return stderr
        if stdout:
            return f"stdout:\n{stdout}"
        return "no output"

    def closeEvent(self, event) -> None:  # noqa: N802
        """Stop subprocess and close reader before window destruction."""
        self._shutdown()
        super().closeEvent(event)

    def _shutdown(self) -> None:
        """Close reader and terminate subprocess."""
        if self._raw_reader is not None:
            self._raw_reader.close()
            self._raw_reader = None

        process = self._orchestrator_process
        self._orchestrator_process = None
        if process is None:
            return

        if process.poll() is None:
            process.terminate()
            try:
                process.wait(timeout=2.0)
            except subprocess.TimeoutExpired:
                process.kill()
                process.wait(timeout=1.0)


def main() -> int:
    """CLI entrypoint for raw orchestrator viewer."""
    parser = argparse.ArgumentParser(description="Run sweep_orchestrator and plot raw sweep collections")
    parser.add_argument(
        "--config",
        type=Path,
        default=PROJECT_ROOT / "run_config.json",
        help="Path to run config JSON",
    )
    parser.add_argument(
        "--no-reset-ring",
        action="store_true",
        help="Do not unlink existing raw ring name before starting",
    )
    parser.add_argument(
        "--skip-radar-prepare",
        action="store_true",
        help="Skip Python pre-configuration of native radar before orchestrator start",
    )
    parser.add_argument(
        "--strict-radar-prepare",
        action="store_true",
        help="Fail immediately if Python pre-configuration cannot run",
    )
    args = parser.parse_args()

    config_path = args.config.resolve()
    if not config_path.exists():
        raise FileNotFoundError(f"Config file not found: {config_path}")

    if os.geteuid() == 0 and os.environ.get("SUDO_USER"):
        print(
            "Warning: running GUI test via sudo can break Qt DBus/session integration. "
            "Prefer regular user with USB/GPIO permissions."
        )

    app = QApplication(sys.argv)
    viewer = RawOrchestratorViewer(
        config_path=config_path,
        reset_ring=not args.no_reset_ring,
        prepare_radar=not args.skip_radar_prepare,
        strict_prepare=args.strict_radar_prepare,
    )
    viewer.show()

    def _sig_handler(_signum, _frame):
        """Close viewer gracefully on process signals."""
        viewer.close()

    signal.signal(signal.SIGINT, _sig_handler)
    signal.signal(signal.SIGTERM, _sig_handler)

    return app.exec()


if __name__ == "__main__":
    raise SystemExit(main())
