Skip to main content

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