"""PM3 Service Layer.

This service encapsulates all PM3-related business logic and is used by
both REST API and BLE GATT handlers to avoid code duplication.
"""
from typing import Optional, Dict, Any, List
from dataclasses import dataclass

from ..workers.pm3_worker import PM3Worker, PM3CommandResult
from ..managers.session_manager import SessionManager
from ..managers.pm3_device_manager import PM3DeviceManager, PM3Device, DeviceStatus


@dataclass
class PM3ServiceError:
    """Error response from PM3 service."""
    code: str  # "session_locked", "pm3_not_connected", "command_failed"
    message: str
    details: Optional[str] = None


@dataclass
class PM3ServiceResult:
    """Result from PM3 service operations."""
    success: bool
    data: Optional[Dict[str, Any]] = None
    error: Optional[PM3ServiceError] = None


class PM3Service:
    """PM3 service layer for command execution and status queries.

    This service can be used by multiple interfaces:
    - REST API (FastAPI endpoints)
    - BLE GATT (Bluetooth handlers)
    - Plugin system (future)

    It encapsulates:
    - Session validation
    - PM3 command execution
    - Session management
    - Response formatting
    """

    def __init__(
        self,
        pm3_worker: Optional[PM3Worker] = None,
        session_manager: Optional[SessionManager] = None,
        device_manager: Optional[PM3DeviceManager] = None
    ):
        """Initialize PM3 service.

        Args:
            pm3_worker: PM3 worker instance (legacy, kept for backward compatibility)
            session_manager: Session manager instance (creates new if not provided)
            device_manager: PM3 device manager for multi-device support (optional)
        """
        self.pm3_worker = pm3_worker or PM3Worker()  # Legacy single-device support
        self.session_manager = session_manager or SessionManager()
        self.device_manager = device_manager  # Multi-device support (optional)

    async def execute_command(
        self,
        command: str,
        session_id: Optional[str] = None,
        timeout: Optional[int] = None,
        device_id: Optional[str] = None
    ) -> PM3ServiceResult:
        """Execute a PM3 command with session validation.

        This method handles:
        1. Session validation
        2. Command execution
        3. Session activity updates
        4. Error handling

        Args:
            command: PM3 command to execute
            session_id: Optional session ID for multi-user support
            timeout: Optional command timeout in seconds
            device_id: Optional device ID for multi-device support (uses legacy worker if not provided)

        Returns:
            PM3ServiceResult with success status and data/error
        """
        # Determine which worker to use
        worker = None
        device_path = None

        if device_id and self.device_manager:
            # Multi-device mode: get device from device manager
            device = await self.device_manager.get_device(device_id)
            if not device:
                return PM3ServiceResult(
                    success=False,
                    error=PM3ServiceError(
                        code="device_not_found",
                        message=f"Device {device_id} not found",
                        details="Device may have been disconnected"
                    )
                )

            if not device.worker:
                return PM3ServiceResult(
                    success=False,
                    error=PM3ServiceError(
                        code="device_no_worker",
                        message=f"Device {device_id} has no worker instance",
                        details="Device may not be fully initialized"
                    )
                )

            worker = device.worker
            device_path = device.device_path
        else:
            # Legacy single-device mode
            worker = self.pm3_worker
            device_path = self.pm3_worker.device_path

        # 1. Validate session (per-device)
        if not self.session_manager.can_execute(session_id, device_id):
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="session_locked",
                    message=f"Another session is active for device {device_id or 'default'}",
                    details="Please take over the session or wait for it to expire"
                )
            )

        # 2. Check PM3 connection
        if not await worker.is_connected():
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="pm3_not_connected",
                    message="Proxmark3 not connected",
                    details=f"Device path: {device_path}"
                )
            )

        # 3. Trigger plugin hooks (non-critical)
        try:
            from ..managers.plugin_manager import get_plugin_manager
            await get_plugin_manager().trigger_hook("pm3_command", command)
        except Exception:
            pass

        # 4. Execute command
        try:
            result = await worker.execute_command(command)

            # 5. Update session activity
            if session_id:
                self.session_manager.update_activity(session_id, device_id)

            # 6. Return result
            if result.success:
                return PM3ServiceResult(
                    success=True,
                    data={
                        "output": result.output,
                        "command": command,
                        "session_id": session_id,
                        "device_id": device_id
                    }
                )
            else:
                # Include both error message and output for better diagnostics
                # PM3 CLI often outputs error details to stdout
                error_details = result.error or ""
                if result.output:
                    error_details = f"{error_details}\nOutput: {result.output}" if error_details else result.output
                return PM3ServiceResult(
                    success=False,
                    error=PM3ServiceError(
                        code="command_failed",
                        message="PM3 command failed",
                        details=error_details
                    )
                )

        except Exception as e:
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="execution_error",
                    message="Command execution error",
                    details=str(e)
                )
            )

    async def get_status(self, device_id: Optional[str] = None) -> PM3ServiceResult:
        """Get PM3 device status.

        Args:
            device_id: Optional device ID. If None and device_manager exists, returns all devices.
                      If None and no device_manager, returns legacy single device status.

        Returns:
            PM3ServiceResult with connection status, version, etc.
        """
        try:
            # Multi-device mode: return all devices or specific device
            if device_id is None and self.device_manager:
                # Return status for all devices
                devices = await self.device_manager.get_all_devices()
                return PM3ServiceResult(
                    success=True,
                    data={
                        "devices": [
                            {
                                "device_id": d.device_id,
                                "device_path": d.device_path,
                                "friendly_name": d.friendly_name or d.device_path.split('/')[-1],
                                "serial_number": d.serial_number,
                                "connected": d.status == DeviceStatus.CONNECTED,
                                "in_use": d.status == DeviceStatus.IN_USE,
                                "status": d.status.value,
                                "firmware_info": {
                                    "bootrom_version": d.firmware_info.bootrom_version,
                                    "os_version": d.firmware_info.os_version,
                                    "compatible": d.firmware_info.compatible,
                                },
                                "last_seen": d.last_seen.isoformat(),
                            }
                            for d in devices
                        ]
                    }
                )
            elif device_id and self.device_manager:
                # Return status for specific device
                device = await self.device_manager.get_device(device_id)
                if not device:
                    return PM3ServiceResult(
                        success=False,
                        error=PM3ServiceError(
                            code="device_not_found",
                            message=f"Device {device_id} not found"
                        )
                    )

                return PM3ServiceResult(
                    success=True,
                    data={
                        "device_id": device.device_id,
                        "device_path": device.device_path,
                        "friendly_name": device.friendly_name or device.device_path.split('/')[-1],
                        "serial_number": device.serial_number,
                        "connected": device.status == DeviceStatus.CONNECTED,
                        "in_use": device.status == DeviceStatus.IN_USE,
                        "status": device.status.value,
                        "firmware_info": {
                            "bootrom_version": device.firmware_info.bootrom_version,
                            "os_version": device.firmware_info.os_version,
                            "compatible": device.firmware_info.compatible,
                        },
                        "last_seen": device.last_seen.isoformat(),
                    }
                )
            else:
                # Legacy single-device mode
                is_connected = await self.pm3_worker.is_connected()
                version = None

                if is_connected:
                    # Try to get version info
                    result = await self.pm3_worker.execute_command("hw version")
                    if result.success:
                        version = result.output.split("\n")[0] if result.output else None

                return PM3ServiceResult(
                    success=True,
                    data={
                        "connected": is_connected,
                        "device_path": self.pm3_worker.device_path,
                        "version": version,
                        "session_active": self.session_manager.has_active_session()
                    }
                )
        except Exception as e:
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="status_error",
                    message="Failed to get PM3 status",
                    details=str(e)
                )
            )

    async def connect(self) -> PM3ServiceResult:
        """Connect to PM3 device.

        Returns:
            PM3ServiceResult indicating success/failure
        """
        try:
            await self.pm3_worker.connect()
            return PM3ServiceResult(
                success=True,
                data={"message": "Connected to Proxmark3"}
            )
        except Exception as e:
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="connection_error",
                    message="Failed to connect to PM3",
                    details=str(e)
                )
            )

    async def disconnect(self) -> PM3ServiceResult:
        """Disconnect from PM3 device.

        Returns:
            PM3ServiceResult indicating success/failure
        """
        try:
            await self.pm3_worker.disconnect()
            return PM3ServiceResult(
                success=True,
                data={"message": "Disconnected from Proxmark3"}
            )
        except Exception as e:
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="disconnection_error",
                    message="Failed to disconnect from PM3",
                    details=str(e)
                )
            )

    # Session management methods (for both REST and BLE)

    async def create_session(
        self,
        client_ip: str = "unknown",
        user_agent: Optional[str] = None,
        force_takeover: bool = False,
        device_id: Optional[str] = None
    ) -> PM3ServiceResult:
        """Create a new session for a device.

        Args:
            client_ip: Client IP address
            user_agent: Client user agent string
            force_takeover: Whether to forcefully take over existing session
            device_id: Device ID to create session for (None for legacy mode)

        Returns:
            PM3ServiceResult with session_id
        """
        success, session_id, error_msg = await self.session_manager.create_session(
            client_ip=client_ip,
            user_agent=user_agent,
            force_takeover=force_takeover,
            device_id=device_id
        )

        if success and session_id:
            return PM3ServiceResult(
                success=True,
                data={"session_id": session_id, "device_id": device_id}
            )
        else:
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="session_exists",
                    message=error_msg or "Session already exists",
                    details="Set force_takeover=true to take over"
                )
            )

    async def release_session(
        self,
        session_id: str,
        device_id: Optional[str] = None
    ) -> PM3ServiceResult:
        """Release a session.

        Args:
            session_id: Session ID to release
            device_id: Device ID (if known)

        Returns:
            PM3ServiceResult indicating success
        """
        released = await self.session_manager.release_session(session_id, device_id)
        if released:
            return PM3ServiceResult(
                success=True,
                data={"message": "Session released"}
            )
        else:
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="session_not_found",
                    message="Session not found or already released"
                )
            )

    def get_session_info(self, device_id: Optional[str] = None) -> PM3ServiceResult:
        """Get session information for a device.

        Args:
            device_id: Device ID to get session for (None for any active session)

        Returns:
            PM3ServiceResult with session details
        """
        session = self.session_manager.get_active_session(device_id)

        if session:
            return PM3ServiceResult(
                success=True,
                data={
                    "session_id": session.session_id,
                    "device_id": session.device_id,
                    "client_ip": session.client_ip,
                    "created_at": session.created_at,
                    "last_activity": session.last_activity,
                }
            )
        else:
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="session_not_found",
                    message=f"No active session for device {device_id or 'default'}"
                )
            )

    def get_all_sessions(self) -> PM3ServiceResult:
        """Get all active sessions.

        Returns:
            PM3ServiceResult with all session details
        """
        sessions = self.session_manager.get_all_active_sessions()
        return PM3ServiceResult(
            success=True,
            data={
                "sessions": [
                    {
                        "session_id": s.session_id,
                        "device_id": s.device_id,
                        "client_ip": s.client_ip,
                        "created_at": s.created_at,
                        "last_activity": s.last_activity,
                    }
                    for s in sessions.values()
                ]
            }
        )

    # Multi-device management methods

    async def list_devices(self) -> PM3ServiceResult:
        """List all discovered PM3 devices.

        Returns:
            PM3ServiceResult with list of all devices
        """
        if not self.device_manager:
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="device_manager_not_available",
                    message="Device manager not initialized",
                    details="Multi-device support is not enabled"
                )
            )

        try:
            devices = await self.device_manager.get_all_devices()
            return PM3ServiceResult(
                success=True,
                data={
                    "devices": [device.to_dict() for device in devices]
                }
            )
        except Exception as e:
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="list_devices_error",
                    message="Failed to list devices",
                    details=str(e)
                )
            )

    async def get_available_devices(self) -> PM3ServiceResult:
        """Get devices without active sessions (available for use).

        Returns:
            PM3ServiceResult with list of available devices
        """
        if not self.device_manager:
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="device_manager_not_available",
                    message="Device manager not initialized",
                    details="Multi-device support is not enabled"
                )
            )

        try:
            all_devices = await self.device_manager.get_all_devices()

            # Filter for devices that are CONNECTED (not IN_USE or other states)
            available_devices = [
                device for device in all_devices
                if device.status == DeviceStatus.CONNECTED
            ]

            return PM3ServiceResult(
                success=True,
                data={
                    "devices": [device.to_dict() for device in available_devices]
                }
            )
        except Exception as e:
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="get_available_devices_error",
                    message="Failed to get available devices",
                    details=str(e)
                )
            )

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

        Args:
            device_id: Device ID to identify
            duration_ms: Duration to blink LEDs in milliseconds (default: 2000)

        Returns:
            PM3ServiceResult indicating success
        """
        if not self.device_manager:
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="device_manager_not_available",
                    message="Device manager not initialized",
                    details="Multi-device support is not enabled"
                )
            )

        try:
            # Check if device exists
            device = await self.device_manager.get_device(device_id)
            if not device:
                return PM3ServiceResult(
                    success=False,
                    error=PM3ServiceError(
                        code="device_not_found",
                        message=f"Device {device_id} not found"
                    )
                )

            # Trigger LED identification
            await self.device_manager.identify_device(device_id, duration_ms)

            return PM3ServiceResult(
                success=True,
                data={
                    "message": f"Device {device_id} identified",
                    "device_path": device.device_path,
                    "duration_ms": duration_ms
                }
            )
        except Exception as e:
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="identify_device_error",
                    message="Failed to identify device",
                    details=str(e)
                )
            )

    async def flash_firmware(self, device_id: str) -> PM3ServiceResult:
        """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:
            PM3ServiceResult indicating success/failure with message
        """
        if not self.device_manager:
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="device_manager_not_available",
                    message="Device manager not initialized",
                    details="Multi-device support is not enabled"
                )
            )

        try:
            # Check if device exists
            device = await self.device_manager.get_device(device_id)
            if not device:
                return PM3ServiceResult(
                    success=False,
                    error=PM3ServiceError(
                        code="device_not_found",
                        message=f"Device {device_id} not found"
                    )
                )

            # Check if device is already flashing
            if device.status == DeviceStatus.FLASHING:
                return PM3ServiceResult(
                    success=False,
                    error=PM3ServiceError(
                        code="device_busy",
                        message="Device is already being flashed",
                        details="Please wait for current flash to complete"
                    )
                )

            # Flash firmware
            result = await self.device_manager.flash_firmware(device_id)

            if result["success"]:
                return PM3ServiceResult(
                    success=True,
                    data={
                        "message": result["message"],
                        "device_id": device_id
                    }
                )
            else:
                return PM3ServiceResult(
                    success=False,
                    error=PM3ServiceError(
                        code="flash_failed",
                        message="Firmware flash failed",
                        details=result.get("error", "Unknown error")
                    )
                )
        except Exception as e:
            return PM3ServiceResult(
                success=False,
                error=PM3ServiceError(
                    code="flash_error",
                    message="Failed to flash firmware",
                    details=str(e)
                )
            )
