"""Proxmark3 Device Manager for multi-device support."""
import asyncio
import hashlib
import logging
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Dict, List, Optional
import json

try:
    import pyudev
    UDEV_AVAILABLE = True
except ImportError:
    UDEV_AVAILABLE = False

try:
    import serial.tools.list_ports
    SERIAL_AVAILABLE = True
except ImportError:
    SERIAL_AVAILABLE = False

from ..workers.pm3_worker import PM3Worker, SubprocessPM3Worker
from .. import config

# Flag to track if Python bindings work (set once on first try)
_python_bindings_work = None

logger = logging.getLogger(__name__)


class DeviceStatus(Enum):
    """Device availability status."""
    CONNECTED = "connected"              # Ready to use
    DISCONNECTED = "disconnected"        # Not detected
    IN_USE = "in_use"                   # Active session
    ERROR = "error"                     # Communication error
    VERSION_MISMATCH = "version_mismatch"  # Firmware incompatible
    FLASHING = "flashing"               # Firmware update in progress
    BOOTLOADER_MODE = "bootloader_mode" # In bootloader, needs flash
    DISABLED = "disabled"               # Mismatch ignored by user


@dataclass
class PM3FirmwareInfo:
    """Firmware version information."""
    bootrom_version: str = "unknown"      # e.g., "v4.14831"
    os_version: str = "unknown"           # e.g., "v4.14831"
    client_version: str = "unknown"       # Local client version
    compatible: bool = False              # Versions match
    needs_upgrade: bool = False           # Device firmware older
    needs_downgrade: bool = False         # Device firmware newer
    bootloader_outdated: bool = False     # Bootloader needs update


@dataclass
class PM3Device:
    """Represents a single Proxmark3 device."""
    device_id: str                       # Unique ID (hash of serial + device path)
    device_path: str                     # /dev/ttyACM0, /dev/ttyACM1, etc.
    serial_number: Optional[str] = None  # USB serial number (if available)
    friendly_name: Optional[str] = None  # User-assigned name
    usb_vid: Optional[str] = None        # USB Vendor ID
    usb_pid: Optional[str] = None        # USB Product ID
    status: DeviceStatus = DeviceStatus.DISCONNECTED
    firmware_info: PM3FirmwareInfo = field(default_factory=PM3FirmwareInfo)
    last_seen: datetime = field(default_factory=datetime.now)
    first_seen: datetime = field(default_factory=datetime.now)
    worker: Optional[PM3Worker] = None   # Dedicated worker instance
    metadata: Dict = field(default_factory=dict)  # Additional metadata

    def to_dict(self) -> dict:
        """Convert to dictionary for API responses."""
        return {
            "device_id": self.device_id,
            "device_path": self.device_path,
            "serial_number": self.serial_number,
            "friendly_name": self.friendly_name or self.device_path.split('/')[-1],
            "usb_vid": self.usb_vid,
            "usb_pid": self.usb_pid,
            "status": self.status.value,
            "firmware_info": {
                "bootrom_version": self.firmware_info.bootrom_version,
                "os_version": self.firmware_info.os_version,
                "client_version": self.firmware_info.client_version,
                "compatible": self.firmware_info.compatible,
                "needs_upgrade": self.firmware_info.needs_upgrade,
                "needs_downgrade": self.firmware_info.needs_downgrade,
            },
            "last_seen": self.last_seen.isoformat(),
            "first_seen": self.first_seen.isoformat(),
            "connected": self.status == DeviceStatus.CONNECTED,
            "in_use": self.status == DeviceStatus.IN_USE,
        }


class PM3DeviceManager:
    """Manages multiple Proxmark3 devices."""

    # USB VID/PID for Proxmark3 devices
    PM3_USB_VID_PRIMARY = "9ac4"      # Standard PM3
    PM3_USB_PID_PRIMARY = "4b8f"
    PM3_USB_VID_EASY = "502d"         # PM3 Easy
    PM3_USB_PID_EASY = "502d"

    # Alternative identifiers
    PM3_VENDOR_IDS = ["9ac4", "502d", "2d0d"]  # Known PM3 vendor IDs
    PM3_PRODUCT_IDS = ["4b8f", "502d"]  # Known PM3 product IDs

    def __init__(self):
        """Initialize device manager."""
        self._devices: Dict[str, PM3Device] = {}  # device_id -> PM3Device
        self._lock = asyncio.Lock()
        self._monitor_task: Optional[asyncio.Task] = None
        self._udev_context = None
        self._udev_monitor = None
        self._device_change_callbacks: List = []  # Callbacks for device changes

        # Device discovery settings
        self.auto_discover = True
        self.discovery_interval = 0.5  # seconds - 500ms for responsive SSE updates
        self.device_timeout = 300     # seconds
        self._last_device_state: Dict[str, str] = {}  # device_id -> status for change detection

        logger.info("PM3DeviceManager initialized")

    def register_device_change_callback(self, callback):
        """Register a callback for device list changes.

        Args:
            callback: Async function that takes a list of PM3Device
        """
        self._device_change_callbacks.append(callback)
        logger.info(f"Registered device change callback: {callback}")

    async def _notify_device_change(self, force: bool = False):
        """Notify all registered callbacks of device list changes.

        Args:
            force: If True, send notification even if no changes detected
        """
        # Build current state snapshot
        current_state = {
            d.device_id: d.status.value if hasattr(d.status, 'value') else str(d.status)
            for d in self._devices.values()
        }

        # Check if anything changed
        if not force and current_state == self._last_device_state:
            return  # No changes, skip notification

        # Update last state
        self._last_device_state = current_state.copy()

        # Notify all callbacks
        devices = list(self._devices.values())
        for callback in self._device_change_callbacks:
            try:
                await callback(devices)
            except Exception as e:
                logger.error(f"Error in device change callback: {e}")

    async def start(self):
        """Start device manager and monitoring."""
        logger.info("Starting PM3 device manager")

        # Initial device discovery (force notification to send initial state)
        await self.discover_devices()
        await self._notify_device_change(force=True)

        # Start monitoring for hotplug events
        if UDEV_AVAILABLE:
            await self._start_udev_monitor()
        else:
            logger.warning("pyudev not available, using polling fallback")
            await self._start_polling_monitor()

    async def stop(self):
        """Stop device manager and monitoring."""
        logger.info("Stopping PM3 device manager")

        if self._monitor_task:
            self._monitor_task.cancel()
            try:
                await self._monitor_task
            except asyncio.CancelledError:
                pass

        # Disconnect all devices
        async with self._lock:
            for device in self._devices.values():
                if device.worker:
                    await device.worker.disconnect()

    async def discover_devices(self) -> List[PM3Device]:
        """Scan USB ports for PM3 devices.

        Returns:
            List of discovered PM3Device objects
        """
        async with self._lock:
            discovered_paths = set()

            # Method 1: Use pyserial to enumerate serial ports
            if SERIAL_AVAILABLE:
                discovered_paths.update(await self._discover_via_serial())

            # Method 2: Scan /dev/ttyACM* directly
            discovered_paths.update(await self._discover_via_dev())

            # Process discovered devices
            current_devices = {}
            for device_path in discovered_paths:
                device_id = self._generate_device_id(device_path)

                if device_id in self._devices:
                    # Update existing device
                    device = self._devices[device_id]
                    device.last_seen = datetime.now()
                    device.status = DeviceStatus.CONNECTED
                else:
                    # Create new device
                    device = await self._create_device(device_path)

                current_devices[device_id] = device

            # Mark missing devices as disconnected
            for device_id, device in self._devices.items():
                if device_id not in current_devices:
                    device.status = DeviceStatus.DISCONNECTED
                    current_devices[device_id] = device

            self._devices = current_devices

            connected_count = len([d for d in self._devices.values() if d.status == DeviceStatus.CONNECTED])
            logger.info(f"Discovered {connected_count} connected PM3 devices")

            # Notify callbacks of device changes (only if state changed)
            await self._notify_device_change()

            return list(self._devices.values())

    async def _discover_via_serial(self) -> set:
        """Discover devices using pyserial."""
        devices = set()

        try:
            ports = serial.tools.list_ports.comports()
            for port in ports:
                # Check if it's a PM3 device by VID/PID
                if port.vid and port.pid:
                    vid = f"{port.vid:04x}"
                    pid = f"{port.pid:04x}"

                    if vid in self.PM3_VENDOR_IDS or pid in self.PM3_PRODUCT_IDS:
                        devices.add(port.device)
                        logger.debug(f"Found PM3 device via serial: {port.device} (VID:{vid} PID:{pid})")
        except Exception as e:
            logger.error(f"Error discovering devices via serial: {e}")

        return devices

    async def _discover_via_dev(self) -> set:
        """Discover devices by scanning /dev/ttyACM*."""
        devices = set()

        try:
            # Look for /dev/ttyACM* devices
            dev_path = Path("/dev")
            for device_file in dev_path.glob("ttyACM*"):
                if device_file.is_char_device():
                    devices.add(str(device_file))
                    logger.debug(f"Found device: {device_file}")
        except Exception as e:
            logger.error(f"Error scanning /dev: {e}")

        return devices

    async def _create_device(self, device_path: str) -> PM3Device:
        """Create a new PM3Device object.

        Args:
            device_path: Path to device (e.g., /dev/ttyACM0)

        Returns:
            PM3Device object
        """
        device_id = self._generate_device_id(device_path)

        # Get USB info if available
        usb_info = await self._get_usb_info(device_path)

        # Create device object
        device = PM3Device(
            device_id=device_id,
            device_path=device_path,
            serial_number=usb_info.get("serial"),
            usb_vid=usb_info.get("vid"),
            usb_pid=usb_info.get("pid"),
            status=DeviceStatus.CONNECTED,
            friendly_name=None,  # Will be set by user or auto-generated
            first_seen=datetime.now(),
            last_seen=datetime.now()
        )

        # Use SubprocessPM3Worker (more reliable than SWIG bindings which may not be built)
        device.worker = SubprocessPM3Worker(device_path=device_path)

        # Query firmware version (in background, don't block)
        asyncio.create_task(self._query_firmware_version(device))

        logger.info(f"Created device {device_id} at {device_path}")

        return device

    def _generate_device_id(self, device_path: str, serial: Optional[str] = None) -> str:
        """Generate unique device ID.

        Args:
            device_path: Device path
            serial: Optional serial number

        Returns:
            Unique device ID
        """
        # Use serial number if available, otherwise use path
        identifier = serial if serial else device_path

        # Generate short hash
        hash_obj = hashlib.md5(identifier.encode())
        return f"pm3_{hash_obj.hexdigest()[:8]}"

    async def _get_usb_info(self, device_path: str) -> dict:
        """Get USB device information.

        Args:
            device_path: Device path

        Returns:
            Dictionary with vid, pid, serial
        """
        info = {}

        if SERIAL_AVAILABLE:
            try:
                # Find port info
                ports = serial.tools.list_ports.comports()
                for port in ports:
                    if port.device == device_path:
                        if port.vid:
                            info["vid"] = f"{port.vid:04x}"
                        if port.pid:
                            info["pid"] = f"{port.pid:04x}"
                        if port.serial_number:
                            info["serial"] = port.serial_number
                        break
            except Exception as e:
                logger.error(f"Error getting USB info: {e}")

        return info

    async def _query_firmware_version(self, device: PM3Device):
        """Query device firmware version.

        Args:
            device: PM3Device to query
        """
        try:
            if not device.worker:
                return

            # Execute hw version command
            result = await device.worker.execute_command("hw version")

            if result.success:
                # Parse firmware version from output
                firmware_info = self._parse_firmware_version(result.output)
                device.firmware_info = firmware_info

                # Update device status based on compatibility
                if not firmware_info.compatible:
                    device.status = DeviceStatus.VERSION_MISMATCH

                logger.info(f"Device {device.device_id} firmware: {firmware_info.os_version}")
        except Exception as e:
            logger.error(f"Error querying firmware version for {device.device_id}: {e}")
            device.status = DeviceStatus.ERROR

    def _parse_firmware_version(self, output: str) -> PM3FirmwareInfo:
        """Parse firmware version from hw version output.

        Args:
            output: Output from 'hw version' command

        Returns:
            PM3FirmwareInfo object
        """
        import re

        info = PM3FirmwareInfo()

        # Parse bootrom version (handles Iceman format: "Iceman/master/v4.20469-104-ge509967ab-suspect")
        bootrom_match = re.search(r'Bootrom[:\s.]+(.+?)(?:\s+\d{4}-\d{2}-\d{2}|\s+[0-9a-f]{9}|$)', output, re.IGNORECASE | re.MULTILINE)
        if bootrom_match:
            info.bootrom_version = bootrom_match.group(1).strip()

        # Parse OS version (handles Iceman format)
        os_match = re.search(r'OS[:\s.]+(.+?)(?:\s+\d{4}-\d{2}-\d{2}|\s+[0-9a-f]{9}|$)', output, re.IGNORECASE | re.MULTILINE)
        if os_match:
            info.os_version = os_match.group(1).strip()

        # Parse client version (handles Iceman format: "Iceman/master/4fa8f27-dirty-suspect")
        client_match = re.search(r'\[\s*Client\s*\]\s+(.+?)(?:\s+\d{4}-\d{2}-\d{2}|\s+[0-9a-f]{9}|$)', output, re.IGNORECASE | re.MULTILINE)
        if client_match:
            info.client_version = client_match.group(1).strip()

        # Check compatibility (exact match required per plan)
        # For Iceman firmware, we compare the version strings directly
        info.compatible = (
            info.os_version != "unknown" and
            info.bootrom_version != "unknown" and
            info.client_version != "unknown" and
            info.os_version == info.client_version and
            info.bootrom_version == info.client_version
        )

        # For version comparison, extract just the version number
        def extract_version(ver_str: str) -> str:
            """Extract version number from version string."""
            ver_match = re.search(r'v?(\d+\.\d+)', ver_str)
            return ver_match.group(1) if ver_match else ver_str

        os_ver = extract_version(info.os_version)
        client_ver = extract_version(info.client_version)
        bootrom_ver = extract_version(info.bootrom_version)

        info.needs_upgrade = os_ver < client_ver if os_ver != "unknown" and client_ver != "unknown" else False
        info.needs_downgrade = os_ver > client_ver if os_ver != "unknown" and client_ver != "unknown" else False
        info.bootloader_outdated = bootrom_ver < client_ver if bootrom_ver != "unknown" and client_ver != "unknown" else False

        return info

    def _get_local_client_version(self) -> str:
        """Get version of locally installed proxmark3 client.

        Returns:
            Client version string or "unknown"
        """
        # TODO: Implement actual version detection
        # For now, return a placeholder
        return "v4.14831"

    async def get_device(self, device_id: str) -> Optional[PM3Device]:
        """Get device by ID.

        Args:
            device_id: Device ID

        Returns:
            PM3Device or None if not found
        """
        async with self._lock:
            return self._devices.get(device_id)

    async def get_all_devices(self) -> List[PM3Device]:
        """Get all known devices.

        Returns:
            List of all PM3Device objects
        """
        async with self._lock:
            return list(self._devices.values())

    async def get_available_devices(self) -> List[PM3Device]:
        """Get devices not currently in use.

        Returns:
            List of available PM3Device objects
        """
        async with self._lock:
            return [
                device for device in self._devices.values()
                if device.status == DeviceStatus.CONNECTED
            ]

    async def get_connected_devices(self) -> List[PM3Device]:
        """Get connected devices.

        Returns:
            List of connected PM3Device objects
        """
        async with self._lock:
            return [
                device for device in self._devices.values()
                if device.status not in [DeviceStatus.DISCONNECTED, DeviceStatus.ERROR]
            ]

    async def set_device_status(self, device_id: str, status: DeviceStatus) -> bool:
        """Set device status.

        Args:
            device_id: Device ID
            status: New status

        Returns:
            True if successful, False if device not found
        """
        async with self._lock:
            device = self._devices.get(device_id)
            if device:
                device.status = status
                return True
            return False

    async def identify_device(self, device_id: str, duration_ms: int = 2000):
        """Blink LEDs on device for identification.

        Args:
            device_id: Device ID
            duration_ms: Blink duration in milliseconds

        Raises:
            ValueError: If device not found
            RuntimeError: If identification fails
        """
        device = await self.get_device(device_id)
        if not device:
            raise ValueError(f"Device {device_id} not found")

        if not device.worker:
            raise RuntimeError(f"Device {device_id} has no worker")

        # Use hw led command to identify the device with visual LED pattern
        try:
            result = await device.worker.execute_command(
                f"hw led --identify --duration {duration_ms}"
            )
            if not result.success:
                raise RuntimeError(f"Failed to identify device: {result.error}")

            logger.info(f"Identified device {device_id}")
        except Exception as e:
            logger.error(f"Error identifying device {device_id}: {e}")
            raise RuntimeError(f"Failed to identify device: {e}")

    async def _start_udev_monitor(self):
        """Start udev monitoring for device hotplug events."""
        try:
            logger.info("Starting udev device monitor")

            # This will be implemented in a separate task
            # For now, fall back to polling
            await self._start_polling_monitor()

        except Exception as e:
            logger.error(f"Error starting udev monitor: {e}")
            await self._start_polling_monitor()

    async def _start_polling_monitor(self):
        """Start polling-based device monitoring."""
        logger.info(f"Starting polling device monitor (interval: {self.discovery_interval}s)")

        async def poll_devices():
            while True:
                try:
                    await asyncio.sleep(self.discovery_interval)
                    await self.discover_devices()
                except asyncio.CancelledError:
                    break
                except Exception as e:
                    logger.error(f"Error in device polling: {e}")

        self._monitor_task = asyncio.create_task(poll_devices())
