"""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 where [ Client ] is on separate line)
        # Format: "[ Client ]\n  Iceman/master/4fa8f27-dirty-suspect 2026-01-04 ..."
        client_match = re.search(
            r'\[\s*Client\s*\]\s*\n\s*([A-Za-z]+/\w+/[^\s]+)',
            output,
            re.IGNORECASE | re.MULTILINE
        )
        if client_match:
            info.client_version = client_match.group(1).strip()

        # Check compatibility by comparing base version (commit hash)
        # Extract the version identifier (e.g., "Iceman/master/66c7374-dirty-suspect")
        # The key part is the commit hash (66c7374) - timestamps will differ between builds
        def extract_base_version(ver_str: str) -> str:
            """Extract base version identifier for comparison."""
            # Match patterns like:
            #   "Iceman/master/66c7374-dirty-suspect" (standard)
            #   "Iceman/DangerousPi/master/66c7374-dirty-suspect" (custom build)
            #   "Iceman/master/v4.20469-104-ge509967ab" (version tag)
            # Capture everything up to and including the commit/version identifier
            match = re.match(r'([A-Za-z]+(?:/[A-Za-z]+)?/\w+/[a-z0-9v.-]+)', ver_str)
            return match.group(1) if match else ver_str

        os_base = extract_base_version(info.os_version)
        bootrom_base = extract_base_version(info.bootrom_version)
        client_base = extract_base_version(info.client_version)

        info.compatible = (
            info.os_version != "unknown" and
            info.bootrom_version != "unknown" and
            info.client_version != "unknown" and
            os_base == client_base and
            bootrom_base == client_base
        )

        # 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.

        Runs the client binary with --version flag to detect installed version.

        Returns:
            Client version string or "unknown"
        """
        import subprocess
        from pathlib import Path

        client_path = Path(config.PM3_CLIENT_PATH)
        if not client_path.exists():
            logger.debug(f"PM3 client not found at {client_path}")
            return "unknown"

        try:
            # Run proxmark3 --version to get version info
            result = subprocess.run(
                [str(client_path), "--version"],
                capture_output=True,
                text=True,
                timeout=5
            )
            output = result.stdout + result.stderr

            # Parse version from output (typically like "Proxmark3 client - (RRG/Iceman v4.14831)")
            import re
            match = re.search(r'v[\d.]+', output)
            if match:
                return match.group(0)

            # Fallback: look for any version-like pattern
            match = re.search(r'(\d+\.\d+)', output)
            if match:
                return f"v{match.group(1)}"

            logger.debug(f"Could not parse version from: {output[:200]}")
            return "unknown"
        except subprocess.TimeoutExpired:
            logger.warning("Timeout getting PM3 client version")
            return "unknown"
        except Exception as e:
            logger.debug(f"Error getting PM3 client version: {e}")
            return "unknown"

    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.

        Uses the custom hw led command (from our led-pwm-control.patch) to
        flash all LEDs in a recognizable pattern.

        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")

        try:
            # Calculate number of blink cycles based on duration
            # Each cycle is ~400ms (200ms on + 200ms off)
            cycles = max(1, duration_ms // 400)

            for i in range(cycles):
                # Turn all LEDs on
                result = await device.worker.execute_command("hw led --led a,b,c,d --on")
                if not result.success:
                    logger.warning(f"hw led on failed: {result.error}")

                await asyncio.sleep(0.2)

                # Turn all LEDs off
                result = await device.worker.execute_command("hw led --led a,b,c,d --off")
                if not result.success:
                    logger.warning(f"hw led off failed: {result.error}")

                if i < cycles - 1:
                    await asyncio.sleep(0.2)

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

    async def flash_firmware(self, device_id: str) -> dict:
        """Flash firmware to a PM3 device.

        Flashes both bootrom and fullimage from bundled firmware files.
        Progress is reported via WebSocket notifications.

        Args:
            device_id: Device ID to flash

        Returns:
            dict with success status and message
        """
        from ..websocket.notifications import notify_pm3_flash_progress

        device = await self.get_device(device_id)
        if not device:
            return {"success": False, "error": "Device not found"}

        # Check device is not already flashing
        if device.status == DeviceStatus.FLASHING:
            return {"success": False, "error": "Device is already being flashed"}

        # Check device is not in use
        if device.status == DeviceStatus.IN_USE:
            return {"success": False, "error": "Device is in use. End session first."}

        # Firmware paths
        firmware_dir = Path(config.FIRMWARE_DIR)
        bootrom_path = firmware_dir / "bootrom.elf"
        fullimage_path = firmware_dir / "fullimage.elf"

        # Validate firmware files exist
        if not bootrom_path.exists():
            return {"success": False, "error": f"Bootrom firmware not found at {bootrom_path}"}
        if not fullimage_path.exists():
            return {"success": False, "error": f"Fullimage firmware not found at {fullimage_path}"}

        # Set device to flashing state
        original_status = device.status
        device.status = DeviceStatus.FLASHING
        await self._notify_device_change(force=True)

        try:
            # Send starting notification
            await notify_pm3_flash_progress(
                device_id=device_id,
                status="starting",
                progress=0,
                message="Preparing to flash firmware..."
            )

            # Find pm3-flash-all utility
            pm3_flash_path = await self._find_pm3_flash_utility()
            if not pm3_flash_path:
                raise RuntimeError("pm3-flash-all utility not found")

            # Phase 1: Flash bootrom (0-40%)
            await notify_pm3_flash_progress(
                device_id=device_id,
                status="bootrom",
                progress=10,
                message="Flashing bootrom..."
            )

            bootrom_result = await self._execute_flash_command(
                pm3_flash_path,
                device.device_path,
                str(bootrom_path),
                "bootrom",
                device_id
            )

            if not bootrom_result["success"]:
                raise RuntimeError(f"Bootrom flash failed: {bootrom_result['error']}")

            await notify_pm3_flash_progress(
                device_id=device_id,
                status="bootrom",
                progress=40,
                message="Bootrom flashed successfully"
            )

            # Brief delay for device to restart
            await asyncio.sleep(2)

            # Phase 2: Flash fullimage (50-90%)
            await notify_pm3_flash_progress(
                device_id=device_id,
                status="fullimage",
                progress=50,
                message="Flashing fullimage..."
            )

            fullimage_result = await self._execute_flash_command(
                pm3_flash_path,
                device.device_path,
                str(fullimage_path),
                "fullimage",
                device_id
            )

            if not fullimage_result["success"]:
                raise RuntimeError(f"Fullimage flash failed: {fullimage_result['error']}")

            await notify_pm3_flash_progress(
                device_id=device_id,
                status="fullimage",
                progress=90,
                message="Fullimage flashed successfully"
            )

            # Phase 3: Verify (95%)
            await notify_pm3_flash_progress(
                device_id=device_id,
                status="verifying",
                progress=95,
                message="Verifying firmware..."
            )

            # Wait for device to restart
            await asyncio.sleep(3)

            # Re-query firmware version
            await self._query_firmware_version(device)

            # Check if now compatible
            if device.firmware_info.compatible:
                device.status = DeviceStatus.CONNECTED
                message = "Firmware updated successfully"
            else:
                device.status = DeviceStatus.VERSION_MISMATCH
                message = "Firmware flashed but version mismatch persists"

            await notify_pm3_flash_progress(
                device_id=device_id,
                status="complete",
                progress=100,
                message=message
            )

            await self._notify_device_change(force=True)

            logger.info(f"Successfully flashed firmware to device {device_id}")
            return {"success": True, "message": message}

        except Exception as e:
            logger.error(f"Firmware flash failed for device {device_id}: {e}")

            # Notify of error
            await notify_pm3_flash_progress(
                device_id=device_id,
                status="error",
                progress=0,
                message=str(e)
            )

            # Restore original status or set to error
            device.status = DeviceStatus.ERROR
            await self._notify_device_change(force=True)

            return {"success": False, "error": str(e)}

    async def _find_pm3_flash_utility(self) -> Optional[str]:
        """Find pm3-flash-all or pm3-flash utility.

        Returns:
            Path to flash utility or None if not found
        """
        import shutil

        # Search locations in order of preference
        search_paths = [
            # Bundled with dangerous-pi
            Path(config.PM3_CLIENT_PATH).parent / "pm3-flash-all" if hasattr(config, 'PM3_CLIENT_PATH') else None,
            # Default installation
            Path("/home/dt/.pm3/proxmark3/pm3-flash-all"),
            Path("/home/dt/.pm3/proxmark3/client/flasher"),
            # System path
            Path(shutil.which("pm3-flash-all") or ""),
            Path(shutil.which("flasher") or ""),
        ]

        for path in search_paths:
            if path and path.exists() and path.is_file():
                logger.info(f"Found pm3 flash utility at: {path}")
                return str(path)

        logger.warning("pm3 flash utility not found in standard locations")
        return None

    async def _execute_flash_command(
        self,
        flash_path: str,
        device_path: str,
        firmware_path: str,
        phase: str,
        device_id: str
    ) -> dict:
        """Execute flash command and parse output.

        Args:
            flash_path: Path to flash utility
            device_path: Device path (e.g., /dev/ttyACM0)
            firmware_path: Path to .elf firmware file
            phase: "bootrom" or "fullimage"
            device_id: Device ID for progress notifications

        Returns:
            dict with success status
        """
        from ..websocket.notifications import notify_pm3_flash_progress

        # Build command
        # pm3-flash-all expects: pm3-flash-all -p /dev/ttyACMx [bootrom.elf] [fullimage.elf]
        # But we're doing them separately for better progress tracking
        cmd = [flash_path, "-p", device_path, firmware_path]

        logger.info(f"Executing flash command: {' '.join(cmd)}")

        try:
            process = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.STDOUT
            )

            output_lines = []
            base_progress = 10 if phase == "bootrom" else 50
            max_progress = 40 if phase == "bootrom" else 90

            # Read output line by line and parse progress
            while True:
                line = await process.stdout.readline()
                if not line:
                    break

                line_str = line.decode('utf-8', errors='replace').strip()
                output_lines.append(line_str)
                logger.debug(f"Flash output: {line_str}")

                # Parse progress from output
                progress = self._parse_flash_progress(line_str, base_progress, max_progress)
                if progress:
                    await notify_pm3_flash_progress(
                        device_id=device_id,
                        status=phase,
                        progress=progress,
                        message=f"Flashing {phase}..."
                    )

            await process.wait()

            output = "\n".join(output_lines)

            if process.returncode == 0:
                return {"success": True, "output": output}
            else:
                # Check for common errors in output
                error_msg = self._extract_flash_error(output)
                return {"success": False, "error": error_msg or f"Flash exited with code {process.returncode}"}

        except Exception as e:
            logger.error(f"Flash command execution failed: {e}")
            return {"success": False, "error": str(e)}

    def _parse_flash_progress(self, line: str, base: int, max_val: int) -> Optional[int]:
        """Parse progress percentage from flash output line.

        Args:
            line: Output line from flash process
            base: Base progress percentage
            max_val: Maximum progress percentage

        Returns:
            Progress percentage or None
        """
        import re

        # Look for percentage patterns like "50%" or "Writing: 50%"
        percent_match = re.search(r'(\d+)%', line)
        if percent_match:
            raw_percent = int(percent_match.group(1))
            # Scale to our range
            scaled = base + (raw_percent / 100) * (max_val - base)
            return int(scaled)

        # Look for block-based progress like "Writing block 10/20"
        block_match = re.search(r'(\d+)\s*/\s*(\d+)', line)
        if block_match and 'block' in line.lower():
            current = int(block_match.group(1))
            total = int(block_match.group(2))
            if total > 0:
                raw_percent = (current / total) * 100
                scaled = base + (raw_percent / 100) * (max_val - base)
                return int(scaled)

        return None

    def _extract_flash_error(self, output: str) -> Optional[str]:
        """Extract error message from flash output.

        Args:
            output: Full flash output

        Returns:
            Error message or None
        """
        error_patterns = [
            r"error[:\s]+(.+?)(?:\n|$)",
            r"failed[:\s]+(.+?)(?:\n|$)",
            r"cannot\s+(.+?)(?:\n|$)",
            r"permission denied",
            r"device not found",
            r"timeout",
        ]

        import re
        for pattern in error_patterns:
            match = re.search(pattern, output, re.IGNORECASE)
            if match:
                return match.group(0).strip()

        return None

    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())
