import secrets
from datetime import timedelta

from django.db import models
from django.utils import timezone


class DeviceStatus(models.TextChoices):
    ONLINE = "online", "Online"
    OFFLINE = "offline", "Offline"
    ERROR = "error", "Error"
    MAINTENANCE = "maintenance", "Maintenance"
    LOW_BATTERY = "low_battery", "Low Battery"
    PROVISIONING = "provisioning", "Provisioning"


class ProcessingStatus(models.TextChoices):
    PENDING = "pending", "Pending"
    PROCESSING = "processing", "Processing"
    COMPLETED = "completed", "Completed"
    FAILED = "failed", "Failed"


class BaseDevice(models.Model):
    """Abstract base for all device models generated by DeviceType.create_models().

    Provides identity, status tracking, MQTT authentication, and lifecycle fields.
    DeviceType-specific StateFields are added as extra columns on the concrete model.
    """

    device_id = models.CharField(
        max_length=128,
        unique=True,
        db_index=True,
        verbose_name="Device ID",
    )
    name = models.CharField(max_length=255, blank=True, verbose_name="Name")
    status = models.CharField(
        max_length=20,
        choices=DeviceStatus.choices,
        default=DeviceStatus.OFFLINE,
        db_index=True,
    )
    battery_level = models.FloatField(
        null=True,
        blank=True,
        verbose_name="Battery (%)",
    )
    last_seen = models.DateTimeField(null=True, blank=True, verbose_name="Last Seen")
    firmware_version = models.CharField(max_length=50, blank=True)
    is_active = models.BooleanField(default=True, db_index=True)

    # MQTT authentication
    mqtt_username = models.CharField(
        max_length=128,
        unique=True,
        null=True,
        blank=True,
    )
    mqtt_password_hash = models.CharField(max_length=256, blank=True)
    api_key = models.CharField(max_length=128, unique=True, null=True, blank=True)

    # Extensible metadata (free-form JSON)
    metadata = models.JSONField(default=dict, blank=True)

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True
        ordering = ["-created_at"]

    def __str__(self):
        return f"{self.name or self.device_id} ({self.status})"

    def generate_credentials(self):
        """Generate MQTT credentials and API key.

        Returns a dict with plaintext values (shown once, then hashed).
        """
        from django.contrib.auth.hashers import make_password

        type_label = self._meta.label_lower.replace(".", "-")
        self.mqtt_username = f"{type_label}-{self.device_id}"
        plaintext_password = secrets.token_urlsafe(32)
        self.mqtt_password_hash = make_password(plaintext_password)
        self.api_key = secrets.token_hex(32)
        return {
            "mqtt_username": self.mqtt_username,
            "mqtt_password": plaintext_password,
            "api_key": self.api_key,
        }

    def check_mqtt_password(self, raw_password):
        """Verify a plaintext password against the stored hash."""
        from django.contrib.auth.hashers import check_password

        return check_password(raw_password, self.mqtt_password_hash)

    def mark_online(self):
        self.status = DeviceStatus.ONLINE
        self.last_seen = timezone.now()
        self.save(update_fields=["status", "last_seen", "updated_at"])

    def mark_offline(self):
        self.status = DeviceStatus.OFFLINE
        self.save(update_fields=["status", "updated_at"])

    def is_heartbeat_overdue(self, heartbeat_interval=300, multiplier=2.5):
        """Check if the device has missed its heartbeat window."""
        if not self.last_seen:
            return True
        threshold = timezone.now() - timedelta(seconds=heartbeat_interval * multiplier)
        return self.last_seen < threshold

    def send_command(self, command_name, payload=None, broker_name="default"):
        """Send a command to this device via the configured broker."""
        dt_cls = getattr(self.__class__, "_device_type_cls", None)
        if dt_cls is None:
            raise ValueError("Device model has no associated DeviceType")

        cmd = dt_cls._commands.get(command_name)
        if cmd is None:
            raise ValueError(f"Unknown command '{command_name}' for {dt_cls.__name__}")

        if payload:
            cmd.validate_payload(payload)

        from .conf import devicehub_settings
        from .signals import device_command_sent

        topic = dt_cls.get_topic(self.device_id, "cmd")
        cmd_payload = {"command": command_name}
        if payload:
            cmd_payload["payload"] = payload

        # Import broker lazily
        broker_config = devicehub_settings.BROKERS.get(broker_name, {})
        engine = broker_config.get("ENGINE", "django_devicehub.brokers.mqtt.MQTTBroker")
        import importlib

        module_path, class_name = engine.rsplit(".", 1)
        module = importlib.import_module(module_path)
        BrokerClass = getattr(module, class_name)

        broker = BrokerClass(name=broker_name, config=broker_config)
        broker.connect()
        broker.publish(
            topic,
            cmd_payload,
            qos=cmd.qos,
            user_properties={"command": command_name},
        )
        broker.disconnect()

        device_command_sent.send(
            sender=dt_cls,
            device=self,
            command=command_name,
            payload=payload or {},
        )


class BaseReading(models.Model):
    """Abstract base for reading models.

    Subclasses get one nullable column per Reading field declared on the DeviceType,
    forming a wide time-series table.
    """

    timestamp = models.DateTimeField(db_index=True)
    received_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        abstract = True
        ordering = ["-timestamp"]

    def __str__(self):
        return f"Reading at {self.timestamp}"


class BaseMessage(models.Model):
    """Abstract base for raw message storage (audit trail / debugging)."""

    topic = models.CharField(max_length=512)
    payload_json = models.JSONField(null=True, blank=True)
    payload_raw = models.BinaryField(null=True, blank=True)
    content_type = models.CharField(max_length=100, blank=True)
    correlation_id = models.CharField(max_length=256, blank=True)
    user_properties = models.JSONField(default=dict, blank=True)

    processing_status = models.CharField(
        max_length=20,
        choices=ProcessingStatus.choices,
        default=ProcessingStatus.PENDING,
        db_index=True,
    )
    processing_error = models.TextField(blank=True)
    received_at = models.DateTimeField(auto_now_add=True)
    processed_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        abstract = True
        ordering = ["-received_at"]

    def __str__(self):
        return f"Message on {self.topic} ({self.processing_status})"

    def mark_completed(self):
        self.processing_status = ProcessingStatus.COMPLETED
        self.processed_at = timezone.now()
        self.save(update_fields=["processing_status", "processed_at"])

    def mark_failed(self, error=""):
        self.processing_status = ProcessingStatus.FAILED
        self.processing_error = str(error)
        self.processed_at = timezone.now()
        self.save(
            update_fields=["processing_status", "processing_error", "processed_at"]
        )
