Skip to main content

mig_bo4e/
engine.rs

1//! Mapping engine — loads TOML definitions and provides bidirectional conversion.
2//!
3//! Supports nested group paths (e.g., "SG4.SG5") for navigating the assembled tree
4//! and provides `map_forward` / `map_reverse` for full entity conversion.
5
6use std::collections::{HashMap, HashSet};
7use std::path::Path;
8
9use mig_assembly::assembler::{
10    AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
11};
12use mig_types::schema::mig::MigSchema;
13use mig_types::segment::OwnedSegment;
14
15use crate::definition::{FieldMapping, MappingDefinition};
16use crate::error::MappingError;
17use crate::segment_structure::SegmentStructure;
18
19/// The mapping engine holds all loaded mapping definitions
20/// and provides methods for bidirectional conversion.
21pub struct MappingEngine {
22    definitions: Vec<MappingDefinition>,
23    segment_structure: Option<SegmentStructure>,
24    code_lookup: Option<crate::code_lookup::CodeLookup>,
25    /// Transaction-root SG id (e.g. "SG4" for UTILMD), when the engine is
26    /// operating at transaction scope. Child entities whose parent group
27    /// equals this id are left at the top level of the forward-mapped JSON
28    /// rather than being nested — SG4 is the transaction envelope, so
29    /// entities inside it (Marktlokation, Geschaeftspartner, …) are peers of
30    /// the transaction metadata, not sub-objects of it.
31    ///
32    /// Nesting still applies to other parent groups: e.g. Kontakt (SG2.SG3)
33    /// remains nested under Marktteilnehmer (SG2) because SG2 is a
34    /// message-level group, not the transaction root.
35    transaction_group: Option<String>,
36    /// PID currently being processed (e.g., "55002"). Used to suppress codelist
37    /// decoration of self-referential PID-identifier fields (e.g., RFF+Z13's
38    /// d1154 in PID 55002 has only "55002" as an allowed value).
39    current_pid: Option<String>,
40}
41
42impl MappingEngine {
43    /// Create an empty engine with no definitions (for unit testing).
44    pub fn new_empty() -> Self {
45        Self {
46            definitions: Vec::new(),
47            segment_structure: None,
48            code_lookup: None,
49            transaction_group: None,
50            current_pid: None,
51        }
52    }
53
54    /// Load all TOML mapping files from a directory.
55    pub fn load(dir: &Path) -> Result<Self, MappingError> {
56        let mut definitions = Vec::new();
57
58        let mut entries: Vec<_> = std::fs::read_dir(dir)?.filter_map(|e| e.ok()).collect();
59        entries.sort_by_key(|e| e.file_name());
60
61        for entry in entries {
62            let path = entry.path();
63            if path.extension().map(|e| e == "toml").unwrap_or(false) {
64                let content = std::fs::read_to_string(&path)?;
65                let def: MappingDefinition =
66                    toml::from_str(&content).map_err(|e| MappingError::TomlParse {
67                        file: path.display().to_string(),
68                        message: e.to_string(),
69                    })?;
70                definitions.push(def);
71            }
72        }
73
74        Ok(Self {
75            definitions,
76            segment_structure: None,
77            code_lookup: None,
78            transaction_group: None,
79            current_pid: None,
80        })
81    }
82
83    /// Load message-level and transaction-level TOML mappings from separate directories.
84    ///
85    /// Returns `(message_engine, transaction_engine)` where:
86    /// - `message_engine` maps SG2/SG3/root-level definitions (shared across PIDs)
87    /// - `transaction_engine` maps SG4+ definitions (PID-specific)
88    pub fn load_split(
89        message_dir: &Path,
90        transaction_dir: &Path,
91    ) -> Result<(Self, Self), MappingError> {
92        let msg_engine = Self::load(message_dir)?;
93        let tx_engine = Self::load(transaction_dir)?;
94        Ok((msg_engine, tx_engine))
95    }
96
97    /// Load TOML mapping files from multiple directories into a single engine.
98    ///
99    /// Useful for combining message-level and transaction-level mappings
100    /// when a single engine with all definitions is needed.
101    pub fn load_merged(dirs: &[&Path]) -> Result<Self, MappingError> {
102        let mut definitions = Vec::new();
103        for dir in dirs {
104            let engine = Self::load(dir)?;
105            definitions.extend(engine.definitions);
106        }
107        Ok(Self {
108            definitions,
109            segment_structure: None,
110            code_lookup: None,
111            transaction_group: None,
112            current_pid: None,
113        })
114    }
115
116    /// Load transaction-level mappings with common template inheritance.
117    ///
118    /// 1. Loads all `.toml` from `common_dir`
119    /// 2. Filters: keeps only definitions whose `source_path` exists in the PID schema
120    /// 3. Loads all `.toml` from `pid_dir`
121    /// 4. For each PID definition, if a common definition has matching
122    ///    `(source_group, discriminator)`, replaces the common one (file-level replacement)
123    /// 5. Merges both sets: common first, then PID additions
124    pub fn load_with_common(
125        common_dir: &Path,
126        pid_dir: &Path,
127        schema_index: &crate::pid_schema_index::PidSchemaIndex,
128    ) -> Result<Self, MappingError> {
129        let mut common_defs = Self::load(common_dir)?.definitions;
130
131        // Filter common defs by schema — keep only groups that exist in this PID
132        common_defs.retain(|d| {
133            d.meta
134                .source_path
135                .as_deref()
136                .map(|sp| schema_index.has_group(sp))
137                .unwrap_or(true)
138        });
139
140        let pid_defs = Self::load(pid_dir)?.definitions;
141
142        // Build set of PID override keys: (source_group_normalized, discriminator)
143        // Normalizations applied:
144        // 1. Strip positional indices from source_group: "SG4.SG5:1" → "SG4.SG5"
145        // 2. Strip occurrence indices from discriminator: "RFF.c506.d1153=TN#0" → "RFF.c506.d1153=TN"
146        let normalize_sg = |sg: &str| -> String {
147            sg.split('.')
148                .map(|part| part.split(':').next().unwrap_or(part))
149                .collect::<Vec<_>>()
150                .join(".")
151        };
152        let pid_keys: HashSet<(String, Option<String>)> = pid_defs
153            .iter()
154            .flat_map(|d| {
155                let sg = normalize_sg(&d.meta.source_group);
156                let disc = d.meta.discriminator.clone();
157                let mut keys = vec![(sg.clone(), disc.clone())];
158                // If discriminator has occurrence index (#N), also add base form
159                if let Some(ref disc_str) = disc {
160                    if let Some(base) = disc_str.rsplit_once('#') {
161                        if base.1.chars().all(|c| c.is_ascii_digit()) {
162                            keys.push((sg, Some(base.0.to_string())));
163                        }
164                    }
165                }
166                keys
167            })
168            .collect();
169
170        // Remove common defs that are overridden by PID defs
171        common_defs.retain(|d| {
172            let key = (
173                normalize_sg(&d.meta.source_group),
174                d.meta.discriminator.clone(),
175            );
176            !pid_keys.contains(&key)
177        });
178
179        // Combine: common first, then PID
180        let mut definitions = common_defs;
181        definitions.extend(pid_defs);
182
183        Ok(Self {
184            definitions,
185            segment_structure: None,
186            code_lookup: None,
187            transaction_group: None,
188            current_pid: None,
189        })
190    }
191
192    /// Load common definitions only (no per-PID dir), filtered by schema index.
193    ///
194    /// Used for PIDs that have no per-PID directory but can use shared common/ definitions.
195    pub fn load_common_only(
196        common_dir: &Path,
197        schema_index: &crate::pid_schema_index::PidSchemaIndex,
198    ) -> Result<Self, MappingError> {
199        let mut common_defs = Self::load(common_dir)?.definitions;
200
201        // Filter common defs by schema — keep only groups that exist in this PID
202        common_defs.retain(|d| {
203            d.meta
204                .source_path
205                .as_deref()
206                .map(|sp| schema_index.has_group(sp))
207                .unwrap_or(true)
208        });
209
210        Ok(Self {
211            definitions: common_defs,
212            segment_structure: None,
213            code_lookup: None,
214            transaction_group: None,
215            current_pid: None,
216        })
217    }
218
219    /// Load message + transaction engines with common template inheritance.
220    ///
221    /// Returns `(message_engine, transaction_engine)` where the transaction engine
222    /// inherits shared templates from `common_dir`, filtered by the PID schema.
223    pub fn load_split_with_common(
224        message_dir: &Path,
225        common_dir: &Path,
226        transaction_dir: &Path,
227        schema_index: &crate::pid_schema_index::PidSchemaIndex,
228    ) -> Result<(Self, Self), MappingError> {
229        let msg_engine = Self::load(message_dir)?;
230        let tx_engine = Self::load_with_common(common_dir, transaction_dir, schema_index)?;
231        Ok((msg_engine, tx_engine))
232    }
233
234    /// Create an engine from an already-parsed list of definitions.
235    pub fn from_definitions(definitions: Vec<MappingDefinition>) -> Self {
236        Self {
237            definitions,
238            segment_structure: None,
239            code_lookup: None,
240            transaction_group: None,
241            current_pid: None,
242        }
243    }
244
245    /// Save definitions to a cache file.
246    ///
247    /// Only the `definitions` are serialized — `segment_structure` and `code_lookup`
248    /// must be re-attached after loading from cache. Paths in the definitions are
249    /// already resolved to numeric indices, so no `PathResolver` is needed at load time.
250    pub fn save_cached(&self, path: &Path) -> Result<(), MappingError> {
251        let encoded =
252            serde_json::to_vec(&self.definitions).map_err(|e| MappingError::CacheWrite {
253                path: path.display().to_string(),
254                message: e.to_string(),
255            })?;
256        if let Some(parent) = path.parent() {
257            std::fs::create_dir_all(parent)?;
258        }
259        std::fs::write(path, encoded)?;
260        Ok(())
261    }
262
263    /// Load from cache if available, otherwise fall back to TOML directory.
264    ///
265    /// When loading from cache, PathResolver is NOT needed (paths pre-resolved).
266    /// When falling back to TOML, the caller should chain `.with_path_resolver()`.
267    pub fn load_cached_or_toml(cache_path: &Path, toml_dir: &Path) -> Result<Self, MappingError> {
268        if cache_path.exists() {
269            Self::load_cached(cache_path)
270        } else {
271            Self::load(toml_dir)
272        }
273    }
274
275    /// Load definitions from a cache file.
276    ///
277    /// Returns an engine with only `definitions` populated. Attach `segment_structure`
278    /// and `code_lookup` via the builder methods if needed.
279    pub fn load_cached(path: &Path) -> Result<Self, MappingError> {
280        let bytes = std::fs::read(path)?;
281        let definitions: Vec<MappingDefinition> =
282            serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
283                path: path.display().to_string(),
284                message: e.to_string(),
285            })?;
286        Ok(Self {
287            definitions,
288            segment_structure: None,
289            code_lookup: None,
290            transaction_group: None,
291            current_pid: None,
292        })
293    }
294
295    /// Attach a MIG-derived segment structure for trailing element padding.
296    ///
297    /// When set, `map_reverse` pads each segment's elements up to the
298    /// MIG-defined count, ensuring trailing empty elements are preserved.
299    pub fn with_segment_structure(mut self, ss: SegmentStructure) -> Self {
300        self.segment_structure = Some(ss);
301        self
302    }
303
304    /// Attach a code lookup for enriching code-type field values.
305    ///
306    /// When set, fields that map to code-type elements in the PID schema
307    /// are emitted as `{"code": "Z15", "meaning": "Ja"}` objects instead of plain strings.
308    pub fn with_code_lookup(mut self, cl: crate::code_lookup::CodeLookup) -> Self {
309        self.code_lookup = Some(cl);
310        self
311    }
312
313    /// Declare which PID this engine is currently processing.
314    ///
315    /// When combined with [`with_code_lookup`](Self::with_code_lookup), code
316    /// fields whose only allowed value equals the PID itself (Class C in the
317    /// 2026-04-28 audit — e.g., RFF+Z13's d1154 in PID 55002 enumerates only
318    /// `55002`) are emitted as plain strings instead of being decorated with
319    /// `{code, meaning, enum}` and a dedup-suffixed enum name.
320    pub fn with_pid(mut self, pid: impl Into<String>) -> Self {
321        self.current_pid = Some(pid.into());
322        self
323    }
324
325    /// Attach a path resolver to normalize EDIFACT ID paths to numeric indices.
326    ///
327    /// This allows TOML mapping files to use named paths like `loc.c517.d3225`
328    /// instead of numeric indices like `loc.1.0`. Resolution happens once at
329    /// load time — the engine hot path is completely unchanged.
330    pub fn with_path_resolver(mut self, resolver: crate::path_resolver::PathResolver) -> Self {
331        for def in &mut self.definitions {
332            def.normalize_paths(&resolver);
333        }
334        self
335    }
336
337    /// Declare the transaction-root SG id (e.g. `"SG4"` for UTILMD).
338    ///
339    /// When set, entities whose parent group equals this id are not nested
340    /// into their parent in the forward-mapped JSON. See the
341    /// [`transaction_group`](Self#structfield.transaction_group-1) field doc
342    /// on `MappingEngine` for the full rationale.
343    pub fn with_transaction_group(mut self, tx: impl Into<String>) -> Self {
344        self.transaction_group = Some(tx.into());
345        self
346    }
347
348    /// Get all loaded definitions.
349    pub fn definitions(&self) -> &[MappingDefinition] {
350        &self.definitions
351    }
352
353    /// Find a definition by entity name.
354    pub fn definition_for_entity(&self, entity: &str) -> Option<&MappingDefinition> {
355        self.definitions.iter().find(|d| d.meta.entity == entity)
356    }
357
358    // ── Forward mapping: tree → BO4E ──
359
360    /// Extract a field value from an assembled tree using a mapping path.
361    ///
362    /// `group_path` supports dotted notation for nested groups (e.g., "SG4.SG5").
363    /// Parent groups default to repetition 0; `repetition` applies to the leaf group.
364    ///
365    /// Path format: "segment.composite.data_element" e.g., "loc.c517.d3225"
366    pub fn extract_field(
367        &self,
368        tree: &AssembledTree,
369        group_path: &str,
370        path: &str,
371        repetition: usize,
372    ) -> Option<String> {
373        let instance = Self::resolve_group_instance(tree, group_path, repetition)?;
374        Self::extract_from_instance(instance, path)
375    }
376
377    /// Navigate a potentially nested group path to find a group instance.
378    ///
379    /// For "SG4.SG5", finds SG4\[0\] then SG5 at the given repetition within it.
380    /// For "SG8", finds SG8 at the given repetition in the top-level groups.
381    ///
382    /// Supports intermediate repetition with colon syntax: "SG4.SG8:1.SG10"
383    /// means SG4\[0\] → SG8\[1\] → SG10\[repetition\]. Without a colon suffix,
384    /// intermediate groups default to repetition 0.
385    pub fn resolve_group_instance<'a>(
386        tree: &'a AssembledTree,
387        group_path: &str,
388        repetition: usize,
389    ) -> Option<&'a AssembledGroupInstance> {
390        let parts: Vec<&str> = group_path.split('.').collect();
391
392        let (first_id, first_rep) = parse_group_spec(parts[0]);
393        let first_group = tree.groups.iter().find(|g| g.group_id == first_id)?;
394
395        if parts.len() == 1 {
396            // Single part — use the explicit rep from spec or the `repetition` param
397            let rep = first_rep.unwrap_or(repetition);
398            return first_group.repetitions.get(rep);
399        }
400
401        // Navigate through groups; intermediate parts default to rep 0
402        // unless explicitly specified via `:N` suffix
403        let mut current_instance = first_group.repetitions.get(first_rep.unwrap_or(0))?;
404
405        for (i, part) in parts[1..].iter().enumerate() {
406            let (group_id, explicit_rep) = parse_group_spec(part);
407            let child_group = current_instance
408                .child_groups
409                .iter()
410                .find(|g| g.group_id == group_id)?;
411
412            if i == parts.len() - 2 {
413                // Last part — use explicit rep, or fall back to `repetition`
414                let rep = explicit_rep.unwrap_or(repetition);
415                return child_group.repetitions.get(rep);
416            }
417            // Intermediate — use explicit rep or 0
418            current_instance = child_group.repetitions.get(explicit_rep.unwrap_or(0))?;
419        }
420
421        None
422    }
423
424    /// Navigate the assembled tree using a source_path with qualifier suffixes.
425    ///
426    /// Source paths like `"sg4.sg8_z98.sg10"` encode qualifiers inline:
427    /// `sg8_z98` means "find the SG8 repetition whose entry segment has qualifier Z98".
428    /// Parts without underscores (e.g., `sg4`, `sg10`) use the first repetition.
429    ///
430    /// Returns `None` if any part of the path can't be resolved.
431    pub fn resolve_by_source_path<'a>(
432        tree: &'a AssembledTree,
433        source_path: &str,
434    ) -> Option<&'a AssembledGroupInstance> {
435        let parts: Vec<&str> = source_path.split('.').collect();
436        if parts.is_empty() {
437            return None;
438        }
439
440        let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
441        let first_group = tree
442            .groups
443            .iter()
444            .find(|g| g.group_id.eq_ignore_ascii_case(first_id))?;
445
446        let mut current_instance = if let Some(q) = first_qualifier {
447            find_rep_by_entry_qualifier(&first_group.repetitions, q)?
448        } else {
449            first_group.repetitions.first()?
450        };
451
452        if parts.len() == 1 {
453            return Some(current_instance);
454        }
455
456        for part in &parts[1..] {
457            let (group_id, qualifier) = parse_source_path_part(part);
458            let child_group = current_instance
459                .child_groups
460                .iter()
461                .find(|g| g.group_id.eq_ignore_ascii_case(group_id))?;
462
463            current_instance = if let Some(q) = qualifier {
464                find_rep_by_entry_qualifier(&child_group.repetitions, q)?
465            } else {
466                child_group.repetitions.first()?
467            };
468        }
469
470        Some(current_instance)
471    }
472
473    /// Resolve ALL matching instances for a source_path, returning a Vec.
474    ///
475    /// Like `resolve_by_source_path` but returns all repetitions matching
476    /// at any level, not just the first.  For example, if there are two SG5
477    /// reps with LOC+Z17, `resolve_all_by_source_path(tree, "sg4.sg5_z17")`
478    /// returns both.  For deeper paths like "sg4.sg8_zf3.sg10", if there are
479    /// two SG8 reps with ZF3, it returns SG10 children from both.
480    pub fn resolve_all_by_source_path<'a>(
481        tree: &'a AssembledTree,
482        source_path: &str,
483    ) -> Vec<&'a AssembledGroupInstance> {
484        let parts: Vec<&str> = source_path.split('.').collect();
485        if parts.is_empty() {
486            return vec![];
487        }
488
489        // First part: match against top-level groups
490        let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
491        let first_group = match tree
492            .groups
493            .iter()
494            .find(|g| g.group_id.eq_ignore_ascii_case(first_id))
495        {
496            Some(g) => g,
497            None => return vec![],
498        };
499
500        let mut current_instances: Vec<&AssembledGroupInstance> = if let Some(q) = first_qualifier {
501            find_all_reps_by_entry_qualifier(&first_group.repetitions, q)
502        } else {
503            first_group.repetitions.iter().collect()
504        };
505
506        // Navigate remaining parts, branching at each level when multiple
507        // instances match a qualifier (e.g., two SG8 reps with ZF3).
508        for part in &parts[1..] {
509            let (group_id, qualifier) = parse_source_path_part(part);
510            let mut next_instances = Vec::new();
511
512            for instance in &current_instances {
513                if let Some(child_group) = instance
514                    .child_groups
515                    .iter()
516                    .find(|g| g.group_id.eq_ignore_ascii_case(group_id))
517                {
518                    if let Some(q) = qualifier {
519                        next_instances.extend(find_all_reps_by_entry_qualifier(
520                            &child_group.repetitions,
521                            q,
522                        ));
523                    } else {
524                        next_instances.extend(child_group.repetitions.iter());
525                    }
526                }
527            }
528
529            current_instances = next_instances;
530        }
531
532        current_instances
533    }
534
535    /// Like `resolve_all_by_source_path` but also returns the direct parent
536    /// rep index that each leaf instance came from. The "direct parent" is the
537    /// group one level above the leaf in the path.
538    ///
539    /// For `"sg2.sg3"`: parent is the SG2 rep index.
540    /// For `"sg17.sg36.sg40"`: parent is the SG36 rep index (not SG17).
541    ///
542    /// For single-level paths, all indices are 0.
543    ///
544    /// Compute child rep indices for the leaf group in a source_path.
545    /// E.g., for "sg29.sg30", returns the position of each matched SG30 rep
546    /// within its parent SG29's SG30 child group.
547    fn compute_child_indices(
548        tree: &AssembledTree,
549        source_path: &str,
550        indexed: &[(usize, &AssembledGroupInstance)],
551    ) -> Vec<usize> {
552        let parts: Vec<&str> = source_path.split('.').collect();
553        if parts.len() < 2 {
554            return vec![];
555        }
556        // Navigate to the parent level and find the child group
557        let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
558        let first_group = match tree
559            .groups
560            .iter()
561            .find(|g| g.group_id.eq_ignore_ascii_case(first_id))
562        {
563            Some(g) => g,
564            None => return vec![],
565        };
566        let parent_reps: Vec<&AssembledGroupInstance> = if let Some(q) = first_qualifier {
567            find_all_reps_by_entry_qualifier(&first_group.repetitions, q)
568        } else {
569            first_group.repetitions.iter().collect()
570        };
571        // For 2-level paths (sg29.sg30), find the child group in the parent
572        let (child_id, _child_qualifier) = parse_source_path_part(parts[parts.len() - 1]);
573        let mut result = Vec::new();
574        for (_, inst) in indexed {
575            // Find which rep index this instance is at in the child group
576            let mut found = false;
577            for parent in &parent_reps {
578                if let Some(child_group) = parent
579                    .child_groups
580                    .iter()
581                    .find(|g| g.group_id.eq_ignore_ascii_case(child_id))
582                {
583                    if let Some(pos) = child_group
584                        .repetitions
585                        .iter()
586                        .position(|r| std::ptr::eq(r, *inst))
587                    {
588                        result.push(pos);
589                        found = true;
590                        break;
591                    }
592                }
593            }
594            if !found {
595                result.push(usize::MAX); // fallback
596            }
597        }
598        result
599    }
600
601    /// Returns `Vec<(parent_rep_index, &AssembledGroupInstance)>`.
602    pub fn resolve_all_with_parent_indices<'a>(
603        tree: &'a AssembledTree,
604        source_path: &str,
605    ) -> Vec<(usize, &'a AssembledGroupInstance)> {
606        let parts: Vec<&str> = source_path.split('.').collect();
607        if parts.is_empty() {
608            return vec![];
609        }
610
611        // First part: match against top-level groups
612        let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
613        let first_group = match tree
614            .groups
615            .iter()
616            .find(|g| g.group_id.eq_ignore_ascii_case(first_id))
617        {
618            Some(g) => g,
619            None => return vec![],
620        };
621
622        // If single-level path, just return instances with index 0
623        if parts.len() == 1 {
624            let instances: Vec<&AssembledGroupInstance> = if let Some(q) = first_qualifier {
625                find_all_reps_by_entry_qualifier(&first_group.repetitions, q)
626            } else {
627                first_group.repetitions.iter().collect()
628            };
629            return instances.into_iter().map(|i| (0, i)).collect();
630        }
631
632        // Multi-level: navigate tracking (parent_rep_idx, instance) at each level.
633        // At intermediate levels, parent_rep_idx is updated to the current rep's
634        // position within its group. At the leaf level, the parent_rep_idx from
635        // the previous level is preserved — giving us the DIRECT parent index.
636        let first_reps: Vec<(usize, &AssembledGroupInstance)> = if let Some(q) = first_qualifier {
637            let matching = find_all_reps_by_entry_qualifier(&first_group.repetitions, q);
638            let mut result = Vec::new();
639            for m in matching {
640                let idx = first_group
641                    .repetitions
642                    .iter()
643                    .position(|r| std::ptr::eq(r, m))
644                    .unwrap_or(0);
645                result.push((idx, m));
646            }
647            result
648        } else {
649            first_group.repetitions.iter().enumerate().collect()
650        };
651
652        let mut current: Vec<(usize, &AssembledGroupInstance)> = first_reps;
653        let remaining = &parts[1..];
654
655        for (level, part) in remaining.iter().enumerate() {
656            let is_leaf = level == remaining.len() - 1;
657            let (group_id, qualifier) = parse_source_path_part(part);
658            let mut next: Vec<(usize, &AssembledGroupInstance)> = Vec::new();
659
660            for (prev_parent_idx, instance) in &current {
661                if let Some(child_group) = instance
662                    .child_groups
663                    .iter()
664                    .find(|g| g.group_id.eq_ignore_ascii_case(group_id))
665                {
666                    let matching: Vec<(usize, &AssembledGroupInstance)> = if let Some(q) = qualifier
667                    {
668                        let filtered =
669                            find_all_reps_by_entry_qualifier(&child_group.repetitions, q);
670                        filtered
671                            .into_iter()
672                            .map(|m| {
673                                let idx = child_group
674                                    .repetitions
675                                    .iter()
676                                    .position(|r| std::ptr::eq(r, m))
677                                    .unwrap_or(0);
678                                (idx, m)
679                            })
680                            .collect()
681                    } else {
682                        child_group.repetitions.iter().enumerate().collect()
683                    };
684
685                    for (rep_idx, child_rep) in matching {
686                        if is_leaf {
687                            // At the leaf: keep the parent index from the previous level
688                            next.push((*prev_parent_idx, child_rep));
689                        } else {
690                            // At intermediate: pass down the current rep index
691                            next.push((rep_idx, child_rep));
692                        }
693                    }
694                }
695            }
696
697            current = next;
698        }
699
700        current
701    }
702
703    /// Extract a field from a group instance by path.
704    ///
705    /// Supports qualifier-based segment selection with `tag[qualifier]` syntax:
706    /// - `"dtm.0.1"` → first DTM segment, elements\[0\]\[1\]
707    /// - `"dtm[92].0.1"` → DTM where elements\[0\]\[0\] == "92", then elements\[0\]\[1\]
708    pub fn extract_from_instance(instance: &AssembledGroupInstance, path: &str) -> Option<String> {
709        let parts: Vec<&str> = path.split('.').collect();
710        if parts.is_empty() {
711            return None;
712        }
713
714        // Parse segment tag, optional qualifier, and occurrence index:
715        // "dtm[92]" → ("DTM", Some("92"), 0), "rff[Z34,1]" → ("RFF", Some("Z34"), 1)
716        let (segment_tag, qualifier, occurrence) = parse_tag_qualifier(parts[0]);
717
718        let segment = if let Some(q) = qualifier {
719            instance
720                .segments
721                .iter()
722                .filter(|s| {
723                    s.tag.eq_ignore_ascii_case(&segment_tag)
724                        && s.elements
725                            .first()
726                            .and_then(|e| e.first())
727                            .map(|v| v.as_str())
728                            == Some(q)
729                })
730                .nth(occurrence)?
731        } else {
732            instance
733                .segments
734                .iter()
735                .filter(|s| s.tag.eq_ignore_ascii_case(&segment_tag))
736                .nth(occurrence)?
737        };
738
739        Self::resolve_field_path(segment, &parts[1..])
740    }
741
742    /// Extract ALL matching values from a group instance for a collect-all path.
743    ///
744    /// Used with wildcard occurrence syntax `tag[qualifier,*]` to collect values
745    /// from every segment matching the qualifier, not just the Nth one.
746    /// Returns a `Vec<String>` of all extracted values in segment order.
747    pub fn extract_all_from_instance(instance: &AssembledGroupInstance, path: &str) -> Vec<String> {
748        let parts: Vec<&str> = path.split('.').collect();
749        if parts.is_empty() {
750            return vec![];
751        }
752
753        let (segment_tag, qualifier, _) = parse_tag_qualifier(parts[0]);
754
755        let matching_segments: Vec<&AssembledSegment> = if let Some(q) = qualifier {
756            instance
757                .segments
758                .iter()
759                .filter(|s| {
760                    s.tag.eq_ignore_ascii_case(&segment_tag)
761                        && s.elements
762                            .first()
763                            .and_then(|e| e.first())
764                            .map(|v| v.as_str())
765                            == Some(q)
766                })
767                .collect()
768        } else {
769            instance
770                .segments
771                .iter()
772                .filter(|s| s.tag.eq_ignore_ascii_case(&segment_tag))
773                .collect()
774        };
775
776        matching_segments
777            .into_iter()
778            .filter_map(|seg| Self::resolve_field_path(seg, &parts[1..]))
779            .collect()
780    }
781
782    /// Map all fields in a definition from the assembled tree to a BO4E JSON object.
783    ///
784    /// `group_path` is the definition's `source_group` (may be dotted, e.g., "SG4.SG5").
785    /// An empty `source_group` maps root-level segments (BGM, DTM, etc.).
786    /// Returns a flat JSON object with target field names as keys.
787    pub fn map_forward(
788        &self,
789        tree: &AssembledTree,
790        def: &MappingDefinition,
791        repetition: usize,
792    ) -> serde_json::Value {
793        self.map_forward_inner(tree, def, repetition, true)
794    }
795
796    /// Inner implementation with enrichment control.
797    fn map_forward_inner(
798        &self,
799        tree: &AssembledTree,
800        def: &MappingDefinition,
801        repetition: usize,
802        enrich_codes: bool,
803    ) -> serde_json::Value {
804        let mut result = serde_json::Map::new();
805
806        // Root-level mapping: source_group is empty → use tree's own segments.
807        // Include all root segments (both pre-group and post-group, e.g., summary
808        // MOA after UNS+S in REMADV) plus any inter_group_segments (e.g., UNS+S
809        // consumed between groups by the assembler).
810        if def.meta.source_group.is_empty() {
811            let mut all_root_segs = tree.segments.clone();
812            for segs in tree.inter_group_segments.values() {
813                all_root_segs.extend(segs.iter().cloned());
814            }
815            let root_instance = AssembledGroupInstance {
816                segments: all_root_segs,
817                child_groups: vec![],
818                entry_mig_number: None,
819                variant_mig_numbers: vec![],
820                skipped_segments: Vec::new(),
821                skipped_positions: Vec::new(),
822            };
823            self.extract_fields_from_instance(&root_instance, def, &mut result, enrich_codes);
824            return serde_json::Value::Object(result);
825        }
826
827        // Try source_path-based resolution when:
828        //   1. source_path has qualifier suffixes (e.g., "sg4.sg8_z98.sg10")
829        //   2. source_group has no explicit :N indices (those take priority)
830        // This allows definitions without positional indices to navigate via
831        // entry-segment qualifiers (e.g., SEQ qualifier Z98).
832        let instance = if let Some(ref sp) = def.meta.source_path {
833            if has_source_path_qualifiers(sp) && !def.meta.source_group.contains(':') {
834                Self::resolve_by_source_path(tree, sp).or_else(|| {
835                    Self::resolve_group_instance(tree, &def.meta.source_group, repetition)
836                })
837            } else {
838                Self::resolve_group_instance(tree, &def.meta.source_group, repetition)
839            }
840        } else {
841            Self::resolve_group_instance(tree, &def.meta.source_group, repetition)
842        };
843
844        if let Some(instance) = instance {
845            // repeat_on_tag: iterate over all segments of that tag, producing an array
846            if let Some(ref tag) = def.meta.repeat_on_tag {
847                let matching: Vec<_> = instance
848                    .segments
849                    .iter()
850                    .filter(|s| s.tag.eq_ignore_ascii_case(tag))
851                    .collect();
852
853                if matching.len() > 1 {
854                    let mut arr = Vec::new();
855                    for seg in &matching {
856                        let sub_instance = AssembledGroupInstance {
857                            segments: vec![(*seg).clone()],
858                            child_groups: vec![],
859                            entry_mig_number: None,
860                            variant_mig_numbers: vec![],
861                            skipped_segments: Vec::new(),
862                            skipped_positions: Vec::new(),
863                        };
864                        let mut elem_result = serde_json::Map::new();
865                        self.extract_fields_from_instance(
866                            &sub_instance,
867                            def,
868                            &mut elem_result,
869                            enrich_codes,
870                        );
871                        if !elem_result.is_empty() {
872                            arr.push(serde_json::Value::Object(elem_result));
873                        }
874                    }
875                    if !arr.is_empty() {
876                        return serde_json::Value::Array(arr);
877                    }
878                }
879            }
880
881            self.extract_fields_from_instance(instance, def, &mut result, enrich_codes);
882        }
883
884        serde_json::Value::Object(result)
885    }
886
887    /// Extract all fields from an instance into a result map.
888    ///
889    /// When a `code_lookup` is configured, code-type fields are emitted as
890    /// `{"code": "E01", "meaning": "..."}` objects. Data-type fields remain plain strings.
891    fn extract_fields_from_instance(
892        &self,
893        instance: &AssembledGroupInstance,
894        def: &MappingDefinition,
895        result: &mut serde_json::Map<String, serde_json::Value>,
896        enrich_codes: bool,
897    ) {
898        for (path, field_mapping) in &def.fields {
899            let (target, enum_map) = match field_mapping {
900                FieldMapping::Simple(t) => (t.as_str(), None),
901                FieldMapping::Structured(s) => (s.target.as_str(), s.enum_map.as_ref()),
902                FieldMapping::Nested(_) => continue,
903            };
904            if target.is_empty() {
905                continue;
906            }
907            if let Some(val) = Self::extract_from_instance(instance, path) {
908                let mapped_val = if let Some(map) = enum_map {
909                    map.get(&val).cloned().unwrap_or_else(|| val.clone())
910                } else {
911                    val.clone()
912                };
913
914                // Enrich code fields with meaning from PID schema
915                if enrich_codes {
916                    if let (Some(ref code_lookup), Some(ref source_path)) =
917                        (&self.code_lookup, &def.meta.source_path)
918                    {
919                        let parts: Vec<&str> = path.split('.').collect();
920                        let (seg_tag, _qualifier, _occ) = parse_tag_qualifier(parts[0]);
921                        let (element_idx, component_idx) =
922                            Self::parse_element_component(&parts[1..]);
923                        let disc_qualifier = Self::discriminator_qualifier(def);
924                        let q = disc_qualifier.as_deref();
925
926                        if code_lookup.is_code_field_q(
927                            source_path,
928                            &seg_tag,
929                            q,
930                            element_idx,
931                            component_idx,
932                        ) {
933                            // Class C: PID self-reference — emit a plain string,
934                            // skipping {code, meaning, enum} decoration when the
935                            // schema's only allowed value at this position is the
936                            // PID itself.
937                            if let Some(ref pid) = self.current_pid {
938                                if code_lookup.is_pid_self_reference(
939                                    source_path,
940                                    &seg_tag,
941                                    q,
942                                    element_idx,
943                                    component_idx,
944                                    pid,
945                                ) {
946                                    set_nested_value(result, target, mapped_val);
947                                    continue;
948                                }
949                            }
950
951                            // Look up the original EDIFACT value for enrichment,
952                            // since schema codes use raw values (e.g., "293")
953                            // not enum_map targets (e.g., "BDEW").
954                            let enrichment = code_lookup.enrichment_for_q(
955                                source_path,
956                                &seg_tag,
957                                q,
958                                element_idx,
959                                component_idx,
960                                &val,
961                            );
962                            let meaning = enrichment
963                                .map(|e| serde_json::Value::String(e.meaning.clone()))
964                                .unwrap_or(serde_json::Value::Null);
965
966                            let mut obj = serde_json::Map::new();
967                            obj.insert("code".into(), serde_json::json!(mapped_val));
968                            obj.insert("meaning".into(), meaning);
969                            if let Some(enum_key) = enrichment.and_then(|e| e.enum_key.as_ref()) {
970                                obj.insert("enum".into(), serde_json::json!(enum_key));
971                            }
972                            let enriched = serde_json::Value::Object(obj);
973                            set_nested_value_json(result, target, enriched);
974                            continue;
975                        }
976                    }
977                }
978
979                set_nested_value(result, target, mapped_val);
980            }
981        }
982    }
983
984    /// Extract the discriminator's qualifier value from a definition's `[meta]`.
985    ///
986    /// `discriminator` strings look like `"RFF.0.0=Z13"` (numeric, post path-resolution)
987    /// or `"RFF.c506.d1153=TN"` (named, pre-resolution). The qualifier is the
988    /// substring after the first `=`. Returns `None` when no discriminator is set
989    /// or the format is unexpected.
990    fn discriminator_qualifier(def: &MappingDefinition) -> Option<String> {
991        def.meta
992            .discriminator
993            .as_deref()
994            .and_then(|d| d.split_once('=').map(|(_, v)| v.to_string()))
995    }
996
997    /// Map a PID struct field's segments to BO4E JSON.
998    ///
999    /// `segments` are the `OwnedSegment`s from a PID wrapper field.
1000    /// Converts to `AssembledSegment` format for compatibility with existing
1001    /// field extraction logic, then applies the definition's field mappings.
1002    pub fn map_forward_from_segments(
1003        &self,
1004        segments: &[OwnedSegment],
1005        def: &MappingDefinition,
1006    ) -> serde_json::Value {
1007        let assembled_segments: Vec<AssembledSegment> = segments
1008            .iter()
1009            .map(|s| AssembledSegment {
1010                tag: s.id.clone(),
1011                elements: s.elements.clone(),
1012                mig_number: None,
1013                segment_number: Some(s.segment_number),
1014            })
1015            .collect();
1016
1017        let instance = AssembledGroupInstance {
1018            segments: assembled_segments,
1019            child_groups: vec![],
1020            entry_mig_number: None,
1021            variant_mig_numbers: vec![],
1022            skipped_segments: Vec::new(),
1023            skipped_positions: Vec::new(),
1024        };
1025
1026        let mut result = serde_json::Map::new();
1027        self.extract_fields_from_instance(&instance, def, &mut result, true);
1028        serde_json::Value::Object(result)
1029    }
1030
1031    // ── Reverse mapping: BO4E → tree ──
1032
1033    /// Map a BO4E JSON object back to an assembled group instance.
1034    ///
1035    /// Uses the definition's field mappings to populate segment elements.
1036    /// Fields with `default` values are used when no BO4E value is present
1037    /// (useful for fixed qualifiers like LOC qualifier "Z16").
1038    ///
1039    /// Supports:
1040    /// - Named paths: `"d3227"` → element\[0\]\[0\], `"c517.d3225"` → element\[1\]\[0\]
1041    /// - Numeric index: `"0"` → element\[0\]\[0\], `"1.2"` → element\[1\]\[2\]
1042    /// - Qualifier selection: `"dtm[92].0.1"` → DTM segment with qualifier "92"
1043    pub fn map_reverse(
1044        &self,
1045        bo4e_value: &serde_json::Value,
1046        def: &MappingDefinition,
1047    ) -> AssembledGroupInstance {
1048        // repeat_on_tag + array input: reverse each element independently, merge segments
1049        if def.meta.repeat_on_tag.is_some() {
1050            if let Some(arr) = bo4e_value.as_array() {
1051                let mut all_segments = Vec::new();
1052                for elem in arr {
1053                    let sub = self.map_reverse_single(elem, def);
1054                    all_segments.extend(sub.segments);
1055                }
1056                return AssembledGroupInstance {
1057                    segments: all_segments,
1058                    child_groups: vec![],
1059                    entry_mig_number: None,
1060                    variant_mig_numbers: vec![],
1061                    skipped_segments: Vec::new(),
1062                    skipped_positions: Vec::new(),
1063                };
1064            }
1065        }
1066        self.map_reverse_single(bo4e_value, def)
1067    }
1068
1069    fn map_reverse_single(
1070        &self,
1071        bo4e_value: &serde_json::Value,
1072        def: &MappingDefinition,
1073    ) -> AssembledGroupInstance {
1074        // Collect (segment_key, element_index, component_index, value) tuples.
1075        // segment_key includes qualifier for disambiguation: "DTM" or "DTM[92]".
1076        let mut field_values: Vec<(String, String, usize, usize, String)> =
1077            Vec::with_capacity(def.fields.len());
1078
1079        // Track whether any field with a non-empty target resolved to an actual
1080        // BO4E value.  When a definition has data fields but none resolved to
1081        // values, only defaults (qualifiers) would be emitted — producing phantom
1082        // segments for groups not present in the original EDIFACT message.
1083        // Definitions with ONLY qualifier/default fields (no data targets) are
1084        // "container" definitions (e.g., SEQ entry segments) and are always kept.
1085        let mut has_real_data = false;
1086        let mut has_data_fields = false;
1087        // Per-segment phantom tracking: segments with data fields but no resolved
1088        // data are phantoms — their entries should be removed from field_values.
1089        let mut seg_has_data_field: HashSet<String> = HashSet::new();
1090        let mut seg_has_real_data: HashSet<String> = HashSet::new();
1091        let mut injected_qualifiers: HashSet<String> = HashSet::new();
1092
1093        for (path, field_mapping) in &def.fields {
1094            let (target, default, enum_map, when_filled) = match field_mapping {
1095                FieldMapping::Simple(t) => (t.as_str(), None, None, None),
1096                FieldMapping::Structured(s) => (
1097                    s.target.as_str(),
1098                    s.default.as_ref(),
1099                    s.enum_map.as_ref(),
1100                    s.when_filled.as_ref(),
1101                ),
1102                FieldMapping::Nested(_) => continue,
1103            };
1104
1105            let parts: Vec<&str> = path.split('.').collect();
1106            if parts.len() < 2 {
1107                continue;
1108            }
1109
1110            let (seg_tag, qualifier, _occ) = parse_tag_qualifier(parts[0]);
1111            // Use the raw first part as segment key to group fields by segment instance.
1112            // Indexed qualifiers like "RFF[Z34,1]" produce a distinct key from "RFF[Z34]".
1113            let seg_key = parts[0].to_uppercase();
1114            let sub_path = &parts[1..];
1115
1116            // Determine (element_idx, component_idx) from path
1117            let (element_idx, component_idx) = if let Ok(ei) = sub_path[0].parse::<usize>() {
1118                let ci = if sub_path.len() > 1 {
1119                    sub_path[1].parse::<usize>().unwrap_or(0)
1120                } else {
1121                    0
1122                };
1123                (ei, ci)
1124            } else {
1125                match sub_path.len() {
1126                    1 => (0, 0),
1127                    2 => (1, 0),
1128                    _ => continue,
1129                }
1130            };
1131
1132            // Try BO4E value first, fall back to default
1133            let val = if target.is_empty() {
1134                match (default, when_filled) {
1135                    // has when_filled → conditional injection
1136                    (Some(d), Some(fields)) => {
1137                        let any_filled = fields
1138                            .iter()
1139                            .any(|f| self.populate_field(bo4e_value, f).is_some());
1140                        if any_filled {
1141                            // A successful when_filled check confirms real data
1142                            // exists — prevent phantom suppression.
1143                            has_real_data = true;
1144                            Some(d.clone())
1145                        } else {
1146                            None
1147                        }
1148                    }
1149                    // no when_filled → unconditional (backward compat)
1150                    (Some(d), None) => Some(d.clone()),
1151                    (None, _) => None,
1152                }
1153            } else {
1154                has_data_fields = true;
1155                seg_has_data_field.insert(seg_key.clone());
1156                let bo4e_val = self.populate_field(bo4e_value, target);
1157                if bo4e_val.is_some() {
1158                    has_real_data = true;
1159                    seg_has_real_data.insert(seg_key.clone());
1160                }
1161                // Apply reverse enum_map: BO4E value → EDIFACT value
1162                let mapped_val = match (bo4e_val, enum_map) {
1163                    (Some(v), Some(map)) => {
1164                        // Reverse lookup: find EDIFACT key for BO4E value
1165                        map.iter()
1166                            .find(|(_, bo4e_v)| *bo4e_v == &v)
1167                            .map(|(edifact_k, _)| edifact_k.clone())
1168                            .or(Some(v))
1169                    }
1170                    (v, _) => v,
1171                };
1172                mapped_val.or_else(|| default.cloned())
1173            };
1174
1175            if let Some(val) = val {
1176                field_values.push((
1177                    seg_key.clone(),
1178                    seg_tag.clone(),
1179                    element_idx,
1180                    component_idx,
1181                    val,
1182                ));
1183            }
1184
1185            // If there's a qualifier, also inject it at elements[0][0]
1186            if let Some(q) = qualifier {
1187                if injected_qualifiers.insert(seg_key.clone()) {
1188                    field_values.push((seg_key, seg_tag, 0, 0, q.to_string()));
1189                }
1190            }
1191        }
1192
1193        // Per-segment phantom prevention for qualified segments: remove entries
1194        // for segments using tag[qualifier] syntax (e.g., FTX[ACB], DTM[Z07])
1195        // that have data fields but none resolved to actual BO4E values.  This
1196        // prevents phantom segments when a definition maps multiple segment types
1197        // and optional qualified segments are not in the original message.
1198        // Unqualified segments (plain tags like SEQ, IDE) are always kept — they
1199        // are typically entry/mandatory segments of their group.
1200        field_values.retain(|(seg_key, _, _, _, _)| {
1201            if !seg_key.contains('[') {
1202                return true; // unqualified segments always kept
1203            }
1204            !seg_has_data_field.contains(seg_key) || seg_has_real_data.contains(seg_key)
1205        });
1206
1207        // If the definition has data fields but none resolved to actual BO4E values,
1208        // return an empty instance to prevent phantom segments for groups not
1209        // present in the original EDIFACT message.  Definitions with only
1210        // qualifier/default fields (has_data_fields=false) are always kept.
1211        if has_data_fields && !has_real_data {
1212            return AssembledGroupInstance {
1213                segments: vec![],
1214                child_groups: vec![],
1215                entry_mig_number: None,
1216                variant_mig_numbers: vec![],
1217                skipped_segments: Vec::new(),
1218                skipped_positions: Vec::new(),
1219            };
1220        }
1221
1222        // Build segments with elements/components in correct positions.
1223        // Group by segment_key to create separate segments for "DTM[92]" vs "DTM[93]".
1224        let mut segments: Vec<AssembledSegment> = Vec::with_capacity(field_values.len());
1225        let mut seen_keys: HashMap<String, usize> = HashMap::new();
1226
1227        for (seg_key, seg_tag, element_idx, component_idx, val) in &field_values {
1228            let seg = if let Some(&pos) = seen_keys.get(seg_key) {
1229                &mut segments[pos]
1230            } else {
1231                let pos = segments.len();
1232                seen_keys.insert(seg_key.clone(), pos);
1233                segments.push(AssembledSegment {
1234                    tag: seg_tag.clone(),
1235                    elements: vec![],
1236                    mig_number: None,
1237                    segment_number: None,
1238                });
1239                &mut segments[pos]
1240            };
1241
1242            while seg.elements.len() <= *element_idx {
1243                seg.elements.push(vec![]);
1244            }
1245            while seg.elements[*element_idx].len() <= *component_idx {
1246                seg.elements[*element_idx].push(String::new());
1247            }
1248            seg.elements[*element_idx][*component_idx] = val.clone();
1249        }
1250
1251        // Pad intermediate empty elements: any [] between position 0 and the last
1252        // populated position becomes [""] so the EDIFACT renderer emits the `+` separator.
1253        for seg in &mut segments {
1254            let last_populated = seg.elements.iter().rposition(|e| !e.is_empty());
1255            if let Some(last_idx) = last_populated {
1256                for i in 0..last_idx {
1257                    if seg.elements[i].is_empty() {
1258                        seg.elements[i] = vec![String::new()];
1259                    }
1260                }
1261            }
1262        }
1263
1264        // MIG-aware trailing padding: extend each segment to the MIG-defined element count.
1265        if let Some(ref ss) = self.segment_structure {
1266            for seg in &mut segments {
1267                if let Some(expected) = ss.element_count(&seg.tag) {
1268                    while seg.elements.len() < expected {
1269                        seg.elements.push(vec![String::new()]);
1270                    }
1271                }
1272            }
1273        }
1274
1275        AssembledGroupInstance {
1276            segments,
1277            child_groups: vec![],
1278            entry_mig_number: None,
1279            variant_mig_numbers: vec![],
1280            skipped_segments: Vec::new(),
1281            skipped_positions: Vec::new(),
1282        }
1283    }
1284
1285    /// Resolve a field path within a segment to extract a value.
1286    ///
1287    /// Two path conventions are supported:
1288    ///
1289    /// **Named paths** (backward compatible):
1290    /// - 1-part `"d3227"` → elements\[0\]\[0\]
1291    /// - 2-part `"c517.d3225"` → elements\[1\]\[0\]
1292    ///
1293    /// **Numeric index paths** (for multi-component access):
1294    /// - `"0"` → elements\[0\]\[0\]
1295    /// - `"1.0"` → elements\[1\]\[0\]
1296    /// - `"1.2"` → elements\[1\]\[2\]
1297    fn resolve_field_path(segment: &AssembledSegment, path: &[&str]) -> Option<String> {
1298        if path.is_empty() {
1299            return None;
1300        }
1301
1302        // Numeric paths only: index-based resolution.
1303        if let Ok(element_idx) = path[0].parse::<usize>() {
1304            let component_idx = if path.len() > 1 {
1305                path[1].parse::<usize>().unwrap_or(0)
1306            } else {
1307                0
1308            };
1309            return segment
1310                .elements
1311                .get(element_idx)?
1312                .get(component_idx)
1313                .filter(|v| !v.is_empty())
1314                .cloned();
1315        }
1316
1317        // Non-numeric path[0] indicates an EDIFACT ID path that the PathResolver
1318        // failed to normalize (e.g. composite/element absent from any loaded PID
1319        // schema). Returning None lets the field be omitted from output instead
1320        // of silently guessing element index 1, which previously surfaced
1321        // unrelated data (e.g. NAD c819.d3229 read as c082.d3039 / rollencodenummer).
1322        None
1323    }
1324
1325    /// Parse element and component indices from path parts after the segment tag.
1326    /// E.g., ["2"] -> (2, 0), ["0", "3"] -> (0, 3), ["1", "0"] -> (1, 0)
1327    fn parse_element_component(parts: &[&str]) -> (usize, usize) {
1328        if parts.is_empty() {
1329            return (0, 0);
1330        }
1331        let element_idx = parts[0].parse::<usize>().unwrap_or(0);
1332        let component_idx = if parts.len() > 1 {
1333            parts[1].parse::<usize>().unwrap_or(0)
1334        } else {
1335            0
1336        };
1337        (element_idx, component_idx)
1338    }
1339
1340    /// Extract a value from a BO4E JSON object by target field name.
1341    /// Supports dotted paths like "nested.field_name".
1342    pub fn populate_field(
1343        &self,
1344        bo4e_value: &serde_json::Value,
1345        target_field: &str,
1346    ) -> Option<String> {
1347        let mut current = bo4e_value;
1348        for part in target_field.split('.') {
1349            current = current.get(part)?;
1350        }
1351        // Handle enriched code objects: {"code": "Z15", "meaning": "..."}
1352        if let Some(code) = current.get("code").and_then(|v| v.as_str()) {
1353            return Some(code.to_string());
1354        }
1355        current.as_str().map(|s| s.to_string())
1356    }
1357
1358    /// Build a segment from BO4E values using the reverse mapping.
1359    pub fn build_segment_from_bo4e(
1360        &self,
1361        bo4e_value: &serde_json::Value,
1362        segment_tag: &str,
1363        target_field: &str,
1364    ) -> AssembledSegment {
1365        let value = self.populate_field(bo4e_value, target_field);
1366        let elements = if let Some(val) = value {
1367            vec![vec![val]]
1368        } else {
1369            vec![]
1370        };
1371        AssembledSegment {
1372            tag: segment_tag.to_uppercase(),
1373            elements,
1374            mig_number: None,
1375            segment_number: None,
1376        }
1377    }
1378
1379    // ── Multi-entity forward mapping ──
1380
1381    /// Parse a discriminator string (e.g., "SEQ.0.0=Z79") and find the matching
1382    /// repetition index within the given group path.
1383    ///
1384    /// Discriminator format: `"TAG.element_idx.component_idx=expected_value"`
1385    /// Scans all repetitions of the leaf group and returns the first rep index
1386    /// where the entry segment matches.
1387    pub fn resolve_repetition(
1388        tree: &AssembledTree,
1389        group_path: &str,
1390        discriminator: &str,
1391    ) -> Option<usize> {
1392        let (spec, expected) = discriminator.split_once('=')?;
1393        let parts: Vec<&str> = spec.split('.').collect();
1394        if parts.len() != 3 {
1395            return None;
1396        }
1397        let tag = parts[0];
1398        let element_idx: usize = parts[1].parse().ok()?;
1399        let component_idx: usize = parts[2].parse().ok()?;
1400
1401        // Navigate to the parent and get the leaf group with all its repetitions
1402        let path_parts: Vec<&str> = group_path.split('.').collect();
1403
1404        let leaf_group = if path_parts.len() == 1 {
1405            let (group_id, _) = parse_group_spec(path_parts[0]);
1406            tree.groups.iter().find(|g| g.group_id == group_id)?
1407        } else {
1408            // Navigate to the parent instance, then find the leaf group
1409            let parent_parts = &path_parts[..path_parts.len() - 1];
1410            let mut current_instance = {
1411                let (first_id, first_rep) = parse_group_spec(parent_parts[0]);
1412                let first_group = tree.groups.iter().find(|g| g.group_id == first_id)?;
1413                first_group.repetitions.get(first_rep.unwrap_or(0))?
1414            };
1415            for part in &parent_parts[1..] {
1416                let (group_id, explicit_rep) = parse_group_spec(part);
1417                let child_group = current_instance
1418                    .child_groups
1419                    .iter()
1420                    .find(|g| g.group_id == group_id)?;
1421                current_instance = child_group.repetitions.get(explicit_rep.unwrap_or(0))?;
1422            }
1423            let (leaf_id, _) = parse_group_spec(path_parts.last()?);
1424            current_instance
1425                .child_groups
1426                .iter()
1427                .find(|g| g.group_id == leaf_id)?
1428        };
1429
1430        // Scan all repetitions for the matching discriminator
1431        let expected_values: Vec<&str> = expected.split('|').collect();
1432        for (rep_idx, instance) in leaf_group.repetitions.iter().enumerate() {
1433            let matches = instance.segments.iter().any(|s| {
1434                s.tag.eq_ignore_ascii_case(tag)
1435                    && s.elements
1436                        .get(element_idx)
1437                        .and_then(|e| e.get(component_idx))
1438                        .map(|v| expected_values.iter().any(|ev| v == ev))
1439                        .unwrap_or(false)
1440            });
1441            if matches {
1442                return Some(rep_idx);
1443            }
1444        }
1445
1446        None
1447    }
1448
1449    /// Like `resolve_repetition`, but returns ALL matching rep indices instead of just the first.
1450    ///
1451    /// This is used for multi-Zeitscheibe support where multiple SG6 reps may match
1452    /// the same discriminator (e.g., multiple RFF+Z49 time slices).
1453    pub fn resolve_all_repetitions(
1454        tree: &AssembledTree,
1455        group_path: &str,
1456        discriminator: &str,
1457    ) -> Vec<usize> {
1458        let Some((spec, expected)) = discriminator.split_once('=') else {
1459            return Vec::new();
1460        };
1461        let parts: Vec<&str> = spec.split('.').collect();
1462        if parts.len() != 3 {
1463            return Vec::new();
1464        }
1465        let tag = parts[0];
1466        let element_idx: usize = match parts[1].parse() {
1467            Ok(v) => v,
1468            Err(_) => return Vec::new(),
1469        };
1470        let component_idx: usize = match parts[2].parse() {
1471            Ok(v) => v,
1472            Err(_) => return Vec::new(),
1473        };
1474
1475        // Navigate to the parent and get the leaf group with all its repetitions
1476        let path_parts: Vec<&str> = group_path.split('.').collect();
1477
1478        let leaf_group = if path_parts.len() == 1 {
1479            let (group_id, _) = parse_group_spec(path_parts[0]);
1480            match tree.groups.iter().find(|g| g.group_id == group_id) {
1481                Some(g) => g,
1482                None => return Vec::new(),
1483            }
1484        } else {
1485            let parent_parts = &path_parts[..path_parts.len() - 1];
1486            let mut current_instance = {
1487                let (first_id, first_rep) = parse_group_spec(parent_parts[0]);
1488                let first_group = match tree.groups.iter().find(|g| g.group_id == first_id) {
1489                    Some(g) => g,
1490                    None => return Vec::new(),
1491                };
1492                match first_group.repetitions.get(first_rep.unwrap_or(0)) {
1493                    Some(i) => i,
1494                    None => return Vec::new(),
1495                }
1496            };
1497            for part in &parent_parts[1..] {
1498                let (group_id, explicit_rep) = parse_group_spec(part);
1499                let child_group = match current_instance
1500                    .child_groups
1501                    .iter()
1502                    .find(|g| g.group_id == group_id)
1503                {
1504                    Some(g) => g,
1505                    None => return Vec::new(),
1506                };
1507                current_instance = match child_group.repetitions.get(explicit_rep.unwrap_or(0)) {
1508                    Some(i) => i,
1509                    None => return Vec::new(),
1510                };
1511            }
1512            let (leaf_id, _) = match path_parts.last() {
1513                Some(p) => parse_group_spec(p),
1514                None => return Vec::new(),
1515            };
1516            match current_instance
1517                .child_groups
1518                .iter()
1519                .find(|g| g.group_id == leaf_id)
1520            {
1521                Some(g) => g,
1522                None => return Vec::new(),
1523            }
1524        };
1525
1526        // Parse optional occurrence index from expected value: "TN#1" → ("TN", Some(1))
1527        let (expected_raw, occurrence) = parse_discriminator_occurrence(expected);
1528
1529        // Collect ALL matching rep indices
1530        let expected_values: Vec<&str> = expected_raw.split('|').collect();
1531        let mut result = Vec::new();
1532        for (rep_idx, instance) in leaf_group.repetitions.iter().enumerate() {
1533            let matches = instance.segments.iter().any(|s| {
1534                s.tag.eq_ignore_ascii_case(tag)
1535                    && s.elements
1536                        .get(element_idx)
1537                        .and_then(|e| e.get(component_idx))
1538                        .map(|v| expected_values.iter().any(|ev| v == ev))
1539                        .unwrap_or(false)
1540            });
1541            if matches {
1542                result.push(rep_idx);
1543            }
1544        }
1545
1546        // If occurrence index specified, return only that match
1547        if let Some(occ) = occurrence {
1548            result.into_iter().nth(occ).into_iter().collect()
1549        } else {
1550            result
1551        }
1552    }
1553
1554    /// Resolve a discriminated instance using source_path for parent navigation.
1555    ///
1556    /// Like `resolve_repetition` + `resolve_group_instance`, but navigates to the
1557    /// parent group via source_path qualifier suffixes. Returns the matching instance
1558    /// directly (not just a rep index) to avoid re-navigation in `map_forward_inner`.
1559    ///
1560    /// For example, `source_path = "sg4.sg8_z98.sg10"` with `discriminator = "CCI.2.0=ZB3"`
1561    /// navigates to the SG8 instance with SEQ qualifier Z98, then finds the SG10 rep
1562    /// where CCI element 2 component 0 equals "ZB3".
1563    /// Map all definitions against a tree, returning a JSON object with entity names as keys.
1564    ///
1565    /// For each definition:
1566    /// - Has discriminator → find matching rep via `resolve_repetition`, map single instance
1567    /// - Root-level (empty source_group) → map rep 0 as single object
1568    /// - No discriminator, 1 rep in tree → map as single object
1569    /// - No discriminator, multiple reps in tree → map ALL reps into a JSON array
1570    ///
1571    /// When multiple definitions share the same `entity` name, their fields are
1572    /// deep-merged into a single JSON object. This allows related TOML files
1573    /// (e.g., LOC location + SEQ info + SG10 characteristics) to contribute
1574    /// fields to the same BO4E entity.
1575    pub fn map_all_forward(&self, tree: &AssembledTree) -> serde_json::Value {
1576        self.map_all_forward_inner(tree, true).0
1577    }
1578
1579    /// Like [`map_all_forward`](Self::map_all_forward) but with explicit
1580    /// `enrich_codes` control (when `false`, code fields are plain strings
1581    /// instead of `{"code": …, "meaning": …}` objects).
1582    pub fn map_all_forward_enriched(
1583        &self,
1584        tree: &AssembledTree,
1585        enrich_codes: bool,
1586    ) -> serde_json::Value {
1587        self.map_all_forward_inner(tree, enrich_codes).0
1588    }
1589
1590    /// Inner implementation with enrichment control.
1591    ///
1592    /// Returns `(json_value, nesting_info, dp_routing)` where:
1593    /// - `nesting_info` maps entity keys to the parent rep index for each
1594    ///   child element (used by the reverse mapper to distribute nested
1595    ///   group children among their parent reps).
1596    /// - `dp_routing` carries side-channel metadata for any NAD+DP segments
1597    ///   that were routed out of marktteilnehmer[] into a lokation entity
1598    ///   (Phase 8). The reverse mapper consumes it to rebuild the segment.
1599    fn map_all_forward_inner(
1600        &self,
1601        tree: &AssembledTree,
1602        enrich_codes: bool,
1603    ) -> (
1604        serde_json::Value,
1605        std::collections::HashMap<String, Vec<usize>>,
1606        DpRouting,
1607    ) {
1608        self.map_all_forward_inner_with_tx(tree, enrich_codes, self.transaction_group.as_deref())
1609    }
1610
1611    /// Like `map_all_forward_inner` but with an explicit transaction-group
1612    /// override. Used by `map_interchange`, which knows the tx group even when
1613    /// the caller-supplied tx_engine wasn't built with `with_transaction_group`.
1614    fn map_all_forward_inner_with_tx(
1615        &self,
1616        tree: &AssembledTree,
1617        enrich_codes: bool,
1618        tx_group_override: Option<&str>,
1619    ) -> (
1620        serde_json::Value,
1621        std::collections::HashMap<String, Vec<usize>>,
1622        DpRouting,
1623    ) {
1624        let mut result = serde_json::Map::new();
1625        let mut nesting_info: std::collections::HashMap<String, Vec<usize>> =
1626            std::collections::HashMap::new();
1627
1628        for def in &self.definitions {
1629            let entity = &def.meta.entity;
1630
1631            let bo4e = if let Some(ref disc) = def.meta.discriminator {
1632                // Has discriminator — resolve to matching rep(s).
1633                // Use source_path navigation when qualifiers are present
1634                // (e.g., "sg4.sg8_z98.sg10" navigates to Z98's SG10 reps,
1635                //  "sg4.sg5_z17" finds all LOC+Z17 when there are multiple).
1636                let use_source_path = def
1637                    .meta
1638                    .source_path
1639                    .as_ref()
1640                    .is_some_and(|sp| has_source_path_qualifiers(sp));
1641                if use_source_path {
1642                    // Navigate via source_path, then filter by discriminator.
1643                    let sp = def.meta.source_path.as_deref().unwrap();
1644                    let all_instances = Self::resolve_all_by_source_path(tree, sp);
1645                    // Apply discriminator filter to resolved instances (respects #N occurrence)
1646                    let instances: Vec<_> = if let Some(matcher) = DiscriminatorMatcher::parse(disc)
1647                    {
1648                        matcher.filter_instances(all_instances)
1649                    } else {
1650                        all_instances
1651                    };
1652                    let extract = |instance: &AssembledGroupInstance| {
1653                        let mut r = serde_json::Map::new();
1654                        self.extract_fields_from_instance(instance, def, &mut r, enrich_codes);
1655                        serde_json::Value::Object(r)
1656                    };
1657                    match instances.len() {
1658                        0 => None,
1659                        1 => Some(extract(instances[0])),
1660                        _ => Some(serde_json::Value::Array(
1661                            instances.iter().map(|i| extract(i)).collect(),
1662                        )),
1663                    }
1664                } else {
1665                    let reps = Self::resolve_all_repetitions(tree, &def.meta.source_group, disc);
1666                    match reps.len() {
1667                        0 => None,
1668                        1 => Some(self.map_forward_inner(tree, def, reps[0], enrich_codes)),
1669                        _ => Some(serde_json::Value::Array(
1670                            reps.iter()
1671                                .map(|&rep| self.map_forward_inner(tree, def, rep, enrich_codes))
1672                                .collect(),
1673                        )),
1674                    }
1675                }
1676            } else if def.meta.source_group.is_empty() {
1677                // Root-level mapping — always single object
1678                Some(self.map_forward_inner(tree, def, 0, enrich_codes))
1679            } else if def.meta.source_path.as_ref().is_some_and(|sp| {
1680                has_source_path_qualifiers(sp) || def.meta.source_group.contains('.')
1681            }) {
1682                // Multi-level source path — navigate via source_path to collect all
1683                // instances across all parent repetitions. Handles both qualified
1684                // paths (e.g., "sg4.sg8_zd7.sg10") and unqualified paths (e.g.,
1685                // "sg17.sg36.sg40") where multiple parent reps each have children.
1686                let sp = def.meta.source_path.as_deref().unwrap();
1687                let mut indexed = Self::resolve_all_with_parent_indices(tree, sp);
1688
1689                // When the LAST part of source_path has no qualifier (e.g., "sg29.sg30"),
1690                // exclude reps that match a qualified sibling definition's qualifier
1691                // (e.g., "sg29.sg30_z35"). This prevents double-extraction when both
1692                // qualified and unqualified definitions target the same group.
1693                if let Some(last_part) = sp.rsplit('.').next() {
1694                    if !last_part.contains('_') {
1695                        // Collect qualifiers from sibling definitions that share the
1696                        // same base group name. E.g., for "sg29.sg30", only match
1697                        // "sg29.sg30_z35" (same base "sg30"), NOT "sg29.sg31_z35".
1698                        let base_prefix = if let Some(parent) = sp.rsplit_once('.') {
1699                            format!("{}.", parent.0)
1700                        } else {
1701                            String::new()
1702                        };
1703                        let sibling_qualifiers: Vec<String> = self
1704                            .definitions
1705                            .iter()
1706                            .filter_map(|d| d.meta.source_path.as_deref())
1707                            .filter(|other_sp| {
1708                                *other_sp != sp
1709                                    && other_sp.starts_with(&base_prefix)
1710                                    && other_sp.split('.').count() == sp.split('.').count()
1711                            })
1712                            .filter_map(|other_sp| {
1713                                let other_last = other_sp.rsplit('.').next()?;
1714                                // Only match siblings with the same base group name
1715                                // e.g., "sg30_z35" has base "sg30", must match "sg30"
1716                                let (base, q) = other_last.split_once('_')?;
1717                                if base == last_part {
1718                                    Some(q.to_string())
1719                                } else {
1720                                    None
1721                                }
1722                            })
1723                            .collect();
1724
1725                        if !sibling_qualifiers.is_empty() {
1726                            indexed.retain(|(_, inst)| {
1727                                let entry_qual = inst
1728                                    .segments
1729                                    .first()
1730                                    .and_then(|seg| seg.elements.first())
1731                                    .and_then(|el| el.first())
1732                                    .map(|v| v.to_lowercase());
1733                                // Keep reps whose entry qualifier does NOT match
1734                                // any sibling's qualifier
1735                                !entry_qual.is_some_and(|q| {
1736                                    sibling_qualifiers.iter().any(|sq| {
1737                                        sq.split('_').any(|part| part.eq_ignore_ascii_case(&q))
1738                                    })
1739                                })
1740                            });
1741                        }
1742                    }
1743                }
1744                let extract = |instance: &AssembledGroupInstance| {
1745                    let mut r = serde_json::Map::new();
1746                    self.extract_fields_from_instance(instance, def, &mut r, enrich_codes);
1747                    serde_json::Value::Object(r)
1748                };
1749                // Track parent rep indices for nesting reconstruction.
1750                // Key by source_path (not entity or source_group) so that definitions
1751                // at different depths or with different qualifiers don't collide.
1752                // e.g., "sg5.sg8_z41.sg9" vs "sg5.sg8_z42.sg9" are distinct keys.
1753                if def.meta.source_group.contains('.') && !indexed.is_empty() {
1754                    if let Some(sp) = &def.meta.source_path {
1755                        let parent_indices: Vec<usize> =
1756                            indexed.iter().map(|(idx, _)| *idx).collect();
1757                        nesting_info.entry(sp.clone()).or_insert(parent_indices);
1758
1759                        // Also store child rep indices (position within the leaf group)
1760                        // for depth-1 reverse placement. Key: "{sp}#child".
1761                        let child_key = format!("{sp}#child");
1762                        if let std::collections::hash_map::Entry::Vacant(e) =
1763                            nesting_info.entry(child_key)
1764                        {
1765                            let child_indices: Vec<usize> =
1766                                Self::compute_child_indices(tree, sp, &indexed);
1767                            if !child_indices.is_empty() {
1768                                e.insert(child_indices);
1769                            }
1770                        }
1771                    }
1772                }
1773                match indexed.len() {
1774                    0 => None,
1775                    1 => Some(extract(indexed[0].1)),
1776                    _ => Some(serde_json::Value::Array(
1777                        indexed.iter().map(|(_, i)| extract(i)).collect(),
1778                    )),
1779                }
1780            } else {
1781                let num_reps = Self::count_repetitions(tree, &def.meta.source_group);
1782                if num_reps <= 1 {
1783                    Some(self.map_forward_inner(tree, def, 0, enrich_codes))
1784                } else {
1785                    // Multiple reps, no discriminator — map all into array
1786                    let mut items = Vec::with_capacity(num_reps);
1787                    for rep in 0..num_reps {
1788                        items.push(self.map_forward_inner(tree, def, rep, enrich_codes));
1789                    }
1790                    Some(serde_json::Value::Array(items))
1791                }
1792            };
1793
1794            if let Some(bo4e) = bo4e {
1795                let key = to_camel_case(entity);
1796                deep_merge_insert(&mut result, &key, bo4e);
1797            }
1798        }
1799
1800        // Post-process: nest child entities under their parent entities.
1801        // E.g., Kontakt (source_group="SG2.SG3") moves under Marktteilnehmer (source_group="SG2").
1802        // Children whose parent group is the transaction root (e.g. SG4 for UTILMD) are
1803        // left at the top level — see MappingEngine::transaction_group.
1804        nest_child_entities_in_result(
1805            &mut result,
1806            &self.definitions,
1807            &nesting_info,
1808            tx_group_override,
1809        );
1810
1811        // Post-process: route NAD+DP entries out of marktteilnehmer[] into a
1812        // standalone Marktlokation or Messlokation entity. Phase 8 of the
1813        // 2026-04-28 audit: NAD+DP carries a delivery-point reference + address,
1814        // not a market role. Routing metadata travels in the side-channel so
1815        // the public BO4E JSON stays free of internal `_dpSource` markers.
1816        let dp_routing = route_nad_dp_to_lokation(&mut result);
1817
1818        (serde_json::Value::Object(result), nesting_info, dp_routing)
1819    }
1820
1821    /// Reverse-map a BO4E entity map back to an AssembledTree.
1822    ///
1823    /// For each definition:
1824    /// 1. Look up entity in input by `meta.entity` name
1825    /// 2. If entity value is an array, map each element as a separate group repetition
1826    /// 3. Place results by `source_group`: `""` → root segments, `"SGn"` → groups
1827    ///
1828    /// This is the inverse of `map_all_forward()`.
1829    pub fn map_all_reverse(
1830        &self,
1831        entities: &serde_json::Value,
1832        nesting_info: Option<&std::collections::HashMap<String, Vec<usize>>>,
1833    ) -> AssembledTree {
1834        let mut root_segments: Vec<AssembledSegment> = Vec::new();
1835        let mut groups: Vec<AssembledGroup> = Vec::new();
1836        // Track parent rep indices for child entities extracted from map-keyed
1837        // or array parents.  Used as fallback when nesting_info is empty.
1838        let mut inferred_nesting: std::collections::HashMap<String, Vec<usize>> =
1839            std::collections::HashMap::new();
1840
1841        for def in &self.definitions {
1842            let entity_key = to_camel_case(&def.meta.entity);
1843
1844            // Look up entity value — first at top level, then nested under parent.
1845            // `_extracted` keeps the owned value alive for the borrow below.
1846            let _extracted: Option<serde_json::Value>;
1847            let entity_value = if let Some(v) = entities.get(&entity_key) {
1848                _extracted = None;
1849                v
1850            } else if def.meta.source_group.contains('.') {
1851                // Child entity not at top level — try extracting from parent entity
1852                match extract_child_from_parent_with_indices(entities, &self.definitions, def) {
1853                    Some((v, parent_indices)) => {
1854                        // Record inferred parent rep indices for nesting distribution
1855                        if let Some(sp) = def.meta.source_path.as_deref() {
1856                            inferred_nesting
1857                                .entry(sp.to_string())
1858                                .or_insert(parent_indices);
1859                        }
1860                        _extracted = Some(v);
1861                        _extracted.as_ref().unwrap()
1862                    }
1863                    None => continue,
1864                }
1865            } else {
1866                continue;
1867            };
1868
1869            // Support map-keyed entities from typed PID format.
1870            // E.g., geschaeftspartner: {"Z04": {name1: "..."}} with discriminator NAD.0.0=Z04.
1871            // Extract inner value using discriminator's qualifier value as key,
1872            // and inject the qualifier into the inner object so companion fields find it.
1873            //
1874            // Also handles non-discriminated maps (e.g., marktteilnehmer: {"MS": {...}, "MR": {...}})
1875            // by converting them to arrays of inner values.
1876            let unwrapped: Option<serde_json::Value>;
1877            let entity_value = if entity_value.is_object() && !entity_value.is_array() {
1878                if let Some(disc_value) = def
1879                    .meta
1880                    .discriminator
1881                    .as_deref()
1882                    .and_then(|d| d.split_once('='))
1883                    .map(|(_, v)| v)
1884                {
1885                    // Discriminated definition: try to extract map key matching qualifier
1886                    if let Some(inner) = entity_value.get(disc_value) {
1887                        let mut injected = inner.clone();
1888                        // Find the field that maps to the discriminator's EDIFACT path
1889                        // and inject the map key as that field's value (e.g., nadQualifier = "Z04")
1890                        if let Some(qualifier_field) =
1891                            find_qualifier_companion_field(&self.definitions, &def.meta.entity)
1892                        {
1893                            if let Some(obj) = injected.as_object_mut() {
1894                                let entry = obj
1895                                    .entry(qualifier_field)
1896                                    .or_insert(serde_json::Value::Null);
1897                                if entry.is_null() {
1898                                    *entry = serde_json::Value::String(disc_value.to_string());
1899                                }
1900                            }
1901                        }
1902                        unwrapped = Some(injected);
1903                        unwrapped.as_ref().unwrap()
1904                    } else {
1905                        entity_value
1906                    }
1907                } else if is_map_keyed_object(entity_value) {
1908                    // Non-discriminated definition: convert map to array
1909                    // e.g., marktteilnehmer: {"MS": {...}, "MR": {...}} → [{...}, {...}]
1910                    // Inject each map key into its inner object using the companion field
1911                    // that maps to the discriminator path (if identifiable from other defs).
1912                    let map = entity_value.as_object().unwrap();
1913                    let arr: Vec<serde_json::Value> = map
1914                        .iter()
1915                        .map(|(key, val)| {
1916                            let mut item = val.clone();
1917                            // Try to find a qualifier companion field from peer definitions
1918                            // that share this entity name and have a discriminator
1919                            if let Some(obj) = item.as_object_mut() {
1920                                if let Some(qualifier_field) = find_qualifier_companion_field(
1921                                    &self.definitions,
1922                                    &def.meta.entity,
1923                                ) {
1924                                    let entry = obj
1925                                        .entry(qualifier_field)
1926                                        .or_insert(serde_json::Value::Null);
1927                                    if entry.is_null() {
1928                                        *entry = serde_json::Value::String(key.clone());
1929                                    }
1930                                }
1931                            }
1932                            item
1933                        })
1934                        .collect();
1935                    unwrapped = Some(serde_json::Value::Array(arr));
1936                    unwrapped.as_ref().unwrap()
1937                } else {
1938                    entity_value
1939                }
1940            } else {
1941                entity_value
1942            };
1943
1944            // Determine target group from source_group (use leaf part after last dot)
1945            let leaf_group = def
1946                .meta
1947                .source_group
1948                .rsplit('.')
1949                .next()
1950                .unwrap_or(&def.meta.source_group);
1951
1952            if def.meta.source_group.is_empty() {
1953                // Root-level: reverse into root segments
1954                let instance = self.map_reverse(entity_value, def);
1955                root_segments.extend(instance.segments);
1956            } else if entity_value.is_array() {
1957                // Array entity: each element becomes a group repetition
1958                let arr = entity_value.as_array().unwrap();
1959                let reps: Vec<_> = arr.iter().map(|item| self.map_reverse(item, def)).collect();
1960
1961                // Merge into existing group or create new one
1962                if let Some(existing) = groups.iter_mut().find(|g| g.group_id == leaf_group) {
1963                    existing.repetitions.extend(reps);
1964                } else {
1965                    groups.push(AssembledGroup {
1966                        group_id: leaf_group.to_string(),
1967                        repetitions: reps,
1968                    });
1969                }
1970            } else {
1971                // Single object: one repetition
1972                let instance = self.map_reverse(entity_value, def);
1973
1974                if let Some(existing) = groups.iter_mut().find(|g| g.group_id == leaf_group) {
1975                    existing.repetitions.push(instance);
1976                } else {
1977                    groups.push(AssembledGroup {
1978                        group_id: leaf_group.to_string(),
1979                        repetitions: vec![instance],
1980                    });
1981                }
1982            }
1983        }
1984
1985        // Post-process: move nested groups under their parent repetitions.
1986        // Definitions with multi-level source_group (e.g., "SG2.SG3") produce
1987        // top-level groups that must be nested inside their parent group.
1988        // Children are distributed sequentially among parent reps (child[i] → parent[i])
1989        // matching the forward mapper's extraction order.
1990        let nested_specs: Vec<(String, String)> = self
1991            .definitions
1992            .iter()
1993            .filter_map(|def| {
1994                let parts: Vec<&str> = def.meta.source_group.split('.').collect();
1995                if parts.len() > 1 {
1996                    Some((parts[0].to_string(), parts[parts.len() - 1].to_string()))
1997                } else {
1998                    None
1999                }
2000            })
2001            .collect();
2002        for (parent_id, child_id) in &nested_specs {
2003            // Only nest if both parent and child exist at the top level
2004            let has_parent = groups.iter().any(|g| g.group_id == *parent_id);
2005            let has_child = groups.iter().any(|g| g.group_id == *child_id);
2006            if has_parent && has_child {
2007                let child_idx = groups.iter().position(|g| g.group_id == *child_id).unwrap();
2008                let child_group = groups.remove(child_idx);
2009                let parent = groups
2010                    .iter_mut()
2011                    .find(|g| g.group_id == *parent_id)
2012                    .unwrap();
2013                // Distribute child reps among parent reps using nesting info
2014                // if available, falling back to all-under-first when not.
2015                // Nesting info is keyed by source_path (e.g., "sg2.sg3").
2016                let child_source_path = self
2017                    .definitions
2018                    .iter()
2019                    .find(|d| {
2020                        let parts: Vec<&str> = d.meta.source_group.split('.').collect();
2021                        parts.len() > 1 && parts[parts.len() - 1] == *child_id
2022                    })
2023                    .and_then(|d| d.meta.source_path.as_deref());
2024                let distribution = child_source_path.and_then(|key| {
2025                    nesting_info
2026                        .and_then(|ni| ni.get(key))
2027                        .or_else(|| inferred_nesting.get(key))
2028                });
2029                for (i, child_rep) in child_group.repetitions.into_iter().enumerate() {
2030                    let target_idx = distribution
2031                        .and_then(|dist| dist.get(i))
2032                        .copied()
2033                        .unwrap_or(0);
2034
2035                    if let Some(target_rep) = parent.repetitions.get_mut(target_idx) {
2036                        if let Some(existing) = target_rep
2037                            .child_groups
2038                            .iter_mut()
2039                            .find(|g| g.group_id == *child_id)
2040                        {
2041                            existing.repetitions.push(child_rep);
2042                        } else {
2043                            target_rep.child_groups.push(AssembledGroup {
2044                                group_id: child_id.clone(),
2045                                repetitions: vec![child_rep],
2046                            });
2047                        }
2048                    }
2049                }
2050            }
2051        }
2052
2053        let post_group_start = root_segments.len();
2054        AssembledTree {
2055            segments: root_segments,
2056            groups,
2057            post_group_start,
2058            inter_group_segments: std::collections::BTreeMap::new(),
2059        }
2060    }
2061
2062    /// Count the number of repetitions available for a group path in the tree.
2063    fn count_repetitions(tree: &AssembledTree, group_path: &str) -> usize {
2064        let parts: Vec<&str> = group_path.split('.').collect();
2065
2066        let (first_id, first_rep) = parse_group_spec(parts[0]);
2067        let first_group = match tree.groups.iter().find(|g| g.group_id == first_id) {
2068            Some(g) => g,
2069            None => return 0,
2070        };
2071
2072        if parts.len() == 1 {
2073            return first_group.repetitions.len();
2074        }
2075
2076        // Navigate to parent, then count leaf group reps
2077        let mut current_instance = match first_group.repetitions.get(first_rep.unwrap_or(0)) {
2078            Some(i) => i,
2079            None => return 0,
2080        };
2081
2082        for (i, part) in parts[1..].iter().enumerate() {
2083            let (group_id, explicit_rep) = parse_group_spec(part);
2084            let child_group = match current_instance
2085                .child_groups
2086                .iter()
2087                .find(|g| g.group_id == group_id)
2088            {
2089                Some(g) => g,
2090                None => return 0,
2091            };
2092
2093            if i == parts.len() - 2 {
2094                // Last part — return rep count
2095                return child_group.repetitions.len();
2096            }
2097            current_instance = match child_group.repetitions.get(explicit_rep.unwrap_or(0)) {
2098                Some(i) => i,
2099                None => return 0,
2100            };
2101        }
2102
2103        0
2104    }
2105
2106    /// Map an assembled tree into message-level and transaction-level results.
2107    ///
2108    /// - `msg_engine`: MappingEngine loaded with message-level definitions (SG2, SG3, root segments)
2109    /// - `tx_engine`: MappingEngine loaded with transaction-level definitions (relative to SG4)
2110    /// - `tree`: The assembled tree for one message
2111    /// - `transaction_group`: The group ID that represents transactions (e.g., "SG4")
2112    ///
2113    /// Returns a `MappedMessage` with message stammdaten and per-transaction results.
2114    pub fn map_interchange(
2115        msg_engine: &MappingEngine,
2116        tx_engine: &MappingEngine,
2117        tree: &AssembledTree,
2118        transaction_group: &str,
2119        enrich_codes: bool,
2120    ) -> crate::model::MappedMessage {
2121        // Map message-level entities (also captures nesting + DP routing info)
2122        let (stammdaten, nesting_info, dp_routing) =
2123            msg_engine.map_all_forward_inner(tree, enrich_codes);
2124
2125        // Find the transaction group and map each repetition
2126        let transaktionen = tree
2127            .groups
2128            .iter()
2129            .find(|g| g.group_id == transaction_group)
2130            .map(|sg| {
2131                sg.repetitions
2132                    .iter()
2133                    .map(|instance| {
2134                        // Wrap the instance in its group so that definitions with
2135                        // source_group paths like "SG4.SG5" can resolve correctly.
2136                        let wrapped_tree = AssembledTree {
2137                            segments: vec![],
2138                            groups: vec![AssembledGroup {
2139                                group_id: transaction_group.to_string(),
2140                                repetitions: vec![instance.clone()],
2141                            }],
2142                            post_group_start: 0,
2143                            inter_group_segments: std::collections::BTreeMap::new(),
2144                        };
2145
2146                        // Pass the transaction_group into the tx_engine so its direct
2147                        // children (Marktlokation etc.) stay top-level peers of
2148                        // Prozessdaten rather than nested under it.
2149                        let (tx_result, tx_nesting, tx_dp_routing) = tx_engine
2150                            .map_all_forward_inner_with_tx(
2151                                &wrapped_tree,
2152                                enrich_codes,
2153                                Some(transaction_group),
2154                            );
2155
2156                        crate::model::MappedTransaktion {
2157                            stammdaten: tx_result,
2158                            nesting_info: tx_nesting,
2159                            dp_routing: tx_dp_routing,
2160                        }
2161                    })
2162                    .collect()
2163            })
2164            .unwrap_or_default();
2165
2166        crate::model::MappedMessage {
2167            stammdaten,
2168            transaktionen,
2169            nesting_info,
2170            dp_routing,
2171            inter_group_segments: tree.inter_group_segments.clone(),
2172        }
2173    }
2174
2175    /// Reverse-map a `MappedMessage` back to an `AssembledTree`.
2176    ///
2177    /// Two-engine approach mirroring `map_interchange()`:
2178    /// - `msg_engine` handles message-level stammdaten → SG2/SG3 groups
2179    /// - `tx_engine` handles per-transaction stammdaten → SG4 instances
2180    ///
2181    /// All entities (including prozessdaten/nachricht) are in `tx.stammdaten`.
2182    /// Results are merged into one `AssembledGroupInstance` per transaction,
2183    /// collected into an SG4 `AssembledGroup`, then combined with message-level groups.
2184    pub fn map_interchange_reverse(
2185        msg_engine: &MappingEngine,
2186        tx_engine: &MappingEngine,
2187        mapped: &crate::model::MappedMessage,
2188        transaction_group: &str,
2189        filtered_mig: Option<&MigSchema>,
2190    ) -> AssembledTree {
2191        // Step 1: Reverse message-level stammdaten. If forward mapping routed
2192        // any NAD+DP entries out of marktteilnehmer[] into a lokation entity,
2193        // unroute them first using the side-channel metadata so the per-def
2194        // loop sees the original marktteilnehmer shape. Clone only when
2195        // routing actually happened — keeps the common path zero-copy.
2196        let _owned_msg: Option<serde_json::Value>;
2197        let msg_stammdaten = if !mapped.dp_routing.is_empty() {
2198            if let serde_json::Value::Object(map) = &mapped.stammdaten {
2199                let mut cloned = map.clone();
2200                unroute_lokation_to_nad_dp(&mut cloned, &mapped.dp_routing);
2201                _owned_msg = Some(serde_json::Value::Object(cloned));
2202                _owned_msg.as_ref().unwrap()
2203            } else {
2204                _owned_msg = None;
2205                &mapped.stammdaten
2206            }
2207        } else {
2208            _owned_msg = None;
2209            &mapped.stammdaten
2210        };
2211
2212        let msg_tree = msg_engine.map_all_reverse(
2213            msg_stammdaten,
2214            if mapped.nesting_info.is_empty() {
2215                None
2216            } else {
2217                Some(&mapped.nesting_info)
2218            },
2219        );
2220
2221        // Step 2: Build transaction instances from each Transaktion
2222        let mut sg4_reps: Vec<AssembledGroupInstance> = Vec::new();
2223
2224        // Collect all definitions with their relative paths and sort by depth.
2225        // Shallower paths (SG8) must be processed before deeper ones (SG8:0.SG10)
2226        // so that parent group repetitions exist before children are added.
2227        struct DefWithMeta<'a> {
2228            def: &'a MappingDefinition,
2229            relative: String,
2230            depth: usize,
2231        }
2232
2233        let mut sorted_defs: Vec<DefWithMeta> = tx_engine
2234            .definitions
2235            .iter()
2236            .map(|def| {
2237                let relative = strip_tx_group_prefix(&def.meta.source_group, transaction_group);
2238                let depth = if relative.is_empty() {
2239                    0
2240                } else {
2241                    relative.chars().filter(|c| *c == '.').count() + 1
2242                };
2243                DefWithMeta {
2244                    def,
2245                    relative,
2246                    depth,
2247                }
2248            })
2249            .collect();
2250
2251        // Build parent source_path → rep_index map from deeper definitions.
2252        // SG10 defs like "SG4.SG8:0.SG10" with source_path "sg4.sg8_z79.sg10"
2253        // tell us that the SG8 def with source_path "sg4.sg8_z79" should be rep 0.
2254        let mut parent_rep_map: std::collections::HashMap<String, usize> =
2255            std::collections::HashMap::new();
2256        for dm in &sorted_defs {
2257            if dm.depth >= 2 {
2258                let parts: Vec<&str> = dm.relative.split('.').collect();
2259                let (_, parent_rep) = parse_group_spec(parts[0]);
2260                if let Some(rep_idx) = parent_rep {
2261                    if let Some(sp) = &dm.def.meta.source_path {
2262                        if let Some((parent_path, _)) = sp.rsplit_once('.') {
2263                            parent_rep_map
2264                                .entry(parent_path.to_string())
2265                                .or_insert(rep_idx);
2266                        }
2267                    }
2268                }
2269            }
2270        }
2271
2272        // Augment shallow definitions with explicit rep indices from the map,
2273        // but only for single-rep cases (no multi-rep — those use dynamic tracking).
2274        for dm in &mut sorted_defs {
2275            if dm.depth == 1 && !dm.relative.contains(':') {
2276                if let Some(sp) = &dm.def.meta.source_path {
2277                    if let Some(rep_idx) = parent_rep_map.get(sp.as_str()) {
2278                        dm.relative = format!("{}:{}", dm.relative, rep_idx);
2279                    }
2280                }
2281            }
2282        }
2283
2284        // Sort: shallower depth first, so SG8 defs create reps before SG8:N.SG10 defs.
2285        // Within same depth, sort by MIG group position (if available) for correct emission order,
2286        // falling back to alphabetical relative path for deterministic ordering.
2287        //
2288        // For variant groups (SG8 with Z01/Z03/Z07 etc.), use per-variant MIG positions
2289        // extracted from each definition's source_path qualifier suffix (e.g., "sg4.sg8_z01" → "Z01").
2290        if let Some(mig) = filtered_mig {
2291            let mig_order = build_reverse_mig_group_order(mig, transaction_group);
2292            sorted_defs.sort_by(|a, b| {
2293                a.depth.cmp(&b.depth).then_with(|| {
2294                    let a_id = a.relative.split(':').next().unwrap_or(&a.relative);
2295                    let b_id = b.relative.split(':').next().unwrap_or(&b.relative);
2296                    // Try per-variant lookup from source_path (e.g., "sg4.sg8_z01" → "SG8_Z01")
2297                    let a_pos = variant_mig_position(a.def, a_id, &mig_order);
2298                    let b_pos = variant_mig_position(b.def, b_id, &mig_order);
2299                    a_pos.cmp(&b_pos).then(a.relative.cmp(&b.relative))
2300                })
2301            });
2302        } else {
2303            sorted_defs.sort_by(|a, b| a.depth.cmp(&b.depth).then(a.relative.cmp(&b.relative)));
2304        }
2305
2306        for tx in &mapped.transaktionen {
2307            let mut root_segs: Vec<AssembledSegment> = Vec::new();
2308            let mut child_groups: Vec<AssembledGroup> = Vec::new();
2309
2310            // Apply per-transaction NAD+DP unroute when forward mapping
2311            // captured tx-level DP routing. Without this, fixtures whose AHB
2312            // places NAD+DP under a transaction-level group (vs message-level
2313            // SG2) would lose the segment on reverse — see issue #61.
2314            let _owned_tx: Option<serde_json::Value>;
2315            let tx_stammdaten: &serde_json::Value = if !tx.dp_routing.is_empty() {
2316                if let serde_json::Value::Object(map) = &tx.stammdaten {
2317                    let mut cloned = map.clone();
2318                    unroute_lokation_to_nad_dp(&mut cloned, &tx.dp_routing);
2319                    _owned_tx = Some(serde_json::Value::Object(cloned));
2320                    _owned_tx.as_ref().unwrap()
2321                } else {
2322                    _owned_tx = None;
2323                    &tx.stammdaten
2324                }
2325            } else {
2326                _owned_tx = None;
2327                &tx.stammdaten
2328            };
2329
2330            // Track source_path → repetition indices for parent groups (top-down).
2331            // Built during depth-1 processing, used by depth-2+ defs without
2332            // explicit rep indices to find their correct parent via source_path.
2333            // Vec<usize> supports multi-rep parents (e.g., two SG8+ZF3 reps).
2334            let mut source_path_to_rep: std::collections::HashMap<String, Vec<usize>> =
2335                std::collections::HashMap::new();
2336
2337            for dm in &sorted_defs {
2338                // Determine the BO4E value to reverse-map from.
2339                // Check top level first, then nested under parent entity.
2340                let entity_key = to_camel_case(&dm.def.meta.entity);
2341                let _tx_extracted: Option<serde_json::Value>;
2342                let bo4e_value = if let Some(v) = tx_stammdaten.get(&entity_key) {
2343                    _tx_extracted = None;
2344                    v
2345                } else if dm.def.meta.source_group.contains('.') {
2346                    match extract_child_from_parent(tx_stammdaten, &tx_engine.definitions, dm.def)
2347                    {
2348                        Some(v) => {
2349                            _tx_extracted = Some(v);
2350                            _tx_extracted.as_ref().unwrap()
2351                        }
2352                        None => continue,
2353                    }
2354                } else {
2355                    continue;
2356                };
2357
2358                // Support map-keyed entities from typed PID format (same logic as map_all_reverse).
2359                let unwrapped_value: Option<serde_json::Value>;
2360                let bo4e_value = if bo4e_value.is_object() && !bo4e_value.is_array() {
2361                    if let Some(disc_value) = dm
2362                        .def
2363                        .meta
2364                        .discriminator
2365                        .as_deref()
2366                        .and_then(|d| d.split_once('='))
2367                        .map(|(_, v)| v)
2368                    {
2369                        if let Some(inner) = bo4e_value.get(disc_value) {
2370                            let mut injected = inner.clone();
2371                            if let Some(qualifier_field) = find_qualifier_companion_field(
2372                                &tx_engine.definitions,
2373                                &dm.def.meta.entity,
2374                            ) {
2375                                if let Some(obj) = injected.as_object_mut() {
2376                                    obj.entry(qualifier_field).or_insert_with(|| {
2377                                        serde_json::Value::String(disc_value.to_string())
2378                                    });
2379                                }
2380                            }
2381                            unwrapped_value = Some(injected);
2382                            unwrapped_value.as_ref().unwrap()
2383                        } else {
2384                            bo4e_value
2385                        }
2386                    } else if is_map_keyed_object(bo4e_value) {
2387                        let map = bo4e_value.as_object().unwrap();
2388                        let arr: Vec<serde_json::Value> = map
2389                            .iter()
2390                            .map(|(key, val)| {
2391                                let mut item = val.clone();
2392                                if let Some(obj) = item.as_object_mut() {
2393                                    if let Some(qualifier_field) = find_qualifier_companion_field(
2394                                        &tx_engine.definitions,
2395                                        &dm.def.meta.entity,
2396                                    ) {
2397                                        let entry = obj
2398                                            .entry(qualifier_field)
2399                                            .or_insert(serde_json::Value::Null);
2400                                        if entry.is_null() {
2401                                            *entry = serde_json::Value::String(key.clone());
2402                                        }
2403                                    }
2404                                }
2405                                item
2406                            })
2407                            .collect();
2408                        unwrapped_value = Some(serde_json::Value::Array(arr));
2409                        unwrapped_value.as_ref().unwrap()
2410                    } else {
2411                        bo4e_value
2412                    }
2413                } else {
2414                    bo4e_value
2415                };
2416
2417                // Handle array entities: each element becomes a separate group rep.
2418                // This supports both the NAD/SG12 pattern (multiple qualifiers) and
2419                // the multi-rep pattern (e.g., two LOC+Z17 Messlokationen).
2420                let items: Vec<&serde_json::Value> = if bo4e_value.is_array() {
2421                    bo4e_value.as_array().unwrap().iter().collect()
2422                } else {
2423                    vec![bo4e_value]
2424                };
2425
2426                for (item_idx, item) in items.iter().enumerate() {
2427                    let instance = tx_engine.map_reverse(item, dm.def);
2428
2429                    // Skip empty instances (definition had no real BO4E data)
2430                    if instance.segments.is_empty() && instance.child_groups.is_empty() {
2431                        continue;
2432                    }
2433
2434                    if dm.relative.is_empty() {
2435                        root_segs.extend(instance.segments);
2436                    } else {
2437                        // For depth-2+ defs without explicit rep index, resolve
2438                        // parent rep from source_path matching (qualifier-based).
2439                        // item_idx selects the correct parent rep for multi-rep entities.
2440                        let effective_relative = if dm.depth >= 2 {
2441                            // Multi-rep: strip hardcoded parent :N indices so
2442                            // resolve_child_relative uses source_path lookup instead.
2443                            let rel = if items.len() > 1 {
2444                                strip_all_rep_indices(&dm.relative)
2445                            } else {
2446                                dm.relative.clone()
2447                            };
2448                            // Use tx nesting info for multi-rep arrays, BUT skip it
2449                            // when source_path is present and resolves to a single
2450                            // parent rep. In that case, nesting_info indices (from the
2451                            // original tree) may not match the reverse tree's rep layout.
2452                            // resolve_child_relative uses reverse-tree source_path_to_rep
2453                            // which is always correct.
2454                            let skip_nesting = dm
2455                                .def
2456                                .meta
2457                                .source_path
2458                                .as_ref()
2459                                .and_then(|sp| sp.rsplit_once('.'))
2460                                .and_then(|(parent_path, _)| source_path_to_rep.get(parent_path))
2461                                .is_some_and(|reps| reps.len() == 1);
2462                            let nesting_idx = if items.len() > 1 && !skip_nesting {
2463                                dm.def
2464                                    .meta
2465                                    .source_path
2466                                    .as_ref()
2467                                    .and_then(|sp| tx.nesting_info.get(sp))
2468                                    .and_then(|dist| dist.get(item_idx))
2469                                    .copied()
2470                            } else {
2471                                None
2472                            };
2473                            if let Some(parent_rep) = nesting_idx {
2474                                // Direct placement using known nesting distribution
2475                                let parts: Vec<&str> = rel.split('.').collect();
2476                                let parent_id = parts[0].split(':').next().unwrap_or(parts[0]);
2477                                let rest = parts[1..].join(".");
2478                                format!("{}:{}.{}", parent_id, parent_rep, rest)
2479                            } else {
2480                                resolve_child_relative(
2481                                    &rel,
2482                                    dm.def.meta.source_path.as_deref(),
2483                                    &source_path_to_rep,
2484                                    item_idx,
2485                                )
2486                            }
2487                        } else if dm.depth == 1 {
2488                            // Depth-1: use nesting_info child indices for correct
2489                            // rep placement (preserves original interleaving order).
2490                            let child_key = dm
2491                                .def
2492                                .meta
2493                                .source_path
2494                                .as_ref()
2495                                .map(|sp| format!("{sp}#child"));
2496                            if let Some(child_indices) =
2497                                child_key.as_ref().and_then(|ck| tx.nesting_info.get(ck))
2498                            {
2499                                if let Some(&target) = child_indices.get(item_idx) {
2500                                    if target != usize::MAX {
2501                                        let base =
2502                                            dm.relative.split(':').next().unwrap_or(&dm.relative);
2503                                        format!("{}:{}", base, target)
2504                                    } else {
2505                                        dm.relative.clone()
2506                                    }
2507                                } else if items.len() > 1 && item_idx > 0 {
2508                                    strip_rep_index(&dm.relative)
2509                                } else {
2510                                    dm.relative.clone()
2511                                }
2512                            } else if items.len() > 1 && item_idx > 0 {
2513                                strip_rep_index(&dm.relative)
2514                            } else {
2515                                dm.relative.clone()
2516                            }
2517                        } else if items.len() > 1 && item_idx > 0 {
2518                            // Multi-rep entity with hardcoded :N index: first item uses
2519                            // the original index, subsequent items append (strip :N).
2520                            strip_rep_index(&dm.relative)
2521                        } else {
2522                            dm.relative.clone()
2523                        };
2524
2525                        let rep_used =
2526                            place_in_groups(&mut child_groups, &effective_relative, instance);
2527
2528                        // Track source_path → rep_index for depth-1 (parent) defs
2529                        if dm.depth == 1 {
2530                            if let Some(sp) = &dm.def.meta.source_path {
2531                                source_path_to_rep
2532                                    .entry(sp.clone())
2533                                    .or_default()
2534                                    .push(rep_used);
2535                            }
2536                        }
2537                    }
2538                }
2539            }
2540
2541            // Sort variant reps within each child group to match MIG order.
2542            // The reverse mapper appends reps in definition-filename order, but
2543            // the assembler captures them in MIG variant order. Use the filtered
2544            // MIG's nested_groups as the canonical ordering.
2545            if let Some(mig) = filtered_mig {
2546                sort_variant_reps_by_mig(&mut child_groups, mig, transaction_group);
2547            }
2548
2549            sg4_reps.push(AssembledGroupInstance {
2550                segments: root_segs,
2551                child_groups,
2552                entry_mig_number: None,
2553                variant_mig_numbers: vec![],
2554                skipped_segments: Vec::new(),
2555                skipped_positions: Vec::new(),
2556            });
2557        }
2558
2559        // Step 3: Combine message tree with transaction group.
2560        // Move UNS section separator from root segments to inter_group_segments.
2561        // UNS+D (detail) goes BEFORE the tx group (MSCONS: header/detail boundary).
2562        // UNS+S (summary) goes AFTER the tx group (ORDERS: detail/summary boundary).
2563        // Any segments that follow UNS in the sequence (e.g., summary MOA in REMADV)
2564        // are also placed in inter_group_segments alongside UNS.
2565        let mut root_segments = Vec::new();
2566        let mut uns_segments = Vec::new();
2567        let mut uns_is_summary = false;
2568        let mut found_uns = false;
2569        for seg in msg_tree.segments {
2570            if seg.tag == "UNS" {
2571                // Check if this is UNS+S (summary separator) vs UNS+D (detail separator)
2572                uns_is_summary = seg
2573                    .elements
2574                    .first()
2575                    .and_then(|el| el.first())
2576                    .map(|v| v == "S")
2577                    .unwrap_or(false);
2578                uns_segments.push(seg);
2579                found_uns = true;
2580            } else if found_uns {
2581                // Segments after UNS belong in the same inter_group position
2582                uns_segments.push(seg);
2583            } else {
2584                root_segments.push(seg);
2585            }
2586        }
2587
2588        let pre_group_count = root_segments.len();
2589        let mut all_groups = msg_tree.groups;
2590        let mut inter_group = msg_tree.inter_group_segments;
2591
2592        // Helper: parse SG number from group_id (e.g., "SG26" → 26).
2593        let sg_num = |id: &str| -> usize {
2594            id.strip_prefix("SG")
2595                .and_then(|n| n.parse::<usize>().ok())
2596                .unwrap_or(0)
2597        };
2598
2599        if !sg4_reps.is_empty() {
2600            if uns_is_summary {
2601                // UNS+S: place AFTER the transaction group (detail/summary boundary)
2602                all_groups.push(AssembledGroup {
2603                    group_id: transaction_group.to_string(),
2604                    repetitions: sg4_reps,
2605                });
2606                if !uns_segments.is_empty() {
2607                    // Sort groups by SG number so the disassembler emits them
2608                    // in MIG order.  Insert UNS right after the tx_group —
2609                    // any groups with higher SG numbers (e.g., SG50/SG52 in
2610                    // INVOIC) are post-UNS summary groups.
2611                    all_groups.sort_by_key(|g| sg_num(&g.group_id));
2612                    let tx_num = sg_num(transaction_group);
2613                    let uns_pos = all_groups
2614                        .iter()
2615                        .rposition(|g| sg_num(&g.group_id) <= tx_num)
2616                        .map(|i| i + 1)
2617                        .unwrap_or(all_groups.len());
2618                    inter_group.insert(uns_pos, uns_segments);
2619                }
2620            } else {
2621                // UNS+D: place BEFORE the transaction group (header/detail boundary)
2622                if !uns_segments.is_empty() {
2623                    inter_group.insert(all_groups.len(), uns_segments);
2624                }
2625                all_groups.push(AssembledGroup {
2626                    group_id: transaction_group.to_string(),
2627                    repetitions: sg4_reps,
2628                });
2629            }
2630        } else if !uns_segments.is_empty() {
2631            if transaction_group.is_empty() {
2632                // Truly message-only (tx_group=""): UNS is a section separator.
2633                // UNS+S (summary) goes AFTER all groups — e.g., ORDCHG UNS+S
2634                // follows SG1 (NAD+CTA+COM) groups.
2635                // UNS+D (detail) goes BEFORE groups.
2636                all_groups.sort_by_key(|g| sg_num(&g.group_id));
2637                if uns_is_summary {
2638                    inter_group.insert(all_groups.len(), uns_segments);
2639                } else {
2640                    inter_group.insert(0, uns_segments);
2641                }
2642            } else {
2643                // Has a tx_group but no tx reps (e.g., INVOIC PID 31004
2644                // Storno — no SG26 data).  Sort groups and insert UNS after
2645                // the last group with SG number ≤ tx_group number.
2646                all_groups.sort_by_key(|g| sg_num(&g.group_id));
2647                let tx_num = sg_num(transaction_group);
2648                let uns_pos = all_groups
2649                    .iter()
2650                    .rposition(|g| sg_num(&g.group_id) <= tx_num)
2651                    .map(|i| i + 1)
2652                    .unwrap_or(all_groups.len());
2653                inter_group.insert(uns_pos, uns_segments);
2654            }
2655        }
2656
2657        // Restore inter_group_segments captured during forward mapping
2658        // (e.g. PID-foreign top-level segments preserved by the assembler's
2659        // skip-unknown mode — see `Assembler::assemble_generic`). Without
2660        // this, BO4E forward + reverse drops anything not represented in a
2661        // TOML mapping definition. We append rather than overwrite so the
2662        // UNS placement computed above survives — same-key collisions are
2663        // rare in practice (UNS goes at well-known positions).
2664        for (k, segs) in &mapped.inter_group_segments {
2665            if segs.is_empty() {
2666                continue;
2667            }
2668            let existing_tags: std::collections::HashSet<String> = inter_group
2669                .get(k)
2670                .map(|v| v.iter().map(|s| s.tag.clone()).collect())
2671                .unwrap_or_default();
2672            for seg in segs {
2673                if existing_tags.contains(&seg.tag) {
2674                    continue;
2675                }
2676                inter_group.entry(*k).or_default().push(seg.clone());
2677            }
2678        }
2679
2680        AssembledTree {
2681            segments: root_segments,
2682            groups: all_groups,
2683            post_group_start: pre_group_count,
2684            inter_group_segments: inter_group,
2685        }
2686    }
2687
2688    /// Build an assembled group from BO4E values and a definition.
2689    pub fn build_group_from_bo4e(
2690        &self,
2691        bo4e_value: &serde_json::Value,
2692        def: &MappingDefinition,
2693    ) -> AssembledGroup {
2694        let instance = self.map_reverse(bo4e_value, def);
2695        let leaf_group = def
2696            .meta
2697            .source_group
2698            .rsplit('.')
2699            .next()
2700            .unwrap_or(&def.meta.source_group);
2701
2702        AssembledGroup {
2703            group_id: leaf_group.to_string(),
2704            repetitions: vec![instance],
2705        }
2706    }
2707
2708    /// Forward-map an assembled tree to a typed interchange.
2709    ///
2710    /// Runs the dynamic mapping pipeline, wraps the result with metadata,
2711    /// then converts via JSON serialization into the caller's typed structs.
2712    ///
2713    /// - `M`: message-level stammdaten type (e.g., `Pid55001MsgStammdaten`)
2714    /// - `T`: transaction-level stammdaten type (e.g., `Pid55001TxStammdaten`)
2715    pub fn map_interchange_typed<M, T>(
2716        msg_engine: &MappingEngine,
2717        tx_engine: &MappingEngine,
2718        tree: &AssembledTree,
2719        tx_group: &str,
2720        enrich_codes: bool,
2721        nachrichtendaten: crate::model::Nachrichtendaten,
2722        interchangedaten: crate::model::Interchangedaten,
2723    ) -> Result<crate::model::Interchange<M, T>, serde_json::Error>
2724    where
2725        M: serde::de::DeserializeOwned,
2726        T: serde::de::DeserializeOwned,
2727    {
2728        let mapped = Self::map_interchange(msg_engine, tx_engine, tree, tx_group, enrich_codes);
2729        let nachricht = mapped.into_dynamic_nachricht(nachrichtendaten);
2730        let dynamic = crate::model::DynamicInterchange {
2731            interchangedaten,
2732            nachrichten: vec![nachricht],
2733        };
2734        let value = serde_json::to_value(&dynamic)?;
2735        serde_json::from_value(value)
2736    }
2737
2738    /// Reverse-map a typed interchange nachricht back to an assembled tree.
2739    ///
2740    /// Serializes the typed struct to JSON, then runs the dynamic reverse pipeline.
2741    ///
2742    /// - `M`: message-level stammdaten type
2743    /// - `T`: transaction-level stammdaten type
2744    pub fn map_interchange_reverse_typed<M, T>(
2745        msg_engine: &MappingEngine,
2746        tx_engine: &MappingEngine,
2747        nachricht: &crate::model::Nachricht<M, T>,
2748        tx_group: &str,
2749    ) -> Result<AssembledTree, serde_json::Error>
2750    where
2751        M: serde::Serialize,
2752        T: serde::Serialize,
2753    {
2754        let stammdaten = serde_json::to_value(&nachricht.stammdaten)?;
2755        let transaktionen: Vec<crate::model::MappedTransaktion> = nachricht
2756            .transaktionen
2757            .iter()
2758            .map(|t| {
2759                Ok(crate::model::MappedTransaktion {
2760                    stammdaten: serde_json::to_value(t)?,
2761                    nesting_info: Default::default(),
2762                    dp_routing: Default::default(),
2763                })
2764            })
2765            .collect::<Result<Vec<_>, serde_json::Error>>()?;
2766        let mapped = crate::model::MappedMessage {
2767            stammdaten,
2768            transaktionen,
2769            nesting_info: Default::default(),
2770            dp_routing: Default::default(),
2771            inter_group_segments: Default::default(),
2772        };
2773        Ok(Self::map_interchange_reverse(
2774            msg_engine, tx_engine, &mapped, tx_group, None,
2775        ))
2776    }
2777}
2778
2779/// Parse a group path part with optional repetition: "SG8:1" → ("SG8", Some(1)).
2780/// Parse a source_path part into (group_id, optional_qualifier).
2781///
2782/// `"sg8_z98"` → `("sg8", Some("z98"))`
2783/// `"sg4"` → `("sg4", None)`
2784/// `"sg10"` → `("sg10", None)`
2785fn parse_source_path_part(part: &str) -> (&str, Option<&str>) {
2786    // Find the first underscore that separates group from qualifier.
2787    // Source path parts look like "sg8_z98", "sg4", "sg10", "sg12_z04".
2788    // The group ID is always "sgN", so the underscore after the digits is the separator.
2789    if let Some(pos) = part.find('_') {
2790        let group = &part[..pos];
2791        let qualifier = &part[pos + 1..];
2792        if !qualifier.is_empty() {
2793            return (group, Some(qualifier));
2794        }
2795    }
2796    (part, None)
2797}
2798
2799/// Build a map from group ID (e.g., "SG5", "SG8") to its position index
2800/// within the transaction group's nested_groups Vec.
2801/// Used by `map_interchange_reverse` to sort definitions in MIG order.
2802///
2803/// For variant groups (same ID with variant_code set, e.g., SG8 with Z01, Z03, Z07),
2804/// stores per-variant positions (e.g., "SG8_Z01" → 0, "SG8_Z03" → 1) so that
2805/// definitions are sorted in MIG XML order rather than alphabetical qualifier order.
2806fn build_reverse_mig_group_order(mig: &MigSchema, tx_group_id: &str) -> HashMap<String, usize> {
2807    let mut order = HashMap::new();
2808    if let Some(tg) = mig.segment_groups.iter().find(|g| g.id == tx_group_id) {
2809        for (i, nested) in tg.nested_groups.iter().enumerate() {
2810            // For variant groups, store per-variant key (e.g., "SG8_Z01" → i)
2811            if let Some(ref vc) = nested.variant_code {
2812                let variant_key = format!("{}_{}", nested.id, vc.to_uppercase());
2813                order.insert(variant_key, i);
2814            }
2815            // Always store base group ID for fallback
2816            order.entry(nested.id.clone()).or_insert(i);
2817        }
2818    }
2819    order
2820}
2821
2822/// Extract the MIG position for a definition, using per-variant lookup when possible.
2823///
2824/// For a definition with source_path "sg4.sg8_z01", extracts the variant qualifier "Z01"
2825/// and looks up "SG8_Z01" in the MIG order map. Falls back to the base group ID (e.g., "SG8")
2826/// if no variant qualifier is found or if the per-variant key isn't in the map.
2827fn variant_mig_position(
2828    def: &MappingDefinition,
2829    base_group_id: &str,
2830    mig_order: &HashMap<String, usize>,
2831) -> usize {
2832    // Try to extract variant qualifier from source_path.
2833    // source_path like "sg4.sg8_z01" or "sg4.sg8_z01.sg10" — we want the part matching base_group_id.
2834    if let Some(ref sp) = def.meta.source_path {
2835        // Find the path segment matching the base group (e.g., "sg8_z01" for base "SG8")
2836        let base_lower = base_group_id.to_lowercase();
2837        for part in sp.split('.') {
2838            if part.starts_with(&base_lower)
2839                || part.starts_with(base_group_id.to_lowercase().as_str())
2840            {
2841                // Extract qualifier suffix: "sg8_z01" → "z01"
2842                if let Some(underscore_pos) = part.find('_') {
2843                    let qualifier = &part[underscore_pos + 1..];
2844                    let variant_key = format!("{}_{}", base_group_id, qualifier.to_uppercase());
2845                    if let Some(&pos) = mig_order.get(&variant_key) {
2846                        return pos;
2847                    }
2848                }
2849            }
2850        }
2851    }
2852    // Fallback to base group position
2853    mig_order.get(base_group_id).copied().unwrap_or(usize::MAX)
2854}
2855
2856/// Find a group repetition whose entry segment has a matching qualifier.
2857///
2858/// The entry segment is the first segment in the instance (e.g., SEQ for SG8).
2859/// The qualifier is matched against `elements[0][0]` (case-insensitive).
2860fn find_rep_by_entry_qualifier<'a>(
2861    reps: &'a [AssembledGroupInstance],
2862    qualifier: &str,
2863) -> Option<&'a AssembledGroupInstance> {
2864    // Support compound qualifiers like "za1_za2" — match any part.
2865    let parts: Vec<&str> = qualifier.split('_').collect();
2866    reps.iter().find(|inst| {
2867        inst.segments.first().is_some_and(|seg| {
2868            seg.elements
2869                .first()
2870                .and_then(|e| e.first())
2871                .is_some_and(|v| parts.iter().any(|part| v.eq_ignore_ascii_case(part)))
2872        })
2873    })
2874}
2875
2876/// Find ALL repetitions whose entry segment qualifier matches (case-insensitive).
2877fn find_all_reps_by_entry_qualifier<'a>(
2878    reps: &'a [AssembledGroupInstance],
2879    qualifier: &str,
2880) -> Vec<&'a AssembledGroupInstance> {
2881    // Support compound qualifiers like "za1_za2" — match any part.
2882    let parts: Vec<&str> = qualifier.split('_').collect();
2883    reps.iter()
2884        .filter(|inst| {
2885            inst.segments.first().is_some_and(|seg| {
2886                seg.elements
2887                    .first()
2888                    .and_then(|e| e.first())
2889                    .is_some_and(|v| parts.iter().any(|part| v.eq_ignore_ascii_case(part)))
2890            })
2891        })
2892        .collect()
2893}
2894
2895/// Check if a source_path contains qualifier suffixes (e.g., "sg8_z98").
2896fn has_source_path_qualifiers(source_path: &str) -> bool {
2897    source_path.split('.').any(|part| {
2898        if let Some(pos) = part.find('_') {
2899            pos < part.len() - 1
2900        } else {
2901            false
2902        }
2903    })
2904}
2905
2906fn parse_group_spec(part: &str) -> (&str, Option<usize>) {
2907    if let Some(colon_pos) = part.find(':') {
2908        let id = &part[..colon_pos];
2909        let rep = part[colon_pos + 1..].parse::<usize>().ok();
2910        (id, rep)
2911    } else {
2912        (part, None)
2913    }
2914}
2915
2916/// Strip the transaction group prefix from a source_group path.
2917///
2918/// Given `source_group = "SG4.SG8:0.SG10"` and `tx_group = "SG4"`,
2919/// returns `"SG8:0.SG10"`.
2920/// Given `source_group = "SG4"` and `tx_group = "SG4"`, returns `""`.
2921fn strip_tx_group_prefix(source_group: &str, tx_group: &str) -> String {
2922    if source_group == tx_group || source_group.is_empty() {
2923        String::new()
2924    } else if let Some(rest) = source_group.strip_prefix(tx_group) {
2925        rest.strip_prefix('.').unwrap_or(rest).to_string()
2926    } else {
2927        source_group.to_string()
2928    }
2929}
2930
2931/// Place a reverse-mapped group instance into the correct nesting position.
2932///
2933/// `relative_path` is the group path relative to the transaction group:
2934/// - `"SG5"` → top-level child group
2935/// - `"SG8:0.SG10"` → SG10 inside SG8 repetition 0
2936///
2937/// Returns the repetition index used at the first nesting level.
2938fn place_in_groups(
2939    groups: &mut Vec<AssembledGroup>,
2940    relative_path: &str,
2941    instance: AssembledGroupInstance,
2942) -> usize {
2943    let parts: Vec<&str> = relative_path.split('.').collect();
2944
2945    if parts.len() == 1 {
2946        // Leaf group: "SG5", "SG8", "SG12", or with explicit index "SG8:0"
2947        let (id, rep) = parse_group_spec(parts[0]);
2948
2949        // Find or create the group
2950        let group = if let Some(g) = groups.iter_mut().find(|g| g.group_id == id) {
2951            g
2952        } else {
2953            groups.push(AssembledGroup {
2954                group_id: id.to_string(),
2955                repetitions: vec![],
2956            });
2957            groups.last_mut().unwrap()
2958        };
2959
2960        if let Some(rep_idx) = rep {
2961            // Explicit index: place at specific position, merging into existing
2962            while group.repetitions.len() <= rep_idx {
2963                group.repetitions.push(AssembledGroupInstance {
2964                    segments: vec![],
2965                    child_groups: vec![],
2966                    entry_mig_number: None,
2967                    variant_mig_numbers: vec![],
2968                    skipped_segments: Vec::new(),
2969                    skipped_positions: Vec::new(),
2970                });
2971            }
2972            group.repetitions[rep_idx]
2973                .segments
2974                .extend(instance.segments);
2975            group.repetitions[rep_idx]
2976                .child_groups
2977                .extend(instance.child_groups);
2978            rep_idx
2979        } else {
2980            // No index: append new repetition
2981            let pos = group.repetitions.len();
2982            group.repetitions.push(instance);
2983            pos
2984        }
2985    } else {
2986        // Nested path: e.g., "SG8:0.SG10" → place SG10 inside SG8 rep 0
2987        let (parent_id, parent_rep) = parse_group_spec(parts[0]);
2988        let rep_idx = parent_rep.unwrap_or(0);
2989
2990        // Find or create the parent group
2991        let parent_group = if let Some(g) = groups.iter_mut().find(|g| g.group_id == parent_id) {
2992            g
2993        } else {
2994            groups.push(AssembledGroup {
2995                group_id: parent_id.to_string(),
2996                repetitions: vec![],
2997            });
2998            groups.last_mut().unwrap()
2999        };
3000
3001        // Ensure the target repetition exists (extend with empty instances if needed)
3002        while parent_group.repetitions.len() <= rep_idx {
3003            parent_group.repetitions.push(AssembledGroupInstance {
3004                segments: vec![],
3005                child_groups: vec![],
3006                entry_mig_number: None,
3007                variant_mig_numbers: vec![],
3008                skipped_segments: Vec::new(),
3009                skipped_positions: Vec::new(),
3010            });
3011        }
3012
3013        let remaining = parts[1..].join(".");
3014        place_in_groups(
3015            &mut parent_group.repetitions[rep_idx].child_groups,
3016            &remaining,
3017            instance,
3018        );
3019        rep_idx
3020    }
3021}
3022
3023/// Resolve the effective relative path for a child definition (depth >= 2).
3024///
3025/// If the child's relative already has an explicit parent rep index (e.g., "SG8:5.SG10"),
3026/// use it as-is. Otherwise, use the `source_path` to look up the parent's actual
3027/// repetition index from `source_path_to_rep`.
3028///
3029/// `item_idx` selects which parent rep to use when the parent created multiple reps
3030/// (e.g., two SG8 reps with ZF3 → item_idx 0 picks the first, 1 picks the second).
3031///
3032/// Example: relative = "SG8.SG10", source_path = "sg4.sg8_zf3.sg10"
3033/// → looks up "sg4.sg8_zf3" in map → finds reps [3, 4] → item_idx=1 → returns "SG8:4.SG10"
3034fn resolve_child_relative(
3035    relative: &str,
3036    source_path: Option<&str>,
3037    source_path_to_rep: &std::collections::HashMap<String, Vec<usize>>,
3038    item_idx: usize,
3039) -> String {
3040    let parts: Vec<&str> = relative.split('.').collect();
3041    if parts.is_empty() {
3042        return relative.to_string();
3043    }
3044
3045    // If first part already has explicit index, keep as-is
3046    let (parent_id, parent_rep) = parse_group_spec(parts[0]);
3047    if parent_rep.is_some() {
3048        return relative.to_string();
3049    }
3050
3051    // Try to resolve from source_path: extract parent path and look up its rep
3052    if let Some(sp) = source_path {
3053        if let Some((parent_path, _child)) = sp.rsplit_once('.') {
3054            // Exact match first.
3055            if let Some(rep_indices) = source_path_to_rep.get(parent_path) {
3056                let rep_idx = rep_indices
3057                    .get(item_idx)
3058                    .or_else(|| rep_indices.last())
3059                    .copied()
3060                    .unwrap_or(0);
3061                let rest = parts[1..].join(".");
3062                return format!("{}:{}.{}", parent_id, rep_idx, rest);
3063            }
3064            // Fallback: variant wildcard. When TOMLs use a flat parent path
3065            // like "sg4" but the schema splits it into variants (e.g. sg4_su,
3066            // sg4_z10..z21), union the reps from every matching variant so a
3067            // per-item iteration can place each child under its own parent.
3068            // `PidSchemaIndex::has_group` already accepts this style for
3069            // forward mapping — reverse mapping needs the same or children
3070            // from all-but-one variant get dropped (PARTIN 12 SG4 reps).
3071            let prefix = format!("{}_", parent_path);
3072            let mut unioned: Vec<usize> = source_path_to_rep
3073                .iter()
3074                .filter(|(k, _)| k.starts_with(&prefix))
3075                .flat_map(|(_, v)| v.iter().copied())
3076                .collect();
3077            if !unioned.is_empty() {
3078                unioned.sort_unstable();
3079                unioned.dedup();
3080                let rep_idx = unioned
3081                    .get(item_idx)
3082                    .or_else(|| unioned.last())
3083                    .copied()
3084                    .unwrap_or(0);
3085                let rest = parts[1..].join(".");
3086                return format!("{}:{}.{}", parent_id, rep_idx, rest);
3087            }
3088        }
3089    }
3090
3091    // No resolution possible, keep original
3092    relative.to_string()
3093}
3094
3095/// Parsed discriminator for filtering assembled group instances.
3096///
3097/// Discriminator format: "TAG.element_idx.component_idx=VALUE" or
3098/// "TAG.element_idx.component_idx=VAL1|VAL2" (pipe-separated multi-value).
3099/// E.g., "LOC.0.0=Z17" → match LOC segments where elements[0][0] == "Z17"
3100/// E.g., "RFF.0.0=Z49|Z53" → match RFF where elements[0][0] is Z49 OR Z53
3101struct DiscriminatorMatcher<'a> {
3102    tag: &'a str,
3103    element_idx: usize,
3104    component_idx: usize,
3105    expected_values: Vec<&'a str>,
3106    /// Optional occurrence index: `#N` selects the Nth match among instances.
3107    occurrence: Option<usize>,
3108}
3109
3110impl<'a> DiscriminatorMatcher<'a> {
3111    fn parse(disc: &'a str) -> Option<Self> {
3112        let (spec, expected) = disc.split_once('=')?;
3113        let parts: Vec<&str> = spec.split('.').collect();
3114        if parts.len() != 3 {
3115            return None;
3116        }
3117        let (expected_raw, occurrence) = parse_discriminator_occurrence(expected);
3118        Some(Self {
3119            tag: parts[0],
3120            element_idx: parts[1].parse().ok()?,
3121            component_idx: parts[2].parse().ok()?,
3122            expected_values: expected_raw.split('|').collect(),
3123            occurrence,
3124        })
3125    }
3126
3127    fn matches(&self, instance: &AssembledGroupInstance) -> bool {
3128        instance.segments.iter().any(|s| {
3129            s.tag.eq_ignore_ascii_case(self.tag)
3130                && s.elements
3131                    .get(self.element_idx)
3132                    .and_then(|e| e.get(self.component_idx))
3133                    .map(|v| self.expected_values.iter().any(|ev| v == ev))
3134                    .unwrap_or(false)
3135        })
3136    }
3137
3138    /// Filter instances, respecting the occurrence index if present.
3139    fn filter_instances<'b>(
3140        &self,
3141        instances: Vec<&'b AssembledGroupInstance>,
3142    ) -> Vec<&'b AssembledGroupInstance> {
3143        let matching: Vec<_> = instances
3144            .into_iter()
3145            .filter(|inst| self.matches(inst))
3146            .collect();
3147        if let Some(occ) = self.occurrence {
3148            matching.into_iter().nth(occ).into_iter().collect()
3149        } else {
3150            matching
3151        }
3152    }
3153}
3154
3155/// Parse an optional occurrence index from a discriminator expected value.
3156///
3157/// `"TN#1"` → `("TN", Some(1))` — select the 2nd matching rep
3158/// `"TN"`   → `("TN", None)` — select all matching reps
3159/// `"Z13|Z14#0"` → `("Z13|Z14", Some(0))` — first match among Z13 or Z14
3160fn parse_discriminator_occurrence(expected: &str) -> (&str, Option<usize>) {
3161    if let Some(hash_pos) = expected.rfind('#') {
3162        if let Ok(occ) = expected[hash_pos + 1..].parse::<usize>() {
3163            return (&expected[..hash_pos], Some(occ));
3164        }
3165    }
3166    (expected, None)
3167}
3168
3169/// Strip explicit rep index from a relative path: "SG5:4" → "SG5", "SG8:3" → "SG8".
3170/// Used for multi-rep entities where subsequent items should append rather than
3171/// merge into the same rep position.
3172fn strip_rep_index(relative: &str) -> String {
3173    let (id, _) = parse_group_spec(relative);
3174    id.to_string()
3175}
3176
3177/// Strip all explicit rep indices from a multi-part relative path:
3178/// "SG8:3.SG10" → "SG8.SG10", "SG8:3.SG10:0" → "SG8.SG10".
3179/// Used for multi-rep depth-2+ entities so resolve_child_relative uses
3180/// source_path lookup instead of hardcoded indices.
3181fn strip_all_rep_indices(relative: &str) -> String {
3182    relative
3183        .split('.')
3184        .map(|part| {
3185            let (id, _) = parse_group_spec(part);
3186            id
3187        })
3188        .collect::<Vec<_>>()
3189        .join(".")
3190}
3191
3192/// Parse a segment tag with optional qualifier and occurrence index.
3193///
3194/// - `"dtm[92]"`    → `("DTM", Some("92"), 0)` — first (default) occurrence
3195/// - `"rff[Z34,1]"` → `("RFF", Some("Z34"), 1)` — second occurrence (0-indexed)
3196/// - `"rff[Z34,*]"` → `("RFF", Some("Z34"), 0)` — wildcard occurrence
3197/// - `"rff"`         → `("RFF", None, 0)`
3198fn parse_tag_qualifier(tag_part: &str) -> (String, Option<&str>, usize) {
3199    if let Some(bracket_start) = tag_part.find('[') {
3200        let tag = tag_part[..bracket_start].to_uppercase();
3201        let inner = tag_part[bracket_start + 1..].trim_end_matches(']');
3202        if let Some(comma_pos) = inner.find(',') {
3203            let qualifier = &inner[..comma_pos];
3204            let index = inner[comma_pos + 1..].parse::<usize>().unwrap_or(0);
3205            // "*" wildcard means no qualifier filter — positional access only
3206            if qualifier == "*" {
3207                (tag, None, index)
3208            } else {
3209                (tag, Some(qualifier), index)
3210            }
3211        } else {
3212            (tag, Some(inner), 0)
3213        }
3214    } else {
3215        (tag_part.to_uppercase(), None, 0)
3216    }
3217}
3218
3219/// Deep-merge a BO4E value into the result map.
3220///
3221/// If the entity already exists as an object, new fields are merged in
3222/// (existing fields are NOT overwritten). This allows multiple TOML
3223/// definitions with the same `entity` name to contribute fields to one object.
3224pub fn deep_merge_insert(
3225    result: &mut serde_json::Map<String, serde_json::Value>,
3226    entity: &str,
3227    bo4e: serde_json::Value,
3228) {
3229    if let Some(existing) = result.get_mut(entity) {
3230        // Array + Array: element-wise merge (same entity from multiple TOML defs,
3231        // each producing an array for multi-rep groups like two LOC+Z17).
3232        if let (Some(existing_arr), Some(new_arr)) =
3233            (existing.as_array().map(|a| a.len()), bo4e.as_array())
3234        {
3235            if existing_arr == new_arr.len() {
3236                let existing_arr = existing.as_array_mut().unwrap();
3237                for (existing_elem, new_elem) in existing_arr.iter_mut().zip(new_arr) {
3238                    if let (Some(existing_map), Some(new_map)) =
3239                        (existing_elem.as_object_mut(), new_elem.as_object())
3240                    {
3241                        for (k, v) in new_map {
3242                            if let Some(existing_v) = existing_map.get_mut(k) {
3243                                if let (Some(existing_inner), Some(new_inner)) =
3244                                    (existing_v.as_object_mut(), v.as_object())
3245                                {
3246                                    for (ik, iv) in new_inner {
3247                                        existing_inner
3248                                            .entry(ik.clone())
3249                                            .or_insert_with(|| iv.clone());
3250                                    }
3251                                }
3252                            } else {
3253                                existing_map.insert(k.clone(), v.clone());
3254                            }
3255                        }
3256                    }
3257                }
3258                return;
3259            }
3260        }
3261        // Object + Object: field-level merge
3262        if let (Some(existing_map), serde_json::Value::Object(new_map)) =
3263            (existing.as_object_mut(), &bo4e)
3264        {
3265            for (k, v) in new_map {
3266                if let Some(existing_v) = existing_map.get_mut(k) {
3267                    // Recursively merge nested objects (e.g., companion types)
3268                    if let (Some(existing_inner), Some(new_inner)) =
3269                        (existing_v.as_object_mut(), v.as_object())
3270                    {
3271                        for (ik, iv) in new_inner {
3272                            existing_inner
3273                                .entry(ik.clone())
3274                                .or_insert_with(|| iv.clone());
3275                        }
3276                    }
3277                    // Don't overwrite existing scalar/array values
3278                } else {
3279                    existing_map.insert(k.clone(), v.clone());
3280                }
3281            }
3282            return;
3283        }
3284    }
3285    result.insert(entity.to_string(), bo4e);
3286}
3287
3288/// Convert a PascalCase name to camelCase by lowering the first character.
3289///
3290/// E.g., `"Ansprechpartner"` → `"ansprechpartner"`,
3291/// `"AnsprechpartnerEdifact"` → `"ansprechpartnerEdifact"`,
3292/// `"ProduktpaketPriorisierung"` → `"produktpaketPriorisierung"`.
3293/// Detect whether a JSON object looks like a map-keyed entity (typed PID format).
3294///
3295/// Map-keyed objects have short uppercase/alphanumeric keys that look like qualifier
3296/// codes (e.g., `{"Z04": {...}, "Z09": {...}}` or `{"MS": {...}, "MR": {...}}`),
3297/// as opposed to normal field-name objects (e.g., `{"name1": "...", "adresse": {...}}`).
3298fn is_map_keyed_object(value: &serde_json::Value) -> bool {
3299    let Some(obj) = value.as_object() else {
3300        return false;
3301    };
3302    if obj.is_empty() {
3303        return false;
3304    }
3305    // All keys must be short (≤5 chars), uppercase/digit only, and all values must be objects
3306    obj.iter().all(|(k, v)| {
3307        k.len() <= 5
3308            && k.chars()
3309                .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
3310            && v.is_object()
3311    })
3312}
3313
3314/// Find the BO4E companion field name used for the qualifier/discriminator
3315/// across definitions that share the same entity name.
3316///
3317/// For example, if `Geschaeftspartner` has a definition with discriminator
3318/// `NAD.0.0=Z04` and companion field `nad.0.0 → nadQualifier`, this returns
3319/// `Some("nadQualifier")`.
3320///
3321/// Used to inject map keys into inner objects when converting map-keyed entities.
3322fn find_qualifier_companion_field(
3323    definitions: &[crate::definition::MappingDefinition],
3324    entity: &str,
3325) -> Option<String> {
3326    for def in definitions {
3327        if def.meta.entity != *entity {
3328            continue;
3329        }
3330        let disc = def.meta.discriminator.as_deref()?;
3331        let (disc_path, _) = disc.split_once('=')?;
3332        let disc_path_lower = disc_path.to_lowercase();
3333
3334        // Search [fields] for the qualifier field (e.g., Marktteilnehmer has
3335        // "marktrolle" in [fields]).
3336        for (path, mapping) in &def.fields {
3337            let cf_path = path.to_lowercase();
3338            let matches = cf_path == disc_path_lower || format!("{}.0", cf_path) == disc_path_lower;
3339            if matches {
3340                let target = match mapping {
3341                    FieldMapping::Simple(t) => t.as_str(),
3342                    FieldMapping::Structured(s) => s.target.as_str(),
3343                    FieldMapping::Nested(_) => continue,
3344                };
3345                if !target.is_empty() {
3346                    return Some(target.to_string());
3347                }
3348            }
3349        }
3350    }
3351    None
3352}
3353
3354/// Extract a child entity from its parent entity in the reverse mapping input.
3355///
3356/// When a child entity (e.g., Kontakt with source_group="SG2.SG3") isn't found
3357/// at the top level, look inside the parent entity (e.g., Marktteilnehmer with
3358/// source_group="SG2") for a nested field matching the child's camelCase name.
3359///
3360/// For map-keyed parents ({"MS": {...}, "MR": {...}}), collects child values
3361/// from all inner objects that have the field, returning them as an array.
3362fn extract_child_from_parent(
3363    entities: &serde_json::Value,
3364    definitions: &[MappingDefinition],
3365    child_def: &MappingDefinition,
3366) -> Option<serde_json::Value> {
3367    extract_child_from_parent_with_indices(entities, definitions, child_def).map(|(v, _)| v)
3368}
3369
3370/// Like `extract_child_from_parent`, but also returns the parent rep indices
3371/// from which each child was extracted.  This allows the nesting distribution
3372/// to place child groups under the correct parent rep even when `nesting_info`
3373/// is unavailable (e.g., typed struct / manual JSON construction).
3374fn extract_child_from_parent_with_indices(
3375    entities: &serde_json::Value,
3376    definitions: &[MappingDefinition],
3377    child_def: &MappingDefinition,
3378) -> Option<(serde_json::Value, Vec<usize>)> {
3379    let parts: Vec<&str> = child_def.meta.source_group.split('.').collect();
3380    if parts.len() < 2 {
3381        return None;
3382    }
3383    let parent_group = parts[0];
3384    let parent_def = definitions
3385        .iter()
3386        .find(|d| d.meta.source_group == parent_group && d.meta.entity != child_def.meta.entity)?;
3387    let parent_key = to_camel_case(&parent_def.meta.entity);
3388    let child_key = to_camel_case(&child_def.meta.entity);
3389    let parent_value = entities.get(&parent_key)?;
3390
3391    // Map-keyed parent: collect child from each inner object
3392    if let Some(parent_map) = parent_value.as_object() {
3393        if is_map_keyed_value(parent_map) {
3394            let mut children: Vec<serde_json::Value> = Vec::new();
3395            let mut indices: Vec<usize> = Vec::new();
3396            for (i, (_key, inner)) in parent_map.iter().enumerate() {
3397                if let Some(child) = inner.get(&child_key) {
3398                    if !child.is_null() {
3399                        children.push(child.clone());
3400                        indices.push(i);
3401                    }
3402                }
3403            }
3404            return match children.len() {
3405                0 => None,
3406                1 => Some((children.into_iter().next().unwrap(), indices)),
3407                _ => Some((serde_json::Value::Array(children), indices)),
3408            };
3409        }
3410    }
3411
3412    // Array parent: collect child from each element
3413    if let Some(parent_arr) = parent_value.as_array() {
3414        let mut children: Vec<serde_json::Value> = Vec::new();
3415        let mut indices: Vec<usize> = Vec::new();
3416        for (i, item) in parent_arr.iter().enumerate() {
3417            if let Some(child) = item.get(&child_key) {
3418                if !child.is_null() {
3419                    children.push(child.clone());
3420                    indices.push(i);
3421                }
3422            }
3423        }
3424        return match children.len() {
3425            0 => None,
3426            1 => Some((children.into_iter().next().unwrap(), indices)),
3427            _ => Some((serde_json::Value::Array(children), indices)),
3428        };
3429    }
3430
3431    // Single parent object — always index 0
3432    let child = parent_value.get(&child_key)?;
3433    if child.is_null() {
3434        return None;
3435    }
3436    Some((child.clone(), vec![0]))
3437}
3438
3439/// Move child entities under their parent entities in the forward-mapped result.
3440///
3441/// For each definition with a dotted `source_group` (e.g., "SG2.SG3"), finds the
3442/// parent definition (e.g., "SG2") and moves the child entity from the top-level
3443/// result into the parent entity as a nested field.
3444fn nest_child_entities_in_result(
3445    result: &mut serde_json::Map<String, serde_json::Value>,
3446    definitions: &[MappingDefinition],
3447    nesting_info: &std::collections::HashMap<String, Vec<usize>>,
3448    transaction_group: Option<&str>,
3449) {
3450    // Collect parent→child relationships from definitions.
3451    // parent_group → (parent_entity, child_entity, child_source_path)
3452    let mut nesting_pairs: Vec<(String, String, String, Option<String>)> = Vec::new();
3453    for def in definitions {
3454        let parts: Vec<&str> = def.meta.source_group.split('.').collect();
3455        if parts.len() < 2 {
3456            continue;
3457        }
3458        let parent_group = parts[0];
3459        // Skip nesting when the parent group is the transaction root. SG4 in UTILMD
3460        // IS the transaction — its direct children (Marktlokation, Geschaeftspartner,
3461        // ProduktpaketDaten, …) are peers of the transaction metadata (Prozessdaten),
3462        // not sub-objects of it. Nesting still applies to other parents (e.g. SG2.SG3
3463        // Kontakt stays nested under SG2 Marktteilnehmer).
3464        if transaction_group.is_some_and(|tx| tx == parent_group) {
3465            continue;
3466        }
3467        let child_entity = def.meta.entity.clone();
3468        // Skip if the child entity also has a definition at the parent group level.
3469        // E.g., Prozessdaten at SG4.SG6 enriches Prozessdaten at SG4 via deep_merge —
3470        // this is same-entity enrichment, not a parent-child nesting relationship.
3471        let child_has_parent_level_def = definitions
3472            .iter()
3473            .any(|d| d.meta.source_group == parent_group && d.meta.entity == child_entity);
3474        if child_has_parent_level_def {
3475            continue;
3476        }
3477        // Find the parent definition (a different entity at the parent group level)
3478        let parent_entity = definitions
3479            .iter()
3480            .find(|d| d.meta.source_group == parent_group && d.meta.entity != child_entity)
3481            .map(|d| d.meta.entity.clone());
3482        if let Some(ref parent_entity) = parent_entity {
3483            // Skip nesting if the parent definition has a dotted field target
3484            // that creates a sub-object with the same name as the child entity.
3485            // E.g., Prozessdaten has "zeitscheibe.referenz" which creates
3486            // prozessdaten.zeitscheibe — collides with nesting Zeitscheibe entity.
3487            let child_key_lc = to_camel_case(&child_entity);
3488            let parent_defs: Vec<_> = definitions
3489                .iter()
3490                .filter(|d| d.meta.entity == *parent_entity)
3491                .collect();
3492            let has_conflicting_field = parent_defs.iter().any(|pd| {
3493                pd.fields.values().any(|fm| {
3494                    let target = match fm {
3495                        crate::definition::FieldMapping::Simple(t) => t.as_str(),
3496                        crate::definition::FieldMapping::Structured(s) => s.target.as_str(),
3497                        crate::definition::FieldMapping::Nested(_) => "",
3498                    };
3499                    target.starts_with(&child_key_lc)
3500                        && target.get(child_key_lc.len()..child_key_lc.len() + 1) == Some(".")
3501                })
3502            });
3503            if has_conflicting_field {
3504                continue;
3505            }
3506            // Avoid duplicates
3507            if nesting_pairs
3508                .iter()
3509                .any(|(_, pe, ce, _)| *pe == *parent_entity && *ce == child_entity)
3510            {
3511                continue;
3512            }
3513            nesting_pairs.push((
3514                parent_group.to_string(),
3515                parent_entity.clone(),
3516                child_entity,
3517                def.meta.source_path.clone(),
3518            ));
3519        }
3520    }
3521
3522    for (_parent_group, parent_entity, child_entity, child_source_path) in nesting_pairs {
3523        let parent_key = to_camel_case(&parent_entity);
3524        let child_key = to_camel_case(&child_entity);
3525
3526        // Remove child from top level (if present)
3527        let child_value = match result.remove(&child_key) {
3528            Some(v) => v,
3529            None => continue,
3530        };
3531
3532        // Get parent value.
3533        // If the parent is a plain array (not map-keyed), nesting would silently
3534        // place the child into arbitrary array elements. Skip and leave the child
3535        // at the top level where the reverse mapper can find it.
3536        let Some(parent_value) = result.get_mut(&parent_key) else {
3537            // Parent doesn't exist — put child back
3538            result.insert(child_key, child_value);
3539            continue;
3540        };
3541        if parent_value.is_array() {
3542            result.insert(child_key, child_value);
3543            continue;
3544        }
3545
3546        // Get the nesting distribution (which parent rep each child rep belongs to)
3547        let distribution = child_source_path
3548            .as_deref()
3549            .and_then(|sp| nesting_info.get(sp));
3550
3551        // Normalize child to a list of (index, value) pairs
3552        let child_items: Vec<(usize, &serde_json::Value)> = match &child_value {
3553            serde_json::Value::Array(arr) => arr.iter().enumerate().collect(),
3554            other => vec![(0, other)],
3555        };
3556
3557        // Helper: insert or append child value into a parent object field.
3558        // First call inserts the value; subsequent calls convert to array and append.
3559        let insert_or_append = |obj: &mut serde_json::Map<String, serde_json::Value>,
3560                                key: &str,
3561                                val: &serde_json::Value| {
3562            match obj.get_mut(key) {
3563                Some(existing) => {
3564                    // Convert single value to array, then push
3565                    if !existing.is_array() {
3566                        let prev = existing.take();
3567                        *existing = serde_json::Value::Array(vec![prev]);
3568                    }
3569                    if let Some(arr) = existing.as_array_mut() {
3570                        arr.push(val.clone());
3571                    }
3572                }
3573                None => {
3574                    obj.insert(key.to_string(), val.clone());
3575                }
3576            }
3577        };
3578
3579        // Handle parent as map-keyed object: {"MS": {...}, "MR": {...}}
3580        if let Some(parent_map) = parent_value.as_object_mut() {
3581            if is_map_keyed_value(parent_map) {
3582                // Map keys in insertion order correspond to rep indices
3583                let keys: Vec<String> = parent_map.keys().cloned().collect();
3584                for (i, child_item) in &child_items {
3585                    let target_idx = distribution
3586                        .and_then(|dist| dist.get(*i))
3587                        .copied()
3588                        .unwrap_or(0);
3589                    if let Some(key) = keys.get(target_idx) {
3590                        if let Some(inner) = parent_map.get_mut(key).and_then(|v| v.as_object_mut())
3591                        {
3592                            insert_or_append(inner, &child_key, child_item);
3593                        }
3594                    }
3595                }
3596                continue;
3597            }
3598        }
3599
3600        // Handle parent as array
3601        if let Some(parent_arr) = parent_value.as_array_mut() {
3602            for (i, child_item) in &child_items {
3603                let target_idx = distribution
3604                    .and_then(|dist| dist.get(*i))
3605                    .copied()
3606                    .unwrap_or(0);
3607                if let Some(parent_obj) = parent_arr
3608                    .get_mut(target_idx)
3609                    .and_then(|v| v.as_object_mut())
3610                {
3611                    insert_or_append(parent_obj, &child_key, child_item);
3612                }
3613            }
3614            continue;
3615        }
3616
3617        // Handle parent as single object
3618        if let Some(parent_obj) = parent_value.as_object_mut() {
3619            for (_i, child_item) in &child_items {
3620                insert_or_append(parent_obj, &child_key, child_item);
3621            }
3622            continue;
3623        }
3624
3625        // Fallback: put child back at top level
3626        result.insert(child_key, child_value);
3627    }
3628}
3629
3630// NAD+DP routing post-processor lives in `crate::dp_routing`.
3631pub use crate::dp_routing::{route_nad_dp_to_lokation, unroute_lokation_to_nad_dp, DpRouting};
3632
3633/// Check if a JSON map looks like a map-keyed entity (short uppercase/code keys → objects).
3634fn is_map_keyed_value(map: &serde_json::Map<String, serde_json::Value>) -> bool {
3635    if map.is_empty() {
3636        return false;
3637    }
3638    map.values().all(|v| v.is_object())
3639        && map.keys().all(|k| {
3640            k.len() <= 5
3641                || k.chars()
3642                    .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
3643        })
3644}
3645
3646fn to_camel_case(name: &str) -> String {
3647    let mut chars = name.chars();
3648    match chars.next() {
3649        Some(c) => c.to_lowercase().to_string() + chars.as_str(),
3650        None => String::new(),
3651    }
3652}
3653
3654/// Set a value in a nested JSON map using a dotted path.
3655/// E.g., "address.city" sets `{"address": {"city": "value"}}`.
3656fn set_nested_value(map: &mut serde_json::Map<String, serde_json::Value>, path: &str, val: String) {
3657    set_nested_value_json(map, path, serde_json::Value::String(val));
3658}
3659
3660/// Like `set_nested_value` but accepts a `serde_json::Value` instead of a `String`.
3661fn set_nested_value_json(
3662    map: &mut serde_json::Map<String, serde_json::Value>,
3663    path: &str,
3664    val: serde_json::Value,
3665) {
3666    if let Some((prefix, leaf)) = path.rsplit_once('.') {
3667        let mut current = map;
3668        for part in prefix.split('.') {
3669            let entry = current
3670                .entry(part.to_string())
3671                .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
3672            current = entry.as_object_mut().expect("expected object in path");
3673        }
3674        current.insert(leaf.to_string(), val);
3675    } else {
3676        map.insert(path.to_string(), val);
3677    }
3678}
3679
3680/// Precompiled cache for a single format-version/variant (e.g., FV2504/UTILMD_Strom).
3681///
3682/// Contains all engines with paths pre-resolved, ready for immediate use.
3683/// Loading one `VariantCache` file replaces thousands of individual `.bin` reads.
3684#[derive(serde::Serialize, serde::Deserialize)]
3685pub struct VariantCache {
3686    /// Message-level definitions (shared across PIDs).
3687    pub message_defs: Vec<MappingDefinition>,
3688    /// Per-PID transaction definitions (key: "pid_55001").
3689    pub transaction_defs: HashMap<String, Vec<MappingDefinition>>,
3690    /// Per-PID combined definitions (key: "pid_55001").
3691    pub combined_defs: HashMap<String, Vec<MappingDefinition>>,
3692    /// Per-PID code lookups (key: "pid_55001"). Cached to avoid reading schema JSONs at load time.
3693    #[serde(default)]
3694    pub code_lookups: HashMap<String, crate::code_lookup::CodeLookup>,
3695    /// Parsed MIG schema — cached to avoid re-parsing MIG XML at startup.
3696    #[serde(default)]
3697    pub mig_schema: Option<mig_types::schema::mig::MigSchema>,
3698    /// Segment element counts derived from MIG — cached for reverse mapping padding.
3699    #[serde(default)]
3700    pub segment_structure: Option<crate::segment_structure::SegmentStructure>,
3701    /// Per-PID AHB segment numbers (key: "pid_55001"). Used for MIG filtering at runtime.
3702    /// Eliminates the need to parse AHB XML files at startup.
3703    #[serde(default)]
3704    pub pid_segment_numbers: HashMap<String, Vec<String>>,
3705    /// Per-PID field requirements (key: "pid_55001"). Built from PID schema + TOML definitions.
3706    /// Used by `validate_pid()` to check field completeness.
3707    #[serde(default)]
3708    pub pid_requirements: HashMap<String, crate::pid_requirements::PidRequirements>,
3709    /// Per-PID pre-built AHB workflow (key: "pid_55001"). The EDIFACT-side rulebook
3710    /// (segment-path keyed), twin of `pid_requirements` (BO4E-entity keyed). Built at
3711    /// compile-mappings from the PID schema JSON so downstream consumers can run full
3712    /// raw-EDIFACT validation (`Mapper::validate_edifact`) without the schema files.
3713    #[serde(default)]
3714    pub pid_ahb_workflows: HashMap<String, ahb_types::AhbWorkflow>,
3715    /// Per-PID transaction group ID (key: "pid_55001", value: "SG4").
3716    /// Derived from the common `source_group` prefix of transaction definitions.
3717    /// Empty string for message-only variants (e.g., ORDCHG).
3718    #[serde(default)]
3719    pub tx_groups: HashMap<String, String>,
3720}
3721
3722impl VariantCache {
3723    /// Save this variant cache to a single JSON file.
3724    pub fn save(&self, path: &Path) -> Result<(), MappingError> {
3725        let encoded = serde_json::to_vec(self).map_err(|e| MappingError::CacheWrite {
3726            path: path.display().to_string(),
3727            message: e.to_string(),
3728        })?;
3729        if let Some(parent) = path.parent() {
3730            std::fs::create_dir_all(parent)?;
3731        }
3732        std::fs::write(path, encoded)?;
3733        Ok(())
3734    }
3735
3736    /// Load a variant cache from a single JSON file.
3737    pub fn load(path: &Path) -> Result<Self, MappingError> {
3738        let bytes = std::fs::read(path)?;
3739        serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
3740            path: path.display().to_string(),
3741            message: e.to_string(),
3742        })
3743    }
3744
3745    /// Get the transaction group for a PID (e.g., "SG4" for UTILMD PIDs).
3746    /// Returns `None` if the PID is not in this variant.
3747    /// Returns `Some("")` for message-only variants (no transaction group).
3748    pub fn tx_group(&self, pid: &str) -> Option<&str> {
3749        self.tx_groups
3750            .get(&format!("pid_{pid}"))
3751            .map(|s| s.as_str())
3752    }
3753
3754    /// Build a `MappingEngine` from the message-level definitions, attaching
3755    /// the per-PID code lookup so forward mapping enriches code fields with
3756    /// `{ code, meaning, enum }` objects.
3757    pub fn msg_engine(&self, pid: &str) -> MappingEngine {
3758        let mut eng = MappingEngine::from_definitions(self.message_defs.clone()).with_pid(pid);
3759        if let Some(cl) = self.code_lookups.get(&format!("pid_{pid}")) {
3760            eng = eng.with_code_lookup(cl.clone());
3761        }
3762        eng
3763    }
3764
3765    /// Build a `MappingEngine` from the transaction-level definitions for a PID,
3766    /// attaching the per-PID code lookup. Returns `None` if the PID is not in
3767    /// this variant.
3768    pub fn tx_engine(&self, pid: &str) -> Option<MappingEngine> {
3769        self.transaction_defs
3770            .get(&format!("pid_{pid}"))
3771            .map(|defs| {
3772                let mut eng = MappingEngine::from_definitions(defs.clone()).with_pid(pid);
3773                if let Some(cl) = self.code_lookups.get(&format!("pid_{pid}")) {
3774                    eng = eng.with_code_lookup(cl.clone());
3775                }
3776                eng
3777            })
3778    }
3779
3780    /// Get a PID-filtered MIG schema.
3781    /// Returns `None` if no MIG schema or no segment numbers for this PID.
3782    ///
3783    /// Falls back to the empty-PID workflow's segment numbers when the AHB
3784    /// has no Pruefidentifikator attribute (e.g., APERAK — one workflow for
3785    /// all BGM doc codes). This lets `from_edifact` work for variants whose
3786    /// AHB doesn't enumerate per-PID segment numbers.
3787    pub fn filtered_mig(&self, pid: &str) -> Option<mig_types::schema::mig::MigSchema> {
3788        let mig = self.mig_schema.as_ref()?;
3789        let numbers = self
3790            .pid_segment_numbers
3791            .get(&format!("pid_{pid}"))
3792            .or_else(|| self.pid_segment_numbers.get("pid_"))?;
3793        let number_set: std::collections::HashSet<String> = numbers.iter().cloned().collect();
3794        Some(mig_assembly::pid_filter::filter_mig_for_pid(
3795            mig,
3796            &number_set,
3797        ))
3798    }
3799}
3800
3801/// Bundled data for a single format version (e.g., FV2504).
3802///
3803/// Contains all VariantCaches for every message type in that FV,
3804/// serialized as one bincode file for distribution via GitHub releases.
3805#[derive(serde::Serialize, serde::Deserialize)]
3806pub struct DataBundle {
3807    pub format_version: String,
3808    pub bundle_version: u32,
3809    pub variants: HashMap<String, VariantCache>,
3810    /// PID-agnostic BO4E type catalog (parsed from `bo4e-german` source).
3811    ///
3812    /// Populated by the bundle generator at compile-mappings time. Older bundles
3813    /// without this field deserialize to an empty catalog.
3814    #[serde(default)]
3815    pub bo4e_catalog: crate::bo4e_catalog::Bo4eCatalog,
3816}
3817
3818impl DataBundle {
3819    pub const CURRENT_VERSION: u32 = 2;
3820
3821    pub fn variant(&self, name: &str) -> Option<&VariantCache> {
3822        self.variants.get(name)
3823    }
3824
3825    pub fn write_to<W: std::io::Write>(&self, writer: &mut W) -> Result<(), MappingError> {
3826        let encoded = serde_json::to_vec(self).map_err(|e| MappingError::CacheWrite {
3827            path: "<stream>".to_string(),
3828            message: e.to_string(),
3829        })?;
3830        writer.write_all(&encoded).map_err(MappingError::Io)
3831    }
3832
3833    pub fn read_from<R: std::io::Read>(reader: &mut R) -> Result<Self, MappingError> {
3834        let mut bytes = Vec::new();
3835        reader.read_to_end(&mut bytes).map_err(MappingError::Io)?;
3836        serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
3837            path: "<stream>".to_string(),
3838            message: e.to_string(),
3839        })
3840    }
3841
3842    pub fn read_from_checked<R: std::io::Read>(reader: &mut R) -> Result<Self, MappingError> {
3843        let bundle = Self::read_from(reader)?;
3844        if bundle.bundle_version != Self::CURRENT_VERSION {
3845            return Err(MappingError::CacheRead {
3846                path: "<stream>".to_string(),
3847                message: format!(
3848                    "Incompatible bundle version {}, expected version {}. \
3849                     Run `edifact-data update` to fetch compatible bundles.",
3850                    bundle.bundle_version,
3851                    Self::CURRENT_VERSION
3852                ),
3853            });
3854        }
3855        Ok(bundle)
3856    }
3857
3858    pub fn save(&self, path: &Path) -> Result<(), MappingError> {
3859        if let Some(parent) = path.parent() {
3860            std::fs::create_dir_all(parent)?;
3861        }
3862        let mut file = std::fs::File::create(path).map_err(MappingError::Io)?;
3863        self.write_to(&mut file)
3864    }
3865
3866    pub fn load(path: &Path) -> Result<Self, MappingError> {
3867        let mut file = std::fs::File::open(path).map_err(MappingError::Io)?;
3868        Self::read_from_checked(&mut file)
3869    }
3870}
3871
3872/// Sort variant reps within child groups to match MIG-defined variant order.
3873///
3874/// The reverse mapper appends reps in definition-filename order, but the
3875/// assembler captures them in the order MIG variants are defined (which is
3876/// the canonical EDIFACT order). This function reorders reps within same-ID
3877/// groups to match the MIG's nested_groups ordering.
3878///
3879/// Uses position-aware qualifier matching: each MIG variant has a
3880/// `variant_code` and `variant_qualifier_position` that specifies WHERE
3881/// the qualifier lives in the entry segment (e.g., SEQ qualifier at [0][0],
3882/// CCI qualifier at [2][0]). This correctly handles groups where different
3883/// variants have qualifiers at different positions.
3884fn sort_variant_reps_by_mig(
3885    child_groups: &mut [AssembledGroup],
3886    mig: &MigSchema,
3887    transaction_group: &str,
3888) {
3889    let tx_def = match mig
3890        .segment_groups
3891        .iter()
3892        .find(|sg| sg.id == transaction_group)
3893    {
3894        Some(d) => d,
3895        None => return,
3896    };
3897
3898    for cg in child_groups.iter_mut() {
3899        if cg.repetitions.len() <= 1 {
3900            continue;
3901        }
3902
3903        // Collect all MIG variant definitions for this group_id, in MIG order.
3904        let variant_defs: Vec<(usize, &mig_types::schema::mig::MigSegmentGroup)> = tx_def
3905            .nested_groups
3906            .iter()
3907            .enumerate()
3908            .filter(|(_, ng)| ng.id == cg.group_id && ng.variant_code.is_some())
3909            .collect();
3910
3911        if variant_defs.is_empty() {
3912            continue;
3913        }
3914
3915        // Sort reps: for each rep, find which MIG variant it matches by
3916        // checking the entry segment's qualifier at each variant's specific position.
3917        cg.repetitions.sort_by_key(|rep| {
3918            let entry_seg = rep.segments.first();
3919            for &(mig_pos, variant_def) in &variant_defs {
3920                let (ei, ci) = variant_def.variant_qualifier_position.unwrap_or((0, 0));
3921                let actual_qual = entry_seg
3922                    .and_then(|s| s.elements.get(ei))
3923                    .and_then(|e| e.get(ci))
3924                    .map(|s| s.as_str())
3925                    .unwrap_or("");
3926                let matches = if !variant_def.variant_codes.is_empty() {
3927                    variant_def
3928                        .variant_codes
3929                        .iter()
3930                        .any(|c| actual_qual.eq_ignore_ascii_case(c))
3931                } else if let Some(ref expected_code) = variant_def.variant_code {
3932                    actual_qual.eq_ignore_ascii_case(expected_code)
3933                } else {
3934                    false
3935                };
3936                if matches {
3937                    return mig_pos;
3938                }
3939            }
3940            usize::MAX // unmatched reps go to the end
3941        });
3942    }
3943}
3944
3945#[cfg(test)]
3946mod variant_cache_helper_tests {
3947    use super::*;
3948
3949    fn make_test_cache() -> VariantCache {
3950        let mut tx_groups = HashMap::new();
3951        tx_groups.insert("pid_55001".to_string(), "SG4".to_string());
3952        tx_groups.insert("pid_21007".to_string(), "SG14".to_string());
3953
3954        let mut transaction_defs = HashMap::new();
3955        transaction_defs.insert("pid_55001".to_string(), vec![]);
3956        transaction_defs.insert("pid_21007".to_string(), vec![]);
3957
3958        VariantCache {
3959            message_defs: vec![],
3960            transaction_defs,
3961            combined_defs: HashMap::new(),
3962            code_lookups: HashMap::new(),
3963            mig_schema: None,
3964            segment_structure: None,
3965            pid_segment_numbers: HashMap::new(),
3966            pid_requirements: HashMap::new(),
3967            pid_ahb_workflows: HashMap::new(),
3968            tx_groups,
3969        }
3970    }
3971
3972    #[test]
3973    fn test_tx_group_returns_correct_group() {
3974        let vc = make_test_cache();
3975        assert_eq!(vc.tx_group("55001").unwrap(), "SG4");
3976        assert_eq!(vc.tx_group("21007").unwrap(), "SG14");
3977    }
3978
3979    #[test]
3980    fn test_tx_group_unknown_pid_returns_none() {
3981        let vc = make_test_cache();
3982        assert!(vc.tx_group("99999").is_none());
3983    }
3984
3985    #[test]
3986    fn test_msg_engine_returns_engine() {
3987        let vc = make_test_cache();
3988        let engine = vc.msg_engine("55001");
3989        assert_eq!(engine.definitions().len(), 0);
3990    }
3991
3992    #[test]
3993    fn test_tx_engine_returns_engine_for_known_pid() {
3994        let vc = make_test_cache();
3995        assert!(vc.tx_engine("55001").is_some());
3996    }
3997
3998    #[test]
3999    fn test_tx_engine_returns_none_for_unknown_pid() {
4000        let vc = make_test_cache();
4001        assert!(vc.tx_engine("99999").is_none());
4002    }
4003}
4004
4005#[cfg(test)]
4006mod tests {
4007    use super::*;
4008    use crate::definition::{MappingDefinition, MappingMeta, StructuredFieldMapping};
4009    use indexmap::IndexMap;
4010
4011    fn make_def(fields: IndexMap<String, FieldMapping>) -> MappingDefinition {
4012        MappingDefinition {
4013            meta: MappingMeta {
4014                entity: "Test".to_string(),
4015                bo4e_type: "Test".to_string(),
4016                source_group: "SG4".to_string(),
4017                source_path: None,
4018                discriminator: None,
4019                repeat_on_tag: None,
4020            },
4021            fields,
4022            complex_handlers: None,
4023        }
4024    }
4025
4026    #[test]
4027    fn test_map_interchange_single_transaction_backward_compat() {
4028        use mig_assembly::assembler::*;
4029
4030        // Single SG4 with SG5 — the common case for current PID 55001 fixtures
4031        let tree = AssembledTree {
4032            segments: vec![
4033                AssembledSegment {
4034                    tag: "UNH".to_string(),
4035                    elements: vec![vec!["001".to_string()]],
4036                    mig_number: None,
4037                    segment_number: None,
4038                },
4039                AssembledSegment {
4040                    tag: "BGM".to_string(),
4041                    elements: vec![vec!["E01".to_string()], vec!["DOC001".to_string()]],
4042                    mig_number: None,
4043                    segment_number: None,
4044                },
4045            ],
4046            groups: vec![
4047                AssembledGroup {
4048                    group_id: "SG2".to_string(),
4049                    repetitions: vec![AssembledGroupInstance {
4050                        segments: vec![AssembledSegment {
4051                            tag: "NAD".to_string(),
4052                            elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
4053                            mig_number: None,
4054                            segment_number: None,
4055                        }],
4056                        child_groups: vec![],
4057                        entry_mig_number: None,
4058                        variant_mig_numbers: vec![],
4059                        skipped_segments: vec![],
4060                        skipped_positions: Vec::new(),
4061                    }],
4062                },
4063                AssembledGroup {
4064                    group_id: "SG4".to_string(),
4065                    repetitions: vec![AssembledGroupInstance {
4066                        segments: vec![AssembledSegment {
4067                            tag: "IDE".to_string(),
4068                            elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
4069                            mig_number: None,
4070                            segment_number: None,
4071                        }],
4072                        child_groups: vec![AssembledGroup {
4073                            group_id: "SG5".to_string(),
4074                            repetitions: vec![AssembledGroupInstance {
4075                                segments: vec![AssembledSegment {
4076                                    tag: "LOC".to_string(),
4077                                    elements: vec![
4078                                        vec!["Z16".to_string()],
4079                                        vec!["DE000111222333".to_string()],
4080                                    ],
4081                                    mig_number: None,
4082                                    segment_number: None,
4083                                }],
4084                                child_groups: vec![],
4085                                entry_mig_number: None,
4086                                variant_mig_numbers: vec![],
4087                                skipped_segments: vec![],
4088                                skipped_positions: Vec::new(),
4089                            }],
4090                        }],
4091                        entry_mig_number: None,
4092                        variant_mig_numbers: vec![],
4093                        skipped_segments: vec![],
4094                        skipped_positions: Vec::new(),
4095                    }],
4096                },
4097            ],
4098            post_group_start: 2,
4099            inter_group_segments: std::collections::BTreeMap::new(),
4100        };
4101
4102        // Empty message engine (no message-level defs for this test)
4103        let msg_engine = MappingEngine::from_definitions(vec![]);
4104
4105        // Transaction defs
4106        let mut tx_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4107        tx_fields.insert(
4108            "ide.1".to_string(),
4109            FieldMapping::Simple("vorgangId".to_string()),
4110        );
4111        let mut malo_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4112        malo_fields.insert(
4113            "loc.1".to_string(),
4114            FieldMapping::Simple("marktlokationsId".to_string()),
4115        );
4116
4117        let tx_engine = MappingEngine::from_definitions(vec![
4118            MappingDefinition {
4119                meta: MappingMeta {
4120                    entity: "Prozessdaten".to_string(),
4121                    bo4e_type: "Prozessdaten".to_string(),
4122                    source_group: "SG4".to_string(),
4123                    source_path: None,
4124                    discriminator: None,
4125                    repeat_on_tag: None,
4126                },
4127                fields: tx_fields,
4128                complex_handlers: None,
4129            },
4130            MappingDefinition {
4131                meta: MappingMeta {
4132                    entity: "Marktlokation".to_string(),
4133                    bo4e_type: "Marktlokation".to_string(),
4134                    source_group: "SG4.SG5".to_string(),
4135                    source_path: None,
4136                    discriminator: None,
4137                    repeat_on_tag: None,
4138                },
4139                fields: malo_fields,
4140                complex_handlers: None,
4141            },
4142        ]);
4143
4144        let result = MappingEngine::map_interchange(&msg_engine, &tx_engine, &tree, "SG4", true);
4145
4146        assert_eq!(result.transaktionen.len(), 1);
4147        assert_eq!(
4148            result.transaktionen[0].stammdaten["prozessdaten"]["vorgangId"]
4149                .as_str()
4150                .unwrap(),
4151            "TX001"
4152        );
4153        // Marktlokation (SG4.SG5) stays top-level — SG4 IS the transaction root,
4154        // so Marktlokation is a peer of Prozessdaten, not a child of it.
4155        assert_eq!(
4156            result.transaktionen[0].stammdaten["marktlokation"]["marktlokationsId"]
4157                .as_str()
4158                .unwrap(),
4159            "DE000111222333"
4160        );
4161    }
4162
4163    #[test]
4164    fn test_map_reverse_pads_intermediate_empty_elements() {
4165        // NAD+Z09+++Muster:Max — positions 0 and 3 populated, 1 and 2 should become [""]
4166        let mut fields = IndexMap::new();
4167        fields.insert(
4168            "nad.0".to_string(),
4169            FieldMapping::Structured(StructuredFieldMapping {
4170                target: String::new(),
4171                transform: None,
4172                when: None,
4173                default: Some("Z09".to_string()),
4174                enum_map: None,
4175                when_filled: None,
4176                also_target: None,
4177                also_enum_map: None,
4178            }),
4179        );
4180        fields.insert(
4181            "nad.3.0".to_string(),
4182            FieldMapping::Simple("name".to_string()),
4183        );
4184        fields.insert(
4185            "nad.3.1".to_string(),
4186            FieldMapping::Simple("vorname".to_string()),
4187        );
4188
4189        let def = make_def(fields);
4190        let engine = MappingEngine::from_definitions(vec![]);
4191
4192        let bo4e = serde_json::json!({
4193            "name": "Muster",
4194            "vorname": "Max"
4195        });
4196
4197        let instance = engine.map_reverse(&bo4e, &def);
4198        assert_eq!(instance.segments.len(), 1);
4199
4200        let nad = &instance.segments[0];
4201        assert_eq!(nad.tag, "NAD");
4202        assert_eq!(nad.elements.len(), 4);
4203        assert_eq!(nad.elements[0], vec!["Z09"]);
4204        // Intermediate positions 1 and 2 should be padded to [""]
4205        assert_eq!(nad.elements[1], vec![""]);
4206        assert_eq!(nad.elements[2], vec![""]);
4207        assert_eq!(nad.elements[3][0], "Muster");
4208        assert_eq!(nad.elements[3][1], "Max");
4209    }
4210
4211    #[test]
4212    fn test_map_reverse_no_padding_when_contiguous() {
4213        // DTM+92:20250531:303 — all three components in element 0, no gaps
4214        let mut fields = IndexMap::new();
4215        fields.insert(
4216            "dtm.0.0".to_string(),
4217            FieldMapping::Structured(StructuredFieldMapping {
4218                target: String::new(),
4219                transform: None,
4220                when: None,
4221                default: Some("92".to_string()),
4222                enum_map: None,
4223                when_filled: None,
4224                also_target: None,
4225                also_enum_map: None,
4226            }),
4227        );
4228        fields.insert(
4229            "dtm.0.1".to_string(),
4230            FieldMapping::Simple("value".to_string()),
4231        );
4232        fields.insert(
4233            "dtm.0.2".to_string(),
4234            FieldMapping::Structured(StructuredFieldMapping {
4235                target: String::new(),
4236                transform: None,
4237                when: None,
4238                default: Some("303".to_string()),
4239                enum_map: None,
4240                when_filled: None,
4241                also_target: None,
4242                also_enum_map: None,
4243            }),
4244        );
4245
4246        let def = make_def(fields);
4247        let engine = MappingEngine::from_definitions(vec![]);
4248
4249        let bo4e = serde_json::json!({ "value": "20250531" });
4250
4251        let instance = engine.map_reverse(&bo4e, &def);
4252        let dtm = &instance.segments[0];
4253        // Single element with 3 components — no intermediate padding needed
4254        assert_eq!(dtm.elements.len(), 1);
4255        assert_eq!(dtm.elements[0], vec!["92", "20250531", "303"]);
4256    }
4257
4258    #[test]
4259    fn test_map_message_level_extracts_sg2_only() {
4260        use mig_assembly::assembler::*;
4261
4262        // Build a tree with SG2 (message-level) and SG4 (transaction-level)
4263        let tree = AssembledTree {
4264            segments: vec![
4265                AssembledSegment {
4266                    tag: "UNH".to_string(),
4267                    elements: vec![vec!["001".to_string()]],
4268                    mig_number: None,
4269                    segment_number: None,
4270                },
4271                AssembledSegment {
4272                    tag: "BGM".to_string(),
4273                    elements: vec![vec!["E01".to_string()]],
4274                    mig_number: None,
4275                    segment_number: None,
4276                },
4277            ],
4278            groups: vec![
4279                AssembledGroup {
4280                    group_id: "SG2".to_string(),
4281                    repetitions: vec![AssembledGroupInstance {
4282                        segments: vec![AssembledSegment {
4283                            tag: "NAD".to_string(),
4284                            elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
4285                            mig_number: None,
4286                            segment_number: None,
4287                        }],
4288                        child_groups: vec![],
4289                        entry_mig_number: None,
4290                        variant_mig_numbers: vec![],
4291                        skipped_segments: vec![],
4292                        skipped_positions: Vec::new(),
4293                    }],
4294                },
4295                AssembledGroup {
4296                    group_id: "SG4".to_string(),
4297                    repetitions: vec![AssembledGroupInstance {
4298                        segments: vec![AssembledSegment {
4299                            tag: "IDE".to_string(),
4300                            elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
4301                            mig_number: None,
4302                            segment_number: None,
4303                        }],
4304                        child_groups: vec![],
4305                        entry_mig_number: None,
4306                        variant_mig_numbers: vec![],
4307                        skipped_segments: vec![],
4308                        skipped_positions: Vec::new(),
4309                    }],
4310                },
4311            ],
4312            post_group_start: 2,
4313            inter_group_segments: std::collections::BTreeMap::new(),
4314        };
4315
4316        // Message-level definition maps SG2
4317        let mut msg_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4318        msg_fields.insert(
4319            "nad.0".to_string(),
4320            FieldMapping::Simple("marktrolle".to_string()),
4321        );
4322        msg_fields.insert(
4323            "nad.1".to_string(),
4324            FieldMapping::Simple("rollencodenummer".to_string()),
4325        );
4326        let msg_def = MappingDefinition {
4327            meta: MappingMeta {
4328                entity: "Marktteilnehmer".to_string(),
4329                bo4e_type: "Marktteilnehmer".to_string(),
4330                source_group: "SG2".to_string(),
4331                source_path: None,
4332                discriminator: None,
4333                repeat_on_tag: None,
4334            },
4335            fields: msg_fields,
4336            complex_handlers: None,
4337        };
4338
4339        let engine = MappingEngine::from_definitions(vec![msg_def.clone()]);
4340        let result = engine.map_all_forward(&tree);
4341
4342        // Should contain Marktteilnehmer from SG2
4343        assert!(result.get("marktteilnehmer").is_some());
4344        let mt = &result["marktteilnehmer"];
4345        assert_eq!(mt["marktrolle"].as_str().unwrap(), "MS");
4346        assert_eq!(mt["rollencodenummer"].as_str().unwrap(), "9900123");
4347    }
4348
4349    #[test]
4350    fn test_map_transaction_scoped_to_sg4_instance() {
4351        use mig_assembly::assembler::*;
4352
4353        // Build a tree with SG4 containing SG5 (LOC+Z16)
4354        let tree = AssembledTree {
4355            segments: vec![
4356                AssembledSegment {
4357                    tag: "UNH".to_string(),
4358                    elements: vec![vec!["001".to_string()]],
4359                    mig_number: None,
4360                    segment_number: None,
4361                },
4362                AssembledSegment {
4363                    tag: "BGM".to_string(),
4364                    elements: vec![vec!["E01".to_string()]],
4365                    mig_number: None,
4366                    segment_number: None,
4367                },
4368            ],
4369            groups: vec![AssembledGroup {
4370                group_id: "SG4".to_string(),
4371                repetitions: vec![AssembledGroupInstance {
4372                    segments: vec![AssembledSegment {
4373                        tag: "IDE".to_string(),
4374                        elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
4375                        mig_number: None,
4376                        segment_number: None,
4377                    }],
4378                    child_groups: vec![AssembledGroup {
4379                        group_id: "SG5".to_string(),
4380                        repetitions: vec![AssembledGroupInstance {
4381                            segments: vec![AssembledSegment {
4382                                tag: "LOC".to_string(),
4383                                elements: vec![
4384                                    vec!["Z16".to_string()],
4385                                    vec!["DE000111222333".to_string()],
4386                                ],
4387                                mig_number: None,
4388                                segment_number: None,
4389                            }],
4390                            child_groups: vec![],
4391                            entry_mig_number: None,
4392                            variant_mig_numbers: vec![],
4393                            skipped_segments: vec![],
4394                            skipped_positions: Vec::new(),
4395                        }],
4396                    }],
4397                    entry_mig_number: None,
4398                    variant_mig_numbers: vec![],
4399                    skipped_segments: vec![],
4400                    skipped_positions: Vec::new(),
4401                }],
4402            }],
4403            post_group_start: 2,
4404            inter_group_segments: std::collections::BTreeMap::new(),
4405        };
4406
4407        // Transaction-level definitions: prozessdaten (root of SG4) + marktlokation (SG5)
4408        let mut proz_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4409        proz_fields.insert(
4410            "ide.1".to_string(),
4411            FieldMapping::Simple("vorgangId".to_string()),
4412        );
4413        let proz_def = MappingDefinition {
4414            meta: MappingMeta {
4415                entity: "Prozessdaten".to_string(),
4416                bo4e_type: "Prozessdaten".to_string(),
4417                source_group: "".to_string(), // Root-level within transaction sub-tree
4418                source_path: None,
4419                discriminator: None,
4420                repeat_on_tag: None,
4421            },
4422            fields: proz_fields,
4423            complex_handlers: None,
4424        };
4425
4426        let mut malo_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4427        malo_fields.insert(
4428            "loc.1".to_string(),
4429            FieldMapping::Simple("marktlokationsId".to_string()),
4430        );
4431        let malo_def = MappingDefinition {
4432            meta: MappingMeta {
4433                entity: "Marktlokation".to_string(),
4434                bo4e_type: "Marktlokation".to_string(),
4435                source_group: "SG5".to_string(), // Relative to SG4, not "SG4.SG5"
4436                source_path: None,
4437                discriminator: None,
4438                repeat_on_tag: None,
4439            },
4440            fields: malo_fields,
4441            complex_handlers: None,
4442        };
4443
4444        let tx_engine = MappingEngine::from_definitions(vec![proz_def, malo_def]);
4445
4446        // Scope to the SG4 instance and map
4447        let sg4 = &tree.groups[0]; // SG4 group
4448        let sg4_instance = &sg4.repetitions[0];
4449        let sub_tree = sg4_instance.as_assembled_tree();
4450
4451        let result = tx_engine.map_all_forward(&sub_tree);
4452
4453        // Should contain Prozessdaten from SG4 root segments
4454        assert_eq!(
4455            result["prozessdaten"]["vorgangId"].as_str().unwrap(),
4456            "TX001"
4457        );
4458
4459        // Should contain Marktlokation from SG5 within SG4
4460        assert_eq!(
4461            result["marktlokation"]["marktlokationsId"]
4462                .as_str()
4463                .unwrap(),
4464            "DE000111222333"
4465        );
4466    }
4467
4468    #[test]
4469    fn test_map_interchange_produces_full_hierarchy() {
4470        use mig_assembly::assembler::*;
4471
4472        // Build a tree with SG2 (message-level) and SG4 with two repetitions (two transactions)
4473        let tree = AssembledTree {
4474            segments: vec![
4475                AssembledSegment {
4476                    tag: "UNH".to_string(),
4477                    elements: vec![vec!["001".to_string()]],
4478                    mig_number: None,
4479                    segment_number: None,
4480                },
4481                AssembledSegment {
4482                    tag: "BGM".to_string(),
4483                    elements: vec![vec!["E01".to_string()]],
4484                    mig_number: None,
4485                    segment_number: None,
4486                },
4487            ],
4488            groups: vec![
4489                AssembledGroup {
4490                    group_id: "SG2".to_string(),
4491                    repetitions: vec![AssembledGroupInstance {
4492                        segments: vec![AssembledSegment {
4493                            tag: "NAD".to_string(),
4494                            elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
4495                            mig_number: None,
4496                            segment_number: None,
4497                        }],
4498                        child_groups: vec![],
4499                        entry_mig_number: None,
4500                        variant_mig_numbers: vec![],
4501                        skipped_segments: vec![],
4502                        skipped_positions: Vec::new(),
4503                    }],
4504                },
4505                AssembledGroup {
4506                    group_id: "SG4".to_string(),
4507                    repetitions: vec![
4508                        AssembledGroupInstance {
4509                            segments: vec![AssembledSegment {
4510                                tag: "IDE".to_string(),
4511                                elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
4512                                mig_number: None,
4513                                segment_number: None,
4514                            }],
4515                            child_groups: vec![],
4516                            entry_mig_number: None,
4517                            variant_mig_numbers: vec![],
4518                            skipped_segments: vec![],
4519                            skipped_positions: Vec::new(),
4520                        },
4521                        AssembledGroupInstance {
4522                            segments: vec![AssembledSegment {
4523                                tag: "IDE".to_string(),
4524                                elements: vec![vec!["24".to_string()], vec!["TX002".to_string()]],
4525                                mig_number: None,
4526                                segment_number: None,
4527                            }],
4528                            child_groups: vec![],
4529                            entry_mig_number: None,
4530                            variant_mig_numbers: vec![],
4531                            skipped_segments: vec![],
4532                            skipped_positions: Vec::new(),
4533                        },
4534                    ],
4535                },
4536            ],
4537            post_group_start: 2,
4538            inter_group_segments: std::collections::BTreeMap::new(),
4539        };
4540
4541        // Message-level definitions
4542        let mut msg_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4543        msg_fields.insert(
4544            "nad.0".to_string(),
4545            FieldMapping::Simple("marktrolle".to_string()),
4546        );
4547        let msg_defs = vec![MappingDefinition {
4548            meta: MappingMeta {
4549                entity: "Marktteilnehmer".to_string(),
4550                bo4e_type: "Marktteilnehmer".to_string(),
4551                source_group: "SG2".to_string(),
4552                source_path: None,
4553                discriminator: None,
4554                repeat_on_tag: None,
4555            },
4556            fields: msg_fields,
4557            complex_handlers: None,
4558        }];
4559
4560        // Transaction-level definitions (source_group includes SG4 prefix)
4561        let mut tx_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4562        tx_fields.insert(
4563            "ide.1".to_string(),
4564            FieldMapping::Simple("vorgangId".to_string()),
4565        );
4566        let tx_defs = vec![MappingDefinition {
4567            meta: MappingMeta {
4568                entity: "Prozessdaten".to_string(),
4569                bo4e_type: "Prozessdaten".to_string(),
4570                source_group: "SG4".to_string(),
4571                source_path: None,
4572                discriminator: None,
4573                repeat_on_tag: None,
4574            },
4575            fields: tx_fields,
4576            complex_handlers: None,
4577        }];
4578
4579        let msg_engine = MappingEngine::from_definitions(msg_defs);
4580        let tx_engine = MappingEngine::from_definitions(tx_defs);
4581
4582        let result = MappingEngine::map_interchange(&msg_engine, &tx_engine, &tree, "SG4", true);
4583
4584        // Message-level stammdaten
4585        assert!(result.stammdaten["marktteilnehmer"].is_object());
4586        assert_eq!(
4587            result.stammdaten["marktteilnehmer"]["marktrolle"]
4588                .as_str()
4589                .unwrap(),
4590            "MS"
4591        );
4592
4593        // Two transactions
4594        assert_eq!(result.transaktionen.len(), 2);
4595        assert_eq!(
4596            result.transaktionen[0].stammdaten["prozessdaten"]["vorgangId"]
4597                .as_str()
4598                .unwrap(),
4599            "TX001"
4600        );
4601        assert_eq!(
4602            result.transaktionen[1].stammdaten["prozessdaten"]["vorgangId"]
4603                .as_str()
4604                .unwrap(),
4605            "TX002"
4606        );
4607    }
4608
4609    #[test]
4610    fn test_map_reverse_with_segment_structure_pads_trailing() {
4611        // STS+7++E01 — position 0 and 2 populated, MIG says 5 elements
4612        let mut fields = IndexMap::new();
4613        fields.insert(
4614            "sts.0".to_string(),
4615            FieldMapping::Structured(StructuredFieldMapping {
4616                target: String::new(),
4617                transform: None,
4618                when: None,
4619                default: Some("7".to_string()),
4620                enum_map: None,
4621                when_filled: None,
4622                also_target: None,
4623                also_enum_map: None,
4624            }),
4625        );
4626        fields.insert(
4627            "sts.2".to_string(),
4628            FieldMapping::Simple("grund".to_string()),
4629        );
4630
4631        let def = make_def(fields);
4632
4633        // Build a SegmentStructure manually via HashMap
4634        let mut counts = std::collections::HashMap::new();
4635        counts.insert("STS".to_string(), 5usize);
4636        let ss = SegmentStructure {
4637            element_counts: counts,
4638        };
4639
4640        let engine = MappingEngine::from_definitions(vec![]).with_segment_structure(ss);
4641
4642        let bo4e = serde_json::json!({ "grund": "E01" });
4643
4644        let instance = engine.map_reverse(&bo4e, &def);
4645        let sts = &instance.segments[0];
4646        // Should have 5 elements: pos 0 = ["7"], pos 1 = [""] (intermediate pad),
4647        // pos 2 = ["E01"], pos 3 = [""] (trailing pad), pos 4 = [""] (trailing pad)
4648        assert_eq!(sts.elements.len(), 5);
4649        assert_eq!(sts.elements[0], vec!["7"]);
4650        assert_eq!(sts.elements[1], vec![""]);
4651        assert_eq!(sts.elements[2], vec!["E01"]);
4652        assert_eq!(sts.elements[3], vec![""]);
4653        assert_eq!(sts.elements[4], vec![""]);
4654    }
4655
4656    #[test]
4657    fn test_resolve_child_relative_with_source_path() {
4658        let mut map: std::collections::HashMap<String, Vec<usize>> =
4659            std::collections::HashMap::new();
4660        map.insert("sg4.sg8_ze1".to_string(), vec![6]);
4661        map.insert("sg4.sg8_z98".to_string(), vec![0]);
4662
4663        // Child without explicit index → resolved from source_path
4664        assert_eq!(
4665            resolve_child_relative("SG8.SG10", Some("sg4.sg8_ze1.sg10"), &map, 0),
4666            "SG8:6.SG10"
4667        );
4668
4669        // Child with explicit index → kept as-is
4670        assert_eq!(
4671            resolve_child_relative("SG8:3.SG10", Some("sg4.sg8_ze1.sg10"), &map, 0),
4672            "SG8:3.SG10"
4673        );
4674
4675        // Source path not in map → kept as-is
4676        assert_eq!(
4677            resolve_child_relative("SG8.SG10", Some("sg4.sg8_unknown.sg10"), &map, 0),
4678            "SG8.SG10"
4679        );
4680
4681        // No source_path → kept as-is
4682        assert_eq!(
4683            resolve_child_relative("SG8.SG10", None, &map, 0),
4684            "SG8.SG10"
4685        );
4686
4687        // SG9 also works
4688        assert_eq!(
4689            resolve_child_relative("SG8.SG9", Some("sg4.sg8_z98.sg9"), &map, 0),
4690            "SG8:0.SG9"
4691        );
4692
4693        // Multi-rep parent: item_idx selects the correct parent rep
4694        map.insert("sg4.sg8_zf3".to_string(), vec![3, 4]);
4695        assert_eq!(
4696            resolve_child_relative("SG8.SG10", Some("sg4.sg8_zf3.sg10"), &map, 0),
4697            "SG8:3.SG10"
4698        );
4699        assert_eq!(
4700            resolve_child_relative("SG8.SG10", Some("sg4.sg8_zf3.sg10"), &map, 1),
4701            "SG8:4.SG10"
4702        );
4703    }
4704
4705    #[test]
4706    fn test_place_in_groups_returns_rep_index() {
4707        let mut groups: Vec<AssembledGroup> = Vec::new();
4708
4709        // Append (no index) → returns position 0
4710        let instance = AssembledGroupInstance {
4711            segments: vec![],
4712            child_groups: vec![],
4713            entry_mig_number: None,
4714            variant_mig_numbers: vec![],
4715            skipped_segments: vec![],
4716            skipped_positions: Vec::new(),
4717        };
4718        assert_eq!(place_in_groups(&mut groups, "SG8", instance), 0);
4719
4720        // Append again → returns position 1
4721        let instance = AssembledGroupInstance {
4722            segments: vec![],
4723            child_groups: vec![],
4724            entry_mig_number: None,
4725            variant_mig_numbers: vec![],
4726            skipped_segments: vec![],
4727            skipped_positions: Vec::new(),
4728        };
4729        assert_eq!(place_in_groups(&mut groups, "SG8", instance), 1);
4730
4731        // Explicit index → returns that index
4732        let instance = AssembledGroupInstance {
4733            segments: vec![],
4734            child_groups: vec![],
4735            entry_mig_number: None,
4736            variant_mig_numbers: vec![],
4737            skipped_segments: vec![],
4738            skipped_positions: Vec::new(),
4739        };
4740        assert_eq!(place_in_groups(&mut groups, "SG8:5", instance), 5);
4741    }
4742
4743    #[test]
4744    fn test_resolve_by_source_path() {
4745        use mig_assembly::assembler::*;
4746
4747        // Build a tree: SG4[0] → SG8 with two reps (Z98 and ZD7) → each has SG10
4748        let tree = AssembledTree {
4749            segments: vec![],
4750            groups: vec![AssembledGroup {
4751                group_id: "SG4".to_string(),
4752                repetitions: vec![AssembledGroupInstance {
4753                    segments: vec![],
4754                    child_groups: vec![AssembledGroup {
4755                        group_id: "SG8".to_string(),
4756                        repetitions: vec![
4757                            AssembledGroupInstance {
4758                                segments: vec![AssembledSegment {
4759                                    tag: "SEQ".to_string(),
4760                                    elements: vec![vec!["Z98".to_string()]],
4761                                    mig_number: None,
4762                                    segment_number: None,
4763                                }],
4764                                child_groups: vec![AssembledGroup {
4765                                    group_id: "SG10".to_string(),
4766                                    repetitions: vec![AssembledGroupInstance {
4767                                        segments: vec![AssembledSegment {
4768                                            tag: "CCI".to_string(),
4769                                            elements: vec![vec![], vec![], vec!["ZB3".to_string()]],
4770                                            mig_number: None,
4771                                            segment_number: None,
4772                                        }],
4773                                        child_groups: vec![],
4774                                        entry_mig_number: None,
4775                                        variant_mig_numbers: vec![],
4776                                        skipped_segments: vec![],
4777                                        skipped_positions: Vec::new(),
4778                                    }],
4779                                }],
4780                                entry_mig_number: None,
4781                                variant_mig_numbers: vec![],
4782                                skipped_segments: vec![],
4783                                skipped_positions: Vec::new(),
4784                            },
4785                            AssembledGroupInstance {
4786                                segments: vec![AssembledSegment {
4787                                    tag: "SEQ".to_string(),
4788                                    elements: vec![vec!["ZD7".to_string()]],
4789                                    mig_number: None,
4790                                    segment_number: None,
4791                                }],
4792                                child_groups: vec![AssembledGroup {
4793                                    group_id: "SG10".to_string(),
4794                                    repetitions: vec![AssembledGroupInstance {
4795                                        segments: vec![AssembledSegment {
4796                                            tag: "CCI".to_string(),
4797                                            elements: vec![vec![], vec![], vec!["ZE6".to_string()]],
4798                                            mig_number: None,
4799                                            segment_number: None,
4800                                        }],
4801                                        child_groups: vec![],
4802                                        entry_mig_number: None,
4803                                        variant_mig_numbers: vec![],
4804                                        skipped_segments: vec![],
4805                                        skipped_positions: Vec::new(),
4806                                    }],
4807                                }],
4808                                entry_mig_number: None,
4809                                variant_mig_numbers: vec![],
4810                                skipped_segments: vec![],
4811                                skipped_positions: Vec::new(),
4812                            },
4813                        ],
4814                    }],
4815                    entry_mig_number: None,
4816                    variant_mig_numbers: vec![],
4817                    skipped_segments: vec![],
4818                    skipped_positions: Vec::new(),
4819                }],
4820            }],
4821            post_group_start: 0,
4822            inter_group_segments: std::collections::BTreeMap::new(),
4823        };
4824
4825        // Resolve SG10 under Z98
4826        let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_z98.sg10");
4827        assert!(inst.is_some());
4828        assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZB3");
4829
4830        // Resolve SG10 under ZD7
4831        let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_zd7.sg10");
4832        assert!(inst.is_some());
4833        assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZE6");
4834
4835        // Unknown qualifier → None
4836        let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_zzz.sg10");
4837        assert!(inst.is_none());
4838
4839        // Without qualifier → first rep (Z98)
4840        let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8.sg10");
4841        assert!(inst.is_some());
4842        assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZB3");
4843    }
4844
4845    #[test]
4846    fn test_parse_source_path_part() {
4847        assert_eq!(parse_source_path_part("sg4"), ("sg4", None));
4848        assert_eq!(parse_source_path_part("sg8_z98"), ("sg8", Some("z98")));
4849        assert_eq!(parse_source_path_part("sg10"), ("sg10", None));
4850        assert_eq!(parse_source_path_part("sg12_z04"), ("sg12", Some("z04")));
4851    }
4852
4853    #[test]
4854    fn test_has_source_path_qualifiers() {
4855        assert!(has_source_path_qualifiers("sg4.sg8_z98.sg10"));
4856        assert!(has_source_path_qualifiers("sg4.sg8_ze1.sg9"));
4857        assert!(!has_source_path_qualifiers("sg4.sg6"));
4858        assert!(!has_source_path_qualifiers("sg4.sg8.sg10"));
4859    }
4860
4861    #[test]
4862    fn test_extract_all_from_instance_collects_all_qualifier_matches() {
4863        use mig_assembly::assembler::*;
4864
4865        // Instance with 3 RFF+Z34 segments
4866        let instance = AssembledGroupInstance {
4867            segments: vec![
4868                AssembledSegment {
4869                    tag: "SEQ".to_string(),
4870                    elements: vec![vec!["ZD6".to_string()]],
4871                    mig_number: None,
4872                    segment_number: None,
4873                },
4874                AssembledSegment {
4875                    tag: "RFF".to_string(),
4876                    elements: vec![vec!["Z34".to_string(), "REF_A".to_string()]],
4877                    mig_number: None,
4878                    segment_number: None,
4879                },
4880                AssembledSegment {
4881                    tag: "RFF".to_string(),
4882                    elements: vec![vec!["Z34".to_string(), "REF_B".to_string()]],
4883                    mig_number: None,
4884                    segment_number: None,
4885                },
4886                AssembledSegment {
4887                    tag: "RFF".to_string(),
4888                    elements: vec![vec!["Z34".to_string(), "REF_C".to_string()]],
4889                    mig_number: None,
4890                    segment_number: None,
4891                },
4892                AssembledSegment {
4893                    tag: "RFF".to_string(),
4894                    elements: vec![vec!["Z35".to_string(), "OTHER".to_string()]],
4895                    mig_number: None,
4896                    segment_number: None,
4897                },
4898            ],
4899            child_groups: vec![],
4900            entry_mig_number: None,
4901            variant_mig_numbers: vec![],
4902            skipped_segments: vec![],
4903            skipped_positions: Vec::new(),
4904        };
4905
4906        // Wildcard collect: rff[Z34,*] should collect all 3 RFF+Z34 values
4907        let all = MappingEngine::extract_all_from_instance(&instance, "rff[Z34,*].0.1");
4908        assert_eq!(all, vec!["REF_A", "REF_B", "REF_C"]);
4909
4910        // Non-wildcard still returns single value via extract_from_instance
4911        let single = MappingEngine::extract_from_instance(&instance, "rff[Z34].0.1");
4912        assert_eq!(single, Some("REF_A".to_string()));
4913
4914        let second = MappingEngine::extract_from_instance(&instance, "rff[Z34,1].0.1");
4915        assert_eq!(second, Some("REF_B".to_string()));
4916    }
4917}