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