Skip to main content

oxigdal_cloud/
multicloud.rs

1//! Multi-cloud support with unified interface, failover, and intelligent routing
2//!
3//! This module provides comprehensive multi-cloud storage capabilities including:
4//!
5//! - **Unified Interface**: Single API for S3, Azure Blob, GCS, and HTTP backends
6//! - **Automatic Failover**: Seamless failover between cloud providers
7//! - **Cross-Cloud Transfer**: Efficient data transfer between different cloud providers
8//! - **Provider Detection**: Automatic detection of cloud provider from URLs
9//! - **Cost-Optimized Routing**: Route requests to minimize costs
10//! - **Latency-Based Selection**: Select providers based on measured latency
11//! - **Region-Aware Operations**: Optimize operations based on geographic regions
12//!
13//! # Example
14//!
15//! ```rust,no_run
16//! # #[cfg(feature = "async")]
17//! # async fn example() -> oxigdal_cloud::Result<()> {
18//! use oxigdal_cloud::multicloud::{MultiCloudManager, CloudProviderConfig, CloudProvider};
19//!
20//! let manager = MultiCloudManager::builder()
21//!     .add_provider(CloudProviderConfig::s3("primary-bucket").with_priority(1))
22//!     .add_provider(CloudProviderConfig::gcs("backup-bucket").with_priority(2))
23//!     .with_failover(true)
24//!     .with_latency_routing(true)
25//!     .build()?;
26//!
27//! // Automatic routing and failover
28//! let data = manager.get("path/to/object").await?;
29//!
30//! # Ok(())
31//! # }
32//! ```
33
34use std::collections::HashMap;
35use std::sync::Arc;
36use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
37use std::time::{Duration, Instant};
38
39use bytes::Bytes;
40
41#[cfg(feature = "async")]
42use crate::backends::CloudStorageBackend;
43use crate::error::{CloudError, Result};
44
45// ============================================================================
46// Cloud Provider Types
47// ============================================================================
48
49/// Supported cloud providers
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub enum CloudProvider {
52    /// Amazon Web Services S3
53    AwsS3,
54    /// Microsoft Azure Blob Storage
55    AzureBlob,
56    /// Google Cloud Storage
57    Gcs,
58    /// HTTP/HTTPS endpoint
59    Http,
60    /// Custom or unknown provider
61    Custom,
62}
63
64impl CloudProvider {
65    /// Detects cloud provider from URL
66    #[must_use]
67    pub fn from_url(url: &str) -> Option<Self> {
68        let lower = url.to_lowercase();
69
70        // Check scheme first
71        if lower.starts_with("s3://") {
72            return Some(Self::AwsS3);
73        }
74        if lower.starts_with("az://") || lower.starts_with("azure://") {
75            return Some(Self::AzureBlob);
76        }
77        if lower.starts_with("gs://") || lower.starts_with("gcs://") {
78            return Some(Self::Gcs);
79        }
80
81        // Check for cloud-specific domains in HTTP URLs
82        if lower.starts_with("http://") || lower.starts_with("https://") {
83            if lower.contains(".s3.") || lower.contains(".amazonaws.com") {
84                return Some(Self::AwsS3);
85            }
86            if lower.contains(".blob.core.windows.net") || lower.contains(".azure.") {
87                return Some(Self::AzureBlob);
88            }
89            if lower.contains("storage.googleapis.com")
90                || lower.contains("storage.cloud.google.com")
91            {
92                return Some(Self::Gcs);
93            }
94            return Some(Self::Http);
95        }
96
97        None
98    }
99
100    /// Returns the display name of the provider
101    #[must_use]
102    pub fn display_name(&self) -> &'static str {
103        match self {
104            Self::AwsS3 => "AWS S3",
105            Self::AzureBlob => "Azure Blob Storage",
106            Self::Gcs => "Google Cloud Storage",
107            Self::Http => "HTTP/HTTPS",
108            Self::Custom => "Custom Provider",
109        }
110    }
111
112    /// Returns typical egress cost per GB (USD)
113    #[must_use]
114    pub fn egress_cost_per_gb(&self) -> f64 {
115        match self {
116            Self::AwsS3 => 0.09,      // AWS S3 standard egress
117            Self::AzureBlob => 0.087, // Azure Blob standard egress
118            Self::Gcs => 0.12,        // GCS standard egress
119            Self::Http => 0.0,        // No cloud-specific egress
120            Self::Custom => 0.0,
121        }
122    }
123
124    /// Returns typical storage cost per GB/month (USD)
125    #[must_use]
126    pub fn storage_cost_per_gb(&self) -> f64 {
127        match self {
128            Self::AwsS3 => 0.023,     // S3 Standard
129            Self::AzureBlob => 0.018, // Azure Blob Hot
130            Self::Gcs => 0.020,       // GCS Standard
131            Self::Http => 0.0,
132            Self::Custom => 0.0,
133        }
134    }
135}
136
137impl std::fmt::Display for CloudProvider {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        write!(f, "{}", self.display_name())
140    }
141}
142
143// ============================================================================
144// Cloud Regions
145// ============================================================================
146
147/// Geographic regions for cloud providers
148#[derive(Debug, Clone, PartialEq, Eq, Hash)]
149pub enum CloudRegion {
150    /// US East (Virginia)
151    UsEast1,
152    /// US East (Ohio)
153    UsEast2,
154    /// US West (Oregon)
155    UsWest2,
156    /// EU West (Ireland)
157    EuWest1,
158    /// EU Central (Frankfurt)
159    EuCentral1,
160    /// Asia Pacific (Tokyo)
161    ApNortheast1,
162    /// Asia Pacific (Singapore)
163    ApSoutheast1,
164    /// Asia Pacific (Sydney)
165    ApSoutheast2,
166    /// Custom region
167    Custom(String),
168}
169
170impl CloudRegion {
171    /// Creates a region from a string identifier
172    #[must_use]
173    pub fn from_string(s: &str) -> Self {
174        match s.to_lowercase().as_str() {
175            "us-east-1" | "useast1" | "eastus" => Self::UsEast1,
176            "us-east-2" | "useast2" | "eastus2" => Self::UsEast2,
177            "us-west-2" | "uswest2" | "westus2" => Self::UsWest2,
178            "eu-west-1" | "euwest1" | "westeurope" => Self::EuWest1,
179            "eu-central-1" | "eucentral1" | "germanywestcentral" => Self::EuCentral1,
180            "ap-northeast-1" | "apnortheast1" | "japaneast" => Self::ApNortheast1,
181            "ap-southeast-1" | "apsoutheast1" | "southeastasia" => Self::ApSoutheast1,
182            "ap-southeast-2" | "apsoutheast2" | "australiaeast" => Self::ApSoutheast2,
183            _ => Self::Custom(s.to_string()),
184        }
185    }
186
187    /// Returns the AWS region code
188    #[must_use]
189    pub fn aws_code(&self) -> &str {
190        match self {
191            Self::UsEast1 => "us-east-1",
192            Self::UsEast2 => "us-east-2",
193            Self::UsWest2 => "us-west-2",
194            Self::EuWest1 => "eu-west-1",
195            Self::EuCentral1 => "eu-central-1",
196            Self::ApNortheast1 => "ap-northeast-1",
197            Self::ApSoutheast1 => "ap-southeast-1",
198            Self::ApSoutheast2 => "ap-southeast-2",
199            Self::Custom(s) => s.as_str(),
200        }
201    }
202
203    /// Returns the Azure region code
204    #[must_use]
205    pub fn azure_code(&self) -> &str {
206        match self {
207            Self::UsEast1 => "eastus",
208            Self::UsEast2 => "eastus2",
209            Self::UsWest2 => "westus2",
210            Self::EuWest1 => "westeurope",
211            Self::EuCentral1 => "germanywestcentral",
212            Self::ApNortheast1 => "japaneast",
213            Self::ApSoutheast1 => "southeastasia",
214            Self::ApSoutheast2 => "australiaeast",
215            Self::Custom(s) => s.as_str(),
216        }
217    }
218
219    /// Returns the GCS region code
220    #[must_use]
221    pub fn gcs_code(&self) -> &str {
222        match self {
223            Self::UsEast1 => "us-east1",
224            Self::UsEast2 => "us-east4",
225            Self::UsWest2 => "us-west1",
226            Self::EuWest1 => "europe-west1",
227            Self::EuCentral1 => "europe-west3",
228            Self::ApNortheast1 => "asia-northeast1",
229            Self::ApSoutheast1 => "asia-southeast1",
230            Self::ApSoutheast2 => "australia-southeast1",
231            Self::Custom(s) => s.as_str(),
232        }
233    }
234
235    /// Calculates approximate latency to another region in milliseconds
236    #[must_use]
237    pub fn estimated_latency_to(&self, other: &Self) -> u32 {
238        if self == other {
239            return 1; // Same region, minimal latency
240        }
241
242        // Approximate cross-region latencies based on geography
243        match (self, other) {
244            // US to US
245            (Self::UsEast1 | Self::UsEast2, Self::UsWest2) => 65,
246            (Self::UsWest2, Self::UsEast1 | Self::UsEast2) => 65,
247            (Self::UsEast1, Self::UsEast2) | (Self::UsEast2, Self::UsEast1) => 10,
248
249            // US to EU
250            (Self::UsEast1 | Self::UsEast2, Self::EuWest1 | Self::EuCentral1) => 80,
251            (Self::EuWest1 | Self::EuCentral1, Self::UsEast1 | Self::UsEast2) => 80,
252            (Self::UsWest2, Self::EuWest1 | Self::EuCentral1) => 140,
253            (Self::EuWest1 | Self::EuCentral1, Self::UsWest2) => 140,
254
255            // EU to EU
256            (Self::EuWest1, Self::EuCentral1) | (Self::EuCentral1, Self::EuWest1) => 20,
257
258            // US to Asia
259            (Self::UsWest2, Self::ApNortheast1 | Self::ApSoutheast1 | Self::ApSoutheast2) => 100,
260            (Self::ApNortheast1 | Self::ApSoutheast1 | Self::ApSoutheast2, Self::UsWest2) => 100,
261            (Self::UsEast1 | Self::UsEast2, Self::ApNortheast1 | Self::ApSoutheast1) => 180,
262            (Self::ApNortheast1 | Self::ApSoutheast1, Self::UsEast1 | Self::UsEast2) => 180,
263
264            // EU to Asia
265            (Self::EuWest1 | Self::EuCentral1, Self::ApNortheast1) => 220,
266            (Self::ApNortheast1, Self::EuWest1 | Self::EuCentral1) => 220,
267            (Self::EuWest1 | Self::EuCentral1, Self::ApSoutheast1 | Self::ApSoutheast2) => 180,
268            (Self::ApSoutheast1 | Self::ApSoutheast2, Self::EuWest1 | Self::EuCentral1) => 180,
269
270            // Asia to Asia
271            (Self::ApNortheast1, Self::ApSoutheast1 | Self::ApSoutheast2) => 80,
272            (Self::ApSoutheast1 | Self::ApSoutheast2, Self::ApNortheast1) => 80,
273            (Self::ApSoutheast1, Self::ApSoutheast2) | (Self::ApSoutheast2, Self::ApSoutheast1) => {
274                60
275            }
276
277            // Default for custom or unknown
278            _ => 150,
279        }
280    }
281}
282
283// ============================================================================
284// Provider Configuration
285// ============================================================================
286
287/// Configuration for a single cloud provider
288#[derive(Debug, Clone)]
289pub struct CloudProviderConfig {
290    /// Provider type
291    pub provider: CloudProvider,
292    /// Bucket or container name
293    pub bucket: String,
294    /// Optional prefix within bucket
295    pub prefix: String,
296    /// Region
297    pub region: Option<CloudRegion>,
298    /// Endpoint URL (for custom endpoints)
299    pub endpoint: Option<String>,
300    /// Priority (lower is higher priority)
301    pub priority: u32,
302    /// Weight for load balancing (0-100)
303    pub weight: u32,
304    /// Maximum concurrent requests
305    pub max_concurrent: usize,
306    /// Request timeout
307    pub timeout: Duration,
308    /// Is this provider read-only
309    pub read_only: bool,
310    /// Custom cost per GB egress (overrides default)
311    pub custom_egress_cost: Option<f64>,
312    /// Provider-specific options
313    pub options: HashMap<String, String>,
314}
315
316impl CloudProviderConfig {
317    /// Creates an S3 provider configuration
318    #[must_use]
319    pub fn s3(bucket: impl Into<String>) -> Self {
320        Self {
321            provider: CloudProvider::AwsS3,
322            bucket: bucket.into(),
323            prefix: String::new(),
324            region: None,
325            endpoint: None,
326            priority: 1,
327            weight: 100,
328            max_concurrent: 100,
329            timeout: Duration::from_secs(300),
330            read_only: false,
331            custom_egress_cost: None,
332            options: HashMap::new(),
333        }
334    }
335
336    /// Creates an Azure Blob provider configuration
337    #[must_use]
338    pub fn azure(container: impl Into<String>) -> Self {
339        Self {
340            provider: CloudProvider::AzureBlob,
341            bucket: container.into(),
342            prefix: String::new(),
343            region: None,
344            endpoint: None,
345            priority: 1,
346            weight: 100,
347            max_concurrent: 100,
348            timeout: Duration::from_secs(300),
349            read_only: false,
350            custom_egress_cost: None,
351            options: HashMap::new(),
352        }
353    }
354
355    /// Creates a GCS provider configuration
356    #[must_use]
357    pub fn gcs(bucket: impl Into<String>) -> Self {
358        Self {
359            provider: CloudProvider::Gcs,
360            bucket: bucket.into(),
361            prefix: String::new(),
362            region: None,
363            endpoint: None,
364            priority: 1,
365            weight: 100,
366            max_concurrent: 100,
367            timeout: Duration::from_secs(300),
368            read_only: false,
369            custom_egress_cost: None,
370            options: HashMap::new(),
371        }
372    }
373
374    /// Creates an HTTP provider configuration
375    #[must_use]
376    pub fn http(base_url: impl Into<String>) -> Self {
377        Self {
378            provider: CloudProvider::Http,
379            bucket: base_url.into(),
380            prefix: String::new(),
381            region: None,
382            endpoint: None,
383            priority: 1,
384            weight: 100,
385            max_concurrent: 100,
386            timeout: Duration::from_secs(300),
387            read_only: true, // HTTP is typically read-only
388            custom_egress_cost: None,
389            options: HashMap::new(),
390        }
391    }
392
393    /// Sets the prefix within the bucket
394    #[must_use]
395    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
396        self.prefix = prefix.into();
397        self
398    }
399
400    /// Sets the region
401    #[must_use]
402    pub fn with_region(mut self, region: CloudRegion) -> Self {
403        self.region = Some(region);
404        self
405    }
406
407    /// Sets a custom endpoint
408    #[must_use]
409    pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
410        self.endpoint = Some(endpoint.into());
411        self
412    }
413
414    /// Sets the priority (lower is higher)
415    #[must_use]
416    pub fn with_priority(mut self, priority: u32) -> Self {
417        self.priority = priority;
418        self
419    }
420
421    /// Sets the weight for load balancing
422    #[must_use]
423    pub fn with_weight(mut self, weight: u32) -> Self {
424        self.weight = weight.min(100);
425        self
426    }
427
428    /// Sets whether this provider is read-only
429    #[must_use]
430    pub fn with_read_only(mut self, read_only: bool) -> Self {
431        self.read_only = read_only;
432        self
433    }
434
435    /// Sets the request timeout
436    #[must_use]
437    pub fn with_timeout(mut self, timeout: Duration) -> Self {
438        self.timeout = timeout;
439        self
440    }
441
442    /// Sets custom egress cost per GB
443    #[must_use]
444    pub fn with_egress_cost(mut self, cost: f64) -> Self {
445        self.custom_egress_cost = Some(cost);
446        self
447    }
448
449    /// Adds a custom option
450    #[must_use]
451    pub fn with_option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
452        self.options.insert(key.into(), value.into());
453        self
454    }
455
456    /// Gets the effective egress cost
457    #[must_use]
458    pub fn effective_egress_cost(&self) -> f64 {
459        self.custom_egress_cost
460            .unwrap_or_else(|| self.provider.egress_cost_per_gb())
461    }
462
463    /// Returns the unique identifier for this provider config
464    #[must_use]
465    pub fn id(&self) -> String {
466        format!("{}:{}/{}", self.provider, self.bucket, self.prefix)
467    }
468}
469
470// ============================================================================
471// Provider Health Status
472// ============================================================================
473
474/// Health status of a cloud provider
475#[derive(Debug, Clone)]
476pub struct ProviderHealth {
477    /// Provider ID
478    pub provider_id: String,
479    /// Is the provider healthy
480    pub healthy: bool,
481    /// Last successful request time
482    pub last_success: Option<Instant>,
483    /// Last failure time
484    pub last_failure: Option<Instant>,
485    /// Consecutive failure count
486    pub consecutive_failures: usize,
487    /// Average latency in milliseconds
488    pub avg_latency_ms: f64,
489    /// Success rate (0.0 to 1.0)
490    pub success_rate: f64,
491    /// Total requests served
492    pub total_requests: u64,
493    /// Total bytes transferred
494    pub total_bytes: u64,
495}
496
497impl ProviderHealth {
498    /// Creates a new healthy provider status
499    fn new(provider_id: String) -> Self {
500        Self {
501            provider_id,
502            healthy: true,
503            last_success: None,
504            last_failure: None,
505            consecutive_failures: 0,
506            avg_latency_ms: 0.0,
507            success_rate: 1.0,
508            total_requests: 0,
509            total_bytes: 0,
510        }
511    }
512
513    /// Records a successful request
514    fn record_success(&mut self, latency_ms: f64, bytes: u64) {
515        self.last_success = Some(Instant::now());
516        self.consecutive_failures = 0;
517        self.healthy = true;
518        self.total_requests += 1;
519        self.total_bytes += bytes;
520
521        // Update average latency with exponential moving average
522        if self.total_requests == 1 {
523            self.avg_latency_ms = latency_ms;
524        } else {
525            self.avg_latency_ms = self.avg_latency_ms * 0.9 + latency_ms * 0.1;
526        }
527
528        // Update success rate
529        self.update_success_rate(true);
530    }
531
532    /// Records a failed request
533    fn record_failure(&mut self) {
534        self.last_failure = Some(Instant::now());
535        self.consecutive_failures += 1;
536        self.total_requests += 1;
537
538        // Mark unhealthy after threshold failures
539        if self.consecutive_failures >= 3 {
540            self.healthy = false;
541        }
542
543        self.update_success_rate(false);
544    }
545
546    fn update_success_rate(&mut self, success: bool) {
547        let success_value = if success { 1.0 } else { 0.0 };
548        self.success_rate = self.success_rate * 0.95 + success_value * 0.05;
549    }
550}
551
552// ============================================================================
553// Routing Strategy
554// ============================================================================
555
556/// Routing strategy for multi-cloud operations
557#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
558pub enum RoutingStrategy {
559    /// Use provider with lowest priority number
560    #[default]
561    Priority,
562    /// Round-robin between providers
563    RoundRobin,
564    /// Select based on weights
565    Weighted,
566    /// Select based on measured latency
567    LatencyBased,
568    /// Select to minimize cost
569    CostOptimized,
570    /// Select based on geographic proximity
571    RegionAware,
572    /// Combination of latency and cost
573    Adaptive,
574}
575
576// ============================================================================
577// Multi-Cloud Manager
578// ============================================================================
579
580/// Statistics tracking for multi-cloud operations
581struct ProviderStats {
582    request_count: AtomicU64,
583    byte_count: AtomicU64,
584    error_count: AtomicU64,
585    latency_sum_ms: AtomicU64,
586}
587
588impl ProviderStats {
589    fn new() -> Self {
590        Self {
591            request_count: AtomicU64::new(0),
592            byte_count: AtomicU64::new(0),
593            error_count: AtomicU64::new(0),
594            latency_sum_ms: AtomicU64::new(0),
595        }
596    }
597
598    fn record_success(&self, bytes: u64, latency_ms: u64) {
599        self.request_count.fetch_add(1, Ordering::Relaxed);
600        self.byte_count.fetch_add(bytes, Ordering::Relaxed);
601        self.latency_sum_ms.fetch_add(latency_ms, Ordering::Relaxed);
602    }
603
604    fn record_error(&self) {
605        self.request_count.fetch_add(1, Ordering::Relaxed);
606        self.error_count.fetch_add(1, Ordering::Relaxed);
607    }
608
609    fn avg_latency_ms(&self) -> f64 {
610        let requests = self.request_count.load(Ordering::Relaxed);
611        let errors = self.error_count.load(Ordering::Relaxed);
612        let successful = requests.saturating_sub(errors);
613        if successful == 0 {
614            return f64::MAX;
615        }
616        self.latency_sum_ms.load(Ordering::Relaxed) as f64 / successful as f64
617    }
618
619    fn success_rate(&self) -> f64 {
620        let requests = self.request_count.load(Ordering::Relaxed);
621        if requests == 0 {
622            return 1.0;
623        }
624        let errors = self.error_count.load(Ordering::Relaxed);
625        (requests - errors) as f64 / requests as f64
626    }
627}
628
629/// Multi-cloud storage manager
630pub struct MultiCloudManager {
631    /// Provider configurations
632    providers: Vec<CloudProviderConfig>,
633    /// Routing strategy
634    routing_strategy: RoutingStrategy,
635    /// Enable automatic failover
636    failover_enabled: bool,
637    /// Maximum failover attempts
638    max_failover_attempts: usize,
639    /// Enable cross-cloud replication
640    replication_enabled: bool,
641    /// Client region for latency calculations
642    client_region: Option<CloudRegion>,
643    /// Provider statistics
644    stats: HashMap<String, Arc<ProviderStats>>,
645    /// Round-robin counter
646    round_robin_counter: AtomicUsize,
647    /// Health check interval
648    health_check_interval: Duration,
649    /// Lazily-built, provider-id-keyed cache of concrete storage backends, so
650    /// each provider's SDK client is constructed at most once.
651    #[cfg(all(feature = "async", feature = "cache"))]
652    backend_cache: dashmap::DashMap<String, Arc<dyn CloudStorageBackend>>,
653}
654
655impl MultiCloudManager {
656    /// Creates a new builder for the multi-cloud manager
657    #[must_use]
658    pub fn builder() -> MultiCloudManagerBuilder {
659        MultiCloudManagerBuilder::new()
660    }
661
662    /// Returns the list of configured providers
663    #[must_use]
664    pub fn providers(&self) -> &[CloudProviderConfig] {
665        &self.providers
666    }
667
668    /// Returns provider statistics
669    #[must_use]
670    pub fn get_stats(&self, provider_id: &str) -> Option<(u64, u64, f64, f64)> {
671        self.stats.get(provider_id).map(|s| {
672            (
673                s.request_count.load(Ordering::Relaxed),
674                s.byte_count.load(Ordering::Relaxed),
675                s.avg_latency_ms(),
676                s.success_rate(),
677            )
678        })
679    }
680
681    /// Selects the best provider based on routing strategy
682    fn select_provider(&self, operation: &str) -> Result<&CloudProviderConfig> {
683        if self.providers.is_empty() {
684            return Err(CloudError::InvalidConfiguration {
685                message: "No providers configured".to_string(),
686            });
687        }
688
689        // Filter out unhealthy providers if we have healthy ones
690        let healthy_providers: Vec<_> = self
691            .providers
692            .iter()
693            .filter(|p| {
694                let stats = self.stats.get(&p.id());
695                stats.is_none_or(|s| s.success_rate() > 0.5)
696            })
697            .collect();
698
699        let candidates = if healthy_providers.is_empty() {
700            // Fallback to all providers if none are healthy
701            self.providers.iter().collect::<Vec<_>>()
702        } else {
703            healthy_providers
704        };
705
706        // Filter write operations for read-only providers
707        let write_operations = ["put", "delete", "write"];
708        let candidates: Vec<_> = if write_operations.contains(&operation.to_lowercase().as_str()) {
709            candidates.into_iter().filter(|p| !p.read_only).collect()
710        } else {
711            candidates
712        };
713
714        if candidates.is_empty() {
715            return Err(CloudError::NotSupported {
716                operation: format!("No writable providers available for {operation}"),
717            });
718        }
719
720        match self.routing_strategy {
721            RoutingStrategy::Priority => {
722                // Select by lowest priority number
723                candidates
724                    .iter()
725                    .min_by_key(|p| p.priority)
726                    .copied()
727                    .ok_or_else(|| CloudError::Internal {
728                        message: "Failed to select provider".to_string(),
729                    })
730            }
731            RoutingStrategy::RoundRobin => {
732                // Round-robin selection
733                let idx = self.round_robin_counter.fetch_add(1, Ordering::Relaxed);
734                Ok(candidates[idx % candidates.len()])
735            }
736            RoutingStrategy::Weighted => {
737                // Weighted selection based on provider weights
738                let total_weight: u32 = candidates.iter().map(|p| p.weight).sum();
739                if total_weight == 0 {
740                    return Ok(candidates[0]);
741                }
742
743                let target = simple_random() % total_weight;
744                let mut cumulative = 0u32;
745
746                for provider in &candidates {
747                    cumulative += provider.weight;
748                    if cumulative > target {
749                        return Ok(provider);
750                    }
751                }
752                Ok(candidates[0])
753            }
754            RoutingStrategy::LatencyBased => {
755                // Select by lowest measured latency
756                candidates
757                    .iter()
758                    .min_by(|a, b| {
759                        let lat_a = self
760                            .stats
761                            .get(&a.id())
762                            .map_or(f64::MAX, |s| s.avg_latency_ms());
763                        let lat_b = self
764                            .stats
765                            .get(&b.id())
766                            .map_or(f64::MAX, |s| s.avg_latency_ms());
767                        lat_a
768                            .partial_cmp(&lat_b)
769                            .unwrap_or(std::cmp::Ordering::Equal)
770                    })
771                    .copied()
772                    .ok_or_else(|| CloudError::Internal {
773                        message: "Failed to select provider".to_string(),
774                    })
775            }
776            RoutingStrategy::CostOptimized => {
777                // Select by lowest egress cost
778                candidates
779                    .iter()
780                    .min_by(|a, b| {
781                        let cost_a = a.effective_egress_cost();
782                        let cost_b = b.effective_egress_cost();
783                        cost_a
784                            .partial_cmp(&cost_b)
785                            .unwrap_or(std::cmp::Ordering::Equal)
786                    })
787                    .copied()
788                    .ok_or_else(|| CloudError::Internal {
789                        message: "Failed to select provider".to_string(),
790                    })
791            }
792            RoutingStrategy::RegionAware => {
793                // Select by geographic proximity
794                if let Some(ref client_region) = self.client_region {
795                    candidates
796                        .iter()
797                        .min_by_key(|p| {
798                            p.region
799                                .as_ref()
800                                .map(|r| client_region.estimated_latency_to(r))
801                                .unwrap_or(500)
802                        })
803                        .copied()
804                        .ok_or_else(|| CloudError::Internal {
805                            message: "Failed to select provider".to_string(),
806                        })
807                } else {
808                    // Fallback to priority if no client region
809                    candidates
810                        .iter()
811                        .min_by_key(|p| p.priority)
812                        .copied()
813                        .ok_or_else(|| CloudError::Internal {
814                            message: "Failed to select provider".to_string(),
815                        })
816                }
817            }
818            RoutingStrategy::Adaptive => {
819                // Combine latency and cost with health
820                candidates
821                    .iter()
822                    .min_by(|a, b| {
823                        let score_a = self.calculate_adaptive_score(a);
824                        let score_b = self.calculate_adaptive_score(b);
825                        score_a
826                            .partial_cmp(&score_b)
827                            .unwrap_or(std::cmp::Ordering::Equal)
828                    })
829                    .copied()
830                    .ok_or_else(|| CloudError::Internal {
831                        message: "Failed to select provider".to_string(),
832                    })
833            }
834        }
835    }
836
837    /// Calculates adaptive score (lower is better)
838    fn calculate_adaptive_score(&self, provider: &CloudProviderConfig) -> f64 {
839        let stats = self.stats.get(&provider.id());
840
841        // Latency component (normalized to 0-1)
842        let latency_score = stats.map_or(0.5, |s| {
843            let lat = s.avg_latency_ms();
844            if lat == f64::MAX {
845                1.0
846            } else {
847                (lat / 1000.0).min(1.0) // Normalize to 1s max
848            }
849        });
850
851        // Cost component (normalized to 0-1)
852        let cost_score = provider.effective_egress_cost() / 0.2; // Normalize to $0.20/GB max
853
854        // Health component (inverse of success rate)
855        let health_score = stats.map_or(0.0, |s| 1.0 - s.success_rate());
856
857        // Priority component
858        let priority_score = provider.priority as f64 / 10.0;
859
860        // Weighted combination
861        latency_score * 0.3 + cost_score * 0.3 + health_score * 0.3 + priority_score * 0.1
862    }
863
864    /// Gets providers for failover in order of preference
865    fn get_failover_providers(
866        &self,
867        failed_id: &str,
868        operation: &str,
869    ) -> Vec<&CloudProviderConfig> {
870        let write_operations = ["put", "delete", "write"];
871        let is_write = write_operations.contains(&operation.to_lowercase().as_str());
872
873        let mut candidates: Vec<_> = self
874            .providers
875            .iter()
876            .filter(|p| p.id() != failed_id && (!is_write || !p.read_only))
877            .collect();
878
879        // Sort by priority
880        candidates.sort_by_key(|p| p.priority);
881        candidates
882    }
883
884    /// Gets data from cloud storage with automatic failover
885    #[cfg(feature = "async")]
886    pub async fn get(&self, key: &str) -> Result<Bytes> {
887        let provider = self.select_provider("get")?;
888        let start = Instant::now();
889
890        match self.get_from_provider(provider, key).await {
891            Ok(data) => {
892                if let Some(stats) = self.stats.get(&provider.id()) {
893                    stats.record_success(data.len() as u64, start.elapsed().as_millis() as u64);
894                }
895                Ok(data)
896            }
897            Err(e) if self.failover_enabled => {
898                if let Some(stats) = self.stats.get(&provider.id()) {
899                    stats.record_error();
900                }
901                tracing::warn!(
902                    "Provider {} failed for get '{}': {}, attempting failover",
903                    provider.id(),
904                    key,
905                    e
906                );
907                self.get_with_failover(key, &provider.id()).await
908            }
909            Err(e) => {
910                if let Some(stats) = self.stats.get(&provider.id()) {
911                    stats.record_error();
912                }
913                Err(e)
914            }
915        }
916    }
917
918    /// Constructs a concrete storage backend for `p`.
919    ///
920    /// Each provider arm is gated by the Cargo feature that pulls in the
921    /// corresponding backend implementation; if that feature is disabled the
922    /// provider is reported as unsupported rather than failing to compile.
923    #[cfg(feature = "async")]
924    fn build_backend(&self, p: &CloudProviderConfig) -> Result<Box<dyn CloudStorageBackend>> {
925        match p.provider {
926            CloudProvider::AwsS3 => {
927                #[cfg(feature = "s3")]
928                {
929                    use crate::backends::S3Backend;
930
931                    let mut backend =
932                        S3Backend::new(p.bucket.clone(), p.prefix.clone()).with_timeout(p.timeout);
933                    if let Some(region) = &p.region {
934                        backend = backend.with_region(region.aws_code());
935                    }
936                    if let Some(endpoint) = &p.endpoint {
937                        backend = backend.with_endpoint(endpoint.clone());
938                    }
939                    Ok(Box::new(backend))
940                }
941                #[cfg(not(feature = "s3"))]
942                {
943                    Err(CloudError::NotSupported {
944                        operation: format!(
945                            "AWS S3 backend for provider '{}': enable the 's3' feature",
946                            p.id()
947                        ),
948                    })
949                }
950            }
951            CloudProvider::Gcs => {
952                #[cfg(feature = "gcs")]
953                {
954                    use crate::backends::GcsBackend;
955
956                    let mut backend = GcsBackend::new(p.bucket.clone())
957                        .with_prefix(p.prefix.clone())
958                        .with_timeout(p.timeout);
959                    if let Some(project_id) = p.options.get("project_id") {
960                        backend = backend.with_project_id(project_id.clone());
961                    }
962                    Ok(Box::new(backend))
963                }
964                #[cfg(not(feature = "gcs"))]
965                {
966                    Err(CloudError::NotSupported {
967                        operation: format!(
968                            "GCS backend for provider '{}': enable the 'gcs' feature",
969                            p.id()
970                        ),
971                    })
972                }
973            }
974            CloudProvider::AzureBlob => {
975                #[cfg(feature = "azure-blob")]
976                {
977                    use crate::backends::AzureBlobBackend;
978
979                    let account_name = p.options.get("account_name").ok_or_else(|| {
980                        CloudError::InvalidConfiguration {
981                            message: format!(
982                                "Azure provider '{}' is missing the required 'account_name' option",
983                                p.id()
984                            ),
985                        }
986                    })?;
987
988                    let mut backend = AzureBlobBackend::new(account_name.clone(), p.bucket.clone())
989                        .with_prefix(p.prefix.clone())
990                        .with_timeout(p.timeout);
991                    if let Some(account_key) = p.options.get("account_key") {
992                        backend = backend.with_account_key(account_key.clone());
993                    }
994                    if let Some(sas_token) = p.options.get("sas_token") {
995                        backend = backend.with_sas_token(sas_token.clone());
996                    }
997                    Ok(Box::new(backend))
998                }
999                #[cfg(not(feature = "azure-blob"))]
1000                {
1001                    Err(CloudError::NotSupported {
1002                        operation: format!(
1003                            "Azure Blob backend for provider '{}': enable the 'azure-blob' feature",
1004                            p.id()
1005                        ),
1006                    })
1007                }
1008            }
1009            CloudProvider::Http => {
1010                #[cfg(feature = "http")]
1011                {
1012                    use crate::backends::HttpBackend;
1013
1014                    let backend = HttpBackend::new(p.bucket.clone()).with_timeout(p.timeout);
1015                    Ok(Box::new(backend))
1016                }
1017                #[cfg(not(feature = "http"))]
1018                {
1019                    Err(CloudError::NotSupported {
1020                        operation: format!(
1021                            "HTTP backend for provider '{}': enable the 'http' feature",
1022                            p.id()
1023                        ),
1024                    })
1025                }
1026            }
1027            CloudProvider::Custom => Err(CloudError::NotSupported {
1028                operation: format!(
1029                    "Custom provider '{}' has no built-in backend implementation",
1030                    p.id()
1031                ),
1032            }),
1033        }
1034    }
1035
1036    /// Resolves the concrete backend for `provider`, reusing a previously
1037    /// built instance from the backend cache when available.
1038    #[cfg(all(feature = "async", feature = "cache"))]
1039    fn resolve_backend(
1040        &self,
1041        provider: &CloudProviderConfig,
1042    ) -> Result<Arc<dyn CloudStorageBackend>> {
1043        let id = provider.id();
1044        if let Some(existing) = self.backend_cache.get(&id) {
1045            return Ok(Arc::clone(existing.value()));
1046        }
1047
1048        let backend: Arc<dyn CloudStorageBackend> = Arc::from(self.build_backend(provider)?);
1049        self.backend_cache.insert(id, Arc::clone(&backend));
1050        Ok(backend)
1051    }
1052
1053    /// Resolves the concrete backend for `provider`. Without the `cache`
1054    /// feature a fresh backend is built on every call.
1055    #[cfg(all(feature = "async", not(feature = "cache")))]
1056    fn resolve_backend(
1057        &self,
1058        provider: &CloudProviderConfig,
1059    ) -> Result<Arc<dyn CloudStorageBackend>> {
1060        Ok(Arc::from(self.build_backend(provider)?))
1061    }
1062
1063    #[cfg(feature = "async")]
1064    async fn get_from_provider(&self, provider: &CloudProviderConfig, key: &str) -> Result<Bytes> {
1065        let backend = self.resolve_backend(provider)?;
1066        backend.get(key).await
1067    }
1068
1069    #[cfg(feature = "async")]
1070    async fn get_with_failover(&self, key: &str, failed_id: &str) -> Result<Bytes> {
1071        let failover_providers = self.get_failover_providers(failed_id, "get");
1072        let mut attempts = 0;
1073
1074        for provider in failover_providers {
1075            if attempts >= self.max_failover_attempts {
1076                break;
1077            }
1078            attempts += 1;
1079
1080            let start = Instant::now();
1081            match self.get_from_provider(provider, key).await {
1082                Ok(data) => {
1083                    if let Some(stats) = self.stats.get(&provider.id()) {
1084                        stats.record_success(data.len() as u64, start.elapsed().as_millis() as u64);
1085                    }
1086                    tracing::info!(
1087                        "Failover successful to provider {} for key '{}'",
1088                        provider.id(),
1089                        key
1090                    );
1091                    return Ok(data);
1092                }
1093                Err(e) => {
1094                    if let Some(stats) = self.stats.get(&provider.id()) {
1095                        stats.record_error();
1096                    }
1097                    tracing::warn!(
1098                        "Failover attempt {} to {} failed: {}",
1099                        attempts,
1100                        provider.id(),
1101                        e
1102                    );
1103                }
1104            }
1105        }
1106
1107        Err(CloudError::Internal {
1108            message: format!("All failover attempts exhausted for key '{key}'"),
1109        })
1110    }
1111
1112    /// Puts data to cloud storage with optional replication
1113    #[cfg(feature = "async")]
1114    pub async fn put(&self, key: &str, data: &[u8]) -> Result<()> {
1115        let provider = self.select_provider("put")?;
1116        let start = Instant::now();
1117
1118        match self.put_to_provider(provider, key, data).await {
1119            Ok(()) => {
1120                if let Some(stats) = self.stats.get(&provider.id()) {
1121                    stats.record_success(data.len() as u64, start.elapsed().as_millis() as u64);
1122                }
1123
1124                // Handle replication if enabled
1125                if self.replication_enabled {
1126                    self.replicate_to_other_providers(key, data, &provider.id())
1127                        .await;
1128                }
1129
1130                Ok(())
1131            }
1132            Err(e) if self.failover_enabled => {
1133                if let Some(stats) = self.stats.get(&provider.id()) {
1134                    stats.record_error();
1135                }
1136                tracing::warn!(
1137                    "Provider {} failed for put '{}': {}, attempting failover",
1138                    provider.id(),
1139                    key,
1140                    e
1141                );
1142                self.put_with_failover(key, data, &provider.id()).await
1143            }
1144            Err(e) => {
1145                if let Some(stats) = self.stats.get(&provider.id()) {
1146                    stats.record_error();
1147                }
1148                Err(e)
1149            }
1150        }
1151    }
1152
1153    #[cfg(feature = "async")]
1154    async fn put_to_provider(
1155        &self,
1156        provider: &CloudProviderConfig,
1157        key: &str,
1158        data: &[u8],
1159    ) -> Result<()> {
1160        let backend = self.resolve_backend(provider)?;
1161        backend.put(key, data).await
1162    }
1163
1164    #[cfg(feature = "async")]
1165    async fn put_with_failover(&self, key: &str, data: &[u8], failed_id: &str) -> Result<()> {
1166        let failover_providers = self.get_failover_providers(failed_id, "put");
1167        let mut attempts = 0;
1168
1169        for provider in failover_providers {
1170            if attempts >= self.max_failover_attempts {
1171                break;
1172            }
1173            attempts += 1;
1174
1175            let start = Instant::now();
1176            match self.put_to_provider(provider, key, data).await {
1177                Ok(()) => {
1178                    if let Some(stats) = self.stats.get(&provider.id()) {
1179                        stats.record_success(data.len() as u64, start.elapsed().as_millis() as u64);
1180                    }
1181                    tracing::info!(
1182                        "Failover successful to provider {} for put '{}'",
1183                        provider.id(),
1184                        key
1185                    );
1186                    return Ok(());
1187                }
1188                Err(e) => {
1189                    if let Some(stats) = self.stats.get(&provider.id()) {
1190                        stats.record_error();
1191                    }
1192                    tracing::warn!(
1193                        "Failover attempt {} to {} failed: {}",
1194                        attempts,
1195                        provider.id(),
1196                        e
1197                    );
1198                }
1199            }
1200        }
1201
1202        Err(CloudError::Internal {
1203            message: format!("All failover attempts exhausted for put '{key}'"),
1204        })
1205    }
1206
1207    #[cfg(feature = "async")]
1208    async fn replicate_to_other_providers(&self, key: &str, data: &[u8], primary_id: &str) {
1209        let replication_targets: Vec<_> = self
1210            .providers
1211            .iter()
1212            .filter(|p| p.id() != primary_id && !p.read_only)
1213            .collect();
1214
1215        for provider in replication_targets {
1216            if let Err(e) = self.put_to_provider(provider, key, data).await {
1217                tracing::warn!(
1218                    "Replication to {} failed for key '{}': {}",
1219                    provider.id(),
1220                    key,
1221                    e
1222                );
1223            }
1224        }
1225    }
1226
1227    /// Checks if an object exists in any provider
1228    #[cfg(feature = "async")]
1229    pub async fn exists(&self, key: &str) -> Result<bool> {
1230        let provider = self.select_provider("exists")?;
1231
1232        match self.exists_in_provider(provider, key).await {
1233            Ok(exists) => Ok(exists),
1234            Err(e) if self.failover_enabled => {
1235                tracing::warn!(
1236                    "Provider {} failed for exists '{}': {}, checking other providers",
1237                    provider.id(),
1238                    key,
1239                    e
1240                );
1241
1242                for fallback in self.get_failover_providers(&provider.id(), "exists") {
1243                    if let Ok(exists) = self.exists_in_provider(fallback, key).await {
1244                        return Ok(exists);
1245                    }
1246                }
1247                Err(e)
1248            }
1249            Err(e) => Err(e),
1250        }
1251    }
1252
1253    #[cfg(feature = "async")]
1254    async fn exists_in_provider(&self, provider: &CloudProviderConfig, key: &str) -> Result<bool> {
1255        let backend = self.resolve_backend(provider)?;
1256        backend.exists(key).await
1257    }
1258
1259    /// Deletes an object from all providers
1260    #[cfg(feature = "async")]
1261    pub async fn delete(&self, key: &str) -> Result<()> {
1262        let mut success = false;
1263        let mut last_error = None;
1264
1265        for provider in &self.providers {
1266            if provider.read_only {
1267                continue;
1268            }
1269
1270            match self.delete_from_provider(provider, key).await {
1271                Ok(()) => success = true,
1272                Err(e) => {
1273                    tracing::warn!(
1274                        "Failed to delete '{}' from provider {}: {}",
1275                        key,
1276                        provider.id(),
1277                        e
1278                    );
1279                    last_error = Some(e);
1280                }
1281            }
1282        }
1283
1284        if success {
1285            Ok(())
1286        } else {
1287            Err(last_error.unwrap_or_else(|| CloudError::NotSupported {
1288                operation: "No writable providers available".to_string(),
1289            }))
1290        }
1291    }
1292
1293    #[cfg(feature = "async")]
1294    async fn delete_from_provider(&self, provider: &CloudProviderConfig, key: &str) -> Result<()> {
1295        let backend = self.resolve_backend(provider)?;
1296        backend.delete(key).await
1297    }
1298
1299    /// Estimates the cost of transferring data
1300    #[must_use]
1301    pub fn estimate_transfer_cost(&self, bytes: u64) -> TransferCostEstimate {
1302        let gb = bytes as f64 / (1024.0 * 1024.0 * 1024.0);
1303
1304        let mut estimates = Vec::new();
1305        for provider in &self.providers {
1306            let egress_cost = provider.effective_egress_cost() * gb;
1307            estimates.push((provider.id(), egress_cost));
1308        }
1309
1310        estimates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1311
1312        let (cheapest_id, cheapest_cost) = estimates.first().cloned().unwrap_or_default();
1313        let (most_expensive_id, most_expensive_cost) =
1314            estimates.last().cloned().unwrap_or_default();
1315
1316        TransferCostEstimate {
1317            bytes,
1318            cheapest_provider: cheapest_id,
1319            cheapest_cost,
1320            most_expensive_provider: most_expensive_id,
1321            most_expensive_cost,
1322            all_estimates: estimates,
1323        }
1324    }
1325}
1326
1327// ============================================================================
1328// Builder
1329// ============================================================================
1330
1331/// Builder for `MultiCloudManager`
1332pub struct MultiCloudManagerBuilder {
1333    providers: Vec<CloudProviderConfig>,
1334    routing_strategy: RoutingStrategy,
1335    failover_enabled: bool,
1336    max_failover_attempts: usize,
1337    replication_enabled: bool,
1338    client_region: Option<CloudRegion>,
1339    health_check_interval: Duration,
1340}
1341
1342impl MultiCloudManagerBuilder {
1343    /// Creates a new builder
1344    #[must_use]
1345    pub fn new() -> Self {
1346        Self {
1347            providers: Vec::new(),
1348            routing_strategy: RoutingStrategy::Priority,
1349            failover_enabled: true,
1350            max_failover_attempts: 3,
1351            replication_enabled: false,
1352            client_region: None,
1353            health_check_interval: Duration::from_secs(60),
1354        }
1355    }
1356
1357    /// Adds a provider configuration
1358    #[must_use]
1359    pub fn add_provider(mut self, config: CloudProviderConfig) -> Self {
1360        self.providers.push(config);
1361        self
1362    }
1363
1364    /// Sets the routing strategy
1365    #[must_use]
1366    pub fn with_routing_strategy(mut self, strategy: RoutingStrategy) -> Self {
1367        self.routing_strategy = strategy;
1368        self
1369    }
1370
1371    /// Enables or disables failover
1372    #[must_use]
1373    pub fn with_failover(mut self, enabled: bool) -> Self {
1374        self.failover_enabled = enabled;
1375        self
1376    }
1377
1378    /// Sets maximum failover attempts
1379    #[must_use]
1380    pub fn with_max_failover_attempts(mut self, attempts: usize) -> Self {
1381        self.max_failover_attempts = attempts;
1382        self
1383    }
1384
1385    /// Enables latency-based routing
1386    #[must_use]
1387    pub fn with_latency_routing(mut self, enabled: bool) -> Self {
1388        if enabled {
1389            self.routing_strategy = RoutingStrategy::LatencyBased;
1390        }
1391        self
1392    }
1393
1394    /// Enables cost-optimized routing
1395    #[must_use]
1396    pub fn with_cost_routing(mut self, enabled: bool) -> Self {
1397        if enabled {
1398            self.routing_strategy = RoutingStrategy::CostOptimized;
1399        }
1400        self
1401    }
1402
1403    /// Enables replication across providers
1404    #[must_use]
1405    pub fn with_replication(mut self, enabled: bool) -> Self {
1406        self.replication_enabled = enabled;
1407        self
1408    }
1409
1410    /// Sets the client region for region-aware routing
1411    #[must_use]
1412    pub fn with_client_region(mut self, region: CloudRegion) -> Self {
1413        self.client_region = Some(region);
1414        self
1415    }
1416
1417    /// Sets the health check interval
1418    #[must_use]
1419    pub fn with_health_check_interval(mut self, interval: Duration) -> Self {
1420        self.health_check_interval = interval;
1421        self
1422    }
1423
1424    /// Builds the multi-cloud manager
1425    pub fn build(self) -> Result<MultiCloudManager> {
1426        if self.providers.is_empty() {
1427            return Err(CloudError::InvalidConfiguration {
1428                message: "At least one provider must be configured".to_string(),
1429            });
1430        }
1431
1432        let mut stats = HashMap::new();
1433        for provider in &self.providers {
1434            stats.insert(provider.id(), Arc::new(ProviderStats::new()));
1435        }
1436
1437        Ok(MultiCloudManager {
1438            providers: self.providers,
1439            routing_strategy: self.routing_strategy,
1440            failover_enabled: self.failover_enabled,
1441            max_failover_attempts: self.max_failover_attempts,
1442            replication_enabled: self.replication_enabled,
1443            client_region: self.client_region,
1444            stats,
1445            round_robin_counter: AtomicUsize::new(0),
1446            health_check_interval: self.health_check_interval,
1447            #[cfg(all(feature = "async", feature = "cache"))]
1448            backend_cache: dashmap::DashMap::new(),
1449        })
1450    }
1451}
1452
1453impl Default for MultiCloudManagerBuilder {
1454    fn default() -> Self {
1455        Self::new()
1456    }
1457}
1458
1459// ============================================================================
1460// Transfer Cost Estimation
1461// ============================================================================
1462
1463/// Estimated transfer cost across providers
1464#[derive(Debug, Clone)]
1465pub struct TransferCostEstimate {
1466    /// Number of bytes to transfer
1467    pub bytes: u64,
1468    /// Cheapest provider ID
1469    pub cheapest_provider: String,
1470    /// Cost for cheapest provider (USD)
1471    pub cheapest_cost: f64,
1472    /// Most expensive provider ID
1473    pub most_expensive_provider: String,
1474    /// Cost for most expensive provider (USD)
1475    pub most_expensive_cost: f64,
1476    /// All provider estimates
1477    pub all_estimates: Vec<(String, f64)>,
1478}
1479
1480impl Default for TransferCostEstimate {
1481    fn default() -> Self {
1482        Self {
1483            bytes: 0,
1484            cheapest_provider: String::new(),
1485            cheapest_cost: 0.0,
1486            most_expensive_provider: String::new(),
1487            most_expensive_cost: 0.0,
1488            all_estimates: Vec::new(),
1489        }
1490    }
1491}
1492
1493// ============================================================================
1494// Cross-Cloud Transfer
1495// ============================================================================
1496
1497/// Configuration for cross-cloud data transfer
1498#[derive(Debug, Clone)]
1499pub struct CrossCloudTransferConfig {
1500    /// Source provider ID
1501    pub source_provider: String,
1502    /// Destination provider ID
1503    pub dest_provider: String,
1504    /// Chunk size for streaming transfer
1505    pub chunk_size: usize,
1506    /// Maximum concurrent transfers
1507    pub max_concurrent: usize,
1508    /// Verify integrity after transfer
1509    pub verify_integrity: bool,
1510    /// Delete source after successful transfer
1511    pub delete_source: bool,
1512}
1513
1514impl Default for CrossCloudTransferConfig {
1515    fn default() -> Self {
1516        Self {
1517            source_provider: String::new(),
1518            dest_provider: String::new(),
1519            chunk_size: 8 * 1024 * 1024, // 8 MB
1520            max_concurrent: 4,
1521            verify_integrity: true,
1522            delete_source: false,
1523        }
1524    }
1525}
1526
1527/// Result of a cross-cloud transfer operation
1528#[derive(Debug, Clone)]
1529pub struct CrossCloudTransferResult {
1530    /// Number of objects transferred
1531    pub objects_transferred: usize,
1532    /// Total bytes transferred
1533    pub bytes_transferred: u64,
1534    /// Transfer duration
1535    pub duration: Duration,
1536    /// Failed transfers
1537    pub failures: Vec<(String, String)>,
1538    /// Estimated cost
1539    pub estimated_cost: f64,
1540}
1541
1542// ============================================================================
1543// Helper Functions
1544// ============================================================================
1545
1546/// Simple random number generator (no external dependencies)
1547fn simple_random() -> u32 {
1548    use std::sync::atomic::{AtomicU64, Ordering};
1549    static SEED: AtomicU64 = AtomicU64::new(0x5deece66d);
1550
1551    let seed = SEED.load(Ordering::Relaxed);
1552    let next = seed.wrapping_mul(0x5deece66d).wrapping_add(0xb);
1553    SEED.store(next, Ordering::Relaxed);
1554
1555    (next >> 17) as u32
1556}
1557
1558// ============================================================================
1559// Tests
1560// ============================================================================
1561
1562#[cfg(test)]
1563mod tests {
1564    use super::*;
1565
1566    #[test]
1567    fn test_cloud_provider_from_url() {
1568        assert_eq!(
1569            CloudProvider::from_url("s3://bucket/key"),
1570            Some(CloudProvider::AwsS3)
1571        );
1572        assert_eq!(
1573            CloudProvider::from_url("gs://bucket/object"),
1574            Some(CloudProvider::Gcs)
1575        );
1576        assert_eq!(
1577            CloudProvider::from_url("az://container/blob"),
1578            Some(CloudProvider::AzureBlob)
1579        );
1580        assert_eq!(
1581            CloudProvider::from_url("https://example.com/path"),
1582            Some(CloudProvider::Http)
1583        );
1584        assert_eq!(
1585            CloudProvider::from_url("https://mybucket.s3.amazonaws.com/key"),
1586            Some(CloudProvider::AwsS3)
1587        );
1588        assert_eq!(
1589            CloudProvider::from_url("https://account.blob.core.windows.net/container"),
1590            Some(CloudProvider::AzureBlob)
1591        );
1592        assert_eq!(
1593            CloudProvider::from_url("https://storage.googleapis.com/bucket/object"),
1594            Some(CloudProvider::Gcs)
1595        );
1596        assert_eq!(CloudProvider::from_url("invalid"), None);
1597    }
1598
1599    #[test]
1600    fn test_cloud_region_codes() {
1601        let region = CloudRegion::UsEast1;
1602        assert_eq!(region.aws_code(), "us-east-1");
1603        assert_eq!(region.azure_code(), "eastus");
1604        assert_eq!(region.gcs_code(), "us-east1");
1605
1606        let region = CloudRegion::from_string("eu-west-1");
1607        assert_eq!(region, CloudRegion::EuWest1);
1608    }
1609
1610    #[test]
1611    fn test_region_latency_estimation() {
1612        let us_east = CloudRegion::UsEast1;
1613        let us_west = CloudRegion::UsWest2;
1614        let eu_west = CloudRegion::EuWest1;
1615
1616        // Same region should be minimal
1617        assert_eq!(us_east.estimated_latency_to(&us_east), 1);
1618
1619        // US to US should be moderate
1620        let us_to_us = us_east.estimated_latency_to(&us_west);
1621        assert!(us_to_us > 50 && us_to_us < 100);
1622
1623        // US to EU should be higher
1624        let us_to_eu = us_east.estimated_latency_to(&eu_west);
1625        assert!(us_to_eu > us_to_us);
1626    }
1627
1628    #[test]
1629    fn test_provider_config_builder() {
1630        let config = CloudProviderConfig::s3("my-bucket")
1631            .with_prefix("data/")
1632            .with_region(CloudRegion::UsWest2)
1633            .with_priority(1)
1634            .with_weight(80)
1635            .with_timeout(Duration::from_secs(60));
1636
1637        assert_eq!(config.bucket, "my-bucket");
1638        assert_eq!(config.prefix, "data/");
1639        assert_eq!(config.region, Some(CloudRegion::UsWest2));
1640        assert_eq!(config.priority, 1);
1641        assert_eq!(config.weight, 80);
1642        assert_eq!(config.timeout, Duration::from_secs(60));
1643    }
1644
1645    #[test]
1646    fn test_provider_config_id() {
1647        let config = CloudProviderConfig::s3("bucket").with_prefix("prefix");
1648        assert_eq!(config.id(), "AWS S3:bucket/prefix");
1649    }
1650
1651    #[test]
1652    fn test_egress_costs() {
1653        assert!(CloudProvider::AwsS3.egress_cost_per_gb() > 0.0);
1654        assert!(CloudProvider::AzureBlob.egress_cost_per_gb() > 0.0);
1655        assert!(CloudProvider::Gcs.egress_cost_per_gb() > 0.0);
1656        assert_eq!(CloudProvider::Http.egress_cost_per_gb(), 0.0);
1657    }
1658
1659    #[test]
1660    fn test_multicloud_manager_builder() {
1661        let manager = MultiCloudManager::builder()
1662            .add_provider(CloudProviderConfig::s3("bucket1").with_priority(1))
1663            .add_provider(CloudProviderConfig::gcs("bucket2").with_priority(2))
1664            .with_failover(true)
1665            .with_latency_routing(true)
1666            .build();
1667
1668        assert!(manager.is_ok());
1669        let manager = manager.expect("Manager should be built");
1670        assert_eq!(manager.providers.len(), 2);
1671        assert_eq!(manager.routing_strategy, RoutingStrategy::LatencyBased);
1672    }
1673
1674    #[test]
1675    fn test_multicloud_manager_empty_providers() {
1676        let manager = MultiCloudManager::builder().build();
1677        assert!(manager.is_err());
1678    }
1679
1680    #[test]
1681    fn test_transfer_cost_estimate() {
1682        let manager = MultiCloudManager::builder()
1683            .add_provider(CloudProviderConfig::s3("bucket1"))
1684            .add_provider(CloudProviderConfig::http("http://example.com"))
1685            .build()
1686            .expect("Manager should be built");
1687
1688        let estimate = manager.estimate_transfer_cost(1024 * 1024 * 1024); // 1 GB
1689        assert!(estimate.cheapest_cost <= estimate.most_expensive_cost);
1690    }
1691
1692    #[test]
1693    fn test_provider_health() {
1694        let mut health = ProviderHealth::new("test-provider".to_string());
1695
1696        assert!(health.healthy);
1697        assert_eq!(health.consecutive_failures, 0);
1698
1699        // Record some successes
1700        health.record_success(100.0, 1000);
1701        health.record_success(120.0, 2000);
1702        assert!(health.avg_latency_ms > 0.0);
1703        assert_eq!(health.total_bytes, 3000);
1704
1705        // Record failures
1706        health.record_failure();
1707        health.record_failure();
1708        health.record_failure();
1709
1710        assert!(!health.healthy);
1711        assert_eq!(health.consecutive_failures, 3);
1712    }
1713
1714    #[test]
1715    fn test_provider_stats() {
1716        let stats = ProviderStats::new();
1717
1718        stats.record_success(1000, 50);
1719        stats.record_success(2000, 60);
1720
1721        assert_eq!(stats.request_count.load(Ordering::Relaxed), 2);
1722        assert_eq!(stats.byte_count.load(Ordering::Relaxed), 3000);
1723        assert!((stats.avg_latency_ms() - 55.0).abs() < 0.001);
1724        assert!((stats.success_rate() - 1.0).abs() < 0.001);
1725
1726        stats.record_error();
1727        assert!(stats.success_rate() < 1.0);
1728    }
1729
1730    #[test]
1731    fn test_cross_cloud_transfer_config() {
1732        let config = CrossCloudTransferConfig::default();
1733
1734        assert_eq!(config.chunk_size, 8 * 1024 * 1024);
1735        assert_eq!(config.max_concurrent, 4);
1736        assert!(config.verify_integrity);
1737        assert!(!config.delete_source);
1738    }
1739
1740    #[test]
1741    fn test_routing_strategy_default() {
1742        let strategy = RoutingStrategy::default();
1743        assert_eq!(strategy, RoutingStrategy::Priority);
1744    }
1745
1746    #[test]
1747    fn test_select_provider_priority() {
1748        let manager = MultiCloudManager::builder()
1749            .add_provider(CloudProviderConfig::s3("bucket1").with_priority(2))
1750            .add_provider(CloudProviderConfig::gcs("bucket2").with_priority(1))
1751            .with_routing_strategy(RoutingStrategy::Priority)
1752            .build()
1753            .expect("Manager should be built");
1754
1755        let provider = manager.select_provider("get");
1756        assert!(provider.is_ok());
1757        let provider = provider.expect("Provider should be selected");
1758        assert_eq!(provider.provider, CloudProvider::Gcs);
1759    }
1760
1761    #[test]
1762    fn test_select_provider_cost_optimized() {
1763        let manager = MultiCloudManager::builder()
1764            .add_provider(CloudProviderConfig::s3("bucket1"))
1765            .add_provider(CloudProviderConfig::http("http://example.com"))
1766            .with_routing_strategy(RoutingStrategy::CostOptimized)
1767            .build()
1768            .expect("Manager should be built");
1769
1770        let provider = manager.select_provider("get");
1771        assert!(provider.is_ok());
1772        let provider = provider.expect("Provider should be selected");
1773        // HTTP should be cheapest (0 egress cost)
1774        assert_eq!(provider.provider, CloudProvider::Http);
1775    }
1776
1777    #[test]
1778    fn test_select_provider_write_filters_readonly() {
1779        let manager = MultiCloudManager::builder()
1780            .add_provider(CloudProviderConfig::http("http://example.com").with_priority(1))
1781            .add_provider(CloudProviderConfig::s3("bucket1").with_priority(2))
1782            .with_routing_strategy(RoutingStrategy::Priority)
1783            .build()
1784            .expect("Manager should be built");
1785
1786        // For write operations, should skip HTTP (read-only)
1787        let provider = manager.select_provider("put");
1788        assert!(provider.is_ok());
1789        let provider = provider.expect("Provider should be selected");
1790        assert_eq!(provider.provider, CloudProvider::AwsS3);
1791    }
1792
1793    #[cfg(feature = "async")]
1794    #[test]
1795    fn test_build_backend_custom_provider_not_supported() {
1796        let manager = MultiCloudManager::builder()
1797            .add_provider(CloudProviderConfig::http("http://example.com"))
1798            .build()
1799            .expect("Manager should be built");
1800
1801        let mut custom = CloudProviderConfig::http("http://example.com");
1802        custom.provider = CloudProvider::Custom;
1803
1804        let result = manager.build_backend(&custom);
1805        assert!(result.is_err());
1806    }
1807
1808    #[cfg(all(feature = "async", feature = "azure-blob"))]
1809    #[test]
1810    fn test_build_backend_azure_requires_account_name() {
1811        let manager = MultiCloudManager::builder()
1812            .add_provider(CloudProviderConfig::azure("container"))
1813            .build()
1814            .expect("Manager should be built");
1815
1816        let config = CloudProviderConfig::azure("container");
1817        let result = manager.build_backend(&config);
1818        assert!(matches!(
1819            result,
1820            Err(CloudError::InvalidConfiguration { .. })
1821        ));
1822
1823        let config_with_account = config.with_option("account_name", "myaccount");
1824        let result = manager.build_backend(&config_with_account);
1825        assert!(result.is_ok());
1826    }
1827
1828    #[cfg(all(feature = "async", feature = "http"))]
1829    #[test]
1830    fn test_build_backend_http() {
1831        let manager = MultiCloudManager::builder()
1832            .add_provider(CloudProviderConfig::http("http://example.com"))
1833            .build()
1834            .expect("Manager should be built");
1835
1836        let config = CloudProviderConfig::http("http://example.com");
1837        let result = manager.build_backend(&config);
1838        assert!(result.is_ok());
1839    }
1840
1841    #[cfg(all(feature = "async", feature = "s3"))]
1842    #[test]
1843    fn test_build_backend_s3_applies_region_and_endpoint() {
1844        let manager = MultiCloudManager::builder()
1845            .add_provider(CloudProviderConfig::s3("bucket"))
1846            .build()
1847            .expect("Manager should be built");
1848
1849        let config = CloudProviderConfig::s3("bucket")
1850            .with_region(CloudRegion::UsWest2)
1851            .with_endpoint("https://custom.example.com");
1852        let result = manager.build_backend(&config);
1853        assert!(result.is_ok());
1854    }
1855
1856    #[cfg(all(feature = "async", feature = "gcs"))]
1857    #[test]
1858    fn test_build_backend_gcs() {
1859        let manager = MultiCloudManager::builder()
1860            .add_provider(CloudProviderConfig::gcs("bucket"))
1861            .build()
1862            .expect("Manager should be built");
1863
1864        let config = CloudProviderConfig::gcs("bucket").with_option("project_id", "my-project");
1865        let result = manager.build_backend(&config);
1866        assert!(result.is_ok());
1867    }
1868
1869    #[cfg(all(feature = "async", feature = "http", feature = "cache"))]
1870    #[test]
1871    fn test_resolve_backend_caches_instance() {
1872        let manager = MultiCloudManager::builder()
1873            .add_provider(CloudProviderConfig::http("http://example.com"))
1874            .build()
1875            .expect("Manager should be built");
1876
1877        let config = CloudProviderConfig::http("http://example.com");
1878        let first = manager
1879            .resolve_backend(&config)
1880            .expect("first resolve should succeed");
1881        let second = manager
1882            .resolve_backend(&config)
1883            .expect("second resolve should succeed");
1884
1885        assert!(Arc::ptr_eq(&first, &second));
1886    }
1887}