# SPDX-License-Identifier: GPL-3.0-or-later
"""aisstream.io WebSocket reader.

A QObject (not a QThread) wrapping Qt's QWebSocket — which is bundled with QGIS and
rides the GUI event loop, so no extra thread or dependency is needed. It presents
the same lifecycle as the serial/TCP readers (start/stop/isRunning +
connected/disconnected/errorOccurred/finished) so the source dialog can treat it
uniformly, and adds a ``recordReceived`` signal carrying already-decoded record
dicts (the JSON feed bypasses the NMEA decoder).

On connect it sends the JSON subscription (API key + bounding box). The box is
derived from the user's own position; :meth:`set_bbox` re-subscribes when the
own-position marker moves. Drops auto-reconnect after a short delay.
"""
import json

from qgis.PyQt.QtCore import QObject, pyqtSignal, QTimer, QUrl

from .internet_sources import subscription, aisstream_to_record


def _qwebsocket_cls():
    """QWebSocket across Qt bindings — qgis.PyQt doesn't proxy QtWebSockets, so
    fall back to the concrete PyQt5/PyQt6 module."""
    for mod in ("qgis.PyQt.QtWebSockets", "PyQt5.QtWebSockets",
                "PyQt6.QtWebSockets"):
        try:
            return __import__(mod, fromlist=["QWebSocket"]).QWebSocket
        except Exception:                          # noqa: BLE001  # nosec B112
            continue
    raise ImportError("QtWebSockets is not available in this QGIS")


class AisStreamReader(QObject):
    lineReceived = pyqtSignal(str)          # present for interface parity (unused)
    recordReceived = pyqtSignal(object)     # decoded record dict
    errorOccurred = pyqtSignal(str)
    connected = pyqtSignal()
    disconnected = pyqtSignal()
    finished = pyqtSignal()

    def __init__(self, url, api_key, bbox, parent=None):
        super().__init__(parent)
        self._url = url
        self._key = api_key
        self._bbox = bbox
        self._ws = None
        self._running = False
        self._want = False              # user intends to stay connected
        self._reconnect = QTimer(self)
        self._reconnect.setSingleShot(True)
        self._reconnect.timeout.connect(self._open)

    # ---- lifecycle (reader interface) ----------------------------------- #
    def start(self):
        self._want = True
        self._open()

    def stop(self):
        self._want = False
        self._reconnect.stop()
        if self._ws is not None:
            try:
                self._ws.close()
            except Exception:                      # noqa: BLE001  # nosec B110
                pass
        self._running = False
        QTimer.singleShot(0, self.finished.emit)

    def isRunning(self):
        return self._running

    def set_bbox(self, bbox):
        """Re-subscribe with a new bounding box (own-position marker moved)."""
        self._bbox = bbox
        if self._ws is not None and self._running:
            self._send_subscription()

    # ---- internals ------------------------------------------------------ #
    def _open(self):
        self._ws = _qwebsocket_cls()()
        self._ws.connected.connect(self._on_connected)
        self._ws.disconnected.connect(self._on_disconnected)
        # aisstream delivers the JSON as BINARY frames (not text); handle both.
        self._ws.textMessageReceived.connect(self._on_text)
        self._ws.binaryMessageReceived.connect(self._on_binary)
        try:                                        # Qt 5.15+ name
            self._ws.errorOccurred.connect(lambda *_: self._on_error())
        except Exception:                           # noqa: BLE001
            try:
                self._ws.error.connect(lambda *_: self._on_error())
            except Exception:                       # noqa: BLE001  # nosec B110
                pass
        self._ws.open(QUrl(self._url))

    def _send_subscription(self):
        try:
            self._ws.sendTextMessage(
                json.dumps(subscription(self._key, self._bbox)))
        except Exception as exc:                    # noqa: BLE001
            self.errorOccurred.emit(str(exc))

    def _on_connected(self):
        self._running = True
        self._send_subscription()
        self.connected.emit()

    def _on_text(self, text):
        self._handle(text)

    def _on_binary(self, data):
        try:
            text = bytes(data).decode("utf-8", "ignore")
        except Exception:                           # noqa: BLE001
            return
        self._handle(text)

    def _handle(self, text):
        try:
            rec = aisstream_to_record(json.loads(text))
        except Exception:                           # noqa: BLE001
            rec = None
        if rec is not None:
            self.recordReceived.emit(rec)

    def _on_disconnected(self):
        was = self._running
        self._running = False
        if was:
            self.disconnected.emit()
        if self._want:
            self._reconnect.start(5000)             # auto-reconnect on a drop
        else:
            self.finished.emit()

    def _on_error(self):
        try:
            msg = self._ws.errorString() if self._ws is not None else "error"
        except Exception:                           # noqa: BLE001
            msg = "websocket error"
        self.errorOccurred.emit(msg)
