import importlib
import json
import logging
import random
import signal as signal_module
import time

from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone

from django_devicehub.conf import devicehub_settings
from django_devicehub.fields import ReadingType
from django_devicehub.registry import registry

logger = logging.getLogger("django_devicehub")


class Command(BaseCommand):
    help = (
        "Simulate one or more IoT devices publishing MQTT messages. "
        "Useful for development and demos without real hardware."
    )

    def add_arguments(self, parser):
        parser.add_argument(
            "device_type",
            help="Name of a registered DeviceType (e.g. 'weatherstation')",
        )
        parser.add_argument(
            "--count",
            type=int,
            default=1,
            help="Number of simulated devices (default: 1)",
        )
        parser.add_argument(
            "--interval",
            type=float,
            default=5,
            help="Publish interval in seconds (default: 5)",
        )
        parser.add_argument(
            "--broker",
            default="default",
            help='Broker name from DJANGO_DEVICEHUB["BROKERS"] (default: "default")',
        )
        parser.add_argument(
            "--duration",
            type=float,
            default=0,
            help="Run for N seconds then stop (default: 0 = infinite)",
        )
        parser.add_argument(
            "--realistic",
            action="store_true",
            help="Generate realistic random data within Reading ranges",
        )
        parser.add_argument(
            "--prefix",
            default="sim-",
            help='Device ID prefix (default: "sim-")',
        )

    def handle(self, *args, **options):
        device_type_name = options["device_type"]
        count = options["count"]
        interval = options["interval"]
        broker_name = options["broker"]
        duration = options["duration"]
        realistic = options["realistic"]
        prefix = options["prefix"]
        verbosity = options["verbosity"]

        # Look up the DeviceType
        dt_cls = registry.get(device_type_name)
        if dt_cls is None:
            available = ", ".join(dt.__name__ for dt in registry.all())
            raise CommandError(
                f"Unknown device type: '{device_type_name}'. "
                f"Registered types: {available or '(none)'}"
            )

        # Check that models exist
        if dt_cls._device_model is None:
            raise CommandError(
                f"DeviceType '{dt_cls.__name__}' has no models. "
                f"Call create_models() in your app's models.py."
            )

        # Resolve broker
        broker_config = devicehub_settings.BROKERS.get(broker_name)
        if not broker_config:
            raise CommandError(f"Broker '{broker_name}' not configured.")

        engine = broker_config.get("ENGINE", "django_devicehub.brokers.mqtt.MQTTBroker")
        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)

        DeviceModel = dt_cls._device_model
        type_name = dt_cls.__name__.lower()

        # Provision simulated devices
        devices = []
        for i in range(1, count + 1):
            device_id = f"{prefix}{type_name}-{i:03d}"
            device, created = DeviceModel.objects.get_or_create(
                device_id=device_id,
                defaults={
                    "name": f"Simulated {dt_cls.__name__} {i:03d}",
                    "status": "online",
                    "is_active": True,
                    "metadata": {"simulated": True},
                },
            )
            if created:
                self.stdout.write(self.style.SUCCESS(f"  Created device: {device_id}"))
            else:
                if verbosity >= 2:
                    self.stdout.write(f"  Using existing device: {device_id}")
            devices.append(device)

        # Connect broker
        broker.connect()
        broker.loop_start()

        self.stdout.write(
            self.style.SUCCESS(
                f"Simulating {count} {dt_cls.__name__} device(s) "
                f"every {interval}s on broker '{broker_name}'"
            )
        )
        if duration:
            self.stdout.write(f"  Duration: {duration}s")

        # Graceful shutdown
        running = True

        def shutdown(signum, frame):
            nonlocal running
            running = False

        signal_module.signal(signal_module.SIGTERM, shutdown)
        signal_module.signal(signal_module.SIGINT, shutdown)

        start_time = time.monotonic()
        cycle = 0

        try:
            while running:
                cycle += 1

                for device in devices:
                    # Generate reading data
                    payload = self._generate_data(dt_cls, realistic=realistic)
                    payload["timestamp"] = timezone.now().isoformat()
                    payload["device_id"] = device.device_id

                    topic = dt_cls.get_topic(device.device_id, "data")
                    broker.publish(topic, payload)

                    if verbosity >= 2:
                        self.stdout.write(
                            f"  [{cycle:04d}] {topic} -> "
                            f"{json.dumps(payload, default=str)}"
                        )
                    elif verbosity >= 1:
                        self.stdout.write(
                            f"  [{cycle:04d}] {device.device_id}: published data"
                        )

                    # Publish status update every 5th cycle
                    if cycle % 5 == 0:
                        status_payload = self._generate_status(device)
                        status_topic = dt_cls.get_topic(device.device_id, "status")
                        broker.publish(status_topic, status_payload)

                        if verbosity >= 2:
                            self.stdout.write(
                                self.style.WARNING(
                                    f"  [{cycle:04d}] {status_topic} -> "
                                    f"{json.dumps(status_payload, default=str)}"
                                )
                            )

                # Check duration limit
                if duration and (time.monotonic() - start_time) >= duration:
                    self.stdout.write(
                        self.style.SUCCESS(
                            f"\nDuration limit reached ({duration}s). "
                            f"Stopping after {cycle} cycles."
                        )
                    )
                    break

                # Sleep in small increments for responsive shutdown
                sleep_end = time.monotonic() + interval
                while running and time.monotonic() < sleep_end:
                    time.sleep(min(0.25, sleep_end - time.monotonic()))

        except KeyboardInterrupt:
            pass
        finally:
            self.stdout.write("\nShutting down simulator...")
            broker.loop_stop()
            broker.disconnect()
            self.stdout.write(
                self.style.SUCCESS(
                    f"Stopped after {cycle} cycles. {count} device(s) simulated."
                )
            )

    def _generate_data(self, dt_cls, realistic=False):
        """Generate a data payload from the DeviceType's Reading definitions."""
        data = {}
        for name, reading in dt_cls._readings.items():
            if realistic and reading.range and len(reading.range) == 2:
                lo, hi = reading.range
                if lo is not None and hi is not None:
                    if reading.type == ReadingType.FLOAT:
                        data[name] = round(random.uniform(lo, hi), 2)
                    elif reading.type == ReadingType.INTEGER:
                        data[name] = random.randint(int(lo), int(hi))
                    else:
                        data[name] = self._generate_for_type(reading)
                else:
                    data[name] = self._generate_for_type(reading)
            else:
                data[name] = self._generate_for_type(reading)
        return data

    def _generate_for_type(self, reading):
        """Generate a plausible value based on reading type."""
        if reading.type == ReadingType.FLOAT:
            return round(random.uniform(0, 100), 2)
        elif reading.type == ReadingType.INTEGER:
            return random.randint(0, 100)
        elif reading.type == ReadingType.BOOLEAN:
            return random.choice([True, False])
        elif reading.type == ReadingType.STRING:
            return random.choice(["ok", "warning", "alert", "normal"])
        elif reading.type == ReadingType.JSON:
            return {"value": random.random()}
        else:
            return None

    def _generate_status(self, device):
        """Generate a status payload for a device."""
        return {
            "device_id": device.device_id,
            "status": "online",
            "battery_level": round(random.uniform(20, 100), 1),
            "uptime_seconds": random.randint(60, 86400),
            "firmware_version": "1.0.0-sim",
            "timestamp": timezone.now().isoformat(),
        }
