import json
import logging
import re

from django.utils import timezone
from django.utils.dateparse import parse_datetime

from .conf import devicehub_settings
from .models import DeviceStatus
from .registry import registry
from .signals import device_data_received, device_status_changed

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


class MessageRouter:
    """Routes incoming broker messages to the appropriate DeviceType handler.

    Parses topics matching {prefix}/{type_name}/{device_id}/{data|status|cmd},
    resolves the device from the database, stores raw messages, creates readings,
    updates device status, fires signals, and calls DeviceType hooks.
    """

    def __init__(self):
        prefix = re.escape(devicehub_settings.TOPIC_PREFIX)
        self._topic_pattern = re.compile(rf"^{prefix}/(\w+)/([^/]+)/(data|status|cmd)$")

    def handle_message(self, mqtt_message):
        """Process an incoming MQTT message. Called by the broker on_message."""
        topic = mqtt_message.topic
        match = self._topic_pattern.match(topic)
        if not match:
            logger.debug("Ignoring unmatched topic: %s", topic)
            return

        type_name, device_id, message_type = match.groups()

        # Look up device type in registry
        device_type_cls = registry.get(type_name)
        if device_type_cls is None:
            logger.warning("Unknown device type in topic: %s", type_name)
            return

        device_model = device_type_cls._device_model
        message_model = device_type_cls._message_model

        if not device_model:
            logger.error(
                "DeviceType %s has no models — call create_models() in models.py",
                type_name,
            )
            return

        # Resolve device
        try:
            device = device_model.objects.get(device_id=device_id, is_active=True)
        except device_model.DoesNotExist:
            logger.warning("Unknown device: %s (type=%s)", device_id, type_name)
            return

        # Parse payload
        try:
            payload = json.loads(mqtt_message.payload.decode())
        except (json.JSONDecodeError, UnicodeDecodeError):
            logger.error("Invalid payload on %s", topic)
            payload = None

        # Extract MQTT 5 properties
        properties = getattr(mqtt_message, "properties", None)
        user_props = {}
        content_type = ""
        correlation_id = ""
        if properties:
            user_props = dict(getattr(properties, "UserProperty", []))
            content_type = getattr(properties, "ContentType", "") or ""
            corr_data = getattr(properties, "CorrelationData", None)
            correlation_id = corr_data.decode() if corr_data else ""

        # Store raw message
        msg_record = message_model.objects.create(
            device=device,
            topic=topic,
            payload_json=payload,
            content_type=content_type,
            correlation_id=correlation_id,
            user_properties=user_props,
        )

        # Route to handler
        try:
            if message_type == "data":
                self._handle_data(device_type_cls, device, payload, msg_record)
            elif message_type == "status":
                self._handle_status(device_type_cls, device, payload, msg_record)
            msg_record.mark_completed()
        except Exception as e:
            logger.exception("Error processing %s from %s", message_type, device_id)
            msg_record.mark_failed(str(e))

    def _handle_data(self, device_type_cls, device, payload, msg_record):
        if payload is None:
            return

        reading_model = device_type_cls._reading_model
        reading_fields = device_type_cls._readings
        now = timezone.now()

        # Two supported payload formats:
        # 1. Flat: {"temperature": 25.3, "humidity": 80.1}
        # 2. Readings list: {"readings": [{"type": "temperature", "value": 25.3}, ...]}
        readings_data = {}

        if "readings" in payload and isinstance(payload["readings"], list):
            for r in payload["readings"]:
                rtype = r.get("type")
                rvalue = r.get("value")
                if rtype in reading_fields and rvalue is not None:
                    readings_data[rtype] = rvalue
        else:
            for key in reading_fields:
                if key in payload:
                    readings_data[key] = payload[key]

        if readings_data:
            # Use device-provided timestamp if available
            ts = payload.get("timestamp")
            if ts:
                try:
                    ts = parse_datetime(ts) or now
                except (ValueError, TypeError):
                    ts = now
            else:
                ts = now

            reading_model.objects.create(
                device=device,
                timestamp=ts,
                **readings_data,
            )

        # Update device heartbeat
        device.last_seen = now
        device.status = DeviceStatus.ONLINE
        device.save(update_fields=["last_seen", "status", "updated_at"])

        # Signal
        device_data_received.send(
            sender=device_type_cls,
            device=device,
            readings=readings_data,
            message=msg_record,
        )

        # DeviceType hook
        instance = device_type_cls()
        instance.on_data(device, readings_data)

    def _handle_status(self, device_type_cls, device, payload, msg_record):
        if payload is None:
            return

        old_status = device.status
        update_fields = ["last_seen", "updated_at"]
        device.last_seen = timezone.now()

        if "status" in payload:
            new_status = payload["status"]
            valid_statuses = [s.value for s in DeviceStatus]
            if new_status in valid_statuses:
                device.status = new_status
                update_fields.append("status")

        if "battery" in payload or "battery_level" in payload:
            device.battery_level = payload.get("battery") or payload.get(
                "battery_level"
            )
            update_fields.append("battery_level")

        if "firmware_version" in payload:
            device.firmware_version = payload["firmware_version"]
            update_fields.append("firmware_version")

        device.save(update_fields=update_fields)

        # Signal + hook if status changed
        if device.status != old_status:
            device_status_changed.send(
                sender=device_type_cls,
                device=device,
                old_status=old_status,
                new_status=device.status,
            )
            instance = device_type_cls()
            instance.on_status_change(device, old_status, device.status)
