"""Update Service Layer. This service provides software update operations that can be consumed by multiple interfaces (REST API, BLE GATT, etc.). """ from dataclasses import asdict from typing import Optional, Dict, Any, List from ..managers.update_manager import ( UpdateManager, UpdateProgress, UpdateStatus, ComponentId, ) from .pm3_service import PM3ServiceError, PM3ServiceResult class UpdateService: """Update service layer for software update management. This service can be used by multiple interfaces: - REST API (FastAPI endpoints) - BLE GATT (Bluetooth handlers) - Plugin system (future) It encapsulates: - Update checking - Update downloading - Update installation - Progress monitoring """ def __init__(self, update_manager: Optional[UpdateManager] = None): """Initialize update service. Args: update_manager: Update manager instance (creates new if not provided) """ from ..managers.update_manager import get_update_manager self.update_manager = update_manager or get_update_manager() async def check_for_updates(self) -> PM3ServiceResult: """Check for available updates. Returns: PM3ServiceResult with update availability and information """ try: result = await self.update_manager.check_for_updates() return PM3ServiceResult( success=True, data=result ) except Exception as e: return PM3ServiceResult( success=False, error=PM3ServiceError( code="update_check_error", message="Failed to check for updates", details=str(e) ) ) async def download_update(self) -> PM3ServiceResult: """Download the latest available update. Returns: PM3ServiceResult indicating download success/failure """ try: success = await self.update_manager.download_update() if success: return PM3ServiceResult( success=True, data={ "message": "Update downloaded successfully", "ready_to_install": True } ) else: return PM3ServiceResult( success=False, error=PM3ServiceError( code="download_failed", message="Update download failed" ) ) except ValueError as e: # No update available return PM3ServiceResult( success=False, error=PM3ServiceError( code="no_update_available", message=str(e) ) ) except Exception as e: return PM3ServiceResult( success=False, error=PM3ServiceError( code="update_download_error", message="Error downloading update", details=str(e) ) ) async def install_update(self) -> PM3ServiceResult: """Install the downloaded update. Returns: PM3ServiceResult indicating installation success/failure """ try: success = await self.update_manager.install_update() if success: return PM3ServiceResult( success=True, data={ "message": "Update installed successfully", "requires_restart": True } ) else: return PM3ServiceResult( success=False, error=PM3ServiceError( code="installation_failed", message="Update installation failed" ) ) except ValueError as e: # No update downloaded return PM3ServiceResult( success=False, error=PM3ServiceError( code="no_update_downloaded", message=str(e) ) ) except Exception as e: return PM3ServiceResult( success=False, error=PM3ServiceError( code="update_install_error", message="Error installing update", details=str(e) ) ) async def get_progress(self) -> PM3ServiceResult: """Get current update progress. Returns: PM3ServiceResult with progress information """ try: progress = await self.update_manager.get_progress() comp_list = [] for comp_id, cp in progress.component_progress.items(): comp_list.append({ "component_id": cp.component_id, "status": cp.status.value, "download_progress": cp.download_progress, "error_message": cp.error_message, }) return PM3ServiceResult( success=True, data={ "status": progress.status.value, "current_version": progress.current_version, "available_version": progress.available_version, "download_progress": progress.download_progress, "error_message": progress.error_message, "last_check": progress.last_check, "active_component": progress.active_component, "components": comp_list, } ) except Exception as e: return PM3ServiceResult( success=False, error=PM3ServiceError( code="progress_error", message="Failed to get update progress", details=str(e) ) ) async def get_release_notes( self, version: Optional[str] = None ) -> PM3ServiceResult: """Get release notes for a specific version or latest. Args: version: Version to get notes for (latest if None) Returns: PM3ServiceResult with release notes """ try: notes = await self.update_manager.get_release_notes(version) return PM3ServiceResult( success=True, data={ "version": version or "latest", "release_notes": notes } ) except Exception as e: return PM3ServiceResult( success=False, error=PM3ServiceError( code="release_notes_error", message="Failed to get release notes", details=str(e) ) ) async def check_and_download_update(self) -> PM3ServiceResult: """Combined operation: check for updates and download if available. Returns: PM3ServiceResult with check and download status """ try: # First check for updates check_result = await self.check_for_updates() if not check_result.success: return check_result # If update available, download it if check_result.data.get("update_available"): download_result = await self.download_update() if download_result.success: return PM3ServiceResult( success=True, data={ "message": "Update checked and downloaded", "update_info": check_result.data, "ready_to_install": True } ) else: return download_result else: return PM3ServiceResult( success=True, data={ "message": "System is up to date", "update_available": False, "current_version": check_result.data.get("current_version") } ) except Exception as e: return PM3ServiceResult( success=False, error=PM3ServiceError( code="check_download_error", message="Error checking and downloading update", details=str(e) ) ) async def full_update(self) -> PM3ServiceResult: """Full update workflow: check, download, and install. Returns: PM3ServiceResult with complete update status """ try: # Check for updates check_result = await self.check_for_updates() if not check_result.success: return check_result if not check_result.data.get("update_available"): return PM3ServiceResult( success=True, data={ "message": "System is already up to date", "update_performed": False } ) # Download update download_result = await self.download_update() if not download_result.success: return download_result # Install update install_result = await self.install_update() if not install_result.success: return install_result return PM3ServiceResult( success=True, data={ "message": "Update completed successfully", "update_performed": True, "previous_version": check_result.data.get("current_version"), "new_version": check_result.data.get("latest_version"), "requires_restart": True } ) except Exception as e: return PM3ServiceResult( success=False, error=PM3ServiceError( code="full_update_error", message="Error performing full update", details=str(e) ) ) # ------------------------------------------------------------------ # Component-level operations # ------------------------------------------------------------------ async def get_installed_components(self) -> PM3ServiceResult: """Get the installed component manifest. Returns: PM3ServiceResult with component manifest data """ try: manifest = self.update_manager.get_manifest() components = {} for comp_id, comp in manifest.components.items(): components[comp_id] = asdict(comp) return PM3ServiceResult( success=True, data={ "schema_version": manifest.schema_version, "system_version": manifest.system_version, "components": components, "plugins": manifest.plugins, "last_updated": manifest.last_updated, } ) except Exception as e: return PM3ServiceResult( success=False, error=PM3ServiceError( code="manifest_error", message="Failed to get component manifest", details=str(e) ) ) async def download_component(self, component_id: str) -> PM3ServiceResult: """Download a specific component update. Args: component_id: Component to download (pm3, frontend, backend, theme) Returns: PM3ServiceResult indicating download success/failure """ try: success = await self.update_manager.download_component(component_id) return PM3ServiceResult( success=True, data={ "message": f"Component {component_id} downloaded successfully", "component_id": component_id, "ready_to_install": True, } ) except ValueError as e: return PM3ServiceResult( success=False, error=PM3ServiceError( code="component_not_available", message=str(e) ) ) except Exception as e: return PM3ServiceResult( success=False, error=PM3ServiceError( code="component_download_error", message=f"Error downloading {component_id}", details=str(e) ) ) async def install_component(self, component_id: str) -> PM3ServiceResult: """Install a downloaded component update. Args: component_id: Component to install Returns: PM3ServiceResult indicating install success/failure """ try: success = await self.update_manager.install_component(component_id) return PM3ServiceResult( success=True, data={ "message": f"Component {component_id} installed successfully", "component_id": component_id, "requires_restart": component_id in ( ComponentId.BACKEND, ComponentId.PM3 ), } ) except ValueError as e: return PM3ServiceResult( success=False, error=PM3ServiceError( code="component_install_error", message=str(e) ) ) except Exception as e: return PM3ServiceResult( success=False, error=PM3ServiceError( code="component_install_error", message=f"Error installing {component_id}", details=str(e) ) ) async def rollback_component(self, component_id: str) -> PM3ServiceResult: """Rollback a component to its previous version. Args: component_id: Component to rollback Returns: PM3ServiceResult indicating rollback success/failure """ try: success = await self.update_manager.rollback_component(component_id) return PM3ServiceResult( success=True, data={ "message": f"Component {component_id} rolled back successfully", "component_id": component_id, } ) except ValueError as e: return PM3ServiceResult( success=False, error=PM3ServiceError( code="rollback_error", message=str(e) ) ) except Exception as e: return PM3ServiceResult( success=False, error=PM3ServiceError( code="rollback_error", message=f"Error rolling back {component_id}", details=str(e) ) )