ddex_builder/messages/
update_release.rs

1//! UpdateReleaseMessage implementation for DDEX Builder
2//!
3//! This module provides support for generating and applying incremental DDEX updates
4//! using the UpdateReleaseMessage format, allowing efficient partial updates to
5//! existing DDEX messages without resending entire catalogs.
6
7use crate::builder::MessageHeaderRequest;
8use crate::diff::types::{ChangeSet, ChangeType, SemanticChange};
9use crate::diff::DiffEngine;
10use crate::error::BuildError;
11use chrono::{DateTime, Utc};
12use indexmap::{IndexMap, IndexSet};
13use serde::{Deserialize, Serialize};
14
15/// Complete UpdateReleaseMessage structure
16///
17/// Represents a complete DDEX UpdateReleaseMessage that describes incremental
18/// changes to an existing DDEX message. This allows efficient partial updates
19/// without resending entire catalogs.
20///
21/// # Example
22/// ```
23/// use ddex_builder::messages::update_release::UpdateReleaseMessage;
24/// use ddex_builder::builder::MessageHeaderRequest;
25/// use indexmap::IndexMap;
26///
27/// // Create an update message to modify a release title
28/// let update_message = UpdateReleaseMessage {
29///     header: MessageHeaderRequest {
30///         message_id: Some("UPD-MSG-20241215-001".to_string()),
31///         // ... other header fields
32///         # message_sender: Default::default(),
33///         # message_recipient: Default::default(),
34///         # message_control_type: Some("UpdateMessage".to_string()),
35///         # message_created_date_time: None,
36///     },
37///     update_list: vec![
38///         // UpdateOperation to change release title
39///         # UpdateOperation {
40///         #     operation_id: "OP000001".to_string(),
41///         #     action: UpdateAction::Replace,
42///         #     target_path: "/Release[@ReleaseId='R001']/Title".to_string(),
43///         #     entity_type: EntityType::Release,
44///         #     entity_id: "R001".to_string(),
45///         #     old_value: Some("Old Title".to_string()),
46///         #     new_value: Some("New Title".to_string()),
47///         #     is_critical: false,
48///         #     description: "Update release title".to_string(),
49///         #     dependencies: vec![],
50///         # }
51///     ],
52///     resource_updates: IndexMap::new(),
53///     release_updates: IndexMap::new(),
54///     deal_updates: IndexMap::new(),
55///     update_metadata: UpdateMetadata {
56///         original_message_id: "ORIG-MSG-001".to_string(),
57///         # original_message_version: None,
58///         # original_message_timestamp: None,
59///         # update_created_timestamp: chrono::Utc::now(),
60///         # update_sequence: 1,
61///         # total_operations: 1,
62///         # impact_level: "Low".to_string(),
63///         # validation_status: ValidationStatus::Pending,
64///         # custom_metadata: IndexMap::new(),
65///     },
66/// };
67/// ```
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct UpdateReleaseMessage {
70    /// Message header containing sender, recipient, and timing information
71    pub header: MessageHeaderRequest,
72
73    /// Ordered list of update operations to perform on the target message
74    pub update_list: Vec<UpdateOperation>,
75
76    /// Resource-specific updates indexed by resource ID
77    pub resource_updates: IndexMap<String, ResourceUpdate>,
78
79    /// Release-specific updates indexed by release ID
80    pub release_updates: IndexMap<String, ReleaseUpdate>,
81
82    /// Deal-specific updates indexed by deal ID (optional)
83    pub deal_updates: IndexMap<String, DealUpdate>,
84
85    /// Metadata describing this update operation
86    pub update_metadata: UpdateMetadata,
87}
88
89/// Individual update operation
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct UpdateOperation {
92    /// Unique identifier for this update operation
93    pub operation_id: String,
94
95    /// Type of update action
96    pub action: UpdateAction,
97
98    /// Path to the element being updated
99    pub target_path: String,
100
101    /// Entity type being updated
102    pub entity_type: EntityType,
103
104    /// Entity ID being updated
105    pub entity_id: String,
106
107    /// Old value (for Replace operations)
108    pub old_value: Option<String>,
109
110    /// New value (for Add/Replace operations)
111    pub new_value: Option<String>,
112
113    /// Whether this update is critical
114    pub is_critical: bool,
115
116    /// Human-readable description
117    pub description: String,
118
119    /// Dependencies on other operations
120    pub dependencies: Vec<String>,
121}
122
123/// Types of update actions
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
125pub enum UpdateAction {
126    /// Add a new element or attribute
127    Add,
128    /// Delete an existing element or attribute
129    Delete,
130    /// Replace an existing element or attribute
131    Replace,
132    /// Move an element to a different location
133    Move,
134}
135
136/// Entity types that can be updated
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
138pub enum EntityType {
139    /// Sound recording resource
140    Resource,
141    /// Release
142    Release,
143    /// Deal
144    Deal,
145    /// Party information
146    Party,
147    /// Message metadata
148    Metadata,
149}
150
151/// Resource update information
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct ResourceUpdate {
154    /// Resource identifier
155    pub resource_id: String,
156
157    /// Resource reference (may change)
158    pub resource_reference: String,
159
160    /// Update action for this resource
161    pub action: UpdateAction,
162
163    /// Updated resource data (for Add/Replace)
164    pub resource_data: Option<ResourceData>,
165
166    /// Technical details updates
167    pub technical_updates: Vec<TechnicalUpdate>,
168
169    /// Metadata updates
170    pub metadata_updates: IndexMap<String, String>,
171}
172
173/// Release update information
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct ReleaseUpdate {
176    /// Release identifier
177    pub release_id: String,
178
179    /// Release reference (may change)
180    pub release_reference: String,
181
182    /// Update action for this release
183    pub action: UpdateAction,
184
185    /// Updated release data (for Add/Replace)
186    pub release_data: Option<ReleaseData>,
187
188    /// Track updates within this release
189    pub track_updates: Vec<TrackUpdate>,
190
191    /// Resource reference updates
192    pub resource_reference_updates: Vec<ReferenceUpdate>,
193
194    /// Metadata updates
195    pub metadata_updates: IndexMap<String, String>,
196}
197
198/// Deal update information
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct DealUpdate {
201    /// Deal identifier
202    pub deal_id: String,
203
204    /// Deal reference
205    pub deal_reference: String,
206
207    /// Update action for this deal
208    pub action: UpdateAction,
209
210    /// Updated deal data
211    pub deal_data: Option<DealData>,
212
213    /// Terms updates
214    pub terms_updates: Vec<TermsUpdate>,
215}
216
217/// Updated resource information
218///
219/// Represents resource data in DDEX update messages, containing all the
220/// metadata needed to describe a sound recording, video, or other resource
221/// that has been modified in an UpdateReleaseMessage.
222///
223/// # Example
224/// ```
225/// use ddex_builder::messages::update_release::{UpdatedResource, TechnicalDetails};
226///
227/// let resource = UpdatedResource {
228///     resource_type: "SoundRecording".to_string(),
229///     title: "Bohemian Rhapsody (Remastered)".to_string(),
230///     artist: "Queen".to_string(),
231///     isrc: Some("GBUM71505078".to_string()),
232///     duration: Some("PT5M55S".to_string()), // 5 minutes 55 seconds
233///     file_path: Some("/audio/queen/bohemian_rhapsody_remastered.flac".to_string()),
234///     technical_details: Some(TechnicalDetails {
235///         file_name: Some("bohemian_rhapsody_remastered.flac".to_string()),
236///         codec_type: Some("FLAC".to_string()),
237///         bit_rate: Some("1411".to_string()), // kbps
238///         sample_rate: Some("44100".to_string()), // Hz
239///     }),
240/// };
241/// ```
242#[derive(Debug, Clone)]
243pub struct UpdatedResource {
244    /// Type of resource (SoundRecording, Video, Image, Text, etc.)
245    pub resource_type: String,
246    /// Title of the resource/track
247    pub title: String,
248    /// Artist name for this resource
249    pub artist: String,
250    /// International Standard Recording Code (12-character alphanumeric)
251    pub isrc: Option<String>,
252    /// Duration in ISO 8601 format (e.g., "PT3M45S" for 3 minutes 45 seconds)
253    pub duration: Option<String>,
254    /// File path or URL to the resource file
255    pub file_path: Option<String>,
256    /// Technical details of the resource (codec, bitrate, etc.)
257    pub technical_details: Option<TechnicalDetails>,
258}
259
260/// Resource data for updates
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct ResourceData {
263    /// Type of resource (SoundRecording, Video, Image, etc.)
264    pub resource_type: String,
265    /// Title of the resource
266    pub title: String,
267    /// Artist name
268    pub artist: String,
269    /// International Standard Recording Code
270    pub isrc: Option<String>,
271    /// Duration in ISO 8601 format
272    pub duration: Option<String>,
273    /// File path or URL
274    pub file_path: Option<String>,
275    /// Technical details
276    pub technical_details: Option<TechnicalDetails>,
277}
278
279/// Updated release information
280///
281/// Represents release data in DDEX update messages, containing all the
282/// metadata needed to describe a music release (album, single, EP, etc.)
283/// that has been modified in an UpdateReleaseMessage.
284///
285/// # Example
286/// ```
287/// use ddex_builder::messages::update_release::UpdatedRelease;
288///
289/// let release = UpdatedRelease {
290///     release_type: "Album".to_string(),
291///     title: "The Greatest Hits (Deluxe Edition)".to_string(),
292///     artist: "The Beatles".to_string(),
293///     label: Some("Apple Records".to_string()),
294///     upc: Some("602537518739".to_string()),
295///     release_date: Some("2024-01-15".to_string()),
296///     genre: Some("Rock".to_string()),
297///     resource_references: vec![
298///         "R12345678".to_string(),
299///         "R87654321".to_string(),
300///         "R11223344".to_string(),
301///     ],
302/// };
303/// ```
304#[derive(Debug, Clone)]
305pub struct UpdatedRelease {
306    /// Release type (Album, Single, EP, Compilation, etc.)
307    pub release_type: String,
308    /// Title of the release
309    pub title: String,
310    /// Main artist name for the release
311    pub artist: String,
312    /// Record label name
313    pub label: Option<String>,
314    /// Universal Product Code (12-digit barcode identifier)
315    pub upc: Option<String>,
316    /// Release date in YYYY-MM-DD format
317    pub release_date: Option<String>,
318    /// Primary genre classification
319    pub genre: Option<String>,
320    /// References to included resources (tracks/recordings)
321    pub resource_references: Vec<String>,
322}
323
324/// Release data for updates
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct ReleaseData {
327    /// Release type (Single, Album, EP, etc.)
328    pub release_type: String,
329    /// Release title
330    pub title: String,
331    /// Main artist name
332    pub artist: String,
333    /// Record label name
334    pub label: Option<String>,
335    /// Universal Product Code
336    pub upc: Option<String>,
337    /// Release date in YYYY-MM-DD format
338    pub release_date: Option<String>,
339    /// Primary genre
340    pub genre: Option<String>,
341    /// References to included resources
342    pub resource_references: Vec<String>,
343}
344
345/// Deal data for updates
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct DealData {
348    /// Type of commercial model (e.g., "PayAsYouGoModel")
349    pub commercial_model_type: String,
350    /// Territory codes where deal applies
351    pub territory_codes: Vec<String>,
352    /// Deal start date in YYYY-MM-DD format
353    pub start_date: Option<String>,
354    /// Deal end date in YYYY-MM-DD format
355    pub end_date: Option<String>,
356    /// Pricing information
357    pub price: Option<PriceData>,
358}
359
360/// Field update details
361#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct TechnicalUpdate {
363    /// Name of the field being updated
364    pub field_name: String,
365    /// Previous value
366    pub old_value: Option<String>,
367    /// New value
368    pub new_value: Option<String>,
369    /// Type of update action
370    pub update_action: UpdateAction,
371}
372
373/// Track update within a release
374#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct TrackUpdate {
376    /// Unique track identifier
377    pub track_id: String,
378    /// Update action to perform
379    pub action: UpdateAction,
380    /// Previous resource reference
381    pub old_resource_reference: Option<String>,
382    /// New resource reference
383    pub new_resource_reference: Option<String>,
384    /// Position change information
385    pub position_change: Option<PositionChange>,
386}
387
388/// Reference update information
389#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct ReferenceUpdate {
391    /// Previous reference value
392    pub old_reference: String,
393    /// New reference value
394    pub new_reference: String,
395    /// Type of reference being updated
396    pub reference_type: String,
397    /// Reason for the update
398    pub update_reason: String,
399}
400
401/// Terms update for deals
402#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct TermsUpdate {
404    /// Name of the field being updated
405    pub field_name: String,
406    /// Previous value
407    pub old_value: Option<String>,
408    /// New value
409    pub new_value: Option<String>,
410    /// Date when update becomes effective
411    pub effective_date: Option<String>,
412}
413
414/// Position change information
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct PositionChange {
417    /// Previous position in sequence
418    pub old_position: usize,
419    /// New position in sequence
420    pub new_position: usize,
421}
422
423/// Technical details for resources
424///
425/// Contains technical specifications for audio/video resources,
426/// including encoding information and quality parameters.
427///
428/// # Example
429/// ```
430/// use ddex_builder::messages::update_release::TechnicalDetails;
431///
432/// let tech_details = TechnicalDetails {
433///     file_name: Some("track_01_mastered.flac".to_string()),
434///     codec_type: Some("FLAC".to_string()),
435///     bit_rate: Some("1411".to_string()), // kbps for lossless
436///     sample_rate: Some("44100".to_string()), // Hz (CD quality)
437/// };
438///
439/// let mp3_details = TechnicalDetails {
440///     file_name: Some("track_01_compressed.mp3".to_string()),
441///     codec_type: Some("MP3".to_string()),
442///     bit_rate: Some("320".to_string()), // kbps
443///     sample_rate: Some("44100".to_string()), // Hz
444/// };
445/// ```
446#[derive(Debug, Clone, Serialize, Deserialize)]
447pub struct TechnicalDetails {
448    /// File name of the resource
449    pub file_name: Option<String>,
450    /// Audio/video codec type (e.g., "MP3", "FLAC", "AAC", "H.264")
451    pub codec_type: Option<String>,
452    /// Bit rate in kilobits per second (kbps)
453    pub bit_rate: Option<String>,
454    /// Sample rate in Hz (e.g., "44100", "48000", "96000")
455    pub sample_rate: Option<String>,
456}
457
458/// Pricing data
459#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct PriceData {
461    /// Price amount
462    pub amount: String,
463    /// ISO currency code
464    pub currency_code: String,
465    /// Type of price (e.g., "WholesalePrice")
466    pub price_type: Option<String>,
467}
468
469/// Metadata about the update
470#[derive(Debug, Clone, Serialize, Deserialize)]
471pub struct UpdateMetadata {
472    /// Original message ID being updated
473    pub original_message_id: String,
474
475    /// Version of the original message
476    pub original_message_version: Option<String>,
477
478    /// Timestamp of the original message
479    pub original_message_timestamp: Option<DateTime<Utc>>,
480
481    /// Update creation timestamp
482    pub update_created_timestamp: DateTime<Utc>,
483
484    /// Update sequence number (for ordering)
485    pub update_sequence: u64,
486
487    /// Total number of operations in this update
488    pub total_operations: usize,
489
490    /// Estimated impact level
491    pub impact_level: String,
492
493    /// Update validation status
494    pub validation_status: ValidationStatus,
495
496    /// Additional metadata
497    pub custom_metadata: IndexMap<String, String>,
498}
499
500/// Validation status for updates
501#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
502pub enum ValidationStatus {
503    /// Update has been validated and is safe to apply
504    Validated,
505    /// Update has validation warnings but can be applied
506    WarningsOnly,
507    /// Update has validation errors and should not be applied
508    Invalid,
509    /// Update validation is pending
510    Pending,
511}
512
513/// Configuration for update generation
514#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct UpdateConfig {
516    /// Include non-critical changes in updates
517    pub include_non_critical: bool,
518
519    /// Maximum number of operations per update message
520    pub max_operations_per_update: usize,
521
522    /// Whether to validate references during update generation
523    pub validate_references: bool,
524
525    /// Whether to optimize reference updates
526    pub optimize_references: bool,
527
528    /// Fields to exclude from updates
529    pub excluded_fields: IndexSet<String>,
530
531    /// Custom update priorities
532    pub update_priorities: IndexMap<String, u8>,
533}
534
535impl Default for UpdateConfig {
536    fn default() -> Self {
537        let mut excluded_fields = IndexSet::new();
538        excluded_fields.insert("MessageId".to_string());
539        excluded_fields.insert("MessageCreatedDateTime".to_string());
540
541        Self {
542            include_non_critical: true,
543            max_operations_per_update: 1000,
544            validate_references: true,
545            optimize_references: true,
546            excluded_fields,
547            update_priorities: IndexMap::new(),
548        }
549    }
550}
551
552/// Update generation engine
553pub struct UpdateGenerator {
554    config: UpdateConfig,
555    diff_engine: DiffEngine,
556    operation_counter: u64,
557}
558
559impl UpdateGenerator {
560    /// Create a new update generator
561    pub fn new() -> Self {
562        Self {
563            config: UpdateConfig::default(),
564            diff_engine: DiffEngine::new(),
565            operation_counter: 0,
566        }
567    }
568
569    /// Create a new update generator with custom configuration
570    pub fn new_with_config(config: UpdateConfig) -> Self {
571        Self {
572            config,
573            diff_engine: DiffEngine::new(),
574            operation_counter: 0,
575        }
576    }
577
578    /// Generate an UpdateReleaseMessage from two DDEX messages
579    pub fn create_update(
580        &mut self,
581        original_xml: &str,
582        updated_xml: &str,
583        original_message_id: &str,
584    ) -> Result<UpdateReleaseMessage, BuildError> {
585        // Parse both messages to ASTs
586        let original_ast = self.parse_xml_to_ast(original_xml)?;
587        let updated_ast = self.parse_xml_to_ast(updated_xml)?;
588
589        // Generate semantic diff
590        let changeset = self.diff_engine.diff(&original_ast, &updated_ast)?;
591
592        // Convert changeset to update operations
593        let update_operations = self.changeset_to_operations(&changeset)?;
594
595        // Group operations by entity type
596        let (resource_updates, release_updates, deal_updates) =
597            self.group_operations_by_entity(&update_operations)?;
598
599        // Create update metadata
600        let metadata =
601            self.create_update_metadata(original_message_id, &update_operations, &changeset);
602
603        // Generate message header
604        let header = self.create_update_header(original_message_id, &metadata);
605
606        let update_message = UpdateReleaseMessage {
607            header,
608            update_list: update_operations,
609            resource_updates,
610            release_updates,
611            deal_updates,
612            update_metadata: metadata,
613        };
614
615        // Validate the update
616        self.validate_update(&update_message)?;
617
618        Ok(update_message)
619    }
620
621    /// Apply an update to a base message to produce a new complete message
622    pub fn apply_update(
623        &self,
624        base_xml: &str,
625        update: &UpdateReleaseMessage,
626    ) -> Result<String, BuildError> {
627        // Parse base message
628        let mut base_ast = self.parse_xml_to_ast(base_xml)?;
629
630        // Apply each operation in dependency order
631        let ordered_operations = self.order_operations_by_dependencies(&update.update_list)?;
632
633        for operation in &ordered_operations {
634            self.apply_operation_to_ast(&mut base_ast, operation)?;
635        }
636
637        // Apply entity-level updates
638        self.apply_resource_updates(&mut base_ast, &update.resource_updates)?;
639        self.apply_release_updates(&mut base_ast, &update.release_updates)?;
640        self.apply_deal_updates(&mut base_ast, &update.deal_updates)?;
641
642        // Serialize back to XML
643        self.ast_to_xml(&base_ast)
644    }
645
646    /// Validate an update for consistency and safety
647    pub fn validate_update(
648        &self,
649        update: &UpdateReleaseMessage,
650    ) -> Result<ValidationStatus, BuildError> {
651        let mut errors = Vec::new();
652        let mut warnings = Vec::new();
653
654        // Validate operations
655        for operation in &update.update_list {
656            if let Err(e) = self.validate_operation(operation, update) {
657                errors.push(format!("Operation {}: {}", operation.operation_id, e));
658            }
659        }
660
661        // Validate references
662        if self.config.validate_references {
663            if let Err(e) = self.validate_references(update) {
664                errors.push(format!("Reference validation: {}", e));
665            }
666        }
667
668        // Validate dependencies
669        if let Err(e) = self.validate_dependencies(&update.update_list) {
670            errors.push(format!("Dependency validation: {}", e));
671        }
672
673        // Check for conflicts
674        let conflicts = self.detect_conflicts(&update.update_list)?;
675        if !conflicts.is_empty() {
676            warnings.push(format!("Found {} potential conflicts", conflicts.len()));
677        }
678
679        if !errors.is_empty() {
680            Err(BuildError::ValidationFailed { errors })
681        } else if !warnings.is_empty() {
682            Ok(ValidationStatus::WarningsOnly)
683        } else {
684            Ok(ValidationStatus::Validated)
685        }
686    }
687
688    // Private helper methods
689
690    fn parse_xml_to_ast(&self, xml: &str) -> Result<crate::ast::AST, BuildError> {
691        // Simplified XML parsing - in production, use proper DDEX parser
692        let root = crate::ast::Element::new("NewReleaseMessage").with_text(xml);
693        Ok(crate::ast::AST {
694            root,
695            namespaces: IndexMap::new(),
696            schema_location: None,
697        })
698    }
699
700    fn changeset_to_operations(
701        &mut self,
702        changeset: &ChangeSet,
703    ) -> Result<Vec<UpdateOperation>, BuildError> {
704        let mut operations = Vec::new();
705
706        for change in &changeset.changes {
707            let operation = self.semantic_change_to_operation(change)?;
708            operations.push(operation);
709        }
710
711        Ok(operations)
712    }
713
714    fn semantic_change_to_operation(
715        &mut self,
716        change: &SemanticChange,
717    ) -> Result<UpdateOperation, BuildError> {
718        self.operation_counter += 1;
719
720        let action = match change.change_type {
721            ChangeType::ElementAdded | ChangeType::AttributeAdded => UpdateAction::Add,
722            ChangeType::ElementRemoved | ChangeType::AttributeRemoved => UpdateAction::Delete,
723            ChangeType::ElementMoved => UpdateAction::Move,
724            _ => UpdateAction::Replace,
725        };
726
727        let entity_type = self.determine_entity_type(&change.path);
728        let entity_id = self.extract_entity_id(&change.path)?;
729
730        Ok(UpdateOperation {
731            operation_id: format!("OP{:06}", self.operation_counter),
732            action,
733            target_path: change.path.to_string(),
734            entity_type,
735            entity_id,
736            old_value: change.old_value.clone(),
737            new_value: change.new_value.clone(),
738            is_critical: change.is_critical,
739            description: change.description.clone(),
740            dependencies: Vec::new(), // Will be filled in later pass
741        })
742    }
743
744    fn determine_entity_type(&self, path: &crate::diff::types::DiffPath) -> EntityType {
745        let path_str = path.to_string().to_lowercase();
746
747        if path_str.contains("resource") {
748            EntityType::Resource
749        } else if path_str.contains("release") {
750            EntityType::Release
751        } else if path_str.contains("deal") {
752            EntityType::Deal
753        } else if path_str.contains("party") {
754            EntityType::Party
755        } else {
756            EntityType::Metadata
757        }
758    }
759
760    fn extract_entity_id(&self, path: &crate::diff::types::DiffPath) -> Result<String, BuildError> {
761        // Extract entity ID from path - simplified implementation
762        let path_str = path.to_string();
763        if let Some(id_start) = path_str.find("Id=") {
764            let id_part = &path_str[id_start + 3..];
765            if let Some(id_end) = id_part.find(&[']', '/', '@'][..]) {
766                Ok(id_part[..id_end].to_string())
767            } else {
768                Ok(id_part.to_string())
769            }
770        } else {
771            let uuid_str = uuid::Uuid::new_v4().to_string();
772            Ok(format!("unknown_{}", &uuid_str[..8]))
773        }
774    }
775
776    fn group_operations_by_entity(
777        &self,
778        operations: &[UpdateOperation],
779    ) -> Result<
780        (
781            IndexMap<String, ResourceUpdate>,
782            IndexMap<String, ReleaseUpdate>,
783            IndexMap<String, DealUpdate>,
784        ),
785        BuildError,
786    > {
787        let mut resource_updates = IndexMap::new();
788        let mut release_updates = IndexMap::new();
789        let mut deal_updates = IndexMap::new();
790
791        for operation in operations {
792            match operation.entity_type {
793                EntityType::Resource => {
794                    let resource_update = self.operation_to_resource_update(operation)?;
795                    resource_updates.insert(operation.entity_id.clone(), resource_update);
796                }
797                EntityType::Release => {
798                    let release_update = self.operation_to_release_update(operation)?;
799                    release_updates.insert(operation.entity_id.clone(), release_update);
800                }
801                EntityType::Deal => {
802                    let deal_update = self.operation_to_deal_update(operation)?;
803                    deal_updates.insert(operation.entity_id.clone(), deal_update);
804                }
805                _ => {} // Handle metadata and party updates separately if needed
806            }
807        }
808
809        Ok((resource_updates, release_updates, deal_updates))
810    }
811
812    fn operation_to_resource_update(
813        &self,
814        operation: &UpdateOperation,
815    ) -> Result<ResourceUpdate, BuildError> {
816        Ok(ResourceUpdate {
817            resource_id: operation.entity_id.clone(),
818            resource_reference: format!(
819                "R{:06}",
820                operation.operation_id[2..].parse::<u32>().unwrap_or(0)
821            ),
822            action: operation.action,
823            resource_data: None, // Would be populated from operation details
824            technical_updates: Vec::new(),
825            metadata_updates: IndexMap::new(),
826        })
827    }
828
829    fn operation_to_release_update(
830        &self,
831        operation: &UpdateOperation,
832    ) -> Result<ReleaseUpdate, BuildError> {
833        Ok(ReleaseUpdate {
834            release_id: operation.entity_id.clone(),
835            release_reference: format!(
836                "REL{:06}",
837                operation.operation_id[2..].parse::<u32>().unwrap_or(0)
838            ),
839            action: operation.action,
840            release_data: None, // Would be populated from operation details
841            track_updates: Vec::new(),
842            resource_reference_updates: Vec::new(),
843            metadata_updates: IndexMap::new(),
844        })
845    }
846
847    fn operation_to_deal_update(
848        &self,
849        operation: &UpdateOperation,
850    ) -> Result<DealUpdate, BuildError> {
851        Ok(DealUpdate {
852            deal_id: operation.entity_id.clone(),
853            deal_reference: format!(
854                "D{:06}",
855                operation.operation_id[2..].parse::<u32>().unwrap_or(0)
856            ),
857            action: operation.action,
858            deal_data: None, // Would be populated from operation details
859            terms_updates: Vec::new(),
860        })
861    }
862
863    fn create_update_metadata(
864        &self,
865        original_message_id: &str,
866        operations: &[UpdateOperation],
867        changeset: &ChangeSet,
868    ) -> UpdateMetadata {
869        UpdateMetadata {
870            original_message_id: original_message_id.to_string(),
871            original_message_version: None,
872            original_message_timestamp: None,
873            update_created_timestamp: Utc::now(),
874            update_sequence: 1,
875            total_operations: operations.len(),
876            impact_level: changeset.impact_level().to_string(),
877            validation_status: ValidationStatus::Pending,
878            custom_metadata: IndexMap::new(),
879        }
880    }
881
882    fn create_update_header(
883        &self,
884        original_message_id: &str,
885        metadata: &UpdateMetadata,
886    ) -> MessageHeaderRequest {
887        MessageHeaderRequest {
888            message_id: Some(format!(
889                "UPD-{}-{:04}",
890                original_message_id, metadata.update_sequence
891            )),
892            message_sender: crate::builder::PartyRequest {
893                party_name: vec![crate::builder::LocalizedStringRequest {
894                    text: "DDEX Builder Update Engine".to_string(),
895                    language_code: None,
896                }],
897                party_id: None,
898                party_reference: None,
899            },
900            message_recipient: crate::builder::PartyRequest {
901                party_name: vec![crate::builder::LocalizedStringRequest {
902                    text: "Update Recipient".to_string(),
903                    language_code: None,
904                }],
905                party_id: None,
906                party_reference: None,
907            },
908            message_control_type: Some("UpdateMessage".to_string()),
909            message_created_date_time: Some(metadata.update_created_timestamp.to_rfc3339()),
910        }
911    }
912
913    fn order_operations_by_dependencies(
914        &self,
915        operations: &[UpdateOperation],
916    ) -> Result<Vec<UpdateOperation>, BuildError> {
917        // Simplified topological sort - in production, implement proper dependency resolution
918        let mut ordered = operations.to_vec();
919
920        // Sort by operation ID as a simple ordering
921        ordered.sort_by(|a, b| a.operation_id.cmp(&b.operation_id));
922
923        Ok(ordered)
924    }
925
926    fn apply_operation_to_ast(
927        &self,
928        _ast: &mut crate::ast::AST,
929        operation: &UpdateOperation,
930    ) -> Result<(), BuildError> {
931        // Simplified AST modification - in production, implement proper AST updates
932        match operation.action {
933            UpdateAction::Add => {
934                // Add logic here
935            }
936            UpdateAction::Delete => {
937                // Delete logic here
938            }
939            UpdateAction::Replace => {
940                // Replace logic here
941            }
942            UpdateAction::Move => {
943                // Move logic here
944            }
945        }
946        Ok(())
947    }
948
949    fn apply_resource_updates(
950        &self,
951        _ast: &mut crate::ast::AST,
952        updates: &IndexMap<String, ResourceUpdate>,
953    ) -> Result<(), BuildError> {
954        // Apply resource-specific updates
955        for (_resource_id, _update) in updates {
956            // Implementation would modify AST based on resource update
957        }
958        Ok(())
959    }
960
961    fn apply_release_updates(
962        &self,
963        _ast: &mut crate::ast::AST,
964        updates: &IndexMap<String, ReleaseUpdate>,
965    ) -> Result<(), BuildError> {
966        // Apply release-specific updates
967        for (_release_id, _update) in updates {
968            // Implementation would modify AST based on release update
969        }
970        Ok(())
971    }
972
973    fn apply_deal_updates(
974        &self,
975        _ast: &mut crate::ast::AST,
976        updates: &IndexMap<String, DealUpdate>,
977    ) -> Result<(), BuildError> {
978        // Apply deal-specific updates
979        for (_deal_id, _update) in updates {
980            // Implementation would modify AST based on deal update
981        }
982        Ok(())
983    }
984
985    fn ast_to_xml(&self, _ast: &crate::ast::AST) -> Result<String, BuildError> {
986        // Simplified XML serialization - in production, use proper XML generation
987        Ok(format!(
988            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- Updated DDEX Message -->\n"
989        ))
990    }
991
992    fn validate_operation(
993        &self,
994        operation: &UpdateOperation,
995        _update: &UpdateReleaseMessage,
996    ) -> Result<(), BuildError> {
997        // Validate individual operation
998        if operation.entity_id.is_empty() {
999            return Err(BuildError::InvalidFormat {
1000                field: "entity_id".to_string(),
1001                message: "Entity ID cannot be empty".to_string(),
1002            });
1003        }
1004
1005        // Validate action compatibility
1006        match operation.action {
1007            UpdateAction::Add => {
1008                if operation.new_value.is_none() {
1009                    return Err(BuildError::InvalidFormat {
1010                        field: "new_value".to_string(),
1011                        message: "Add operation requires new_value".to_string(),
1012                    });
1013                }
1014            }
1015            UpdateAction::Delete => {
1016                if operation.old_value.is_none() {
1017                    return Err(BuildError::InvalidFormat {
1018                        field: "old_value".to_string(),
1019                        message: "Delete operation requires old_value".to_string(),
1020                    });
1021                }
1022            }
1023            UpdateAction::Replace => {
1024                if operation.old_value.is_none() || operation.new_value.is_none() {
1025                    return Err(BuildError::InvalidFormat {
1026                        field: "values".to_string(),
1027                        message: "Replace operation requires both old_value and new_value"
1028                            .to_string(),
1029                    });
1030                }
1031            }
1032            UpdateAction::Move => {
1033                // Move operations have specific validation requirements
1034            }
1035        }
1036
1037        Ok(())
1038    }
1039
1040    fn validate_references(&self, update: &UpdateReleaseMessage) -> Result<(), BuildError> {
1041        // Validate that all referenced entities exist
1042        let mut referenced_resources = IndexSet::new();
1043        let mut referenced_releases = IndexSet::new();
1044
1045        // Collect all references
1046        for operation in &update.update_list {
1047            match operation.entity_type {
1048                EntityType::Resource => {
1049                    referenced_resources.insert(operation.entity_id.clone());
1050                }
1051                EntityType::Release => {
1052                    referenced_releases.insert(operation.entity_id.clone());
1053                }
1054                _ => {}
1055            }
1056        }
1057
1058        // Check that referenced entities have corresponding updates
1059        for resource_id in &referenced_resources {
1060            if !update.resource_updates.contains_key(resource_id) {
1061                return Err(BuildError::InvalidReference {
1062                    reference: resource_id.clone(),
1063                });
1064            }
1065        }
1066
1067        Ok(())
1068    }
1069
1070    fn validate_dependencies(&self, operations: &[UpdateOperation]) -> Result<(), BuildError> {
1071        let operation_ids: IndexSet<_> = operations.iter().map(|op| &op.operation_id).collect();
1072
1073        for operation in operations {
1074            for dependency in &operation.dependencies {
1075                if !operation_ids.contains(&dependency) {
1076                    return Err(BuildError::InvalidReference {
1077                        reference: format!("Missing dependency: {}", dependency),
1078                    });
1079                }
1080            }
1081        }
1082
1083        Ok(())
1084    }
1085
1086    fn detect_conflicts(&self, operations: &[UpdateOperation]) -> Result<Vec<String>, BuildError> {
1087        let mut conflicts = Vec::new();
1088
1089        // Check for operations targeting the same path
1090        let mut path_operations: IndexMap<String, Vec<&UpdateOperation>> = IndexMap::new();
1091
1092        for operation in operations {
1093            path_operations
1094                .entry(operation.target_path.clone())
1095                .or_default()
1096                .push(operation);
1097        }
1098
1099        for (path, ops) in path_operations {
1100            if ops.len() > 1 {
1101                let conflicting_ops: Vec<_> = ops.iter().map(|op| &op.operation_id).collect();
1102                conflicts.push(format!(
1103                    "Path {} has conflicting operations: {:?}",
1104                    path, conflicting_ops
1105                ));
1106            }
1107        }
1108
1109        Ok(conflicts)
1110    }
1111}
1112
1113impl Default for UpdateGenerator {
1114    fn default() -> Self {
1115        Self::new()
1116    }
1117}
1118
1119// Display implementations for better debugging
1120impl std::fmt::Display for UpdateAction {
1121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1122        match self {
1123            UpdateAction::Add => write!(f, "Add"),
1124            UpdateAction::Delete => write!(f, "Delete"),
1125            UpdateAction::Replace => write!(f, "Replace"),
1126            UpdateAction::Move => write!(f, "Move"),
1127        }
1128    }
1129}
1130
1131impl std::fmt::Display for EntityType {
1132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1133        match self {
1134            EntityType::Resource => write!(f, "Resource"),
1135            EntityType::Release => write!(f, "Release"),
1136            EntityType::Deal => write!(f, "Deal"),
1137            EntityType::Party => write!(f, "Party"),
1138            EntityType::Metadata => write!(f, "Metadata"),
1139        }
1140    }
1141}
1142
1143impl std::fmt::Display for ValidationStatus {
1144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1145        match self {
1146            ValidationStatus::Validated => write!(f, "Validated"),
1147            ValidationStatus::WarningsOnly => write!(f, "Warnings Only"),
1148            ValidationStatus::Invalid => write!(f, "Invalid"),
1149            ValidationStatus::Pending => write!(f, "Pending"),
1150        }
1151    }
1152}
1153
1154#[cfg(test)]
1155mod tests {
1156    use super::*;
1157
1158    #[test]
1159    fn test_update_generator_creation() {
1160        let generator = UpdateGenerator::new();
1161        assert_eq!(generator.operation_counter, 0);
1162    }
1163
1164    #[test]
1165    fn test_update_config_defaults() {
1166        let config = UpdateConfig::default();
1167        assert!(config.include_non_critical);
1168        assert_eq!(config.max_operations_per_update, 1000);
1169        assert!(config.validate_references);
1170    }
1171
1172    #[test]
1173    fn test_operation_validation() {
1174        let generator = UpdateGenerator::new();
1175
1176        let operation = UpdateOperation {
1177            operation_id: "OP000001".to_string(),
1178            action: UpdateAction::Add,
1179            target_path: "/Release/Title".to_string(),
1180            entity_type: EntityType::Release,
1181            entity_id: "release-001".to_string(),
1182            old_value: None,
1183            new_value: Some("New Title".to_string()),
1184            is_critical: false,
1185            description: "Update title".to_string(),
1186            dependencies: Vec::new(),
1187        };
1188
1189        let update = UpdateReleaseMessage {
1190            header: MessageHeaderRequest {
1191                message_id: Some("TEST-001".to_string()),
1192                message_sender: crate::builder::PartyRequest {
1193                    party_name: vec![crate::builder::LocalizedStringRequest {
1194                        text: "Test".to_string(),
1195                        language_code: None,
1196                    }],
1197                    party_id: None,
1198                    party_reference: None,
1199                },
1200                message_recipient: crate::builder::PartyRequest {
1201                    party_name: vec![crate::builder::LocalizedStringRequest {
1202                        text: "Test".to_string(),
1203                        language_code: None,
1204                    }],
1205                    party_id: None,
1206                    party_reference: None,
1207                },
1208                message_control_type: None,
1209                message_created_date_time: None,
1210            },
1211            update_list: vec![operation.clone()],
1212            resource_updates: IndexMap::new(),
1213            release_updates: IndexMap::new(),
1214            deal_updates: IndexMap::new(),
1215            update_metadata: UpdateMetadata {
1216                original_message_id: "ORIG-001".to_string(),
1217                original_message_version: None,
1218                original_message_timestamp: None,
1219                update_created_timestamp: Utc::now(),
1220                update_sequence: 1,
1221                total_operations: 1,
1222                impact_level: "Low".to_string(),
1223                validation_status: ValidationStatus::Pending,
1224                custom_metadata: IndexMap::new(),
1225            },
1226        };
1227
1228        assert!(generator.validate_operation(&operation, &update).is_ok());
1229    }
1230
1231    #[test]
1232    fn test_entity_type_determination() {
1233        let generator = UpdateGenerator::new();
1234
1235        let resource_path = crate::diff::types::DiffPath::root()
1236            .with_element("ResourceList")
1237            .with_element("SoundRecording");
1238
1239        let release_path = crate::diff::types::DiffPath::root()
1240            .with_element("ReleaseList")
1241            .with_element("Release");
1242
1243        assert_eq!(
1244            generator.determine_entity_type(&resource_path),
1245            EntityType::Resource
1246        );
1247        assert_eq!(
1248            generator.determine_entity_type(&release_path),
1249            EntityType::Release
1250        );
1251    }
1252}