Skip to main content

AirLibrary/Configuration/
mod.rs

1//! # Configuration Management
2//!
3//! This module provides comprehensive configuration management for the Air
4//! daemon, serving as the central configuration layer for the entire Land
5//! ecosystem.
6//!
7//! ## Responsibilities
8//!
9//! - **Configuration Loading**: Load and parse configuration from TOML files
10//!   with fallback to defaults
11//! - **Schema Validation**: Validate all configuration values against defined
12//!   schemas with detailed error messages
13//! - **Type Safety**: Strong typing with compile-time guarantees and runtime
14//!   validation
15//! - **Value Constraints**: Range validation, path validation, and security
16//!   checks
17//! - **Environment Integration**: Support environment variable overrides and
18//!   profile-based configuration
19//! - **Hot Reload**: Live configuration updates without service restart (via
20//!   HotReload module)
21//! - **Change Tracking**: Audit trail for all configuration changes with
22//!   rollback support
23//! - **Migration Support**: Automated configuration schema versioning and
24//!   migration
25//!
26//! ## VSCode Configuration System References
27//!
28//! This configuration system is designed to be compatible with VSCode's
29//! configuration architecture:
30//! - VSCode config reference:
31//!   `Dependency/Microsoft/Editor/src/vs/platform/configuration/`
32//! - Format compatibility with `settings.json` schema structure
33//! - Support for workspace-specific overrides similar to VSCode's multi-layer
34//!   config
35//! - Configuration inheritance and overriding patterns aligned with VSCode
36//!
37//! ## Connection to Mountain's Configuration Needs
38//!
39//! Mountain (the VSCode application layer) consumes Air's configuration:
40//! - User settings in Mountain flow through to Air's daemon configuration
41//! - Wind services read centralized configuration for consistency
42//! - Configuration changes propagate through the hot-reload system to all
43//!   services
44//! - Profile switching (dev/staging/prod) affects entire Land ecosystem
45//!
46//! ## Configuration Flow
47//!
48//! ```text
49//! Mountain (User Settings) → Air config file → Wind services
50//! ↓ ↓ ↓
51//! settings.json ~/.Air/config.toml Service-specific overrides
52//! ↓ ↓ ↓
53//! Workspace settings Environment variables Hot-reload notifications
54//! ```
55//!
56//! ## FUTURE: Schema Validation
57//! - Implement JSON Schema generation for validation
58//! - Add schema versioning and migration support
59//! - Provide schema validation errors with detailed field-level information
60//! - Support schema evolution with backward compatibility
61//!
62//! ## FUTURE: Configuration Migration
63//! - Add version field to configuration structure
64//! - Implement automatic migration between schema versions
65//! - Provide migration tools for manual upgrades
66//! - Document migration paths and breaking changes
67//!
68//! ## FUTURE: Configuration Inheritance
69//! - Implement base profile templates
70//! - Support profile inheritance and overrides
71//! - Add configuration layer merging logic
72//! - Document precedence rules (defaults → file → env → runtime)
73//!
74//! ## Profiles and Environments
75//!
76//! Configuration supports multiple profiles for different deployment scenarios:
77//! - **dev**: Development environment with debug logging
78//! - **staging**: Pre-production with production-like settings
79//! - **prod**: Production optimized settings
80//! - **custom**: User-defined profiles
81//!
82//! ## Security Considerations
83//!
84//! - Path validation prevents directory traversal attacks
85//! - Sensitive values support environment variable injection
86//! - Configuration files enforce proper permissions
87//! - Atomic updates prevent partial/corrupted state
88
89pub mod HotReload;
90
91pub mod Schema;
92
93use std::{
94	collections::HashMap,
95	env,
96	path::{Path, PathBuf},
97};
98
99pub use Schema::generate_schema;
100use serde::{Deserialize, Serialize};
101use sha2::Digest;
102
103use crate::{AirError, DefaultConfigFile, Result, dev_log};
104
105// =============================================================================
106// Configuration Main Structure
107// =============================================================================
108
109/// Main configuration structure
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct AirConfiguration {
112	/// Configuration schema version for migration tracking
113	#[serde(default = "default_schema_version")]
114	pub SchemaVersion:String,
115
116	/// Profile name (dev, staging, prod, custom)
117	#[serde(default = "default_profile")]
118	pub Profile:String,
119
120	/// gRPC server configuration
121	pub gRPC:gRPCConfig,
122
123	/// Authentication configuration
124	pub Authentication:AuthConfig,
125
126	/// Update configuration
127	pub Updates:UpdateConfig,
128
129	/// Download configuration
130	pub Downloader:DownloadConfig,
131
132	/// Indexing configuration
133	pub Indexing:IndexingConfig,
134
135	/// Logging configuration
136	pub Logging:LoggingConfig,
137
138	/// Performance configuration
139	pub Performance:PerformanceConfig,
140}
141
142fn default_schema_version() -> String { "1.0.0".to_string() }
143
144fn default_profile() -> String { "dev".to_string() }
145
146/// gRPC server configuration
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct gRPCConfig {
149	/// Bind address for gRPC server
150	/// Validation: Must be a valid IP:port or hostname:port combination
151	/// Format: `[IPv6]`:port or IPv4:port or hostname:port
152	/// Example: `"[::1]:50053"`, `"127.0.0.1:50053"`, `"localhost:50053"`
153	#[serde(default = "default_grpc_bind_address")]
154	pub BindAddress:String,
155
156	/// Maximum concurrent connections
157	/// Validation: Range [10, 10000]
158	/// Default: 100
159	#[serde(default = "default_grpc_max_connections")]
160	pub MaxConnections:u32,
161
162	/// Request timeout in seconds
163	/// Validation: Range [1, 3600] (1 second to 1 hour)
164	/// Default: 30
165	#[serde(default = "default_grpc_request_timeout")]
166	pub RequestTimeoutSecs:u64,
167}
168
169fn default_grpc_bind_address() -> String { "[::1]:50053".to_string() }
170
171fn default_grpc_max_connections() -> u32 { 100 }
172
173fn default_grpc_request_timeout() -> u64 { 30 }
174
175/// Authentication configuration
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct AuthConfig {
178	/// Enable authentication service
179	#[serde(default = "default_auth_enabled")]
180	pub Enabled:bool,
181
182	/// Path to credentials storage
183	/// Validation: Must be a valid absolute or home-relative path
184	/// Security: Ensures directory traversal prevention
185	/// Default: "~/.Air/credentials"
186	#[serde(default = "default_auth_credentials_path")]
187	pub CredentialsPath:String,
188
189	/// Token expiration in hours
190	/// Validation: Range [1, 8760] (1 hour to 1 year)
191	/// Default: 24
192	#[serde(default = "default_auth_token_expiration")]
193	pub TokenExpirationHours:u32,
194
195	/// Maximum concurrent auth sessions
196	/// Validation: Range [1, 1000]
197	/// Default: 10
198	#[serde(default = "default_auth_max_sessions")]
199	pub MaxSessions:u32,
200}
201
202fn default_auth_enabled() -> bool { true }
203
204fn default_auth_credentials_path() -> String { "~/.Air/credentials".to_string() }
205
206fn default_auth_token_expiration() -> u32 { 24 }
207
208fn default_auth_max_sessions() -> u32 { 10 }
209
210/// Update configuration
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct UpdateConfig {
213	/// Enable update service
214	#[serde(default = "default_update_enabled")]
215	pub Enabled:bool,
216
217	/// Update check interval in hours
218	/// Validation: Range [1, 168] (1 hour to 1 week)
219	/// Default: 6
220	#[serde(default = "default_update_check_interval")]
221	pub CheckIntervalHours:u32,
222
223	/// Update server URL
224	/// Validation: Must be a valid HTTPS URL
225	/// Security: HTTPS required for security
226	/// Default: <https://updates.land.playform.cloud>
227	#[serde(default = "default_update_server_url")]
228	pub UpdateServerUrl:String,
229
230	/// Auto-download updates
231	#[serde(default = "default_update_auto_download")]
232	pub AutoDownload:bool,
233
234	/// Auto-install updates
235	/// Warning: Use with caution in production
236	#[serde(default = "default_update_auto_install")]
237	pub AutoInstall:bool,
238
239	/// Update channel
240	/// Validation: Must be one of: "stable", "insiders", "preview"
241	/// Default: "stable"
242	#[serde(default = "default_update_channel")]
243	pub Channel:String,
244}
245
246fn default_update_enabled() -> bool { true }
247
248fn default_update_check_interval() -> u32 { 6 }
249
250fn default_update_server_url() -> String { "https://updates.land.playform.cloud".to_string() }
251
252fn default_update_auto_download() -> bool { true }
253
254fn default_update_auto_install() -> bool { false }
255
256fn default_update_channel() -> String { "stable".to_string() }
257
258/// Download configuration
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct DownloadConfig {
261	/// Enable download service
262	#[serde(default = "default_download_enabled")]
263	pub Enabled:bool,
264
265	/// Maximum concurrent downloads
266	/// Validation: Range [1, 50]
267	/// Default: 5
268	#[serde(default = "default_download_max_concurrent")]
269	pub MaxConcurrentDownloads:u32,
270
271	/// Download timeout in seconds
272	/// Validation: Range [10, 3600] (10 seconds to 1 hour)
273	/// Default: 300
274	#[serde(default = "default_download_timeout")]
275	pub DownloadTimeoutSecs:u64,
276
277	/// Maximum retry attempts
278	/// Validation: Range [0, 10]
279	/// Default: 3
280	#[serde(default = "default_download_max_retries")]
281	pub MaxRetries:u32,
282
283	/// Download cache directory
284	/// Validation: Must be a valid absolute or home-relative path
285	/// Default: "~/.Air/cache"
286	#[serde(default = "default_download_cache_dir")]
287	pub CacheDirectory:String,
288}
289
290fn default_download_enabled() -> bool { true }
291
292fn default_download_max_concurrent() -> u32 { 5 }
293
294fn default_download_timeout() -> u64 { 300 }
295
296fn default_download_max_retries() -> u32 { 3 }
297
298fn default_download_cache_dir() -> String { "~/.Air/cache".to_string() }
299
300/// Indexing configuration
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct IndexingConfig {
303	/// Enable indexing service
304	#[serde(default = "default_indexing_enabled")]
305	pub Enabled:bool,
306
307	/// Maximum file size to index (MB)
308	/// Validation: Range [1, 1024] (1MB to 1GB)
309	/// Default: 10
310	#[serde(default = "default_indexing_max_file_size")]
311	pub MaxFileSizeMb:u32,
312
313	/// File types to index
314	/// Format: Glob patterns like "*.rs", "*.ts", etc.
315	/// Validation: Each pattern must be a valid glob pattern
316	/// Default: Common source code file types
317	#[serde(default = "default_indexing_file_types")]
318	pub FileTypes:Vec<String>,
319
320	/// Index update interval in minutes
321	/// Validation: Range [1, 1440] (1 minute to 1 day)
322	/// Default: 30
323	#[serde(default = "default_indexing_update_interval")]
324	pub UpdateIntervalMinutes:u32,
325
326	/// Index storage directory
327	/// Validation: Must be a valid absolute or home-relative path
328	/// Default: "~/.Air/index"
329	#[serde(default = "default_indexing_directory")]
330	pub IndexDirectory:String,
331
332	/// Maximum parallel indexing operations
333	/// Validation: Range [1, 100] (1 to 100 concurrent operations)
334	/// Default: 10
335	#[serde(default = "default_max_parallel_indexing")]
336	pub MaxParallelIndexing:u32,
337}
338
339fn default_indexing_enabled() -> bool { true }
340
341fn default_indexing_max_file_size() -> u32 { 10 }
342
343fn default_indexing_file_types() -> Vec<String> {
344	vec![
345		"*.rs".to_string(),
346		"*.ts".to_string(),
347		"*.js".to_string(),
348		"*.json".to_string(),
349		"*.toml".to_string(),
350		"*.md".to_string(),
351	]
352}
353
354fn default_indexing_update_interval() -> u32 { 30 }
355
356fn default_indexing_directory() -> String { "~/.Air/index".to_string() }
357
358fn default_max_parallel_indexing() -> u32 { 10 }
359
360/// Logging configuration
361#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct LoggingConfig {
363	/// Log level
364	/// Validation: Must be one of: "trace", "debug", "info", "warn", "error"
365	/// Default: "info"
366	#[serde(default = "default_logging_level")]
367	pub Level:String,
368
369	/// Log file path
370	/// Validation: Must be a valid absolute or home-relative path if provided
371	/// Default: "~/.Air/logs/Air.log"
372	#[serde(default = "default_logging_file_path")]
373	pub FilePath:Option<String>,
374
375	/// Enable console logging
376	#[serde(default = "default_logging_console_enabled")]
377	pub ConsoleEnabled:bool,
378
379	/// Maximum log file size (MB)
380	/// Validation: Range [1, 1000]
381	/// Default: 10
382	#[serde(default = "default_logging_max_file_size")]
383	pub MaxFileSizeMb:u32,
384
385	/// Maximum log files to keep
386	/// Validation: Range [1, 50]
387	/// Default: 5
388	#[serde(default = "default_logging_max_files")]
389	pub MaxFiles:u32,
390}
391
392fn default_logging_level() -> String { "info".to_string() }
393
394fn default_logging_file_path() -> Option<String> { Some("~/.Air/logs/Air.log".to_string()) }
395
396fn default_logging_console_enabled() -> bool { true }
397
398fn default_logging_max_file_size() -> u32 { 10 }
399
400fn default_logging_max_files() -> u32 { 5 }
401
402/// Performance configuration
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct PerformanceConfig {
405	/// Memory usage limit (MB)
406	/// Validation: Range [64, 16384] (64MB to 16GB)
407	/// Default: 512
408	#[serde(default = "default_perf_memory_limit")]
409	pub MemoryLimitMb:u32,
410
411	/// CPU usage limit (%)
412	/// Validation: Range [10, 100]
413	/// Default: 50
414	#[serde(default = "default_perf_cpu_limit")]
415	pub CPULimitPercent:u32,
416
417	/// Disk usage limit (MB)
418	/// Validation: Range [100, 102400] (100MB to 100GB)
419	/// Default: 1024
420	#[serde(default = "default_perf_disk_limit")]
421	pub DiskLimitMb:u32,
422
423	/// Background task interval in seconds
424	/// Validation: Range [1, 3600] (1 second to 1 hour)
425	/// Default: 60
426	#[serde(default = "default_perf_task_interval")]
427	pub BackgroundTaskIntervalSecs:u64,
428}
429
430fn default_perf_memory_limit() -> u32 { 512 }
431
432fn default_perf_cpu_limit() -> u32 { 50 }
433
434fn default_perf_disk_limit() -> u32 { 1024 }
435
436fn default_perf_task_interval() -> u64 { 60 }
437
438impl Default for AirConfiguration {
439	fn default() -> Self {
440		Self {
441			SchemaVersion:default_schema_version(),
442
443			Profile:default_profile(),
444
445			gRPC:gRPCConfig {
446				BindAddress:default_grpc_bind_address(),
447
448				MaxConnections:default_grpc_max_connections(),
449
450				RequestTimeoutSecs:default_grpc_request_timeout(),
451			},
452
453			Authentication:AuthConfig {
454				Enabled:default_auth_enabled(),
455
456				CredentialsPath:default_auth_credentials_path(),
457
458				TokenExpirationHours:default_auth_token_expiration(),
459
460				MaxSessions:default_auth_max_sessions(),
461			},
462
463			Updates:UpdateConfig {
464				Enabled:default_update_enabled(),
465
466				CheckIntervalHours:default_update_check_interval(),
467
468				UpdateServerUrl:default_update_server_url(),
469
470				AutoDownload:default_update_auto_download(),
471
472				AutoInstall:default_update_auto_install(),
473
474				Channel:default_update_channel(),
475			},
476
477			Downloader:DownloadConfig {
478				Enabled:default_download_enabled(),
479
480				MaxConcurrentDownloads:default_download_max_concurrent(),
481
482				DownloadTimeoutSecs:default_download_timeout(),
483
484				MaxRetries:default_download_max_retries(),
485
486				CacheDirectory:default_download_cache_dir(),
487			},
488
489			Indexing:IndexingConfig {
490				Enabled:default_indexing_enabled(),
491
492				MaxFileSizeMb:default_indexing_max_file_size(),
493
494				FileTypes:default_indexing_file_types(),
495
496				UpdateIntervalMinutes:default_indexing_update_interval(),
497
498				IndexDirectory:default_indexing_directory(),
499
500				MaxParallelIndexing:default_max_parallel_indexing(),
501			},
502
503			Logging:LoggingConfig {
504				Level:default_logging_level(),
505
506				FilePath:default_logging_file_path(),
507
508				ConsoleEnabled:default_logging_console_enabled(),
509
510				MaxFileSizeMb:default_logging_max_file_size(),
511
512				MaxFiles:default_logging_max_files(),
513			},
514
515			Performance:PerformanceConfig {
516				MemoryLimitMb:default_perf_memory_limit(),
517
518				CPULimitPercent:default_perf_cpu_limit(),
519
520				DiskLimitMb:default_perf_disk_limit(),
521
522				BackgroundTaskIntervalSecs:default_perf_task_interval(),
523			},
524		}
525	}
526}
527
528// =============================================================================
529// Configuration Schema
530// =============================================================================
531
532// =============================================================================
533// Configuration Manager
534// =============================================================================
535
536/// Configuration manager with comprehensive validation, backup, and hot-reload
537/// support
538pub struct ConfigurationManager {
539	/// Path to configuration file
540	ConfigPath:Option<PathBuf>,
541
542	/// Backup configuration directory
543	BackupDir:Option<PathBuf>,
544
545	/// Enable configuration backup
546	EnableBackup:bool,
547
548	/// Environment variable prefix for overrides
549	EnvPrefix:String,
550}
551
552impl ConfigurationManager {
553	/// Create a new configuration manager
554	///
555	/// # Arguments
556	///
557	/// * `ConfigPath` - Optional path to configuration file. If None, uses
558	///   default location
559	///
560	/// # Returns
561	///
562	/// Returns a new ConfigurationManager instance
563	pub fn New(ConfigPath:Option<String>) -> Result<Self> {
564		let path = ConfigPath.map(PathBuf::from);
565
566		let BackupDir = path
567			.as_ref()
568			.and_then(|p| p.parent())
569			.map(|parent| parent.join(".ConfigBackups"));
570
571		Ok(Self { ConfigPath:path, BackupDir, EnableBackup:true, EnvPrefix:"AIR_".to_string() })
572	}
573
574	/// Create a new configuration manager with custom settings
575	///
576	/// # Arguments
577	///
578	/// * `ConfigPath` - Optional path to configuration file
579	/// * `EnableBackup` - Whether to enable automatic backups
580	/// * `EnvPrefix` - Prefix for environment variable overrides
581	pub fn NewWithSettings(ConfigPath:Option<String>, EnableBackup:bool, EnvPrefix:String) -> Result<Self> {
582		let path = ConfigPath.map(PathBuf::from);
583
584		let BackupDir = if EnableBackup {
585			path.as_ref()
586				.and_then(|p| p.parent())
587				.map(|parent| parent.join(".ConfigBackups"))
588		} else {
589			None
590		};
591
592		Ok(Self { ConfigPath:path, BackupDir, EnableBackup, EnvPrefix })
593	}
594
595	/// Load configuration from file, environment, or create default
596	///
597	/// This method implements the configuration priority chain:
598	/// 1. Defaults from code
599	/// 2. Configuration file
600	/// 3. Environment variables (with prefix)
601	///
602	/// # Returns
603	///
604	/// Validated and loaded configuration
605	pub async fn LoadConfiguration(&self) -> Result<AirConfiguration> {
606		// Start with default configuration
607		let mut config = AirConfiguration::default();
608
609		// Try to load from specified or default path
610		let ConfigPath = self.GetConfigPath()?;
611
612		if ConfigPath.exists() {
613			dev_log!("config", "Loading configuration from: {}", ConfigPath.display());
614
615			config = self.LoadFromFile(&ConfigPath).await?;
616		} else {
617			dev_log!("config", "No configuration file found, using defaults");
618		}
619
620		// Apply environment variable overrides
621		self.ApplyEnvironmentOverrides(&mut config)?;
622
623		// Schema validation
624		self.SchemaValidate(&config)?;
625
626		// Validate all configuration values
627		self.ValidateConfiguration(&config)?;
628
629		dev_log!("config", "Configuration loaded successfully (profile: {})", config.Profile);
630
631		Ok(config)
632	}
633
634	/// Load configuration from a specific file
635	///
636	/// # Arguments
637	///
638	/// * `path` - Path to the configuration file
639	///
640	/// # Returns
641	///
642	/// Parsed and validated configuration
643	async fn LoadFromFile(&self, path:&Path) -> Result<AirConfiguration> {
644		let content = tokio::fs::read_to_string(path)
645			.await
646			.map_err(|e| AirError::Configuration(format!("Failed to read config file '{}': {}", path.display(), e)))?;
647
648		let config:AirConfiguration = toml::from_str(&content).map_err(|e| {
649			AirError::Configuration(format!("Failed to parse TOML config file '{}': {}", path.display(), e))
650		})?;
651
652		// Type validation is done by serde automatically
653		dev_log!("config", "Configuration file parsed successfully");
654
655		Ok(config)
656	}
657
658	/// Save configuration to file with backup and atomic write
659	///
660	/// # Arguments
661	///
662	/// * `config` - Configuration to save
663	///
664	/// # Implementation Details
665	///
666	/// - Validates configuration before saving
667	/// - Creates backup if enabled
668	/// - Uses atomic write (write to temp file, then rename)
669	/// - Creates parent directories if needed
670	pub async fn SaveConfiguration(&self, config:&AirConfiguration) -> Result<()> {
671		// Validate before saving
672		self.ValidateConfiguration(config)?;
673
674		let ConfigPath = self.GetConfigPath()?;
675
676		// Create backup if enabled and file exists
677		if self.EnableBackup && ConfigPath.exists() {
678			self.BackupConfiguration(&ConfigPath).await?;
679		}
680
681		// Create parent directory if it doesn't exist
682		if let Some(parent) = ConfigPath.parent() {
683			tokio::fs::create_dir_all(parent).await.map_err(|e| {
684				AirError::Configuration(format!("Failed to create config directory '{}': {}", parent.display(), e))
685			})?;
686		}
687
688		// Atomic write: write to temp file, then rename
689		let TempPath = ConfigPath.with_extension("tmp");
690
691		let content = toml::to_string_pretty(config)
692			.map_err(|e| AirError::Configuration(format!("Failed to serialize config: {}", e)))?;
693
694		tokio::fs::write(&TempPath, content).await.map_err(|e| {
695			AirError::Configuration(format!("Failed to write temp config file '{}': {}", TempPath.display(), e))
696		})?;
697
698		// Atomic rename
699		tokio::fs::rename(&TempPath, &ConfigPath).await.map_err(|e| {
700			AirError::Configuration(format!("Failed to rename temp config to '{}': {}", ConfigPath.display(), e))
701		})?;
702
703		dev_log!("config", "Configuration saved to: {}", ConfigPath.display());
704
705		Ok(())
706	}
707
708	/// Validate configuration with comprehensive checks
709	///
710	/// Performs:
711	/// - Schema validation
712	/// - Type checking with detailed errors
713	/// - Range validation for numeric values
714	/// - Path validation for security
715	/// - URL validation for network resources
716	fn ValidateConfiguration(&self, config:&AirConfiguration) -> Result<()> {
717		// Schema version validation
718		self.ValidateSchemaVersion(&config.SchemaVersion)?;
719
720		// Profile validation
721		self.ValidateProfile(&config.Profile)?;
722
723		// gRPC configuration validation
724		self.ValidategRPCConfig(&config.gRPC)?;
725
726		// Authentication configuration validation
727		self.ValidateAuthConfig(&config.Authentication)?;
728
729		// Update configuration validation
730		self.ValidateUpdateConfig(&config.Updates)?;
731
732		// Download configuration validation
733		self.ValidateDownloadConfig(&config.Downloader)?;
734
735		// Indexing configuration validation
736		self.ValidateIndexingConfig(&config.Indexing)?;
737
738		// Logging configuration validation
739		self.ValidateLoggingConfig(&config.Logging)?;
740
741		// Performance configuration validation
742		self.ValidatePerformanceConfig(&config.Performance)?;
743
744		dev_log!("config", "All configuration validation checks passed");
745
746		Ok(())
747	}
748
749	/// Validate schema version format
750	fn ValidateSchemaVersion(&self, version:&str) -> Result<()> {
751		if !version.chars().all(|c| c.is_digit(10) || c == '.') {
752			return Err(AirError::Configuration(format!(
753				"Invalid schema version '{}': must be in format X.Y.Z",
754				version
755			)));
756		}
757
758		let parts:Vec<&str> = version.split('.').collect();
759
760		if parts.len() != 3 {
761			return Err(AirError::Configuration(format!(
762				"Invalid schema version '{}': must have 3 parts (X.Y.Z)",
763				version
764			)));
765		}
766
767		for (i, part) in parts.iter().enumerate() {
768			if part.is_empty() {
769				return Err(AirError::Configuration(format!(
770					"Invalid schema version '{}': part {} is empty",
771					version,
772					i + 1
773				)));
774			}
775		}
776
777		Ok(())
778	}
779
780	/// Validate profile name
781	fn ValidateProfile(&self, profile:&str) -> Result<()> {
782		let ValidProfiles = ["dev", "staging", "prod", "custom"];
783
784		if !ValidProfiles.contains(&profile) {
785			return Err(AirError::Configuration(format!(
786				"Invalid profile '{}': must be one of: {}",
787				profile,
788				ValidProfiles.join(", ")
789			)));
790		}
791
792		Ok(())
793	}
794
795	/// Validate gRPC configuration with range checking
796	fn ValidategRPCConfig(&self, grpc:&gRPCConfig) -> Result<()> {
797		// Validate bind address
798		if grpc.BindAddress.is_empty() {
799			return Err(AirError::Configuration("gRPC bind address cannot be empty".to_string()));
800		}
801
802		// Validate address format
803		if !Self::IsValidAddress(&grpc.BindAddress) {
804			return Err(AirError::Configuration(format!(
805				"Invalid gRPC bind address '{}': must be in format host:port or [IPv6]:port",
806				grpc.BindAddress
807			)));
808		}
809
810		// Validate MaxConnections range [10, 10000]
811		if grpc.MaxConnections < 10 {
812			return Err(AirError::Configuration(format!(
813				"gRPC MaxConnections {} is below minimum (10)",
814				grpc.MaxConnections
815			)));
816		}
817
818		if grpc.MaxConnections > 10000 {
819			return Err(AirError::Configuration(format!(
820				"gRPC MaxConnections {} exceeds maximum (10000)",
821				grpc.MaxConnections
822			)));
823		}
824
825		// Validate RequestTimeoutSecs range [1, 3600]
826		if grpc.RequestTimeoutSecs < 1 {
827			return Err(AirError::Configuration(format!(
828				"gRPC RequestTimeoutSecs {} is below minimum (1 second)",
829				grpc.RequestTimeoutSecs
830			)));
831		}
832
833		if grpc.RequestTimeoutSecs > 3600 {
834			return Err(AirError::Configuration(format!(
835				"gRPC RequestTimeoutSecs {} exceeds maximum (3600 seconds = 1 hour)",
836				grpc.RequestTimeoutSecs
837			)));
838		}
839
840		Ok(())
841	}
842
843	/// Validate authentication configuration
844	fn ValidateAuthConfig(&self, auth:&AuthConfig) -> Result<()> {
845		// If authentication is enabled, validate credentials path
846		if auth.Enabled {
847			if auth.CredentialsPath.is_empty() {
848				return Err(AirError::Configuration(
849					"Authentication credentials path cannot be empty when authentication is enabled".to_string(),
850				));
851			}
852
853			// Validate path for security (prevent directory traversal)
854			self.ValidatePath(&auth.CredentialsPath)?;
855		}
856
857		// Validate TokenExpirationHours range [1, 8760]
858		if auth.TokenExpirationHours < 1 {
859			return Err(AirError::Configuration(format!(
860				"Token expiration hours {} is below minimum (1 hour)",
861				auth.TokenExpirationHours
862			)));
863		}
864
865		if auth.TokenExpirationHours > 8760 {
866			return Err(AirError::Configuration(format!(
867				"Token expiration hours {} exceeds maximum (8760 hours = 1 year)",
868				auth.TokenExpirationHours
869			)));
870		}
871
872		// Validate MaxSessions range [1, 1000]
873		if auth.MaxSessions < 1 {
874			return Err(AirError::Configuration(format!(
875				"Max sessions {} is below minimum (1)",
876				auth.MaxSessions
877			)));
878		}
879
880		if auth.MaxSessions > 1000 {
881			return Err(AirError::Configuration(format!(
882				"Max sessions {} exceeds maximum (1000)",
883				auth.MaxSessions
884			)));
885		}
886
887		Ok(())
888	}
889
890	/// Validate update configuration
891	fn ValidateUpdateConfig(&self, updates:&UpdateConfig) -> Result<()> {
892		if updates.Enabled {
893			// Validate update server URL
894			if updates.UpdateServerUrl.is_empty() {
895				return Err(AirError::Configuration(
896					"Update server URL cannot be empty when updates are enabled".to_string(),
897				));
898			}
899
900			// Must be HTTPS for security
901			if !updates.UpdateServerUrl.starts_with("https://") {
902				return Err(AirError::Configuration(format!(
903					"Update server URL must use HTTPS, got: {}",
904					updates.UpdateServerUrl
905				)));
906			}
907
908			// Validate URL format
909			if !Self::IsValidUrl(&updates.UpdateServerUrl) {
910				return Err(AirError::Configuration(format!(
911					"Invalid update server URL '{}'",
912					updates.UpdateServerUrl
913				)));
914			}
915		}
916
917		// Validate CheckIntervalHours range [1, 168]
918		if updates.CheckIntervalHours < 1 {
919			return Err(AirError::Configuration(format!(
920				"Update check interval {} hours is below minimum (1 hour)",
921				updates.CheckIntervalHours
922			)));
923		}
924
925		if updates.CheckIntervalHours > 168 {
926			return Err(AirError::Configuration(format!(
927				"Update check interval {} hours exceeds maximum (168 hours = 1 week)",
928				updates.CheckIntervalHours
929			)));
930		}
931
932		Ok(())
933	}
934
935	/// Validate download configuration
936	fn ValidateDownloadConfig(&self, downloader:&DownloadConfig) -> Result<()> {
937		if downloader.Enabled {
938			if downloader.CacheDirectory.is_empty() {
939				return Err(AirError::Configuration(
940					"Download cache directory cannot be empty when downloader is enabled".to_string(),
941				));
942			}
943
944			// Validate path for security
945			self.ValidatePath(&downloader.CacheDirectory)?;
946		}
947
948		// Validate MaxConcurrentDownloads range [1, 50]
949		if downloader.MaxConcurrentDownloads < 1 {
950			return Err(AirError::Configuration(format!(
951				"Max concurrent downloads {} is below minimum (1)",
952				downloader.MaxConcurrentDownloads
953			)));
954		}
955
956		if downloader.MaxConcurrentDownloads > 50 {
957			return Err(AirError::Configuration(format!(
958				"Max concurrent downloads {} exceeds maximum (50)",
959				downloader.MaxConcurrentDownloads
960			)));
961		}
962
963		// Validate DownloadTimeoutSecs range [10, 3600]
964		if downloader.DownloadTimeoutSecs < 10 {
965			return Err(AirError::Configuration(format!(
966				"Download timeout {} seconds is below minimum (10 seconds)",
967				downloader.DownloadTimeoutSecs
968			)));
969		}
970
971		if downloader.DownloadTimeoutSecs > 3600 {
972			return Err(AirError::Configuration(format!(
973				"Download timeout {} seconds exceeds maximum (3600 seconds = 1 hour)",
974				downloader.DownloadTimeoutSecs
975			)));
976		}
977
978		// Validate MaxRetries range [0, 10]
979		if downloader.MaxRetries > 10 {
980			return Err(AirError::Configuration(format!(
981				"Max retries {} exceeds maximum (10)",
982				downloader.MaxRetries
983			)));
984		}
985
986		Ok(())
987	}
988
989	/// Validate indexing configuration
990	fn ValidateIndexingConfig(&self, indexing:&IndexingConfig) -> Result<()> {
991		if indexing.Enabled {
992			if indexing.IndexDirectory.is_empty() {
993				return Err(AirError::Configuration(
994					"Index directory cannot be empty when indexing is enabled".to_string(),
995				));
996			}
997
998			// Validate path for security
999			self.ValidatePath(&indexing.IndexDirectory)?;
1000
1001			// Validate FileTypes is not empty
1002			if indexing.FileTypes.is_empty() {
1003				return Err(AirError::Configuration(
1004					"File types to index cannot be empty when indexing is enabled".to_string(),
1005				));
1006			}
1007
1008			// Validate each file type pattern
1009			for FileType in &indexing.FileTypes {
1010				if FileType.is_empty() {
1011					return Err(AirError::Configuration("File type pattern cannot be empty".to_string()));
1012				}
1013
1014				if !FileType.contains('*') {
1015					dev_log!(
1016						"config",
1017						"warn: File type pattern '{}' does not contain wildcards, may not match as expected",
1018						FileType
1019					);
1020				}
1021			}
1022		}
1023
1024		// Validate MaxFileSizeMb range [1, 1024]
1025		if indexing.MaxFileSizeMb < 1 {
1026			return Err(AirError::Configuration(format!(
1027				"Max file size {} MB is below minimum (1 MB)",
1028				indexing.MaxFileSizeMb
1029			)));
1030		}
1031
1032		if indexing.MaxFileSizeMb > 1024 {
1033			return Err(AirError::Configuration(format!(
1034				"Max file size {} MB exceeds maximum (1024 MB = 1 GB)",
1035				indexing.MaxFileSizeMb
1036			)));
1037		}
1038
1039		// Validate UpdateIntervalMinutes range [1, 1440]
1040		if indexing.UpdateIntervalMinutes < 1 {
1041			return Err(AirError::Configuration(format!(
1042				"Index update interval {} minutes is below minimum (1 minute)",
1043				indexing.UpdateIntervalMinutes
1044			)));
1045		}
1046
1047		if indexing.UpdateIntervalMinutes > 1440 {
1048			return Err(AirError::Configuration(format!(
1049				"Index update interval {} minutes exceeds maximum (1440 minutes = 1 day)",
1050				indexing.UpdateIntervalMinutes
1051			)));
1052		}
1053
1054		Ok(())
1055	}
1056
1057	/// Validate logging configuration
1058	fn ValidateLoggingConfig(&self, logging:&LoggingConfig) -> Result<()> {
1059		// Validate log level
1060		let ValidLevels = ["trace", "debug", "info", "warn", "error"];
1061
1062		if !ValidLevels.contains(&logging.Level.as_str()) {
1063			return Err(AirError::Configuration(format!(
1064				"Invalid log level '{}': must be one of: {}",
1065				logging.Level,
1066				ValidLevels.join(", ")
1067			)));
1068		}
1069
1070		// Validate file path if provided
1071		if let Some(ref FilePath) = logging.FilePath {
1072			if !FilePath.is_empty() {
1073				self.ValidatePath(FilePath)?;
1074			}
1075		}
1076
1077		// Validate MaxFileSizeMb range [1, 1000]
1078		if logging.MaxFileSizeMb < 1 {
1079			return Err(AirError::Configuration(format!(
1080				"Max log file size {} MB is below minimum (1 MB)",
1081				logging.MaxFileSizeMb
1082			)));
1083		}
1084
1085		if logging.MaxFileSizeMb > 1000 {
1086			return Err(AirError::Configuration(format!(
1087				"Max log file size {} MB exceeds maximum (1000 MB = 1 GB)",
1088				logging.MaxFileSizeMb
1089			)));
1090		}
1091
1092		// Validate MaxFiles range [1, 50]
1093		if logging.MaxFiles < 1 {
1094			return Err(AirError::Configuration(format!(
1095				"Max log files {} is below minimum (1)",
1096				logging.MaxFiles
1097			)));
1098		}
1099
1100		if logging.MaxFiles > 50 {
1101			return Err(AirError::Configuration(format!(
1102				"Max log files {} exceeds maximum (50)",
1103				logging.MaxFiles
1104			)));
1105		}
1106
1107		Ok(())
1108	}
1109
1110	/// Validate performance configuration
1111	fn ValidatePerformanceConfig(&self, performance:&PerformanceConfig) -> Result<()> {
1112		// Validate MemoryLimitMb range [64, 16384]
1113		if performance.MemoryLimitMb < 64 {
1114			return Err(AirError::Configuration(format!(
1115				"Memory limit {} MB is below minimum (64 MB)",
1116				performance.MemoryLimitMb
1117			)));
1118		}
1119
1120		if performance.MemoryLimitMb > 16384 {
1121			return Err(AirError::Configuration(format!(
1122				"Memory limit {} MB exceeds maximum (16384 MB = 16 GB)",
1123				performance.MemoryLimitMb
1124			)));
1125		}
1126
1127		// Validate CPULimitPercent range [10, 100]
1128		if performance.CPULimitPercent < 10 {
1129			return Err(AirError::Configuration(format!(
1130				"CPU limit {}% is below minimum (10%)",
1131				performance.CPULimitPercent
1132			)));
1133		}
1134
1135		if performance.CPULimitPercent > 100 {
1136			return Err(AirError::Configuration(format!(
1137				"CPU limit {}% exceeds maximum (100%)",
1138				performance.CPULimitPercent
1139			)));
1140		}
1141
1142		// Validate DiskLimitMb range [100, 102400]
1143		if performance.DiskLimitMb < 100 {
1144			return Err(AirError::Configuration(format!(
1145				"Disk limit {} MB is below minimum (100 MB)",
1146				performance.DiskLimitMb
1147			)));
1148		}
1149
1150		if performance.DiskLimitMb > 102400 {
1151			return Err(AirError::Configuration(format!(
1152				"Disk limit {} MB exceeds maximum (102400 MB = 100 GB)",
1153				performance.DiskLimitMb
1154			)));
1155		}
1156
1157		// Validate BackgroundTaskIntervalSecs range [1, 3600]
1158		if performance.BackgroundTaskIntervalSecs < 1 {
1159			return Err(AirError::Configuration(format!(
1160				"Background task interval {} seconds is below minimum (1 second)",
1161				performance.BackgroundTaskIntervalSecs
1162			)));
1163		}
1164
1165		if performance.BackgroundTaskIntervalSecs > 3600 {
1166			return Err(AirError::Configuration(format!(
1167				"Background task interval {} seconds exceeds maximum (3600 seconds = 1 hour)",
1168				performance.BackgroundTaskIntervalSecs
1169			)));
1170		}
1171
1172		Ok(())
1173	}
1174
1175	/// Validate path for security (prevent directory traversal)
1176	fn ValidatePath(&self, path:&str) -> Result<()> {
1177		if path.is_empty() {
1178			return Err(AirError::Configuration("Path cannot be empty".to_string()));
1179		}
1180
1181		// Check for path traversal attempts
1182		if path.contains("..") {
1183			return Err(AirError::Configuration(format!(
1184				"Path '{}' contains '..' which is not allowed for security reasons",
1185				path
1186			)));
1187		}
1188
1189		// Check for absolute path patterns that might be problematic
1190		if path.starts_with("\\\\") || path.starts_with("//") {
1191			return Err(AirError::Configuration(format!(
1192				"Path '{}' uses UNC/network path format which may not be supported",
1193				path
1194			)));
1195		}
1196
1197		// Validate that the path doesn't contain null bytes
1198		if path.contains('\0') {
1199			return Err(AirError::Configuration(
1200				"Path contains null bytes which is not allowed".to_string(),
1201			));
1202		}
1203
1204		Ok(())
1205	}
1206
1207	/// Validate address format (IP:port or hostname:port)
1208	fn IsValidAddress(addr:&str) -> bool {
1209		// Check for IPv6 format: [IPv6]:port
1210		if addr.starts_with('[') && addr.contains("]:") {
1211			return true;
1212		}
1213
1214		// Check for IPv4 or hostname format: host:port
1215		if addr.contains(':') {
1216			let parts:Vec<&str> = addr.split(':').collect();
1217
1218			if parts.len() != 2 {
1219				return false;
1220			}
1221
1222			// Validate port
1223			if let Ok(port) = parts[1].parse::<u16>() {
1224				return port > 0;
1225			}
1226
1227			return false;
1228		}
1229
1230		false
1231	}
1232
1233	/// Validate URL format
1234	fn IsValidUrl(url:&str) -> bool { url::Url::parse(url).is_ok() }
1235
1236	/// Perform schema-based validation
1237	fn SchemaValidate(&self, config:&AirConfiguration) -> Result<()> {
1238		let _schema = generate_schema();
1239
1240		// Convert config to JSON for validation
1241		let ConfigJson = serde_json::to_value(config)
1242			.map_err(|e| AirError::Configuration(format!("Failed to serialize config for schema validation: {}", e)))?;
1243
1244		// Basic schema validation (would use jsonschema crate in production)
1245		// For now, we do manual validation
1246		if !ConfigJson.is_object() {
1247			return Err(AirError::Configuration("Configuration must be an object".to_string()));
1248		}
1249
1250		dev_log!("config", "Schema validation passed");
1251
1252		Ok(())
1253	}
1254
1255	/// Apply environment variable overrides to configuration
1256	///
1257	/// Environment variables are read with the configured prefix.
1258	/// For example, with prefix "AIR_", the variable "AIR_GRPC_BIND_ADDRESS"
1259	/// would override grpc.bind_address.
1260	///
1261	/// Variable naming convention: {PREFIX}_{SECTION}_{FIELD} (uppercase,
1262	/// underscores)
1263	fn ApplyEnvironmentOverrides(&self, config:&mut AirConfiguration) -> Result<()> {
1264		let mut override_count = 0;
1265
1266		// gRPC overrides
1267		if let Ok(val) = env::var(&format!("{}GRPC_BIND_ADDRESS", self.EnvPrefix)) {
1268			config.gRPC.BindAddress = val;
1269
1270			override_count += 1;
1271		}
1272
1273		if let Ok(val) = env::var(&format!("{}GRPC_MAX_CONNECTIONS", self.EnvPrefix)) {
1274			config.gRPC.MaxConnections = val
1275				.parse()
1276				.map_err(|e| AirError::Configuration(format!("Invalid GRPC_MAX_CONNECTIONS value: {}", e)))?;
1277
1278			override_count += 1;
1279		}
1280
1281		// Authentication overrides
1282		if let Ok(val) = env::var(&format!("{}AUTH_ENABLED", self.EnvPrefix)) {
1283			config.Authentication.Enabled = val
1284				.parse()
1285				.map_err(|e| AirError::Configuration(format!("Invalid AUTH_ENABLED value: {}", e)))?;
1286
1287			override_count += 1;
1288		}
1289
1290		if let Ok(val) = env::var(&format!("{}AUTH_CREDENTIALS_PATH", self.EnvPrefix)) {
1291			config.Authentication.CredentialsPath = val;
1292
1293			override_count += 1;
1294		}
1295
1296		// Update overrides
1297		if let Ok(val) = env::var(&format!("{}UPDATE_ENABLED", self.EnvPrefix)) {
1298			config.Updates.Enabled = val
1299				.parse()
1300				.map_err(|e| AirError::Configuration(format!("Invalid UPDATE_ENABLED value: {}", e)))?;
1301
1302			override_count += 1;
1303		}
1304
1305		if let Ok(val) = env::var(&format!("{}UPDATE_AUTO_DOWNLOAD", self.EnvPrefix)) {
1306			config.Updates.AutoDownload = val
1307				.parse()
1308				.map_err(|e| AirError::Configuration(format!("Invalid UPDATE_AUTO_DOWNLOAD value: {}", e)))?;
1309
1310			override_count += 1;
1311		}
1312
1313		// Logging overrides
1314		if let Ok(val) = env::var(&format!("{}LOGGING_LEVEL", self.EnvPrefix)) {
1315			config.Logging.Level = val.to_lowercase();
1316
1317			override_count += 1;
1318		}
1319
1320		if override_count > 0 {
1321			dev_log!("config", "Applied {} environment variable override(s)", override_count);
1322		}
1323
1324		Ok(())
1325	}
1326
1327	/// Backup current configuration file
1328	///
1329	/// Creates a timestamped backup of the current configuration file
1330	/// in the configured backup directory.
1331	async fn BackupConfiguration(&self, config_path:&Path) -> Result<()> {
1332		let backup_dir = self
1333			.BackupDir
1334			.as_ref()
1335			.ok_or_else(|| AirError::Configuration("Backup directory not configured".to_string()))?;
1336
1337		// Create backup directory if it doesn't exist
1338		tokio::fs::create_dir_all(backup_dir).await.map_err(|e| {
1339			AirError::Configuration(format!("Failed to create backup directory '{}': {}", backup_dir.display(), e))
1340		})?;
1341
1342		// Generate backup filename with timestamp
1343		let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
1344
1345		let backup_filename = format!(
1346			"{}_config_{}.toml.bak",
1347			config_path.file_stem().and_then(|s| s.to_str()).unwrap_or("config"),
1348			timestamp
1349		);
1350
1351		let backup_path = backup_dir.join(&backup_filename);
1352
1353		// Copy current config to backup
1354		tokio::fs::copy(config_path, &backup_path).await.map_err(|e| {
1355			AirError::Configuration(format!("Failed to create backup '{}': {}", backup_path.display(), e))
1356		})?;
1357
1358		dev_log!("config", "Configuration backed up to: {}", backup_path.display());
1359
1360		Ok(())
1361	}
1362
1363	/// Rollback configuration from the most recent backup
1364	///
1365	/// # Returns
1366	///
1367	/// Returns the path to the backup file that was restored
1368	pub async fn RollbackConfiguration(&self) -> Result<PathBuf> {
1369		let config_path = self.GetConfigPath()?;
1370
1371		let backup_dir = self
1372			.BackupDir
1373			.as_ref()
1374			.ok_or_else(|| AirError::Configuration("Backup directory not configured".to_string()))?;
1375
1376		// Find the most recent backup
1377		let mut backups = tokio::fs::read_dir(backup_dir).await.map_err(|e| {
1378			AirError::Configuration(format!("Failed to read backup directory '{}': {}", backup_dir.display(), e))
1379		})?;
1380
1381		let mut most_recent:Option<(tokio::fs::DirEntry, std::time::SystemTime)> = None;
1382
1383		while let Some(entry) = backups
1384			.next_entry()
1385			.await
1386			.map_err(|e| AirError::Configuration(format!("Failed to read backup entry: {}", e)))?
1387		{
1388			let metadata = entry
1389				.metadata()
1390				.await
1391				.map_err(|e| AirError::Configuration(format!("Failed to get metadata: {}", e)))?;
1392
1393			if let Ok(modified) = metadata.modified() {
1394				if most_recent.is_none() || modified > most_recent.as_ref().unwrap().1 {
1395					most_recent = Some((entry, modified));
1396				}
1397			}
1398		}
1399
1400		let (backup_entry, _) =
1401			most_recent.ok_or_else(|| AirError::Configuration("No backup files found".to_string()))?;
1402
1403		let backup_path = backup_entry.path();
1404
1405		// Restore from backup
1406		tokio::fs::copy(&backup_path, &config_path).await.map_err(|e| {
1407			AirError::Configuration(format!("Failed to restore from backup '{}': {}", backup_path.display(), e))
1408		})?;
1409
1410		dev_log!("config", "Configuration rolled back from: {}", backup_path.display());
1411
1412		Ok(backup_path)
1413	}
1414
1415	/// Get the configuration file path
1416	///
1417	/// Returns the configured path or the default path
1418	fn GetConfigPath(&self) -> Result<PathBuf> {
1419		if let Some(ref path) = self.ConfigPath {
1420			Ok(path.clone())
1421		} else {
1422			Self::GetDefaultConfigPath()
1423		}
1424	}
1425
1426	/// Get default configuration file path
1427	///
1428	/// Returns the default configuration file path in the user's config
1429	/// directory
1430	fn GetDefaultConfigPath() -> Result<PathBuf> {
1431		let config_dir = dirs::config_dir()
1432			.ok_or_else(|| AirError::Configuration("Cannot determine config directory".to_string()))?;
1433
1434		Ok(config_dir.join("Air").join(DefaultConfigFile))
1435	}
1436
1437	/// Get profile-specific default configuration
1438	///
1439	/// # Arguments
1440	///
1441	/// * `profile` - Profile name (dev, staging, prod, custom)
1442	///
1443	/// # Returns
1444	///
1445	/// Configuration with profile-appropriate defaults
1446	pub fn GetProfileDefaults(profile:&str) -> AirConfiguration {
1447		let mut config = AirConfiguration::default();
1448
1449		config.Profile = profile.to_string();
1450
1451		match profile {
1452			"prod" => {
1453				config.Logging.Level = "warn".to_string();
1454
1455				config.Logging.ConsoleEnabled = false;
1456
1457				config.Performance.MemoryLimitMb = 1024;
1458
1459				config.Performance.CPULimitPercent = 80;
1460			},
1461
1462			"staging" => {
1463				config.Logging.Level = "info".to_string();
1464
1465				config.Performance.MemoryLimitMb = 768;
1466
1467				config.Performance.CPULimitPercent = 70;
1468			},
1469
1470			"dev" | _ => {
1471				// Dev defaults are already set
1472				config.Logging.Level = "debug".to_string();
1473
1474				config.Logging.ConsoleEnabled = true;
1475
1476				config.Performance.MemoryLimitMb = 512;
1477
1478				config.Performance.CPULimitPercent = 50;
1479			},
1480		}
1481
1482		config
1483	}
1484
1485	/// Expand path with home directory (~) expansion
1486	///
1487	/// # Arguments
1488	///
1489	/// * `path` - Path string to expand
1490	///
1491	/// # Returns
1492	///
1493	/// Expanded PathBuf
1494	pub fn ExpandPath(path:&str) -> Result<PathBuf> {
1495		if path.is_empty() {
1496			return Err(AirError::Configuration("Cannot expand empty path".to_string()));
1497		}
1498
1499		if path.starts_with('~') {
1500			let home = dirs::home_dir()
1501				.ok_or_else(|| AirError::Configuration("Cannot determine home directory".to_string()))?;
1502
1503			let rest = &path[1..]; // Remove ~
1504			if rest.starts_with('/') || rest.starts_with('\\') {
1505				Ok(home.join(&rest[1..]))
1506			} else {
1507				Ok(home.join(rest))
1508			}
1509		} else {
1510			Ok(PathBuf::from(path))
1511		}
1512	}
1513
1514	/// Generate configuration hash for change detection
1515	///
1516	/// # Arguments
1517	///
1518	/// * `config` - Configuration to hash
1519	///
1520	/// # Returns
1521	///
1522	/// SHA256 hash of the configuration
1523	pub fn ComputeHash(config:&AirConfiguration) -> Result<String> {
1524		let config_str = toml::to_string_pretty(config)
1525			.map_err(|e| AirError::Configuration(format!("Failed to serialize config: {}", e)))?;
1526
1527		let mut hasher = sha2::Sha256::new();
1528
1529		hasher.update(config_str.as_bytes());
1530
1531		let hash = hasher.finalize();
1532
1533		Ok(hex::encode(hash))
1534	}
1535
1536	/// Export configuration to JSON (for VSCode compatibility)
1537	///
1538	/// # Arguments
1539	///
1540	/// * `config` - Configuration to export
1541	///
1542	/// # Returns
1543	///
1544	/// JSON string representation of configuration
1545	pub fn ExportToJson(config:&AirConfiguration) -> Result<String> {
1546		serde_json::to_string_pretty(config)
1547			.map_err(|e| AirError::Configuration(format!("Failed to export to JSON: {}", e)))
1548	}
1549
1550	/// Import configuration from JSON (for VSCode compatibility)
1551	///
1552	/// # Arguments
1553	///
1554	/// * `json_str` - JSON string to import
1555	///
1556	/// # Returns
1557	///
1558	/// Parsed and validated configuration
1559	pub fn ImportFromJson(json_str:&str) -> Result<AirConfiguration> {
1560		let config:AirConfiguration = serde_json::from_str(json_str)
1561			.map_err(|e| AirError::Configuration(format!("Failed to import from JSON: {}", e)))?;
1562
1563		Ok(config)
1564	}
1565
1566	/// Get environment variable mappings
1567	///
1568	/// Returns a mapping of configuration paths to environment variable names
1569	pub fn GetEnvironmentMappings(&self) -> HashMap<String, String> {
1570		let prefix = &self.EnvPrefix;
1571
1572		let mut mappings = HashMap::new();
1573
1574		mappings.insert("grpc.bind_address".to_string(), format!("{}GRPC_BIND_ADDRESS", prefix));
1575
1576		mappings.insert("grpc.max_connections".to_string(), format!("{}GRPC_MAX_CONNECTIONS", prefix));
1577
1578		mappings.insert(
1579			"grpc.request_timeout_secs".to_string(),
1580			format!("{}GRPC_REQUEST_TIMEOUT_SECS", prefix),
1581		);
1582
1583		mappings.insert("authentication.enabled".to_string(), format!("{}AUTH_ENABLED", prefix));
1584
1585		mappings.insert(
1586			"authentication.credentials_path".to_string(),
1587			format!("{}AUTH_CREDENTIALS_PATH", prefix),
1588		);
1589
1590		mappings.insert(
1591			"authentication.token_expiration_hours".to_string(),
1592			format!("{}AUTH_TOKEN_EXPIRATION_HOURS", prefix),
1593		);
1594
1595		mappings.insert("updates.enabled".to_string(), format!("{}UPDATE_ENABLED", prefix));
1596
1597		mappings.insert("updates.auto_download".to_string(), format!("{}UPDATE_AUTO_DOWNLOAD", prefix));
1598
1599		mappings.insert("updates.auto_install".to_string(), format!("{}UPDATE_AUTO_INSTALL", prefix));
1600
1601		mappings.insert("logging.level".to_string(), format!("{}LOGGING_LEVEL", prefix));
1602
1603		mappings.insert(
1604			"logging.console_enabled".to_string(),
1605			format!("{}LOGGING_CONSOLE_ENABLED", prefix),
1606		);
1607
1608		mappings
1609	}
1610}
1611
1612#[cfg(test)]
1613mod tests {
1614
1615	use super::*;
1616
1617	#[test]
1618	fn test_default_configuration() {
1619		let config = AirConfiguration::default();
1620
1621		assert_eq!(config.SchemaVersion, "1.0.0");
1622
1623		assert_eq!(config.Profile, "dev");
1624
1625		assert!(config.Authentication.Enabled);
1626
1627		assert!(config.Logging.ConsoleEnabled);
1628	}
1629
1630	#[test]
1631	fn test_profile_defaults() {
1632		let DevConfig = ConfigurationManager::GetProfileDefaults("dev");
1633
1634		assert_eq!(DevConfig.Profile, "dev");
1635
1636		assert_eq!(DevConfig.Logging.Level, "debug");
1637
1638		let ProdConfig = ConfigurationManager::GetProfileDefaults("prod");
1639
1640		assert_eq!(ProdConfig.Profile, "prod");
1641
1642		assert_eq!(ProdConfig.Logging.Level, "warn");
1643
1644		assert!(!ProdConfig.Logging.ConsoleEnabled);
1645	}
1646
1647	#[test]
1648	fn test_path_expansion() {
1649		let Home = dirs::home_dir().expect("Cannot determine home directory");
1650
1651		let Expanded = ConfigurationManager::ExpandPath("~/test").unwrap();
1652
1653		assert_eq!(Expanded, Home.join("test"));
1654
1655		let Absolute = ConfigurationManager::ExpandPath("/tmp/test").unwrap();
1656
1657		assert_eq!(Absolute, PathBuf::from("/tmp/test"));
1658	}
1659
1660	#[test]
1661	fn test_address_validation() {
1662		assert!(ConfigurationManager::IsValidAddress("[::1]:50053"));
1663
1664		assert!(ConfigurationManager::IsValidAddress("127.0.0.1:50053"));
1665
1666		assert!(ConfigurationManager::IsValidAddress("localhost:50053"));
1667
1668		assert!(!ConfigurationManager::IsValidAddress("invalid"));
1669	}
1670
1671	#[test]
1672	fn test_url_validation() {
1673		assert!(ConfigurationManager::IsValidUrl("https://example.com"));
1674
1675		assert!(ConfigurationManager::IsValidUrl("https://updates.land.playform.cloud"));
1676
1677		assert!(!ConfigurationManager::IsValidUrl("not-a-url"));
1678
1679		assert!(!ConfigurationManager::IsValidUrl("http://insecure.com"));
1680	}
1681
1682	#[test]
1683	fn test_path_validation() {
1684		let manager = ConfigurationManager::New(None).unwrap();
1685
1686		assert!(manager.ValidatePath("~/config").is_ok());
1687
1688		assert!(manager.ValidatePath("/tmp/config").is_ok());
1689
1690		assert!(manager.ValidatePath("../escaped").is_err());
1691
1692		assert!(manager.ValidatePath("").is_err());
1693	}
1694
1695	#[tokio::test]
1696	async fn test_export_import_json() {
1697		let config = AirConfiguration::default();
1698
1699		let json_str = ConfigurationManager::ExportToJson(&config).unwrap();
1700
1701		let imported = ConfigurationManager::ImportFromJson(&json_str).unwrap();
1702
1703		assert_eq!(imported.SchemaVersion, config.SchemaVersion);
1704
1705		assert_eq!(imported.Profile, config.Profile);
1706
1707		assert_eq!(imported.gRPC.BindAddress, config.gRPC.BindAddress);
1708	}
1709
1710	#[test]
1711	fn test_compute_hash() {
1712		let config = AirConfiguration::default();
1713
1714		let hash1 = ConfigurationManager::ComputeHash(&config).unwrap();
1715
1716		let hash2 = ConfigurationManager::ComputeHash(&config).unwrap();
1717
1718		assert_eq!(hash1, hash2);
1719
1720		let mut modified = config;
1721
1722		modified.gRPC.BindAddress = "[::1]:50054".to_string();
1723
1724		let hash3 = ConfigurationManager::ComputeHash(&modified).unwrap();
1725
1726		assert_ne!(hash1, hash3);
1727	}
1728
1729	#[test]
1730	fn test_generate_schema() {
1731		let schema = generate_schema();
1732
1733		assert!(schema.is_object());
1734
1735		assert!(schema.get("$schema").is_some());
1736
1737		assert!(schema.get("properties").is_some());
1738	}
1739}