"""Core realtime bridge for django-devicehub.

Listens to Django signals (device_data_received, device_status_changed)
and broadcasts events to connected web clients via the configured
backend (Django Channels WebSocket or SSE).

Configuration::

    DJANGO_DEVICEHUB = {
        "REALTIME": {
            # "channels" (WebSocket via Django Channels) or "sse"
            "BACKEND": "channels",
        },
    }

Usage::

    # The bridge auto-connects when imported and the app is ready.
    # For manual control:
    from django_devicehub.realtime.bridge import realtime_bridge

    realtime_bridge.broadcast(
        device_type="weatherstation",
        device_id="ws-001",
        event_type="data",
        data={"temperature": 25.3},
    )
"""

import json
import logging
import threading
from datetime import datetime, timezone


from ..conf import devicehub_settings
from ..signals import device_data_received, device_status_changed

logger = logging.getLogger("django_devicehub.realtime")


def _json_serializer(obj):
    """JSON serializer for objects not serializable by default json."""
    if isinstance(obj, datetime):
        return obj.isoformat()
    raise TypeError(f"Type {type(obj)} not serializable")


class RealtimeBridge:
    """Bridges Django signals to realtime web clients.

    Supports two delivery backends:
      - **channels**: Uses Django Channels channel layer (Redis) to
        broadcast to WebSocket consumers. Best for bidirectional comms.
      - **sse**: Pushes events to an in-process queue consumed by the
        SSE streaming view. Lightweight, no Redis required.

    The bridge is a singleton — import ``realtime_bridge`` from this module.
    """

    def __init__(self):
        self._connected = False
        self._lock = threading.Lock()
        # SSE subscribers: {group_name: [queue, ...]}
        self._sse_subscribers = {}
        self._sse_lock = threading.Lock()

    @property
    def backend(self):
        """Return the configured realtime backend name."""
        rt_conf = getattr(devicehub_settings, "REALTIME", {})
        return rt_conf.get("BACKEND", "channels")

    def connect(self):
        """Connect signal handlers. Called once during app startup."""
        with self._lock:
            if self._connected:
                return
            device_data_received.connect(self._on_data_received)
            device_status_changed.connect(self._on_status_changed)
            self._connected = True
            logger.debug("RealtimeBridge connected (backend=%s)", self.backend)

    def disconnect(self):
        """Disconnect signal handlers."""
        with self._lock:
            if not self._connected:
                return
            device_data_received.disconnect(self._on_data_received)
            device_status_changed.disconnect(self._on_status_changed)
            self._connected = False
            logger.debug("RealtimeBridge disconnected")

    def broadcast(self, device_type, device_id, event_type, data):
        """Broadcast an event to all subscribed clients.

        Parameters:
            device_type: Device type name (lowercase, e.g., "weatherstation").
            device_id: The specific device ID.
            event_type: "data" or "status".
            data: Dict of payload data.
        """
        message = {
            "event": event_type,
            "device_type": device_type,
            "device_id": str(device_id),
            "data": data,
            "timestamp": datetime.now(timezone.utc).isoformat(),
        }

        backend = self.backend

        if backend == "channels":
            self._broadcast_channels(device_type, device_id, message)
        elif backend == "sse":
            self._broadcast_sse(device_type, device_id, message)
        else:
            logger.warning("Unknown realtime backend: %s", backend)

    # ------------------------------------------------------------------
    # Signal handlers
    # ------------------------------------------------------------------

    def _on_data_received(self, sender, device, readings, message=None, **kwargs):
        """Handle device_data_received signal."""
        device_type = sender.__name__.lower()
        device_id = device.device_id
        self.broadcast(device_type, device_id, "data", readings)

    def _on_status_changed(self, sender, device, old_status, new_status, **kwargs):
        """Handle device_status_changed signal."""
        device_type = sender.__name__.lower()
        device_id = device.device_id
        self.broadcast(
            device_type,
            device_id,
            "status",
            {
                "old_status": old_status,
                "new_status": new_status,
            },
        )

    # ------------------------------------------------------------------
    # Channels backend
    # ------------------------------------------------------------------

    def _broadcast_channels(self, device_type, device_id, message):
        """Send via Django Channels channel layer."""
        try:
            from channels.layers import get_channel_layer
            from asgiref.sync import async_to_sync
        except ImportError:
            logger.error(
                "Django Channels is required for the 'channels' realtime backend. "
                "Install with: pip install django-devicehub[channels]"
            )
            return

        channel_layer = get_channel_layer()
        if channel_layer is None:
            logger.error("No channel layer configured. Add CHANNEL_LAYERS to settings.")
            return

        send = async_to_sync(channel_layer.group_send)

        # Broadcast to type-level group (all devices of this type)
        type_group = f"iot_{device_type}"
        try:
            send(
                type_group,
                {
                    "type": "iot.event",
                    "message": message,
                },
            )
        except Exception:
            logger.exception("Failed to broadcast to group '%s'", type_group)

        # Broadcast to device-specific group
        device_group = f"iot_{device_type}_{device_id}"
        try:
            send(
                device_group,
                {
                    "type": "iot.event",
                    "message": message,
                },
            )
        except Exception:
            logger.exception("Failed to broadcast to group '%s'", device_group)

    # ------------------------------------------------------------------
    # SSE backend
    # ------------------------------------------------------------------

    def _broadcast_sse(self, device_type, device_id, message):
        """Push to SSE subscriber queues."""
        serialized = json.dumps(message, default=_json_serializer)

        groups_to_notify = [
            f"iot_{device_type}",
            f"iot_{device_type}_{device_id}",
        ]

        with self._sse_lock:
            for group in groups_to_notify:
                subscribers = self._sse_subscribers.get(group, [])
                dead = []
                for queue in subscribers:
                    try:
                        queue.put_nowait(serialized)
                    except Exception:
                        dead.append(queue)
                # Clean up dead subscribers
                for q in dead:
                    subscribers.remove(q)

    def sse_subscribe(self, group_name, queue):
        """Register an SSE subscriber queue for a group.

        Called by the SSE view when a client connects.
        """
        with self._sse_lock:
            if group_name not in self._sse_subscribers:
                self._sse_subscribers[group_name] = []
            self._sse_subscribers[group_name].append(queue)
            logger.debug("SSE subscriber added to group '%s'", group_name)

    def sse_unsubscribe(self, group_name, queue):
        """Remove an SSE subscriber queue.

        Called by the SSE view when a client disconnects.
        """
        with self._sse_lock:
            subscribers = self._sse_subscribers.get(group_name, [])
            if queue in subscribers:
                subscribers.remove(queue)
            logger.debug("SSE subscriber removed from group '%s'", group_name)


# Module-level singleton
realtime_bridge = RealtimeBridge()
