"""
USB Somewear Node Detector

Detects Somewear nodes connected via USB (excludes Android devices).
"""
import glob
import json
import os
import re
import subprocess
from dataclasses import dataclass
from typing import List, Optional, Tuple


@dataclass
class SomewearNode:
    """Represents a connected Somewear node."""
    serial_number: str
    device_path: str
    product_id: str
    vendor_id: str
    product_name: str
    manufacturer: str
    version: str
    bootloader_mode: bool = False

    def __str__(self):
        mode = " [BOOTLOADER]" if self.bootloader_mode else ""
        return f"{self.product_name} ({self.serial_number}) @ {self.device_path}{mode}"

    def get_board_version(self) -> Optional[int]:
        """
        Get board version from serial number.
        Board version is determined by the alphabetic position of the second character.

        Alphabetic position mapping:
        - E (position 5) → PC02
        - F (position 6) → PC03
        - G (position 7) → PC04
        - H (position 8) → PC05

        Returns:
            Board version number (e.g., 2 for PC02, 3 for PC03, 4 for PC04) or None if cannot determine.
        """
        if len(self.serial_number) < 2:
            return None

        second_char = self.serial_number[1].upper()
        if not second_char.isalpha():
            return None

        # Get alphabetic position (A=1, B=2, C=3, ... G=7, H=8)
        alpha_position = ord(second_char) - ord('A') + 1

        # Map alphabetic position to board version
        # Position 5 (E) → PC02 (version 2)
        # Position 6 (F) → PC03 (version 3)
        # Position 7 (G) → PC04 (version 4)
        # Position 8 (H) → PC05 (version 5)
        position_to_version = {
            5: 2,
            6: 3,
            7: 4,
            8: 5,
        }

        return position_to_version.get(alpha_position)

    def get_hardware_flavor(self) -> Optional[int]:
        """
        Get hardware flavor based on alphabetic position of second character in serial number.

        Flavor mapping:
        - E (position 5) → PC02 → Flavor 5
        - F (position 6) → PC03 → Flavor 5
        - G (position 7) → PC04 → Flavor 6
        - H (position 8) → PC05 → Flavor 6

        Returns:
            Hardware flavor number or None if cannot determine.
        """
        if len(self.serial_number) < 2:
            return None

        second_char = self.serial_number[1].upper()
        if not second_char.isalpha():
            return None

        # Get alphabetic position (A=1, B=2, C=3, ... G=7, H=8)
        alpha_position = ord(second_char) - ord('A') + 1

        # Map alphabetic position to hardware flavor
        # PC02 (E, pos 5) and PC03 (F, pos 6) use Flavor 5
        # PC04 (G, pos 7) and PC05 (H, pos 8) use Flavor 6
        flavor_map = {
            5: 5,  # PC02 → Flavor 5
            6: 5,  # PC03 → Flavor 5
            7: 6,  # PC04 → Flavor 6
            8: 6,  # PC05 → Flavor 6 (assuming same as PC04)
        }

        return flavor_map.get(alpha_position)


class USBNodeDetector:
    """Detects Somewear nodes connected via USB."""

    # Vendor ID for Nordic Semiconductor (used by Somewear devices)
    NORDIC_VENDOR_ID = "0x1915"
    SOMEWEAR_MANUFACTURER = "Somewear"

    def __init__(self):
        self.nodes: List[SomewearNode] = []

    def detect_nodes(self) -> List[SomewearNode]:
        """
        Detect all Somewear nodes connected via USB.
        Includes nodes in both normal and bootloader mode.

        Returns:
            List of SomewearNode objects representing connected devices.
        """
        self.nodes = []

        # Get USB device info from system_profiler
        usb_devices = self._get_usb_devices()

        # Get serial device paths
        serial_devices = self._get_serial_devices()

        # Track which serial devices have been matched
        matched_serial_devices = set()

        # Match USB devices with serial paths
        for device in usb_devices:
            print(u"Checking device: {}".format(device.get('product_name', 'Unknown')))
            is_somewear, is_bootloader = self._is_somewear_device(device)
            if is_somewear:
                serial_path = self._find_serial_path(
                    device.get('serial_number', ''),
                    serial_devices,
                    is_bootloader
                )
                if serial_path:
                    matched_serial_devices.add(serial_path)
                    node = SomewearNode(
                        serial_number=device.get('serial_number', 'Unknown'),
                        device_path=serial_path,
                        product_id=device.get('product_id', 'Unknown'),
                        vendor_id=device.get('vendor_id', 'Unknown'),
                        product_name=device.get('product_name', 'Unknown'),
                        manufacturer=device.get('manufacturer', 'Unknown'),
                        version=device.get('version', 'Unknown'),
                        bootloader_mode=is_bootloader
                    )
                    self.nodes.append(node)

        # Check for unmatched numeric-only paths (likely bootloader nodes)
        print(u"\nChecking for bootloader nodes in unmatched serial devices...")
        print(u"Total serial devices: {}".format(len(serial_devices)))
        print(u"Matched serial devices: {}".format(len(matched_serial_devices)))

        for serial_path in serial_devices:
            if serial_path not in matched_serial_devices:
                print(u"  Unmatched path: {}".format(serial_path))
                # Check if it's a numeric-only path pattern (bootloader)
                basename = serial_path.split('/')[-1]
                if basename.startswith('cu.usbmodem'):
                    suffix = basename.replace('cu.usbmodem', '')
                    print(u"    Suffix: '{}', Is digit: {}".format(suffix, suffix.isdigit()))
                    # If suffix is numeric-only, it's likely a bootloader node
                    if suffix and suffix.isdigit():
                        print(u"    -> Detected as BOOTLOADER node")
                        node = SomewearNode(
                            serial_number='Unknown',
                            device_path=serial_path,
                            product_id='Unknown',
                            vendor_id=self.NORDIC_VENDOR_ID,
                            product_name='Nordic Bootloader',
                            manufacturer='Nordic',
                            version='Unknown',
                            bootloader_mode=True
                        )
                        self.nodes.append(node)

        print(u"\nTotal nodes detected: {}".format(len(self.nodes)))
        for node in self.nodes:
            print(u"  - {} @ {} (bootloader: {})".format(
                node.product_name,
                node.device_path,
                node.bootloader_mode
            ))

        return self.nodes

    def _get_usb_devices(self) -> List[dict]:
        """
        Get USB device information from system_profiler.

        Returns:
            List of dictionaries containing USB device information.
        """
        try:
            result = subprocess.run(
                ['system_profiler', 'SPUSBDataType'],
                capture_output=True,
                text=True,
                check=True
            )
            return self._parse_usb_output(result.stdout)
        except subprocess.CalledProcessError as e:
            print(f"Error running system_profiler: {e}")
            return []
        except FileNotFoundError:
            print("system_profiler not found (not on macOS?)")
            return []

    def _parse_usb_output(self, output: str) -> List[dict]:
        """
        Parse system_profiler SPUSBDataType output.

        Args:
            output: Raw output from system_profiler command.

        Returns:
            List of dictionaries containing parsed device information.
        """
        devices = []
        current_device = {}

        for line in output.split('\n'):
            # Device name line (product name)
            if line.strip() and line.strip().endswith(':') and not line.strip().startswith('Product'):
                # Save previous device if it exists
                if current_device and 'product_name' in current_device:
                    devices.append(current_device)
                # Start new device
                current_device = {
                    'product_name': line.strip().rstrip(':').strip()
                }

            # Parse device attributes
            elif 'Product ID:' in line:
                match = re.search(r'Product ID:\s+(0x[0-9a-fA-F]+)', line)
                if match:
                    current_device['product_id'] = match.group(1)

            elif 'Vendor ID:' in line:
                match = re.search(r'Vendor ID:\s+(0x[0-9a-fA-F]+)\s*(?:\((.+?)\))?', line)
                if match:
                    current_device['vendor_id'] = match.group(1)
                    if match.group(2):
                        current_device['vendor_name'] = match.group(2)

            elif 'Serial Number:' in line:
                match = re.search(r'Serial Number:\s+(.+)', line)
                if match:
                    current_device['serial_number'] = match.group(1).strip()

            elif 'Manufacturer:' in line:
                match = re.search(r'Manufacturer:\s+(.+)', line)
                if match:
                    current_device['manufacturer'] = match.group(1).strip()

            elif 'Version:' in line:
                match = re.search(r'Version:\s+(.+)', line)
                if match:
                    current_device['version'] = match.group(1).strip()

        # Add last device
        if current_device and 'product_name' in current_device:
            devices.append(current_device)

        return devices

    def _get_serial_devices(self) -> List[str]:
        """
        Get list of serial device paths.

        Returns:
            List of serial device paths (e.g., /dev/cu.usbmodemXXX, /dev/tty.usbmodemXXX).
        """
        try:
            # Check both cu.* and tty.* devices
            cu_devices = glob.glob('/dev/cu.usb*')
            tty_devices = glob.glob('/dev/tty.usb*')
            all_devices = cu_devices + tty_devices

            print(u"Found serial devices: {}".format(all_devices))

            return [d for d in all_devices if d]  # Filter out empty strings
        except Exception as e:
            print(f"Error listing serial devices: {e}")
            return []

    def _is_somewear_device(self, device: dict) -> Tuple[bool, bool]:
        """
        Check if a USB device is a Somewear node and if it's in bootloader mode.

        Args:
            device: Dictionary containing device information.

        Returns:
            Tuple of (is_somewear, is_bootloader)
        """
        product_name = device.get('product_name', '').lower()
        manufacturer = device.get('manufacturer', '')
        vendor_id = device.get('vendor_id', '')

        # Check for bootloader mode (Mynewt Boot or similar Nordic bootloader)
        if vendor_id == self.NORDIC_VENDOR_ID:
            if 'boot' in product_name or 'mynewt' in product_name:
                return (True, True)

        # Check for Somewear manufacturer (normal mode)
        if manufacturer == self.SOMEWEAR_MANUFACTURER:
            return (True, False)

        # Check for Nordic Semiconductor vendor ID (backup check for normal mode)
        if vendor_id == self.NORDIC_VENDOR_ID:
            # Make sure it's not an Android device
            if 'android' not in product_name and device.get('serial_number'):
                return (True, False)

        return (False, False)

    def _find_serial_path(
        self,
        serial_number: str,
        serial_devices: List[str],
        is_bootloader: bool = False
    ) -> Optional[str]:
        """
        Find the serial device path for a given serial number.

        Args:
            serial_number: Device serial number.
            serial_devices: List of available serial device paths.
            is_bootloader: True if device is in bootloader mode.

        Returns:
            Serial device path if found, None otherwise.
        """
        if is_bootloader:
            # In bootloader mode, the serial number may not be in the path
            # Return any usbmodem device that hasn't been matched yet
            # For now, return the first available numeric-only path
            used_paths = [node.device_path for node in self.nodes]
            for device_path in serial_devices:
                if device_path not in used_paths:
                    # Check if it's a numeric-only path (typical for bootloader)
                    # e.g., /dev/cu.usbmodem2144301
                    basename = device_path.split('/')[-1]
                    if basename.startswith('cu.usbmodem'):
                        # Extract the part after 'cu.usbmodem'
                        suffix = basename.replace('cu.usbmodem', '')
                        if suffix.isdigit():
                            return device_path

        # Normal mode: Serial number is typically embedded in the device path
        # e.g., /dev/cu.usbmodemNGBDELYC20381
        if serial_number:
            for device_path in serial_devices:
                if serial_number in device_path:
                    return device_path

        return None

    def get_node_by_serial(self, serial_number: str) -> Optional[SomewearNode]:
        """
        Get a specific node by serial number.

        Args:
            serial_number: Device serial number (can be partial match).

        Returns:
            SomewearNode if found, None otherwise.
        """
        for node in self.nodes:
            if serial_number in node.serial_number:
                return node
        return None

    def print_nodes(self):
        """Print all detected nodes in a formatted list."""
        if not self.nodes:
            print("No Somewear nodes detected.")
            return

        print(f"\nDetected {len(self.nodes)} Somewear node(s):\n")
        for i, node in enumerate(self.nodes, 1):
            mode_indicator = " [BOOTLOADER MODE]" if node.bootloader_mode else ""
            print(f"{i}. {node.product_name}{mode_indicator}")
            print(f"   Serial Number: {node.serial_number}")

            # Display board version and hardware flavor (only for normal mode)
            if not node.bootloader_mode:
                board_version = node.get_board_version()
                hardware_flavor = node.get_hardware_flavor()
                if board_version is not None:
                    print(f"   Board Version: PC{board_version:02d}")
                if hardware_flavor is not None:
                    print(f"   HW Flavor:     {hardware_flavor}")

            print(f"   Device Path:   {node.device_path}")
            print(f"   Vendor ID:     {node.vendor_id}")
            print(f"   Product ID:    {node.product_id}")
            print(f"   Version:       {node.version}")
            print()


def main():
    """CLI entry point for testing."""
    detector = USBNodeDetector()
    nodes = detector.detect_nodes()
    detector.print_nodes()

    # Return JSON for scripting
    if nodes:
        nodes_dict = [
            {
                'serial_number': n.serial_number,
                'device_path': n.device_path,
                'product_name': n.product_name,
                'product_id': n.product_id,
                'vendor_id': n.vendor_id,
                'version': n.version,
                'bootloader_mode': n.bootloader_mode,
                'board_version': n.get_board_version() if not n.bootloader_mode else None,
                'hardware_flavor': n.get_hardware_flavor() if not n.bootloader_mode else None
            }
            for n in nodes
        ]
        print(json.dumps(nodes_dict, indent=2))


if __name__ == '__main__':
    main()
