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//! ```
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//! ## TODO: 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//! ## TODO: 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//! ## TODO: 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};
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			Profile:default_profile(),
441			Grpc:GrpcConfig {
442				BindAddress:default_grpc_bind_address(),
443				MaxConnections:default_grpc_max_connections(),
444				RequestTimeoutSecs:default_grpc_request_timeout(),
445			},
446			Authentication:AuthConfig {
447				Enabled:default_auth_enabled(),
448				CredentialsPath:default_auth_credentials_path(),
449				TokenExpirationHours:default_auth_token_expiration(),
450				MaxSessions:default_auth_max_sessions(),
451			},
452			Updates:UpdateConfig {
453				Enabled:default_update_enabled(),
454				CheckIntervalHours:default_update_check_interval(),
455				UpdateServerUrl:default_update_server_url(),
456				AutoDownload:default_update_auto_download(),
457				AutoInstall:default_update_auto_install(),
458				Channel:default_update_channel(),
459			},
460			Downloader:DownloadConfig {
461				Enabled:default_download_enabled(),
462				MaxConcurrentDownloads:default_download_max_concurrent(),
463				DownloadTimeoutSecs:default_download_timeout(),
464				MaxRetries:default_download_max_retries(),
465				CacheDirectory:default_download_cache_dir(),
466			},
467			Indexing:IndexingConfig {
468				Enabled:default_indexing_enabled(),
469				MaxFileSizeMb:default_indexing_max_file_size(),
470				FileTypes:default_indexing_file_types(),
471				UpdateIntervalMinutes:default_indexing_update_interval(),
472				IndexDirectory:default_indexing_directory(),
473				MaxParallelIndexing:default_max_parallel_indexing(),
474			},
475			Logging:LoggingConfig {
476				Level:default_logging_level(),
477				FilePath:default_logging_file_path(),
478				ConsoleEnabled:default_logging_console_enabled(),
479				MaxFileSizeMb:default_logging_max_file_size(),
480				MaxFiles:default_logging_max_files(),
481			},
482			Performance:PerformanceConfig {
483				MemoryLimitMb:default_perf_memory_limit(),
484				CPULimitPercent:default_perf_cpu_limit(),
485				DiskLimitMb:default_perf_disk_limit(),
486				BackgroundTaskIntervalSecs:default_perf_task_interval(),
487			},
488		}
489	}
490}
491
492// =============================================================================
493// Configuration Schema
494// =============================================================================
495
496/// Generate JSON Schema for configuration validation
497pub fn generate_schema() -> JsonValue {
498	json!({
499		"$schema": "http://json-schema.org/draft-07/schema#",
500		"title": "Air Configuration Schema",
501		"description": "Configuration schema for Air daemon",
502		"type": "object",
503		"required": ["SchemaVersion", "profile"],
504		"properties": {
505			"SchemaVersion": {
506				"type": "string",
507				"description": "Configuration schema version for migration tracking",
508				"pattern": "^\\d+\\.\\d+\\.\\d+$"
509			},
510			"profile": {
511				"type": "string",
512				"description": "Profile name (dev, staging, prod, custom)",
513				"enum": ["dev", "staging", "prod", "custom"]
514			},
515			"grpc": {
516				"type": "object",
517				"description": "gRPC server configuration",
518				"properties": {
519					"BindAddress": {
520						"type": "string",
521						"description": "gRPC server bind address",
522						"format": "hostname-port"
523					},
524					"MaxConnections": {
525						"type": "integer",
526						"minimum": 10,
527						"maximum": 10000
528					},
529					"RequestTimeoutSecs": {
530						"type": "integer",
531						"minimum": 1,
532						"maximum": 3600
533					}
534				}
535			},
536			"authentication": {
537				"type": "object",
538				"description": "Authentication configuration",
539				"properties": {
540					"enabled": {"type": "boolean"},
541					"CredentialsPath": {"type": "string"},
542					"TokenExpirationHours": {
543						"type": "integer",
544						"minimum": 1,
545						"maximum": 8760
546					},
547					"MaxSessions": {
548						"type": "integer",
549						"minimum": 1,
550						"maximum": 1000
551					}
552				}
553			},
554			"updates": {
555				"type": "object",
556				"properties": {
557					"enabled": {"type": "boolean"},
558					"CheckIntervalHours": {
559						"type": "integer",
560						"minimum": 1,
561						"maximum": 168
562					},
563					"UpdateServerUrl": {
564						"type": "string",
565						"pattern": "^https://"
566					},
567					"AutoDownload": {"type": "boolean"},
568					"AutoInstall": {"type": "boolean"},
569					"channel": {
570						"type": "string",
571						"enum": ["stable", "insiders", "preview"]
572					}
573				}
574			},
575			"downloader": {
576				"type": "object",
577				"properties": {
578					"enabled": {"type": "boolean"},
579					"MaxConcurrentDownloads": {
580						"type": "integer",
581						"minimum": 1,
582						"maximum": 50
583					},
584					"DownloadTimeoutSecs": {
585						"type": "integer",
586						"minimum": 10,
587						"maximum": 3600
588					},
589					"MaxRetries": {
590						"type": "integer",
591						"minimum": 0,
592						"maximum": 10
593					},
594					"CacheDirectory": {"type": "string"}
595				}
596			},
597			"indexing": {
598				"type": "object",
599				"properties": {
600					"enabled": {"type": "boolean"},
601					"MaxFileSizeMb": {
602						"type": "integer",
603						"minimum": 1,
604						"maximum": 1024
605					},
606					"FileTypes": {
607						"type": "array",
608						"items": {"type": "string"}
609					},
610					"UpdateIntervalMinutes": {
611						"type": "integer",
612						"minimum": 1,
613						"maximum": 1440
614					},
615					"IndexDirectory": {"type": "string"}
616				}
617			},
618			"logging": {
619				"type": "object",
620				"properties": {
621					"level": {
622						"type": "string",
623						"enum": ["trace", "debug", "info", "warn", "error"]
624					},
625					"FilePath": {"type": ["string", "null"]},
626					"ConsoleEnabled": {"type": "boolean"},
627					"MaxFileSizeMb": {
628						"type": "integer",
629						"minimum": 1,
630						"maximum": 1000
631					},
632					"MaxFiles": {
633						"type": "integer",
634						"minimum": 1,
635						"maximum": 50
636					}
637				}
638			},
639			"performance": {
640				"type": "object",
641				"properties": {
642					"MemoryLimitMb": {
643						"type": "integer",
644						"minimum": 64,
645						"maximum": 16384
646					},
647					"CPULimitPercent": {
648						"type": "integer",
649						"minimum": 10,
650						"maximum": 100
651					},
652					"DiskLimitMb": {
653						"type": "integer",
654						"minimum": 100,
655						"maximum": 102400
656					},
657					"BackgroundTaskIntervalSecs": {
658						"type": "integer",
659						"minimum": 1,
660						"maximum": 3600
661					}
662				}
663			}
664		}
665	})
666}
667
668// =============================================================================
669// Configuration Manager
670// =============================================================================
671
672/// Configuration manager with comprehensive validation, backup, and hot-reload
673/// support
674pub struct ConfigurationManager {
675	/// Path to configuration file
676	ConfigPath:Option<PathBuf>,
677
678	/// Backup configuration directory
679	BackupDir:Option<PathBuf>,
680
681	/// Enable configuration backup
682	EnableBackup:bool,
683
684	/// Environment variable prefix for overrides
685	EnvPrefix:String,
686}
687
688impl ConfigurationManager {
689	/// Create a new configuration manager
690	///
691	/// # Arguments
692	///
693	/// * `ConfigPath` - Optional path to configuration file. If None, uses
694	///   default location
695	///
696	/// # Returns
697	///
698	/// Returns a new ConfigurationManager instance
699	pub fn New(ConfigPath:Option<String>) -> Result<Self> {
700		let path = ConfigPath.map(PathBuf::from);
701		let BackupDir = path
702			.as_ref()
703			.and_then(|p| p.parent())
704			.map(|parent| parent.join(".ConfigBackups"));
705
706		Ok(Self { ConfigPath:path, BackupDir, EnableBackup:true, EnvPrefix:"AIR_".to_string() })
707	}
708
709	/// Create a new configuration manager with custom settings
710	///
711	/// # Arguments
712	///
713	/// * `ConfigPath` - Optional path to configuration file
714	/// * `EnableBackup` - Whether to enable automatic backups
715	/// * `EnvPrefix` - Prefix for environment variable overrides
716	pub fn NewWithSettings(ConfigPath:Option<String>, EnableBackup:bool, EnvPrefix:String) -> Result<Self> {
717		let path = ConfigPath.map(PathBuf::from);
718		let BackupDir = if EnableBackup {
719			path.as_ref()
720				.and_then(|p| p.parent())
721				.map(|parent| parent.join(".ConfigBackups"))
722		} else {
723			None
724		};
725
726		Ok(Self { ConfigPath:path, BackupDir, EnableBackup, EnvPrefix })
727	}
728
729	/// Load configuration from file, environment, or create default
730	///
731	/// This method implements the configuration priority chain:
732	/// 1. Defaults from code
733	/// 2. Configuration file
734	/// 3. Environment variables (with prefix)
735	///
736	/// # Returns
737	///
738	/// Validated and loaded configuration
739	pub async fn LoadConfiguration(&self) -> Result<AirConfiguration> {
740		// Start with default configuration
741		let mut config = AirConfiguration::default();
742
743		// Try to load from specified or default path
744		let ConfigPath = self.GetConfigPath()?;
745
746		if ConfigPath.exists() {
747			log::info!("Loading configuration from: {}", ConfigPath.display());
748			config = self.LoadFromFile(&ConfigPath).await?;
749		} else {
750			log::info!("No configuration file found, using defaults");
751		}
752
753		// Apply environment variable overrides
754		self.ApplyEnvironmentOverrides(&mut config)?;
755
756		// Schema validation
757		self.SchemaValidate(&config)?;
758
759		// Validate all configuration values
760		self.ValidateConfiguration(&config)?;
761
762		log::info!("Configuration loaded successfully (profile: {})", config.Profile);
763		Ok(config)
764	}
765
766	/// Load configuration from a specific file
767	///
768	/// # Arguments
769	///
770	/// * `path` - Path to the configuration file
771	///
772	/// # Returns
773	///
774	/// Parsed and validated configuration
775	async fn LoadFromFile(&self, path:&Path) -> Result<AirConfiguration> {
776		let content = tokio::fs::read_to_string(path)
777			.await
778			.map_err(|e| AirError::Configuration(format!("Failed to read config file '{}': {}", path.display(), e)))?;
779
780		let config:AirConfiguration = toml::from_str(&content).map_err(|e| {
781			AirError::Configuration(format!("Failed to parse TOML config file '{}': {}", path.display(), e))
782		})?;
783
784		// Type validation is done by serde automatically
785		log::debug!("Configuration file parsed successfully");
786		Ok(config)
787	}
788
789	/// Save configuration to file with backup and atomic write
790	///
791	/// # Arguments
792	///
793	/// * `config` - Configuration to save
794	///
795	/// # Implementation Details
796	///
797	/// - Validates configuration before saving
798	/// - Creates backup if enabled
799	/// - Uses atomic write (write to temp file, then rename)
800	/// - Creates parent directories if needed
801	pub async fn SaveConfiguration(&self, config:&AirConfiguration) -> Result<()> {
802		// Validate before saving
803		self.ValidateConfiguration(config)?;
804
805		let ConfigPath = self.GetConfigPath()?;
806
807		// Create backup if enabled and file exists
808		if self.EnableBackup && ConfigPath.exists() {
809			self.BackupConfiguration(&ConfigPath).await?;
810		}
811
812		// Create parent directory if it doesn't exist
813		if let Some(parent) = ConfigPath.parent() {
814			tokio::fs::create_dir_all(parent).await.map_err(|e| {
815				AirError::Configuration(format!("Failed to create config directory '{}': {}", parent.display(), e))
816			})?;
817		}
818
819		// Atomic write: write to temp file, then rename
820		let TempPath = ConfigPath.with_extension("tmp");
821		let content = toml::to_string_pretty(config)
822			.map_err(|e| AirError::Configuration(format!("Failed to serialize config: {}", e)))?;
823
824		tokio::fs::write(&TempPath, content).await.map_err(|e| {
825			AirError::Configuration(format!("Failed to write temp config file '{}': {}", TempPath.display(), e))
826		})?;
827
828		// Atomic rename
829		tokio::fs::rename(&TempPath, &ConfigPath).await.map_err(|e| {
830			AirError::Configuration(format!("Failed to rename temp config to '{}': {}", ConfigPath.display(), e))
831		})?;
832
833		log::info!("Configuration saved to: {}", ConfigPath.display());
834		Ok(())
835	}
836
837	/// Validate configuration with comprehensive checks
838	///
839	/// Performs:
840	/// - Schema validation
841	/// - Type checking with detailed errors
842	/// - Range validation for numeric values
843	/// - Path validation for security
844	/// - URL validation for network resources
845	fn ValidateConfiguration(&self, config:&AirConfiguration) -> Result<()> {
846		// Schema version validation
847		self.ValidateSchemaVersion(&config.SchemaVersion)?;
848
849		// Profile validation
850		self.ValidateProfile(&config.Profile)?;
851
852		// gRPC configuration validation
853		self.ValidateGrpcConfig(&config.Grpc)?;
854
855		// Authentication configuration validation
856		self.ValidateAuthConfig(&config.Authentication)?;
857
858		// Update configuration validation
859		self.ValidateUpdateConfig(&config.Updates)?;
860
861		// Download configuration validation
862		self.ValidateDownloadConfig(&config.Downloader)?;
863
864		// Indexing configuration validation
865		self.ValidateIndexingConfig(&config.Indexing)?;
866
867		// Logging configuration validation
868		self.ValidateLoggingConfig(&config.Logging)?;
869
870		// Performance configuration validation
871		self.ValidatePerformanceConfig(&config.Performance)?;
872
873		log::debug!("All configuration validation checks passed");
874		Ok(())
875	}
876
877	/// Validate schema version format
878	fn ValidateSchemaVersion(&self, version:&str) -> Result<()> {
879		if !version.chars().all(|c| c.is_digit(10) || c == '.') {
880			return Err(AirError::Configuration(format!(
881				"Invalid schema version '{}': must be in format X.Y.Z",
882				version
883			)));
884		}
885
886		let parts:Vec<&str> = version.split('.').collect();
887		if parts.len() != 3 {
888			return Err(AirError::Configuration(format!(
889				"Invalid schema version '{}': must have 3 parts (X.Y.Z)",
890				version
891			)));
892		}
893
894		for (i, part) in parts.iter().enumerate() {
895			if part.is_empty() {
896				return Err(AirError::Configuration(format!(
897					"Invalid schema version '{}': part {} is empty",
898					version,
899					i + 1
900				)));
901			}
902		}
903
904		Ok(())
905	}
906
907	/// Validate profile name
908	fn ValidateProfile(&self, profile:&str) -> Result<()> {
909		let ValidProfiles = ["dev", "staging", "prod", "custom"];
910
911		if !ValidProfiles.contains(&profile) {
912			return Err(AirError::Configuration(format!(
913				"Invalid profile '{}': must be one of: {}",
914				profile,
915				ValidProfiles.join(", ")
916			)));
917		}
918
919		Ok(())
920	}
921
922	/// Validate gRPC configuration with range checking
923	fn ValidateGrpcConfig(&self, grpc:&GrpcConfig) -> Result<()> {
924		// Validate bind address
925		if grpc.BindAddress.is_empty() {
926			return Err(AirError::Configuration("gRPC bind address cannot be empty".to_string()));
927		}
928
929		// Validate address format
930		if !Self::IsValidAddress(&grpc.BindAddress) {
931			return Err(AirError::Configuration(format!(
932				"Invalid gRPC bind address '{}': must be in format host:port or [IPv6]:port",
933				grpc.BindAddress
934			)));
935		}
936
937		// Validate MaxConnections range [10, 10000]
938		if grpc.MaxConnections < 10 {
939			return Err(AirError::Configuration(format!(
940				"gRPC MaxConnections {} is below minimum (10)",
941				grpc.MaxConnections
942			)));
943		}
944
945		if grpc.MaxConnections > 10000 {
946			return Err(AirError::Configuration(format!(
947				"gRPC MaxConnections {} exceeds maximum (10000)",
948				grpc.MaxConnections
949			)));
950		}
951
952		// Validate RequestTimeoutSecs range [1, 3600]
953		if grpc.RequestTimeoutSecs < 1 {
954			return Err(AirError::Configuration(format!(
955				"gRPC RequestTimeoutSecs {} is below minimum (1 second)",
956				grpc.RequestTimeoutSecs
957			)));
958		}
959
960		if grpc.RequestTimeoutSecs > 3600 {
961			return Err(AirError::Configuration(format!(
962				"gRPC RequestTimeoutSecs {} exceeds maximum (3600 seconds = 1 hour)",
963				grpc.RequestTimeoutSecs
964			)));
965		}
966
967		Ok(())
968	}
969
970	/// Validate authentication configuration
971	fn ValidateAuthConfig(&self, auth:&AuthConfig) -> Result<()> {
972		// If authentication is enabled, validate credentials path
973		if auth.Enabled {
974			if auth.CredentialsPath.is_empty() {
975				return Err(AirError::Configuration(
976					"Authentication credentials path cannot be empty when authentication is enabled".to_string(),
977				));
978			}
979
980			// Validate path for security (prevent directory traversal)
981			self.ValidatePath(&auth.CredentialsPath)?;
982		}
983
984		// Validate TokenExpirationHours range [1, 8760]
985		if auth.TokenExpirationHours < 1 {
986			return Err(AirError::Configuration(format!(
987				"Token expiration hours {} is below minimum (1 hour)",
988				auth.TokenExpirationHours
989			)));
990		}
991
992		if auth.TokenExpirationHours > 8760 {
993			return Err(AirError::Configuration(format!(
994				"Token expiration hours {} exceeds maximum (8760 hours = 1 year)",
995				auth.TokenExpirationHours
996			)));
997		}
998
999		// Validate MaxSessions range [1, 1000]
1000		if auth.MaxSessions < 1 {
1001			return Err(AirError::Configuration(format!(
1002				"Max sessions {} is below minimum (1)",
1003				auth.MaxSessions
1004			)));
1005		}
1006
1007		if auth.MaxSessions > 1000 {
1008			return Err(AirError::Configuration(format!(
1009				"Max sessions {} exceeds maximum (1000)",
1010				auth.MaxSessions
1011			)));
1012		}
1013
1014		Ok(())
1015	}
1016
1017	/// Validate update configuration
1018	fn ValidateUpdateConfig(&self, updates:&UpdateConfig) -> Result<()> {
1019		if updates.Enabled {
1020			// Validate update server URL
1021			if updates.UpdateServerUrl.is_empty() {
1022				return Err(AirError::Configuration(
1023					"Update server URL cannot be empty when updates are enabled".to_string(),
1024				));
1025			}
1026
1027			// Must be HTTPS for security
1028			if !updates.UpdateServerUrl.starts_with("https://") {
1029				return Err(AirError::Configuration(format!(
1030					"Update server URL must use HTTPS, got: {}",
1031					updates.UpdateServerUrl
1032				)));
1033			}
1034
1035			// Validate URL format
1036			if !Self::IsValidUrl(&updates.UpdateServerUrl) {
1037				return Err(AirError::Configuration(format!(
1038					"Invalid update server URL '{}'",
1039					updates.UpdateServerUrl
1040				)));
1041			}
1042		}
1043
1044		// Validate CheckIntervalHours range [1, 168]
1045		if updates.CheckIntervalHours < 1 {
1046			return Err(AirError::Configuration(format!(
1047				"Update check interval {} hours is below minimum (1 hour)",
1048				updates.CheckIntervalHours
1049			)));
1050		}
1051
1052		if updates.CheckIntervalHours > 168 {
1053			return Err(AirError::Configuration(format!(
1054				"Update check interval {} hours exceeds maximum (168 hours = 1 week)",
1055				updates.CheckIntervalHours
1056			)));
1057		}
1058
1059		Ok(())
1060	}
1061
1062	/// Validate download configuration
1063	fn ValidateDownloadConfig(&self, downloader:&DownloadConfig) -> Result<()> {
1064		if downloader.Enabled {
1065			if downloader.CacheDirectory.is_empty() {
1066				return Err(AirError::Configuration(
1067					"Download cache directory cannot be empty when downloader is enabled".to_string(),
1068				));
1069			}
1070
1071			// Validate path for security
1072			self.ValidatePath(&downloader.CacheDirectory)?;
1073		}
1074
1075		// Validate MaxConcurrentDownloads range [1, 50]
1076		if downloader.MaxConcurrentDownloads < 1 {
1077			return Err(AirError::Configuration(format!(
1078				"Max concurrent downloads {} is below minimum (1)",
1079				downloader.MaxConcurrentDownloads
1080			)));
1081		}
1082
1083		if downloader.MaxConcurrentDownloads > 50 {
1084			return Err(AirError::Configuration(format!(
1085				"Max concurrent downloads {} exceeds maximum (50)",
1086				downloader.MaxConcurrentDownloads
1087			)));
1088		}
1089
1090		// Validate DownloadTimeoutSecs range [10, 3600]
1091		if downloader.DownloadTimeoutSecs < 10 {
1092			return Err(AirError::Configuration(format!(
1093				"Download timeout {} seconds is below minimum (10 seconds)",
1094				downloader.DownloadTimeoutSecs
1095			)));
1096		}
1097
1098		if downloader.DownloadTimeoutSecs > 3600 {
1099			return Err(AirError::Configuration(format!(
1100				"Download timeout {} seconds exceeds maximum (3600 seconds = 1 hour)",
1101				downloader.DownloadTimeoutSecs
1102			)));
1103		}
1104
1105		// Validate MaxRetries range [0, 10]
1106		if downloader.MaxRetries > 10 {
1107			return Err(AirError::Configuration(format!(
1108				"Max retries {} exceeds maximum (10)",
1109				downloader.MaxRetries
1110			)));
1111		}
1112
1113		Ok(())
1114	}
1115
1116	/// Validate indexing configuration
1117	fn ValidateIndexingConfig(&self, indexing:&IndexingConfig) -> Result<()> {
1118		if indexing.Enabled {
1119			if indexing.IndexDirectory.is_empty() {
1120				return Err(AirError::Configuration(
1121					"Index directory cannot be empty when indexing is enabled".to_string(),
1122				));
1123			}
1124
1125			// Validate path for security
1126			self.ValidatePath(&indexing.IndexDirectory)?;
1127
1128			// Validate FileTypes is not empty
1129			if indexing.FileTypes.is_empty() {
1130				return Err(AirError::Configuration(
1131					"File types to index cannot be empty when indexing is enabled".to_string(),
1132				));
1133			}
1134
1135			// Validate each file type pattern
1136			for FileType in &indexing.FileTypes {
1137				if FileType.is_empty() {
1138					return Err(AirError::Configuration("File type pattern cannot be empty".to_string()));
1139				}
1140
1141				if !FileType.contains('*') {
1142					log::warn!(
1143						"File type pattern '{}' does not contain wildcards, may not match as expected",
1144						FileType
1145					);
1146				}
1147			}
1148		}
1149
1150		// Validate MaxFileSizeMb range [1, 1024]
1151		if indexing.MaxFileSizeMb < 1 {
1152			return Err(AirError::Configuration(format!(
1153				"Max file size {} MB is below minimum (1 MB)",
1154				indexing.MaxFileSizeMb
1155			)));
1156		}
1157
1158		if indexing.MaxFileSizeMb > 1024 {
1159			return Err(AirError::Configuration(format!(
1160				"Max file size {} MB exceeds maximum (1024 MB = 1 GB)",
1161				indexing.MaxFileSizeMb
1162			)));
1163		}
1164
1165		// Validate UpdateIntervalMinutes range [1, 1440]
1166		if indexing.UpdateIntervalMinutes < 1 {
1167			return Err(AirError::Configuration(format!(
1168				"Index update interval {} minutes is below minimum (1 minute)",
1169				indexing.UpdateIntervalMinutes
1170			)));
1171		}
1172
1173		if indexing.UpdateIntervalMinutes > 1440 {
1174			return Err(AirError::Configuration(format!(
1175				"Index update interval {} minutes exceeds maximum (1440 minutes = 1 day)",
1176				indexing.UpdateIntervalMinutes
1177			)));
1178		}
1179
1180		Ok(())
1181	}
1182
1183	/// Validate logging configuration
1184	fn ValidateLoggingConfig(&self, logging:&LoggingConfig) -> Result<()> {
1185		// Validate log level
1186		let ValidLevels = ["trace", "debug", "info", "warn", "error"];
1187		if !ValidLevels.contains(&logging.Level.as_str()) {
1188			return Err(AirError::Configuration(format!(
1189				"Invalid log level '{}': must be one of: {}",
1190				logging.Level,
1191				ValidLevels.join(", ")
1192			)));
1193		}
1194
1195		// Validate file path if provided
1196		if let Some(ref FilePath) = logging.FilePath {
1197			if !FilePath.is_empty() {
1198				self.ValidatePath(FilePath)?;
1199			}
1200		}
1201
1202		// Validate MaxFileSizeMb range [1, 1000]
1203		if logging.MaxFileSizeMb < 1 {
1204			return Err(AirError::Configuration(format!(
1205				"Max log file size {} MB is below minimum (1 MB)",
1206				logging.MaxFileSizeMb
1207			)));
1208		}
1209
1210		if logging.MaxFileSizeMb > 1000 {
1211			return Err(AirError::Configuration(format!(
1212				"Max log file size {} MB exceeds maximum (1000 MB = 1 GB)",
1213				logging.MaxFileSizeMb
1214			)));
1215		}
1216
1217		// Validate MaxFiles range [1, 50]
1218		if logging.MaxFiles < 1 {
1219			return Err(AirError::Configuration(format!(
1220				"Max log files {} is below minimum (1)",
1221				logging.MaxFiles
1222			)));
1223		}
1224
1225		if logging.MaxFiles > 50 {
1226			return Err(AirError::Configuration(format!(
1227				"Max log files {} exceeds maximum (50)",
1228				logging.MaxFiles
1229			)));
1230		}
1231
1232		Ok(())
1233	}
1234
1235	/// Validate performance configuration
1236	fn ValidatePerformanceConfig(&self, performance:&PerformanceConfig) -> Result<()> {
1237		// Validate MemoryLimitMb range [64, 16384]
1238		if performance.MemoryLimitMb < 64 {
1239			return Err(AirError::Configuration(format!(
1240				"Memory limit {} MB is below minimum (64 MB)",
1241				performance.MemoryLimitMb
1242			)));
1243		}
1244
1245		if performance.MemoryLimitMb > 16384 {
1246			return Err(AirError::Configuration(format!(
1247				"Memory limit {} MB exceeds maximum (16384 MB = 16 GB)",
1248				performance.MemoryLimitMb
1249			)));
1250		}
1251
1252		// Validate CPULimitPercent range [10, 100]
1253		if performance.CPULimitPercent < 10 {
1254			return Err(AirError::Configuration(format!(
1255				"CPU limit {}% is below minimum (10%)",
1256				performance.CPULimitPercent
1257			)));
1258		}
1259
1260		if performance.CPULimitPercent > 100 {
1261			return Err(AirError::Configuration(format!(
1262				"CPU limit {}% exceeds maximum (100%)",
1263				performance.CPULimitPercent
1264			)));
1265		}
1266
1267		// Validate DiskLimitMb range [100, 102400]
1268		if performance.DiskLimitMb < 100 {
1269			return Err(AirError::Configuration(format!(
1270				"Disk limit {} MB is below minimum (100 MB)",
1271				performance.DiskLimitMb
1272			)));
1273		}
1274
1275		if performance.DiskLimitMb > 102400 {
1276			return Err(AirError::Configuration(format!(
1277				"Disk limit {} MB exceeds maximum (102400 MB = 100 GB)",
1278				performance.DiskLimitMb
1279			)));
1280		}
1281
1282		// Validate BackgroundTaskIntervalSecs range [1, 3600]
1283		if performance.BackgroundTaskIntervalSecs < 1 {
1284			return Err(AirError::Configuration(format!(
1285				"Background task interval {} seconds is below minimum (1 second)",
1286				performance.BackgroundTaskIntervalSecs
1287			)));
1288		}
1289
1290		if performance.BackgroundTaskIntervalSecs > 3600 {
1291			return Err(AirError::Configuration(format!(
1292				"Background task interval {} seconds exceeds maximum (3600 seconds = 1 hour)",
1293				performance.BackgroundTaskIntervalSecs
1294			)));
1295		}
1296
1297		Ok(())
1298	}
1299
1300	/// Validate path for security (prevent directory traversal)
1301	fn ValidatePath(&self, path:&str) -> Result<()> {
1302		if path.is_empty() {
1303			return Err(AirError::Configuration("Path cannot be empty".to_string()));
1304		}
1305
1306		// Check for path traversal attempts
1307		if path.contains("..") {
1308			return Err(AirError::Configuration(format!(
1309				"Path '{}' contains '..' which is not allowed for security reasons",
1310				path
1311			)));
1312		}
1313
1314		// Check for absolute path patterns that might be problematic
1315		if path.starts_with("\\\\") || path.starts_with("//") {
1316			return Err(AirError::Configuration(format!(
1317				"Path '{}' uses UNC/network path format which may not be supported",
1318				path
1319			)));
1320		}
1321
1322		// Validate that the path doesn't contain null bytes
1323		if path.contains('\0') {
1324			return Err(AirError::Configuration(
1325				"Path contains null bytes which is not allowed".to_string(),
1326			));
1327		}
1328
1329		Ok(())
1330	}
1331
1332	/// Validate address format (IP:port or hostname:port)
1333	fn IsValidAddress(addr:&str) -> bool {
1334		// Check for IPv6 format: [IPv6]:port
1335		if addr.starts_with('[') && addr.contains("]:") {
1336			return true;
1337		}
1338
1339		// Check for IPv4 or hostname format: host:port
1340		if addr.contains(':') {
1341			let parts:Vec<&str> = addr.split(':').collect();
1342			if parts.len() != 2 {
1343				return false;
1344			}
1345
1346			// Validate port
1347			if let Ok(port) = parts[1].parse::<u16>() {
1348				return port > 0;
1349			}
1350
1351			return false;
1352		}
1353
1354		false
1355	}
1356
1357	/// Validate URL format
1358	fn IsValidUrl(url:&str) -> bool { url::Url::parse(url).is_ok() }
1359
1360	/// Perform schema-based validation
1361	fn SchemaValidate(&self, config:&AirConfiguration) -> Result<()> {
1362		let _schema = generate_schema();
1363
1364		// Convert config to JSON for validation
1365		let ConfigJson = serde_json::to_value(config)
1366			.map_err(|e| AirError::Configuration(format!("Failed to serialize config for schema validation: {}", e)))?;
1367
1368		// Basic schema validation (would use jsonschema crate in production)
1369		// For now, we do manual validation
1370		if !ConfigJson.is_object() {
1371			return Err(AirError::Configuration("Configuration must be an object".to_string()));
1372		}
1373
1374		log::debug!("Schema validation passed");
1375		Ok(())
1376	}
1377
1378	/// Apply environment variable overrides to configuration
1379	///
1380	/// Environment variables are read with the configured prefix.
1381	/// For example, with prefix "AIR_", the variable "AIR_GRPC_BIND_ADDRESS"
1382	/// would override grpc.bind_address.
1383	///
1384	/// Variable naming convention: {PREFIX}_{SECTION}_{FIELD} (uppercase,
1385	/// underscores)
1386	fn ApplyEnvironmentOverrides(&self, config:&mut AirConfiguration) -> Result<()> {
1387		let mut override_count = 0;
1388
1389		// gRPC overrides
1390		if let Ok(val) = env::var(&format!("{}GRPC_BIND_ADDRESS", self.EnvPrefix)) {
1391			config.Grpc.BindAddress = val;
1392			override_count += 1;
1393		}
1394
1395		if let Ok(val) = env::var(&format!("{}GRPC_MAX_CONNECTIONS", self.EnvPrefix)) {
1396			config.Grpc.MaxConnections = val
1397				.parse()
1398				.map_err(|e| AirError::Configuration(format!("Invalid GRPC_MAX_CONNECTIONS value: {}", e)))?;
1399			override_count += 1;
1400		}
1401
1402		// Authentication overrides
1403		if let Ok(val) = env::var(&format!("{}AUTH_ENABLED", self.EnvPrefix)) {
1404			config.Authentication.Enabled = val
1405				.parse()
1406				.map_err(|e| AirError::Configuration(format!("Invalid AUTH_ENABLED value: {}", e)))?;
1407			override_count += 1;
1408		}
1409
1410		if let Ok(val) = env::var(&format!("{}AUTH_CREDENTIALS_PATH", self.EnvPrefix)) {
1411			config.Authentication.CredentialsPath = val;
1412			override_count += 1;
1413		}
1414
1415		// Update overrides
1416		if let Ok(val) = env::var(&format!("{}UPDATE_ENABLED", self.EnvPrefix)) {
1417			config.Updates.Enabled = val
1418				.parse()
1419				.map_err(|e| AirError::Configuration(format!("Invalid UPDATE_ENABLED value: {}", e)))?;
1420			override_count += 1;
1421		}
1422
1423		if let Ok(val) = env::var(&format!("{}UPDATE_AUTO_DOWNLOAD", self.EnvPrefix)) {
1424			config.Updates.AutoDownload = val
1425				.parse()
1426				.map_err(|e| AirError::Configuration(format!("Invalid UPDATE_AUTO_DOWNLOAD value: {}", e)))?;
1427			override_count += 1;
1428		}
1429
1430		// Logging overrides
1431		if let Ok(val) = env::var(&format!("{}LOGGING_LEVEL", self.EnvPrefix)) {
1432			config.Logging.Level = val.to_lowercase();
1433			override_count += 1;
1434		}
1435
1436		if override_count > 0 {
1437			log::info!("Applied {} environment variable override(s)", override_count);
1438		}
1439
1440		Ok(())
1441	}
1442
1443	/// Backup current configuration file
1444	///
1445	/// Creates a timestamped backup of the current configuration file
1446	/// in the configured backup directory.
1447	async fn BackupConfiguration(&self, config_path:&Path) -> Result<()> {
1448		let backup_dir = self
1449			.BackupDir
1450			.as_ref()
1451			.ok_or_else(|| AirError::Configuration("Backup directory not configured".to_string()))?;
1452
1453		// Create backup directory if it doesn't exist
1454		tokio::fs::create_dir_all(backup_dir).await.map_err(|e| {
1455			AirError::Configuration(format!("Failed to create backup directory '{}': {}", backup_dir.display(), e))
1456		})?;
1457
1458		// Generate backup filename with timestamp
1459		let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
1460		let backup_filename = format!(
1461			"{}_config_{}.toml.bak",
1462			config_path.file_stem().and_then(|s| s.to_str()).unwrap_or("config"),
1463			timestamp
1464		);
1465		let backup_path = backup_dir.join(&backup_filename);
1466
1467		// Copy current config to backup
1468		tokio::fs::copy(config_path, &backup_path).await.map_err(|e| {
1469			AirError::Configuration(format!("Failed to create backup '{}': {}", backup_path.display(), e))
1470		})?;
1471
1472		log::info!("Configuration backed up to: {}", backup_path.display());
1473		Ok(())
1474	}
1475
1476	/// Rollback configuration from the most recent backup
1477	///
1478	/// # Returns
1479	///
1480	/// Returns the path to the backup file that was restored
1481	pub async fn RollbackConfiguration(&self) -> Result<PathBuf> {
1482		let config_path = self.GetConfigPath()?;
1483
1484		let backup_dir = self
1485			.BackupDir
1486			.as_ref()
1487			.ok_or_else(|| AirError::Configuration("Backup directory not configured".to_string()))?;
1488
1489		// Find the most recent backup
1490		let mut backups = tokio::fs::read_dir(backup_dir).await.map_err(|e| {
1491			AirError::Configuration(format!("Failed to read backup directory '{}': {}", backup_dir.display(), e))
1492		})?;
1493
1494		let mut most_recent:Option<(tokio::fs::DirEntry, std::time::SystemTime)> = None;
1495
1496		while let Some(entry) = backups
1497			.next_entry()
1498			.await
1499			.map_err(|e| AirError::Configuration(format!("Failed to read backup entry: {}", e)))?
1500		{
1501			let metadata = entry
1502				.metadata()
1503				.await
1504				.map_err(|e| AirError::Configuration(format!("Failed to get metadata: {}", e)))?;
1505
1506			if let Ok(modified) = metadata.modified() {
1507				if most_recent.is_none() || modified > most_recent.as_ref().unwrap().1 {
1508					most_recent = Some((entry, modified));
1509				}
1510			}
1511		}
1512
1513		let (backup_entry, _) =
1514			most_recent.ok_or_else(|| AirError::Configuration("No backup files found".to_string()))?;
1515
1516		let backup_path = backup_entry.path();
1517
1518		// Restore from backup
1519		tokio::fs::copy(&backup_path, &config_path).await.map_err(|e| {
1520			AirError::Configuration(format!("Failed to restore from backup '{}': {}", backup_path.display(), e))
1521		})?;
1522
1523		log::info!("Configuration rolled back from: {}", backup_path.display());
1524		Ok(backup_path)
1525	}
1526
1527	/// Get the configuration file path
1528	///
1529	/// Returns the configured path or the default path
1530	fn GetConfigPath(&self) -> Result<PathBuf> {
1531		if let Some(ref path) = self.ConfigPath {
1532			Ok(path.clone())
1533		} else {
1534			Self::GetDefaultConfigPath()
1535		}
1536	}
1537
1538	/// Get default configuration file path
1539	///
1540	/// Returns the default configuration file path in the user's config
1541	/// directory
1542	fn GetDefaultConfigPath() -> Result<PathBuf> {
1543		let config_dir = dirs::config_dir()
1544			.ok_or_else(|| AirError::Configuration("Cannot determine config directory".to_string()))?;
1545
1546		Ok(config_dir.join("Air").join(DefaultConfigFile))
1547	}
1548
1549	/// Get profile-specific default configuration
1550	///
1551	/// # Arguments
1552	///
1553	/// * `profile` - Profile name (dev, staging, prod, custom)
1554	///
1555	/// # Returns
1556	///
1557	/// Configuration with profile-appropriate defaults
1558	pub fn GetProfileDefaults(profile:&str) -> AirConfiguration {
1559		let mut config = AirConfiguration::default();
1560		config.Profile = profile.to_string();
1561
1562		match profile {
1563			"prod" => {
1564				config.Logging.Level = "warn".to_string();
1565				config.Logging.ConsoleEnabled = false;
1566				config.Performance.MemoryLimitMb = 1024;
1567				config.Performance.CPULimitPercent = 80;
1568			},
1569			"staging" => {
1570				config.Logging.Level = "info".to_string();
1571				config.Performance.MemoryLimitMb = 768;
1572				config.Performance.CPULimitPercent = 70;
1573			},
1574			"dev" | _ => {
1575				// Dev defaults are already set
1576				config.Logging.Level = "debug".to_string();
1577				config.Logging.ConsoleEnabled = true;
1578				config.Performance.MemoryLimitMb = 512;
1579				config.Performance.CPULimitPercent = 50;
1580			},
1581		}
1582
1583		config
1584	}
1585
1586	/// Expand path with home directory (~) expansion
1587	///
1588	/// # Arguments
1589	///
1590	/// * `path` - Path string to expand
1591	///
1592	/// # Returns
1593	///
1594	/// Expanded PathBuf
1595	pub fn ExpandPath(path:&str) -> Result<PathBuf> {
1596		if path.is_empty() {
1597			return Err(AirError::Configuration("Cannot expand empty path".to_string()));
1598		}
1599
1600		if path.starts_with('~') {
1601			let home = dirs::home_dir()
1602				.ok_or_else(|| AirError::Configuration("Cannot determine home directory".to_string()))?;
1603
1604			let rest = &path[1..]; // Remove ~
1605			if rest.starts_with('/') || rest.starts_with('\\') {
1606				Ok(home.join(&rest[1..]))
1607			} else {
1608				Ok(home.join(rest))
1609			}
1610		} else {
1611			Ok(PathBuf::from(path))
1612		}
1613	}
1614
1615	/// Generate configuration hash for change detection
1616	///
1617	/// # Arguments
1618	///
1619	/// * `config` - Configuration to hash
1620	///
1621	/// # Returns
1622	///
1623	/// SHA256 hash of the configuration
1624	pub fn ComputeHash(config:&AirConfiguration) -> Result<String> {
1625		let config_str = toml::to_string_pretty(config)
1626			.map_err(|e| AirError::Configuration(format!("Failed to serialize config: {}", e)))?;
1627
1628		let mut hasher = sha2::Sha256::new();
1629		hasher.update(config_str.as_bytes());
1630		let hash = hasher.finalize();
1631
1632		Ok(hex::encode(hash))
1633	}
1634
1635	/// Export configuration to JSON (for VSCode compatibility)
1636	///
1637	/// # Arguments
1638	///
1639	/// * `config` - Configuration to export
1640	///
1641	/// # Returns
1642	///
1643	/// JSON string representation of configuration
1644	pub fn ExportToJson(config:&AirConfiguration) -> Result<String> {
1645		serde_json::to_string_pretty(config)
1646			.map_err(|e| AirError::Configuration(format!("Failed to export to JSON: {}", e)))
1647	}
1648
1649	/// Import configuration from JSON (for VSCode compatibility)
1650	///
1651	/// # Arguments
1652	///
1653	/// * `json_str` - JSON string to import
1654	///
1655	/// # Returns
1656	///
1657	/// Parsed and validated configuration
1658	pub fn ImportFromJson(json_str:&str) -> Result<AirConfiguration> {
1659		let config:AirConfiguration = serde_json::from_str(json_str)
1660			.map_err(|e| AirError::Configuration(format!("Failed to import from JSON: {}", e)))?;
1661
1662		Ok(config)
1663	}
1664
1665	/// Get environment variable mappings
1666	///
1667	/// Returns a mapping of configuration paths to environment variable names
1668	pub fn GetEnvironmentMappings(&self) -> HashMap<String, String> {
1669		let prefix = &self.EnvPrefix;
1670		let mut mappings = HashMap::new();
1671
1672		mappings.insert("grpc.bind_address".to_string(), format!("{}GRPC_BIND_ADDRESS", prefix));
1673		mappings.insert("grpc.max_connections".to_string(), format!("{}GRPC_MAX_CONNECTIONS", prefix));
1674		mappings.insert(
1675			"grpc.request_timeout_secs".to_string(),
1676			format!("{}GRPC_REQUEST_TIMEOUT_SECS", prefix),
1677		);
1678
1679		mappings.insert("authentication.enabled".to_string(), format!("{}AUTH_ENABLED", prefix));
1680		mappings.insert(
1681			"authentication.credentials_path".to_string(),
1682			format!("{}AUTH_CREDENTIALS_PATH", prefix),
1683		);
1684		mappings.insert(
1685			"authentication.token_expiration_hours".to_string(),
1686			format!("{}AUTH_TOKEN_EXPIRATION_HOURS", prefix),
1687		);
1688
1689		mappings.insert("updates.enabled".to_string(), format!("{}UPDATE_ENABLED", prefix));
1690		mappings.insert("updates.auto_download".to_string(), format!("{}UPDATE_AUTO_DOWNLOAD", prefix));
1691		mappings.insert("updates.auto_install".to_string(), format!("{}UPDATE_AUTO_INSTALL", prefix));
1692
1693		mappings.insert("logging.level".to_string(), format!("{}LOGGING_LEVEL", prefix));
1694		mappings.insert(
1695			"logging.console_enabled".to_string(),
1696			format!("{}LOGGING_CONSOLE_ENABLED", prefix),
1697		);
1698
1699		mappings
1700	}
1701}
1702
1703#[cfg(test)]
1704mod tests {
1705	use super::*;
1706
1707	#[test]
1708	fn test_default_configuration() {
1709		let config = AirConfiguration::default();
1710		assert_eq!(config.SchemaVersion, "1.0.0");
1711		assert_eq!(config.Profile, "dev");
1712		assert!(config.Authentication.Enabled);
1713		assert!(config.Logging.ConsoleEnabled);
1714	}
1715
1716	#[test]
1717	fn test_profile_defaults() {
1718		let DevConfig = ConfigurationManager::GetProfileDefaults("dev");
1719		assert_eq!(DevConfig.Profile, "dev");
1720		assert_eq!(DevConfig.Logging.Level, "debug");
1721
1722		let ProdConfig = ConfigurationManager::GetProfileDefaults("prod");
1723		assert_eq!(ProdConfig.Profile, "prod");
1724		assert_eq!(ProdConfig.Logging.Level, "warn");
1725		assert!(!ProdConfig.Logging.ConsoleEnabled);
1726	}
1727
1728	#[test]
1729	fn test_path_expansion() {
1730		let Home = dirs::home_dir().expect("Cannot determine home directory");
1731		let Expanded = ConfigurationManager::ExpandPath("~/test").unwrap();
1732		assert_eq!(Expanded, Home.join("test"));
1733
1734		let Absolute = ConfigurationManager::ExpandPath("/tmp/test").unwrap();
1735		assert_eq!(Absolute, PathBuf::from("/tmp/test"));
1736	}
1737
1738	#[test]
1739	fn test_address_validation() {
1740		assert!(ConfigurationManager::IsValidAddress("[::1]:50053"));
1741		assert!(ConfigurationManager::IsValidAddress("127.0.0.1:50053"));
1742		assert!(ConfigurationManager::IsValidAddress("localhost:50053"));
1743		assert!(!ConfigurationManager::IsValidAddress("invalid"));
1744	}
1745
1746	#[test]
1747	fn test_url_validation() {
1748		assert!(ConfigurationManager::IsValidUrl("https://example.com"));
1749		assert!(ConfigurationManager::IsValidUrl("https://updates.editor.land"));
1750		assert!(!ConfigurationManager::IsValidUrl("not-a-url"));
1751		assert!(!ConfigurationManager::IsValidUrl("http://insecure.com"));
1752	}
1753
1754	#[test]
1755	fn test_path_validation() {
1756		let manager = ConfigurationManager::New(None).unwrap();
1757		assert!(manager.ValidatePath("~/config").is_ok());
1758		assert!(manager.ValidatePath("/tmp/config").is_ok());
1759		assert!(manager.ValidatePath("../escaped").is_err());
1760		assert!(manager.ValidatePath("").is_err());
1761	}
1762
1763	#[tokio::test]
1764	async fn test_export_import_json() {
1765		let config = AirConfiguration::default();
1766		let json_str = ConfigurationManager::ExportToJson(&config).unwrap();
1767
1768		let imported = ConfigurationManager::ImportFromJson(&json_str).unwrap();
1769		assert_eq!(imported.SchemaVersion, config.SchemaVersion);
1770		assert_eq!(imported.Profile, config.Profile);
1771		assert_eq!(imported.Grpc.BindAddress, config.Grpc.BindAddress);
1772	}
1773
1774	#[test]
1775	fn test_compute_hash() {
1776		let config = AirConfiguration::default();
1777		let hash1 = ConfigurationManager::ComputeHash(&config).unwrap();
1778		let hash2 = ConfigurationManager::ComputeHash(&config).unwrap();
1779		assert_eq!(hash1, hash2);
1780
1781		let mut modified = config;
1782		modified.Grpc.BindAddress = "[::1]:50054".to_string();
1783		let hash3 = ConfigurationManager::ComputeHash(&modified).unwrap();
1784		assert_ne!(hash1, hash3);
1785	}
1786
1787	#[test]
1788	fn test_generate_schema() {
1789		let schema = generate_schema();
1790		assert!(schema.is_object());
1791		assert!(schema.get("$schema").is_some());
1792		assert!(schema.get("properties").is_some());
1793	}
1794}