"""Django Channels WebSocket consumer for django-devicehub realtime events.

Clients connect via WebSocket to receive live device data and status updates.

URL patterns (defined in ``routing.py``)::

    ws://host/ws/iot/{device_type}/               # all devices of a type
    ws://host/ws/iot/{device_type}/{device_id}/    # specific device

Messages sent to clients (JSON)::

    {
        "event": "data",           // or "status"
        "device_type": "weatherstation",
        "device_id": "ws-001",
        "data": {"temperature": 25.3, "humidity": 80.1},
        "timestamp": "2025-01-15T12:00:00+00:00"
    }

Requirements:
    pip install django-devicehub[channels]
"""

import logging

try:
    from channels.generic.websocket import JsonWebSocketConsumer
except ImportError:
    # Provide a stub so the module can be imported without channels installed.
    # Attempting to actually use the consumer will fail at connection time.
    class JsonWebSocketConsumer:
        """Stub — install django-channels to use WebSocket consumers."""

        def __init__(self, *args, **kwargs):
            raise ImportError(
                "Django Channels is required for WebSocket support. "
                "Install with: pip install django-devicehub[channels]"
            )


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


class DeviceConsumer(JsonWebSocketConsumer):
    """WebSocket consumer for realtime IoT device events.

    Subscribes the client to one or two channel groups:
      - ``iot_{device_type}`` — events for all devices of a type
      - ``iot_{device_type}_{device_id}`` — events for a specific device
        (only if ``device_id`` is in the URL)

    Authentication:
      Only authenticated users are allowed to connect. Anonymous
      connections are rejected with a 4403 close code.
    """

    def connect(self):
        """Handle WebSocket connection."""
        user = self.scope.get("user")
        if user is None or user.is_anonymous:
            logger.warning("Rejected anonymous WebSocket connection")
            self.close(code=4403)
            return

        self.device_type = self.scope["url_route"]["kwargs"]["device_type"]
        self.device_id = self.scope["url_route"]["kwargs"].get("device_id")

        # Subscribe to the type-level group
        self.type_group = f"iot_{self.device_type}"
        self.channel_layer_group_add(self.type_group)

        # Subscribe to device-specific group if device_id provided
        self.device_group = None
        if self.device_id:
            self.device_group = f"iot_{self.device_type}_{self.device_id}"
            self.channel_layer_group_add(self.device_group)

        self.accept()
        logger.info(
            "WebSocket connected: user=%s, type=%s, device=%s",
            user,
            self.device_type,
            self.device_id or "*",
        )

    def disconnect(self, close_code):
        """Handle WebSocket disconnection."""
        self.channel_layer_group_discard(self.type_group)
        if self.device_group:
            self.channel_layer_group_discard(self.device_group)

        logger.info(
            "WebSocket disconnected: type=%s, device=%s, code=%s",
            getattr(self, "device_type", "?"),
            getattr(self, "device_id", "*"),
            close_code,
        )

    def receive_json(self, content, **kwargs):
        """Handle incoming messages from the client.

        Currently supports:
          - ``{"action": "ping"}`` — responds with ``{"event": "pong"}``

        Device commands should go through the REST API, not the WebSocket.
        """
        action = content.get("action")

        if action == "ping":
            self.send_json({"event": "pong"})
        else:
            self.send_json(
                {
                    "event": "error",
                    "message": f"Unknown action: {action}. "
                    "Use the REST API to send device commands.",
                }
            )

    def iot_event(self, event):
        """Handle a broadcast from the realtime bridge (channel layer group_send).

        The ``event`` dict has shape::

            {"type": "iot.event", "message": {...}}

        We forward ``message`` directly to the WebSocket client.
        """
        message = event.get("message", {})

        # If the client subscribed to a type-level group but the event is
        # for a different device, still send it — the client asked for all
        # devices of this type.
        self.send_json(message)

    # ------------------------------------------------------------------
    # Helpers for channel layer group management
    # ------------------------------------------------------------------

    def channel_layer_group_add(self, group):
        """Add this channel to a group (async wrapper)."""
        from asgiref.sync import async_to_sync

        async_to_sync(self.channel_layer.group_add)(group, self.channel_name)

    def channel_layer_group_discard(self, group):
        """Remove this channel from a group (async wrapper)."""
        from asgiref.sync import async_to_sync

        async_to_sync(self.channel_layer.group_discard)(group, self.channel_name)
