ddex_builder/
builder.rs

1//! Main builder implementation
2
3use crate::generator::{ASTGenerator, xml_writer::XmlWriter};
4use indexmap::IndexMap;
5use serde::{Deserialize, Serialize};
6pub use super::preflight::PreflightLevel;
7
8/// Build request structure
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct BuildRequest {
11    /// Message header
12    pub header: MessageHeaderRequest,
13    
14    /// ERN version
15    pub version: String,
16    
17    /// Profile
18    pub profile: Option<String>,
19    
20    /// Releases (uses IndexMap for order preservation)
21    pub releases: Vec<ReleaseRequest>,
22    
23    /// Deals
24    pub deals: Vec<DealRequest>,
25    
26    /// Extensions (uses IndexMap for determinism)
27    pub extensions: Option<IndexMap<String, String>>,
28}
29
30/// Message header request
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct MessageHeaderRequest {
33    pub message_id: Option<String>,
34    pub message_sender: PartyRequest,
35    pub message_recipient: PartyRequest,
36    pub message_control_type: Option<String>,
37    pub message_created_date_time: Option<String>,
38}
39
40/// Party request
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct PartyRequest {
43    pub party_name: Vec<LocalizedStringRequest>,
44    pub party_id: Option<String>,
45    pub party_reference: Option<String>,
46}
47
48/// Localized string request
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct LocalizedStringRequest {
51    pub text: String,
52    pub language_code: Option<String>,
53}
54
55/// Release request
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ReleaseRequest {
58    pub release_id: String,
59    pub release_reference: Option<String>,  // Added for linker
60    pub title: Vec<LocalizedStringRequest>,
61    pub artist: String,
62    pub label: Option<String>,              // Added for metadata
63    pub release_date: Option<String>,       // Added for metadata
64    pub upc: Option<String>,                // Added for validation
65    pub tracks: Vec<TrackRequest>,
66    pub resource_references: Option<Vec<String>>,  // Added for linker
67}
68
69/// Track request
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct TrackRequest {
72    pub track_id: String,                     // Added for linker
73    pub resource_reference: Option<String>,   // Added for linker
74    pub isrc: String,                        // Changed from Option<String>
75    pub title: String,
76    pub duration: String,                    // Keep as String for ISO 8601 format
77    pub artist: String,
78}
79
80/// Deal request
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct DealRequest {
83    pub deal_reference: Option<String>,       // Added for linker
84    pub deal_terms: DealTerms,               // Define this
85    pub release_references: Vec<String>,      // Added for linker
86}
87
88/// Deal terms (simple definition for now)
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct DealTerms {
91    pub commercial_model_type: String,
92    pub territory_code: Vec<String>,
93    pub start_date: Option<String>,
94}
95
96/// Build options
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct BuildOptions {
99    /// Determinism configuration
100    pub determinism: Option<super::determinism::DeterminismConfig>,
101    
102    /// Validation level
103    pub preflight_level: super::preflight::PreflightLevel,
104    
105    /// ID generation strategy
106    pub id_strategy: IdStrategy,
107    
108    /// Stable hash configuration (when using StableHash strategy)
109    pub stable_hash_config: Option<super::id_generator::StableHashConfig>,
110}
111
112impl Default for BuildOptions {
113    fn default() -> Self {
114        Self {
115            determinism: None,
116            preflight_level: super::preflight::PreflightLevel::Warn,
117            id_strategy: IdStrategy::UUID,
118            stable_hash_config: None,
119        }
120    }
121}
122
123/// ID generation strategy
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125pub enum IdStrategy {
126    /// UUID v4
127    UUID,
128    /// UUID v7 (time-ordered)
129    UUIDv7,
130    /// Sequential
131    Sequential,
132    /// Stable hash-based
133    StableHash,
134}
135
136/// Build result
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct BuildResult {
139    /// Generated XML
140    pub xml: String,
141    
142    /// Warnings
143    pub warnings: Vec<BuildWarning>,
144    
145    /// Errors (if any)
146    pub errors: Vec<super::error::BuildError>,
147    
148    /// Statistics
149    pub statistics: BuildStatistics,
150    
151    /// Canonical hash (if deterministic)
152    pub canonical_hash: Option<String>,
153    
154    /// Reproducibility banner (if requested)
155    pub reproducibility_banner: Option<String>,
156}
157
158/// Build warning
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct BuildWarning {
161    pub code: String,
162    pub message: String,
163    pub location: Option<String>,
164}
165
166/// Build statistics
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct BuildStatistics {
169    pub releases: usize,
170    pub tracks: usize,
171    pub deals: usize,
172    pub generation_time_ms: u64,
173    pub xml_size_bytes: usize,
174}
175
176impl Default for BuildStatistics {
177    fn default() -> Self {
178        Self {
179            releases: 0,
180            tracks: 0,
181            deals: 0,
182            generation_time_ms: 0,
183            xml_size_bytes: 0,
184        }
185    }
186}
187
188/// Main DDEX Builder
189pub struct DDEXBuilder {
190    inner: super::Builder,
191}
192
193impl DDEXBuilder {
194    /// Create new builder
195    pub fn new() -> Self {
196        Self {
197            inner: super::Builder::new(),
198        }
199    }
200    
201    /// Build DDEX XML from request
202    pub fn build(&self, mut request: BuildRequest, options: BuildOptions) -> Result<BuildResult, super::error::BuildError> {
203        let start = std::time::Instant::now();
204        let mut warnings = Vec::new();
205        
206        // 1. Enhanced preflight checks with new validator
207        let validator = super::preflight::PreflightValidator::new(
208            super::preflight::ValidationConfig {
209                level: options.preflight_level,
210                profile: request.profile.clone(),
211                validate_identifiers: true,
212                validate_checksums: true,
213                check_required_fields: true,
214                validate_dates: true,
215                validate_references: true,
216            }
217        );
218        
219        let validation_result = validator.validate(&request)?;
220        
221        // Convert validation warnings to build warnings
222        for warning in validation_result.warnings {
223            warnings.push(BuildWarning {
224                code: warning.code,
225                message: warning.message,
226                location: Some(warning.location),
227            });
228        }
229        
230        // Fail if validation didn't pass
231        if !validation_result.passed {
232            if options.preflight_level == super::preflight::PreflightLevel::Strict {
233                return Err(super::error::BuildError::ValidationFailed {
234                    errors: validation_result.errors.iter()
235                        .map(|e| format!("{}: {}", e.code, e.message))
236                        .collect(),
237                });
238            }
239        }
240        
241        // 2. Generate IDs based on strategy
242        self.generate_ids(&mut request, &options)?;
243        
244        // 3. Generate AST
245        let mut generator = ASTGenerator::new(request.version.clone());
246        let ast = generator.generate(&request)?;
247        
248        // 4. Apply determinism config
249        let config = options.determinism.unwrap_or_default();
250        
251        // 5. Generate XML
252        let writer = XmlWriter::new(config.clone());
253        let xml = writer.write(&ast)?;
254        
255        // 6. Apply canonicalization if requested
256        let (final_xml, canonical_hash) = if config.canon_mode == super::determinism::CanonMode::DbC14n {
257            let canonicalizer = super::canonical::DB_C14N::new(config.clone());
258            let canonical = canonicalizer.canonicalize(&xml)?;
259            let hash = Some(canonicalizer.canonical_hash(&canonical)?);
260            (canonical, hash)
261        } else {
262            (xml, None)
263        };
264        
265        // 7. Generate reproducibility banner if requested
266        let reproducibility_banner = if config.emit_reproducibility_banner {
267            Some(format!(
268                "Generated by DDEX Builder v{} with DB-C14N/{}",
269                env!("CARGO_PKG_VERSION"),
270                super::DB_C14N_VERSION
271            ))
272        } else {
273            None
274        };
275        
276        let elapsed = start.elapsed();
277        
278        Ok(BuildResult {
279            xml: final_xml.clone(),
280            warnings,
281            errors: Vec::new(),
282            statistics: BuildStatistics {
283                releases: request.releases.len(),
284                tracks: request.releases.iter().map(|r| r.tracks.len()).sum(),
285                deals: request.deals.len(),
286                generation_time_ms: elapsed.as_millis() as u64,
287                xml_size_bytes: final_xml.len(),
288            },
289            canonical_hash,
290            reproducibility_banner,
291        })
292    }
293    
294    /// Generate IDs based on the selected strategy
295    fn generate_ids(&self, request: &mut BuildRequest, options: &BuildOptions) -> Result<(), super::error::BuildError> {
296        match options.id_strategy {
297            IdStrategy::UUID => {
298                self.generate_uuid_ids(request)?;
299            },
300            IdStrategy::UUIDv7 => {
301                self.generate_uuidv7_ids(request)?;
302            },
303            IdStrategy::Sequential => {
304                self.generate_sequential_ids(request)?;
305            },
306            IdStrategy::StableHash => {
307                self.generate_stable_hash_ids(request, options)?;
308            },
309        }
310        Ok(())
311    }
312    
313    /// Generate UUID v4 IDs
314    fn generate_uuid_ids(&self, request: &mut BuildRequest) -> Result<(), super::error::BuildError> {
315        use uuid::Uuid;
316        
317        // Generate message ID if missing
318        if request.header.message_id.is_none() {
319            request.header.message_id = Some(format!("MSG_{}", Uuid::new_v4()));
320        }
321        
322        // Generate release references if missing
323        for release in &mut request.releases {
324            if release.release_reference.is_none() {
325                release.release_reference = Some(format!("R{}", Uuid::new_v4().simple()));
326            }
327            
328            // Generate resource references for tracks
329            for track in &mut release.tracks {
330                if track.resource_reference.is_none() {
331                    track.resource_reference = Some(format!("A{}", Uuid::new_v4().simple()));
332                }
333            }
334        }
335        
336        // Generate deal references if missing
337        for (idx, deal) in request.deals.iter_mut().enumerate() {
338            if deal.deal_reference.is_none() {
339                deal.deal_reference = Some(format!("D{}", idx + 1));
340            }
341        }
342        
343        Ok(())
344    }
345    
346    /// Generate UUID v7 IDs (time-ordered)
347    fn generate_uuidv7_ids(&self, request: &mut BuildRequest) -> Result<(), super::error::BuildError> {
348        // For now, fall back to UUID v4
349        // TODO: Implement proper UUID v7 generation
350        self.generate_uuid_ids(request)
351    }
352    
353    /// Generate sequential IDs
354    fn generate_sequential_ids(&self, request: &mut BuildRequest) -> Result<(), super::error::BuildError> {
355        // Generate message ID if missing
356        if request.header.message_id.is_none() {
357            request.header.message_id = Some(format!("MSG_{}", chrono::Utc::now().timestamp()));
358        }
359        
360        // Generate release references if missing
361        for (idx, release) in request.releases.iter_mut().enumerate() {
362            if release.release_reference.is_none() {
363                release.release_reference = Some(format!("R{}", idx + 1));
364            }
365            
366            // Generate resource references for tracks
367            for (track_idx, track) in release.tracks.iter_mut().enumerate() {
368                if track.resource_reference.is_none() {
369                    track.resource_reference = Some(format!("A{}", (idx * 1000) + track_idx + 1));
370                }
371            }
372        }
373        
374        // Generate deal references if missing
375        for (idx, deal) in request.deals.iter_mut().enumerate() {
376            if deal.deal_reference.is_none() {
377                deal.deal_reference = Some(format!("D{}", idx + 1));
378            }
379        }
380        
381        Ok(())
382    }
383    
384    /// Generate stable hash-based IDs
385    fn generate_stable_hash_ids(&self, request: &mut BuildRequest, options: &BuildOptions) -> Result<(), super::error::BuildError> {
386        let config = options.stable_hash_config.clone()
387            .unwrap_or_default();
388        let mut id_gen = super::id_generator::StableHashGenerator::new(config);
389        
390        // Generate message ID if missing
391        if request.header.message_id.is_none() {
392            // Use sender/recipient info for stable message ID
393            let sender_name = request.header.message_sender.party_name
394                .first()
395                .map(|s| s.text.clone())
396                .unwrap_or_default();
397            let recipient_name = request.header.message_recipient.party_name
398                .first()
399                .map(|s| s.text.clone())
400                .unwrap_or_default();
401            
402            let msg_id = id_gen.generate_party_id(
403                &format!("{}-{}", sender_name, recipient_name),
404                "MessageHeader",
405                &[chrono::Utc::now().format("%Y%m%d").to_string()],
406            )?;
407            request.header.message_id = Some(msg_id);
408        }
409        
410        // Generate stable IDs for releases
411        for release in &mut request.releases {
412            if release.release_reference.is_none() {
413                let id = id_gen.generate_release_id(
414                    release.upc.as_deref().unwrap_or(&release.release_id),
415                    "Album",
416                    &release.tracks.iter()
417                        .map(|t| t.isrc.clone())
418                        .collect::<Vec<_>>(),
419                    &[], // Empty territory set for now
420                )?;
421                release.release_reference = Some(id);
422            }
423            
424            // Generate stable IDs for tracks/resources
425            for track in &mut release.tracks {
426                if track.resource_reference.is_none() {
427                    // Parse duration to seconds for stable hash
428                    let duration_seconds = self.parse_duration_to_seconds(&track.duration)
429                        .unwrap_or(0);
430                    
431                    let id = id_gen.generate_resource_id(
432                        &track.isrc,
433                        duration_seconds,
434                        None, // No file hash available
435                    )?;
436                    track.resource_reference = Some(id);
437                }
438            }
439        }
440        
441        // Generate deal references if missing
442        for (_idx, deal) in request.deals.iter_mut().enumerate() {
443            if deal.deal_reference.is_none() {
444                // Create stable deal ID based on terms
445                let territories = deal.deal_terms.territory_code.join(",");
446                deal.deal_reference = Some(format!("DEAL_{}_{}", 
447                    deal.deal_terms.commercial_model_type,
448                    territories));
449            }
450        }
451        
452        Ok(())
453    }
454    
455    /// Parse ISO 8601 duration to seconds
456    fn parse_duration_to_seconds(&self, duration: &str) -> Option<u32> {
457        // Simple parser for PT3M45S format
458        if !duration.starts_with("PT") {
459            return None;
460        }
461        
462        let mut seconds = 0u32;
463        let mut current_num = String::new();
464        
465        for ch in duration[2..].chars() {
466            match ch {
467                '0'..='9' => current_num.push(ch),
468                'H' => {
469                    if let Ok(hours) = current_num.parse::<u32>() {
470                        seconds += hours * 3600;
471                    }
472                    current_num.clear();
473                },
474                'M' => {
475                    if let Ok(minutes) = current_num.parse::<u32>() {
476                        seconds += minutes * 60;
477                    }
478                    current_num.clear();
479                },
480                'S' => {
481                    if let Ok(secs) = current_num.parse::<u32>() {
482                        seconds += secs;
483                    }
484                    current_num.clear();
485                },
486                _ => {}
487            }
488        }
489        
490        Some(seconds)
491    }
492    
493    /// Legacy preflight check method (kept for compatibility)
494    fn preflight(&self, request: &BuildRequest, level: super::preflight::PreflightLevel) -> Result<Vec<BuildWarning>, super::error::BuildError> {
495        let mut warnings = Vec::new();
496        
497        if level == super::preflight::PreflightLevel::None {
498            return Ok(warnings);
499        }
500        
501        // Basic checks (enhanced validation is done in main build method)
502        if request.releases.is_empty() {
503            warnings.push(BuildWarning {
504                code: "NO_RELEASES".to_string(),
505                message: "No releases in request".to_string(),
506                location: Some("/releases".to_string()),
507            });
508        }
509        
510        if level == super::preflight::PreflightLevel::Strict && !warnings.is_empty() {
511            return Err(super::error::BuildError::InvalidFormat {
512                field: "request".to_string(),
513                message: format!("{} validation warnings in strict mode", warnings.len()),
514            });
515        }
516        
517        Ok(warnings)
518    }
519    
520    /// Compare two DDEX XML documents and return semantic differences
521    /// 
522    /// This method performs semantic diffing that understands DDEX business logic,
523    /// not just XML structure differences.
524    pub fn diff_xml(&self, old_xml: &str, new_xml: &str) -> Result<super::diff::types::ChangeSet, super::error::BuildError> {
525        self.diff_xml_with_config(old_xml, new_xml, super::diff::DiffConfig::default())
526    }
527    
528    /// Compare two DDEX XML documents with custom diff configuration
529    pub fn diff_xml_with_config(
530        &self, 
531        old_xml: &str, 
532        new_xml: &str, 
533        config: super::diff::DiffConfig
534    ) -> Result<super::diff::types::ChangeSet, super::error::BuildError> {
535        // Parse both XML documents to AST
536        let old_ast = self.parse_xml_to_ast(old_xml)?;
537        let new_ast = self.parse_xml_to_ast(new_xml)?;
538        
539        // Create diff engine and compare
540        let mut diff_engine = super::diff::DiffEngine::new_with_config(config);
541        diff_engine.diff(&old_ast, &new_ast)
542    }
543    
544    /// Compare a BuildRequest with existing XML to see what would change
545    pub fn diff_request_with_xml(
546        &self, 
547        request: &BuildRequest, 
548        existing_xml: &str
549    ) -> Result<super::diff::types::ChangeSet, super::error::BuildError> {
550        // Build new XML from request
551        let build_result = self.build(request.clone(), BuildOptions::default())?;
552        
553        // Compare existing XML with newly built XML
554        self.diff_xml(existing_xml, &build_result.xml)
555    }
556    
557    /// Helper to parse XML string to AST
558    fn parse_xml_to_ast(&self, xml: &str) -> Result<super::ast::AST, super::error::BuildError> {
559        use quick_xml::Reader;
560        
561        let mut reader = Reader::from_str(xml);
562        reader.config_mut().trim_text(true);
563        
564        // This is a simplified XML->AST parser
565        // In a production system, you'd want to use the actual ddex-parser
566        let mut root_element = super::ast::Element::new("Root");
567        let namespace_map = indexmap::IndexMap::new();
568        
569        // For now, create a basic AST structure
570        // TODO: Implement proper XML parsing or integrate with ddex-parser
571        root_element = root_element.with_text(xml);
572        
573        Ok(super::ast::AST {
574            root: root_element,
575            namespaces: namespace_map,
576            schema_location: None,
577        })
578    }
579    
580    /// Create an UpdateReleaseMessage from two DDEX messages
581    /// 
582    /// This method compares an original DDEX message with an updated version and
583    /// generates a minimal UpdateReleaseMessage containing only the differences.
584    pub fn create_update(
585        &self,
586        original_xml: &str,
587        updated_xml: &str,
588        original_message_id: &str,
589    ) -> Result<super::messages::UpdateReleaseMessage, super::error::BuildError> {
590        let mut update_generator = super::messages::UpdateGenerator::new();
591        update_generator.create_update(original_xml, updated_xml, original_message_id)
592    }
593    
594    /// Create an UpdateReleaseMessage with custom configuration
595    pub fn create_update_with_config(
596        &self,
597        original_xml: &str,
598        updated_xml: &str,
599        original_message_id: &str,
600        config: super::messages::UpdateConfig,
601    ) -> Result<super::messages::UpdateReleaseMessage, super::error::BuildError> {
602        let mut update_generator = super::messages::UpdateGenerator::new_with_config(config);
603        update_generator.create_update(original_xml, updated_xml, original_message_id)
604    }
605    
606    /// Apply an UpdateReleaseMessage to a base DDEX message
607    /// 
608    /// This method takes a base DDEX message and applies the operations from an
609    /// UpdateReleaseMessage to produce a new complete DDEX message.
610    pub fn apply_update(
611        &self,
612        base_xml: &str,
613        update: &super::messages::UpdateReleaseMessage,
614    ) -> Result<String, super::error::BuildError> {
615        let update_generator = super::messages::UpdateGenerator::new();
616        update_generator.apply_update(base_xml, update)
617    }
618    
619    /// Create an update from a BuildRequest compared to existing XML
620    /// 
621    /// This is useful for generating updates when you have a new BuildRequest
622    /// that represents the desired state and need to update an existing message.
623    pub fn create_update_from_request(
624        &self,
625        existing_xml: &str,
626        request: &BuildRequest,
627        original_message_id: &str,
628    ) -> Result<super::messages::UpdateReleaseMessage, super::error::BuildError> {
629        // Build new XML from request
630        let build_result = self.build(request.clone(), BuildOptions::default())?;
631        
632        // Create update between existing and new XML
633        self.create_update(existing_xml, &build_result.xml, original_message_id)
634    }
635    
636    /// Validate an UpdateReleaseMessage for safety and consistency
637    pub fn validate_update(
638        &self,
639        update: &super::messages::UpdateReleaseMessage,
640    ) -> Result<super::messages::ValidationStatus, super::error::BuildError> {
641        let update_generator = super::messages::UpdateGenerator::new();
642        update_generator.validate_update(update)
643    }
644    
645    /// Generate an UpdateReleaseMessage as XML
646    pub fn serialize_update(
647        &self,
648        update: &super::messages::UpdateReleaseMessage,
649    ) -> Result<String, super::error::BuildError> {
650        self.serialize_update_message_to_xml(update)
651    }
652    
653    // Helper methods for update serialization
654    
655    fn serialize_update_message_to_xml(
656        &self,
657        update: &super::messages::UpdateReleaseMessage,
658    ) -> Result<String, super::error::BuildError> {
659        let mut xml = String::new();
660        
661        // XML declaration and root element
662        xml.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
663        xml.push('\n');
664        xml.push_str(r#"<UpdateReleaseMessage xmlns="http://ddex.net/xml/ern/43" MessageSchemaVersionId="ern/43">"#);
665        xml.push('\n');
666        
667        // Message header
668        self.serialize_update_header(&mut xml, &update.header)?;
669        
670        // Update metadata
671        self.serialize_update_metadata(&mut xml, &update.update_metadata)?;
672        
673        // Update list
674        self.serialize_update_list(&mut xml, &update.update_list)?;
675        
676        // Resource updates
677        if !update.resource_updates.is_empty() {
678            self.serialize_resource_updates(&mut xml, &update.resource_updates)?;
679        }
680        
681        // Release updates
682        if !update.release_updates.is_empty() {
683            self.serialize_release_updates(&mut xml, &update.release_updates)?;
684        }
685        
686        // Deal updates
687        if !update.deal_updates.is_empty() {
688            self.serialize_deal_updates(&mut xml, &update.deal_updates)?;
689        }
690        
691        // Close root element
692        xml.push_str("</UpdateReleaseMessage>\n");
693        
694        Ok(xml)
695    }
696    
697    fn serialize_update_header(
698        &self,
699        xml: &mut String,
700        header: &MessageHeaderRequest,
701    ) -> Result<(), super::error::BuildError> {
702        xml.push_str("  <MessageHeader>\n");
703        
704        if let Some(ref message_id) = header.message_id {
705            xml.push_str(&format!("    <MessageId>{}</MessageId>\n", self.escape_xml(message_id)));
706        }
707        
708        // Message sender
709        xml.push_str("    <MessageSender>\n");
710        if !header.message_sender.party_name.is_empty() {
711            xml.push_str(&format!("      <PartyName>{}</PartyName>\n", 
712                self.escape_xml(&header.message_sender.party_name[0].text)));
713        }
714        xml.push_str("    </MessageSender>\n");
715        
716        // Message recipient
717        xml.push_str("    <MessageRecipient>\n");
718        if !header.message_recipient.party_name.is_empty() {
719            xml.push_str(&format!("      <PartyName>{}</PartyName>\n", 
720                self.escape_xml(&header.message_recipient.party_name[0].text)));
721        }
722        xml.push_str("    </MessageRecipient>\n");
723        
724        // Created date time
725        if let Some(ref created_time) = header.message_created_date_time {
726            xml.push_str(&format!("    <MessageCreatedDateTime>{}</MessageCreatedDateTime>\n", 
727                self.escape_xml(created_time)));
728        } else {
729            let default_time = chrono::Utc::now().to_rfc3339();
730            xml.push_str(&format!("    <MessageCreatedDateTime>{}</MessageCreatedDateTime>\n", 
731                self.escape_xml(&default_time)));
732        }
733        
734        xml.push_str("  </MessageHeader>\n");
735        Ok(())
736    }
737    
738    fn serialize_update_metadata(
739        &self,
740        xml: &mut String,
741        metadata: &super::messages::UpdateMetadata,
742    ) -> Result<(), super::error::BuildError> {
743        xml.push_str("  <UpdateMetadata>\n");
744        xml.push_str(&format!("    <OriginalMessageId>{}</OriginalMessageId>\n", 
745            self.escape_xml(&metadata.original_message_id)));
746        xml.push_str(&format!("    <UpdateSequence>{}</UpdateSequence>\n", metadata.update_sequence));
747        xml.push_str(&format!("    <TotalOperations>{}</TotalOperations>\n", metadata.total_operations));
748        xml.push_str(&format!("    <ImpactLevel>{}</ImpactLevel>\n", 
749            self.escape_xml(&metadata.impact_level)));
750        xml.push_str(&format!("    <ValidationStatus>{}</ValidationStatus>\n", metadata.validation_status));
751        xml.push_str(&format!("    <UpdateCreatedDateTime>{}</UpdateCreatedDateTime>\n", 
752            metadata.update_created_timestamp.to_rfc3339()));
753        xml.push_str("  </UpdateMetadata>\n");
754        Ok(())
755    }
756    
757    fn serialize_update_list(
758        &self,
759        xml: &mut String,
760        operations: &[super::messages::UpdateOperation],
761    ) -> Result<(), super::error::BuildError> {
762        xml.push_str("  <UpdateList>\n");
763        
764        for operation in operations {
765            xml.push_str("    <UpdateOperation>\n");
766            xml.push_str(&format!("      <OperationId>{}</OperationId>\n", 
767                self.escape_xml(&operation.operation_id)));
768            xml.push_str(&format!("      <Action>{}</Action>\n", operation.action));
769            xml.push_str(&format!("      <TargetPath>{}</TargetPath>\n", 
770                self.escape_xml(&operation.target_path)));
771            xml.push_str(&format!("      <EntityType>{}</EntityType>\n", operation.entity_type));
772            xml.push_str(&format!("      <EntityId>{}</EntityId>\n", 
773                self.escape_xml(&operation.entity_id)));
774            
775            if let Some(ref old_value) = operation.old_value {
776                xml.push_str(&format!("      <OldValue>{}</OldValue>\n", 
777                    self.escape_xml(old_value)));
778            }
779            
780            if let Some(ref new_value) = operation.new_value {
781                xml.push_str(&format!("      <NewValue>{}</NewValue>\n", 
782                    self.escape_xml(new_value)));
783            }
784            
785            xml.push_str(&format!("      <IsCritical>{}</IsCritical>\n", operation.is_critical));
786            xml.push_str(&format!("      <Description>{}</Description>\n", 
787                self.escape_xml(&operation.description)));
788            
789            if !operation.dependencies.is_empty() {
790                xml.push_str("      <Dependencies>\n");
791                for dependency in &operation.dependencies {
792                    xml.push_str(&format!("        <Dependency>{}</Dependency>\n", 
793                        self.escape_xml(dependency)));
794                }
795                xml.push_str("      </Dependencies>\n");
796            }
797            
798            xml.push_str("    </UpdateOperation>\n");
799        }
800        
801        xml.push_str("  </UpdateList>\n");
802        Ok(())
803    }
804    
805    fn serialize_resource_updates(
806        &self,
807        xml: &mut String,
808        resource_updates: &indexmap::IndexMap<String, super::messages::ResourceUpdate>,
809    ) -> Result<(), super::error::BuildError> {
810        xml.push_str("  <ResourceUpdates>\n");
811        
812        for (resource_id, update) in resource_updates {
813            xml.push_str("    <ResourceUpdate>\n");
814            xml.push_str(&format!("      <ResourceId>{}</ResourceId>\n", 
815                self.escape_xml(resource_id)));
816            xml.push_str(&format!("      <ResourceReference>{}</ResourceReference>\n", 
817                self.escape_xml(&update.resource_reference)));
818            xml.push_str(&format!("      <Action>{}</Action>\n", update.action));
819            
820            // Add resource data if present
821            if let Some(ref data) = update.resource_data {
822                xml.push_str("      <ResourceData>\n");
823                xml.push_str(&format!("        <Type>{}</Type>\n", 
824                    self.escape_xml(&data.resource_type)));
825                xml.push_str(&format!("        <Title>{}</Title>\n", 
826                    self.escape_xml(&data.title)));
827                xml.push_str(&format!("        <Artist>{}</Artist>\n", 
828                    self.escape_xml(&data.artist)));
829                
830                if let Some(ref isrc) = data.isrc {
831                    xml.push_str(&format!("        <ISRC>{}</ISRC>\n", 
832                        self.escape_xml(isrc)));
833                }
834                
835                if let Some(ref duration) = data.duration {
836                    xml.push_str(&format!("        <Duration>{}</Duration>\n", 
837                        self.escape_xml(duration)));
838                }
839                
840                xml.push_str("      </ResourceData>\n");
841            }
842            
843            xml.push_str("    </ResourceUpdate>\n");
844        }
845        
846        xml.push_str("  </ResourceUpdates>\n");
847        Ok(())
848    }
849    
850    fn serialize_release_updates(
851        &self,
852        xml: &mut String,
853        release_updates: &indexmap::IndexMap<String, super::messages::ReleaseUpdate>,
854    ) -> Result<(), super::error::BuildError> {
855        xml.push_str("  <ReleaseUpdates>\n");
856        
857        for (release_id, update) in release_updates {
858            xml.push_str("    <ReleaseUpdate>\n");
859            xml.push_str(&format!("      <ReleaseId>{}</ReleaseId>\n", 
860                self.escape_xml(release_id)));
861            xml.push_str(&format!("      <ReleaseReference>{}</ReleaseReference>\n", 
862                self.escape_xml(&update.release_reference)));
863            xml.push_str(&format!("      <Action>{}</Action>\n", update.action));
864            
865            // Add release data if present
866            if let Some(ref data) = update.release_data {
867                xml.push_str("      <ReleaseData>\n");
868                xml.push_str(&format!("        <Type>{}</Type>\n", 
869                    self.escape_xml(&data.release_type)));
870                xml.push_str(&format!("        <Title>{}</Title>\n", 
871                    self.escape_xml(&data.title)));
872                xml.push_str(&format!("        <Artist>{}</Artist>\n", 
873                    self.escape_xml(&data.artist)));
874                
875                if let Some(ref label) = data.label {
876                    xml.push_str(&format!("        <Label>{}</Label>\n", 
877                        self.escape_xml(label)));
878                }
879                
880                if let Some(ref upc) = data.upc {
881                    xml.push_str(&format!("        <UPC>{}</UPC>\n", 
882                        self.escape_xml(upc)));
883                }
884                
885                xml.push_str("      </ReleaseData>\n");
886            }
887            
888            xml.push_str("    </ReleaseUpdate>\n");
889        }
890        
891        xml.push_str("  </ReleaseUpdates>\n");
892        Ok(())
893    }
894    
895    fn serialize_deal_updates(
896        &self,
897        xml: &mut String,
898        deal_updates: &indexmap::IndexMap<String, super::messages::DealUpdate>,
899    ) -> Result<(), super::error::BuildError> {
900        xml.push_str("  <DealUpdates>\n");
901        
902        for (deal_id, update) in deal_updates {
903            xml.push_str("    <DealUpdate>\n");
904            xml.push_str(&format!("      <DealId>{}</DealId>\n", 
905                self.escape_xml(deal_id)));
906            xml.push_str(&format!("      <DealReference>{}</DealReference>\n", 
907                self.escape_xml(&update.deal_reference)));
908            xml.push_str(&format!("      <Action>{}</Action>\n", update.action));
909            
910            xml.push_str("    </DealUpdate>\n");
911        }
912        
913        xml.push_str("  </DealUpdates>\n");
914        Ok(())
915    }
916    
917    fn escape_xml(&self, text: &str) -> String {
918        text.replace('&', "&amp;")
919            .replace('<', "&lt;")
920            .replace('>', "&gt;")
921            .replace('"', "&quot;")
922            .replace('\'', "&apos;")
923    }
924}
925
926impl Default for DDEXBuilder {
927    fn default() -> Self {
928        Self::new()
929    }
930}