bcms 0.1.12

Bluetooth Client Manager Service for IoT device management
Documentation
use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;

/// Device information stored in memory
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DeviceInfo {
    pub address: String,
    pub name: String,
    pub approved: bool,
    pub paired: bool,
    pub last_seen: Option<i64>,
    pub connection_type: Option<String>,
    /// Backend device ID from API registration (None if not registered)
    pub backend_device_id: Option<String>,
    /// Bluetooth Class of Device (CoD)
    #[serde(default)]
    pub device_class: u32,
    /// BLE Appearance value
    #[serde(default)]
    pub appearance: u16,
    /// Current connection state
    #[serde(default)]
    pub connected: bool,
}

impl DeviceInfo {
    pub fn new(address: String, name: String) -> Self {
        Self {
            address,
            name,
            approved: false,
            paired: false,
            last_seen: None,
            connection_type: None,
            backend_device_id: None,
            device_class: 0,
            appearance: 0,
            connected: false,
        }
    }

    pub fn with_type(address: String, name: String, connection_type: String) -> Self {
        Self {
            address,
            name,
            approved: false,
            paired: false,
            last_seen: None,
            connection_type: Some(connection_type),
            backend_device_id: None,
            device_class: 0,
            appearance: 0,
            connected: false,
        }
    }
}

/// Working mode for device management
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub enum WorkingMode {
    Any,
    Approved,
}

impl Default for WorkingMode {
    fn default() -> Self {
        WorkingMode::Approved
    }
}

/// Device memory manager - equivalent to Python devices_memory.py
#[derive(Debug)]
pub struct DeviceMemory {
    devices: Arc<RwLock<HashMap<String, DeviceInfo>>>,
    working_mode: Arc<RwLock<WorkingMode>>,
    known_devices_file: Option<PathBuf>,
}

impl DeviceMemory {
    pub fn new(known_devices_file: Option<PathBuf>) -> Self {
        Self {
            devices: Arc::new(RwLock::new(HashMap::new())),
            working_mode: Arc::new(RwLock::new(WorkingMode::default())),
            known_devices_file,
        }
    }

    /// Load devices from file if it exists
    pub async fn load_devices(&self) -> Result<()> {
        if let Some(file_path) = &self.known_devices_file {
            if file_path.exists() {
                let content = tokio::fs::read_to_string(file_path).await?;
                let devices: HashMap<String, DeviceInfo> = serde_json::from_str(&content)?;

                let mut device_lock = self.devices.write().await;
                *device_lock = devices;
            }
        }
        Ok(())
    }

    /// Save devices to file
    pub async fn save_devices(&self) -> Result<()> {
        if let Some(file_path) = &self.known_devices_file {
            if let Some(parent) = file_path.parent() {
                tokio::fs::create_dir_all(parent).await?;
            }

            let devices = self.devices.read().await;
            let content = serde_json::to_string_pretty(&*devices)?;
            tokio::fs::write(file_path, content).await?;
        }
        Ok(())
    }

    /// Add or update a device (preserves approval/pairing status for existing devices)
    pub async fn add_device(&self, device: DeviceInfo) -> Result<()> {
        let mut devices = self.devices.write().await;
        let address = device.address.clone();
        let is_new = !devices.contains_key(&address);

        if is_new {
            // New device - insert as-is
            devices.insert(address.clone(), device);
            drop(devices);

            // Save devices asynchronously without blocking the discovery loop
            if let Err(e) = self.save_devices().await {
                tracing::warn!("Failed to save devices to file: {}", e);
            }
            tracing::debug!("Added new device to memory: {}", address);
        } else {
            // Existing device - preserve approval, pairing status, and last seen
            if let Some(existing_device) = devices.get_mut(&address) {
                let original_approved = existing_device.approved;
                let original_paired = existing_device.paired;
                let original_backend_id = existing_device.backend_device_id.clone();
                let original_last_seen = existing_device.last_seen;

                // Update with new device info but preserve critical status
                *existing_device = device;
                existing_device.approved = original_approved;
                existing_device.paired = original_paired;
                existing_device.backend_device_id = original_backend_id;
                existing_device.last_seen = original_last_seen;
            }
            drop(devices);
            tracing::trace!("Updated existing device (preserved approval/pairing): {}", address);
        }
        Ok(())
    }

    /// Remove a device
    pub async fn remove_device(&self, address: &str) -> Result<bool> {
        let mut devices = self.devices.write().await;
        let removed = devices.remove(address).is_some();
        drop(devices);

        if removed {
            self.save_devices().await?;
        }
        Ok(removed)
    }

    /// Get a device by address
    pub async fn get_device(&self, address: &str) -> Option<DeviceInfo> {
        let devices = self.devices.read().await;
        devices.get(address).cloned()
    }

    /// Get all devices
    pub async fn get_all_devices(&self) -> Vec<DeviceInfo> {
        let devices = self.devices.read().await;
        devices.values().cloned().collect()
    }

    /// Get only approved devices
    pub async fn get_approved_devices(&self) -> Vec<DeviceInfo> {
        let devices = self.devices.read().await;
        devices.values()
            .filter(|device| device.approved)
            .cloned()
            .collect()
    }

    /// Approve a device
    pub async fn approve_device(&self, address: &str) -> Result<bool> {
        let mut devices = self.devices.write().await;
        if let Some(device) = devices.get_mut(address) {
            device.approved = true;
            drop(devices);
            self.save_devices().await?;
            Ok(true)
        } else {
            Ok(false)
        }
    }

    /// Set device paired status
    pub async fn set_device_paired(&self, address: &str, paired: bool) -> Result<bool> {
        let mut devices = self.devices.write().await;
        if let Some(device) = devices.get_mut(address) {
            device.paired = paired;
            drop(devices);
            self.save_devices().await?;
            Ok(true)
        } else {
            Ok(false)
        }
    }

    /// Check if device is paired
    pub async fn is_device_paired(&self, address: &str) -> bool {
        let devices = self.devices.read().await;
        devices.get(address)
            .map(|device| device.paired)
            .unwrap_or(false)
    }

    /// Update device from discovery (only updates discoverable fields)
    pub async fn update_device_from_discovery(
        &self,
        address: String,
        name: String,
        timestamp: i64,
        device_class: u32,
        appearance: u16,
    ) -> Result<()> {
        let mut devices = self.devices.write().await;
        let is_new = !devices.contains_key(&address);

        if is_new {
            // New device discovered
            let device = DeviceInfo {
                address: address.clone(),
                name: name.clone(),
                approved: false,
                paired: false,
                last_seen: Some(timestamp),
                connection_type: None,
                backend_device_id: None,
                device_class,
                appearance,
                connected: false,
            };
            devices.insert(address.clone(), device);
            drop(devices);

            // Save new device
            if let Err(e) = self.save_devices().await {
                tracing::warn!("Failed to save devices to file: {}", e);
            }
            tracing::debug!("Discovered new device: {} ({})", name, address);
        } else {
            // Existing device - only update discoverable fields
            if let Some(device) = devices.get_mut(&address) {
                device.name = name; // Update name in case it changed
                device.last_seen = Some(timestamp);
                device.device_class = device_class;
                device.appearance = appearance;
                // Preserve approved and paired status
            }
            drop(devices);
            tracing::trace!("Updated discovery info for existing device: {}", address);
        }
        Ok(())
    }

    /// Update device last seen timestamp
    pub async fn update_last_seen(&self, address: &str, timestamp: i64) -> Result<()> {
        let mut devices = self.devices.write().await;
        if let Some(device) = devices.get_mut(address) {
            device.last_seen = Some(timestamp);
        }
        Ok(())
    }

    /// Update device connection status
    pub async fn set_device_connected(&self, address: &str, connected: bool) -> Result<bool> {
        let mut devices = self.devices.write().await;
        if let Some(device) = devices.get_mut(address) {
            device.connected = connected;
            Ok(true)
        } else {
            Ok(false)
        }
    }

    /// Get working mode
    pub async fn get_working_mode(&self) -> WorkingMode {
        *self.working_mode.read().await
    }

    /// Set working mode
    pub async fn set_working_mode(&self, mode: WorkingMode) -> Result<()> {
        let mut working_mode = self.working_mode.write().await;
        *working_mode = mode;
        Ok(())
    }

    /// Check if device should be connected based on working mode
    pub async fn should_connect_to_device(&self, address: &str) -> bool {
        let mode = self.get_working_mode().await;
        match mode {
            WorkingMode::Any => true,
            WorkingMode::Approved => {
                if let Some(device) = self.get_device(address).await {
                    device.approved
                } else {
                    false
                }
            }
        }
    }

    /// Get devices filtered by approval status
    pub async fn get_devices_filtered(&self, only_approved: bool) -> Vec<DeviceInfo> {
        let all_devices = self.get_all_devices().await;
        tracing::debug!("get_devices_filtered: only_approved={}, total_devices={}", only_approved, all_devices.len());

        if only_approved {
            let approved = self.get_approved_devices().await;
            tracing::debug!("  Returning {} approved devices", approved.len());
            approved
        } else {
            tracing::debug!("  Returning {} total devices", all_devices.len());
            all_devices
        }
    }

    /// Set backend device ID for a device
    pub async fn set_backend_device_id(&self, address: &str, backend_id: String) -> Result<bool> {
        let mut devices = self.devices.write().await;
        if let Some(device) = devices.get_mut(address) {
            device.backend_device_id = Some(backend_id);
            drop(devices);
            self.save_devices().await?;
            Ok(true)
        } else {
            Ok(false)
        }
    }

    /// Get devices that are registered with the backend (have backend device IDs)
    pub async fn get_registered_devices(&self) -> Vec<crate::api::RegisteredDeviceInfo> {
        let devices = self.devices.read().await;
        devices.values()
            .filter_map(|device| {
                device.backend_device_id.as_ref().map(|backend_id| {
                    crate::api::RegisteredDeviceInfo {
                        address: device.address.clone(),
                        backend_device_id: Some(backend_id.clone()),
                        approved: device.approved,
                    }
                })
            })
            .collect()
    }

    /// Get all devices as RegisteredDeviceInfo (including those without backend IDs)
    pub async fn get_all_devices_as_registered(&self) -> Vec<crate::api::RegisteredDeviceInfo> {
        let devices = self.devices.read().await;
        devices.values()
            .map(|device| crate::api::RegisteredDeviceInfo {
                address: device.address.clone(),
                backend_device_id: device.backend_device_id.clone(),
                approved: device.approved,
            })
            .collect()
    }

    /// Add or update a device with auto-approval (for RPC submissions)
    pub async fn add_device_with_auto_approval(&self, address: &str, name: &str, connection_type: Option<String>) -> Result<()> {
        let mut device = if let Some(connection_type) = connection_type {
            DeviceInfo::with_type(address.to_string(), name.to_string(), connection_type)
        } else {
            DeviceInfo::new(address.to_string(), name.to_string())
        };

        // Auto-approve the device
        device.approved = true;

        self.add_device(device).await
    }
}

impl Default for DeviceMemory {
    fn default() -> Self {
        Self::new(None)
    }
}