Mountain/Air/
AirClient.rs

1//! # AirClient
2//!
3//! gRPC client wrapper for the Air daemon service, providing Mountain with
4//! access to cloud-based backend services including updates, authentication,
5//! file indexing, and system monitoring.
6//!
7//! IMPORTANT: The gRPC client is wrapped in Arc<Mutex<>> to enable safe
8//! concurrent access from multiple threads, as tonic's client methods require
9//! mutable access.
10//!
11//! ## RESPONSIBILITIES
12//!
13//! - **Connection Management**: Manage gRPC connection lifecycle to Air service
14//! - **Service Methods**: Implement all Air service RPC methods
15//! - **Error Translation**: Convert tonic/transport errors to CommonError
16//! - **Connection Retry**: (Optional) Provide automatic retry with backoff
17//! - **Health Checking**: Monitor Air service availability
18//!
19//! ## ARCHITECTURAL ROLE
20//!
21//! AirClient serves as the primary interface between Mountain and the Air
22//! backend service:
23//!
24//! ```text
25//! Mountain (Frontend) ──► AirClient ──► gRPC ──► Air Daemon (Backend)
26//! ```
27//!
28//! ### Position in Mountain
29//! - Communication module for Air integration
30//! - Part of the service management layer
31//! - Features-gated behind `AirIntegration` feature flag
32//!
33//! ## IMPLEMENTATION
34//!
35//! This implementation uses the generated gRPC client from the Air library:
36//! - `AirLibrary::Vine::Generated::air_service_client::AirServiceClient`
37//!
38//! ## CONFIGURATION
39//!
40//! - **Default Address**: `[::1]:50053` (configurable via constructor)
41//! - **Transport**: gRPC over TCP/IP with optional TLS
42//! - **Connection Pooling**: (TODO) Implement for multiple concurrent requests
43//!
44//! ## ERROR HANDLING
45//!
46//! All methods return `Result<T, CommonError>` with appropriate error types:
47//! - `IPCError`: gRPC communication failures
48//! - `SerializationError`: Message encoding/decoding failures
49//! - `Unknown`: Uncategorized errors
50//!
51//! ## THREAD SAFETY
52//!
53//! - `AirClient` is `Clone`able and can be shared across threads via
54//!   `Arc<AirClient>`
55//! - The underlying tonic client is thread-safe
56//! - All public methods are safe to call from multiple threads
57//!
58//! ## PERFORMANCE CONSIDERATIONS
59//!
60//! - Connection establishment is lazy (deferred until first use)
61//! - (TODO) Implement connection pooling for high-throughput scenarios
62//! - (TODO) Add request caching for frequently accessed data
63//! - (TODO) Implement request timeout configuration
64//!
65//! ## TODO
66//!
67//! High Priority:
68//! - [ ] Add connection retry with exponential backoff
69//! - [ ] Implement proper connection pooling
70//!
71//! Medium Priority:
72//! - [ ] Add request/response logging for debugging
73//! - [ ] Implement connection health monitoring
74//! - [ ] Add metrics collection for RPC calls
75//!
76//! ## MODULE CONTENTS
77//!
78//! - [`AirClient`]: Main client struct
79//! - [`DEFAULT_AIR_SERVER_ADDRESS`]: Default gRPC server address constant
80
81use std::{collections::HashMap, sync::Arc};
82use tokio::sync::Mutex;
83
84use CommonLibrary::Error::CommonError::CommonError;
85#[cfg(feature = "AirIntegration")]
86use AirLibrary::Vine::Generated::air::air_service_client::AirServiceClient;
87use futures_util::StreamExt;
88use log::{debug, error, info, warn};
89use tonic::{Request, transport::Channel};
90
91/// Default gRPC server address for the Air daemon.
92///
93/// Port Allocation:
94/// - 50051: Mountain Vine server
95/// - 50052: Cocoon Vine server (VS Code extension hosting)
96/// - 50053: Air Vine server (Air daemon services - authentication, updates, and
97///   more)
98pub const DEFAULT_AIR_SERVER_ADDRESS:&str = "[::1]:50053";
99
100/// Air gRPC client wrapper that handles connection to the Air daemon service.
101/// This provides a clean interface for Mountain to interact with Air's
102/// capabilities including update management, authentication, file indexing,
103/// and system monitoring.
104#[derive(Clone)]
105pub struct AirClient {
106	#[cfg(feature = "AirIntegration")]
107	/// The underlying tonic gRPC client wrapped in Arc<Mutex<>> for thread-safe access
108	client:Option<Arc<Mutex<AirServiceClient<Channel>>>>,
109	/// Address of the Air daemon
110	address:String,
111}
112
113impl AirClient {
114	/// Creates a new AirClient and connects to the Air daemon service.
115	///
116	/// # Arguments
117	/// * `address` - The gRPC server address (e.g., "http://\\[::1\\]:50053")
118	///
119	/// # Returns
120	/// * `Ok(Self)` - Successfully created client
121	/// * `Err(CommonError)` - Connection failure with descriptive error
122	///
123	/// # Example
124	///
125	/// ```text
126	/// use Mountain::Air::AirClient::{AirClient, DEFAULT_AIR_SERVER_ADDRESS};
127	///
128	/// # #[tokio::main]
129	/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
130	/// let client = AirClient::new(DEFAULT_AIR_SERVER_ADDRESS).await?;
131	/// # Ok(())
132	/// # }
133	/// ```
134	pub async fn new(address:&str) -> Result<Self, CommonError> {
135		info!("[AirClient] Connecting to Air daemon at: {}", address);
136
137		#[cfg(feature = "AirIntegration")]
138		{
139			let endpoint = address.parse::<tonic::transport::Endpoint>().map_err(|e| {
140				error!("[AirClient] Failed to parse address '{}': {}", address, e);
141				CommonError::IPCError { Description:format!("Invalid address '{}': {}", address, e) }
142			})?;
143
144			let channel = endpoint.connect().await.map_err(|e| {
145				error!("[AirClient] Failed to connect to Air daemon: {}", e);
146				CommonError::IPCError { Description:format!("Connection failed: {}", e) }
147			})?;
148
149			info!("[AirClient] Successfully connected to Air daemon at: {}", address);
150
151			let client = Arc::new(Mutex::new(AirServiceClient::new(channel)));
152			Ok(Self { client:Some(client), address:address.to_string() })
153		}
154
155		#[cfg(not(feature = "AirIntegration"))]
156		{
157			error!("[AirClient] AirIntegration feature is not enabled");
158			Err(CommonError::FeatureNotAvailable { FeatureName:"AirIntegration".to_string() })
159		}
160	}
161
162	/// Checks if the client is connected to the Air daemon.
163	///
164	/// # Returns
165	/// * `true` - Client is connected
166	/// * `false` - Client is not connected
167	pub fn is_connected(&self) -> bool {
168		#[cfg(feature = "AirIntegration")]
169		{
170			self.client.is_some()
171		}
172
173		#[cfg(not(feature = "AirIntegration"))]
174		{
175			false
176		}
177	}
178
179	/// Gets the address of the Air daemon.
180	///
181	/// # Returns
182	/// The address string
183	pub fn address(&self) -> &str { &self.address }
184
185	// =========================================================================
186	// Authentication Operations
187	// =========================================================================
188
189	/// Authenticates a user with the Air daemon.
190	///
191	/// # Arguments
192	/// * `username` - User's username
193	/// * `password` - User's password
194	/// * `provider` - Authentication provider (e.g., "github", "gitlab",
195	///   "microsoft")
196	///
197	/// # Returns
198	/// * `Ok(token)` - Authentication token if successful
199	/// * `Err(CommonError)` - Authentication failure
200	pub async fn authenticate(
201		&self,
202		request_id:String,
203		username:String,
204		password:String,
205		provider:String,
206	) -> Result<String, CommonError> {
207		debug!("[AirClient] Authenticating user '{}' with provider '{}'", username, provider);
208
209		#[cfg(feature = "AirIntegration")]
210		{
211			use AirLibrary::Vine::Generated::air::AuthenticationRequest;
212
213			let username_display = username.clone();
214			let request = AuthenticationRequest { request_id, username, password, provider };
215
216			let client = self.client.as_ref().ok_or_else(|| {
217				CommonError::IPCError { Description:"Air client not initialized".to_string() }
218			})?;
219
220			let mut client_guard = client.lock().await;
221			match client_guard.authenticate(Request::new(request)).await {
222				Ok(response) => {
223					let response = response.into_inner();
224					if response.success {
225						info!("[AirClient] Authentication successful for user '{}'", username_display);
226						Ok(response.token)
227					} else {
228						error!("[AirClient] Authentication failed for user '{}': {}", username_display, response.error);
229						Err(CommonError::AccessDenied { Reason:response.error })
230					}
231				},
232				Err(e) => {
233					error!("[AirClient] Authentication RPC error: {}", e);
234					Err(CommonError::IPCError { Description:format!("Authentication RPC error: {}", e) })
235				},
236			}
237		}
238
239		#[cfg(not(feature = "AirIntegration"))]
240		{
241			Err(CommonError::FeatureNotAvailable { FeatureName:"AirIntegration".to_string() })
242		}
243	}
244
245	// =========================================================================
246	// Update Operations
247	// =========================================================================
248
249	/// Checks for available updates.
250	///
251	/// # Arguments
252	/// * `current_version` - Current application version
253	/// * `channel` - Update channel (e.g., "stable", "beta", "nightly")
254	///
255	/// # Returns
256	/// * `Ok(update_info)` - Update information if available
257	/// * `Err(CommonError)` - Check failure
258	pub async fn check_for_updates(
259		&self,
260		request_id:String,
261		current_version:String,
262		channel:String,
263	) -> Result<UpdateInfo, CommonError> {
264		debug!("[AirClient] Checking for updates for version '{}'", current_version);
265
266		#[cfg(feature = "AirIntegration")]
267		{
268			use AirLibrary::Vine::Generated::air::UpdateCheckRequest;
269
270			let request = UpdateCheckRequest { request_id, current_version, channel };
271
272			let client = self.client.as_ref().ok_or_else(|| CommonError::IPCError { Description:"Air client not initialized".to_string() })?;
273			let mut client_guard = client.lock().await;
274
275			match client_guard.check_for_updates(Request::new(request)).await {
276				Ok(response) => {
277					let response: AirLibrary::Vine::Generated::air::UpdateCheckResponse = response.into_inner();
278					info!(
279						"[AirClient] Update check completed. Update available: {}",
280						response.update_available
281					);
282					Ok(UpdateInfo {
283						update_available:response.update_available,
284						version:response.version,
285						download_url:response.download_url,
286						release_notes:response.release_notes,
287					})
288				},
289				Err(e) => {
290					error!("[AirClient] Check for updates RPC error: {}", e);
291					Err(CommonError::IPCError { Description:format!("Check for updates RPC error: {}", e) })
292				},
293			}
294		}
295
296		#[cfg(not(feature = "AirIntegration"))]
297		{
298			Err(CommonError::FeatureNotAvailable { FeatureName:"AirIntegration".to_string() })
299		}
300	}
301
302	/// Downloads an update package.
303	///
304	/// # Arguments
305	/// * `url` - URL of the update package
306	/// * `destination_path` - Local path to save the downloaded file
307	/// * `checksum` - Optional SHA256 checksum for verification
308	/// * `headers` - Optional HTTP headers
309	///
310	/// # Returns
311	/// * `Ok(file_info)` - Downloaded file information
312	/// * `Err(CommonError)` - Download failure
313	pub async fn download_update(
314		&self,
315		request_id:String,
316		url:String,
317		destination_path:String,
318		checksum:String,
319		headers:HashMap<String, String>,
320	) -> Result<FileInfo, CommonError> {
321		debug!("[AirClient] Downloading update from: {}", url);
322
323		#[cfg(feature = "AirIntegration")]
324		{
325			use AirLibrary::Vine::Generated::air::DownloadRequest;
326
327			let request = DownloadRequest { request_id, url, destination_path, checksum, headers };
328
329			let client = self.client.as_ref().ok_or_else(|| CommonError::IPCError { Description:"Air client not initialized".to_string() })?;
330			let mut client_guard = client.lock().await;
331
332			match client_guard.download_update(Request::new(request)).await {
333				Ok(response) => {
334					let response: AirLibrary::Vine::Generated::air::DownloadResponse = response.into_inner();
335					if response.success {
336						info!("[AirClient] Update downloaded successfully to: {}", response.file_path);
337						Ok(FileInfo {
338							file_path:response.file_path,
339							file_size:response.file_size,
340							checksum:response.checksum,
341						})
342					} else {
343						error!("[AirClient] Update download failed: {}", response.error);
344						Err(CommonError::IPCError { Description:response.error })
345					}
346				},
347				Err(e) => {
348					error!("[AirClient] Download update RPC error: {}", e);
349					Err(CommonError::IPCError { Description:format!("Download update RPC error: {}", e) })
350				},
351			}
352		}
353
354		#[cfg(not(feature = "AirIntegration"))]
355		{
356			Err(CommonError::FeatureNotAvailable { FeatureName:"AirIntegration".to_string() })
357		}
358	}
359
360	/// Applies an update package.
361	///
362	/// # Arguments
363	/// * `version` - Version of the update
364	/// * `update_path` - Path to the update package
365	///
366	/// # Returns
367	/// * `Ok(())` - Update applied successfully
368	/// * `Err(CommonError)` - Application failure
369	pub async fn apply_update(&self, request_id:String, version:String, update_path:String) -> Result<(), CommonError> {
370		debug!("[AirClient] Applying update version: {}", version);
371
372		#[cfg(feature = "AirIntegration")]
373		{
374			use AirLibrary::Vine::Generated::air::ApplyUpdateRequest;
375
376			let request = ApplyUpdateRequest { request_id, version, update_path };
377
378			let client = self.client.as_ref().ok_or_else(|| CommonError::IPCError { Description:"Air client not initialized".to_string() })?;
379			let mut client_guard = client.lock().await;
380
381			match client_guard.apply_update(Request::new(request)).await {
382				Ok(response) => {
383					let response: AirLibrary::Vine::Generated::air::ApplyUpdateResponse = response.into_inner();
384					if response.success {
385						info!("[AirClient] Update applied successfully");
386						Ok(())
387					} else {
388						error!("[AirClient] Update application failed: {}", response.error);
389						Err(CommonError::IPCError { Description:response.error })
390					}
391				},
392				Err(e) => {
393					error!("[AirClient] Apply update RPC error: {}", e);
394					Err(CommonError::IPCError { Description:format!("Apply update RPC error: {}", e) })
395				},
396			}
397		}
398
399		#[cfg(not(feature = "AirIntegration"))]
400		{
401			Err(CommonError::FeatureNotAvailable { FeatureName:"AirIntegration".to_string() })
402		}
403	}
404
405	// =========================================================================
406	// Download Operations
407	// =========================================================================
408
409	/// Downloads a file.
410	///
411	/// # Arguments
412	/// * `url` - URL of the file to download
413	/// * `destination_path` - Local path to save the downloaded file
414	/// * `checksum` - Optional SHA256 checksum for verification
415	/// * `headers` - Optional HTTP headers
416	///
417	/// # Returns
418	/// * `Ok(file_info)` - Downloaded file information
419	/// * `Err(CommonError)` - Download failure
420	pub async fn download_file(
421		&self,
422		request_id:String,
423		url:String,
424		destination_path:String,
425		checksum:String,
426		headers:HashMap<String, String>,
427	) -> Result<FileInfo, CommonError> {
428		debug!("[AirClient] Downloading file from: {}", url);
429
430		#[cfg(feature = "AirIntegration")]
431		{
432			use AirLibrary::Vine::Generated::air::DownloadRequest;
433
434			let request = DownloadRequest { request_id, url, destination_path, checksum, headers };
435
436			let client = self.client.as_ref().ok_or_else(|| CommonError::IPCError { Description:"Air client not initialized".to_string() })?;
437			let mut client_guard = client.lock().await;
438
439			match client_guard.download_file(Request::new(request)).await {
440				Ok(response) => {
441					let response: AirLibrary::Vine::Generated::air::DownloadResponse = response.into_inner();
442					if response.success {
443						info!("[AirClient] File downloaded successfully to: {}", response.file_path);
444						Ok(FileInfo {
445							file_path:response.file_path,
446							file_size:response.file_size,
447							checksum:response.checksum,
448						})
449					} else {
450						error!("[AirClient] File download failed: {}", response.error);
451						Err(CommonError::IPCError { Description:response.error })
452					}
453				},
454				Err(e) => {
455					error!("[AirClient] Download file RPC error: {}", e);
456					Err(CommonError::IPCError { Description:format!("Download file RPC error: {}", e) })
457				},
458			}
459		}
460
461		#[cfg(not(feature = "AirIntegration"))]
462		{
463			Err(CommonError::FeatureNotAvailable { FeatureName:"AirIntegration".to_string() })
464		}
465	}
466
467	/// Downloads a file as a stream.
468	///
469	/// This method initiates a streaming download from the given URL, returning
470	/// a stream of chunks that can be processed incrementally without loading
471	/// the entire file into memory.
472	///
473	/// # Arguments
474	/// * `request_id` - Unique request identifier
475	/// * `url` - URL of the file to download
476	/// * `headers` - Optional HTTP headers
477	///
478	/// # Returns
479	/// * `Ok(stream)` - Stream that yields download chunks
480	/// * `Err(CommonError)` - Download initiation failure
481	///
482	/// # Stream Chunk Information
483	///
484	/// Each chunk contains:
485	/// - `chunk`: The binary data chunk
486	/// - `total_size`: Total file size (if known)
487	/// - `downloaded`: Number of bytes downloaded so far
488	/// - `completed`: Whether this is the final chunk
489	/// - `error`: Error message if download failed
490	///
491	/// # Example
492	///
493	/// ```text
494	/// use Mountain::Air::AirClient::AirClient;
495	/// use CommonLibrary::Error::CommonError::CommonError;
496	///
497	/// # #[tokio::main]
498	/// # async fn main() -> Result<(), CommonError> {
499	/// # let client = AirClient::new("http://[::1]:50053").await?;
500	/// let mut stream = client
501	/// 	.download_stream(
502	/// 		"req-123".to_string(),
503	/// 		"https://example.com/large-file.zip".to_string(),
504	/// 		std::collections::HashMap::new(),
505	/// 	)
506	/// 	.await?;
507	///
508	/// let mut buffer = Vec::new();
509	/// while let Some(chunk) = stream.next().await {
510	/// 	let chunk = chunk?;
511	/// 	buffer.extend_from_slice(&chunk.data);
512	/// 	println!("Downloaded: {} / {} bytes", chunk.downloaded, chunk.total_size);
513	/// 	if chunk.completed {
514	/// 		break;
515	/// 	}
516	/// }
517	/// # Ok(())
518	/// # }
519	/// ```
520	pub async fn download_stream(
521		&self,
522		request_id:String,
523		url:String,
524		headers:HashMap<String, String>,
525	) -> Result<DownloadStream, CommonError> {
526		debug!("[AirClient] Starting stream download from: {}", url);
527
528		#[cfg(feature = "AirIntegration")]
529		{
530			use AirLibrary::Vine::Generated::air::DownloadStreamRequest;
531
532			let request = DownloadStreamRequest { request_id, url, headers };
533
534			let client = self.client.as_ref().ok_or_else(|| CommonError::IPCError { Description:"Air client not initialized".to_string() })?;
535			let mut client_guard = client.lock().await;
536
537			match client_guard.download_stream(Request::new(request)).await {
538				Ok(response) => {
539					info!("[AirClient] Stream download initiated successfully");
540					Ok(DownloadStream::new(response.into_inner()))
541				},
542				Err(e) => {
543					error!("[AirClient] Download stream RPC error: {}", e);
544					Err(CommonError::IPCError { Description:format!("Download stream RPC error: {}", e) })
545				},
546			}
547		}
548
549		#[cfg(not(feature = "AirIntegration"))]
550		{
551			Err(CommonError::FeatureNotAvailable { FeatureName:"AirIntegration".to_string() })
552		}
553	}
554
555	// =========================================================================
556	// File Indexing Operations
557	// =========================================================================
558
559	/// Indexes files in a directory.
560	///
561	/// # Arguments
562	/// * `path` - Path to the directory to index
563	/// * `patterns` - File patterns to include
564	/// * `exclude_patterns` - File patterns to exclude
565	/// * `max_depth` - Maximum depth for recursion
566	///
567	/// # Returns
568	/// * `Ok(index_info)` - Index information
569	/// * `Err(CommonError)` - Indexing failure
570	pub async fn index_files(
571		&self,
572		request_id:String,
573		path:String,
574		patterns:Vec<String>,
575		exclude_patterns:Vec<String>,
576		max_depth:u32,
577	) -> Result<IndexInfo, CommonError> {
578		debug!("[AirClient] Indexing files in: {}", path);
579
580		#[cfg(feature = "AirIntegration")]
581		{
582			use AirLibrary::Vine::Generated::air::IndexRequest;
583
584			let request = IndexRequest { request_id, path, patterns, exclude_patterns, max_depth };
585
586			let client = self.client.as_ref().ok_or_else(|| CommonError::IPCError { Description:"Air client not initialized".to_string() })?;
587			let mut client_guard = client.lock().await;
588
589			match client_guard.index_files(Request::new(request)).await {
590				Ok(response) => {
591					let response = response.into_inner();
592					// Use fields that actually exist in IndexResponse
593					info!(
594						"[AirClient] Files indexed: {} (total size: {} bytes)",
595						response.files_indexed, response.total_size
596					);
597					Ok(IndexInfo { files_indexed:response.files_indexed, total_size:response.total_size })
598				},
599				Err(e) => {
600					error!("[AirClient] Index files RPC error: {}", e);
601					Err(CommonError::IPCError { Description:format!("Index files RPC error: {}", e) })
602				},
603			}
604		}
605
606		#[cfg(not(feature = "AirIntegration"))]
607		{
608			Err(CommonError::FeatureNotAvailable { FeatureName:"AirIntegration".to_string() })
609		}
610	}
611
612	/// Searches for files matching a query.
613	///
614	/// # Arguments
615	/// * `query` - Search query string
616	/// * `path` - Path to search in
617	/// * `max_results` - Maximum number of results to return
618	///
619	/// # Returns
620	/// * `Ok(results)` - Search results
621	/// * `Err(CommonError)` - Search failure
622	pub async fn search_files(
623		&self,
624		request_id:String,
625		query:String,
626		path:String,
627		max_results:u32,
628	) -> Result<Vec<FileResult>, CommonError> {
629		debug!("[AirClient] Searching for files with query: '{}' in: {}", query, path);
630
631		#[cfg(feature = "AirIntegration")]
632		{
633			use AirLibrary::Vine::Generated::air::SearchRequest;
634
635			let request = SearchRequest { request_id, query, path, max_results };
636
637			let client = self.client.as_ref().ok_or_else(|| CommonError::IPCError { Description:"Air client not initialized".to_string() })?;
638			let mut client_guard = client.lock().await;
639
640			match client_guard.search_files(Request::new(request)).await {
641				Ok(_response) => {
642					info!("[AirClient] Search completed");
643					// Placeholder implementation - actual response structure may vary
644					Ok(Vec::new())
645				},
646				Err(e) => {
647					error!("[AirClient] Search files RPC error: {}", e);
648					Err(CommonError::IPCError { Description:format!("Search files RPC error: {}", e) })
649				},
650			}
651		}
652
653		#[cfg(not(feature = "AirIntegration"))]
654		{
655			Err(CommonError::FeatureNotAvailable { FeatureName:"AirIntegration".to_string() })
656		}
657	}
658
659	/// Gets file information.
660	///
661	/// # Arguments
662	/// * `path` - Path to the file
663	///
664	/// # Returns
665	/// * `Ok(file_info)` - File information
666	/// * `Err(CommonError)` - Request failure
667	pub async fn get_file_info(&self, request_id:String, path:String) -> Result<ExtendedFileInfo, CommonError> {
668		let path_display = path.clone();
669		debug!("[AirClient] Getting file info for: {}", path);
670
671		#[cfg(feature = "AirIntegration")]
672		{
673			use AirLibrary::Vine::Generated::air::FileInfoRequest;
674
675			let request = FileInfoRequest { request_id, path };
676
677			let client = self.client.as_ref().ok_or_else(|| CommonError::IPCError { Description:"Air client not initialized".to_string() })?;
678			let mut client_guard = client.lock().await;
679
680			match client_guard.get_file_info(Request::new(request)).await {
681				Ok(response) => {
682					let response: AirLibrary::Vine::Generated::air::FileInfoResponse = response.into_inner();
683					info!("[AirClient] File info retrieved for: {} (exists: {})", path_display, response.exists);
684					Ok(ExtendedFileInfo {
685						exists:response.exists,
686						size:response.size,
687						mime_type:response.mime_type,
688						checksum:response.checksum,
689						modified_time:response.modified_time,
690					})
691				},
692				Err(e) => {
693					error!("[AirClient] Get file info RPC error: {}", e);
694					Err(CommonError::IPCError { Description:format!("Get file info RPC error: {}", e) })
695				},
696			}
697		}
698
699		#[cfg(not(feature = "AirIntegration"))]
700		{
701			Err(CommonError::FeatureNotAvailable { FeatureName:"AirIntegration".to_string() })
702		}
703	}
704
705	// =========================================================================
706	// Status and Monitoring Operations
707	// =========================================================================
708
709	/// Gets the status of the Air daemon.
710	///
711	/// # Returns
712	/// * `Ok(status)` - Air daemon status
713	/// * `Err(CommonError)` - Request failure
714	pub async fn get_status(&self, request_id:String) -> Result<AirStatus, CommonError> {
715		debug!("[AirClient] Getting Air daemon status");
716
717		#[cfg(feature = "AirIntegration")]
718		{
719			use AirLibrary::Vine::Generated::air::StatusRequest;
720
721			let request = StatusRequest { request_id };
722
723			let client = self.client.as_ref().ok_or_else(|| CommonError::IPCError { Description:"Air client not initialized".to_string() })?;
724			let mut client_guard = client.lock().await;
725
726			match client_guard.get_status(Request::new(request)).await {
727				Ok(response) => {
728					let response: AirLibrary::Vine::Generated::air::StatusResponse = response.into_inner();
729					info!("[AirClient] Status retrieved. Active requests: {}", response.active_requests);
730					Ok(AirStatus {
731						version:response.version,
732						uptime_seconds:response.uptime_seconds,
733						total_requests:response.total_requests,
734						successful_requests:response.successful_requests,
735						failed_requests:response.failed_requests,
736						average_response_time:response.average_response_time,
737						memory_usage_mb:response.memory_usage_mb,
738						cpu_usage_percent:response.cpu_usage_percent,
739						active_requests:response.active_requests,
740					})
741				},
742				Err(e) => {
743					error!("[AirClient] Get status RPC error: {}", e);
744					Err(CommonError::IPCError { Description:format!("Get status RPC error: {}", e) })
745				},
746			}
747		}
748
749		#[cfg(not(feature = "AirIntegration"))]
750		{
751			Err(CommonError::FeatureNotAvailable { FeatureName:"AirIntegration".to_string() })
752		}
753	}
754
755	/// Performs a health check on the Air daemon.
756	///
757	/// # Returns
758	/// * `Ok(healthy)` - Health status
759	/// * `Err(CommonError)` - Check failure
760	pub async fn health_check(&self) -> Result<bool, CommonError> {
761		debug!("[AirClient] Performing health check");
762
763		#[cfg(feature = "AirIntegration")]
764		{
765			use AirLibrary::Vine::Generated::air::HealthCheckRequest;
766
767			let request = HealthCheckRequest {};
768
769			let client = self.client.as_ref().ok_or_else(|| CommonError::IPCError { Description:"Air client not initialized".to_string() })?;
770			let mut client_guard = client.lock().await;
771
772			match client_guard.health_check(Request::new(request)).await {
773				Ok(response) => {
774					let response: AirLibrary::Vine::Generated::air::HealthCheckResponse = response.into_inner();
775					debug!("[AirClient] Health check result: {}", response.healthy);
776					Ok(response.healthy)
777				},
778				Err(e) => {
779					error!("[AirClient] Health check RPC error: {}", e);
780					Err(CommonError::IPCError { Description:format!("Health check RPC error: {}", e) })
781				},
782			}
783		}
784
785		#[cfg(not(feature = "AirIntegration"))]
786		{
787			// When AirIntegration is not enabled, we return true to allow
788			// the application to function without Air
789			Ok(true)
790		}
791	}
792
793	/// Gets metrics from the Air daemon.
794	///
795	/// # Arguments
796	/// * `metric_type` - Type of metrics (e.g., "performance", "resources",
797	///   "requests")
798	///
799	/// # Returns
800	/// * `Ok(metrics)` - Metrics data
801	/// * `Err(CommonError)` - Request failure
802	pub async fn get_metrics(&self, request_id:String, metric_type:Option<String>) -> Result<AirMetrics, CommonError> {
803		debug!("[AirClient] Getting metrics (type: {:?})", metric_type.as_deref());
804
805		#[cfg(feature = "AirIntegration")]
806		{
807			use AirLibrary::Vine::Generated::air::MetricsRequest;
808
809			let request = MetricsRequest { request_id, metric_type:metric_type.unwrap_or_default() };
810
811			let client = self.client.as_ref().ok_or_else(|| CommonError::IPCError { Description:"Air client not initialized".to_string() })?;
812			let mut client_guard = client.lock().await;
813
814			match client_guard.get_metrics(Request::new(request)).await {
815				Ok(response) => {
816					let response: AirLibrary::Vine::Generated::air::MetricsResponse = response.into_inner();
817					info!("[AirClient] Metrics retrieved");
818					// Parse metrics from the string map - this is a simplified implementation
819					let metrics = AirMetrics {
820						memory_usage_mb:response
821							.metrics
822							.get("memory_usage_mb")
823							.and_then(|s| s.parse::<f64>().ok())
824							.unwrap_or(0.0),
825						cpu_usage_percent:response
826							.metrics
827							.get("cpu_usage_percent")
828							.and_then(|s| s.parse::<f64>().ok())
829							.unwrap_or(0.0),
830						network_usage_mbps:response
831							.metrics
832							.get("network_usage_mbps")
833							.and_then(|s| s.parse::<f64>().ok())
834							.unwrap_or(0.0),
835						disk_usage_mb:response
836							.metrics
837							.get("disk_usage_mb")
838							.and_then(|s| s.parse::<f64>().ok())
839							.unwrap_or(0.0),
840						average_response_time:response
841							.metrics
842							.get("average_response_time")
843							.and_then(|s| s.parse::<f64>().ok())
844							.unwrap_or(0.0),
845					};
846					Ok(metrics)
847				},
848				Err(e) => {
849					error!("[AirClient] Get metrics RPC error: {}", e);
850					Err(CommonError::IPCError { Description:format!("Get metrics RPC error: {}", e) })
851				},
852			}
853		}
854
855		#[cfg(not(feature = "AirIntegration"))]
856		{
857			Err(CommonError::FeatureNotAvailable { FeatureName:"AirIntegration".to_string() })
858		}
859	}
860
861	// =========================================================================
862	// Resource Management Operations
863	// =========================================================================
864
865	/// Gets resource usage information.
866	///
867	/// # Arguments
868	/// * `request_id` - Unique request identifier
869	///
870	/// # Returns
871	/// * `Ok(usage)` - Resource usage data
872	/// * `Err(CommonError)` - Request failure
873	pub async fn get_resource_usage(&self, request_id:String) -> Result<ResourceUsage, CommonError> {
874		debug!("[AirClient] Getting resource usage");
875
876		#[cfg(feature = "AirIntegration")]
877		{
878			use AirLibrary::Vine::Generated::air::ResourceUsageRequest;
879
880			let request = ResourceUsageRequest { request_id };
881
882			let client = self.client.as_ref().ok_or_else(|| CommonError::IPCError { Description:"Air client not initialized".to_string() })?;
883			let mut client_guard = client.lock().await;
884
885			match client_guard.get_resource_usage(Request::new(request)).await {
886				Ok(response) => {
887					let response: AirLibrary::Vine::Generated::air::ResourceUsageResponse = response.into_inner();
888					info!("[AirClient] Resource usage retrieved");
889					Ok(ResourceUsage {
890						memory_usage_mb:response.memory_usage_mb,
891						cpu_usage_percent:response.cpu_usage_percent,
892						disk_usage_mb:response.disk_usage_mb,
893						network_usage_mbps:response.network_usage_mbps,
894						thread_count:0,      // Not provided in ResourceUsageResponse
895						open_file_handles:0, // Not provided in ResourceUsageResponse
896					})
897				},
898				Err(e) => {
899					error!("[AirClient] Get resource usage RPC error: {}", e);
900					Err(CommonError::IPCError { Description:format!("Get resource usage RPC error: {}", e) })
901				},
902			}
903		}
904
905		#[cfg(not(feature = "AirIntegration"))]
906		{
907			Err(CommonError::FeatureNotAvailable { FeatureName:"AirIntegration".to_string() })
908		}
909	}
910
911	/// Sets resource limits.
912	///
913	/// # Arguments
914	/// * `request_id` - Unique request identifier
915	/// * `memory_limit_mb` - Memory limit in MB
916	/// * `cpu_limit_percent` - CPU limit as percentage
917	/// * `disk_limit_mb` - Disk limit in MB
918	///
919	/// # Returns
920	/// * `Ok(())` - Limits set successfully
921	/// * `Err(CommonError)` - Set failure
922	pub async fn set_resource_limits(
923		&self,
924		request_id:String,
925		memory_limit_mb:u32,
926		cpu_limit_percent:u32,
927		disk_limit_mb:u32,
928	) -> Result<(), CommonError> {
929		debug!(
930			"[AirClient] Setting resource limits: memory={}MB, cpu={}%, disk={}MB",
931			memory_limit_mb, cpu_limit_percent, disk_limit_mb
932		);
933
934		#[cfg(feature = "AirIntegration")]
935		{
936			use AirLibrary::Vine::Generated::air::ResourceLimitsRequest;
937
938			let request = ResourceLimitsRequest { request_id, memory_limit_mb, cpu_limit_percent, disk_limit_mb };
939
940			let client = self.client.as_ref().ok_or_else(|| CommonError::IPCError { Description:"Air client not initialized".to_string() })?;
941			let mut client_guard = client.lock().await;
942
943			match client_guard.set_resource_limits(Request::new(request)).await {
944				Ok(response) => {
945					let response: AirLibrary::Vine::Generated::air::ResourceLimitsResponse = response.into_inner();
946					if response.success {
947						info!("[AirClient] Resource limits set successfully");
948						Ok(())
949					} else {
950						error!("[AirClient] Failed to set resource limits: {}", response.error);
951						Err(CommonError::IPCError { Description:response.error })
952					}
953				},
954				Err(e) => {
955					error!("[AirClient] Set resource limits RPC error: {}", e);
956					Err(CommonError::IPCError { Description:format!("Set resource limits RPC error: {}", e) })
957				},
958			}
959		}
960
961		#[cfg(not(feature = "AirIntegration"))]
962		{
963			Err(CommonError::FeatureNotAvailable { FeatureName:"AirIntegration".to_string() })
964		}
965	}
966
967	// =========================================================================
968	// Configuration Management Operations
969	// =========================================================================
970
971	/// Gets configuration.
972	///
973	/// # Arguments
974	/// * `section` - Configuration section (e.g., "grpc", "authentication",
975	///   "updates")
976	///
977	/// # Returns
978	/// * `Ok(config)` - Configuration data
979	/// * `Err(CommonError)` - Request failure
980	pub async fn get_configuration(
981		&self,
982		request_id:String,
983		section:String,
984	) -> Result<HashMap<String, String>, CommonError> {
985		let section_display = section.clone();
986		debug!("[AirClient] Getting configuration for section: {}", section);
987
988		#[cfg(feature = "AirIntegration")]
989		{
990			use AirLibrary::Vine::Generated::air::ConfigurationRequest;
991
992			let request = ConfigurationRequest { request_id, section };
993
994			let client = self.client.as_ref().ok_or_else(|| CommonError::IPCError { Description:"Air client not initialized".to_string() })?;
995			let mut client_guard = client.lock().await;
996
997			match client_guard.get_configuration(Request::new(request)).await {
998				Ok(response) => {
999					let response: AirLibrary::Vine::Generated::air::ConfigurationResponse = response.into_inner();
1000					info!(
1001						"[AirClient] Configuration retrieved for section: {} ({} keys)",
1002						section_display,
1003						response.configuration.len()
1004					);
1005					Ok(response.configuration)
1006				},
1007				Err(e) => {
1008					error!("[AirClient] Get configuration RPC error: {}", e);
1009					Err(CommonError::IPCError { Description:format!("Get configuration RPC error: {}", e) })
1010				},
1011			}
1012		}
1013
1014		#[cfg(not(feature = "AirIntegration"))]
1015		{
1016			Err(CommonError::FeatureNotAvailable { FeatureName:"AirIntegration".to_string() })
1017		}
1018	}
1019
1020	/// Updates configuration.
1021	///
1022	/// # Arguments
1023	/// * `section` - Configuration section
1024	/// * `updates` - Configuration updates
1025	///
1026	/// # Returns
1027	/// * `Ok(())` - Configuration updated successfully
1028	/// * `Err(CommonError)` - Update failure
1029	pub async fn update_configuration(
1030		&self,
1031		request_id:String,
1032		section:String,
1033		updates:HashMap<String, String>,
1034	) -> Result<(), CommonError> {
1035		let section_display = section.clone();
1036		debug!(
1037			"[AirClient] Updating configuration for section: {} ({} keys)",
1038			section_display,
1039			updates.len()
1040		);
1041
1042		#[cfg(feature = "AirIntegration")]
1043		{
1044			use AirLibrary::Vine::Generated::air::UpdateConfigurationRequest;
1045
1046			let request = UpdateConfigurationRequest { request_id, section, updates };
1047
1048			let client = self.client.as_ref().ok_or_else(|| CommonError::IPCError { Description:"Air client not initialized".to_string() })?;
1049			let mut client_guard = client.lock().await;
1050
1051			match client_guard.update_configuration(Request::new(request)).await {
1052				Ok(response) => {
1053					let response: AirLibrary::Vine::Generated::air::UpdateConfigurationResponse = response.into_inner();
1054					if response.success {
1055						info!("[AirClient] Configuration updated successfully for section: {}", section_display);
1056						Ok(())
1057					} else {
1058						error!("[AirClient] Failed to update configuration: {}", response.error);
1059						Err(CommonError::IPCError { Description:response.error })
1060					}
1061				},
1062				Err(e) => {
1063					error!("[AirClient] Update configuration RPC error: {}", e);
1064					Err(CommonError::IPCError { Description:format!("Update configuration RPC error: {}", e) })
1065				},
1066			}
1067		}
1068
1069		#[cfg(not(feature = "AirIntegration"))]
1070		{
1071			Err(CommonError::FeatureNotAvailable { FeatureName:"AirIntegration".to_string() })
1072		}
1073	}
1074}
1075
1076// ============================================================================
1077// Response Types
1078// ============================================================================
1079
1080/// Information about an available update.
1081#[derive(Debug, Clone)]
1082pub struct UpdateInfo {
1083	pub update_available:bool,
1084	pub version:String,
1085	pub download_url:String,
1086	pub release_notes:String,
1087}
1088
1089/// Information about a downloaded file.
1090#[derive(Debug, Clone)]
1091pub struct FileInfo {
1092	pub file_path:String,
1093	pub file_size:u64,
1094	pub checksum:String,
1095}
1096
1097/// Information about file indexing.
1098#[derive(Debug, Clone)]
1099pub struct IndexInfo {
1100	pub files_indexed:u32,
1101	pub total_size:u64,
1102}
1103
1104/// Result of a file search.
1105#[derive(Debug, Clone)]
1106pub struct FileResult {
1107	pub path:String,
1108	pub size:u64,
1109	pub match_preview:String,
1110	pub line_number:u32,
1111}
1112
1113/// Extended file information.
1114#[derive(Debug, Clone)]
1115pub struct ExtendedFileInfo {
1116	pub exists:bool,
1117	pub size:u64,
1118	pub mime_type:String,
1119	pub checksum:String,
1120	pub modified_time:u64,
1121}
1122
1123/// Status of the Air daemon.
1124#[derive(Debug, Clone)]
1125pub struct AirStatus {
1126	pub version:String,
1127	pub uptime_seconds:u64,
1128	pub total_requests:u64,
1129	pub successful_requests:u64,
1130	pub failed_requests:u64,
1131	pub average_response_time:f64,
1132	pub memory_usage_mb:f64,
1133	pub cpu_usage_percent:f64,
1134	pub active_requests:u32,
1135}
1136
1137/// Metrics from the Air daemon.
1138#[derive(Debug, Clone)]
1139pub struct AirMetrics {
1140	pub memory_usage_mb:f64,
1141	pub cpu_usage_percent:f64,
1142	pub network_usage_mbps:f64,
1143	pub disk_usage_mb:f64,
1144	pub average_response_time:f64,
1145}
1146
1147/// Resource usage information.
1148#[derive(Debug, Clone)]
1149pub struct ResourceUsage {
1150	pub memory_usage_mb:f64,
1151	pub cpu_usage_percent:f64,
1152	pub disk_usage_mb:f64,
1153	pub network_usage_mbps:f64,
1154	pub thread_count:u32,
1155	pub open_file_handles:u32,
1156}
1157
1158/// Chunk of data from a streaming download.
1159///
1160/// Each chunk represents a portion of the downloaded file with metadata
1161/// about the download progress.
1162#[derive(Debug, Clone)]
1163pub struct DownloadStreamChunk {
1164	/// Binary data chunk
1165	pub data:Vec<u8>,
1166	/// Total file size in bytes (0 if unknown)
1167	pub total_size:u64,
1168	/// Number of bytes downloaded so far
1169	pub downloaded:u64,
1170	/// Whether this is the final chunk
1171	pub completed:bool,
1172	/// Error message if download failed
1173	pub error:String,
1174}
1175
1176/// Wrapper for an asynchronous download stream.
1177///
1178/// This type wraps the tonic streaming API to provide a convenient
1179/// interface for iterating over download chunks.
1180///
1181/// # Example
1182///
1183/// ```text
1184/// use Mountain::Air::AirClient::DownloadStream;
1185/// use CommonLibrary::Error::CommonError::CommonError;
1186///
1187/// # #[tokio::main]
1188/// # async fn main() -> Result<(), CommonError> {
1189/// # let mut stream = DownloadStream::new(/* tonic stream */);
1190/// let mut buffer = Vec::new();
1191/// while let Some(chunk) = stream.next().await {
1192/// 	let chunk = chunk?;
1193/// 	buffer.extend_from_slice(&chunk.data);
1194/// 	if chunk.completed {
1195/// 		break;
1196/// 	}
1197/// }
1198/// # Ok(())
1199/// # }
1200/// ```
1201pub struct DownloadStream {
1202	inner:tonic::codec::Streaming<AirLibrary::Vine::Generated::air::DownloadStreamResponse>,
1203}
1204
1205impl DownloadStream {
1206	/// Creates a new DownloadStream from a tonic streaming response.
1207	pub fn new(stream:tonic::codec::Streaming<AirLibrary::Vine::Generated::air::DownloadStreamResponse>) -> Self {
1208		Self { inner:stream }
1209	}
1210
1211	/// Returns the next chunk from the stream.
1212	///
1213	/// Returns `None` when the stream ends.
1214	pub async fn next(&mut self) -> Option<Result<DownloadStreamChunk, CommonError>> {
1215		match futures_util::stream::StreamExt::next(&mut self.inner).await {
1216			Some(Ok(response)) => {
1217				Some(Ok(DownloadStreamChunk {
1218					data:response.chunk,
1219					total_size:response.total_size,
1220					downloaded:response.downloaded,
1221					completed:response.completed,
1222					error:response.error,
1223				}))
1224			},
1225			Some(Err(e)) => {
1226				error!("[DownloadStream] Stream error: {}", e);
1227				Some(Err(CommonError::IPCError { Description:format!("Stream error: {}", e) }))
1228			},
1229			None => None,
1230		}
1231	}
1232}
1233
1234// ============================================================================
1235// Debug Implementation
1236// ============================================================================
1237
1238impl std::fmt::Debug for AirClient {
1239	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1240		write!(f, "AirClient({})", self.address)
1241	}
1242}
1243
1244// ============================================================================
1245// tonic::Request Helper
1246// ============================================================================
1247
1248/// Helper trait for converting types to tonic::Request
1249trait IntoRequestExt {
1250	fn into_request(self) -> tonic::Request<Self>
1251	where
1252		Self: Sized, {
1253		tonic::Request::new(self)
1254	}
1255}
1256
1257impl<T> IntoRequestExt for T {}