Skip to main content

winterbaume_backup/
state.rs

1use std::collections::HashMap;
2
3use chrono::Utc;
4use thiserror::Error;
5use uuid::Uuid;
6
7use crate::types::*;
8
9#[derive(Debug, Default)]
10pub struct BackupState {
11    pub vaults: HashMap<String, BackupVault>,
12    pub backup_plans: HashMap<String, BackupPlanData>,
13    /// Selections keyed by selection_id.
14    pub backup_selections: HashMap<String, BackupSelectionData>,
15    pub report_plans: HashMap<String, ReportPlanData>,
16    /// Recovery points keyed by ARN.
17    pub recovery_points: HashMap<String, RecoveryPointData>,
18    /// Backup jobs keyed by job_id.
19    pub backup_jobs: HashMap<String, BackupJobData>,
20    /// Tags keyed by resource ARN.
21    pub resource_tags: HashMap<String, HashMap<String, String>>,
22    /// Vault access policies keyed by vault name.
23    pub vault_access_policies: HashMap<String, VaultAccessPolicy>,
24    /// Vault notification configurations keyed by vault name.
25    pub vault_notifications: HashMap<String, VaultNotificationConfig>,
26    /// Audit frameworks keyed by framework name.
27    pub frameworks: HashMap<String, FrameworkData>,
28    /// Global settings (account-level).
29    pub global_settings: GlobalSettings,
30    /// Region settings.
31    pub region_settings: RegionSettings,
32    /// Report jobs keyed by job_id.
33    pub report_jobs: HashMap<String, ReportJobData>,
34    /// Scan jobs keyed by scan_job_id.
35    pub scan_jobs: HashMap<String, ScanJobData>,
36    /// Tiering configurations keyed by configuration name.
37    pub tiering_configs: HashMap<String, TieringConfigData>,
38    /// Legal holds keyed by legal_hold_id.
39    pub legal_holds: HashMap<String, LegalHoldData>,
40    /// Copy jobs keyed by copy_job_id.
41    pub copy_jobs: HashMap<String, CopyJobData>,
42    /// Restore jobs keyed by restore_job_id.
43    pub restore_jobs: HashMap<String, RestoreJobData>,
44    /// Restore testing plans keyed by plan name.
45    pub restore_testing_plans: HashMap<String, RestoreTestingPlanData>,
46    /// Restore testing selections keyed by (plan_name, selection_name).
47    pub restore_testing_selections: HashMap<(String, String), RestoreTestingSelectionData>,
48}
49
50#[derive(Debug, Error)]
51pub enum BackupError {
52    #[error("Backup vault {0} already exists.")]
53    VaultAlreadyExists(String),
54
55    #[error("Backup vault {0} does not exist.")]
56    VaultNotFound(String),
57
58    #[error("Backup plan with name {0} already exists.")]
59    PlanAlreadyExists(String),
60
61    #[error("Backup plan {0} does not exist.")]
62    PlanNotFound(String),
63
64    #[error("Report plan {0} already exists.")]
65    ReportPlanAlreadyExists(String),
66
67    #[error("Report plan {0} does not exist.")]
68    ReportPlanNotFound(String),
69
70    #[error("Framework {0} already exists.")]
71    FrameworkAlreadyExists(String),
72
73    #[error("Framework {0} does not exist.")]
74    FrameworkNotFound(String),
75
76    #[error("Backup selection {0} does not exist.")]
77    SelectionNotFound(String),
78
79    #[error("Recovery point {0} does not exist.")]
80    RecoveryPointNotFound(String),
81
82    #[error("Backup vault access policy for {0} does not exist.")]
83    VaultAccessPolicyNotFound(String),
84
85    #[error("Notification configuration for vault {0} does not exist.")]
86    VaultNotificationsNotFound(String),
87
88    #[error("Backup job {0} does not exist.")]
89    BackupJobNotFound(String),
90
91    #[error("Report job {0} does not exist.")]
92    ReportJobNotFound(String),
93
94    #[error("Scan job {0} does not exist.")]
95    ScanJobNotFound(String),
96
97    #[error("Tiering configuration {0} already exists.")]
98    TieringConfigAlreadyExists(String),
99
100    #[error("Tiering configuration {0} does not exist.")]
101    TieringConfigNotFound(String),
102
103    #[error("Vault {0} does not have a lock configured.")]
104    VaultNotLocked(String),
105
106    #[error("Backup job {0} is not in a cancellable state.")]
107    BackupJobNotCancellable(String),
108
109    #[error("Legal hold {0} already exists.")]
110    LegalHoldAlreadyExists(String),
111
112    #[error("Legal hold {0} does not exist.")]
113    LegalHoldNotFound(String),
114
115    #[error("Copy job {0} does not exist.")]
116    CopyJobNotFound(String),
117
118    #[error("Restore job {0} does not exist.")]
119    RestoreJobNotFound(String),
120
121    #[error("Restore testing plan {0} already exists.")]
122    RestoreTestingPlanAlreadyExists(String),
123
124    #[error("Restore testing plan {0} does not exist.")]
125    RestoreTestingPlanNotFound(String),
126
127    #[error("Restore testing selection {0} already exists.")]
128    RestoreTestingSelectionAlreadyExists(String),
129
130    #[error("Restore testing selection {0} does not exist.")]
131    RestoreTestingSelectionNotFound(String),
132}
133
134impl BackupState {
135    pub fn create_backup_vault(
136        &mut self,
137        name: &str,
138        arn: &str,
139        tags: HashMap<String, String>,
140    ) -> Result<&BackupVault, BackupError> {
141        if self.vaults.contains_key(name) {
142            return Err(BackupError::VaultAlreadyExists(name.to_string()));
143        }
144
145        if !tags.is_empty() {
146            self.resource_tags.insert(arn.to_string(), tags.clone());
147        }
148
149        let vault = BackupVault {
150            backup_vault_name: name.to_string(),
151            backup_vault_arn: arn.to_string(),
152            creation_date: Utc::now(),
153            number_of_recovery_points: 0,
154            locked: false,
155            min_retention_days: None,
156            max_retention_days: None,
157            lock_date: None,
158            tags,
159        };
160
161        self.vaults.insert(name.to_string(), vault);
162        Ok(self.vaults.get(name).unwrap())
163    }
164
165    pub fn describe_backup_vault(&self, name: &str) -> Result<&BackupVault, BackupError> {
166        self.vaults
167            .get(name)
168            .ok_or_else(|| BackupError::VaultNotFound(name.to_string()))
169    }
170
171    pub fn delete_backup_vault(&mut self, name: &str) -> Result<(), BackupError> {
172        if self.vaults.remove(name).is_none() {
173            return Err(BackupError::VaultNotFound(name.to_string()));
174        }
175        Ok(())
176    }
177
178    pub fn list_backup_vaults(&self) -> Vec<&BackupVault> {
179        self.vaults.values().collect()
180    }
181
182    // --- Backup Plan operations ---
183
184    pub fn create_backup_plan(
185        &mut self,
186        name: &str,
187        plan_json: &serde_json::Value,
188        region: &str,
189        account_id: &str,
190        tags: HashMap<String, String>,
191    ) -> Result<&BackupPlanData, BackupError> {
192        // Check for duplicate plan names
193        let already_exists = self
194            .backup_plans
195            .values()
196            .any(|p| p.backup_plan_name == name);
197        if already_exists {
198            return Err(BackupError::PlanAlreadyExists(name.to_string()));
199        }
200
201        let plan_id = Uuid::new_v4().to_string();
202        let version_id = Uuid::new_v4().to_string();
203        let arn = format!("arn:aws:backup:{region}:{account_id}:backup-plan:{plan_id}");
204
205        // Generate rule IDs for each rule in the plan JSON
206        let enriched_json =
207            if let Some(rules_arr) = plan_json.get("Rules").and_then(|v| v.as_array()) {
208                let enriched_rules: Vec<serde_json::Value> = rules_arr
209                    .iter()
210                    .map(|rule| {
211                        let mut r = rule.clone();
212                        if let serde_json::Value::Object(ref mut map) = r {
213                            map.entry("RuleId").or_insert_with(|| {
214                                serde_json::Value::String(Uuid::new_v4().to_string())
215                            });
216                        }
217                        r
218                    })
219                    .collect();
220                let mut enriched = plan_json.clone();
221                if let serde_json::Value::Object(ref mut map) = enriched {
222                    map.insert(
223                        "Rules".to_string(),
224                        serde_json::Value::Array(enriched_rules),
225                    );
226                }
227                enriched
228            } else {
229                plan_json.clone()
230            };
231
232        let plan = BackupPlanData {
233            backup_plan_id: plan_id.clone(),
234            backup_plan_arn: arn.clone(),
235            backup_plan_name: name.to_string(),
236            version_id,
237            creation_date: Utc::now(),
238            backup_plan_json: enriched_json,
239            tags: tags.clone(),
240        };
241
242        if !tags.is_empty() {
243            self.resource_tags.insert(arn, tags);
244        }
245
246        self.backup_plans.insert(plan_id.clone(), plan);
247        Ok(self.backup_plans.get(&plan_id).unwrap())
248    }
249
250    pub fn get_backup_plan(&self, plan_id: &str) -> Result<&BackupPlanData, BackupError> {
251        self.backup_plans
252            .get(plan_id)
253            .ok_or_else(|| BackupError::PlanNotFound(plan_id.to_string()))
254    }
255
256    pub fn delete_backup_plan(&mut self, plan_id: &str) -> Result<BackupPlanData, BackupError> {
257        self.backup_plans
258            .remove(plan_id)
259            .ok_or_else(|| BackupError::PlanNotFound(plan_id.to_string()))
260    }
261
262    pub fn list_backup_plans(&self) -> Vec<&BackupPlanData> {
263        self.backup_plans.values().collect()
264    }
265
266    // --- Report Plan operations ---
267
268    pub fn create_report_plan(
269        &mut self,
270        name: &str,
271        description: &str,
272        delivery_channel: &serde_json::Value,
273        report_setting: &serde_json::Value,
274        region: &str,
275        account_id: &str,
276        tags: HashMap<String, String>,
277    ) -> Result<&ReportPlanData, BackupError> {
278        if self.report_plans.contains_key(name) {
279            return Err(BackupError::ReportPlanAlreadyExists(name.to_string()));
280        }
281
282        let arn = format!("arn:aws:backup:{region}:{account_id}:report-plan:{name}");
283
284        let plan = ReportPlanData {
285            report_plan_name: name.to_string(),
286            report_plan_arn: arn.clone(),
287            report_plan_description: description.to_string(),
288            report_delivery_channel: delivery_channel.clone(),
289            report_setting: report_setting.clone(),
290            creation_time: Utc::now(),
291            deployment_status: "COMPLETED".to_string(),
292            tags: tags.clone(),
293        };
294
295        if !tags.is_empty() {
296            self.resource_tags.insert(arn, tags);
297        }
298
299        self.report_plans.insert(name.to_string(), plan);
300        Ok(self.report_plans.get(name).unwrap())
301    }
302
303    pub fn describe_report_plan(&self, name: &str) -> Result<&ReportPlanData, BackupError> {
304        self.report_plans
305            .get(name)
306            .ok_or_else(|| BackupError::ReportPlanNotFound(name.to_string()))
307    }
308
309    pub fn delete_report_plan(&mut self, name: &str) -> Result<(), BackupError> {
310        if self.report_plans.remove(name).is_none() {
311            return Err(BackupError::ReportPlanNotFound(name.to_string()));
312        }
313        Ok(())
314    }
315
316    pub fn list_report_plans(&self) -> Vec<&ReportPlanData> {
317        self.report_plans.values().collect()
318    }
319
320    // --- Vault lock operations ---
321
322    pub fn put_backup_vault_lock_configuration(
323        &mut self,
324        vault_name: &str,
325        min_retention_days: Option<i64>,
326        max_retention_days: Option<i64>,
327    ) -> Result<(), BackupError> {
328        let vault = self
329            .vaults
330            .get_mut(vault_name)
331            .ok_or_else(|| BackupError::VaultNotFound(vault_name.to_string()))?;
332        vault.locked = true;
333        vault.min_retention_days = min_retention_days;
334        vault.max_retention_days = max_retention_days;
335        vault.lock_date = Some(Utc::now());
336        Ok(())
337    }
338
339    pub fn delete_backup_vault_lock_configuration(
340        &mut self,
341        vault_name: &str,
342    ) -> Result<(), BackupError> {
343        let vault = self
344            .vaults
345            .get_mut(vault_name)
346            .ok_or_else(|| BackupError::VaultNotFound(vault_name.to_string()))?;
347        if !vault.locked {
348            return Err(BackupError::VaultNotLocked(vault_name.to_string()));
349        }
350        vault.locked = false;
351        vault.min_retention_days = None;
352        vault.max_retention_days = None;
353        vault.lock_date = None;
354        Ok(())
355    }
356
357    // --- Tag operations ---
358
359    pub fn tag_resource(
360        &mut self,
361        resource_arn: &str,
362        tags: HashMap<String, String>,
363    ) -> Result<(), BackupError> {
364        let entry = self
365            .resource_tags
366            .entry(resource_arn.to_string())
367            .or_default();
368        entry.extend(tags);
369        Ok(())
370    }
371
372    pub fn untag_resource(
373        &mut self,
374        resource_arn: &str,
375        tag_keys: &[String],
376    ) -> Result<(), BackupError> {
377        if let Some(tags) = self.resource_tags.get_mut(resource_arn) {
378            for key in tag_keys {
379                tags.remove(key);
380            }
381        }
382        Ok(())
383    }
384
385    pub fn list_tags(&self, resource_arn: &str) -> HashMap<String, String> {
386        self.resource_tags
387            .get(resource_arn)
388            .cloned()
389            .unwrap_or_default()
390    }
391
392    // --- Backup Selection operations ---
393
394    pub fn create_backup_selection(
395        &mut self,
396        plan_id: &str,
397        selection_name: &str,
398        iam_role_arn: &str,
399        resources: Vec<String>,
400        selection_json: serde_json::Value,
401    ) -> Result<&BackupSelectionData, BackupError> {
402        if !self.backup_plans.contains_key(plan_id) {
403            return Err(BackupError::PlanNotFound(plan_id.to_string()));
404        }
405
406        let selection_id = Uuid::new_v4().to_string();
407        let selection = BackupSelectionData {
408            selection_id: selection_id.clone(),
409            backup_plan_id: plan_id.to_string(),
410            selection_name: selection_name.to_string(),
411            iam_role_arn: iam_role_arn.to_string(),
412            resources,
413            creation_date: Utc::now(),
414            selection_json,
415        };
416
417        self.backup_selections
418            .insert(selection_id.clone(), selection);
419        Ok(self.backup_selections.get(&selection_id).unwrap())
420    }
421
422    pub fn get_backup_selection(
423        &self,
424        plan_id: &str,
425        selection_id: &str,
426    ) -> Result<&BackupSelectionData, BackupError> {
427        self.backup_selections
428            .get(selection_id)
429            .filter(|s| s.backup_plan_id == plan_id)
430            .ok_or_else(|| BackupError::SelectionNotFound(selection_id.to_string()))
431    }
432
433    pub fn delete_backup_selection(
434        &mut self,
435        plan_id: &str,
436        selection_id: &str,
437    ) -> Result<(), BackupError> {
438        let exists = self
439            .backup_selections
440            .get(selection_id)
441            .map(|s| s.backup_plan_id == plan_id)
442            .unwrap_or(false);
443        if !exists {
444            return Err(BackupError::SelectionNotFound(selection_id.to_string()));
445        }
446        self.backup_selections.remove(selection_id);
447        Ok(())
448    }
449
450    pub fn list_backup_selections(&self, plan_id: &str) -> Vec<&BackupSelectionData> {
451        self.backup_selections
452            .values()
453            .filter(|s| s.backup_plan_id == plan_id)
454            .collect()
455    }
456
457    // --- Recovery Point operations ---
458
459    pub fn create_recovery_point(
460        &mut self,
461        vault_name: &str,
462        vault_arn: &str,
463        resource_arn: &str,
464        resource_type: &str,
465        iam_role_arn: &str,
466        account_id: &str,
467        region: &str,
468    ) -> Result<&RecoveryPointData, BackupError> {
469        if !self.vaults.contains_key(vault_name) {
470            return Err(BackupError::VaultNotFound(vault_name.to_string()));
471        }
472
473        let recovery_point_id = Uuid::new_v4().to_string();
474        let recovery_point_arn =
475            format!("arn:aws:backup:{region}:{account_id}:recovery-point:{recovery_point_id}");
476
477        let rp = RecoveryPointData {
478            recovery_point_arn: recovery_point_arn.clone(),
479            backup_vault_name: vault_name.to_string(),
480            backup_vault_arn: vault_arn.to_string(),
481            resource_arn: resource_arn.to_string(),
482            resource_type: resource_type.to_string(),
483            iam_role_arn: iam_role_arn.to_string(),
484            status: "COMPLETED".to_string(),
485            creation_date: Utc::now(),
486            backup_size_bytes: 0,
487            account_id: account_id.to_string(),
488        };
489
490        // Update vault recovery point count
491        if let Some(vault) = self.vaults.get_mut(vault_name) {
492            vault.number_of_recovery_points += 1;
493        }
494
495        self.recovery_points.insert(recovery_point_arn.clone(), rp);
496        Ok(self.recovery_points.get(&recovery_point_arn).unwrap())
497    }
498
499    pub fn describe_recovery_point(
500        &self,
501        vault_name: &str,
502        recovery_point_arn: &str,
503    ) -> Result<&RecoveryPointData, BackupError> {
504        self.recovery_points
505            .get(recovery_point_arn)
506            .filter(|rp| rp.backup_vault_name == vault_name)
507            .ok_or_else(|| BackupError::RecoveryPointNotFound(recovery_point_arn.to_string()))
508    }
509
510    pub fn delete_recovery_point(
511        &mut self,
512        vault_name: &str,
513        recovery_point_arn: &str,
514    ) -> Result<(), BackupError> {
515        let exists = self
516            .recovery_points
517            .get(recovery_point_arn)
518            .map(|rp| rp.backup_vault_name == vault_name)
519            .unwrap_or(false);
520        if !exists {
521            return Err(BackupError::RecoveryPointNotFound(
522                recovery_point_arn.to_string(),
523            ));
524        }
525        self.recovery_points.remove(recovery_point_arn);
526        // Update vault recovery point count
527        if let Some(vault) = self.vaults.get_mut(vault_name) {
528            vault.number_of_recovery_points = vault.number_of_recovery_points.saturating_sub(1);
529        }
530        Ok(())
531    }
532
533    pub fn list_recovery_points_by_backup_vault(
534        &self,
535        vault_name: &str,
536    ) -> Vec<&RecoveryPointData> {
537        self.recovery_points
538            .values()
539            .filter(|rp| rp.backup_vault_name == vault_name)
540            .collect()
541    }
542
543    // --- Backup Job operations ---
544
545    pub fn start_backup_job(
546        &mut self,
547        vault_name: &str,
548        vault_arn: &str,
549        resource_arn: &str,
550        resource_type: &str,
551        iam_role_arn: &str,
552        account_id: &str,
553        region: &str,
554    ) -> Result<&BackupJobData, BackupError> {
555        if !self.vaults.contains_key(vault_name) {
556            return Err(BackupError::VaultNotFound(vault_name.to_string()));
557        }
558
559        let job_id = Uuid::new_v4().to_string();
560        let recovery_point_id = Uuid::new_v4().to_string();
561        let recovery_point_arn =
562            format!("arn:aws:backup:{region}:{account_id}:recovery-point:{recovery_point_id}");
563
564        let job = BackupJobData {
565            backup_job_id: job_id.clone(),
566            backup_vault_name: vault_name.to_string(),
567            backup_vault_arn: vault_arn.to_string(),
568            recovery_point_arn: recovery_point_arn.clone(),
569            resource_arn: resource_arn.to_string(),
570            resource_type: resource_type.to_string(),
571            iam_role_arn: iam_role_arn.to_string(),
572            state: "RUNNING".to_string(),
573            creation_date: Utc::now(),
574            completion_date: None,
575            account_id: account_id.to_string(),
576        };
577
578        self.backup_jobs.insert(job_id.clone(), job);
579        Ok(self.backup_jobs.get(&job_id).unwrap())
580    }
581
582    pub fn describe_backup_job(&self, job_id: &str) -> Result<&BackupJobData, BackupError> {
583        self.backup_jobs
584            .get(job_id)
585            .ok_or_else(|| BackupError::BackupJobNotFound(job_id.to_string()))
586    }
587
588    pub fn stop_backup_job(&mut self, job_id: &str) -> Result<(), BackupError> {
589        let job = self
590            .backup_jobs
591            .get_mut(job_id)
592            .ok_or_else(|| BackupError::BackupJobNotFound(job_id.to_string()))?;
593        if job.state == "COMPLETED" || job.state == "ABORTED" {
594            return Err(BackupError::BackupJobNotCancellable(job_id.to_string()));
595        }
596        job.state = "ABORTED".to_string();
597        job.completion_date = Some(Utc::now());
598        Ok(())
599    }
600
601    pub fn list_backup_jobs(&self) -> Vec<&BackupJobData> {
602        self.backup_jobs.values().collect()
603    }
604
605    // --- Vault Access Policy operations ---
606
607    pub fn put_backup_vault_access_policy(
608        &mut self,
609        vault_name: &str,
610        vault_arn: &str,
611        policy: &str,
612    ) -> Result<(), BackupError> {
613        if !self.vaults.contains_key(vault_name) {
614            return Err(BackupError::VaultNotFound(vault_name.to_string()));
615        }
616        self.vault_access_policies.insert(
617            vault_name.to_string(),
618            VaultAccessPolicy {
619                backup_vault_name: vault_name.to_string(),
620                backup_vault_arn: vault_arn.to_string(),
621                policy: policy.to_string(),
622            },
623        );
624        Ok(())
625    }
626
627    pub fn get_backup_vault_access_policy(
628        &self,
629        vault_name: &str,
630    ) -> Result<&VaultAccessPolicy, BackupError> {
631        self.vault_access_policies
632            .get(vault_name)
633            .ok_or_else(|| BackupError::VaultAccessPolicyNotFound(vault_name.to_string()))
634    }
635
636    pub fn delete_backup_vault_access_policy(
637        &mut self,
638        vault_name: &str,
639    ) -> Result<(), BackupError> {
640        if !self.vaults.contains_key(vault_name) {
641            return Err(BackupError::VaultNotFound(vault_name.to_string()));
642        }
643        self.vault_access_policies.remove(vault_name);
644        Ok(())
645    }
646
647    // --- Vault Notification operations ---
648
649    pub fn put_backup_vault_notifications(
650        &mut self,
651        vault_name: &str,
652        vault_arn: &str,
653        sns_topic_arn: &str,
654        backup_vault_events: Vec<String>,
655    ) -> Result<(), BackupError> {
656        if !self.vaults.contains_key(vault_name) {
657            return Err(BackupError::VaultNotFound(vault_name.to_string()));
658        }
659        self.vault_notifications.insert(
660            vault_name.to_string(),
661            VaultNotificationConfig {
662                backup_vault_name: vault_name.to_string(),
663                backup_vault_arn: vault_arn.to_string(),
664                sns_topic_arn: sns_topic_arn.to_string(),
665                backup_vault_events,
666            },
667        );
668        Ok(())
669    }
670
671    pub fn get_backup_vault_notifications(
672        &self,
673        vault_name: &str,
674    ) -> Result<&VaultNotificationConfig, BackupError> {
675        self.vault_notifications
676            .get(vault_name)
677            .ok_or_else(|| BackupError::VaultNotificationsNotFound(vault_name.to_string()))
678    }
679
680    pub fn delete_backup_vault_notifications(
681        &mut self,
682        vault_name: &str,
683    ) -> Result<(), BackupError> {
684        if !self.vaults.contains_key(vault_name) {
685            return Err(BackupError::VaultNotFound(vault_name.to_string()));
686        }
687        self.vault_notifications.remove(vault_name);
688        Ok(())
689    }
690
691    // --- Framework operations ---
692
693    #[allow(clippy::too_many_arguments)]
694    pub fn create_framework(
695        &mut self,
696        name: &str,
697        description: &str,
698        controls: serde_json::Value,
699        region: &str,
700        account_id: &str,
701        tags: HashMap<String, String>,
702    ) -> Result<&FrameworkData, BackupError> {
703        if self.frameworks.contains_key(name) {
704            return Err(BackupError::FrameworkAlreadyExists(name.to_string()));
705        }
706
707        let arn = format!("arn:aws:backup:{region}:{account_id}:framework:{name}");
708        let num_controls = controls.as_array().map(|a| a.len() as i32).unwrap_or(0);
709
710        let framework = FrameworkData {
711            framework_name: name.to_string(),
712            framework_arn: arn.clone(),
713            framework_description: description.to_string(),
714            framework_controls: controls,
715            creation_time: Utc::now(),
716            deployment_status: "COMPLETED".to_string(),
717            number_of_controls: num_controls,
718        };
719
720        if !tags.is_empty() {
721            self.resource_tags.insert(arn, tags);
722        }
723
724        self.frameworks.insert(name.to_string(), framework);
725        Ok(self.frameworks.get(name).unwrap())
726    }
727
728    pub fn describe_framework(&self, name: &str) -> Result<&FrameworkData, BackupError> {
729        self.frameworks
730            .get(name)
731            .ok_or_else(|| BackupError::FrameworkNotFound(name.to_string()))
732    }
733
734    pub fn delete_framework(&mut self, name: &str) -> Result<(), BackupError> {
735        if self.frameworks.remove(name).is_none() {
736            return Err(BackupError::FrameworkNotFound(name.to_string()));
737        }
738        Ok(())
739    }
740
741    pub fn update_framework(
742        &mut self,
743        name: &str,
744        description: Option<&str>,
745        controls: Option<serde_json::Value>,
746    ) -> Result<&FrameworkData, BackupError> {
747        let framework = self
748            .frameworks
749            .get_mut(name)
750            .ok_or_else(|| BackupError::FrameworkNotFound(name.to_string()))?;
751        if let Some(desc) = description {
752            framework.framework_description = desc.to_string();
753        }
754        if let Some(c) = controls {
755            framework.number_of_controls = c.as_array().map(|a| a.len() as i32).unwrap_or(0);
756            framework.framework_controls = c;
757        }
758        Ok(self.frameworks.get(name).unwrap())
759    }
760
761    pub fn list_frameworks(&self) -> Vec<&FrameworkData> {
762        self.frameworks.values().collect()
763    }
764
765    // --- Global Settings operations ---
766
767    pub fn update_global_settings(&mut self, settings: HashMap<String, String>) {
768        self.global_settings.global_settings.extend(settings);
769    }
770
771    pub fn describe_global_settings(&self) -> &GlobalSettings {
772        &self.global_settings
773    }
774
775    // --- Region Settings operations ---
776
777    pub fn update_region_settings(
778        &mut self,
779        opt_in: Option<HashMap<String, bool>>,
780        management: Option<HashMap<String, bool>>,
781    ) {
782        if let Some(p) = opt_in {
783            self.region_settings
784                .resource_type_opt_in_preference
785                .extend(p);
786        }
787        if let Some(p) = management {
788            self.region_settings
789                .resource_type_management_preference
790                .extend(p);
791        }
792    }
793
794    pub fn describe_region_settings(&self) -> &RegionSettings {
795        &self.region_settings
796    }
797
798    // --- Report Job operations ---
799
800    pub fn start_report_job(
801        &mut self,
802        report_plan_name: &str,
803        region: &str,
804        account_id: &str,
805    ) -> Result<&ReportJobData, BackupError> {
806        let plan = self
807            .report_plans
808            .get(report_plan_name)
809            .ok_or_else(|| BackupError::ReportPlanNotFound(report_plan_name.to_string()))?;
810        let report_plan_arn = plan.report_plan_arn.clone();
811        let report_template = plan
812            .report_setting
813            .get("ReportTemplate")
814            .and_then(|v| v.as_str())
815            .unwrap_or("")
816            .to_string();
817
818        let job_id = Uuid::new_v4().to_string();
819        let job = ReportJobData {
820            report_job_id: job_id.clone(),
821            report_plan_arn,
822            report_template,
823            creation_time: Utc::now(),
824            completion_time: Some(Utc::now()),
825            status: "COMPLETED".to_string(),
826        };
827
828        self.report_jobs.insert(job_id.clone(), job);
829        Ok(self.report_jobs.get(&job_id).unwrap())
830    }
831
832    pub fn describe_report_job(&self, job_id: &str) -> Result<&ReportJobData, BackupError> {
833        self.report_jobs
834            .get(job_id)
835            .ok_or_else(|| BackupError::ReportJobNotFound(job_id.to_string()))
836    }
837
838    pub fn list_report_jobs(&self, report_plan_name: Option<&str>) -> Vec<&ReportJobData> {
839        self.report_jobs
840            .values()
841            .filter(|j| {
842                if let Some(name) = report_plan_name {
843                    j.report_plan_arn.ends_with(&format!(":{name}"))
844                } else {
845                    true
846                }
847            })
848            .collect()
849    }
850
851    // --- Scan Job operations ---
852
853    pub fn start_scan_job(
854        &mut self,
855        vault_name: &str,
856        vault_arn: &str,
857        recovery_point_arn: &str,
858        iam_role_arn: &str,
859        malware_scanner: &str,
860        scan_mode: &str,
861        scanner_role_arn: &str,
862        scan_base_recovery_point_arn: Option<String>,
863        account_id: &str,
864        region: &str,
865    ) -> Result<&ScanJobData, BackupError> {
866        let scan_job_id = Uuid::new_v4().to_string();
867        let job = ScanJobData {
868            scan_job_id: scan_job_id.clone(),
869            backup_vault_name: vault_name.to_string(),
870            backup_vault_arn: vault_arn.to_string(),
871            recovery_point_arn: recovery_point_arn.to_string(),
872            iam_role_arn: iam_role_arn.to_string(),
873            malware_scanner: malware_scanner.to_string(),
874            scan_mode: scan_mode.to_string(),
875            scanner_role_arn: scanner_role_arn.to_string(),
876            scan_base_recovery_point_arn,
877            state: "RUNNING".to_string(),
878            creation_date: Utc::now(),
879            completion_date: None,
880            account_id: account_id.to_string(),
881        };
882        self.scan_jobs.insert(scan_job_id.clone(), job);
883        Ok(self.scan_jobs.get(&scan_job_id).unwrap())
884    }
885
886    pub fn describe_scan_job(&self, scan_job_id: &str) -> Result<&ScanJobData, BackupError> {
887        self.scan_jobs
888            .get(scan_job_id)
889            .ok_or_else(|| BackupError::ScanJobNotFound(scan_job_id.to_string()))
890    }
891
892    pub fn list_scan_jobs(&self) -> Vec<&ScanJobData> {
893        self.scan_jobs.values().collect()
894    }
895
896    // --- Tiering Configuration operations ---
897
898    pub fn create_tiering_configuration(
899        &mut self,
900        name: &str,
901        vault_name: &str,
902        resource_selection: serde_json::Value,
903        creator_request_id: Option<String>,
904        region: &str,
905        account_id: &str,
906        tags: HashMap<String, String>,
907    ) -> Result<&TieringConfigData, BackupError> {
908        if self.tiering_configs.contains_key(name) {
909            return Err(BackupError::TieringConfigAlreadyExists(name.to_string()));
910        }
911
912        let arn = format!("arn:aws:backup:{region}:{account_id}:tiering-configuration:{name}");
913        let now = Utc::now();
914
915        let config = TieringConfigData {
916            tiering_configuration_name: name.to_string(),
917            tiering_configuration_arn: arn.clone(),
918            backup_vault_name: vault_name.to_string(),
919            resource_selection,
920            creation_time: now,
921            last_updated_time: now,
922            creator_request_id,
923            tags: tags.clone(),
924        };
925
926        if !tags.is_empty() {
927            self.resource_tags.insert(arn, tags);
928        }
929
930        self.tiering_configs.insert(name.to_string(), config);
931        Ok(self.tiering_configs.get(name).unwrap())
932    }
933
934    pub fn get_tiering_configuration(&self, name: &str) -> Result<&TieringConfigData, BackupError> {
935        self.tiering_configs
936            .get(name)
937            .ok_or_else(|| BackupError::TieringConfigNotFound(name.to_string()))
938    }
939
940    pub fn delete_tiering_configuration(&mut self, name: &str) -> Result<(), BackupError> {
941        if self.tiering_configs.remove(name).is_none() {
942            return Err(BackupError::TieringConfigNotFound(name.to_string()));
943        }
944        Ok(())
945    }
946
947    pub fn list_tiering_configurations(&self) -> Vec<&TieringConfigData> {
948        self.tiering_configs.values().collect()
949    }
950
951    pub fn update_tiering_configuration(
952        &mut self,
953        name: &str,
954        vault_name: Option<&str>,
955        resource_selection: Option<serde_json::Value>,
956    ) -> Result<&TieringConfigData, BackupError> {
957        let config = self
958            .tiering_configs
959            .get_mut(name)
960            .ok_or_else(|| BackupError::TieringConfigNotFound(name.to_string()))?;
961        if let Some(vn) = vault_name {
962            config.backup_vault_name = vn.to_string();
963        }
964        if let Some(rs) = resource_selection {
965            config.resource_selection = rs;
966        }
967        config.last_updated_time = Utc::now();
968        Ok(self.tiering_configs.get(name).unwrap())
969    }
970
971    // --- UpdateReportPlan ---
972
973    pub fn update_report_plan(
974        &mut self,
975        name: &str,
976        description: Option<&str>,
977        delivery_channel: Option<serde_json::Value>,
978        report_setting: Option<serde_json::Value>,
979    ) -> Result<&ReportPlanData, BackupError> {
980        let plan = self
981            .report_plans
982            .get_mut(name)
983            .ok_or_else(|| BackupError::ReportPlanNotFound(name.to_string()))?;
984        if let Some(desc) = description {
985            plan.report_plan_description = desc.to_string();
986        }
987        if let Some(dc) = delivery_channel {
988            plan.report_delivery_channel = dc;
989        }
990        if let Some(rs) = report_setting {
991            plan.report_setting = rs;
992        }
993        Ok(self.report_plans.get(name).unwrap())
994    }
995
996    // --- Legal Hold operations ---
997
998    pub fn create_legal_hold(
999        &mut self,
1000        title: &str,
1001        description: &str,
1002        recovery_point_selection: serde_json::Value,
1003        region: &str,
1004        account_id: &str,
1005        tags: HashMap<String, String>,
1006    ) -> Result<&LegalHoldData, BackupError> {
1007        let legal_hold_id = Uuid::new_v4().to_string();
1008        let arn = format!("arn:aws:backup:{region}:{account_id}:legal-hold:{legal_hold_id}");
1009
1010        let hold = LegalHoldData {
1011            legal_hold_id: legal_hold_id.clone(),
1012            legal_hold_arn: arn.clone(),
1013            title: title.to_string(),
1014            description: description.to_string(),
1015            status: "ACTIVE".to_string(),
1016            creation_date: Utc::now(),
1017            cancellation_date: None,
1018            recovery_point_selection,
1019            tags: tags.clone(),
1020        };
1021
1022        if !tags.is_empty() {
1023            self.resource_tags.insert(arn, tags);
1024        }
1025
1026        self.legal_holds.insert(legal_hold_id.clone(), hold);
1027        Ok(self.legal_holds.get(&legal_hold_id).unwrap())
1028    }
1029
1030    pub fn cancel_legal_hold(&mut self, legal_hold_id: &str) -> Result<(), BackupError> {
1031        let hold = self
1032            .legal_holds
1033            .get_mut(legal_hold_id)
1034            .ok_or_else(|| BackupError::LegalHoldNotFound(legal_hold_id.to_string()))?;
1035        hold.status = "CANCELED".to_string();
1036        hold.cancellation_date = Some(Utc::now());
1037        Ok(())
1038    }
1039
1040    pub fn get_legal_hold(&self, legal_hold_id: &str) -> Result<&LegalHoldData, BackupError> {
1041        self.legal_holds
1042            .get(legal_hold_id)
1043            .ok_or_else(|| BackupError::LegalHoldNotFound(legal_hold_id.to_string()))
1044    }
1045
1046    pub fn list_legal_holds(&self) -> Vec<&LegalHoldData> {
1047        self.legal_holds.values().collect()
1048    }
1049
1050    // --- Copy Job operations ---
1051
1052    pub fn start_copy_job(
1053        &mut self,
1054        source_backup_vault_name: &str,
1055        source_recovery_point_arn: &str,
1056        destination_backup_vault_arn: &str,
1057        iam_role_arn: &str,
1058        account_id: &str,
1059        region: &str,
1060    ) -> Result<&CopyJobData, BackupError> {
1061        let copy_job_id = Uuid::new_v4().to_string();
1062        let source_vault_arn =
1063            format!("arn:aws:backup:{region}:{account_id}:backup-vault:{source_backup_vault_name}");
1064        let dest_rp_id = Uuid::new_v4().to_string();
1065        let dest_rp_arn =
1066            format!("arn:aws:backup:{region}:{account_id}:recovery-point:{dest_rp_id}");
1067
1068        let job = CopyJobData {
1069            copy_job_id: copy_job_id.clone(),
1070            source_backup_vault_name: source_backup_vault_name.to_string(),
1071            source_backup_vault_arn: source_vault_arn,
1072            source_recovery_point_arn: source_recovery_point_arn.to_string(),
1073            destination_backup_vault_arn: destination_backup_vault_arn.to_string(),
1074            destination_recovery_point_arn: dest_rp_arn,
1075            resource_arn: String::new(),
1076            resource_type: String::new(),
1077            iam_role_arn: iam_role_arn.to_string(),
1078            state: "COMPLETED".to_string(),
1079            creation_date: Utc::now(),
1080            completion_date: Some(Utc::now()),
1081            account_id: account_id.to_string(),
1082        };
1083
1084        self.copy_jobs.insert(copy_job_id.clone(), job);
1085        Ok(self.copy_jobs.get(&copy_job_id).unwrap())
1086    }
1087
1088    pub fn describe_copy_job(&self, copy_job_id: &str) -> Result<&CopyJobData, BackupError> {
1089        self.copy_jobs
1090            .get(copy_job_id)
1091            .ok_or_else(|| BackupError::CopyJobNotFound(copy_job_id.to_string()))
1092    }
1093
1094    pub fn list_copy_jobs(&self) -> Vec<&CopyJobData> {
1095        self.copy_jobs.values().collect()
1096    }
1097
1098    // --- Restore Job operations ---
1099
1100    pub fn start_restore_job(
1101        &mut self,
1102        recovery_point_arn: &str,
1103        iam_role_arn: &str,
1104        resource_type: &str,
1105        metadata: HashMap<String, String>,
1106        account_id: &str,
1107    ) -> Result<&RestoreJobData, BackupError> {
1108        let restore_job_id = Uuid::new_v4().to_string();
1109
1110        let job = RestoreJobData {
1111            restore_job_id: restore_job_id.clone(),
1112            recovery_point_arn: recovery_point_arn.to_string(),
1113            resource_type: resource_type.to_string(),
1114            iam_role_arn: iam_role_arn.to_string(),
1115            status: "COMPLETED".to_string(),
1116            creation_date: Utc::now(),
1117            completion_date: Some(Utc::now()),
1118            backup_size_in_bytes: 0,
1119            account_id: account_id.to_string(),
1120            metadata,
1121            validation_status: None,
1122            validation_status_message: None,
1123        };
1124
1125        self.restore_jobs.insert(restore_job_id.clone(), job);
1126        Ok(self.restore_jobs.get(&restore_job_id).unwrap())
1127    }
1128
1129    pub fn describe_restore_job(
1130        &self,
1131        restore_job_id: &str,
1132    ) -> Result<&RestoreJobData, BackupError> {
1133        self.restore_jobs
1134            .get(restore_job_id)
1135            .ok_or_else(|| BackupError::RestoreJobNotFound(restore_job_id.to_string()))
1136    }
1137
1138    pub fn list_restore_jobs(&self) -> Vec<&RestoreJobData> {
1139        self.restore_jobs.values().collect()
1140    }
1141
1142    pub fn list_restore_jobs_by_recovery_point(&self, resource_arn: &str) -> Vec<&RestoreJobData> {
1143        self.restore_jobs
1144            .values()
1145            .filter(|j| j.recovery_point_arn.contains(resource_arn))
1146            .collect()
1147    }
1148
1149    pub fn put_restore_validation_result(
1150        &mut self,
1151        restore_job_id: &str,
1152        validation_status: &str,
1153        validation_status_message: Option<&str>,
1154    ) -> Result<(), BackupError> {
1155        let job = self
1156            .restore_jobs
1157            .get_mut(restore_job_id)
1158            .ok_or_else(|| BackupError::RestoreJobNotFound(restore_job_id.to_string()))?;
1159        job.validation_status = Some(validation_status.to_string());
1160        job.validation_status_message = validation_status_message.map(|s| s.to_string());
1161        Ok(())
1162    }
1163
1164    pub fn get_restore_job_metadata(
1165        &self,
1166        restore_job_id: &str,
1167    ) -> Result<&RestoreJobData, BackupError> {
1168        self.restore_jobs
1169            .get(restore_job_id)
1170            .ok_or_else(|| BackupError::RestoreJobNotFound(restore_job_id.to_string()))
1171    }
1172
1173    // --- Restore Testing Plan operations ---
1174
1175    #[allow(clippy::too_many_arguments)]
1176    pub fn create_restore_testing_plan(
1177        &mut self,
1178        name: &str,
1179        schedule_expression: &str,
1180        schedule_expression_timezone: Option<String>,
1181        start_window_hours: Option<i32>,
1182        recovery_point_selection: serde_json::Value,
1183        creator_request_id: Option<String>,
1184        region: &str,
1185        account_id: &str,
1186        tags: HashMap<String, String>,
1187    ) -> Result<&RestoreTestingPlanData, BackupError> {
1188        if self.restore_testing_plans.contains_key(name) {
1189            return Err(BackupError::RestoreTestingPlanAlreadyExists(
1190                name.to_string(),
1191            ));
1192        }
1193
1194        let arn = format!("arn:aws:backup:{region}:{account_id}:restore-testing-plan:{name}");
1195        let now = Utc::now();
1196        let plan = RestoreTestingPlanData {
1197            restore_testing_plan_name: name.to_string(),
1198            restore_testing_plan_arn: arn.clone(),
1199            schedule_expression: schedule_expression.to_string(),
1200            schedule_expression_timezone,
1201            start_window_hours,
1202            recovery_point_selection,
1203            creator_request_id,
1204            creation_time: now,
1205            last_update_time: now,
1206            tags: tags.clone(),
1207        };
1208
1209        if !tags.is_empty() {
1210            self.resource_tags.insert(arn, tags);
1211        }
1212
1213        self.restore_testing_plans.insert(name.to_string(), plan);
1214        Ok(self.restore_testing_plans.get(name).unwrap())
1215    }
1216
1217    pub fn get_restore_testing_plan(
1218        &self,
1219        name: &str,
1220    ) -> Result<&RestoreTestingPlanData, BackupError> {
1221        self.restore_testing_plans
1222            .get(name)
1223            .ok_or_else(|| BackupError::RestoreTestingPlanNotFound(name.to_string()))
1224    }
1225
1226    pub fn delete_restore_testing_plan(&mut self, name: &str) -> Result<(), BackupError> {
1227        if self.restore_testing_plans.remove(name).is_none() {
1228            return Err(BackupError::RestoreTestingPlanNotFound(name.to_string()));
1229        }
1230        // Also remove associated selections
1231        self.restore_testing_selections
1232            .retain(|(pn, _), _| pn != name);
1233        Ok(())
1234    }
1235
1236    pub fn list_restore_testing_plans(&self) -> Vec<&RestoreTestingPlanData> {
1237        self.restore_testing_plans.values().collect()
1238    }
1239
1240    pub fn update_restore_testing_plan(
1241        &mut self,
1242        name: &str,
1243        schedule_expression: Option<&str>,
1244        schedule_expression_timezone: Option<String>,
1245        start_window_hours: Option<i32>,
1246        recovery_point_selection: Option<serde_json::Value>,
1247    ) -> Result<&RestoreTestingPlanData, BackupError> {
1248        let plan = self
1249            .restore_testing_plans
1250            .get_mut(name)
1251            .ok_or_else(|| BackupError::RestoreTestingPlanNotFound(name.to_string()))?;
1252        if let Some(se) = schedule_expression {
1253            plan.schedule_expression = se.to_string();
1254        }
1255        if let Some(tz) = schedule_expression_timezone {
1256            plan.schedule_expression_timezone = Some(tz);
1257        }
1258        if let Some(swh) = start_window_hours {
1259            plan.start_window_hours = Some(swh);
1260        }
1261        if let Some(rps) = recovery_point_selection {
1262            plan.recovery_point_selection = rps;
1263        }
1264        plan.last_update_time = Utc::now();
1265        Ok(self.restore_testing_plans.get(name).unwrap())
1266    }
1267
1268    // --- Restore Testing Selection operations ---
1269
1270    #[allow(clippy::too_many_arguments)]
1271    pub fn create_restore_testing_selection(
1272        &mut self,
1273        plan_name: &str,
1274        selection_name: &str,
1275        iam_role_arn: &str,
1276        protected_resource_type: &str,
1277        protected_resource_arns: Vec<String>,
1278        protected_resource_conditions: serde_json::Value,
1279        restore_metadata_overrides: HashMap<String, String>,
1280        validation_window_hours: Option<i32>,
1281        creator_request_id: Option<String>,
1282    ) -> Result<&RestoreTestingSelectionData, BackupError> {
1283        let plan = self
1284            .restore_testing_plans
1285            .get(plan_name)
1286            .ok_or_else(|| BackupError::RestoreTestingPlanNotFound(plan_name.to_string()))?;
1287        let plan_arn = plan.restore_testing_plan_arn.clone();
1288
1289        let key = (plan_name.to_string(), selection_name.to_string());
1290        if self.restore_testing_selections.contains_key(&key) {
1291            return Err(BackupError::RestoreTestingSelectionAlreadyExists(
1292                selection_name.to_string(),
1293            ));
1294        }
1295
1296        let now = Utc::now();
1297        let sel = RestoreTestingSelectionData {
1298            restore_testing_selection_name: selection_name.to_string(),
1299            restore_testing_plan_name: plan_name.to_string(),
1300            restore_testing_plan_arn: plan_arn,
1301            iam_role_arn: iam_role_arn.to_string(),
1302            protected_resource_type: protected_resource_type.to_string(),
1303            protected_resource_arns,
1304            protected_resource_conditions,
1305            restore_metadata_overrides,
1306            validation_window_hours,
1307            creator_request_id,
1308            creation_time: now,
1309            last_update_time: now,
1310        };
1311
1312        self.restore_testing_selections.insert(key.clone(), sel);
1313        Ok(self.restore_testing_selections.get(&key).unwrap())
1314    }
1315
1316    pub fn get_restore_testing_selection(
1317        &self,
1318        plan_name: &str,
1319        selection_name: &str,
1320    ) -> Result<&RestoreTestingSelectionData, BackupError> {
1321        let key = (plan_name.to_string(), selection_name.to_string());
1322        self.restore_testing_selections
1323            .get(&key)
1324            .ok_or_else(|| BackupError::RestoreTestingSelectionNotFound(selection_name.to_string()))
1325    }
1326
1327    pub fn delete_restore_testing_selection(
1328        &mut self,
1329        plan_name: &str,
1330        selection_name: &str,
1331    ) -> Result<(), BackupError> {
1332        let key = (plan_name.to_string(), selection_name.to_string());
1333        if self.restore_testing_selections.remove(&key).is_none() {
1334            return Err(BackupError::RestoreTestingSelectionNotFound(
1335                selection_name.to_string(),
1336            ));
1337        }
1338        Ok(())
1339    }
1340
1341    pub fn list_restore_testing_selections(
1342        &self,
1343        plan_name: &str,
1344    ) -> Vec<&RestoreTestingSelectionData> {
1345        self.restore_testing_selections
1346            .values()
1347            .filter(|s| s.restore_testing_plan_name == plan_name)
1348            .collect()
1349    }
1350
1351    pub fn update_restore_testing_selection(
1352        &mut self,
1353        plan_name: &str,
1354        selection_name: &str,
1355        iam_role_arn: Option<&str>,
1356        protected_resource_arns: Option<Vec<String>>,
1357        protected_resource_conditions: Option<serde_json::Value>,
1358        restore_metadata_overrides: Option<HashMap<String, String>>,
1359        validation_window_hours: Option<i32>,
1360    ) -> Result<&RestoreTestingSelectionData, BackupError> {
1361        let key = (plan_name.to_string(), selection_name.to_string());
1362        let sel = self
1363            .restore_testing_selections
1364            .get_mut(&key)
1365            .ok_or_else(|| {
1366                BackupError::RestoreTestingSelectionNotFound(selection_name.to_string())
1367            })?;
1368        if let Some(role) = iam_role_arn {
1369            sel.iam_role_arn = role.to_string();
1370        }
1371        if let Some(arns) = protected_resource_arns {
1372            sel.protected_resource_arns = arns;
1373        }
1374        if let Some(conds) = protected_resource_conditions {
1375            sel.protected_resource_conditions = conds;
1376        }
1377        if let Some(overrides) = restore_metadata_overrides {
1378            sel.restore_metadata_overrides = overrides;
1379        }
1380        if let Some(vwh) = validation_window_hours {
1381            sel.validation_window_hours = Some(vwh);
1382        }
1383        sel.last_update_time = Utc::now();
1384        Ok(self.restore_testing_selections.get(&key).unwrap())
1385    }
1386
1387    // --- UpdateBackupPlan ---
1388
1389    pub fn update_backup_plan(
1390        &mut self,
1391        plan_id: &str,
1392        backup_plan_json: &serde_json::Value,
1393    ) -> Result<&BackupPlanData, BackupError> {
1394        let plan = self
1395            .backup_plans
1396            .get_mut(plan_id)
1397            .ok_or_else(|| BackupError::PlanNotFound(plan_id.to_string()))?;
1398
1399        // Enrich rules with RuleId if missing
1400        let enriched_json =
1401            if let Some(rules_arr) = backup_plan_json.get("Rules").and_then(|v| v.as_array()) {
1402                let enriched_rules: Vec<serde_json::Value> = rules_arr
1403                    .iter()
1404                    .map(|rule| {
1405                        let mut r = rule.clone();
1406                        if let serde_json::Value::Object(ref mut map) = r {
1407                            map.entry("RuleId").or_insert_with(|| {
1408                                serde_json::Value::String(Uuid::new_v4().to_string())
1409                            });
1410                        }
1411                        r
1412                    })
1413                    .collect();
1414                let mut enriched = backup_plan_json.clone();
1415                if let serde_json::Value::Object(ref mut map) = enriched {
1416                    map.insert(
1417                        "Rules".to_string(),
1418                        serde_json::Value::Array(enriched_rules),
1419                    );
1420                }
1421                enriched
1422            } else {
1423                backup_plan_json.clone()
1424            };
1425
1426        plan.backup_plan_json = enriched_json;
1427        plan.version_id = Uuid::new_v4().to_string();
1428        if let Some(name) = backup_plan_json
1429            .get("BackupPlanName")
1430            .and_then(|v| v.as_str())
1431        {
1432            plan.backup_plan_name = name.to_string();
1433        }
1434
1435        Ok(self.backup_plans.get(plan_id).unwrap())
1436    }
1437}