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::{BTreeMap, 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    /// The shared code-list tables a definition's `code_list` names. One `Arc`
41    /// per mappings tree, shared by every engine built from it -- a format
42    /// version builds a couple of thousand engines and the tables are the same
43    /// for all of them.
44    code_lists: std::sync::Arc<crate::code_lists::CodeLists>,
45    /// Forward mapping writes each code as it stands on the wire instead of
46    /// through its rule's table (see [`MappingEngine::with_raw_codes`]).
47    raw_codes: bool,
48}
49
50impl MappingEngine {
51    /// Create an empty engine with no definitions (for unit testing).
52    pub fn new_empty() -> Self {
53        Self {
54            definitions: Vec::new(),
55            segment_structure: None,
56            code_lookup: None,
57            transaction_group: None,
58            current_pid: None,
59            raw_codes: false,
60            code_lists: std::sync::Arc::new(crate::code_lists::CodeLists::default()),
61        }
62    }
63
64    /// Load all TOML mapping files from a directory.
65    pub fn load(dir: &Path) -> Result<Self, MappingError> {
66        let mut definitions = Vec::new();
67
68        let mut entries: Vec<_> = std::fs::read_dir(dir)?.filter_map(|e| e.ok()).collect();
69        entries.sort_by_key(|e| e.file_name());
70
71        for entry in entries {
72            let path = entry.path();
73            if path.extension().map(|e| e == "toml").unwrap_or(false) {
74                let content = std::fs::read_to_string(&path)?;
75                let def = MappingDefinition::from_toml_str(&content).map_err(|message| {
76                    MappingError::TomlParse {
77                        file: path.display().to_string(),
78                        message,
79                    }
80                })?;
81                definitions.push(def);
82            }
83        }
84
85        // Emission order comes from `meta.order` where a definition states it,
86        // and from the filename otherwise — which is what every file relies on
87        // today, via the `_30_12_` prefix convention. `sort_by_key` is stable,
88        // so definitions without the key keep their filename order exactly, and
89        // `u32::MAX` puts them after any that opt in.
90        definitions.sort_by_key(|d| d.meta.order.unwrap_or(u32::MAX));
91
92        Ok(Self {
93            definitions,
94            segment_structure: None,
95            code_lookup: None,
96            transaction_group: None,
97            current_pid: None,
98            raw_codes: false,
99            code_lists: crate::code_lists::CodeLists::discover(dir),
100        })
101    }
102
103    /// Load message-level and transaction-level TOML mappings from separate directories.
104    ///
105    /// Returns `(message_engine, transaction_engine)` where:
106    /// - `message_engine` maps SG2/SG3/root-level definitions (shared across PIDs)
107    /// - `transaction_engine` maps SG4+ definitions (PID-specific)
108    pub fn load_split(
109        message_dir: &Path,
110        transaction_dir: &Path,
111    ) -> Result<(Self, Self), MappingError> {
112        let msg_engine = Self::load(message_dir)?;
113        let tx_engine = Self::load(transaction_dir)?;
114        Ok((msg_engine, tx_engine))
115    }
116
117    /// Load TOML mapping files from multiple directories into a single engine.
118    ///
119    /// Useful for combining message-level and transaction-level mappings
120    /// when a single engine with all definitions is needed.
121    pub fn load_merged(dirs: &[&Path]) -> Result<Self, MappingError> {
122        let mut definitions = Vec::new();
123        for dir in dirs {
124            let engine = Self::load(dir)?;
125            definitions.extend(engine.definitions);
126        }
127        Ok(Self {
128            definitions,
129            segment_structure: None,
130            code_lookup: None,
131            transaction_group: None,
132            current_pid: None,
133            raw_codes: false,
134            code_lists: crate::code_lists::CodeLists::discover(
135                dirs.first().copied().unwrap_or(Path::new("")),
136            ),
137        })
138    }
139
140    /// Load transaction-level mappings with common template inheritance.
141    ///
142    /// 1. Loads all `.toml` from `common_dir`
143    /// 2. Filters: keeps only definitions whose `source_path` exists in the PID schema
144    /// 3. Loads all `.toml` from `pid_dir`
145    /// 4. For each PID definition, if a common definition has matching
146    ///    `(source_group, discriminator)`, replaces the common one (file-level replacement)
147    /// 5. Merges both sets: common first, then PID additions
148    pub fn load_with_common(
149        common_dir: &Path,
150        pid_dir: &Path,
151        schema_index: &crate::pid_schema_index::PidSchemaIndex,
152    ) -> Result<Self, MappingError> {
153        let mut common_defs = Self::load(common_dir)?.definitions;
154
155        // Filter common defs by schema — keep only groups that exist in this PID
156        common_defs.retain(|d| {
157            d.meta
158                .source_path
159                .as_deref()
160                .map(|sp| schema_index.has_group(sp))
161                .unwrap_or(true)
162        });
163
164        let pid_defs = Self::load(pid_dir)?.definitions;
165
166        // Build set of PID override keys: (source_group_normalized, discriminator)
167        // Normalizations applied:
168        // 1. Strip positional indices from source_group: "SG4.SG5:1" → "SG4.SG5"
169        // 2. Strip occurrence indices from discriminator: "RFF.c506.d1153=TN#0" → "RFF.c506.d1153=TN"
170        let normalize_sg = |sg: &str| -> String {
171            sg.split('.')
172                .map(|part| part.split(':').next().unwrap_or(part))
173                .collect::<Vec<_>>()
174                .join(".")
175        };
176        let pid_keys: HashSet<(String, Option<String>)> = pid_defs
177            .iter()
178            .flat_map(|d| {
179                let sg = normalize_sg(&d.meta.source_group);
180                let disc = d.meta.discriminator.clone();
181                let mut keys = vec![(sg.clone(), disc.clone())];
182                // If discriminator has occurrence index (#N), also add base form
183                if let Some(ref disc_str) = disc {
184                    if let Some(base) = disc_str.rsplit_once('#') {
185                        if base.1.chars().all(|c| c.is_ascii_digit()) {
186                            keys.push((sg, Some(base.0.to_string())));
187                        }
188                    }
189                }
190                keys
191            })
192            .collect();
193
194        // Remove common defs that are overridden by PID defs
195        common_defs.retain(|d| {
196            let key = (
197                normalize_sg(&d.meta.source_group),
198                d.meta.discriminator.clone(),
199            );
200            !pid_keys.contains(&key)
201        });
202
203        // Combine: common first, then PID
204        let mut definitions = common_defs;
205        definitions.extend(pid_defs);
206
207        Ok(Self {
208            definitions,
209            segment_structure: None,
210            code_lookup: None,
211            transaction_group: None,
212            current_pid: None,
213            raw_codes: false,
214            code_lists: crate::code_lists::CodeLists::discover(pid_dir),
215        })
216    }
217
218    /// Load common definitions only (no per-PID dir), filtered by schema index.
219    ///
220    /// Used for PIDs that have no per-PID directory but can use shared common/ definitions.
221    pub fn load_common_only(
222        common_dir: &Path,
223        schema_index: &crate::pid_schema_index::PidSchemaIndex,
224    ) -> Result<Self, MappingError> {
225        let mut common_defs = Self::load(common_dir)?.definitions;
226
227        // Filter common defs by schema — keep only groups that exist in this PID
228        common_defs.retain(|d| {
229            d.meta
230                .source_path
231                .as_deref()
232                .map(|sp| schema_index.has_group(sp))
233                .unwrap_or(true)
234        });
235
236        Ok(Self {
237            definitions: common_defs,
238            segment_structure: None,
239            code_lookup: None,
240            transaction_group: None,
241            current_pid: None,
242            raw_codes: false,
243            code_lists: crate::code_lists::CodeLists::discover(common_dir),
244        })
245    }
246
247    /// Load message + transaction engines with common template inheritance.
248    ///
249    /// Returns `(message_engine, transaction_engine)` where the transaction engine
250    /// inherits shared templates from `common_dir`, filtered by the PID schema.
251    pub fn load_split_with_common(
252        message_dir: &Path,
253        common_dir: &Path,
254        transaction_dir: &Path,
255        schema_index: &crate::pid_schema_index::PidSchemaIndex,
256    ) -> Result<(Self, Self), MappingError> {
257        let msg_engine = Self::load(message_dir)?;
258        let tx_engine = Self::load_with_common(common_dir, transaction_dir, schema_index)?;
259        Ok((msg_engine, tx_engine))
260    }
261
262    /// Create an engine from an already-parsed list of definitions.
263    /// Whether any definition names a shared code list.
264    fn names_a_code_list(definitions: &[MappingDefinition]) -> bool {
265        definitions.iter().any(|d| {
266            d.fields.values().any(|f| {
267                matches!(f, FieldMapping::Structured(s)
268                    if s.code_list.is_some() || s.also_code_list.is_some())
269            })
270        })
271    }
272
273    /// The table a structured mapping translates through: its own inline
274    /// `enum_map`, or the shared list its `code_list` names.
275    ///
276    /// Returning a reference rather than resolving at load time is what keeps
277    /// the tables out of the compiled cache: a definition serialises the name,
278    /// not 94 entries, in each of the files that use it.
279    fn table<'a>(
280        &'a self,
281        inline: Option<&'a BTreeMap<String, String>>,
282        named: Option<&str>,
283    ) -> Option<&'a BTreeMap<String, String>> {
284        self.code_lists.resolve(inline, named)
285    }
286
287    /// Attach shared code lists to an engine built from cached definitions.
288    pub fn with_code_lists(
289        mut self,
290        code_lists: std::sync::Arc<crate::code_lists::CodeLists>,
291    ) -> Self {
292        self.code_lists = code_lists;
293        self
294    }
295
296    /// The shared tables this engine resolves `code_list` names against.
297    pub fn code_lists(&self) -> &std::sync::Arc<crate::code_lists::CodeLists> {
298        &self.code_lists
299    }
300
301    /// Build from cached definitions, with the shared tables their `code_list`
302    /// names resolve against.
303    pub fn from_definitions_with_code_lists(
304        code_lists: std::sync::Arc<crate::code_lists::CodeLists>,
305        definitions: Vec<MappingDefinition>,
306    ) -> Self {
307        // The assertion belongs here too, not only in `from_definitions`:
308        // `DataBundle::load` called *this* constructor with an empty `Arc` for
309        // months of work, so checking only the other one meant the check could
310        // not see the one path that actually shipped.
311        debug_assert!(
312            !(code_lists.is_empty() && Self::names_a_code_list(&definitions)),
313            "definitions name a shared code list but the supplied tables are \
314             empty — whatever produced them (a bundle, a cache) is not carrying \
315             them, and every code they translate will reach the output raw"
316        );
317        // Built directly rather than through `from_definitions`, whose debug
318        // assertion is precisely "nobody supplied the tables" -- routing the
319        // correct call through it would fire on every translated definition.
320        Self {
321            definitions,
322            segment_structure: None,
323            code_lookup: None,
324            transaction_group: None,
325            current_pid: None,
326            raw_codes: false,
327            code_lists,
328        }
329    }
330
331    pub fn from_definitions(definitions: Vec<MappingDefinition>) -> Self {
332        // A definition that names a code list is useless without the tables:
333        // the name resolves to nothing and the EDIFACT code reaches the output
334        // raw, which reads as "the guide lists no codes here" rather than as
335        // the wiring mistake it is. It has happened twice -- once in the API,
336        // once in the test harness -- so say so where it happens instead of
337        // letting a wrong value travel.
338        debug_assert!(
339            !Self::names_a_code_list(&definitions),
340            "definitions name a shared code list but none were supplied — build \
341             this engine with `from_definitions_with_code_lists`, or the codes \
342             they translate will reach the output untranslated"
343        );
344        Self {
345            definitions,
346            segment_structure: None,
347            code_lookup: None,
348            transaction_group: None,
349            current_pid: None,
350            raw_codes: false,
351            code_lists: std::sync::Arc::new(crate::code_lists::CodeLists::default()),
352        }
353    }
354
355    /// Save definitions to a cache file.
356    ///
357    /// Only the `definitions` are serialized — `segment_structure` and `code_lookup`
358    /// must be re-attached after loading from cache. Paths in the definitions are
359    /// already resolved to numeric indices, so no `PathResolver` is needed at load time.
360    pub fn save_cached(&self, path: &Path) -> Result<(), MappingError> {
361        let encoded =
362            serde_json::to_vec(&self.definitions).map_err(|e| MappingError::CacheWrite {
363                path: path.display().to_string(),
364                message: e.to_string(),
365            })?;
366        if let Some(parent) = path.parent() {
367            std::fs::create_dir_all(parent)?;
368        }
369        std::fs::write(path, encoded)?;
370        Ok(())
371    }
372
373    /// Load from cache if available, otherwise fall back to TOML directory.
374    ///
375    /// When loading from cache, PathResolver is NOT needed (paths pre-resolved).
376    /// When falling back to TOML, the caller should chain `.with_path_resolver()`.
377    pub fn load_cached_or_toml(cache_path: &Path, toml_dir: &Path) -> Result<Self, MappingError> {
378        if cache_path.exists() {
379            Self::load_cached(cache_path)
380        } else {
381            Self::load(toml_dir)
382        }
383    }
384
385    /// Load definitions from a cache file.
386    ///
387    /// Returns an engine with only `definitions` populated. Attach `segment_structure`
388    /// and `code_lookup` via the builder methods if needed.
389    pub fn load_cached(path: &Path) -> Result<Self, MappingError> {
390        let bytes = std::fs::read(path)?;
391        let definitions: Vec<MappingDefinition> =
392            serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
393                path: path.display().to_string(),
394                message: e.to_string(),
395            })?;
396        Ok(Self {
397            definitions,
398            segment_structure: None,
399            code_lookup: None,
400            transaction_group: None,
401            current_pid: None,
402            raw_codes: false,
403            code_lists: crate::code_lists::CodeLists::discover(path),
404        })
405    }
406
407    /// Attach a MIG-derived segment structure for trailing element padding.
408    ///
409    /// When set, `map_reverse` pads each segment's elements up to the
410    /// MIG-defined count, ensuring trailing empty elements are preserved.
411    pub fn with_segment_structure(mut self, ss: SegmentStructure) -> Self {
412        self.segment_structure = Some(ss);
413        self
414    }
415
416    /// Attach a code lookup for enriching code-type field values.
417    ///
418    /// When set, fields that map to code-type elements in the PID schema
419    /// are emitted as `{"code": "Z15", "meaning": "Ja"}` objects instead of plain strings.
420    pub fn with_code_lookup(mut self, cl: crate::code_lookup::CodeLookup) -> Self {
421        self.code_lookup = Some(cl);
422        self
423    }
424
425    /// Declare which PID this engine is currently processing.
426    ///
427    /// When combined with [`with_code_lookup`](Self::with_code_lookup), code
428    /// fields whose only allowed value equals the PID itself (Class C in the
429    /// 2026-04-28 audit — e.g., RFF+Z13's d1154 in PID 55002 enumerates only
430    /// `55002`) are emitted as plain strings instead of being decorated with
431    /// `{code, meaning, enum}` and a dedup-suffixed enum name.
432    pub fn with_pid(mut self, pid: impl Into<String>) -> Self {
433        self.current_pid = Some(pid.into());
434        self
435    }
436
437    /// Write codes as they stand on the wire in the forward direction.
438    ///
439    /// By default a code with a table (`enum_map`, `code_list`) is written as
440    /// its name — `NAD+Z65` as `"partnerrolle": "kundeDesLf"` — and an
441    /// `also_target` field receives the second value the code carries. Names
442    /// belong to the release that wrote them; codes do not. With raw codes each
443    /// element is written once, as its code, and no `also_target` field is
444    /// derived; enrichment still adds the `meaning`. The reverse direction
445    /// accepts raw codes, so the output renders the same message.
446    pub fn with_raw_codes(mut self, raw: bool) -> Self {
447        self.raw_codes = raw;
448        self
449    }
450
451    /// Attach a path resolver to normalize EDIFACT ID paths to numeric indices.
452    ///
453    /// This allows TOML mapping files to use named paths like `loc.c517.d3225`
454    /// instead of numeric indices like `loc.1.0`. Resolution happens once at
455    /// load time — the engine hot path is completely unchanged.
456    pub fn with_path_resolver(mut self, resolver: crate::path_resolver::PathResolver) -> Self {
457        for def in &mut self.definitions {
458            def.normalize_paths(&resolver);
459        }
460        self
461    }
462
463    /// Declare the transaction-root SG id (e.g. `"SG4"` for UTILMD).
464    ///
465    /// When set, entities whose parent group equals this id are not nested
466    /// into their parent in the forward-mapped JSON. See the
467    /// [`transaction_group`](Self#structfield.transaction_group-1) field doc
468    /// on `MappingEngine` for the full rationale.
469    pub fn with_transaction_group(mut self, tx: impl Into<String>) -> Self {
470        self.transaction_group = Some(tx.into());
471        self
472    }
473
474    /// Add definitions to an already-built engine, keeping everything else it
475    /// carries (code lookup, segment structure, PID, transaction group).
476    ///
477    /// Used to widen a message-level engine into one flat engine over a whole
478    /// variant — what APERAK and CONTRL are converted with, since they have no
479    /// message/transaction split in the v2 `convert` route. A definition whose
480    /// `(entity, source_group, source_path, discriminator, parent_field)` the
481    /// engine already has is skipped, so the same rule reached through two
482    /// PIDs is added once.
483    ///
484    /// The definitions are taken as they are; run them through
485    /// [`with_path_resolver`](Self::with_path_resolver) first if their paths
486    /// are still named.
487    pub fn extend_definitions(mut self, defs: impl IntoIterator<Item = MappingDefinition>) -> Self {
488        fn key(d: &MappingDefinition) -> (String, String, String, String, String) {
489            (
490                d.meta.entity.clone(),
491                d.meta.source_group.clone(),
492                d.meta.source_path.clone().unwrap_or_default(),
493                d.meta.discriminator.clone().unwrap_or_default(),
494                d.meta.parent_field.clone().unwrap_or_default(),
495            )
496        }
497        let mut seen: std::collections::HashSet<_> = self.definitions.iter().map(key).collect();
498        for def in defs {
499            if seen.insert(key(&def)) {
500                self.definitions.push(def);
501            }
502        }
503        self
504    }
505
506    /// Get all loaded definitions.
507    pub fn definitions(&self) -> &[MappingDefinition] {
508        &self.definitions
509    }
510
511    /// Find a definition by entity name.
512    /// The entity's own definition. `parent_field` children (which carry their
513    /// parent's entity name and are mapped inside the parent's instance) are
514    /// skipped — they are not a definition *of* the entity.
515    pub fn definition_for_entity(&self, entity: &str) -> Option<&MappingDefinition> {
516        self.definitions
517            .iter()
518            .find(|d| d.meta.entity == entity && d.meta.parent_field.is_none())
519    }
520
521    // ── Forward mapping: tree → BO4E ──
522
523    /// Extract a field value from an assembled tree using a mapping path.
524    ///
525    /// `group_path` supports dotted notation for nested groups (e.g., "SG4.SG5").
526    /// Parent groups default to repetition 0; `repetition` applies to the leaf group.
527    ///
528    /// Path format: "segment.composite.data_element" e.g., "loc.c517.d3225"
529    pub fn extract_field(
530        &self,
531        tree: &AssembledTree,
532        group_path: &str,
533        path: &str,
534        repetition: usize,
535    ) -> Option<String> {
536        let instance = Self::resolve_group_instance(tree, group_path, repetition)?;
537        Self::extract_from_instance(instance, path)
538    }
539
540    /// Navigate a potentially nested group path to find a group instance.
541    ///
542    /// For "SG4.SG5", finds SG4\[0\] then SG5 at the given repetition within it.
543    /// For "SG8", finds SG8 at the given repetition in the top-level groups.
544    ///
545    /// Supports intermediate repetition with colon syntax: "SG4.SG8:1.SG10"
546    /// means SG4\[0\] → SG8\[1\] → SG10\[repetition\]. Without a colon suffix,
547    /// intermediate groups default to repetition 0.
548    pub fn resolve_group_instance<'a>(
549        tree: &'a AssembledTree,
550        group_path: &str,
551        repetition: usize,
552    ) -> Option<&'a AssembledGroupInstance> {
553        let parts: Vec<&str> = group_path.split('.').collect();
554
555        let (first_id, first_rep) = parse_group_spec(parts[0]);
556        let first_group = tree.groups.iter().find(|g| g.group_id == first_id)?;
557
558        if parts.len() == 1 {
559            // Single part — use the explicit rep from spec or the `repetition` param
560            let rep = first_rep.unwrap_or(repetition);
561            return first_group.repetitions.get(rep);
562        }
563
564        // Navigate through groups; intermediate parts default to rep 0
565        // unless explicitly specified via `:N` suffix
566        let mut current_instance = first_group.repetitions.get(first_rep.unwrap_or(0))?;
567
568        for (i, part) in parts[1..].iter().enumerate() {
569            let (group_id, explicit_rep) = parse_group_spec(part);
570            let child_group = current_instance
571                .child_groups
572                .iter()
573                .find(|g| g.group_id == group_id)?;
574
575            if i == parts.len() - 2 {
576                // Last part — use explicit rep, or fall back to `repetition`
577                let rep = explicit_rep.unwrap_or(repetition);
578                return child_group.repetitions.get(rep);
579            }
580            // Intermediate — use explicit rep or 0
581            current_instance = child_group.repetitions.get(explicit_rep.unwrap_or(0))?;
582        }
583
584        None
585    }
586
587    /// Navigate the assembled tree using a source_path with qualifier suffixes.
588    ///
589    /// Source paths like `"sg4.sg8_z98.sg10"` encode qualifiers inline:
590    /// `sg8_z98` means "find the SG8 repetition whose entry segment has qualifier Z98".
591    /// Parts without underscores (e.g., `sg4`, `sg10`) use the first repetition.
592    ///
593    /// Returns `None` if any part of the path can't be resolved.
594    pub fn resolve_by_source_path<'a>(
595        tree: &'a AssembledTree,
596        source_path: &str,
597    ) -> Option<&'a AssembledGroupInstance> {
598        let parts: Vec<&str> = source_path.split('.').collect();
599        if parts.is_empty() {
600            return None;
601        }
602
603        let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
604        let first_group = tree
605            .groups
606            .iter()
607            .find(|g| g.group_id.eq_ignore_ascii_case(first_id))?;
608
609        let mut current_instance = if let Some(q) = first_qualifier {
610            find_rep_by_entry_qualifier(&first_group.repetitions, q)?
611        } else {
612            first_group.repetitions.first()?
613        };
614
615        if parts.len() == 1 {
616            return Some(current_instance);
617        }
618
619        for part in &parts[1..] {
620            let (group_id, qualifier) = parse_source_path_part(part);
621            let child_group = current_instance
622                .child_groups
623                .iter()
624                .find(|g| g.group_id.eq_ignore_ascii_case(group_id))?;
625
626            current_instance = if let Some(q) = qualifier {
627                find_rep_by_entry_qualifier(&child_group.repetitions, q)?
628            } else {
629                child_group.repetitions.first()?
630            };
631        }
632
633        Some(current_instance)
634    }
635
636    /// Resolve ALL matching instances for a source_path, returning a Vec.
637    ///
638    /// Like `resolve_by_source_path` but returns all repetitions matching
639    /// at any level, not just the first.  For example, if there are two SG5
640    /// reps with LOC+Z17, `resolve_all_by_source_path(tree, "sg4.sg5_z17")`
641    /// returns both.  For deeper paths like "sg4.sg8_zf3.sg10", if there are
642    /// two SG8 reps with ZF3, it returns SG10 children from both.
643    pub fn resolve_all_by_source_path<'a>(
644        tree: &'a AssembledTree,
645        source_path: &str,
646    ) -> Vec<&'a AssembledGroupInstance> {
647        let parts: Vec<&str> = source_path.split('.').collect();
648        if parts.is_empty() {
649            return vec![];
650        }
651
652        // First part: match against top-level groups
653        let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
654        let first_group = match tree
655            .groups
656            .iter()
657            .find(|g| g.group_id.eq_ignore_ascii_case(first_id))
658        {
659            Some(g) => g,
660            None => return vec![],
661        };
662
663        let mut current_instances: Vec<&AssembledGroupInstance> = if let Some(q) = first_qualifier {
664            find_all_reps_by_entry_qualifier(&first_group.repetitions, q)
665        } else {
666            first_group.repetitions.iter().collect()
667        };
668
669        // Navigate remaining parts, branching at each level when multiple
670        // instances match a qualifier (e.g., two SG8 reps with ZF3).
671        for part in &parts[1..] {
672            let (group_id, qualifier) = parse_source_path_part(part);
673            let mut next_instances = Vec::new();
674
675            for instance in &current_instances {
676                if let Some(child_group) = instance
677                    .child_groups
678                    .iter()
679                    .find(|g| g.group_id.eq_ignore_ascii_case(group_id))
680                {
681                    if let Some(q) = qualifier {
682                        next_instances.extend(find_all_reps_by_entry_qualifier(
683                            &child_group.repetitions,
684                            q,
685                        ));
686                    } else {
687                        next_instances.extend(child_group.repetitions.iter());
688                    }
689                }
690            }
691
692            current_instances = next_instances;
693        }
694
695        current_instances
696    }
697
698    /// Like `resolve_all_by_source_path` but also returns the direct parent
699    /// rep index that each leaf instance came from. The "direct parent" is the
700    /// group one level above the leaf in the path.
701    ///
702    /// For `"sg2.sg3"`: parent is the SG2 rep index.
703    /// For `"sg17.sg36.sg40"`: parent is the SG36 rep index (not SG17).
704    ///
705    /// For single-level paths, all indices are 0.
706    ///
707    /// Compute child rep indices for the leaf group in a source_path.
708    /// E.g., for "sg29.sg30", returns the position of each matched SG30 rep
709    /// within its parent SG29's SG30 child group.
710    fn compute_child_indices(
711        tree: &AssembledTree,
712        source_path: &str,
713        indexed: &[(usize, &AssembledGroupInstance)],
714    ) -> Vec<usize> {
715        let parts: Vec<&str> = source_path.split('.').collect();
716        if parts.len() < 2 {
717            return vec![];
718        }
719        // Navigate to the parent level and find the child group
720        let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
721        let first_group = match tree
722            .groups
723            .iter()
724            .find(|g| g.group_id.eq_ignore_ascii_case(first_id))
725        {
726            Some(g) => g,
727            None => return vec![],
728        };
729        let parent_reps: Vec<&AssembledGroupInstance> = if let Some(q) = first_qualifier {
730            find_all_reps_by_entry_qualifier(&first_group.repetitions, q)
731        } else {
732            first_group.repetitions.iter().collect()
733        };
734        // For 2-level paths (sg29.sg30), find the child group in the parent
735        let (child_id, _child_qualifier) = parse_source_path_part(parts[parts.len() - 1]);
736        let mut result = Vec::new();
737        for (_, inst) in indexed {
738            // Find which rep index this instance is at in the child group
739            let mut found = false;
740            for parent in &parent_reps {
741                if let Some(child_group) = parent
742                    .child_groups
743                    .iter()
744                    .find(|g| g.group_id.eq_ignore_ascii_case(child_id))
745                {
746                    if let Some(pos) = child_group
747                        .repetitions
748                        .iter()
749                        .position(|r| std::ptr::eq(r, *inst))
750                    {
751                        result.push(pos);
752                        found = true;
753                        break;
754                    }
755                }
756            }
757            if !found {
758                result.push(usize::MAX); // fallback
759            }
760        }
761        result
762    }
763
764    /// Returns `Vec<(parent_rep_index, &AssembledGroupInstance)>`.
765    pub fn resolve_all_with_parent_indices<'a>(
766        tree: &'a AssembledTree,
767        source_path: &str,
768    ) -> Vec<(usize, &'a AssembledGroupInstance)> {
769        let parts: Vec<&str> = source_path.split('.').collect();
770        if parts.is_empty() {
771            return vec![];
772        }
773
774        // First part: match against top-level groups
775        let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
776        let first_group = match tree
777            .groups
778            .iter()
779            .find(|g| g.group_id.eq_ignore_ascii_case(first_id))
780        {
781            Some(g) => g,
782            None => return vec![],
783        };
784
785        // If single-level path, just return instances with index 0
786        if parts.len() == 1 {
787            let instances: Vec<&AssembledGroupInstance> = if let Some(q) = first_qualifier {
788                find_all_reps_by_entry_qualifier(&first_group.repetitions, q)
789            } else {
790                first_group.repetitions.iter().collect()
791            };
792            return instances.into_iter().map(|i| (0, i)).collect();
793        }
794
795        // Multi-level: navigate tracking (parent_rep_idx, instance) at each level.
796        // At intermediate levels, parent_rep_idx is updated to the current rep's
797        // position within its group. At the leaf level, the parent_rep_idx from
798        // the previous level is preserved — giving us the DIRECT parent index.
799        let first_reps: Vec<(usize, &AssembledGroupInstance)> = if let Some(q) = first_qualifier {
800            let matching = find_all_reps_by_entry_qualifier(&first_group.repetitions, q);
801            let mut result = Vec::new();
802            for m in matching {
803                let idx = first_group
804                    .repetitions
805                    .iter()
806                    .position(|r| std::ptr::eq(r, m))
807                    .unwrap_or(0);
808                result.push((idx, m));
809            }
810            result
811        } else {
812            first_group.repetitions.iter().enumerate().collect()
813        };
814
815        let mut current: Vec<(usize, &AssembledGroupInstance)> = first_reps;
816        let remaining = &parts[1..];
817
818        for (level, part) in remaining.iter().enumerate() {
819            let is_leaf = level == remaining.len() - 1;
820            let (group_id, qualifier) = parse_source_path_part(part);
821            let mut next: Vec<(usize, &AssembledGroupInstance)> = Vec::new();
822
823            for (prev_parent_idx, instance) in &current {
824                if let Some(child_group) = instance
825                    .child_groups
826                    .iter()
827                    .find(|g| g.group_id.eq_ignore_ascii_case(group_id))
828                {
829                    let matching: Vec<(usize, &AssembledGroupInstance)> = if let Some(q) = qualifier
830                    {
831                        let filtered =
832                            find_all_reps_by_entry_qualifier(&child_group.repetitions, q);
833                        filtered
834                            .into_iter()
835                            .map(|m| {
836                                let idx = child_group
837                                    .repetitions
838                                    .iter()
839                                    .position(|r| std::ptr::eq(r, m))
840                                    .unwrap_or(0);
841                                (idx, m)
842                            })
843                            .collect()
844                    } else {
845                        child_group.repetitions.iter().enumerate().collect()
846                    };
847
848                    for (rep_idx, child_rep) in matching {
849                        if is_leaf {
850                            // At the leaf: keep the parent index from the previous level
851                            next.push((*prev_parent_idx, child_rep));
852                        } else {
853                            // At intermediate: pass down the current rep index
854                            next.push((rep_idx, child_rep));
855                        }
856                    }
857                }
858            }
859
860            current = next;
861        }
862
863        current
864    }
865
866    /// Extract a field from a group instance by path.
867    ///
868    /// Supports qualifier-based segment selection with `tag[qualifier]` syntax:
869    /// - `"dtm.0.1"` → first DTM segment, elements\[0\]\[1\]
870    /// - `"dtm[92].0.1"` → DTM where elements\[0\]\[0\] == "92", then elements\[0\]\[1\]
871    pub fn extract_from_instance(instance: &AssembledGroupInstance, path: &str) -> Option<String> {
872        let parts: Vec<&str> = path.split('.').collect();
873        if parts.is_empty() {
874            return None;
875        }
876
877        // Parse segment tag, optional qualifier, and occurrence index:
878        // "dtm[92]" → ("DTM", Some("92"), 0), "rff[Z34,1]" → ("RFF", Some("Z34"), 1)
879        let (segment_tag, qualifier, occurrence) = parse_tag_qualifier(parts[0]);
880
881        let segment = if let Some(q) = qualifier {
882            instance
883                .segments
884                .iter()
885                .filter(|s| {
886                    s.tag.eq_ignore_ascii_case(&segment_tag)
887                        && s.elements
888                            .first()
889                            .and_then(|e| e.first())
890                            .map(|v| v.as_str())
891                            == Some(q)
892                })
893                .nth(occurrence)?
894        } else {
895            instance
896                .segments
897                .iter()
898                .filter(|s| s.tag.eq_ignore_ascii_case(&segment_tag))
899                .nth(occurrence)?
900        };
901
902        Self::resolve_field_path(segment, &parts[1..])
903    }
904
905    /// Extract ALL matching values from a group instance for a collect-all path.
906    ///
907    /// Used with wildcard occurrence syntax `tag[qualifier,*]` to collect values
908    /// from every segment matching the qualifier, not just the Nth one.
909    /// Returns a `Vec<String>` of all extracted values in segment order.
910    pub fn extract_all_from_instance(instance: &AssembledGroupInstance, path: &str) -> Vec<String> {
911        let parts: Vec<&str> = path.split('.').collect();
912        if parts.is_empty() {
913            return vec![];
914        }
915
916        let (segment_tag, qualifier, _) = parse_tag_qualifier(parts[0]);
917
918        let matching_segments: Vec<&AssembledSegment> = if let Some(q) = qualifier {
919            instance
920                .segments
921                .iter()
922                .filter(|s| {
923                    s.tag.eq_ignore_ascii_case(&segment_tag)
924                        && s.elements
925                            .first()
926                            .and_then(|e| e.first())
927                            .map(|v| v.as_str())
928                            == Some(q)
929                })
930                .collect()
931        } else {
932            instance
933                .segments
934                .iter()
935                .filter(|s| s.tag.eq_ignore_ascii_case(&segment_tag))
936                .collect()
937        };
938
939        matching_segments
940            .into_iter()
941            .filter_map(|seg| Self::resolve_field_path(seg, &parts[1..]))
942            .collect()
943    }
944
945    /// Map all fields in a definition from the assembled tree to a BO4E JSON object.
946    ///
947    /// `group_path` is the definition's `source_group` (may be dotted, e.g., "SG4.SG5").
948    /// An empty `source_group` maps root-level segments (BGM, DTM, etc.).
949    /// Returns a flat JSON object with target field names as keys.
950    pub fn map_forward(
951        &self,
952        tree: &AssembledTree,
953        def: &MappingDefinition,
954        repetition: usize,
955    ) -> serde_json::Value {
956        self.map_forward_inner(tree, def, repetition, true)
957    }
958
959    /// Inner implementation with enrichment control.
960    fn map_forward_inner(
961        &self,
962        tree: &AssembledTree,
963        def: &MappingDefinition,
964        repetition: usize,
965        enrich_codes: bool,
966    ) -> serde_json::Value {
967        let mut result = serde_json::Map::new();
968
969        // Root-level mapping: source_group is empty → use tree's own segments.
970        // Include all root segments (both pre-group and post-group, e.g., summary
971        // MOA after UNS+S in REMADV) plus any inter_group_segments (e.g., UNS+S
972        // consumed between groups by the assembler).
973        if def.meta.source_group.is_empty() {
974            let mut all_root_segs = tree.segments.clone();
975            for segs in tree.inter_group_segments.values() {
976                all_root_segs.extend(segs.iter().cloned());
977            }
978            let root_instance = AssembledGroupInstance {
979                segments: all_root_segs,
980                child_groups: vec![],
981                entry_mig_number: None,
982                variant_mig_numbers: vec![],
983                skipped_segments: Vec::new(),
984                skipped_positions: Vec::new(),
985            };
986            self.extract_fields_from_instance(&root_instance, def, &mut result, enrich_codes);
987            return serde_json::Value::Object(result);
988        }
989
990        // Try source_path-based resolution when:
991        //   1. source_path has qualifier suffixes (e.g., "sg4.sg8_z98.sg10")
992        //   2. source_group has no explicit :N indices (those take priority)
993        // This allows definitions without positional indices to navigate via
994        // entry-segment qualifiers (e.g., SEQ qualifier Z98).
995        let instance = if let Some(ref sp) = def.meta.source_path {
996            if has_source_path_qualifiers(sp) && !def.meta.source_group.contains(':') {
997                Self::resolve_by_source_path(tree, sp).or_else(|| {
998                    Self::resolve_group_instance(tree, &def.meta.source_group, repetition)
999                })
1000            } else {
1001                Self::resolve_group_instance(tree, &def.meta.source_group, repetition)
1002            }
1003        } else {
1004            Self::resolve_group_instance(tree, &def.meta.source_group, repetition)
1005        };
1006
1007        if let Some(instance) = instance {
1008            // repeat_on_tag: iterate over all segments of that tag, producing an array
1009            if let Some(ref tag) = def.meta.repeat_on_tag {
1010                let matching: Vec<_> = instance
1011                    .segments
1012                    .iter()
1013                    .filter(|s| s.tag.eq_ignore_ascii_case(tag))
1014                    .collect();
1015
1016                if matching.len() > 1 {
1017                    let mut arr = Vec::new();
1018                    for seg in &matching {
1019                        let sub_instance = AssembledGroupInstance {
1020                            segments: vec![(*seg).clone()],
1021                            child_groups: vec![],
1022                            entry_mig_number: None,
1023                            variant_mig_numbers: vec![],
1024                            skipped_segments: Vec::new(),
1025                            skipped_positions: Vec::new(),
1026                        };
1027                        let mut elem_result = serde_json::Map::new();
1028                        self.extract_fields_from_instance(
1029                            &sub_instance,
1030                            def,
1031                            &mut elem_result,
1032                            enrich_codes,
1033                        );
1034                        if !elem_result.is_empty() {
1035                            arr.push(serde_json::Value::Object(elem_result));
1036                        }
1037                    }
1038                    if !arr.is_empty() {
1039                        return serde_json::Value::Array(arr);
1040                    }
1041                }
1042            }
1043
1044            self.extract_fields_from_instance(instance, def, &mut result, enrich_codes);
1045        }
1046
1047        serde_json::Value::Object(result)
1048    }
1049
1050    /// Extract all fields from an instance into a result map.
1051    ///
1052    /// When a `code_lookup` is configured, code-type fields are emitted as
1053    /// `{"code": "E01", "meaning": "..."}` objects. Data-type fields remain plain strings.
1054    fn extract_fields_from_instance(
1055        &self,
1056        instance: &AssembledGroupInstance,
1057        def: &MappingDefinition,
1058        result: &mut serde_json::Map<String, serde_json::Value>,
1059        enrich_codes: bool,
1060    ) {
1061        for (path, field_mapping) in &def.fields {
1062            let (target, enum_map) = match field_mapping {
1063                FieldMapping::Simple(t) => (t.as_str(), None),
1064                FieldMapping::Structured(s) => (
1065                    s.target.as_str(),
1066                    self.table(s.enum_map.as_ref(), s.code_list.as_deref()),
1067                ),
1068                FieldMapping::Nested(_) => continue,
1069            };
1070            if target.is_empty() {
1071                continue;
1072            }
1073            if let Some((list, sub)) = list_target(target) {
1074                self.extract_list_field(
1075                    instance,
1076                    def,
1077                    path,
1078                    list,
1079                    sub,
1080                    enum_map,
1081                    enrich_codes,
1082                    result,
1083                );
1084                continue;
1085            }
1086            if let Some(val) = Self::extract_from_instance(instance, path) {
1087                // Dual decomposition: one EDIFACT code also feeds a second BO4E
1088                // field (e.g. the NAD qualifier carries both partnerrolle and
1089                // datenqualitaet). Without this the code cannot be recovered in
1090                // reverse, because several codes share the primary value.
1091                if let FieldMapping::Structured(s) = field_mapping {
1092                    if let (false, Some(also), Some(also_map)) = (
1093                        self.raw_codes,
1094                        s.also_target.as_deref(),
1095                        self.table(s.also_enum_map.as_ref(), s.also_code_list.as_deref()),
1096                    ) {
1097                        if let Some(also_val) = also_map.get(&val) {
1098                            set_nested_value(result, also, also_val.clone());
1099                        }
1100                    }
1101                }
1102
1103                let mapped_val = match enum_map {
1104                    Some(map) if !self.raw_codes => {
1105                        map.get(&val).cloned().unwrap_or_else(|| val.clone())
1106                    }
1107                    _ => val.clone(),
1108                };
1109
1110                // Enrich code fields with meaning from PID schema
1111                if enrich_codes {
1112                    if let (Some(ref code_lookup), Some(ref source_path)) =
1113                        (&self.code_lookup, &def.meta.source_path)
1114                    {
1115                        let parts: Vec<&str> = path.split('.').collect();
1116                        let (seg_tag, path_qualifier, _occ) = parse_tag_qualifier(parts[0]);
1117                        let (element_idx, component_idx) =
1118                            Self::parse_element_component(&parts[1..]);
1119                        let disc_qualifier = Self::discriminator_qualifier_for_tag(def, &seg_tag);
1120                        let q = disc_qualifier.as_deref();
1121
1122                        if let Some(codes) = code_lookup.enrichment_codes(
1123                            source_path,
1124                            &seg_tag,
1125                            path_qualifier,
1126                            q,
1127                            element_idx,
1128                            component_idx,
1129                        ) {
1130                            // Class C: PID self-reference — emit a plain string,
1131                            // skipping {code, meaning, enum} decoration when the
1132                            // schema's only allowed value at this position is the
1133                            // PID itself.
1134                            if let Some(ref pid) = self.current_pid {
1135                                if codes.len() == 1 && codes.contains_key(pid.as_str()) {
1136                                    set_nested_value(result, target, mapped_val);
1137                                    continue;
1138                                }
1139                            }
1140
1141                            // Look up the original EDIFACT value for enrichment,
1142                            // since schema codes use raw values (e.g., "293")
1143                            // not enum_map targets (e.g., "BDEW").
1144                            let enrichment = codes.get(&val);
1145                            let meaning = enrichment
1146                                .map(|e| serde_json::Value::String(e.meaning.clone()))
1147                                .unwrap_or(serde_json::Value::Null);
1148
1149                            let mut obj = serde_json::Map::new();
1150                            obj.insert("code".into(), serde_json::json!(mapped_val));
1151                            obj.insert("meaning".into(), meaning);
1152                            if let Some(enum_key) = enrichment.and_then(|e| e.enum_key.as_ref()) {
1153                                obj.insert("enum".into(), serde_json::json!(enum_key));
1154                            }
1155                            let enriched = serde_json::Value::Object(obj);
1156                            set_nested_value_json(result, target, enriched);
1157                            continue;
1158                        }
1159                    }
1160                }
1161
1162                set_nested_value(result, target, mapped_val);
1163            }
1164        }
1165
1166        // Also for `parent_field` children: they can be parents of deeper
1167        // `parent_field` definitions (SG15 → SG17 → SG18).
1168        if !instance.child_groups.is_empty() {
1169            self.extract_nested_children(instance, def, result, enrich_codes);
1170        }
1171    }
1172
1173    /// Forward half of a list target (`werte[].code`): one element of the array
1174    /// per segment the path matches — `cav[*,*]` every CAV, `cav[Z30,*]` every
1175    /// CAV+Z30 — in wire order. A positional slot (`cav[*,1]` → `wert2`) names
1176    /// a position, and the SG10 CAVs are identified by their code, not their
1177    /// place: an absent first CAV moved the second into the first's field, and
1178    /// CAVs beyond the last slot were dropped. The list keeps every CAV with
1179    /// its own code.
1180    #[allow(clippy::too_many_arguments)]
1181    fn extract_list_field(
1182        &self,
1183        instance: &AssembledGroupInstance,
1184        def: &MappingDefinition,
1185        path: &str,
1186        list: &str,
1187        sub: &str,
1188        enum_map: Option<&std::collections::BTreeMap<String, String>>,
1189        enrich_codes: bool,
1190        result: &mut serde_json::Map<String, serde_json::Value>,
1191    ) {
1192        let parts: Vec<&str> = path.split('.').collect();
1193        if parts.len() < 2 {
1194            return;
1195        }
1196        let (seg_tag, qualifier, _) = parse_tag_qualifier(parts[0]);
1197        let segments: Vec<&AssembledSegment> = instance
1198            .segments
1199            .iter()
1200            .filter(|s| {
1201                s.tag.eq_ignore_ascii_case(&seg_tag)
1202                    && qualifier.map_or(true, |q| {
1203                        s.elements
1204                            .first()
1205                            .and_then(|e| e.first())
1206                            .map(|v| v.as_str())
1207                            == Some(q)
1208                    })
1209            })
1210            .collect();
1211        if segments.is_empty() {
1212            return;
1213        }
1214        let items = result
1215            .entry(list.to_string())
1216            .or_insert_with(|| serde_json::Value::Array(Vec::new()));
1217        let Some(items) = items.as_array_mut() else {
1218            return;
1219        };
1220        while items.len() < segments.len() {
1221            items.push(serde_json::Value::Object(serde_json::Map::new()));
1222        }
1223        let codes = if enrich_codes {
1224            match (&self.code_lookup, &def.meta.source_path) {
1225                (Some(lookup), Some(source_path)) => {
1226                    let (element_idx, component_idx) = Self::parse_element_component(&parts[1..]);
1227                    let disc = Self::discriminator_qualifier_for_tag(def, &seg_tag);
1228                    lookup
1229                        .enrichment_codes(
1230                            source_path,
1231                            &seg_tag,
1232                            qualifier,
1233                            disc.as_deref(),
1234                            element_idx,
1235                            component_idx,
1236                        )
1237                        .cloned()
1238                }
1239                _ => None,
1240            }
1241        } else {
1242            None
1243        };
1244        for (item, segment) in items.iter_mut().zip(segments) {
1245            let Some(val) = Self::resolve_field_path(segment, &parts[1..]) else {
1246                continue;
1247            };
1248            let mapped = match enum_map {
1249                Some(map) if !self.raw_codes => {
1250                    map.get(&val).cloned().unwrap_or_else(|| val.clone())
1251                }
1252                _ => val.clone(),
1253            };
1254            let value = match &codes {
1255                Some(codes) => {
1256                    let enrichment = codes.get(&val);
1257                    let mut obj = serde_json::Map::new();
1258                    obj.insert("code".into(), serde_json::json!(mapped));
1259                    obj.insert(
1260                        "meaning".into(),
1261                        enrichment
1262                            .map(|e| serde_json::Value::String(e.meaning.clone()))
1263                            .unwrap_or(serde_json::Value::Null),
1264                    );
1265                    if let Some(enum_key) = enrichment.and_then(|e| e.enum_key.as_ref()) {
1266                        obj.insert("enum".into(), serde_json::json!(enum_key));
1267                    }
1268                    serde_json::Value::Object(obj)
1269                }
1270                None => serde_json::Value::String(mapped),
1271            };
1272            if let Some(obj) = item.as_object_mut() {
1273                set_nested_value_json(obj, sub, value);
1274            }
1275        }
1276    }
1277
1278    /// Forward half of `[meta] parent_field`: map the child groups of `instance`
1279    /// (the parent group repetition `def` was just extracted from) into array
1280    /// fields of the same object. Placement is instance-local — a child can only
1281    /// land in the object produced from the group repetition that contains it.
1282    fn extract_nested_children(
1283        &self,
1284        instance: &AssembledGroupInstance,
1285        def: &MappingDefinition,
1286        result: &mut serde_json::Map<String, serde_json::Value>,
1287        enrich_codes: bool,
1288    ) {
1289        for child in self
1290            .definitions
1291            .iter()
1292            .filter(|c| is_nested_child_of(c, def))
1293        {
1294            if nested_parent_qualifier(child).is_some_and(|q| !entry_qualifier_matches(instance, q))
1295            {
1296                continue;
1297            }
1298            let (leaf_id, leaf_qualifier) = nested_child_leaf(child);
1299            let Some(group) = instance
1300                .child_groups
1301                .iter()
1302                .find(|g| g.group_id.eq_ignore_ascii_case(&leaf_id))
1303            else {
1304                continue;
1305            };
1306            let reps: Vec<&AssembledGroupInstance> = match leaf_qualifier {
1307                Some(q) => find_all_reps_by_entry_qualifier(&group.repetitions, q),
1308                None => group.repetitions.iter().collect(),
1309            };
1310
1311            let mut items: Vec<serde_json::Value> = Vec::new();
1312            let mut push_item = |sub: &AssembledGroupInstance| {
1313                let mut obj = serde_json::Map::new();
1314                self.extract_fields_from_instance(sub, child, &mut obj, enrich_codes);
1315                if !obj.is_empty() {
1316                    items.push(serde_json::Value::Object(obj));
1317                }
1318            };
1319            for rep in reps {
1320                let repeat_tag = child
1321                    .meta
1322                    .repeat_on_tag
1323                    .as_deref()
1324                    .filter(|tag| rep.segments.iter().any(|s| s.tag.eq_ignore_ascii_case(tag)));
1325                let Some(tag) = repeat_tag else {
1326                    push_item(rep);
1327                    continue;
1328                };
1329                // One element per repeating segment; the group's other segments
1330                // (e.g. the CTA entry segment) are visible to every element.
1331                let shared: Vec<AssembledSegment> = rep
1332                    .segments
1333                    .iter()
1334                    .filter(|s| !s.tag.eq_ignore_ascii_case(tag))
1335                    .cloned()
1336                    .collect();
1337                for seg in rep
1338                    .segments
1339                    .iter()
1340                    .filter(|s| s.tag.eq_ignore_ascii_case(tag))
1341                {
1342                    let mut segments = shared.clone();
1343                    segments.push(seg.clone());
1344                    push_item(&AssembledGroupInstance {
1345                        segments,
1346                        child_groups: vec![],
1347                        entry_mig_number: None,
1348                        variant_mig_numbers: vec![],
1349                        skipped_segments: Vec::new(),
1350                        skipped_positions: Vec::new(),
1351                    });
1352                }
1353            }
1354            if items.is_empty() {
1355                continue;
1356            }
1357            let field = child.meta.parent_field.as_deref().unwrap_or_default();
1358            match result.get_mut(field) {
1359                Some(serde_json::Value::Array(existing)) => existing.extend(items),
1360                _ => {
1361                    result.insert(field.to_string(), serde_json::Value::Array(items));
1362                }
1363            }
1364        }
1365    }
1366
1367    /// Reverse half of `[meta] parent_field`: emit the elements of
1368    /// `bo4e_value[parent_field]` as child group(s) of `instance`, the parent
1369    /// group repetition just rebuilt from that same object.
1370    fn reverse_nested_children(
1371        &self,
1372        bo4e_value: &serde_json::Value,
1373        def: &MappingDefinition,
1374        instance: &mut AssembledGroupInstance,
1375    ) {
1376        let mut handled_fields: Vec<&str> = Vec::new();
1377        for child in self
1378            .definitions
1379            .iter()
1380            .filter(|c| is_nested_child_of(c, def))
1381        {
1382            let field = child.meta.parent_field.as_deref().unwrap_or_default();
1383            if handled_fields.contains(&field) {
1384                continue;
1385            }
1386            if nested_parent_qualifier(child)
1387                .is_some_and(|q| !rebuilt_entry_qualifier_matches(instance, def, q))
1388            {
1389                continue;
1390            }
1391            let elements: Vec<&serde_json::Value> = match bo4e_value.get(field) {
1392                Some(serde_json::Value::Array(arr)) => arr.iter().collect(),
1393                Some(serde_json::Value::Null) | None => continue,
1394                Some(other) => vec![other],
1395            };
1396
1397            let mut reps: Vec<AssembledGroupInstance> = Vec::new();
1398            if let Some(tag) = child.meta.repeat_on_tag.as_deref() {
1399                // All elements share one group repetition: the non-repeating
1400                // segments (entry segment) once, then one repeating segment each.
1401                let mut merged: Option<AssembledGroupInstance> = None;
1402                for element in elements {
1403                    let sub = self.map_reverse_single(element, child);
1404                    if sub.segments.is_empty() {
1405                        continue;
1406                    }
1407                    match merged.as_mut() {
1408                        None => merged = Some(sub),
1409                        Some(m) => m.segments.extend(
1410                            sub.segments
1411                                .into_iter()
1412                                .filter(|s| s.tag.eq_ignore_ascii_case(tag)),
1413                        ),
1414                    }
1415                }
1416                reps.extend(merged);
1417            } else {
1418                for element in elements {
1419                    let mut sub = self.map_reverse_single(element, child);
1420                    if sub.segments.is_empty() {
1421                        continue;
1422                    }
1423                    // Grandchildren nested in this element (multi-level nesting).
1424                    self.reverse_nested_children(element, child, &mut sub);
1425                    reps.push(sub);
1426                }
1427            }
1428            if reps.is_empty() {
1429                continue;
1430            }
1431            handled_fields.push(field);
1432
1433            let (leaf_id, _) = nested_child_leaf(child);
1434            match instance
1435                .child_groups
1436                .iter_mut()
1437                .find(|g| g.group_id.eq_ignore_ascii_case(&leaf_id))
1438            {
1439                Some(group) => group.repetitions.extend(reps),
1440                None => instance.child_groups.push(AssembledGroup {
1441                    group_id: leaf_id,
1442                    repetitions: reps,
1443                }),
1444            }
1445        }
1446    }
1447
1448    /// Extract the discriminator's qualifier value from a definition's `[meta]`.
1449    ///
1450    /// `discriminator` strings look like `"RFF.0.0=Z13"` (numeric, post path-resolution)
1451    /// or `"RFF.c506.d1153=TN"` (named, pre-resolution). The qualifier is the
1452    /// substring after the first `=`. Returns `None` when no discriminator is set
1453    /// or the format is unexpected.
1454    pub(crate) fn discriminator_qualifier(def: &MappingDefinition) -> Option<String> {
1455        def.meta
1456            .discriminator
1457            .as_deref()
1458            .and_then(|d| d.split_once('=').map(|(_, v)| v.to_string()))
1459    }
1460
1461    /// The discriminator value when the discriminator selects on `segment_tag`
1462    /// itself (`RFF.0.0=Z13` for an RFF field). A discriminator on another segment
1463    /// (`SEQ.0.0=Z98` for a CCI field) says nothing about which variant of
1464    /// `segment_tag` a field reads.
1465    pub(crate) fn discriminator_qualifier_for_tag(
1466        def: &MappingDefinition,
1467        segment_tag: &str,
1468    ) -> Option<String> {
1469        let (lhs, value) = def.meta.discriminator.as_deref()?.split_once('=')?;
1470        let disc_tag = lhs.split('.').next().unwrap_or(lhs);
1471        disc_tag
1472            .eq_ignore_ascii_case(segment_tag)
1473            .then(|| value.to_string())
1474    }
1475
1476    /// Map a PID struct field's segments to BO4E JSON.
1477    ///
1478    /// `segments` are the `OwnedSegment`s from a PID wrapper field.
1479    /// Converts to `AssembledSegment` format for compatibility with existing
1480    /// field extraction logic, then applies the definition's field mappings.
1481    pub fn map_forward_from_segments(
1482        &self,
1483        segments: &[OwnedSegment],
1484        def: &MappingDefinition,
1485    ) -> serde_json::Value {
1486        let assembled_segments: Vec<AssembledSegment> = segments
1487            .iter()
1488            .map(|s| AssembledSegment {
1489                tag: s.id.clone(),
1490                elements: s.elements.clone(),
1491                mig_number: None,
1492                segment_number: Some(s.segment_number),
1493            })
1494            .collect();
1495
1496        let instance = AssembledGroupInstance {
1497            segments: assembled_segments,
1498            child_groups: vec![],
1499            entry_mig_number: None,
1500            variant_mig_numbers: vec![],
1501            skipped_segments: Vec::new(),
1502            skipped_positions: Vec::new(),
1503        };
1504
1505        let mut result = serde_json::Map::new();
1506        self.extract_fields_from_instance(&instance, def, &mut result, true);
1507        serde_json::Value::Object(result)
1508    }
1509
1510    // ── Reverse mapping: BO4E → tree ──
1511
1512    /// Map a BO4E JSON object back to an assembled group instance.
1513    ///
1514    /// Uses the definition's field mappings to populate segment elements.
1515    /// Fields with `default` values are used when no BO4E value is present
1516    /// (useful for fixed qualifiers like LOC qualifier "Z16").
1517    ///
1518    /// Supports:
1519    /// - Named paths: `"d3227"` → element\[0\]\[0\], `"c517.d3225"` → element\[1\]\[0\]
1520    /// - Numeric index: `"0"` → element\[0\]\[0\], `"1.2"` → element\[1\]\[2\]
1521    /// - Qualifier selection: `"dtm[92].0.1"` → DTM segment with qualifier "92"
1522    pub fn map_reverse(
1523        &self,
1524        bo4e_value: &serde_json::Value,
1525        def: &MappingDefinition,
1526    ) -> AssembledGroupInstance {
1527        // repeat_on_tag + array input: reverse each element independently, merge segments
1528        if def.meta.repeat_on_tag.is_some() {
1529            if let Some(arr) = bo4e_value.as_array() {
1530                let mut all_segments = Vec::new();
1531                for elem in arr {
1532                    let sub = self.map_reverse_single(elem, def);
1533                    all_segments.extend(sub.segments);
1534                }
1535                return AssembledGroupInstance {
1536                    segments: all_segments,
1537                    child_groups: vec![],
1538                    entry_mig_number: None,
1539                    variant_mig_numbers: vec![],
1540                    skipped_segments: Vec::new(),
1541                    skipped_positions: Vec::new(),
1542                };
1543            }
1544        }
1545        let mut instance = self.map_reverse_single(bo4e_value, def);
1546        if def.meta.parent_field.is_none() && !instance.segments.is_empty() {
1547            self.reverse_nested_children(bo4e_value, def, &mut instance);
1548        }
1549        instance
1550    }
1551
1552    fn map_reverse_single(
1553        &self,
1554        bo4e_value: &serde_json::Value,
1555        def: &MappingDefinition,
1556    ) -> AssembledGroupInstance {
1557        // Collect (segment_key, element_index, component_index, value) tuples.
1558        // segment_key includes qualifier for disambiguation: "DTM" or "DTM[92]".
1559        let mut field_values: Vec<(String, String, usize, usize, String)> =
1560            Vec::with_capacity(def.fields.len());
1561
1562        // Track whether any field with a non-empty target resolved to an actual
1563        // BO4E value.  When a definition has data fields but none resolved to
1564        // values, only defaults (qualifiers) would be emitted — producing phantom
1565        // segments for groups not present in the original EDIFACT message.
1566        // Definitions with ONLY qualifier/default fields (no data targets) are
1567        // "container" definitions (e.g., SEQ entry segments) and are always kept.
1568        let mut has_real_data = false;
1569        let mut has_data_fields = false;
1570        // Per-segment phantom tracking: segments with data fields but no resolved
1571        // data are phantoms — their entries should be removed from field_values.
1572        let mut seg_has_data_field: HashSet<String> = HashSet::new();
1573        let mut seg_has_real_data: HashSet<String> = HashSet::new();
1574        let mut injected_qualifiers: HashSet<String> = HashSet::new();
1575        // List-target fields (`werte[].code`), emitted after the loop item by
1576        // item so the segments come out in the list's order.
1577        type ListField<'a> = (
1578            &'a str,
1579            &'a str,
1580            String,
1581            Option<String>,
1582            usize,
1583            usize,
1584            Option<&'a std::collections::BTreeMap<String, String>>,
1585        );
1586        let mut list_fields: Vec<ListField<'_>> = Vec::new();
1587
1588        for (path, field_mapping) in &def.fields {
1589            let (target, default, enum_map, when_filled, also_target, also_enum_map) =
1590                match field_mapping {
1591                    FieldMapping::Simple(t) => (t.as_str(), None, None, None, None, None),
1592                    FieldMapping::Structured(s) => (
1593                        s.target.as_str(),
1594                        s.default.as_ref(),
1595                        self.table(s.enum_map.as_ref(), s.code_list.as_deref()),
1596                        s.when_filled.as_ref(),
1597                        s.also_target.as_deref(),
1598                        self.table(s.also_enum_map.as_ref(), s.also_code_list.as_deref()),
1599                    ),
1600                    FieldMapping::Nested(_) => continue,
1601                };
1602
1603            let parts: Vec<&str> = path.split('.').collect();
1604            if parts.len() < 2 {
1605                continue;
1606            }
1607
1608            let (seg_tag, qualifier, _occ) = parse_tag_qualifier(parts[0]);
1609            // Use the raw first part as segment key to group fields by segment instance.
1610            // Indexed qualifiers like "RFF[Z34,1]" produce a distinct key from "RFF[Z34]".
1611            let seg_key = parts[0].to_uppercase();
1612            let sub_path = &parts[1..];
1613
1614            // Determine (element_idx, component_idx) from path
1615            let (element_idx, component_idx) = if let Ok(ei) = sub_path[0].parse::<usize>() {
1616                let ci = if sub_path.len() > 1 {
1617                    sub_path[1].parse::<usize>().unwrap_or(0)
1618                } else {
1619                    0
1620                };
1621                (ei, ci)
1622            } else {
1623                match sub_path.len() {
1624                    1 => (0, 0),
1625                    2 => (1, 0),
1626                    _ => continue,
1627                }
1628            };
1629
1630            if let Some((list, sub)) = list_target(target) {
1631                list_fields.push((
1632                    list,
1633                    sub,
1634                    seg_tag.clone(),
1635                    qualifier.map(str::to_string),
1636                    element_idx,
1637                    component_idx,
1638                    enum_map,
1639                ));
1640                continue;
1641            }
1642
1643            // Try BO4E value first, fall back to default
1644            let val = if target.is_empty() {
1645                match (default, when_filled) {
1646                    // has when_filled → conditional injection
1647                    (Some(d), Some(fields)) => {
1648                        let any_filled = fields.iter().any(|f| field_is_filled(bo4e_value, f));
1649                        if any_filled {
1650                            // A successful when_filled check confirms real data
1651                            // exists — prevent phantom suppression.
1652                            has_real_data = true;
1653                            Some(d.clone())
1654                        } else {
1655                            None
1656                        }
1657                    }
1658                    // no when_filled → unconditional (backward compat)
1659                    (Some(d), None) => Some(d.clone()),
1660                    (None, _) => None,
1661                }
1662            } else {
1663                has_data_fields = true;
1664                seg_has_data_field.insert(seg_key.clone());
1665                let bo4e_val = self.populate_field(bo4e_value, target);
1666                if bo4e_val.is_some() {
1667                    has_real_data = true;
1668                    seg_has_real_data.insert(seg_key.clone());
1669                }
1670                // Apply reverse enum_map: BO4E value → EDIFACT value
1671                let mapped_val = match (bo4e_val, enum_map) {
1672                    (Some(v), Some(map)) => {
1673                        // Dual decomposition (`also_target`): one EDIFACT code was
1674                        // split across two BO4E fields, so neither alone identifies
1675                        // it. Find the code both maps agree on; several codes share
1676                        // a `partnerrolle` and are told apart only by the second
1677                        // field. Falls back to the single-map lookup when the
1678                        // second field is absent or no code matches both.
1679                        let joint = match (also_target, also_enum_map) {
1680                            (Some(also), Some(also_map)) => {
1681                                self.populate_field(bo4e_value, also).and_then(|also_v| {
1682                                    map.iter()
1683                                        .find(|(code, bo4e_v)| {
1684                                            *bo4e_v == &v && also_map.get(*code) == Some(&also_v)
1685                                        })
1686                                        .map(|(code, _)| code.clone())
1687                                })
1688                            }
1689                            _ => None,
1690                        };
1691                        joint
1692                            .or_else(|| {
1693                                // Reverse lookup: find EDIFACT key for BO4E value
1694                                map.iter()
1695                                    .find(|(_, bo4e_v)| *bo4e_v == &v)
1696                                    .map(|(edifact_k, _)| edifact_k.clone())
1697                            })
1698                            .or(Some(v))
1699                    }
1700                    (v, _) => v,
1701                };
1702                mapped_val.or_else(|| default.cloned())
1703            };
1704
1705            if let Some(val) = val {
1706                field_values.push((
1707                    seg_key.clone(),
1708                    seg_tag.clone(),
1709                    element_idx,
1710                    component_idx,
1711                    val,
1712                ));
1713            }
1714
1715            // If there's a qualifier, also inject it at elements[0][0]
1716            if let Some(q) = qualifier {
1717                if injected_qualifiers.insert(seg_key.clone()) {
1718                    field_values.push((seg_key, seg_tag, 0, 0, q.to_string()));
1719                }
1720            }
1721        }
1722
1723        // Reverse half of list targets: element i of the array becomes the i-th
1724        // segment (`CAV[*,i]`, or `CAV[Q,i]` with the qualifier written back).
1725        let longest = list_fields
1726            .iter()
1727            .filter_map(|(list, ..)| bo4e_value.get(*list).and_then(|v| v.as_array()))
1728            .map(|a| a.len())
1729            .max()
1730            .unwrap_or(0);
1731        if !list_fields.is_empty() {
1732            has_data_fields = true;
1733        }
1734        for i in 0..longest {
1735            for (list, sub, seg_tag, qualifier, element_idx, component_idx, enum_map) in
1736                &list_fields
1737            {
1738                let key = match qualifier {
1739                    Some(q) => format!("{seg_tag}[{q},{i}]"),
1740                    None => format!("{seg_tag}[*,{i}]"),
1741                };
1742                seg_has_data_field.insert(key.clone());
1743                let Some(item) = bo4e_value
1744                    .get(*list)
1745                    .and_then(|v| v.as_array())
1746                    .and_then(|a| a.get(i))
1747                else {
1748                    continue;
1749                };
1750                let Some(value) = self.populate_field(item, sub) else {
1751                    continue;
1752                };
1753                let value = match enum_map {
1754                    Some(map) => map
1755                        .iter()
1756                        .find(|(_, name)| **name == value)
1757                        .map(|(code, _)| code.clone())
1758                        .unwrap_or(value),
1759                    None => value,
1760                };
1761                has_real_data = true;
1762                seg_has_real_data.insert(key.clone());
1763                field_values.push((
1764                    key.clone(),
1765                    seg_tag.clone(),
1766                    *element_idx,
1767                    *component_idx,
1768                    value,
1769                ));
1770                if let Some(q) = qualifier {
1771                    if injected_qualifiers.insert(key.clone()) {
1772                        field_values.push((key, seg_tag.clone(), 0, 0, q.clone()));
1773                    }
1774                }
1775            }
1776        }
1777
1778        // Per-segment phantom prevention for qualified segments: remove entries
1779        // for segments using tag[qualifier] syntax (e.g., FTX[ACB], DTM[Z07])
1780        // that have data fields but none resolved to actual BO4E values.  This
1781        // prevents phantom segments when a definition maps multiple segment types
1782        // and optional qualified segments are not in the original message.
1783        // Unqualified segments (plain tags like SEQ, IDE) are always kept — they
1784        // are typically entry/mandatory segments of their group.
1785        field_values.retain(|(seg_key, _, _, _, _)| {
1786            if !seg_key.contains('[') {
1787                return true; // unqualified segments always kept
1788            }
1789            !seg_has_data_field.contains(seg_key) || seg_has_real_data.contains(seg_key)
1790        });
1791
1792        // If the definition has data fields but none resolved to actual BO4E values,
1793        // return an empty instance to prevent phantom segments for groups not
1794        // present in the original EDIFACT message.  Definitions with only
1795        // qualifier/default fields (has_data_fields=false) are always kept.
1796        if has_data_fields && !has_real_data {
1797            return AssembledGroupInstance {
1798                segments: vec![],
1799                child_groups: vec![],
1800                entry_mig_number: None,
1801                variant_mig_numbers: vec![],
1802                skipped_segments: Vec::new(),
1803                skipped_positions: Vec::new(),
1804            };
1805        }
1806
1807        // Build segments with elements/components in correct positions.
1808        // Group by segment_key to create separate segments for "DTM[92]" vs "DTM[93]".
1809        let mut segments: Vec<AssembledSegment> = Vec::with_capacity(field_values.len());
1810        let mut seen_keys: HashMap<String, usize> = HashMap::new();
1811
1812        for (seg_key, seg_tag, element_idx, component_idx, val) in &field_values {
1813            let seg = if let Some(&pos) = seen_keys.get(seg_key) {
1814                &mut segments[pos]
1815            } else {
1816                let pos = segments.len();
1817                seen_keys.insert(seg_key.clone(), pos);
1818                segments.push(AssembledSegment {
1819                    tag: seg_tag.clone(),
1820                    elements: vec![],
1821                    mig_number: None,
1822                    segment_number: None,
1823                });
1824                &mut segments[pos]
1825            };
1826
1827            while seg.elements.len() <= *element_idx {
1828                seg.elements.push(vec![]);
1829            }
1830            while seg.elements[*element_idx].len() <= *component_idx {
1831                seg.elements[*element_idx].push(String::new());
1832            }
1833            seg.elements[*element_idx][*component_idx] = val.clone();
1834        }
1835
1836        // Pad intermediate empty elements: any [] between position 0 and the last
1837        // populated position becomes [""] so the EDIFACT renderer emits the `+` separator.
1838        for seg in &mut segments {
1839            let last_populated = seg.elements.iter().rposition(|e| !e.is_empty());
1840            if let Some(last_idx) = last_populated {
1841                for i in 0..last_idx {
1842                    if seg.elements[i].is_empty() {
1843                        seg.elements[i] = vec![String::new()];
1844                    }
1845                }
1846            }
1847        }
1848
1849        // MIG-aware trailing padding: extend each segment to the MIG-defined element count.
1850        if let Some(ref ss) = self.segment_structure {
1851            for seg in &mut segments {
1852                if let Some(expected) = ss.element_count(&seg.tag) {
1853                    while seg.elements.len() < expected {
1854                        seg.elements.push(vec![String::new()]);
1855                    }
1856                }
1857            }
1858        }
1859
1860        AssembledGroupInstance {
1861            segments,
1862            child_groups: vec![],
1863            entry_mig_number: None,
1864            variant_mig_numbers: vec![],
1865            skipped_segments: Vec::new(),
1866            skipped_positions: Vec::new(),
1867        }
1868    }
1869
1870    /// Resolve a field path within a segment to extract a value.
1871    ///
1872    /// Two path conventions are supported:
1873    ///
1874    /// **Named paths** (backward compatible):
1875    /// - 1-part `"d3227"` → elements\[0\]\[0\]
1876    /// - 2-part `"c517.d3225"` → elements\[1\]\[0\]
1877    ///
1878    /// **Numeric index paths** (for multi-component access):
1879    /// - `"0"` → elements\[0\]\[0\]
1880    /// - `"1.0"` → elements\[1\]\[0\]
1881    /// - `"1.2"` → elements\[1\]\[2\]
1882    fn resolve_field_path(segment: &AssembledSegment, path: &[&str]) -> Option<String> {
1883        if path.is_empty() {
1884            return None;
1885        }
1886
1887        // Numeric paths only: index-based resolution.
1888        if let Ok(element_idx) = path[0].parse::<usize>() {
1889            let component_idx = if path.len() > 1 {
1890                path[1].parse::<usize>().unwrap_or(0)
1891            } else {
1892                0
1893            };
1894            return segment
1895                .elements
1896                .get(element_idx)?
1897                .get(component_idx)
1898                .filter(|v| !v.is_empty())
1899                .cloned();
1900        }
1901
1902        // Non-numeric path[0] indicates an EDIFACT ID path that the PathResolver
1903        // failed to normalize (e.g. composite/element absent from any loaded PID
1904        // schema). Returning None lets the field be omitted from output instead
1905        // of silently guessing element index 1, which previously surfaced
1906        // unrelated data (e.g. NAD c819.d3229 read as c082.d3039 / rollencodenummer).
1907        None
1908    }
1909
1910    /// Parse element and component indices from path parts after the segment tag.
1911    /// E.g., ["2"] -> (2, 0), ["0", "3"] -> (0, 3), ["1", "0"] -> (1, 0)
1912    pub(crate) fn parse_element_component(parts: &[&str]) -> (usize, usize) {
1913        if parts.is_empty() {
1914            return (0, 0);
1915        }
1916        let element_idx = parts[0].parse::<usize>().unwrap_or(0);
1917        let component_idx = if parts.len() > 1 {
1918            parts[1].parse::<usize>().unwrap_or(0)
1919        } else {
1920            0
1921        };
1922        (element_idx, component_idx)
1923    }
1924
1925    /// Extract a value from a BO4E JSON object by target field name.
1926    /// Supports dotted paths like "nested.field_name".
1927    pub fn populate_field(
1928        &self,
1929        bo4e_value: &serde_json::Value,
1930        target_field: &str,
1931    ) -> Option<String> {
1932        let mut current = bo4e_value;
1933        for part in target_field.split('.') {
1934            current = current.get(part)?;
1935        }
1936        // Handle enriched code objects: {"code": "Z15", "meaning": "..."}
1937        if let Some(code) = current.get("code").and_then(|v| v.as_str()) {
1938            return Some(code.to_string());
1939        }
1940        current.as_str().map(|s| s.to_string())
1941    }
1942
1943    /// Build a segment from BO4E values using the reverse mapping.
1944    pub fn build_segment_from_bo4e(
1945        &self,
1946        bo4e_value: &serde_json::Value,
1947        segment_tag: &str,
1948        target_field: &str,
1949    ) -> AssembledSegment {
1950        let value = self.populate_field(bo4e_value, target_field);
1951        let elements = if let Some(val) = value {
1952            vec![vec![val]]
1953        } else {
1954            vec![]
1955        };
1956        AssembledSegment {
1957            tag: segment_tag.to_uppercase(),
1958            elements,
1959            mig_number: None,
1960            segment_number: None,
1961        }
1962    }
1963
1964    // ── Multi-entity forward mapping ──
1965
1966    /// Parse a discriminator string (e.g., "SEQ.0.0=Z79") and find the matching
1967    /// repetition index within the given group path.
1968    ///
1969    /// Discriminator format: `"TAG.element_idx.component_idx=expected_value"`
1970    /// Scans all repetitions of the leaf group and returns the first rep index
1971    /// where the entry segment matches.
1972    pub fn resolve_repetition(
1973        tree: &AssembledTree,
1974        group_path: &str,
1975        discriminator: &str,
1976    ) -> Option<usize> {
1977        let (spec, expected) = discriminator.split_once('=')?;
1978        let parts: Vec<&str> = spec.split('.').collect();
1979        if parts.len() != 3 {
1980            return None;
1981        }
1982        let tag = parts[0];
1983        let element_idx: usize = parts[1].parse().ok()?;
1984        let component_idx: usize = parts[2].parse().ok()?;
1985
1986        // Navigate to the parent and get the leaf group with all its repetitions
1987        let path_parts: Vec<&str> = group_path.split('.').collect();
1988
1989        let leaf_group = if path_parts.len() == 1 {
1990            let (group_id, _) = parse_group_spec(path_parts[0]);
1991            tree.groups.iter().find(|g| g.group_id == group_id)?
1992        } else {
1993            // Navigate to the parent instance, then find the leaf group
1994            let parent_parts = &path_parts[..path_parts.len() - 1];
1995            let mut current_instance = {
1996                let (first_id, first_rep) = parse_group_spec(parent_parts[0]);
1997                let first_group = tree.groups.iter().find(|g| g.group_id == first_id)?;
1998                first_group.repetitions.get(first_rep.unwrap_or(0))?
1999            };
2000            for part in &parent_parts[1..] {
2001                let (group_id, explicit_rep) = parse_group_spec(part);
2002                let child_group = current_instance
2003                    .child_groups
2004                    .iter()
2005                    .find(|g| g.group_id == group_id)?;
2006                current_instance = child_group.repetitions.get(explicit_rep.unwrap_or(0))?;
2007            }
2008            let (leaf_id, _) = parse_group_spec(path_parts.last()?);
2009            current_instance
2010                .child_groups
2011                .iter()
2012                .find(|g| g.group_id == leaf_id)?
2013        };
2014
2015        // Scan all repetitions for the matching discriminator
2016        let expected_values: Vec<&str> = expected.split('|').collect();
2017        for (rep_idx, instance) in leaf_group.repetitions.iter().enumerate() {
2018            let matches = instance.segments.iter().any(|s| {
2019                s.tag.eq_ignore_ascii_case(tag)
2020                    && s.elements
2021                        .get(element_idx)
2022                        .and_then(|e| e.get(component_idx))
2023                        .map(|v| expected_values.iter().any(|ev| v == ev))
2024                        .unwrap_or(false)
2025            });
2026            if matches {
2027                return Some(rep_idx);
2028            }
2029        }
2030
2031        None
2032    }
2033
2034    /// Like `resolve_repetition`, but returns ALL matching rep indices instead of just the first.
2035    ///
2036    /// This is used for multi-Zeitscheibe support where multiple SG6 reps may match
2037    /// the same discriminator (e.g., multiple RFF+Z49 time slices).
2038    pub fn resolve_all_repetitions(
2039        tree: &AssembledTree,
2040        group_path: &str,
2041        discriminator: &str,
2042    ) -> Vec<usize> {
2043        let Some((spec, expected)) = discriminator.split_once('=') else {
2044            return Vec::new();
2045        };
2046        let parts: Vec<&str> = spec.split('.').collect();
2047        if parts.len() != 3 {
2048            return Vec::new();
2049        }
2050        let tag = parts[0];
2051        let element_idx: usize = match parts[1].parse() {
2052            Ok(v) => v,
2053            Err(_) => return Vec::new(),
2054        };
2055        let component_idx: usize = match parts[2].parse() {
2056            Ok(v) => v,
2057            Err(_) => return Vec::new(),
2058        };
2059
2060        // Navigate to the parent and get the leaf group with all its repetitions
2061        let path_parts: Vec<&str> = group_path.split('.').collect();
2062
2063        let leaf_group = if path_parts.len() == 1 {
2064            let (group_id, _) = parse_group_spec(path_parts[0]);
2065            match tree.groups.iter().find(|g| g.group_id == group_id) {
2066                Some(g) => g,
2067                None => return Vec::new(),
2068            }
2069        } else {
2070            let parent_parts = &path_parts[..path_parts.len() - 1];
2071            let mut current_instance = {
2072                let (first_id, first_rep) = parse_group_spec(parent_parts[0]);
2073                let first_group = match tree.groups.iter().find(|g| g.group_id == first_id) {
2074                    Some(g) => g,
2075                    None => return Vec::new(),
2076                };
2077                match first_group.repetitions.get(first_rep.unwrap_or(0)) {
2078                    Some(i) => i,
2079                    None => return Vec::new(),
2080                }
2081            };
2082            for part in &parent_parts[1..] {
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 Vec::new(),
2091                };
2092                current_instance = match child_group.repetitions.get(explicit_rep.unwrap_or(0)) {
2093                    Some(i) => i,
2094                    None => return Vec::new(),
2095                };
2096            }
2097            let (leaf_id, _) = match path_parts.last() {
2098                Some(p) => parse_group_spec(p),
2099                None => return Vec::new(),
2100            };
2101            match current_instance
2102                .child_groups
2103                .iter()
2104                .find(|g| g.group_id == leaf_id)
2105            {
2106                Some(g) => g,
2107                None => return Vec::new(),
2108            }
2109        };
2110
2111        // Parse optional occurrence index from expected value: "TN#1" → ("TN", Some(1))
2112        let (expected_raw, occurrence) = parse_discriminator_occurrence(expected);
2113
2114        // Collect ALL matching rep indices
2115        let expected_values: Vec<&str> = expected_raw.split('|').collect();
2116        let mut result = Vec::new();
2117        for (rep_idx, instance) in leaf_group.repetitions.iter().enumerate() {
2118            let matches = instance.segments.iter().any(|s| {
2119                s.tag.eq_ignore_ascii_case(tag)
2120                    && s.elements
2121                        .get(element_idx)
2122                        .and_then(|e| e.get(component_idx))
2123                        .map(|v| expected_values.iter().any(|ev| v == ev))
2124                        .unwrap_or(false)
2125            });
2126            if matches {
2127                result.push(rep_idx);
2128            }
2129        }
2130
2131        // If occurrence index specified, return only that match
2132        if let Some(occ) = occurrence {
2133            result.into_iter().nth(occ).into_iter().collect()
2134        } else {
2135            result
2136        }
2137    }
2138
2139    /// Resolve a discriminated instance using source_path for parent navigation.
2140    ///
2141    /// Like `resolve_repetition` + `resolve_group_instance`, but navigates to the
2142    /// parent group via source_path qualifier suffixes. Returns the matching instance
2143    /// directly (not just a rep index) to avoid re-navigation in `map_forward_inner`.
2144    ///
2145    /// For example, `source_path = "sg4.sg8_z98.sg10"` with `discriminator = "CCI.2.0=ZB3"`
2146    /// navigates to the SG8 instance with SEQ qualifier Z98, then finds the SG10 rep
2147    /// where CCI element 2 component 0 equals "ZB3".
2148    /// Map all definitions against a tree, returning a JSON object with entity names as keys.
2149    ///
2150    /// For each definition:
2151    /// - Has discriminator → find matching rep via `resolve_repetition`, map single instance
2152    /// - Root-level (empty source_group) → map rep 0 as single object
2153    /// - No discriminator, 1 rep in tree → map as single object
2154    /// - No discriminator, multiple reps in tree → map ALL reps into a JSON array
2155    ///
2156    /// When multiple definitions share the same `entity` name, their fields are
2157    /// deep-merged into a single JSON object. This allows related TOML files
2158    /// (e.g., LOC location + SEQ info + SG10 characteristics) to contribute
2159    /// fields to the same BO4E entity.
2160    pub fn map_all_forward(&self, tree: &AssembledTree) -> serde_json::Value {
2161        self.map_all_forward_inner(tree, true).0
2162    }
2163
2164    /// Like [`map_all_forward`](Self::map_all_forward) but with explicit
2165    /// `enrich_codes` control (when `false`, code fields are plain strings
2166    /// instead of `{"code": …, "meaning": …}` objects).
2167    pub fn map_all_forward_enriched(
2168        &self,
2169        tree: &AssembledTree,
2170        enrich_codes: bool,
2171    ) -> serde_json::Value {
2172        self.map_all_forward_inner(tree, enrich_codes).0
2173    }
2174
2175    /// Inner implementation with enrichment control.
2176    ///
2177    /// Returns `(json_value, nesting_info)`, where `nesting_info` maps entity
2178    /// keys to the parent rep index for each child element (used by the reverse
2179    /// mapper to distribute nested group children among their parent reps).
2180    fn map_all_forward_inner(
2181        &self,
2182        tree: &AssembledTree,
2183        enrich_codes: bool,
2184    ) -> (
2185        serde_json::Value,
2186        std::collections::HashMap<String, Vec<usize>>,
2187    ) {
2188        self.map_all_forward_inner_with_tx(tree, enrich_codes, self.transaction_group.as_deref())
2189    }
2190
2191    /// Like `map_all_forward_inner` but with an explicit transaction-group
2192    /// override. Used by `map_interchange`, which knows the tx group even when
2193    /// the caller-supplied tx_engine wasn't built with `with_transaction_group`.
2194    fn map_all_forward_inner_with_tx(
2195        &self,
2196        tree: &AssembledTree,
2197        enrich_codes: bool,
2198        tx_group_override: Option<&str>,
2199    ) -> (
2200        serde_json::Value,
2201        std::collections::HashMap<String, Vec<usize>>,
2202    ) {
2203        let mut result = serde_json::Map::new();
2204        let mut nesting_info: std::collections::HashMap<String, Vec<usize>> =
2205            std::collections::HashMap::new();
2206        // Source groups that have written each entity key so far.
2207        let mut contributors: std::collections::HashMap<String, Vec<String>> =
2208            std::collections::HashMap::new();
2209
2210        for def in &self.definitions {
2211            // `parent_field` children are mapped inside their parent's instance
2212            // (see `extract_nested_children`), never as top-level entities.
2213            if def.meta.parent_field.is_some() {
2214                continue;
2215            }
2216            let entity = &def.meta.entity;
2217
2218            let bo4e = if let Some(ref disc) = def.meta.discriminator {
2219                // Has discriminator — resolve to matching rep(s).
2220                // Use source_path navigation when qualifiers are present
2221                // (e.g., "sg4.sg8_z98.sg10" navigates to Z98's SG10 reps,
2222                //  "sg4.sg5_z17" finds all LOC+Z17 when there are multiple).
2223                let use_source_path = def
2224                    .meta
2225                    .source_path
2226                    .as_ref()
2227                    .is_some_and(|sp| has_source_path_qualifiers(sp));
2228                if use_source_path {
2229                    // Navigate via source_path, then filter by discriminator.
2230                    let sp = def.meta.source_path.as_deref().unwrap();
2231                    let all_instances = Self::resolve_all_by_source_path(tree, sp);
2232                    // Apply discriminator filter to resolved instances (respects #N occurrence)
2233                    let instances: Vec<_> = if let Some(matcher) = DiscriminatorMatcher::parse(disc)
2234                    {
2235                        matcher.filter_instances(all_instances)
2236                    } else {
2237                        all_instances
2238                    };
2239                    let extract = |instance: &AssembledGroupInstance| {
2240                        let mut r = serde_json::Map::new();
2241                        self.extract_fields_from_instance(instance, def, &mut r, enrich_codes);
2242                        serde_json::Value::Object(r)
2243                    };
2244                    match instances.len() {
2245                        0 => None,
2246                        1 => Some(extract(instances[0])),
2247                        _ => Some(serde_json::Value::Array(
2248                            instances.iter().map(|i| extract(i)).collect(),
2249                        )),
2250                    }
2251                } else {
2252                    let reps = Self::resolve_all_repetitions(tree, &def.meta.source_group, disc);
2253                    match reps.len() {
2254                        0 => None,
2255                        1 => Some(self.map_forward_inner(tree, def, reps[0], enrich_codes)),
2256                        _ => Some(serde_json::Value::Array(
2257                            reps.iter()
2258                                .map(|&rep| self.map_forward_inner(tree, def, rep, enrich_codes))
2259                                .collect(),
2260                        )),
2261                    }
2262                }
2263            } else if def.meta.source_group.is_empty() {
2264                // Root-level mapping — always single object
2265                Some(self.map_forward_inner(tree, def, 0, enrich_codes))
2266            } else if def.meta.source_path.as_ref().is_some_and(|sp| {
2267                has_source_path_qualifiers(sp) || def.meta.source_group.contains('.')
2268            }) {
2269                // Multi-level source path — navigate via source_path to collect all
2270                // instances across all parent repetitions. Handles both qualified
2271                // paths (e.g., "sg4.sg8_zd7.sg10") and unqualified paths (e.g.,
2272                // "sg17.sg36.sg40") where multiple parent reps each have children.
2273                let sp = def.meta.source_path.as_deref().unwrap();
2274                let mut indexed = Self::resolve_all_with_parent_indices(tree, sp);
2275
2276                // When the LAST part of source_path has no qualifier (e.g., "sg29.sg30"),
2277                // exclude reps that match a qualified sibling definition's qualifier
2278                // (e.g., "sg29.sg30_z35"). This prevents double-extraction when both
2279                // qualified and unqualified definitions target the same group.
2280                if let Some(last_part) = sp.rsplit('.').next() {
2281                    if !last_part.contains('_') {
2282                        // Collect qualifiers from sibling definitions that share the
2283                        // same base group name. E.g., for "sg29.sg30", only match
2284                        // "sg29.sg30_z35" (same base "sg30"), NOT "sg29.sg31_z35".
2285                        let base_prefix = if let Some(parent) = sp.rsplit_once('.') {
2286                            format!("{}.", parent.0)
2287                        } else {
2288                            String::new()
2289                        };
2290                        let sibling_qualifiers: Vec<String> = self
2291                            .definitions
2292                            .iter()
2293                            .filter_map(|d| d.meta.source_path.as_deref())
2294                            .filter(|other_sp| {
2295                                *other_sp != sp
2296                                    && other_sp.starts_with(&base_prefix)
2297                                    && other_sp.split('.').count() == sp.split('.').count()
2298                            })
2299                            .filter_map(|other_sp| {
2300                                let other_last = other_sp.rsplit('.').next()?;
2301                                // Only match siblings with the same base group name
2302                                // e.g., "sg30_z35" has base "sg30", must match "sg30"
2303                                let (base, q) = other_last.split_once('_')?;
2304                                if base == last_part {
2305                                    Some(q.to_string())
2306                                } else {
2307                                    None
2308                                }
2309                            })
2310                            .collect();
2311
2312                        if !sibling_qualifiers.is_empty() {
2313                            indexed.retain(|(_, inst)| {
2314                                let entry_qual = inst
2315                                    .segments
2316                                    .first()
2317                                    .and_then(|seg| seg.elements.first())
2318                                    .and_then(|el| el.first())
2319                                    .map(|v| v.to_lowercase());
2320                                // Keep reps whose entry qualifier does NOT match
2321                                // any sibling's qualifier
2322                                !entry_qual.is_some_and(|q| {
2323                                    sibling_qualifiers.iter().any(|sq| {
2324                                        sq.split('_').any(|part| part.eq_ignore_ascii_case(&q))
2325                                    })
2326                                })
2327                            });
2328                        }
2329                    }
2330                }
2331                let extract = |instance: &AssembledGroupInstance| {
2332                    let mut r = serde_json::Map::new();
2333                    self.extract_fields_from_instance(instance, def, &mut r, enrich_codes);
2334                    serde_json::Value::Object(r)
2335                };
2336                // Track parent rep indices for nesting reconstruction.
2337                // Key by source_path (not entity or source_group) so that definitions
2338                // at different depths or with different qualifiers don't collide.
2339                // e.g., "sg5.sg8_z41.sg9" vs "sg5.sg8_z42.sg9" are distinct keys.
2340                if def.meta.source_group.contains('.') && !indexed.is_empty() {
2341                    if let Some(sp) = &def.meta.source_path {
2342                        let parent_indices: Vec<usize> =
2343                            indexed.iter().map(|(idx, _)| *idx).collect();
2344                        nesting_info.entry(sp.clone()).or_insert(parent_indices);
2345
2346                        // Also store child rep indices (position within the leaf group)
2347                        // for depth-1 reverse placement. Key: "{sp}#child".
2348                        let child_key = format!("{sp}#child");
2349                        if let std::collections::hash_map::Entry::Vacant(e) =
2350                            nesting_info.entry(child_key)
2351                        {
2352                            let child_indices: Vec<usize> =
2353                                Self::compute_child_indices(tree, sp, &indexed);
2354                            if !child_indices.is_empty() {
2355                                e.insert(child_indices);
2356                            }
2357                        }
2358                    }
2359                }
2360                match indexed.len() {
2361                    0 => None,
2362                    1 => Some(extract(indexed[0].1)),
2363                    _ => Some(serde_json::Value::Array(
2364                        indexed.iter().map(|(_, i)| extract(i)).collect(),
2365                    )),
2366                }
2367            } else {
2368                let num_reps = Self::count_repetitions(tree, &def.meta.source_group);
2369                if num_reps <= 1 {
2370                    Some(self.map_forward_inner(tree, def, 0, enrich_codes))
2371                } else {
2372                    // Multiple reps, no discriminator — map all into array
2373                    let mut items = Vec::with_capacity(num_reps);
2374                    for rep in 0..num_reps {
2375                        items.push(self.map_forward_inner(tree, def, rep, enrich_codes));
2376                    }
2377                    Some(serde_json::Value::Array(items))
2378                }
2379            };
2380
2381            if let Some(bo4e) = bo4e {
2382                let key = to_camel_case(entity);
2383                match def.meta.target_list.as_deref() {
2384                    Some(list_field) => append_to_list_field(&mut result, &key, list_field, bo4e),
2385                    None => {
2386                        // Keep both on a shape mismatch only for sibling groups:
2387                        // no earlier contributor of this entity is this group's
2388                        // ancestor or descendant (see `merge_entity`).
2389                        let group = def
2390                            .meta
2391                            .source_path
2392                            .clone()
2393                            .unwrap_or_else(|| def.meta.source_group.to_lowercase());
2394                        let seen = contributors.entry(key.clone()).or_default();
2395                        let nested = seen.iter().any(|other: &String| {
2396                            group.starts_with(&format!("{other}."))
2397                                || other.starts_with(&format!("{group}."))
2398                        });
2399                        seen.push(group);
2400                        merge_entity(&mut result, &key, bo4e, !nested);
2401                    }
2402                }
2403            }
2404        }
2405
2406        // Post-process: nest child entities under their parent entities.
2407        // E.g., Kontakt (source_group="SG2.SG3") moves under Marktteilnehmer (source_group="SG2").
2408        // Children whose parent group is the transaction root (e.g. SG4 for UTILMD) are
2409        // left at the top level — see MappingEngine::transaction_group.
2410        nest_child_entities_in_result(
2411            &mut result,
2412            &self.definitions,
2413            &nesting_info,
2414            tx_group_override,
2415        );
2416
2417        (serde_json::Value::Object(result), nesting_info)
2418    }
2419
2420    /// Reverse-map a BO4E entity map back to an AssembledTree.
2421    ///
2422    /// For each definition:
2423    /// 1. Look up entity in input by `meta.entity` name
2424    /// 2. If entity value is an array, map each element as a separate group repetition
2425    /// 3. Place results by `source_group`: `""` → root segments, `"SGn"` → groups
2426    ///
2427    /// This is the inverse of `map_all_forward()`.
2428    pub fn map_all_reverse(
2429        &self,
2430        entities: &serde_json::Value,
2431        nesting_info: Option<&std::collections::HashMap<String, Vec<usize>>>,
2432    ) -> AssembledTree {
2433        self.map_all_reverse_with_mig(entities, nesting_info, None)
2434    }
2435
2436    /// [`map_all_reverse`](Self::map_all_reverse) with the PID-filtered MIG,
2437    /// which decides the parent of a nested child entity the BO4E JSON does
2438    /// not link to a parent (see the nesting step below).
2439    pub fn map_all_reverse_with_mig(
2440        &self,
2441        entities: &serde_json::Value,
2442        nesting_info: Option<&std::collections::HashMap<String, Vec<usize>>>,
2443        mig: Option<&MigSchema>,
2444    ) -> AssembledTree {
2445        let mut root_segments: Vec<AssembledSegment> = Vec::new();
2446        let mut groups: Vec<AssembledGroup> = Vec::new();
2447        // Track parent rep indices for child entities extracted from map-keyed
2448        // or array parents.  Used as fallback when nesting_info is empty.
2449        let mut inferred_nesting: std::collections::HashMap<String, Vec<usize>> =
2450            std::collections::HashMap::new();
2451
2452        for def in &self.definitions {
2453            // `parent_field` children are reversed with their parent object
2454            // (see `reverse_nested_children`).
2455            if def.meta.parent_field.is_some() {
2456                continue;
2457            }
2458            let entity_key = to_camel_case(&def.meta.entity);
2459
2460            // Look up entity value — first at top level, then nested under parent.
2461            // `_extracted` keeps the owned value alive for the borrow below.
2462            let _extracted: Option<serde_json::Value>;
2463            let entity_value = if let Some(list_field) = def.meta.target_list.as_deref() {
2464                // `target_list`: this definition's data is not the entity object,
2465                // it is the elements of a list field on it. Handing the array
2466                // straight to the array branch below turns each element back into
2467                // one group repetition, which is the exact inverse of the forward
2468                // "one repetition -> one element" rule.
2469                match entities.get(&entity_key).and_then(|e| e.get(list_field)) {
2470                    Some(v) if v.is_array() => {
2471                        _extracted = None;
2472                        v
2473                    }
2474                    _ => continue,
2475                }
2476            } else if let Some(v) = entities.get(&entity_key) {
2477                _extracted = None;
2478                v
2479            } else if def.meta.source_group.contains('.') {
2480                // Child entity not at top level — try extracting from parent entity
2481                match extract_child_from_parent_with_indices(entities, &self.definitions, def) {
2482                    Some((v, parent_indices)) => {
2483                        // Record inferred parent rep indices for nesting distribution
2484                        if let Some(sp) = def.meta.source_path.as_deref() {
2485                            inferred_nesting
2486                                .entry(sp.to_string())
2487                                .or_insert(parent_indices);
2488                        }
2489                        _extracted = Some(v);
2490                        _extracted.as_ref().unwrap()
2491                    }
2492                    None => continue,
2493                }
2494            } else {
2495                continue;
2496            };
2497
2498            // Support map-keyed entities from typed PID format.
2499            // E.g., geschaeftspartner: {"Z04": {name1: "..."}} with discriminator NAD.0.0=Z04.
2500            // Extract inner value using discriminator's qualifier value as key,
2501            // and inject the qualifier into the inner object so companion fields find it.
2502            //
2503            // Also handles non-discriminated maps (e.g., marktteilnehmer: {"MS": {...}, "MR": {...}})
2504            // by converting them to arrays of inner values.
2505            let unwrapped: Option<serde_json::Value>;
2506            let entity_value = if entity_value.is_object() && !entity_value.is_array() {
2507                if let Some(disc_value) = def
2508                    .meta
2509                    .discriminator
2510                    .as_deref()
2511                    .and_then(|d| d.split_once('='))
2512                    .map(|(_, v)| v)
2513                {
2514                    // Discriminated definition: try to extract map key matching qualifier
2515                    if let Some(inner) = entity_value.get(disc_value) {
2516                        let mut injected = inner.clone();
2517                        // Find the field that maps to the discriminator's EDIFACT path
2518                        // and inject the map key as that field's value (e.g., nadQualifier = "Z04")
2519                        if let Some(qualifier_field) =
2520                            find_qualifier_companion_field(&self.definitions, &def.meta.entity)
2521                        {
2522                            if let Some(obj) = injected.as_object_mut() {
2523                                let entry = obj
2524                                    .entry(qualifier_field)
2525                                    .or_insert(serde_json::Value::Null);
2526                                if entry.is_null() {
2527                                    *entry = serde_json::Value::String(disc_value.to_string());
2528                                }
2529                            }
2530                        }
2531                        unwrapped = Some(injected);
2532                        unwrapped.as_ref().unwrap()
2533                    } else {
2534                        entity_value
2535                    }
2536                } else if is_map_keyed_object(entity_value) {
2537                    // Non-discriminated definition: convert map to array
2538                    // e.g., marktteilnehmer: {"MS": {...}, "MR": {...}} → [{...}, {...}]
2539                    // Inject each map key into its inner object using the companion field
2540                    // that maps to the discriminator path (if identifiable from other defs).
2541                    let map = entity_value.as_object().unwrap();
2542                    let arr: Vec<serde_json::Value> = map
2543                        .iter()
2544                        .map(|(key, val)| {
2545                            let mut item = val.clone();
2546                            // Try to find a qualifier companion field from peer definitions
2547                            // that share this entity name and have a discriminator
2548                            if let Some(obj) = item.as_object_mut() {
2549                                if let Some(qualifier_field) = find_qualifier_companion_field(
2550                                    &self.definitions,
2551                                    &def.meta.entity,
2552                                ) {
2553                                    let entry = obj
2554                                        .entry(qualifier_field)
2555                                        .or_insert(serde_json::Value::Null);
2556                                    if entry.is_null() {
2557                                        *entry = serde_json::Value::String(key.clone());
2558                                    }
2559                                }
2560                            }
2561                            item
2562                        })
2563                        .collect();
2564                    unwrapped = Some(serde_json::Value::Array(arr));
2565                    unwrapped.as_ref().unwrap()
2566                } else {
2567                    entity_value
2568                }
2569            } else {
2570                entity_value
2571            };
2572
2573            // Determine target group from source_group (use leaf part after last dot)
2574            let leaf_group = def
2575                .meta
2576                .source_group
2577                .rsplit('.')
2578                .next()
2579                .unwrap_or(&def.meta.source_group);
2580
2581            if def.meta.source_group.is_empty() {
2582                // Root-level: reverse into root segments
2583                let instance = self.map_reverse(entity_value, def);
2584                root_segments.extend(instance.segments);
2585            } else if entity_value.is_array() {
2586                // Array entity: each element becomes a group repetition
2587                let arr = entity_value.as_array().unwrap();
2588                let reps: Vec<_> = arr.iter().map(|item| self.map_reverse(item, def)).collect();
2589
2590                // Merge into existing group or create new one
2591                if let Some(existing) = groups.iter_mut().find(|g| g.group_id == leaf_group) {
2592                    existing.repetitions.extend(reps);
2593                } else {
2594                    groups.push(AssembledGroup {
2595                        group_id: leaf_group.to_string(),
2596                        repetitions: reps,
2597                    });
2598                }
2599            } else {
2600                // Single object: one repetition
2601                let instance = self.map_reverse(entity_value, def);
2602
2603                if let Some(existing) = groups.iter_mut().find(|g| g.group_id == leaf_group) {
2604                    existing.repetitions.push(instance);
2605                } else {
2606                    groups.push(AssembledGroup {
2607                        group_id: leaf_group.to_string(),
2608                        repetitions: vec![instance],
2609                    });
2610                }
2611            }
2612        }
2613
2614        // Post-process: move nested groups under their parent repetitions.
2615        // Definitions with multi-level source_group (e.g., "SG2.SG3") produce
2616        // top-level groups that must be nested inside their parent group.
2617        // Children are distributed sequentially among parent reps (child[i] → parent[i])
2618        // matching the forward mapper's extraction order.
2619        let nested_specs: Vec<(String, String)> = self
2620            .definitions
2621            .iter()
2622            .filter(|def| def.meta.parent_field.is_none())
2623            .filter_map(|def| {
2624                let parts: Vec<&str> = def.meta.source_group.split('.').collect();
2625                if parts.len() > 1 {
2626                    Some((parts[0].to_string(), parts[parts.len() - 1].to_string()))
2627                } else {
2628                    None
2629                }
2630            })
2631            .collect();
2632        for (parent_id, child_id) in &nested_specs {
2633            // Only nest if both parent and child exist at the top level
2634            let has_parent = groups.iter().any(|g| g.group_id == *parent_id);
2635            let has_child = groups.iter().any(|g| g.group_id == *child_id);
2636            if has_parent && has_child {
2637                let child_idx = groups.iter().position(|g| g.group_id == *child_id).unwrap();
2638                let child_group = groups.remove(child_idx);
2639                let parent = groups
2640                    .iter_mut()
2641                    .find(|g| g.group_id == *parent_id)
2642                    .unwrap();
2643                // Distribute child reps among parent reps using nesting info
2644                // if available, falling back to all-under-first when not.
2645                // Nesting info is keyed by source_path (e.g., "sg2.sg3").
2646                let child_source_path = self
2647                    .definitions
2648                    .iter()
2649                    .find(|d| {
2650                        let parts: Vec<&str> = d.meta.source_group.split('.').collect();
2651                        d.meta.parent_field.is_none()
2652                            && parts.len() > 1
2653                            && parts[parts.len() - 1] == *child_id
2654                    })
2655                    .and_then(|d| d.meta.source_path.as_deref());
2656                let distribution = child_source_path.and_then(|key| {
2657                    nesting_info
2658                        .and_then(|ni| ni.get(key))
2659                        .or_else(|| inferred_nesting.get(key))
2660                });
2661                // Without a link from the JSON, the parent follows from the MIG:
2662                // the first repetition (in MIG variant order) whose variant
2663                // defines this child group — e.g. the SG2 NAD+MS repetition for
2664                // the sender's SG3 contact. Not "the first array element": BO4E
2665                // carries no ordering information.
2666                let unlinked_target = mig
2667                    .and_then(|m| {
2668                        mig_assembly::repetition_order::preferred_parent_repetition(
2669                            parent,
2670                            &m.segment_groups,
2671                            child_id,
2672                        )
2673                    })
2674                    .unwrap_or(0);
2675                for (i, child_rep) in child_group.repetitions.into_iter().enumerate() {
2676                    let target_idx = distribution
2677                        .and_then(|dist| dist.get(i))
2678                        .copied()
2679                        .unwrap_or(unlinked_target);
2680
2681                    if let Some(target_rep) = parent.repetitions.get_mut(target_idx) {
2682                        if let Some(existing) = target_rep
2683                            .child_groups
2684                            .iter_mut()
2685                            .find(|g| g.group_id == *child_id)
2686                        {
2687                            existing.repetitions.push(child_rep);
2688                        } else {
2689                            target_rep.child_groups.push(AssembledGroup {
2690                                group_id: child_id.clone(),
2691                                repetitions: vec![child_rep],
2692                            });
2693                        }
2694                    }
2695                }
2696            }
2697        }
2698
2699        let post_group_start = root_segments.len();
2700        AssembledTree {
2701            segments: root_segments,
2702            groups,
2703            post_group_start,
2704            inter_group_segments: std::collections::BTreeMap::new(),
2705        }
2706    }
2707
2708    /// Count the number of repetitions available for a group path in the tree.
2709    fn count_repetitions(tree: &AssembledTree, group_path: &str) -> usize {
2710        let parts: Vec<&str> = group_path.split('.').collect();
2711
2712        let (first_id, first_rep) = parse_group_spec(parts[0]);
2713        let first_group = match tree.groups.iter().find(|g| g.group_id == first_id) {
2714            Some(g) => g,
2715            None => return 0,
2716        };
2717
2718        if parts.len() == 1 {
2719            return first_group.repetitions.len();
2720        }
2721
2722        // Navigate to parent, then count leaf group reps
2723        let mut current_instance = match first_group.repetitions.get(first_rep.unwrap_or(0)) {
2724            Some(i) => i,
2725            None => return 0,
2726        };
2727
2728        for (i, part) in parts[1..].iter().enumerate() {
2729            let (group_id, explicit_rep) = parse_group_spec(part);
2730            let child_group = match current_instance
2731                .child_groups
2732                .iter()
2733                .find(|g| g.group_id == group_id)
2734            {
2735                Some(g) => g,
2736                None => return 0,
2737            };
2738
2739            if i == parts.len() - 2 {
2740                // Last part — return rep count
2741                return child_group.repetitions.len();
2742            }
2743            current_instance = match child_group.repetitions.get(explicit_rep.unwrap_or(0)) {
2744                Some(i) => i,
2745                None => return 0,
2746            };
2747        }
2748
2749        0
2750    }
2751
2752    /// Translate an assembled tree into BO4E, without code enrichment.
2753    ///
2754    /// This is the translation proper: every code field is a plain string, as it
2755    /// appears in the EDIFACT message. Enrichment (`{code, meaning, enum}`) is a
2756    /// display concern and is applied separately by [`Self::enrich_bo4e_types`],
2757    /// so a caller that does not need it never pays for it and never has to
2758    /// strip it back out.
2759    pub fn translate_edifact_to_bo4e(
2760        msg_engine: &MappingEngine,
2761        tx_engine: &MappingEngine,
2762        tree: &AssembledTree,
2763        transaction_group: &str,
2764    ) -> crate::model::MappedMessage {
2765        Self::map_interchange_inner(msg_engine, tx_engine, tree, transaction_group, false)
2766    }
2767
2768    /// Decorate code fields of an already-translated message in place.
2769    ///
2770    /// Replaces the plain string at each code position with
2771    /// `{"code": …, "meaning": …, "enum": …}`. Needs a [`CodeLookup`] on the
2772    /// engines; without one this is a no-op, which is why CI — which never
2773    /// attaches a lookup — sees the unenriched shape.
2774    ///
2775    /// Works from the mapping definitions rather than from the EDIFACT tree: a
2776    /// definition knows both where a value came from (`source_path` plus the
2777    /// segment/element coordinates of the field) and where it went (`target`),
2778    /// which is all the lookup needs. The original EDIFACT value is recovered by
2779    /// inverting `enum_map` the same way the reverse mapper does, including the
2780    /// `also_target` disambiguation for codes that share a primary value.
2781    pub fn enrich_bo4e_types(
2782        msg_engine: &MappingEngine,
2783        tx_engine: &MappingEngine,
2784        mapped: &mut crate::model::MappedMessage,
2785    ) {
2786        msg_engine.enrich_entities(&mut mapped.stammdaten);
2787        for tx in &mut mapped.transaktionen {
2788            tx_engine.enrich_entities(&mut tx.stammdaten);
2789        }
2790
2791        // The metadata slots hold mapped entities too. The forward pass splits
2792        // them out of `stammdaten`, so walking `stammdaten` alone no longer
2793        // reaches them — and their code fields would silently stay plain.
2794        msg_engine.enrich_named_entity(
2795            &mut mapped.nachricht_meta,
2796            crate::model::MSG_METADATA_ENTITY,
2797        );
2798        for tx in &mut mapped.transaktionen {
2799            tx_engine
2800                .enrich_named_entity(&mut tx.transaktionsdaten, crate::model::TX_METADATA_ENTITY);
2801        }
2802    }
2803
2804    /// Apply the code sites of one named entity to a value holding that entity.
2805    ///
2806    /// The entity-map walk keys on the enclosing object's field name; a metadata
2807    /// slot has no such name, so the entity is named explicitly here.
2808    fn enrich_named_entity(&self, value: &mut serde_json::Value, entity_key: &str) {
2809        if self.code_lookup.is_none() || value.is_null() {
2810            return;
2811        }
2812        let sites = self.code_sites();
2813        if let Some(entity_sites) = sites.get(entity_key) {
2814            Self::apply_sites(self, value, entity_sites);
2815        }
2816    }
2817
2818    /// Apply this engine's code enrichment to one entity map.
2819    ///
2820    /// Entities are located by key at any depth, because the forward pass moves
2821    /// them after extraction: `nest_child_entities_in_result` puts children
2822    /// under their parents.
2823    fn enrich_entities(&self, value: &mut serde_json::Value) {
2824        if self.code_lookup.is_none() {
2825            return;
2826        }
2827        let sites: HashMap<String, Vec<CodeSite<'_>>> = self.code_sites();
2828        if sites.is_empty() {
2829            return;
2830        }
2831        Self::walk_and_enrich(self, value, &sites);
2832    }
2833
2834    /// Every code-field position this engine's definitions write to, grouped by
2835    /// the entity key the value ends up under.
2836    fn code_sites(&self) -> HashMap<String, Vec<CodeSite<'_>>> {
2837        let Some(ref code_lookup) = self.code_lookup else {
2838            return HashMap::new();
2839        };
2840        let mut sites: HashMap<String, Vec<CodeSite<'_>>> = HashMap::new();
2841
2842        for def in &self.definitions {
2843            let Some(ref source_path) = def.meta.source_path else {
2844                continue;
2845            };
2846            let entity_key = to_camel_case(&def.meta.entity);
2847
2848            for (path, field_mapping) in &def.fields {
2849                let (target, enum_map, also_target, also_enum_map) = match field_mapping {
2850                    FieldMapping::Simple(t) => (t.as_str(), None, None, None),
2851                    FieldMapping::Structured(s) => (
2852                        s.target.as_str(),
2853                        self.table(s.enum_map.as_ref(), s.code_list.as_deref()),
2854                        s.also_target.as_deref(),
2855                        self.table(s.also_enum_map.as_ref(), s.also_code_list.as_deref()),
2856                    ),
2857                    FieldMapping::Nested(_) => continue,
2858                };
2859                if target.is_empty() {
2860                    continue;
2861                }
2862
2863                let parts: Vec<&str> = path.split('.').collect();
2864                let (seg_tag, path_qualifier, _occ) = parse_tag_qualifier(parts[0]);
2865                let (element_idx, component_idx) = Self::parse_element_component(&parts[1..]);
2866                // Same predicate, and the same two qualifiers, as the pre-split
2867                // path in `extract_fields_from_instance`: the field key's own
2868                // qualifier selects the schema variant, the discriminator's only
2869                // where the key has none. Asking with one merged qualifier — as
2870                // this did — calls `cav[Z30]`'s device number a code field and
2871                // decorates it, which the pre-split path never did.
2872                let disc_qualifier = Self::discriminator_qualifier_for_tag(def, &seg_tag);
2873                if code_lookup
2874                    .enrichment_codes(
2875                        source_path,
2876                        &seg_tag,
2877                        path_qualifier,
2878                        disc_qualifier.as_deref(),
2879                        element_idx,
2880                        component_idx,
2881                    )
2882                    .is_none()
2883                {
2884                    continue;
2885                }
2886
2887                sites.entry(entity_key.clone()).or_default().push(CodeSite {
2888                    target,
2889                    parent_field: def.meta.parent_field.as_deref(),
2890                    source_path,
2891                    seg_tag,
2892                    path_qualifier: path_qualifier.map(str::to_string),
2893                    disc_qualifier,
2894                    element_idx,
2895                    component_idx,
2896                    enum_map,
2897                    also_target,
2898                    also_enum_map,
2899                });
2900            }
2901        }
2902        sites
2903    }
2904
2905    /// Descend through the result, enriching every object that sits under a key
2906    /// naming an entity this engine maps.
2907    fn walk_and_enrich(
2908        engine: &MappingEngine,
2909        value: &mut serde_json::Value,
2910        sites: &HashMap<String, Vec<CodeSite<'_>>>,
2911    ) {
2912        match value {
2913            serde_json::Value::Object(map) => {
2914                for (key, child) in map.iter_mut() {
2915                    if let Some(entity_sites) = sites.get(key.as_str()) {
2916                        Self::apply_sites(engine, child, entity_sites);
2917                    }
2918                    Self::walk_and_enrich(engine, child, sites);
2919                }
2920            }
2921            serde_json::Value::Array(items) => {
2922                for item in items.iter_mut() {
2923                    Self::walk_and_enrich(engine, item, sites);
2924                }
2925            }
2926            _ => {}
2927        }
2928    }
2929
2930    /// Apply one entity's code sites to an entity value (an object, or an array
2931    /// of them when the group repeats).
2932    fn apply_sites(engine: &MappingEngine, value: &mut serde_json::Value, sites: &[CodeSite<'_>]) {
2933        match value {
2934            serde_json::Value::Array(items) => {
2935                for item in items.iter_mut() {
2936                    Self::apply_sites(engine, item, sites);
2937                }
2938            }
2939            serde_json::Value::Object(_) => {
2940                for site in sites {
2941                    match site.parent_field {
2942                        None => engine.enrich_one(value, site),
2943                        Some(field) => {
2944                            if let Some(nested) = value.get_mut(field) {
2945                                Self::apply_nested_site(engine, nested, site);
2946                            }
2947                        }
2948                    }
2949                }
2950            }
2951            _ => {}
2952        }
2953    }
2954
2955    /// Apply one nested site to every element of the `parent_field` array.
2956    fn apply_nested_site(
2957        engine: &MappingEngine,
2958        value: &mut serde_json::Value,
2959        site: &CodeSite<'_>,
2960    ) {
2961        match value {
2962            serde_json::Value::Array(items) => {
2963                for item in items.iter_mut() {
2964                    Self::apply_nested_site(engine, item, site);
2965                }
2966            }
2967            serde_json::Value::Object(_) => engine.enrich_one(value, site),
2968            _ => {}
2969        }
2970    }
2971
2972    /// Enrich a single position, if it currently holds a plain string.
2973    fn enrich_one(&self, entity: &mut serde_json::Value, site: &CodeSite<'_>) {
2974        // A list target (`werte[].code`) enriches the key in every element.
2975        if let Some((list, sub)) = list_target(site.target) {
2976            if let Some(items) = entity.get_mut(list).and_then(|v| v.as_array_mut()) {
2977                let element_site = CodeSite {
2978                    target: sub,
2979                    ..site.clone()
2980                };
2981                for item in items {
2982                    self.enrich_one(item, &element_site);
2983                }
2984            }
2985            return;
2986        }
2987        let Some(ref code_lookup) = self.code_lookup else {
2988            return;
2989        };
2990        // Already an object means another definition enriched this position.
2991        let Some(mapped_val) = Self::read_plain_string(entity, site.target) else {
2992            return;
2993        };
2994
2995        // Recover the EDIFACT value: the schema's codes are raw ("293"), while
2996        // the JSON holds the enum_map target ("BDEW").
2997        let raw = match site.enum_map {
2998            None => mapped_val.clone(),
2999            Some(map) => {
3000                let joint = match (site.also_target, site.also_enum_map) {
3001                    (Some(also), Some(also_map)) => {
3002                        Self::read_plain_string(entity, also).and_then(|also_v| {
3003                            map.iter()
3004                                .find(|(code, bo4e_v)| {
3005                                    *bo4e_v == &mapped_val && also_map.get(*code) == Some(&also_v)
3006                                })
3007                                .map(|(code, _)| code.clone())
3008                        })
3009                    }
3010                    _ => None,
3011                };
3012                joint
3013                    .or_else(|| {
3014                        map.iter()
3015                            .find(|(_, bo4e_v)| *bo4e_v == &mapped_val)
3016                            .map(|(code, _)| code.clone())
3017                    })
3018                    .unwrap_or_else(|| mapped_val.clone())
3019            }
3020        };
3021
3022        let Some(codes) = code_lookup.enrichment_codes(
3023            site.source_path,
3024            &site.seg_tag,
3025            site.path_qualifier.as_deref(),
3026            site.disc_qualifier.as_deref(),
3027            site.element_idx,
3028            site.component_idx,
3029        ) else {
3030            return;
3031        };
3032
3033        // Class C: PID self-reference stays a plain string.
3034        if let Some(ref pid) = self.current_pid {
3035            if codes.len() == 1 && codes.contains_key(pid.as_str()) {
3036                return;
3037            }
3038        }
3039
3040        let enrichment = codes.get(&raw);
3041        let meaning = enrichment
3042            .map(|e| serde_json::Value::String(e.meaning.clone()))
3043            .unwrap_or(serde_json::Value::Null);
3044
3045        let mut obj = serde_json::Map::new();
3046        obj.insert("code".into(), serde_json::json!(mapped_val));
3047        obj.insert("meaning".into(), meaning);
3048        if let Some(enum_key) = enrichment.and_then(|e| e.enum_key.as_ref()) {
3049            obj.insert("enum".into(), serde_json::json!(enum_key));
3050        }
3051
3052        if let serde_json::Value::Object(map) = entity {
3053            set_nested_value_json(map, site.target, serde_json::Value::Object(obj));
3054        }
3055    }
3056
3057    /// The string at a dotted target path, or `None` when it is absent or has
3058    /// already been replaced by an enrichment object.
3059    fn read_plain_string(entity: &serde_json::Value, target: &str) -> Option<String> {
3060        let mut current = entity;
3061        for part in target.split('.') {
3062            current = current.get(part)?;
3063        }
3064        current.as_str().map(str::to_string)
3065    }
3066
3067    /// Map an assembled tree into message-level and transaction-level results.
3068    ///
3069    /// - `msg_engine`: MappingEngine loaded with message-level definitions (SG2, SG3, root segments)
3070    /// - `tx_engine`: MappingEngine loaded with transaction-level definitions (relative to SG4)
3071    /// - `tree`: The assembled tree for one message
3072    /// - `transaction_group`: The group ID that represents transactions (e.g., "SG4")
3073    ///
3074    /// Returns a `MappedMessage` with message stammdaten and per-transaction results.
3075    pub fn map_interchange(
3076        msg_engine: &MappingEngine,
3077        tx_engine: &MappingEngine,
3078        tree: &AssembledTree,
3079        transaction_group: &str,
3080        enrich_codes: bool,
3081    ) -> crate::model::MappedMessage {
3082        let mut mapped =
3083            Self::translate_edifact_to_bo4e(msg_engine, tx_engine, tree, transaction_group);
3084        if enrich_codes {
3085            Self::enrich_bo4e_types(msg_engine, tx_engine, &mut mapped);
3086        }
3087        mapped
3088    }
3089
3090    /// The translation itself, with enrichment still inlined in the extraction.
3091    ///
3092    /// Retained so the split can be proven equivalent: `map_interchange_inner`
3093    /// with `enrich_codes = true` must produce exactly what
3094    /// `translate_edifact_to_bo4e` followed by `enrich_bo4e_types` produces.
3095    /// See `enrich_split_parity_test`.
3096    /// Test-only door onto the pre-split path, so the parity gate can compare
3097    /// the two. Not part of the public pipeline.
3098    #[doc(hidden)]
3099    pub fn map_interchange_inner_for_test(
3100        msg_engine: &MappingEngine,
3101        tx_engine: &MappingEngine,
3102        tree: &AssembledTree,
3103        transaction_group: &str,
3104        enrich_codes: bool,
3105    ) -> crate::model::MappedMessage {
3106        Self::map_interchange_inner(msg_engine, tx_engine, tree, transaction_group, enrich_codes)
3107    }
3108
3109    pub(crate) fn map_interchange_inner(
3110        msg_engine: &MappingEngine,
3111        tx_engine: &MappingEngine,
3112        tree: &AssembledTree,
3113        transaction_group: &str,
3114        enrich_codes: bool,
3115    ) -> crate::model::MappedMessage {
3116        // Map message-level entities (also captures nesting info)
3117        let (stammdaten, nesting_info) = msg_engine.map_all_forward_inner(tree, enrich_codes);
3118
3119        // Find the transaction group and map each repetition
3120        let transaktionen = tree
3121            .groups
3122            .iter()
3123            .find(|g| g.group_id == transaction_group)
3124            .map(|sg| {
3125                sg.repetitions
3126                    .iter()
3127                    .map(|instance| {
3128                        // Wrap the instance in its group so that definitions with
3129                        // source_group paths like "SG4.SG5" can resolve correctly.
3130                        let wrapped_tree = AssembledTree {
3131                            segments: vec![],
3132                            groups: vec![AssembledGroup {
3133                                group_id: transaction_group.to_string(),
3134                                repetitions: vec![instance.clone()],
3135                            }],
3136                            post_group_start: 0,
3137                            inter_group_segments: std::collections::BTreeMap::new(),
3138                        };
3139
3140                        // Pass the transaction_group into the tx_engine so its direct
3141                        // children (Marktlokation etc.) stay top-level peers of
3142                        // Prozessdaten rather than nested under it.
3143                        let (tx_result, tx_nesting) = tx_engine.map_all_forward_inner_with_tx(
3144                            &wrapped_tree,
3145                            enrich_codes,
3146                            Some(transaction_group),
3147                        );
3148
3149                        // Split the transaction's own metadata out of its
3150                        // business objects. The engine maps `Prozessdaten` like
3151                        // any other entity; it just does not belong among the
3152                        // BOs once mapped.
3153                        let mut tx_result = tx_result;
3154                        let transaktionsdaten = crate::model::take_entity(
3155                            &mut tx_result,
3156                            crate::model::TX_METADATA_ENTITY,
3157                        );
3158
3159                        crate::model::MappedTransaktion {
3160                            stammdaten: tx_result,
3161                            transaktionsdaten,
3162                            nesting_info: tx_nesting,
3163                        }
3164                    })
3165                    .collect()
3166            })
3167            .unwrap_or_default();
3168
3169        // Same split one level up: `Nachricht` is metadata about the message.
3170        let mut stammdaten = stammdaten;
3171        let nachricht_meta =
3172            crate::model::take_entity(&mut stammdaten, crate::model::MSG_METADATA_ENTITY);
3173
3174        crate::model::MappedMessage {
3175            stammdaten,
3176            nachricht_meta,
3177            transaktionen,
3178            nesting_info,
3179            inter_group_segments: tree.inter_group_segments.clone(),
3180        }
3181    }
3182
3183    /// Reverse-map a `MappedMessage` back to an `AssembledTree`.
3184    ///
3185    /// Two-engine approach mirroring `map_interchange()`:
3186    /// - `msg_engine` handles message-level stammdaten → SG2/SG3 groups
3187    /// - `tx_engine` handles per-transaction stammdaten → SG4 instances
3188    ///
3189    /// All entities (including prozessdaten/nachricht) are in `tx.stammdaten`.
3190    /// Results are merged into one `AssembledGroupInstance` per transaction,
3191    /// collected into an SG4 `AssembledGroup`, then combined with message-level groups.
3192    pub fn map_interchange_reverse(
3193        msg_engine: &MappingEngine,
3194        tx_engine: &MappingEngine,
3195        mapped: &crate::model::MappedMessage,
3196        transaction_group: &str,
3197        filtered_mig: Option<&MigSchema>,
3198    ) -> AssembledTree {
3199        // Step 1: Reverse message-level stammdaten.
3200        //
3201        // The message's metadata entity goes back in here first: the forward
3202        // pass split `Nachricht` out into its own slot, but the definitions
3203        // resolve against one flat entity map, so without this the BGM/DTM
3204        // segments it feeds cannot be rebuilt. Clone only when there is
3205        // metadata to restore — keeps the common path zero-copy.
3206        let _owned_msg: Option<serde_json::Value>;
3207        let msg_stammdaten = if !mapped.nachricht_meta.is_null() {
3208            let mut merged = mapped.stammdaten.clone();
3209            crate::model::restore_entity(
3210                &mut merged,
3211                crate::model::MSG_METADATA_ENTITY,
3212                &mapped.nachricht_meta,
3213            );
3214            _owned_msg = Some(merged);
3215            _owned_msg.as_ref().unwrap()
3216        } else {
3217            _owned_msg = None;
3218            &mapped.stammdaten
3219        };
3220
3221        let msg_tree = msg_engine.map_all_reverse_with_mig(
3222            msg_stammdaten,
3223            if mapped.nesting_info.is_empty() {
3224                None
3225            } else {
3226                Some(&mapped.nesting_info)
3227            },
3228            filtered_mig,
3229        );
3230
3231        // Step 2: Build transaction instances from each Transaktion
3232        let mut sg4_reps: Vec<AssembledGroupInstance> = Vec::new();
3233
3234        // Collect all definitions with their relative paths and sort by depth.
3235        // Shallower paths (SG8) must be processed before deeper ones (SG8:0.SG10)
3236        // so that parent group repetitions exist before children are added.
3237        struct DefWithMeta<'a> {
3238            def: &'a MappingDefinition,
3239            relative: String,
3240            depth: usize,
3241        }
3242
3243        let mut sorted_defs: Vec<DefWithMeta> = tx_engine
3244            .definitions
3245            .iter()
3246            // `parent_field` children are reversed with their parent object
3247            // (see `reverse_nested_children`).
3248            .filter(|def| def.meta.parent_field.is_none())
3249            .map(|def| {
3250                let relative = strip_tx_group_prefix(&def.meta.source_group, transaction_group);
3251                let depth = if relative.is_empty() {
3252                    0
3253                } else {
3254                    relative.chars().filter(|c| *c == '.').count() + 1
3255                };
3256                DefWithMeta {
3257                    def,
3258                    relative,
3259                    depth,
3260                }
3261            })
3262            .collect();
3263
3264        // Build parent source_path → rep_index map from deeper definitions.
3265        // SG10 defs like "SG4.SG8:0.SG10" with source_path "sg4.sg8_z79.sg10"
3266        // tell us that the SG8 def with source_path "sg4.sg8_z79" should be rep 0.
3267        let mut parent_rep_map: std::collections::HashMap<String, usize> =
3268            std::collections::HashMap::new();
3269        for dm in &sorted_defs {
3270            if dm.depth >= 2 {
3271                let parts: Vec<&str> = dm.relative.split('.').collect();
3272                let (_, parent_rep) = parse_group_spec(parts[0]);
3273                if let Some(rep_idx) = parent_rep {
3274                    if let Some(sp) = &dm.def.meta.source_path {
3275                        if let Some((parent_path, _)) = sp.rsplit_once('.') {
3276                            parent_rep_map
3277                                .entry(parent_path.to_string())
3278                                .or_insert(rep_idx);
3279                        }
3280                    }
3281                }
3282            }
3283        }
3284
3285        // Augment shallow definitions with explicit rep indices from the map,
3286        // but only for single-rep cases (no multi-rep — those use dynamic tracking).
3287        for dm in &mut sorted_defs {
3288            if dm.depth == 1 && !dm.relative.contains(':') {
3289                if let Some(sp) = &dm.def.meta.source_path {
3290                    if let Some(rep_idx) = parent_rep_map.get(sp.as_str()) {
3291                        dm.relative = format!("{}:{}", dm.relative, rep_idx);
3292                    }
3293                }
3294            }
3295        }
3296
3297        // Sort: shallower depth first, so SG8 defs create reps before SG8:N.SG10 defs.
3298        // Within same depth, sort by MIG group position (if available) for correct emission order,
3299        // falling back to alphabetical relative path for deterministic ordering.
3300        //
3301        // For variant groups (SG8 with Z01/Z03/Z07 etc.), use per-variant MIG positions
3302        // extracted from each definition's source_path qualifier suffix (e.g., "sg4.sg8_z01" → "Z01").
3303        if let Some(mig) = filtered_mig {
3304            let mig_order = build_reverse_mig_group_order(mig, transaction_group);
3305            sorted_defs.sort_by(|a, b| {
3306                a.depth.cmp(&b.depth).then_with(|| {
3307                    let a_id = a.relative.split(':').next().unwrap_or(&a.relative);
3308                    let b_id = b.relative.split(':').next().unwrap_or(&b.relative);
3309                    // Try per-variant lookup from source_path (e.g., "sg4.sg8_z01" → "SG8_Z01")
3310                    let a_pos = variant_mig_position(a.def, a_id, &mig_order);
3311                    let b_pos = variant_mig_position(b.def, b_id, &mig_order);
3312                    a_pos.cmp(&b_pos).then(a.relative.cmp(&b.relative))
3313                })
3314            });
3315        } else {
3316            sorted_defs.sort_by(|a, b| a.depth.cmp(&b.depth).then(a.relative.cmp(&b.relative)));
3317        }
3318
3319        for tx in &mapped.transaktionen {
3320            let mut root_segs: Vec<AssembledSegment> = Vec::new();
3321            let mut child_groups: Vec<AssembledGroup> = Vec::new();
3322
3323            // `transaktionsdaten` is merged back for the same reason as the
3324            // message's metadata above — the definitions expect one flat map.
3325            let _owned_tx: Option<serde_json::Value>;
3326            let tx_stammdaten: &serde_json::Value = if !tx.transaktionsdaten.is_null() {
3327                let mut merged = tx.stammdaten.clone();
3328                crate::model::restore_entity(
3329                    &mut merged,
3330                    crate::model::TX_METADATA_ENTITY,
3331                    &tx.transaktionsdaten,
3332                );
3333                _owned_tx = Some(merged);
3334                _owned_tx.as_ref().unwrap()
3335            } else {
3336                _owned_tx = None;
3337                &tx.stammdaten
3338            };
3339
3340            // Track source_path → repetition indices for parent groups (top-down).
3341            // Built during depth-1 processing, used by depth-2+ defs without
3342            // explicit rep indices to find their correct parent via source_path.
3343            // Vec<usize> supports multi-rep parents (e.g., two SG8+ZF3 reps).
3344            let mut source_path_to_rep: std::collections::HashMap<String, Vec<usize>> =
3345                std::collections::HashMap::new();
3346
3347            for dm in &sorted_defs {
3348                // Determine the BO4E value to reverse-map from.
3349                // Check top level first, then nested under parent entity.
3350                let entity_key = to_camel_case(&dm.def.meta.entity);
3351                let _tx_extracted: Option<serde_json::Value>;
3352                let bo4e_value = if let Some(v) = tx_stammdaten.get(&entity_key) {
3353                    _tx_extracted = None;
3354                    v
3355                } else if dm.def.meta.source_group.contains('.') {
3356                    match extract_child_from_parent(tx_stammdaten, &tx_engine.definitions, dm.def) {
3357                        Some(v) => {
3358                            _tx_extracted = Some(v);
3359                            _tx_extracted.as_ref().unwrap()
3360                        }
3361                        None => continue,
3362                    }
3363                } else {
3364                    continue;
3365                };
3366
3367                // Support map-keyed entities from typed PID format (same logic as map_all_reverse).
3368                let unwrapped_value: Option<serde_json::Value>;
3369                let bo4e_value = if bo4e_value.is_object() && !bo4e_value.is_array() {
3370                    if let Some(disc_value) = dm
3371                        .def
3372                        .meta
3373                        .discriminator
3374                        .as_deref()
3375                        .and_then(|d| d.split_once('='))
3376                        .map(|(_, v)| v)
3377                    {
3378                        if let Some(inner) = bo4e_value.get(disc_value) {
3379                            let mut injected = inner.clone();
3380                            if let Some(qualifier_field) = find_qualifier_companion_field(
3381                                &tx_engine.definitions,
3382                                &dm.def.meta.entity,
3383                            ) {
3384                                if let Some(obj) = injected.as_object_mut() {
3385                                    obj.entry(qualifier_field).or_insert_with(|| {
3386                                        serde_json::Value::String(disc_value.to_string())
3387                                    });
3388                                }
3389                            }
3390                            unwrapped_value = Some(injected);
3391                            unwrapped_value.as_ref().unwrap()
3392                        } else {
3393                            bo4e_value
3394                        }
3395                    } else if is_map_keyed_object(bo4e_value) {
3396                        let map = bo4e_value.as_object().unwrap();
3397                        let arr: Vec<serde_json::Value> = map
3398                            .iter()
3399                            .map(|(key, val)| {
3400                                let mut item = val.clone();
3401                                if let Some(obj) = item.as_object_mut() {
3402                                    if let Some(qualifier_field) = find_qualifier_companion_field(
3403                                        &tx_engine.definitions,
3404                                        &dm.def.meta.entity,
3405                                    ) {
3406                                        let entry = obj
3407                                            .entry(qualifier_field)
3408                                            .or_insert(serde_json::Value::Null);
3409                                        if entry.is_null() {
3410                                            *entry = serde_json::Value::String(key.clone());
3411                                        }
3412                                    }
3413                                }
3414                                item
3415                            })
3416                            .collect();
3417                        unwrapped_value = Some(serde_json::Value::Array(arr));
3418                        unwrapped_value.as_ref().unwrap()
3419                    } else {
3420                        bo4e_value
3421                    }
3422                } else {
3423                    bo4e_value
3424                };
3425
3426                // Handle array entities: each element becomes a separate group rep.
3427                // This supports both the NAD/SG12 pattern (multiple qualifiers) and
3428                // the multi-rep pattern (e.g., two LOC+Z17 Messlokationen).
3429                let items: Vec<&serde_json::Value> = if bo4e_value.is_array() {
3430                    bo4e_value.as_array().unwrap().iter().collect()
3431                } else {
3432                    vec![bo4e_value]
3433                };
3434
3435                for (item_idx, item) in items.iter().enumerate() {
3436                    let instance = tx_engine.map_reverse(item, dm.def);
3437
3438                    // Skip empty instances (definition had no real BO4E data)
3439                    if instance.segments.is_empty() && instance.child_groups.is_empty() {
3440                        continue;
3441                    }
3442
3443                    if dm.relative.is_empty() {
3444                        // The definition maps the transaction group itself
3445                        // (CONTRL's SG1, UTILMD's SG4): its segments are the
3446                        // instance's own root segments. Children it nested with
3447                        // `parent_field` (CONTRL SG1.SG2, the UCS/UCD errors of
3448                        // this checked message) are already built as child
3449                        // groups of that instance and must travel with it.
3450                        root_segs.extend(instance.segments);
3451                        for child in instance.child_groups {
3452                            match child_groups
3453                                .iter_mut()
3454                                .find(|g| g.group_id == child.group_id)
3455                            {
3456                                Some(existing) => existing.repetitions.extend(child.repetitions),
3457                                None => child_groups.push(child),
3458                            }
3459                        }
3460                    } else {
3461                        // For depth-2+ defs without explicit rep index, resolve
3462                        // parent rep from source_path matching (qualifier-based).
3463                        // item_idx selects the correct parent rep for multi-rep entities.
3464                        let effective_relative = if dm.depth >= 2 {
3465                            // Multi-rep: strip hardcoded parent :N indices so
3466                            // resolve_child_relative uses source_path lookup instead.
3467                            let rel = if items.len() > 1 {
3468                                strip_all_rep_indices(&dm.relative)
3469                            } else {
3470                                dm.relative.clone()
3471                            };
3472                            // Use tx nesting info for multi-rep arrays, BUT skip it
3473                            // when source_path is present and resolves to a single
3474                            // parent rep. In that case, nesting_info indices (from the
3475                            // original tree) may not match the reverse tree's rep layout.
3476                            // resolve_child_relative uses reverse-tree source_path_to_rep
3477                            // which is always correct.
3478                            let skip_nesting = dm
3479                                .def
3480                                .meta
3481                                .source_path
3482                                .as_ref()
3483                                .and_then(|sp| sp.rsplit_once('.'))
3484                                .and_then(|(parent_path, _)| source_path_to_rep.get(parent_path))
3485                                .is_some_and(|reps| reps.len() == 1);
3486                            let nesting_idx = if items.len() > 1 && !skip_nesting {
3487                                dm.def
3488                                    .meta
3489                                    .source_path
3490                                    .as_ref()
3491                                    .and_then(|sp| tx.nesting_info.get(sp))
3492                                    .and_then(|dist| dist.get(item_idx))
3493                                    .copied()
3494                            } else {
3495                                None
3496                            };
3497                            if let Some(parent_rep) = nesting_idx {
3498                                // Direct placement using known nesting distribution
3499                                let parts: Vec<&str> = rel.split('.').collect();
3500                                let parent_id = parts[0].split(':').next().unwrap_or(parts[0]);
3501                                let rest = parts[1..].join(".");
3502                                format!("{}:{}.{}", parent_id, parent_rep, rest)
3503                            } else {
3504                                resolve_child_relative(
3505                                    &rel,
3506                                    dm.def.meta.source_path.as_deref(),
3507                                    &source_path_to_rep,
3508                                    item_idx,
3509                                )
3510                            }
3511                        } else if dm.depth == 1 {
3512                            // Depth-1: use nesting_info child indices for correct
3513                            // rep placement (preserves original interleaving order).
3514                            let child_key = dm
3515                                .def
3516                                .meta
3517                                .source_path
3518                                .as_ref()
3519                                .map(|sp| format!("{sp}#child"));
3520                            if let Some(child_indices) =
3521                                child_key.as_ref().and_then(|ck| tx.nesting_info.get(ck))
3522                            {
3523                                if let Some(&target) = child_indices.get(item_idx) {
3524                                    if target != usize::MAX {
3525                                        let base =
3526                                            dm.relative.split(':').next().unwrap_or(&dm.relative);
3527                                        format!("{}:{}", base, target)
3528                                    } else {
3529                                        dm.relative.clone()
3530                                    }
3531                                } else if items.len() > 1 && item_idx > 0 {
3532                                    strip_rep_index(&dm.relative)
3533                                } else {
3534                                    dm.relative.clone()
3535                                }
3536                            } else if items.len() > 1 && item_idx > 0 {
3537                                strip_rep_index(&dm.relative)
3538                            } else {
3539                                dm.relative.clone()
3540                            }
3541                        } else if items.len() > 1 && item_idx > 0 {
3542                            // Multi-rep entity with hardcoded :N index: first item uses
3543                            // the original index, subsequent items append (strip :N).
3544                            strip_rep_index(&dm.relative)
3545                        } else {
3546                            dm.relative.clone()
3547                        };
3548
3549                        let rep_used =
3550                            place_in_groups(&mut child_groups, &effective_relative, instance);
3551
3552                        // Track source_path → rep_index for depth-1 (parent) defs
3553                        if dm.depth == 1 {
3554                            if let Some(sp) = &dm.def.meta.source_path {
3555                                source_path_to_rep
3556                                    .entry(sp.clone())
3557                                    .or_default()
3558                                    .push(rep_used);
3559                            }
3560                        }
3561                    }
3562                }
3563            }
3564
3565            sg4_reps.push(AssembledGroupInstance {
3566                segments: root_segs,
3567                child_groups,
3568                entry_mig_number: None,
3569                variant_mig_numbers: vec![],
3570                skipped_segments: Vec::new(),
3571                skipped_positions: Vec::new(),
3572            });
3573        }
3574
3575        // Step 3: Combine message tree with transaction group.
3576        // Move UNS section separator from root segments to inter_group_segments.
3577        // UNS+D (detail) goes BEFORE the tx group (MSCONS: header/detail boundary).
3578        // UNS+S (summary) goes AFTER the tx group (ORDERS: detail/summary boundary).
3579        // Any segments that follow UNS in the sequence (e.g., summary MOA in REMADV)
3580        // are also placed in inter_group_segments alongside UNS.
3581        let mut root_segments = Vec::new();
3582        let mut uns_segments = Vec::new();
3583        let mut uns_is_summary = false;
3584        let mut found_uns = false;
3585        for seg in msg_tree.segments {
3586            if seg.tag == "UNS" {
3587                // Check if this is UNS+S (summary separator) vs UNS+D (detail separator)
3588                uns_is_summary = seg
3589                    .elements
3590                    .first()
3591                    .and_then(|el| el.first())
3592                    .map(|v| v == "S")
3593                    .unwrap_or(false);
3594                uns_segments.push(seg);
3595                found_uns = true;
3596            } else if found_uns {
3597                // Segments after UNS belong in the same inter_group position
3598                uns_segments.push(seg);
3599            } else {
3600                root_segments.push(seg);
3601            }
3602        }
3603
3604        let pre_group_count = root_segments.len();
3605        let mut all_groups = msg_tree.groups;
3606        let mut inter_group = msg_tree.inter_group_segments;
3607
3608        // Helper: parse SG number from group_id (e.g., "SG26" → 26).
3609        let sg_num = |id: &str| -> usize {
3610            id.strip_prefix("SG")
3611                .and_then(|n| n.parse::<usize>().ok())
3612                .unwrap_or(0)
3613        };
3614
3615        if !sg4_reps.is_empty() {
3616            if uns_is_summary {
3617                // UNS+S: place AFTER the transaction group (detail/summary boundary)
3618                all_groups.push(AssembledGroup {
3619                    group_id: transaction_group.to_string(),
3620                    repetitions: sg4_reps,
3621                });
3622                if !uns_segments.is_empty() {
3623                    // Sort groups by SG number so the disassembler emits them
3624                    // in MIG order.  Insert UNS right after the tx_group —
3625                    // any groups with higher SG numbers (e.g., SG50/SG52 in
3626                    // INVOIC) are post-UNS summary groups.
3627                    all_groups.sort_by_key(|g| sg_num(&g.group_id));
3628                    let tx_num = sg_num(transaction_group);
3629                    let uns_pos = all_groups
3630                        .iter()
3631                        .rposition(|g| sg_num(&g.group_id) <= tx_num)
3632                        .map(|i| i + 1)
3633                        .unwrap_or(all_groups.len());
3634                    inter_group.insert(uns_pos, uns_segments);
3635                }
3636            } else {
3637                // UNS+D: place BEFORE the transaction group (header/detail boundary)
3638                if !uns_segments.is_empty() {
3639                    inter_group.insert(all_groups.len(), uns_segments);
3640                }
3641                all_groups.push(AssembledGroup {
3642                    group_id: transaction_group.to_string(),
3643                    repetitions: sg4_reps,
3644                });
3645            }
3646        } else if !uns_segments.is_empty() {
3647            if transaction_group.is_empty() {
3648                // Truly message-only (tx_group=""): UNS is a section separator.
3649                // UNS+S (summary) goes AFTER all groups — e.g., ORDCHG UNS+S
3650                // follows SG1 (NAD+CTA+COM) groups.
3651                // UNS+D (detail) goes BEFORE groups.
3652                all_groups.sort_by_key(|g| sg_num(&g.group_id));
3653                if uns_is_summary {
3654                    inter_group.insert(all_groups.len(), uns_segments);
3655                } else {
3656                    inter_group.insert(0, uns_segments);
3657                }
3658            } else {
3659                // Has a tx_group but no tx reps (e.g., INVOIC PID 31004
3660                // Storno — no SG26 data).  Sort groups and insert UNS after
3661                // the last group with SG number ≤ tx_group number.
3662                all_groups.sort_by_key(|g| sg_num(&g.group_id));
3663                let tx_num = sg_num(transaction_group);
3664                let uns_pos = all_groups
3665                    .iter()
3666                    .rposition(|g| sg_num(&g.group_id) <= tx_num)
3667                    .map(|i| i + 1)
3668                    .unwrap_or(all_groups.len());
3669                inter_group.insert(uns_pos, uns_segments);
3670            }
3671        }
3672
3673        // Restore inter_group_segments captured during forward mapping
3674        // (e.g. PID-foreign top-level segments preserved by the assembler's
3675        // skip-unknown mode — see `Assembler::assemble_generic`). Without
3676        // this, BO4E forward + reverse drops anything not represented in a
3677        // TOML mapping definition. We append rather than overwrite so the
3678        // UNS placement computed above survives — same-key collisions are
3679        // rare in practice (UNS goes at well-known positions).
3680        for (k, segs) in &mapped.inter_group_segments {
3681            if segs.is_empty() {
3682                continue;
3683            }
3684            let existing_tags: std::collections::HashSet<String> = inter_group
3685                .get(k)
3686                .map(|v| v.iter().map(|s| s.tag.clone()).collect())
3687                .unwrap_or_default();
3688            for seg in segs {
3689                if existing_tags.contains(&seg.tag) {
3690                    continue;
3691                }
3692                inter_group.entry(*k).or_default().push(seg.clone());
3693            }
3694        }
3695
3696        let mut tree = AssembledTree {
3697            segments: root_segments,
3698            groups: all_groups,
3699            post_group_start: pre_group_count,
3700            inter_group_segments: inter_group,
3701        };
3702
3703        // Order repetitions of same-ID group variants (SG2 NAD+MS / NAD+MR,
3704        // SG12 NAD+Z07 / NAD+Z08, SG10 CCI variants, …) by MIG variant order.
3705        // The reps above were appended in definition order and, within one
3706        // definition, in the order of the BO4E JSON array — which carries no
3707        // ordering information. The transaction group itself keeps its order:
3708        // the order of transactions is data.
3709        if let Some(mig) = filtered_mig {
3710            mig_assembly::repetition_order::sort_repetitions_by_mig_variant(
3711                &mut tree,
3712                mig,
3713                (!transaction_group.is_empty()).then_some(transaction_group),
3714            );
3715        }
3716        tree
3717    }
3718
3719    /// Build an assembled group from BO4E values and a definition.
3720    pub fn build_group_from_bo4e(
3721        &self,
3722        bo4e_value: &serde_json::Value,
3723        def: &MappingDefinition,
3724    ) -> AssembledGroup {
3725        let instance = self.map_reverse(bo4e_value, def);
3726        let leaf_group = def
3727            .meta
3728            .source_group
3729            .rsplit('.')
3730            .next()
3731            .unwrap_or(&def.meta.source_group);
3732
3733        AssembledGroup {
3734            group_id: leaf_group.to_string(),
3735            repetitions: vec![instance],
3736        }
3737    }
3738
3739    /// Forward-map an assembled tree to a typed interchange.
3740    ///
3741    /// Runs the dynamic mapping pipeline, wraps the result with metadata,
3742    /// then converts via JSON serialization into the caller's typed structs.
3743    ///
3744    /// - `M`: message-level stammdaten type (e.g., `Pid55001MsgStammdaten`)
3745    /// - `T`: transaction-level stammdaten type (e.g., `Pid55001TxStammdaten`)
3746    pub fn map_interchange_typed<M, T>(
3747        msg_engine: &MappingEngine,
3748        tx_engine: &MappingEngine,
3749        tree: &AssembledTree,
3750        tx_group: &str,
3751        enrich_codes: bool,
3752        nachrichtendaten: crate::model::Nachrichtendaten,
3753        interchangedaten: crate::model::Interchangedaten,
3754    ) -> Result<crate::model::Interchange<M, T>, serde_json::Error>
3755    where
3756        M: serde::de::DeserializeOwned,
3757        T: serde::de::DeserializeOwned,
3758    {
3759        let mapped = Self::map_interchange(msg_engine, tx_engine, tree, tx_group, enrich_codes);
3760        let nachricht = mapped.into_dynamic_nachricht(nachrichtendaten);
3761        let dynamic = crate::model::DynamicInterchange {
3762            interchangedaten,
3763            nachrichten: vec![nachricht],
3764        };
3765        let value = serde_json::to_value(&dynamic)?;
3766        serde_json::from_value(value)
3767    }
3768
3769    /// Reverse-map a typed interchange nachricht back to an assembled tree.
3770    ///
3771    /// Serializes the typed struct to JSON, then runs the dynamic reverse pipeline.
3772    ///
3773    /// - `M`: message-level stammdaten type
3774    /// - `T`: transaction-level stammdaten type
3775    pub fn map_interchange_reverse_typed<M, T>(
3776        msg_engine: &MappingEngine,
3777        tx_engine: &MappingEngine,
3778        nachricht: &crate::model::Nachricht<M, T>,
3779        tx_group: &str,
3780    ) -> Result<AssembledTree, serde_json::Error>
3781    where
3782        M: serde::Serialize,
3783        T: serde::Serialize,
3784    {
3785        // The reverse resolves definitions against one flat entity map, so both
3786        // metadata slots go back where the mappings expect to find them.
3787        let mut stammdaten = serde_json::to_value(&nachricht.stammdaten)?;
3788        crate::model::restore_message_metadata(&mut stammdaten, &nachricht.nachrichtendaten);
3789        let transaktionen: Vec<crate::model::MappedTransaktion> = nachricht
3790            .transaktionen
3791            .iter()
3792            .map(|t| {
3793                Ok(crate::model::MappedTransaktion {
3794                    stammdaten: serde_json::to_value(t)?,
3795                    transaktionsdaten: serde_json::Value::Null,
3796                    nesting_info: Default::default(),
3797                })
3798            })
3799            .collect::<Result<Vec<_>, serde_json::Error>>()?;
3800        let mapped = crate::model::MappedMessage {
3801            stammdaten,
3802            nachricht_meta: serde_json::Value::Null,
3803            transaktionen,
3804            nesting_info: Default::default(),
3805            inter_group_segments: Default::default(),
3806        };
3807        Ok(Self::map_interchange_reverse(
3808            msg_engine, tx_engine, &mapped, tx_group, None,
3809        ))
3810    }
3811}
3812
3813/// Parse a group path part with optional repetition: "SG8:1" → ("SG8", Some(1)).
3814/// Parse a source_path part into (group_id, optional_qualifier).
3815///
3816/// `"sg8_z98"` → `("sg8", Some("z98"))`
3817/// `"sg4"` → `("sg4", None)`
3818/// `"sg10"` → `("sg10", None)`
3819fn parse_source_path_part(part: &str) -> (&str, Option<&str>) {
3820    // Find the first underscore that separates group from qualifier.
3821    // Source path parts look like "sg8_z98", "sg4", "sg10", "sg12_z04".
3822    // The group ID is always "sgN", so the underscore after the digits is the separator.
3823    if let Some(pos) = part.find('_') {
3824        let group = &part[..pos];
3825        let qualifier = &part[pos + 1..];
3826        if !qualifier.is_empty() {
3827            return (group, Some(qualifier));
3828        }
3829    }
3830    (part, None)
3831}
3832
3833/// Build a map from group ID (e.g., "SG5", "SG8") to its position index
3834/// within the transaction group's nested_groups Vec.
3835/// Used by `map_interchange_reverse` to sort definitions in MIG order.
3836///
3837/// For variant groups (same ID with variant_code set, e.g., SG8 with Z01, Z03, Z07),
3838/// stores per-variant positions (e.g., "SG8_Z01" → 0, "SG8_Z03" → 1) so that
3839/// definitions are sorted in MIG XML order rather than alphabetical qualifier order.
3840fn build_reverse_mig_group_order(mig: &MigSchema, tx_group_id: &str) -> HashMap<String, usize> {
3841    let mut order = HashMap::new();
3842    if let Some(tg) = mig.segment_groups.iter().find(|g| g.id == tx_group_id) {
3843        for (i, nested) in tg.nested_groups.iter().enumerate() {
3844            // For variant groups, store per-variant key (e.g., "SG8_Z01" → i)
3845            if let Some(ref vc) = nested.variant_code {
3846                let variant_key = format!("{}_{}", nested.id, vc.to_uppercase());
3847                order.insert(variant_key, i);
3848            }
3849            // Always store base group ID for fallback
3850            order.entry(nested.id.clone()).or_insert(i);
3851        }
3852    }
3853    order
3854}
3855
3856/// Extract the MIG position for a definition, using per-variant lookup when possible.
3857///
3858/// For a definition with source_path "sg4.sg8_z01", extracts the variant qualifier "Z01"
3859/// and looks up "SG8_Z01" in the MIG order map. Falls back to the base group ID (e.g., "SG8")
3860/// if no variant qualifier is found or if the per-variant key isn't in the map.
3861fn variant_mig_position(
3862    def: &MappingDefinition,
3863    base_group_id: &str,
3864    mig_order: &HashMap<String, usize>,
3865) -> usize {
3866    // Try to extract variant qualifier from source_path.
3867    // source_path like "sg4.sg8_z01" or "sg4.sg8_z01.sg10" — we want the part matching base_group_id.
3868    if let Some(ref sp) = def.meta.source_path {
3869        // Find the path segment matching the base group (e.g., "sg8_z01" for base "SG8")
3870        let base_lower = base_group_id.to_lowercase();
3871        for part in sp.split('.') {
3872            if part.starts_with(&base_lower)
3873                || part.starts_with(base_group_id.to_lowercase().as_str())
3874            {
3875                // Extract qualifier suffix: "sg8_z01" → "z01"
3876                if let Some(underscore_pos) = part.find('_') {
3877                    let qualifier = &part[underscore_pos + 1..];
3878                    let variant_key = format!("{}_{}", base_group_id, qualifier.to_uppercase());
3879                    if let Some(&pos) = mig_order.get(&variant_key) {
3880                        return pos;
3881                    }
3882                }
3883            }
3884        }
3885    }
3886    // Fallback to base group position
3887    mig_order.get(base_group_id).copied().unwrap_or(usize::MAX)
3888}
3889
3890/// Find a group repetition whose entry segment has a matching qualifier.
3891///
3892/// The entry segment is the first segment in the instance (e.g., SEQ for SG8).
3893/// The qualifier is matched against `elements[0][0]` (case-insensitive).
3894fn find_rep_by_entry_qualifier<'a>(
3895    reps: &'a [AssembledGroupInstance],
3896    qualifier: &str,
3897) -> Option<&'a AssembledGroupInstance> {
3898    // Support compound qualifiers like "za1_za2" — match any part.
3899    let parts: Vec<&str> = qualifier.split('_').collect();
3900    reps.iter().find(|inst| {
3901        inst.segments.first().is_some_and(|seg| {
3902            seg.elements
3903                .first()
3904                .and_then(|e| e.first())
3905                .is_some_and(|v| parts.iter().any(|part| v.eq_ignore_ascii_case(part)))
3906        })
3907    })
3908}
3909
3910/// Find ALL repetitions whose entry segment qualifier matches (case-insensitive).
3911fn find_all_reps_by_entry_qualifier<'a>(
3912    reps: &'a [AssembledGroupInstance],
3913    qualifier: &str,
3914) -> Vec<&'a AssembledGroupInstance> {
3915    // Support compound qualifiers like "za1_za2" — match any part.
3916    let parts: Vec<&str> = qualifier.split('_').collect();
3917    reps.iter()
3918        .filter(|inst| {
3919            inst.segments.first().is_some_and(|seg| {
3920                seg.elements
3921                    .first()
3922                    .and_then(|e| e.first())
3923                    .is_some_and(|v| parts.iter().any(|part| v.eq_ignore_ascii_case(part)))
3924            })
3925        })
3926        .collect()
3927}
3928
3929/// Check if a source_path contains qualifier suffixes (e.g., "sg8_z98").
3930fn has_source_path_qualifiers(source_path: &str) -> bool {
3931    source_path.split('.').any(|part| {
3932        if let Some(pos) = part.find('_') {
3933            pos < part.len() - 1
3934        } else {
3935            false
3936        }
3937    })
3938}
3939
3940fn parse_group_spec(part: &str) -> (&str, Option<usize>) {
3941    if let Some(colon_pos) = part.find(':') {
3942        let id = &part[..colon_pos];
3943        let rep = part[colon_pos + 1..].parse::<usize>().ok();
3944        (id, rep)
3945    } else {
3946        (part, None)
3947    }
3948}
3949
3950/// Strip the transaction group prefix from a source_group path.
3951///
3952/// Given `source_group = "SG4.SG8:0.SG10"` and `tx_group = "SG4"`,
3953/// returns `"SG8:0.SG10"`.
3954/// Given `source_group = "SG4"` and `tx_group = "SG4"`, returns `""`.
3955fn strip_tx_group_prefix(source_group: &str, tx_group: &str) -> String {
3956    if source_group == tx_group || source_group.is_empty() {
3957        String::new()
3958    } else if let Some(rest) = source_group.strip_prefix(tx_group) {
3959        rest.strip_prefix('.').unwrap_or(rest).to_string()
3960    } else {
3961        source_group.to_string()
3962    }
3963}
3964
3965/// Place a reverse-mapped group instance into the correct nesting position.
3966///
3967/// `relative_path` is the group path relative to the transaction group:
3968/// - `"SG5"` → top-level child group
3969/// - `"SG8:0.SG10"` → SG10 inside SG8 repetition 0
3970///
3971/// Returns the repetition index used at the first nesting level.
3972fn place_in_groups(
3973    groups: &mut Vec<AssembledGroup>,
3974    relative_path: &str,
3975    instance: AssembledGroupInstance,
3976) -> usize {
3977    let parts: Vec<&str> = relative_path.split('.').collect();
3978
3979    if parts.len() == 1 {
3980        // Leaf group: "SG5", "SG8", "SG12", or with explicit index "SG8:0"
3981        let (id, rep) = parse_group_spec(parts[0]);
3982
3983        // Find or create the group
3984        let group = if let Some(g) = groups.iter_mut().find(|g| g.group_id == id) {
3985            g
3986        } else {
3987            groups.push(AssembledGroup {
3988                group_id: id.to_string(),
3989                repetitions: vec![],
3990            });
3991            groups.last_mut().unwrap()
3992        };
3993
3994        if let Some(rep_idx) = rep {
3995            // Explicit index: place at specific position, merging into existing
3996            while group.repetitions.len() <= rep_idx {
3997                group.repetitions.push(AssembledGroupInstance {
3998                    segments: vec![],
3999                    child_groups: vec![],
4000                    entry_mig_number: None,
4001                    variant_mig_numbers: vec![],
4002                    skipped_segments: Vec::new(),
4003                    skipped_positions: Vec::new(),
4004                });
4005            }
4006            group.repetitions[rep_idx]
4007                .segments
4008                .extend(instance.segments);
4009            group.repetitions[rep_idx]
4010                .child_groups
4011                .extend(instance.child_groups);
4012            rep_idx
4013        } else {
4014            // No index: append new repetition
4015            let pos = group.repetitions.len();
4016            group.repetitions.push(instance);
4017            pos
4018        }
4019    } else {
4020        // Nested path: e.g., "SG8:0.SG10" → place SG10 inside SG8 rep 0
4021        let (parent_id, parent_rep) = parse_group_spec(parts[0]);
4022        let rep_idx = parent_rep.unwrap_or(0);
4023
4024        // Find or create the parent group
4025        let parent_group = if let Some(g) = groups.iter_mut().find(|g| g.group_id == parent_id) {
4026            g
4027        } else {
4028            groups.push(AssembledGroup {
4029                group_id: parent_id.to_string(),
4030                repetitions: vec![],
4031            });
4032            groups.last_mut().unwrap()
4033        };
4034
4035        // Ensure the target repetition exists (extend with empty instances if needed)
4036        while parent_group.repetitions.len() <= rep_idx {
4037            parent_group.repetitions.push(AssembledGroupInstance {
4038                segments: vec![],
4039                child_groups: vec![],
4040                entry_mig_number: None,
4041                variant_mig_numbers: vec![],
4042                skipped_segments: Vec::new(),
4043                skipped_positions: Vec::new(),
4044            });
4045        }
4046
4047        let remaining = parts[1..].join(".");
4048        place_in_groups(
4049            &mut parent_group.repetitions[rep_idx].child_groups,
4050            &remaining,
4051            instance,
4052        );
4053        rep_idx
4054    }
4055}
4056
4057/// Resolve the effective relative path for a child definition (depth >= 2).
4058///
4059/// If the child's relative already has an explicit parent rep index (e.g., "SG8:5.SG10"),
4060/// use it as-is. Otherwise, use the `source_path` to look up the parent's actual
4061/// repetition index from `source_path_to_rep`.
4062///
4063/// `item_idx` selects which parent rep to use when the parent created multiple reps
4064/// (e.g., two SG8 reps with ZF3 → item_idx 0 picks the first, 1 picks the second).
4065///
4066/// Example: relative = "SG8.SG10", source_path = "sg4.sg8_zf3.sg10"
4067/// → looks up "sg4.sg8_zf3" in map → finds reps [3, 4] → item_idx=1 → returns "SG8:4.SG10"
4068fn resolve_child_relative(
4069    relative: &str,
4070    source_path: Option<&str>,
4071    source_path_to_rep: &std::collections::HashMap<String, Vec<usize>>,
4072    item_idx: usize,
4073) -> String {
4074    let parts: Vec<&str> = relative.split('.').collect();
4075    if parts.is_empty() {
4076        return relative.to_string();
4077    }
4078
4079    // If first part already has explicit index, keep as-is
4080    let (parent_id, parent_rep) = parse_group_spec(parts[0]);
4081    if parent_rep.is_some() {
4082        return relative.to_string();
4083    }
4084
4085    // Try to resolve from source_path: extract parent path and look up its rep
4086    if let Some(sp) = source_path {
4087        if let Some((parent_path, _child)) = sp.rsplit_once('.') {
4088            // Exact match first.
4089            if let Some(rep_indices) = source_path_to_rep.get(parent_path) {
4090                let rep_idx = rep_indices
4091                    .get(item_idx)
4092                    .or_else(|| rep_indices.last())
4093                    .copied()
4094                    .unwrap_or(0);
4095                let rest = parts[1..].join(".");
4096                return format!("{}:{}.{}", parent_id, rep_idx, rest);
4097            }
4098            // Fallback: variant wildcard. When TOMLs use a flat parent path
4099            // like "sg4" but the schema splits it into variants (e.g. sg4_su,
4100            // sg4_z10..z21), union the reps from every matching variant so a
4101            // per-item iteration can place each child under its own parent.
4102            // `PidSchemaIndex::has_group` already accepts this style for
4103            // forward mapping — reverse mapping needs the same or children
4104            // from all-but-one variant get dropped (PARTIN 12 SG4 reps).
4105            let prefix = format!("{}_", parent_path);
4106            let mut unioned: Vec<usize> = source_path_to_rep
4107                .iter()
4108                .filter(|(k, _)| k.starts_with(&prefix))
4109                .flat_map(|(_, v)| v.iter().copied())
4110                .collect();
4111            if !unioned.is_empty() {
4112                unioned.sort_unstable();
4113                unioned.dedup();
4114                let rep_idx = unioned
4115                    .get(item_idx)
4116                    .or_else(|| unioned.last())
4117                    .copied()
4118                    .unwrap_or(0);
4119                let rest = parts[1..].join(".");
4120                return format!("{}:{}.{}", parent_id, rep_idx, rest);
4121            }
4122        }
4123    }
4124
4125    // No resolution possible, keep original
4126    relative.to_string()
4127}
4128
4129/// Parsed discriminator for filtering assembled group instances.
4130///
4131/// Discriminator format: "TAG.element_idx.component_idx=VALUE" or
4132/// "TAG.element_idx.component_idx=VAL1|VAL2" (pipe-separated multi-value).
4133/// E.g., "LOC.0.0=Z17" → match LOC segments where elements[0][0] == "Z17"
4134/// E.g., "RFF.0.0=Z49|Z53" → match RFF where elements[0][0] is Z49 OR Z53
4135struct DiscriminatorMatcher<'a> {
4136    tag: &'a str,
4137    element_idx: usize,
4138    component_idx: usize,
4139    expected_values: Vec<&'a str>,
4140    /// Optional occurrence index: `#N` selects the Nth match among instances.
4141    occurrence: Option<usize>,
4142}
4143
4144impl<'a> DiscriminatorMatcher<'a> {
4145    fn parse(disc: &'a str) -> Option<Self> {
4146        let (spec, expected) = disc.split_once('=')?;
4147        let parts: Vec<&str> = spec.split('.').collect();
4148        if parts.len() != 3 {
4149            return None;
4150        }
4151        let (expected_raw, occurrence) = parse_discriminator_occurrence(expected);
4152        Some(Self {
4153            tag: parts[0],
4154            element_idx: parts[1].parse().ok()?,
4155            component_idx: parts[2].parse().ok()?,
4156            expected_values: expected_raw.split('|').collect(),
4157            occurrence,
4158        })
4159    }
4160
4161    fn matches(&self, instance: &AssembledGroupInstance) -> bool {
4162        instance.segments.iter().any(|s| {
4163            s.tag.eq_ignore_ascii_case(self.tag)
4164                && s.elements
4165                    .get(self.element_idx)
4166                    .and_then(|e| e.get(self.component_idx))
4167                    .map(|v| self.expected_values.iter().any(|ev| v == ev))
4168                    .unwrap_or(false)
4169        })
4170    }
4171
4172    /// Filter instances, respecting the occurrence index if present.
4173    fn filter_instances<'b>(
4174        &self,
4175        instances: Vec<&'b AssembledGroupInstance>,
4176    ) -> Vec<&'b AssembledGroupInstance> {
4177        let matching: Vec<_> = instances
4178            .into_iter()
4179            .filter(|inst| self.matches(inst))
4180            .collect();
4181        if let Some(occ) = self.occurrence {
4182            matching.into_iter().nth(occ).into_iter().collect()
4183        } else {
4184            matching
4185        }
4186    }
4187}
4188
4189/// Parse an optional occurrence index from a discriminator expected value.
4190///
4191/// `"TN#1"` → `("TN", Some(1))` — select the 2nd matching rep
4192/// `"TN"`   → `("TN", None)` — select all matching reps
4193/// `"Z13|Z14#0"` → `("Z13|Z14", Some(0))` — first match among Z13 or Z14
4194fn parse_discriminator_occurrence(expected: &str) -> (&str, Option<usize>) {
4195    if let Some(hash_pos) = expected.rfind('#') {
4196        if let Ok(occ) = expected[hash_pos + 1..].parse::<usize>() {
4197            return (&expected[..hash_pos], Some(occ));
4198        }
4199    }
4200    (expected, None)
4201}
4202
4203/// Strip explicit rep index from a relative path: "SG5:4" → "SG5", "SG8:3" → "SG8".
4204/// Used for multi-rep entities where subsequent items should append rather than
4205/// merge into the same rep position.
4206fn strip_rep_index(relative: &str) -> String {
4207    let (id, _) = parse_group_spec(relative);
4208    id.to_string()
4209}
4210
4211/// Strip all explicit rep indices from a multi-part relative path:
4212/// "SG8:3.SG10" → "SG8.SG10", "SG8:3.SG10:0" → "SG8.SG10".
4213/// Used for multi-rep depth-2+ entities so resolve_child_relative uses
4214/// source_path lookup instead of hardcoded indices.
4215pub(crate) fn strip_all_rep_indices(relative: &str) -> String {
4216    relative
4217        .split('.')
4218        .map(|part| {
4219            let (id, _) = parse_group_spec(part);
4220            id
4221        })
4222        .collect::<Vec<_>>()
4223        .join(".")
4224}
4225
4226// ── Nested child groups (`[meta] parent_field`) ──
4227
4228/// Whether `child` is a `parent_field` definition nested directly below `parent`
4229/// (which may itself be a `parent_field` definition — nesting can span several
4230/// group levels, one `parent_field` per level):
4231/// same entity, `source_group` exactly one level deeper, and (when both carry a
4232/// `source_path`) a structurally compatible parent path. Qualifiers on the parent
4233/// part are compared only when both sides specify one; the instance-level check
4234/// is [`nested_parent_qualifier`] + [`entry_qualifier_matches`].
4235pub fn is_nested_child_of(child: &MappingDefinition, parent: &MappingDefinition) -> bool {
4236    if child.meta.parent_field.is_none() || child.meta.entity != parent.meta.entity {
4237        return false;
4238    }
4239    let child_sg = strip_all_rep_indices(&child.meta.source_group);
4240    let parent_sg = strip_all_rep_indices(&parent.meta.source_group);
4241    match child_sg.rsplit_once('.') {
4242        Some((head, _)) if head.eq_ignore_ascii_case(&parent_sg) => {}
4243        _ => return false,
4244    }
4245    let (Some(child_sp), Some(parent_sp)) = (
4246        child.meta.source_path.as_deref(),
4247        parent.meta.source_path.as_deref(),
4248    ) else {
4249        return true;
4250    };
4251    let Some((child_parent_sp, _)) = child_sp.rsplit_once('.') else {
4252        return false;
4253    };
4254    let child_parts: Vec<&str> = child_parent_sp.split('.').collect();
4255    let parent_parts: Vec<&str> = parent_sp.split('.').collect();
4256    child_parts.len() == parent_parts.len()
4257        && child_parts.iter().zip(&parent_parts).all(|(c, p)| {
4258            let (c_id, c_q) = parse_source_path_part(c);
4259            let (p_id, p_q) = parse_source_path_part(p);
4260            c_id.eq_ignore_ascii_case(p_id)
4261                && match (c_q, p_q) {
4262                    (Some(cq), Some(pq)) => cq.eq_ignore_ascii_case(pq),
4263                    _ => true,
4264                }
4265        })
4266}
4267
4268/// Entry qualifier the parent group instance must carry for a nested child
4269/// definition to apply (e.g. `"z08"` for `source_path = "sg4.sg12_z08.sg13"`).
4270fn nested_parent_qualifier(child: &MappingDefinition) -> Option<&str> {
4271    let (parent_path, _) = child.meta.source_path.as_deref()?.rsplit_once('.')?;
4272    let last = parent_path.rsplit('.').next()?;
4273    parse_source_path_part(last).1
4274}
4275
4276/// Leaf group id and optional entry qualifier of a nested child definition
4277/// (e.g. `("SG13", None)` for `source_group = "SG4.SG12.SG13"`).
4278fn nested_child_leaf(child: &MappingDefinition) -> (String, Option<&str>) {
4279    let leaf_group = strip_all_rep_indices(
4280        child
4281            .meta
4282            .source_group
4283            .rsplit('.')
4284            .next()
4285            .unwrap_or(&child.meta.source_group),
4286    );
4287    let leaf_qualifier = child
4288        .meta
4289        .source_path
4290        .as_deref()
4291        .and_then(|sp| sp.rsplit('.').next())
4292        .and_then(|part| parse_source_path_part(part).1);
4293    (leaf_group, leaf_qualifier)
4294}
4295
4296/// Whether the instance's entry segment (its first segment) carries `qualifier`
4297/// at `elements[0][0]`. Compound qualifiers (`"z53_z54"`) match any part.
4298fn entry_qualifier_matches(instance: &AssembledGroupInstance, qualifier: &str) -> bool {
4299    segment_qualifier_matches(instance.segments.first(), qualifier)
4300}
4301
4302/// [`entry_qualifier_matches`] for a group repetition rebuilt by the reverse
4303/// mapping from `def`. Its segments follow the order of `def`'s fields, so the
4304/// entry segment need not come first (e.g. `PIA` listed before `SEQ`). When
4305/// `def` has a discriminator, its segment tag names the entry segment.
4306fn rebuilt_entry_qualifier_matches(
4307    instance: &AssembledGroupInstance,
4308    def: &MappingDefinition,
4309    qualifier: &str,
4310) -> bool {
4311    let entry_tag = def
4312        .meta
4313        .discriminator
4314        .as_deref()
4315        .and_then(|d| d.split('.').next())
4316        .filter(|tag| !tag.is_empty());
4317    let entry = match entry_tag {
4318        Some(tag) => instance
4319            .segments
4320            .iter()
4321            .find(|s| s.tag.eq_ignore_ascii_case(tag)),
4322        None => instance.segments.first(),
4323    };
4324    segment_qualifier_matches(entry, qualifier)
4325}
4326
4327fn segment_qualifier_matches(segment: Option<&AssembledSegment>, qualifier: &str) -> bool {
4328    segment
4329        .and_then(|seg| seg.elements.first())
4330        .and_then(|e| e.first())
4331        .is_some_and(|v| qualifier.split('_').any(|q| v.eq_ignore_ascii_case(q)))
4332}
4333
4334/// Whether a `when_filled` guard's field carries data: a string (or an enriched
4335/// `{code, …}` object), or a non-empty array or object. A group whose content
4336/// sits in nested children (`parent_field`, e.g. `zuordnungen` from SG10) has
4337/// nothing else to name — its entry segment must still be written when they
4338/// are there, or the group cannot be rendered from data the AHB allows.
4339fn field_is_filled(bo4e_value: &serde_json::Value, field: &str) -> bool {
4340    let mut current = bo4e_value;
4341    for part in field.split('.') {
4342        match current.get(part) {
4343            Some(v) => current = v,
4344            None => return false,
4345        }
4346    }
4347    match current {
4348        serde_json::Value::String(s) => !s.is_empty(),
4349        serde_json::Value::Array(a) => !a.is_empty(),
4350        serde_json::Value::Object(o) => !o.is_empty(),
4351        serde_json::Value::Number(_) | serde_json::Value::Bool(_) => true,
4352        serde_json::Value::Null => false,
4353    }
4354}
4355
4356/// A list target `name[].sub` → `("name", "sub")`: the field writes `sub` of
4357/// one element of the array `name` per matching segment.
4358pub(crate) fn list_target(target: &str) -> Option<(&str, &str)> {
4359    let (list, sub) = target.split_once("[].")?;
4360    (!list.is_empty() && !sub.is_empty()).then_some((list, sub))
4361}
4362
4363/// Parse a segment tag with optional qualifier and occurrence index.
4364///
4365/// - `"dtm[92]"`    → `("DTM", Some("92"), 0)` — first (default) occurrence
4366/// - `"rff[Z34,1]"` → `("RFF", Some("Z34"), 1)` — second occurrence (0-indexed)
4367/// - `"rff[Z34,*]"` → `("RFF", Some("Z34"), 0)` — wildcard occurrence
4368/// - `"rff"`         → `("RFF", None, 0)`
4369pub(crate) fn parse_tag_qualifier(tag_part: &str) -> (String, Option<&str>, usize) {
4370    if let Some(bracket_start) = tag_part.find('[') {
4371        let tag = tag_part[..bracket_start].to_uppercase();
4372        let inner = tag_part[bracket_start + 1..].trim_end_matches(']');
4373        if let Some(comma_pos) = inner.find(',') {
4374            let qualifier = &inner[..comma_pos];
4375            let index = inner[comma_pos + 1..].parse::<usize>().unwrap_or(0);
4376            // "*" wildcard means no qualifier filter — positional access only
4377            if qualifier == "*" {
4378                (tag, None, index)
4379            } else {
4380                (tag, Some(qualifier), index)
4381            }
4382        } else {
4383            (tag, Some(inner), 0)
4384        }
4385    } else {
4386        (tag_part.to_uppercase(), None, 0)
4387    }
4388}
4389
4390/// Deep-merge a BO4E value into the result map.
4391///
4392/// If the entity already exists as an object, new fields are merged in
4393/// (existing fields are NOT overwritten). This allows multiple TOML
4394/// definitions with the same `entity` name to contribute fields to one object.
4395pub fn deep_merge_insert(
4396    result: &mut serde_json::Map<String, serde_json::Value>,
4397    entity: &str,
4398    bo4e: serde_json::Value,
4399) {
4400    merge_entity(result, entity, bo4e, false);
4401}
4402
4403/// [`deep_merge_insert`], choosing what happens when the two values do not
4404/// line up (an array meets an object, or arrays of different lengths):
4405/// `keep_both` appends them as separate repetitions, otherwise the new value
4406/// replaces the old.
4407///
4408/// Keeping both is right for *sibling* groups sharing an entity — two LOC+Z16
4409/// (an array) and one LOC+Z22 (an object) are three `Marktlokation`
4410/// repetitions, and replacing dropped both MaLos without an error. It is wrong
4411/// for a group and its own flat child rule (an SG8 and its SG10 on one entity
4412/// without `parent_field`): their values are one repetition's fields, and
4413/// appending the child rows would turn each into an SG8 repetition of its own.
4414fn merge_entity(
4415    result: &mut serde_json::Map<String, serde_json::Value>,
4416    entity: &str,
4417    bo4e: serde_json::Value,
4418    keep_both: bool,
4419) {
4420    if let Some(existing) = result.get_mut(entity) {
4421        // Array + Array: element-wise merge (same entity from multiple TOML defs,
4422        // each producing an array for multi-rep groups like two LOC+Z17).
4423        if let (Some(existing_arr), Some(new_arr)) =
4424            (existing.as_array().map(|a| a.len()), bo4e.as_array())
4425        {
4426            if existing_arr == new_arr.len() {
4427                let existing_arr = existing.as_array_mut().unwrap();
4428                for (existing_elem, new_elem) in existing_arr.iter_mut().zip(new_arr) {
4429                    if let (Some(existing_map), Some(new_map)) =
4430                        (existing_elem.as_object_mut(), new_elem.as_object())
4431                    {
4432                        for (k, v) in new_map {
4433                            if let Some(existing_v) = existing_map.get_mut(k) {
4434                                if let (Some(existing_inner), Some(new_inner)) =
4435                                    (existing_v.as_object_mut(), v.as_object())
4436                                {
4437                                    for (ik, iv) in new_inner {
4438                                        existing_inner
4439                                            .entry(ik.clone())
4440                                            .or_insert_with(|| iv.clone());
4441                                    }
4442                                }
4443                            } else {
4444                                existing_map.insert(k.clone(), v.clone());
4445                            }
4446                        }
4447                    }
4448                }
4449                return;
4450            }
4451        }
4452        // Object + Object: field-level merge
4453        if let (Some(existing_map), serde_json::Value::Object(new_map)) =
4454            (existing.as_object_mut(), &bo4e)
4455        {
4456            for (k, v) in new_map {
4457                if let Some(existing_v) = existing_map.get_mut(k) {
4458                    // Recursively merge nested objects (e.g., companion types)
4459                    if let (Some(existing_inner), Some(new_inner)) =
4460                        (existing_v.as_object_mut(), v.as_object())
4461                    {
4462                        for (ik, iv) in new_inner {
4463                            existing_inner
4464                                .entry(ik.clone())
4465                                .or_insert_with(|| iv.clone());
4466                        }
4467                    }
4468                    // Don't overwrite existing scalar/array values
4469                } else {
4470                    existing_map.insert(k.clone(), v.clone());
4471                }
4472            }
4473            return;
4474        }
4475        if !keep_both {
4476            result.insert(entity.to_string(), bo4e);
4477            return;
4478        }
4479        // Shapes that do not line up are different repetitions: keep them all.
4480        let existing_items = match std::mem::take(existing) {
4481            serde_json::Value::Array(items) => items,
4482            other => vec![other],
4483        };
4484        let new_items = match bo4e {
4485            serde_json::Value::Array(items) => items,
4486            other => vec![other],
4487        };
4488        *existing = serde_json::Value::Array(existing_items.into_iter().chain(new_items).collect());
4489        return;
4490    }
4491    result.insert(entity.to_string(), bo4e);
4492}
4493
4494/// Append a definition's per-repetition output to a **list-valued field** on an
4495/// entity — the write half of `MappingMeta::target_list`.
4496///
4497/// This cannot go through `deep_merge_insert`, which documents that it does
4498/// "not overwrite existing scalar/array values". That rule is right for ordinary
4499/// fields and wrong here: a list field is the one place where several
4500/// definitions are *expected* to contribute to the same key (four separate
4501/// `Obis*` definitions all feed `zaehlwerke`), and under `deep_merge_insert`
4502/// every contribution after the first would be dropped without a trace.
4503///
4504/// Empty elements are skipped so an absent optional group does not leave a
4505/// `[{}]` behind, which would reverse into a phantom segment.
4506fn append_to_list_field(
4507    result: &mut serde_json::Map<String, serde_json::Value>,
4508    entity: &str,
4509    list_field: &str,
4510    bo4e: serde_json::Value,
4511) {
4512    let mut items = match bo4e {
4513        serde_json::Value::Array(a) => a,
4514        other => vec![other],
4515    };
4516    items.retain(|v| !v.as_object().is_some_and(|o| o.is_empty()));
4517    if items.is_empty() {
4518        return;
4519    }
4520    let entry = result
4521        .entry(entity.to_string())
4522        .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
4523    // An entity carrying a list field is a single object. If it is already an
4524    // array, some other definition made it multi-rep and the two shapes are
4525    // incompatible — leave it alone rather than corrupt it silently.
4526    let Some(obj) = entry.as_object_mut() else {
4527        return;
4528    };
4529    match obj.get_mut(list_field).and_then(|v| v.as_array_mut()) {
4530        Some(existing) => existing.extend(items),
4531        None => {
4532            obj.insert(list_field.to_string(), serde_json::Value::Array(items));
4533        }
4534    }
4535}
4536
4537/// Convert a PascalCase name to camelCase by lowering the first character.
4538///
4539/// E.g., `"Ansprechpartner"` → `"ansprechpartner"`,
4540/// `"AnsprechpartnerEdifact"` → `"ansprechpartnerEdifact"`,
4541/// `"ProduktpaketPriorisierung"` → `"produktpaketPriorisierung"`.
4542/// Detect whether a JSON object looks like a map-keyed entity (typed PID format).
4543///
4544/// Map-keyed objects have short uppercase/alphanumeric keys that look like qualifier
4545/// codes (e.g., `{"Z04": {...}, "Z09": {...}}` or `{"MS": {...}, "MR": {...}}`),
4546/// as opposed to normal field-name objects (e.g., `{"name1": "...", "adresse": {...}}`).
4547fn is_map_keyed_object(value: &serde_json::Value) -> bool {
4548    let Some(obj) = value.as_object() else {
4549        return false;
4550    };
4551    if obj.is_empty() {
4552        return false;
4553    }
4554    // All keys must be short (≤5 chars), uppercase/digit only, and all values must be objects
4555    obj.iter().all(|(k, v)| {
4556        k.len() <= 5
4557            && k.chars()
4558                .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
4559            && v.is_object()
4560    })
4561}
4562
4563/// Find the BO4E companion field name used for the qualifier/discriminator
4564/// across definitions that share the same entity name.
4565///
4566/// For example, if `Geschaeftspartner` has a definition with discriminator
4567/// `NAD.0.0=Z04` and companion field `nad.0.0 → nadQualifier`, this returns
4568/// `Some("nadQualifier")`.
4569///
4570/// Used to inject map keys into inner objects when converting map-keyed entities.
4571fn find_qualifier_companion_field(
4572    definitions: &[crate::definition::MappingDefinition],
4573    entity: &str,
4574) -> Option<String> {
4575    for def in definitions {
4576        if def.meta.entity != *entity || def.meta.parent_field.is_some() {
4577            continue;
4578        }
4579        let disc = def.meta.discriminator.as_deref()?;
4580        let (disc_path, _) = disc.split_once('=')?;
4581        let disc_path_lower = disc_path.to_lowercase();
4582
4583        // Search [fields] for the qualifier field (e.g., Marktteilnehmer has
4584        // "marktrolle" in [fields]).
4585        for (path, mapping) in &def.fields {
4586            let cf_path = path.to_lowercase();
4587            let matches = cf_path == disc_path_lower || format!("{}.0", cf_path) == disc_path_lower;
4588            if matches {
4589                let target = match mapping {
4590                    FieldMapping::Simple(t) => t.as_str(),
4591                    FieldMapping::Structured(s) => s.target.as_str(),
4592                    FieldMapping::Nested(_) => continue,
4593                };
4594                if !target.is_empty() {
4595                    return Some(target.to_string());
4596                }
4597            }
4598        }
4599    }
4600    None
4601}
4602
4603/// Extract a child entity from its parent entity in the reverse mapping input.
4604///
4605/// When a child entity (e.g., Kontakt with source_group="SG2.SG3") isn't found
4606/// at the top level, look inside the parent entity (e.g., Marktteilnehmer with
4607/// source_group="SG2") for a nested field matching the child's camelCase name.
4608///
4609/// For map-keyed parents ({"MS": {...}, "MR": {...}}), collects child values
4610/// from all inner objects that have the field, returning them as an array.
4611fn extract_child_from_parent(
4612    entities: &serde_json::Value,
4613    definitions: &[MappingDefinition],
4614    child_def: &MappingDefinition,
4615) -> Option<serde_json::Value> {
4616    extract_child_from_parent_with_indices(entities, definitions, child_def).map(|(v, _)| v)
4617}
4618
4619/// Like `extract_child_from_parent`, but also returns the parent rep indices
4620/// from which each child was extracted.  This allows the nesting distribution
4621/// to place child groups under the correct parent rep even when `nesting_info`
4622/// is unavailable (e.g., typed struct / manual JSON construction).
4623fn extract_child_from_parent_with_indices(
4624    entities: &serde_json::Value,
4625    definitions: &[MappingDefinition],
4626    child_def: &MappingDefinition,
4627) -> Option<(serde_json::Value, Vec<usize>)> {
4628    let parts: Vec<&str> = child_def.meta.source_group.split('.').collect();
4629    if parts.len() < 2 {
4630        return None;
4631    }
4632    let parent_group = parts[0];
4633    let parent_def = definitions
4634        .iter()
4635        .find(|d| d.meta.source_group == parent_group && d.meta.entity != child_def.meta.entity)?;
4636    let parent_key = to_camel_case(&parent_def.meta.entity);
4637    let child_key = to_camel_case(&child_def.meta.entity);
4638    let parent_value = entities.get(&parent_key)?;
4639
4640    // Map-keyed parent: collect child from each inner object
4641    if let Some(parent_map) = parent_value.as_object() {
4642        if is_map_keyed_value(parent_map) {
4643            let mut children: Vec<serde_json::Value> = Vec::new();
4644            let mut indices: Vec<usize> = Vec::new();
4645            for (i, (_key, inner)) in parent_map.iter().enumerate() {
4646                if let Some(child) = inner.get(&child_key) {
4647                    if !child.is_null() {
4648                        children.push(child.clone());
4649                        indices.push(i);
4650                    }
4651                }
4652            }
4653            return match children.len() {
4654                0 => None,
4655                1 => Some((children.into_iter().next().unwrap(), indices)),
4656                _ => Some((serde_json::Value::Array(children), indices)),
4657            };
4658        }
4659    }
4660
4661    // Array parent: collect child from each element
4662    if let Some(parent_arr) = parent_value.as_array() {
4663        let mut children: Vec<serde_json::Value> = Vec::new();
4664        let mut indices: Vec<usize> = Vec::new();
4665        for (i, item) in parent_arr.iter().enumerate() {
4666            if let Some(child) = item.get(&child_key) {
4667                if !child.is_null() {
4668                    children.push(child.clone());
4669                    indices.push(i);
4670                }
4671            }
4672        }
4673        return match children.len() {
4674            0 => None,
4675            1 => Some((children.into_iter().next().unwrap(), indices)),
4676            _ => Some((serde_json::Value::Array(children), indices)),
4677        };
4678    }
4679
4680    // Single parent object — always index 0
4681    let child = parent_value.get(&child_key)?;
4682    if child.is_null() {
4683        return None;
4684    }
4685    Some((child.clone(), vec![0]))
4686}
4687
4688/// Move child entities under their parent entities in the forward-mapped result.
4689///
4690/// For each definition with a dotted `source_group` (e.g., "SG2.SG3"), finds the
4691/// parent definition (e.g., "SG2") and moves the child entity from the top-level
4692/// result into the parent entity as a nested field.
4693fn nest_child_entities_in_result(
4694    result: &mut serde_json::Map<String, serde_json::Value>,
4695    definitions: &[MappingDefinition],
4696    nesting_info: &std::collections::HashMap<String, Vec<usize>>,
4697    transaction_group: Option<&str>,
4698) {
4699    let nesting_pairs = child_entity_nesting_pairs(definitions, transaction_group);
4700
4701    for (_parent_group, parent_entity, child_entity, child_source_path) in nesting_pairs {
4702        let parent_key = to_camel_case(&parent_entity);
4703        let child_key = to_camel_case(&child_entity);
4704
4705        // Remove child from top level (if present)
4706        let child_value = match result.remove(&child_key) {
4707            Some(v) => v,
4708            None => continue,
4709        };
4710
4711        // Get parent value.
4712        // If the parent is a plain array (not map-keyed), nesting would silently
4713        // place the child into arbitrary array elements. Skip and leave the child
4714        // at the top level where the reverse mapper can find it.
4715        let Some(parent_value) = result.get_mut(&parent_key) else {
4716            // Parent doesn't exist — put child back
4717            result.insert(child_key, child_value);
4718            continue;
4719        };
4720        if parent_value.is_array() {
4721            result.insert(child_key, child_value);
4722            continue;
4723        }
4724
4725        // Get the nesting distribution (which parent rep each child rep belongs to)
4726        let distribution = child_source_path
4727            .as_deref()
4728            .and_then(|sp| nesting_info.get(sp));
4729
4730        // Normalize child to a list of (index, value) pairs
4731        let child_items: Vec<(usize, &serde_json::Value)> = match &child_value {
4732            serde_json::Value::Array(arr) => arr.iter().enumerate().collect(),
4733            other => vec![(0, other)],
4734        };
4735
4736        // Helper: insert or append child value into a parent object field.
4737        // First call inserts the value; subsequent calls convert to array and append.
4738        let insert_or_append = |obj: &mut serde_json::Map<String, serde_json::Value>,
4739                                key: &str,
4740                                val: &serde_json::Value| {
4741            match obj.get_mut(key) {
4742                Some(existing) => {
4743                    // Convert single value to array, then push
4744                    if !existing.is_array() {
4745                        let prev = existing.take();
4746                        *existing = serde_json::Value::Array(vec![prev]);
4747                    }
4748                    if let Some(arr) = existing.as_array_mut() {
4749                        arr.push(val.clone());
4750                    }
4751                }
4752                None => {
4753                    obj.insert(key.to_string(), val.clone());
4754                }
4755            }
4756        };
4757
4758        // Handle parent as map-keyed object: {"MS": {...}, "MR": {...}}
4759        if let Some(parent_map) = parent_value.as_object_mut() {
4760            if is_map_keyed_value(parent_map) {
4761                // Map keys in insertion order correspond to rep indices
4762                let keys: Vec<String> = parent_map.keys().cloned().collect();
4763                for (i, child_item) in &child_items {
4764                    let target_idx = distribution
4765                        .and_then(|dist| dist.get(*i))
4766                        .copied()
4767                        .unwrap_or(0);
4768                    if let Some(key) = keys.get(target_idx) {
4769                        if let Some(inner) = parent_map.get_mut(key).and_then(|v| v.as_object_mut())
4770                        {
4771                            insert_or_append(inner, &child_key, child_item);
4772                        }
4773                    }
4774                }
4775                continue;
4776            }
4777        }
4778
4779        // Handle parent as array
4780        if let Some(parent_arr) = parent_value.as_array_mut() {
4781            for (i, child_item) in &child_items {
4782                let target_idx = distribution
4783                    .and_then(|dist| dist.get(*i))
4784                    .copied()
4785                    .unwrap_or(0);
4786                if let Some(parent_obj) = parent_arr
4787                    .get_mut(target_idx)
4788                    .and_then(|v| v.as_object_mut())
4789                {
4790                    insert_or_append(parent_obj, &child_key, child_item);
4791                }
4792            }
4793            continue;
4794        }
4795
4796        // Handle parent as single object
4797        if let Some(parent_obj) = parent_value.as_object_mut() {
4798            for (_i, child_item) in &child_items {
4799                insert_or_append(parent_obj, &child_key, child_item);
4800            }
4801            continue;
4802        }
4803
4804        // Fallback: put child back at top level
4805        result.insert(child_key, child_value);
4806    }
4807}
4808
4809/// Parent/child entity pairs the forward mapping nests (see
4810/// [`nest_child_entities_in_result`]): `(parent_group, parent_entity,
4811/// child_entity, child_source_path)`.
4812///
4813/// A child entity (dotted `source_group`, e.g. `SG2.SG3` Kontakt) is moved into
4814/// the object of the entity mapped from its parent group (e.g. `SG2`
4815/// Marktteilnehmer) — unless the parent group is the transaction root, the
4816/// child also has a definition at the parent level (same-entity enrichment), or
4817/// the parent maps a dotted field of the child's name.
4818pub(crate) fn child_entity_nesting_pairs(
4819    definitions: &[MappingDefinition],
4820    transaction_group: Option<&str>,
4821) -> Vec<(String, String, String, Option<String>)> {
4822    // Collect parent→child relationships from definitions.
4823    // parent_group → (parent_entity, child_entity, child_source_path)
4824    let mut nesting_pairs: Vec<(String, String, String, Option<String>)> = Vec::new();
4825    for def in definitions {
4826        let parts: Vec<&str> = def.meta.source_group.split('.').collect();
4827        if parts.len() < 2 || def.meta.parent_field.is_some() {
4828            continue;
4829        }
4830        let parent_group = parts[0];
4831        // Skip nesting when the parent group is the transaction root. SG4 in UTILMD
4832        // IS the transaction — its direct children (Marktlokation, Geschaeftspartner,
4833        // ProduktpaketDaten, …) are peers of the transaction metadata (Prozessdaten),
4834        // not sub-objects of it. Nesting still applies to other parents (e.g. SG2.SG3
4835        // Kontakt stays nested under SG2 Marktteilnehmer).
4836        if transaction_group.is_some_and(|tx| tx == parent_group) {
4837            continue;
4838        }
4839        let child_entity = def.meta.entity.clone();
4840        // Skip if the child entity also has a definition at the parent group level.
4841        // E.g., Prozessdaten at SG4.SG6 enriches Prozessdaten at SG4 via deep_merge —
4842        // this is same-entity enrichment, not a parent-child nesting relationship.
4843        let child_has_parent_level_def = definitions
4844            .iter()
4845            .any(|d| d.meta.source_group == parent_group && d.meta.entity == child_entity);
4846        if child_has_parent_level_def {
4847            continue;
4848        }
4849        // Find the parent definition (a different entity at the parent group level)
4850        let parent_entity = definitions
4851            .iter()
4852            .find(|d| d.meta.source_group == parent_group && d.meta.entity != child_entity)
4853            .map(|d| d.meta.entity.clone());
4854        if let Some(ref parent_entity) = parent_entity {
4855            // Skip nesting if the parent definition has a dotted field target
4856            // that creates a sub-object with the same name as the child entity.
4857            // E.g., Prozessdaten has "zeitscheibe.referenz" which creates
4858            // prozessdaten.zeitscheibe — collides with nesting Zeitscheibe entity.
4859            let child_key_lc = to_camel_case(&child_entity);
4860            let parent_defs: Vec<_> = definitions
4861                .iter()
4862                .filter(|d| d.meta.entity == *parent_entity)
4863                .collect();
4864            let has_conflicting_field = parent_defs.iter().any(|pd| {
4865                pd.fields.values().any(|fm| {
4866                    let target = match fm {
4867                        crate::definition::FieldMapping::Simple(t) => t.as_str(),
4868                        crate::definition::FieldMapping::Structured(s) => s.target.as_str(),
4869                        crate::definition::FieldMapping::Nested(_) => "",
4870                    };
4871                    target.starts_with(&child_key_lc)
4872                        && target.get(child_key_lc.len()..child_key_lc.len() + 1) == Some(".")
4873                })
4874            });
4875            if has_conflicting_field {
4876                continue;
4877            }
4878            // Avoid duplicates
4879            if nesting_pairs
4880                .iter()
4881                .any(|(_, pe, ce, _)| *pe == *parent_entity && *ce == child_entity)
4882            {
4883                continue;
4884            }
4885            nesting_pairs.push((
4886                parent_group.to_string(),
4887                parent_entity.clone(),
4888                child_entity,
4889                def.meta.source_path.clone(),
4890            ));
4891        }
4892    }
4893
4894    nesting_pairs
4895}
4896
4897/// Check if a JSON map looks like a map-keyed entity (short uppercase/code keys → objects).
4898fn is_map_keyed_value(map: &serde_json::Map<String, serde_json::Value>) -> bool {
4899    if map.is_empty() {
4900        return false;
4901    }
4902    map.values().all(|v| v.is_object())
4903        && map.keys().all(|k| {
4904            k.len() <= 5
4905                || k.chars()
4906                    .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
4907        })
4908}
4909
4910/// One code-field position recovered from a mapping definition: where the value
4911/// came from in EDIFACT, and where it landed in BO4E.
4912#[derive(Clone)]
4913struct CodeSite<'a> {
4914    target: &'a str,
4915    /// `[meta] parent_field`: the site is a key of an element of that array on
4916    /// the entity, not a key of the entity. Without this the split enrichment
4917    /// looks for the target directly on the carrier and finds nothing, while
4918    /// the pre-split path enriched it at write time — the two would disagree on
4919    /// every nested rule.
4920    parent_field: Option<&'a str>,
4921    source_path: &'a str,
4922    seg_tag: String,
4923    /// The qualifier on the field key itself (`cav[Z30]...`).
4924    path_qualifier: Option<String>,
4925    /// The qualifier the definition's discriminator pins for this tag.
4926    disc_qualifier: Option<String>,
4927    element_idx: usize,
4928    component_idx: usize,
4929    enum_map: Option<&'a std::collections::BTreeMap<String, String>>,
4930    also_target: Option<&'a str>,
4931    also_enum_map: Option<&'a std::collections::BTreeMap<String, String>>,
4932}
4933
4934pub(crate) fn to_camel_case(name: &str) -> String {
4935    let mut chars = name.chars();
4936    match chars.next() {
4937        Some(c) => c.to_lowercase().to_string() + chars.as_str(),
4938        None => String::new(),
4939    }
4940}
4941
4942/// Set a value in a nested JSON map using a dotted path.
4943/// E.g., "address.city" sets `{"address": {"city": "value"}}`.
4944fn set_nested_value(map: &mut serde_json::Map<String, serde_json::Value>, path: &str, val: String) {
4945    set_nested_value_json(map, path, serde_json::Value::String(val));
4946}
4947
4948/// Like `set_nested_value` but accepts a `serde_json::Value` instead of a `String`.
4949fn set_nested_value_json(
4950    map: &mut serde_json::Map<String, serde_json::Value>,
4951    path: &str,
4952    val: serde_json::Value,
4953) {
4954    if let Some((prefix, leaf)) = path.rsplit_once('.') {
4955        let mut current = map;
4956        for part in prefix.split('.') {
4957            let entry = current
4958                .entry(part.to_string())
4959                .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
4960            current = entry.as_object_mut().expect("expected object in path");
4961        }
4962        current.insert(leaf.to_string(), val);
4963    } else {
4964        map.insert(path.to_string(), val);
4965    }
4966}
4967
4968/// Precompiled cache for a single format-version/variant (e.g., FV2504/UTILMD_Strom).
4969///
4970/// Contains all engines with paths pre-resolved, ready for immediate use.
4971/// Loading one `VariantCache` file replaces thousands of individual `.bin` reads.
4972#[derive(serde::Serialize, serde::Deserialize)]
4973pub struct VariantCache {
4974    /// Message-level definitions (shared across PIDs).
4975    pub message_defs: Vec<MappingDefinition>,
4976    /// Per-PID transaction definitions (key: "pid_55001").
4977    pub transaction_defs: BTreeMap<String, Vec<MappingDefinition>>,
4978    /// Per-PID combined definitions (key: "pid_55001").
4979    pub combined_defs: BTreeMap<String, Vec<MappingDefinition>>,
4980    /// Per-PID code lookups (key: "pid_55001"). Cached to avoid reading schema JSONs at load time.
4981    #[serde(default)]
4982    pub code_lookups: BTreeMap<String, crate::code_lookup::CodeLookup>,
4983    /// Parsed MIG schema — cached to avoid re-parsing MIG XML at startup.
4984    #[serde(default)]
4985    pub mig_schema: Option<mig_types::schema::mig::MigSchema>,
4986    /// Segment element counts derived from MIG — cached for reverse mapping padding.
4987    #[serde(default)]
4988    pub segment_structure: Option<crate::segment_structure::SegmentStructure>,
4989    /// The shared code-list tables the definitions' `code_list` names resolve
4990    /// against. Not part of the cache file: the tables live once beside it, so
4991    /// `load` finds them and every engine this cache builds inherits them.
4992    /// Without that a translated code reaches the output raw.
4993    #[serde(skip)]
4994    pub code_lists: std::sync::Arc<crate::code_lists::CodeLists>,
4995    /// Per-PID AHB segment numbers (key: "pid_55001"). Used for MIG filtering at runtime.
4996    /// Eliminates the need to parse AHB XML files at startup.
4997    #[serde(default)]
4998    pub pid_segment_numbers: BTreeMap<String, Vec<String>>,
4999    /// Per-PID field requirements (key: "pid_55001"). Built from PID schema + TOML definitions.
5000    /// Used by `validate_pid()` to check field completeness.
5001    #[serde(default)]
5002    pub pid_requirements: BTreeMap<String, crate::pid_requirements::PidRequirements>,
5003    /// Per-PID pre-built AHB workflow (key: "pid_55001"). The EDIFACT-side rulebook
5004    /// (segment-path keyed), twin of `pid_requirements` (BO4E-entity keyed). Built at
5005    /// compile-mappings from the PID schema JSON so downstream consumers can run full
5006    /// raw-EDIFACT validation (`Mapper::validate_edifact`) without the schema files.
5007    #[serde(default)]
5008    pub pid_ahb_workflows: BTreeMap<String, ahb_types::AhbWorkflow>,
5009    /// Per-PID transaction group ID (key: "pid_55001", value: "SG4").
5010    /// Derived from the common `source_group` prefix of transaction definitions.
5011    /// Empty string for message-only variants (e.g., ORDCHG).
5012    #[serde(default)]
5013    pub tx_groups: BTreeMap<String, String>,
5014}
5015
5016impl VariantCache {
5017    /// Save this variant cache to a single JSON file.
5018    pub fn save(&self, path: &Path) -> Result<(), MappingError> {
5019        let encoded = serde_json::to_vec(self).map_err(|e| MappingError::CacheWrite {
5020            path: path.display().to_string(),
5021            message: e.to_string(),
5022        })?;
5023        if let Some(parent) = path.parent() {
5024            std::fs::create_dir_all(parent)?;
5025        }
5026        std::fs::write(path, encoded)?;
5027        Ok(())
5028    }
5029
5030    /// Load a variant cache from a single JSON file.
5031    pub fn load(path: &Path) -> Result<Self, MappingError> {
5032        let bytes = std::fs::read(path)?;
5033        let mut cache: Self =
5034            serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
5035                path: path.display().to_string(),
5036                message: e.to_string(),
5037            })?;
5038        cache.code_lists = crate::code_lists::CodeLists::discover(path);
5039        Ok(cache)
5040    }
5041
5042    /// Get the transaction group for a PID (e.g., "SG4" for UTILMD PIDs).
5043    /// Returns `None` if the PID is not in this variant.
5044    /// Returns `Some("")` for message-only variants (no transaction group).
5045    pub fn tx_group(&self, pid: &str) -> Option<&str> {
5046        self.tx_groups
5047            .get(&format!("pid_{pid}"))
5048            .map(|s| s.as_str())
5049    }
5050
5051    /// Build a `MappingEngine` from the message-level definitions, attaching
5052    /// the per-PID code lookup so forward mapping enriches code fields with
5053    /// `{ code, meaning, enum }` objects.
5054    pub fn msg_engine(&self, pid: &str) -> MappingEngine {
5055        let mut eng = MappingEngine::from_definitions_with_code_lists(
5056            std::sync::Arc::clone(&self.code_lists),
5057            self.message_defs.clone(),
5058        )
5059        .with_pid(pid);
5060        if let Some(cl) = self.code_lookups.get(&format!("pid_{pid}")) {
5061            eng = eng.with_code_lookup(cl.clone());
5062        }
5063        eng
5064    }
5065
5066    /// Build a `MappingEngine` from the transaction-level definitions for a PID,
5067    /// attaching the per-PID code lookup. Returns `None` if the PID is not in
5068    /// this variant.
5069    pub fn tx_engine(&self, pid: &str) -> Option<MappingEngine> {
5070        self.transaction_defs
5071            .get(&format!("pid_{pid}"))
5072            .map(|defs| {
5073                let mut eng = MappingEngine::from_definitions_with_code_lists(
5074                    std::sync::Arc::clone(&self.code_lists),
5075                    defs.clone(),
5076                )
5077                .with_pid(pid);
5078                if let Some(cl) = self.code_lookups.get(&format!("pid_{pid}")) {
5079                    eng = eng.with_code_lookup(cl.clone());
5080                }
5081                eng
5082            })
5083    }
5084
5085    /// Get a PID-filtered MIG schema.
5086    /// Returns `None` if no MIG schema or no segment numbers for this PID.
5087    ///
5088    /// Falls back to the empty-PID workflow's segment numbers when the AHB
5089    /// has no Pruefidentifikator attribute (e.g., APERAK — one workflow for
5090    /// all BGM doc codes). This lets `from_edifact` work for variants whose
5091    /// AHB doesn't enumerate per-PID segment numbers.
5092    pub fn filtered_mig(&self, pid: &str) -> Option<mig_types::schema::mig::MigSchema> {
5093        let mig = self.mig_schema.as_ref()?;
5094        let numbers = self
5095            .pid_segment_numbers
5096            .get(&format!("pid_{pid}"))
5097            .or_else(|| self.pid_segment_numbers.get("pid_"))?;
5098        let number_set: std::collections::HashSet<String> = numbers.iter().cloned().collect();
5099        Some(mig_assembly::pid_filter::filter_mig_for_pid(
5100            mig,
5101            &number_set,
5102        ))
5103    }
5104}
5105
5106/// Bundled data for a single format version (e.g., FV2504).
5107///
5108/// Contains all VariantCaches for every message type in that FV,
5109/// serialized as one bincode file for distribution via GitHub releases.
5110#[derive(serde::Serialize, serde::Deserialize)]
5111pub struct DataBundle {
5112    pub format_version: String,
5113    pub bundle_version: u32,
5114    pub variants: BTreeMap<String, VariantCache>,
5115    /// PID-agnostic BO4E type catalog (parsed from `bo4e-german` source).
5116    ///
5117    /// Populated by the bundle generator at compile-mappings time. Older bundles
5118    /// without this field deserialize to an empty catalog.
5119    #[serde(default)]
5120    pub bo4e_catalog: crate::bo4e_catalog::Bo4eCatalog,
5121
5122    /// The crate version that produced this bundle.
5123    ///
5124    /// Distinct from [`bundle_version`](Self::bundle_version), which guards the
5125    /// serialisation *format* and has been unchanged for many releases — a
5126    /// bundle can satisfy it while its mappings, schemas and code lists come
5127    /// from another era. That is not a hypothetical: a bundle five months old
5128    /// passed the format check, loaded cleanly, and rendered 12% of a message
5129    /// with no error (issue #158).
5130    ///
5131    /// `None` for bundles produced before this field existed, which is itself
5132    /// evidence of age.
5133    #[serde(default, skip_serializing_if = "Option::is_none")]
5134    pub built_by: Option<String>,
5135    /// The shared code-list tables the definitions' `code_list` names resolve
5136    /// against.
5137    ///
5138    /// Carried IN the bundle, unlike `VariantCache`, which is written beside a
5139    /// copy of `code_lists.toml` and repairs itself from it on load. A bundle
5140    /// is a bare `.bin` fetched into `~/.edifact/data` with nothing beside it,
5141    /// so a bundle that does not carry its tables cannot resolve a single
5142    /// name: forward, the EDIFACT code reaches the output untranslated;
5143    /// reverse, the BO4E name is written into the EDIFACT slot verbatim. The
5144    /// deduplication that made naming worth doing does not argue against this
5145    /// -- there is one bundle per format version, so the tables appear once.
5146    #[serde(default)]
5147    pub code_lists: crate::code_lists::CodeLists,
5148}
5149
5150impl DataBundle {
5151    pub const CURRENT_VERSION: u32 = 2;
5152
5153    /// The release a bundle built now belongs to, and the one a bundle must
5154    /// have been built by to be read.
5155    ///
5156    /// Taken from `mig-bo4e` rather than from whichever crate produces or
5157    /// consumes a bundle: `mig-bo4e` carries the workspace version, which is
5158    /// what the release process stamps, while `automapper-generator` versions
5159    /// itself separately. Reading it from the producer gave `0.1.0` against a
5160    /// consumer expecting `0.1.1` — a mismatch that is an artefact of where the
5161    /// constant was read, not of the data.
5162    pub const PRODUCING_VERSION: &'static str = env!("CARGO_PKG_VERSION");
5163
5164    pub fn variant(&self, name: &str) -> Option<&VariantCache> {
5165        self.variants.get(name)
5166    }
5167
5168    pub fn write_to<W: std::io::Write>(&self, writer: &mut W) -> Result<(), MappingError> {
5169        let encoded = serde_json::to_vec(self).map_err(|e| MappingError::CacheWrite {
5170            path: "<stream>".to_string(),
5171            message: e.to_string(),
5172        })?;
5173        writer.write_all(&encoded).map_err(MappingError::Io)
5174    }
5175
5176    pub fn read_from<R: std::io::Read>(reader: &mut R) -> Result<Self, MappingError> {
5177        let mut bytes = Vec::new();
5178        reader.read_to_end(&mut bytes).map_err(MappingError::Io)?;
5179        serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
5180            path: "<stream>".to_string(),
5181            message: e.to_string(),
5182        })
5183    }
5184
5185    pub fn read_from_checked<R: std::io::Read>(reader: &mut R) -> Result<Self, MappingError> {
5186        let mut bundle = Self::read_from(reader)?;
5187        // Every engine this bundle builds resolves names through its variant's
5188        // `Arc`, which `#[serde(skip)]` left empty on the way in.
5189        let shared = std::sync::Arc::new(std::mem::take(&mut bundle.code_lists));
5190        for variant in bundle.variants.values_mut() {
5191            variant.code_lists = std::sync::Arc::clone(&shared);
5192        }
5193        bundle.code_lists = (*shared).clone();
5194        if bundle.bundle_version != Self::CURRENT_VERSION {
5195            return Err(MappingError::CacheRead {
5196                path: "<stream>".to_string(),
5197                message: format!(
5198                    "Incompatible bundle version {}, expected version {}. \
5199                     Run `edifact-data update` to fetch compatible bundles.",
5200                    bundle.bundle_version,
5201                    Self::CURRENT_VERSION
5202                ),
5203            });
5204        }
5205        Ok(bundle)
5206    }
5207
5208    pub fn save(&self, path: &Path) -> Result<(), MappingError> {
5209        if let Some(parent) = path.parent() {
5210            std::fs::create_dir_all(parent)?;
5211        }
5212        let mut file = std::fs::File::create(path).map_err(MappingError::Io)?;
5213        self.write_to(&mut file)
5214    }
5215
5216    pub fn load(path: &Path) -> Result<Self, MappingError> {
5217        let mut file = std::fs::File::open(path).map_err(MappingError::Io)?;
5218        Self::read_from_checked(&mut file)
5219    }
5220}
5221
5222#[cfg(test)]
5223mod variant_cache_helper_tests {
5224    use super::*;
5225
5226    fn make_test_cache() -> VariantCache {
5227        let mut tx_groups = BTreeMap::new();
5228        tx_groups.insert("pid_55001".to_string(), "SG4".to_string());
5229        tx_groups.insert("pid_21007".to_string(), "SG14".to_string());
5230
5231        let mut transaction_defs = BTreeMap::new();
5232        transaction_defs.insert("pid_55001".to_string(), vec![]);
5233        transaction_defs.insert("pid_21007".to_string(), vec![]);
5234
5235        VariantCache {
5236            code_lists: Default::default(),
5237            message_defs: vec![],
5238            transaction_defs,
5239            combined_defs: BTreeMap::new(),
5240            code_lookups: BTreeMap::new(),
5241            mig_schema: None,
5242            segment_structure: None,
5243            pid_segment_numbers: BTreeMap::new(),
5244            pid_requirements: BTreeMap::new(),
5245            pid_ahb_workflows: BTreeMap::new(),
5246            tx_groups,
5247        }
5248    }
5249
5250    #[test]
5251    fn test_tx_group_returns_correct_group() {
5252        let vc = make_test_cache();
5253        assert_eq!(vc.tx_group("55001").unwrap(), "SG4");
5254        assert_eq!(vc.tx_group("21007").unwrap(), "SG14");
5255    }
5256
5257    #[test]
5258    fn test_tx_group_unknown_pid_returns_none() {
5259        let vc = make_test_cache();
5260        assert!(vc.tx_group("99999").is_none());
5261    }
5262
5263    #[test]
5264    fn test_msg_engine_returns_engine() {
5265        let vc = make_test_cache();
5266        let engine = vc.msg_engine("55001");
5267        assert_eq!(engine.definitions().len(), 0);
5268    }
5269
5270    #[test]
5271    fn test_tx_engine_returns_engine_for_known_pid() {
5272        let vc = make_test_cache();
5273        assert!(vc.tx_engine("55001").is_some());
5274    }
5275
5276    #[test]
5277    fn test_tx_engine_returns_none_for_unknown_pid() {
5278        let vc = make_test_cache();
5279        assert!(vc.tx_engine("99999").is_none());
5280    }
5281
5282    /// Build a cache whose every map holds many keys. Each call creates fresh
5283    /// `HashMap`s (fresh random hash seeds), so an order-dependent serializer
5284    /// produces different bytes on different calls.
5285    fn make_populated_cache() -> VariantCache {
5286        let pids: Vec<String> = (0..40).map(|i| format!("pid_{}", 55000 + i * 7)).collect();
5287        let schema: serde_json::Value = serde_json::from_str(include_str!(
5288            "../../mig-types/src/generated/fv2504/utilmd/pids/pid_55001_schema.json"
5289        ))
5290        .unwrap();
5291        let code_lookup = crate::code_lookup::CodeLookup::from_schema_value(&schema);
5292        let element_counts: serde_json::Map<String, serde_json::Value> = (0..40)
5293            .map(|i| (format!("T{i:02}"), serde_json::json!(i)))
5294            .collect();
5295        let segment_structure: SegmentStructure =
5296            serde_json::from_value(serde_json::json!({ "element_counts": element_counts }))
5297                .unwrap();
5298        let ubs: serde_json::Map<String, serde_json::Value> = (0..40)
5299            .map(|i| (format!("UB{i}"), serde_json::json!({ "Ref": i })))
5300            .collect();
5301        let workflow: ahb_types::AhbWorkflow = serde_json::from_value(serde_json::json!({
5302            "pruefidentifikator": "55001",
5303            "description": "",
5304            "communication_direction": null,
5305            "fields": [],
5306            "ub_definitions": ubs,
5307        }))
5308        .unwrap();
5309
5310        let mut vc = make_test_cache();
5311        vc.segment_structure = Some(segment_structure);
5312        for pid in &pids {
5313            vc.transaction_defs.insert(pid.clone(), vec![]);
5314            vc.combined_defs.insert(pid.clone(), vec![]);
5315            vc.code_lookups.insert(pid.clone(), code_lookup.clone());
5316            vc.pid_segment_numbers
5317                .insert(pid.clone(), vec!["00001".to_string()]);
5318            vc.pid_ahb_workflows.insert(pid.clone(), workflow.clone());
5319            vc.tx_groups.insert(pid.clone(), "SG4".to_string());
5320        }
5321        vc
5322    }
5323
5324    /// Enrichment of qualified field paths uses the codes of the segment variant
5325    /// the field reads, never those of a sibling variant of the same tag.
5326    #[test]
5327    fn test_enrichment_uses_codes_of_the_path_qualifier_variant() {
5328        let comp = |sub: u64, id: &str, codes: Option<serde_json::Value>| match codes {
5329            Some(c) => serde_json::json!({"sub_index": sub, "id": id, "type": "code", "codes": c}),
5330            None => serde_json::json!({"sub_index": sub, "id": id, "type": "data"}),
5331        };
5332        let code = |v: &str, n: &str| serde_json::json!([{"value": v, "name": n}]);
5333        let seg = |tag: &str, composite: &str, comps: Vec<serde_json::Value>| serde_json::json!({"id": tag, "elements": [{"index": 0, "composite": composite, "components": comps}]});
5334        let schema = serde_json::json!({"fields": {"sg15": {"segments": [
5335            seg("RFF", "C506", vec![comp(0, "1153", Some(code("Z13", "PID"))), comp(1, "1154", Some(code("21037", "RD / NB-Bewertung")))]),
5336            seg("RFF", "C506", vec![comp(0, "1153", Some(code("ACW", "Referenz"))), comp(1, "1154", None)]),
5337            seg("CAV", "C889", vec![comp(0, "7111", Some(code("Z91", "Z91"))), comp(1, "7110", Some(code("A", "Alpha")))]),
5338            seg("CAV", "C889", vec![comp(0, "7111", Some(code("ZF0", "ZF0"))), comp(1, "7110", Some(code("C", "Gamma")))]),
5339        ]}}});
5340        let engine = MappingEngine::new_empty()
5341            .with_code_lookup(crate::code_lookup::CodeLookup::from_schema_value(&schema));
5342        let def = MappingDefinition::from_toml_str(
5343            r#"
5344[meta]
5345entity = "Status"
5346bo4e_type = "Status"
5347source_group = "SG15"
5348source_path = "sg15"
5349discriminator = "RFF.0.0=Z13"
5350
5351[fields]
5352"rff.0.1" = "pruefidentifikator"
5353"rff[ACW].0.1" = "referenz"
5354"cav[Z91].0.1" = "z91Wert"
5355"cav[ZF0].0.1" = "zf0Wert"
5356"#,
5357        )
5358        .unwrap();
5359        let segment = |tag: &str, elements: &[&[&str]]| OwnedSegment {
5360            id: tag.to_string(),
5361            elements: elements
5362                .iter()
5363                .map(|e| e.iter().map(|c| c.to_string()).collect())
5364                .collect(),
5365            segment_number: 1,
5366        };
5367        let json = engine.map_forward_from_segments(
5368            &[
5369                segment("RFF", &[&["Z13", "21037"]]),
5370                segment("RFF", &[&["ACW", "REF-1"]]),
5371                segment("CAV", &[&["Z91", "C"]]),
5372                segment("CAV", &[&["ZF0", "C"]]),
5373            ],
5374            &def,
5375        );
5376        assert_eq!(
5377            json["referenz"],
5378            serde_json::json!("REF-1"),
5379            "RFF+ACW d1154 is data; RFF+Z13's codes must not apply: {json}"
5380        );
5381        assert_eq!(json["pruefidentifikator"]["meaning"], "RD / NB-Bewertung");
5382        assert_eq!(
5383            json["z91Wert"]["meaning"],
5384            serde_json::Value::Null,
5385            "'C' is a CAV+ZF0 code, unknown to CAV+Z91: {json}"
5386        );
5387        assert_eq!(json["zf0Wert"]["meaning"], "Gamma");
5388    }
5389
5390    /// A list target keeps every CAV with its own code, in wire order, and
5391    /// writes them back the same way — an absent first CAV no longer shifts
5392    /// the others, and a third CAV has somewhere to go.
5393    #[test]
5394    fn list_target_reads_and_writes_every_repetition_in_order() {
5395        let engine = MappingEngine::new_empty();
5396        let def = MappingDefinition::from_toml_str(
5397            r#"
5398[meta]
5399entity = "Zuordnung"
5400bo4e_type = "Zuordnung"
5401source_group = "SG10"
5402source_path = "sg10"
5403
5404[fields]
5405"cci.2.0" = "merkmal.code"
5406"cav[*,*].0.0" = "werte[].code"
5407"cav[*,*].0.3" = "werte[].text"
5408"cav[Z30,*].0.3" = "geraetenummern[].nummer"
5409"#,
5410        )
5411        .unwrap();
5412        let segment = |tag: &str, elements: &[&[&str]]| OwnedSegment {
5413            id: tag.to_string(),
5414            elements: elements
5415                .iter()
5416                .map(|e| e.iter().map(|c| c.to_string()).collect())
5417                .collect(),
5418            segment_number: 1,
5419        };
5420        let json = engine.map_forward_from_segments(
5421            &[
5422                segment("CCI", &[&[""], &[""], &["ZB3"]]),
5423                segment("CAV", &[&["Z90", "", "", "UENB"]]),
5424                segment("CAV", &[&["Z91", "", "", "MSB"]]),
5425                segment("CAV", &[&["Z30", "", "", "W1"]]),
5426                segment("CAV", &[&["Z30", "", "", "W2"]]),
5427            ],
5428            &def,
5429        );
5430        assert_eq!(
5431            json["werte"],
5432            serde_json::json!([
5433                {"code": "Z90", "text": "UENB"},
5434                {"code": "Z91", "text": "MSB"},
5435                {"code": "Z30", "text": "W1"},
5436                {"code": "Z30", "text": "W2"},
5437            ]),
5438            "{json}"
5439        );
5440        assert_eq!(
5441            json["geraetenummern"],
5442            serde_json::json!([{"nummer": "W1"}, {"nummer": "W2"}])
5443        );
5444
5445        // Back: one CAV per element, in the list's order.
5446        let only_werte = serde_json::json!({
5447            "merkmal": {"code": "ZB3"},
5448            "werte": [{"text": "UENB", "code": "Z90"}, {"code": "Z91", "text": "MSB"}],
5449        });
5450        let instance = engine.map_reverse(&only_werte, &def);
5451        let cavs: Vec<Vec<String>> = instance
5452            .segments
5453            .iter()
5454            .filter(|s| s.tag == "CAV")
5455            .map(|s| s.elements[0].clone())
5456            .collect();
5457        assert_eq!(
5458            cavs,
5459            vec![
5460                vec![
5461                    "Z90".to_string(),
5462                    String::new(),
5463                    String::new(),
5464                    "UENB".to_string()
5465                ],
5466                vec![
5467                    "Z91".to_string(),
5468                    String::new(),
5469                    String::new(),
5470                    "MSB".to_string()
5471                ],
5472            ]
5473        );
5474    }
5475
5476    #[test]
5477    fn test_variant_cache_serialization_is_deterministic() {
5478        let reference = serde_json::to_vec(&make_populated_cache()).unwrap();
5479        for _ in 0..5 {
5480            let again = serde_json::to_vec(&make_populated_cache()).unwrap();
5481            assert!(
5482                reference == again,
5483                "VariantCache serialization must not depend on HashMap iteration order"
5484            );
5485        }
5486    }
5487
5488    #[test]
5489    fn test_variant_cache_serializes_map_keys_sorted() {
5490        use indexmap::IndexMap;
5491        use serde::de::IgnoredAny;
5492
5493        #[derive(serde::Deserialize)]
5494        struct ProbeWorkflow {
5495            ub_definitions: IndexMap<String, IgnoredAny>,
5496        }
5497        #[derive(serde::Deserialize)]
5498        struct ProbeStructure {
5499            element_counts: IndexMap<String, usize>,
5500        }
5501        #[derive(serde::Deserialize)]
5502        struct Probe {
5503            transaction_defs: IndexMap<String, IgnoredAny>,
5504            combined_defs: IndexMap<String, IgnoredAny>,
5505            code_lookups: IndexMap<String, IndexMap<String, IgnoredAny>>,
5506            segment_structure: ProbeStructure,
5507            pid_segment_numbers: IndexMap<String, IgnoredAny>,
5508            pid_requirements: IndexMap<String, IgnoredAny>,
5509            pid_ahb_workflows: IndexMap<String, ProbeWorkflow>,
5510            tx_groups: IndexMap<String, String>,
5511        }
5512        fn assert_sorted<'a>(what: &str, keys: impl Iterator<Item = &'a String>) {
5513            let keys: Vec<&String> = keys.collect();
5514            let mut sorted = keys.clone();
5515            sorted.sort();
5516            assert_eq!(keys, sorted, "{what} keys must serialize in sorted order");
5517        }
5518
5519        let json = serde_json::to_string(&make_populated_cache()).unwrap();
5520        let probe: Probe = serde_json::from_str(&json).unwrap();
5521        assert_sorted("transaction_defs", probe.transaction_defs.keys());
5522        assert_sorted("combined_defs", probe.combined_defs.keys());
5523        assert_sorted("code_lookups", probe.code_lookups.keys());
5524        let lookup = probe.code_lookups.values().next().unwrap();
5525        assert!(lookup.len() > 10, "fixture lookup should have many entries");
5526        assert_sorted("code_lookup entries", lookup.keys());
5527        assert_sorted(
5528            "segment_structure",
5529            probe.segment_structure.element_counts.keys(),
5530        );
5531        assert_sorted("pid_segment_numbers", probe.pid_segment_numbers.keys());
5532        assert_sorted("pid_requirements", probe.pid_requirements.keys());
5533        assert_sorted("pid_ahb_workflows", probe.pid_ahb_workflows.keys());
5534        let wf = probe.pid_ahb_workflows.values().next().unwrap();
5535        assert_sorted("ub_definitions", wf.ub_definitions.keys());
5536        assert_sorted("tx_groups", probe.tx_groups.keys());
5537    }
5538
5539    #[test]
5540    fn test_data_bundle_serializes_variants_sorted() {
5541        use indexmap::IndexMap;
5542        use serde::de::IgnoredAny;
5543
5544        #[derive(serde::Deserialize)]
5545        struct Probe {
5546            variants: IndexMap<String, IgnoredAny>,
5547        }
5548        let variants: BTreeMap<String, VariantCache> = (0..20)
5549            .map(|i| (format!("VARIANT_{i:02}"), make_test_cache()))
5550            .collect();
5551        let bundle = DataBundle {
5552            format_version: "FV2504".to_string(),
5553            bundle_version: DataBundle::CURRENT_VERSION,
5554            built_by: Some(DataBundle::PRODUCING_VERSION.to_string()),
5555            variants,
5556            bo4e_catalog: Default::default(),
5557            code_lists: Default::default(),
5558        };
5559        let mut bytes = Vec::new();
5560        bundle.write_to(&mut bytes).unwrap();
5561        let probe: Probe = serde_json::from_slice(&bytes).unwrap();
5562        let keys: Vec<&String> = probe.variants.keys().collect();
5563        let mut sorted = keys.clone();
5564        sorted.sort();
5565        assert_eq!(keys, sorted);
5566    }
5567}
5568
5569#[cfg(test)]
5570mod tests {
5571    use super::*;
5572    use crate::definition::{MappingDefinition, MappingMeta, StructuredFieldMapping};
5573    use indexmap::IndexMap;
5574
5575    fn make_def(fields: IndexMap<String, FieldMapping>) -> MappingDefinition {
5576        MappingDefinition {
5577            meta: MappingMeta {
5578                entity: "Test".to_string(),
5579                bo4e_type: "Test".to_string(),
5580                source_group: "SG4".to_string(),
5581                source_path: None,
5582                discriminator: None,
5583                repeat_on_tag: None,
5584                parent_field: None,
5585                target_list: None,
5586                order: None,
5587            },
5588            fields,
5589            complex_handlers: None,
5590        }
5591    }
5592
5593    #[test]
5594    fn test_map_interchange_single_transaction_backward_compat() {
5595        use mig_assembly::assembler::*;
5596
5597        // Single SG4 with SG5 — the common case for current PID 55001 fixtures
5598        let tree = AssembledTree {
5599            segments: vec![
5600                AssembledSegment {
5601                    tag: "UNH".to_string(),
5602                    elements: vec![vec!["001".to_string()]],
5603                    mig_number: None,
5604                    segment_number: None,
5605                },
5606                AssembledSegment {
5607                    tag: "BGM".to_string(),
5608                    elements: vec![vec!["E01".to_string()], vec!["DOC001".to_string()]],
5609                    mig_number: None,
5610                    segment_number: None,
5611                },
5612            ],
5613            groups: vec![
5614                AssembledGroup {
5615                    group_id: "SG2".to_string(),
5616                    repetitions: vec![AssembledGroupInstance {
5617                        segments: vec![AssembledSegment {
5618                            tag: "NAD".to_string(),
5619                            elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
5620                            mig_number: None,
5621                            segment_number: None,
5622                        }],
5623                        child_groups: vec![],
5624                        entry_mig_number: None,
5625                        variant_mig_numbers: vec![],
5626                        skipped_segments: vec![],
5627                        skipped_positions: Vec::new(),
5628                    }],
5629                },
5630                AssembledGroup {
5631                    group_id: "SG4".to_string(),
5632                    repetitions: vec![AssembledGroupInstance {
5633                        segments: vec![AssembledSegment {
5634                            tag: "IDE".to_string(),
5635                            elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
5636                            mig_number: None,
5637                            segment_number: None,
5638                        }],
5639                        child_groups: vec![AssembledGroup {
5640                            group_id: "SG5".to_string(),
5641                            repetitions: vec![AssembledGroupInstance {
5642                                segments: vec![AssembledSegment {
5643                                    tag: "LOC".to_string(),
5644                                    elements: vec![
5645                                        vec!["Z16".to_string()],
5646                                        vec!["DE000111222333".to_string()],
5647                                    ],
5648                                    mig_number: None,
5649                                    segment_number: None,
5650                                }],
5651                                child_groups: vec![],
5652                                entry_mig_number: None,
5653                                variant_mig_numbers: vec![],
5654                                skipped_segments: vec![],
5655                                skipped_positions: Vec::new(),
5656                            }],
5657                        }],
5658                        entry_mig_number: None,
5659                        variant_mig_numbers: vec![],
5660                        skipped_segments: vec![],
5661                        skipped_positions: Vec::new(),
5662                    }],
5663                },
5664            ],
5665            post_group_start: 2,
5666            inter_group_segments: std::collections::BTreeMap::new(),
5667        };
5668
5669        // Empty message engine (no message-level defs for this test)
5670        let msg_engine = MappingEngine::from_definitions(vec![]);
5671
5672        // Transaction defs
5673        let mut tx_fields: IndexMap<String, FieldMapping> = IndexMap::new();
5674        tx_fields.insert(
5675            "ide.1".to_string(),
5676            FieldMapping::Simple("vorgangId".to_string()),
5677        );
5678        let mut malo_fields: IndexMap<String, FieldMapping> = IndexMap::new();
5679        malo_fields.insert(
5680            "loc.1".to_string(),
5681            FieldMapping::Simple("marktlokationsId".to_string()),
5682        );
5683
5684        let tx_engine = MappingEngine::from_definitions(vec![
5685            MappingDefinition {
5686                meta: MappingMeta {
5687                    entity: "Prozessdaten".to_string(),
5688                    bo4e_type: "Prozessdaten".to_string(),
5689                    source_group: "SG4".to_string(),
5690                    source_path: None,
5691                    discriminator: None,
5692                    repeat_on_tag: None,
5693                    parent_field: None,
5694                    target_list: None,
5695                    order: None,
5696                },
5697                fields: tx_fields,
5698                complex_handlers: None,
5699            },
5700            MappingDefinition {
5701                meta: MappingMeta {
5702                    entity: "Marktlokation".to_string(),
5703                    bo4e_type: "Marktlokation".to_string(),
5704                    source_group: "SG4.SG5".to_string(),
5705                    source_path: None,
5706                    discriminator: None,
5707                    repeat_on_tag: None,
5708                    parent_field: None,
5709                    target_list: None,
5710                    order: None,
5711                },
5712                fields: malo_fields,
5713                complex_handlers: None,
5714            },
5715        ]);
5716
5717        let result = MappingEngine::map_interchange(&msg_engine, &tx_engine, &tree, "SG4", true);
5718
5719        assert_eq!(result.transaktionen.len(), 1);
5720        assert_eq!(
5721            result.transaktionen[0].transaktionsdaten["vorgangId"]
5722                .as_str()
5723                .unwrap(),
5724            "TX001"
5725        );
5726        // Marktlokation (SG4.SG5) stays top-level — SG4 IS the transaction root,
5727        // so Marktlokation is a peer of Prozessdaten, not a child of it.
5728        assert_eq!(
5729            result.transaktionen[0].stammdaten["marktlokation"]["marktlokationsId"]
5730                .as_str()
5731                .unwrap(),
5732            "DE000111222333"
5733        );
5734    }
5735
5736    #[test]
5737    fn test_map_reverse_pads_intermediate_empty_elements() {
5738        // NAD+Z09+++Muster:Max — positions 0 and 3 populated, 1 and 2 should become [""]
5739        let mut fields = IndexMap::new();
5740        fields.insert(
5741            "nad.0".to_string(),
5742            FieldMapping::Structured(StructuredFieldMapping {
5743                target: String::new(),
5744                transform: None,
5745                when: None,
5746                default: Some("Z09".to_string()),
5747                enum_map: None,
5748                code_list: None,
5749                also_code_list: None,
5750                when_filled: None,
5751                also_target: None,
5752                also_enum_map: None,
5753            }),
5754        );
5755        fields.insert(
5756            "nad.3.0".to_string(),
5757            FieldMapping::Simple("name".to_string()),
5758        );
5759        fields.insert(
5760            "nad.3.1".to_string(),
5761            FieldMapping::Simple("vorname".to_string()),
5762        );
5763
5764        let def = make_def(fields);
5765        let engine = MappingEngine::from_definitions(vec![]);
5766
5767        let bo4e = serde_json::json!({
5768            "name": "Muster",
5769            "vorname": "Max"
5770        });
5771
5772        let instance = engine.map_reverse(&bo4e, &def);
5773        assert_eq!(instance.segments.len(), 1);
5774
5775        let nad = &instance.segments[0];
5776        assert_eq!(nad.tag, "NAD");
5777        assert_eq!(nad.elements.len(), 4);
5778        assert_eq!(nad.elements[0], vec!["Z09"]);
5779        // Intermediate positions 1 and 2 should be padded to [""]
5780        assert_eq!(nad.elements[1], vec![""]);
5781        assert_eq!(nad.elements[2], vec![""]);
5782        assert_eq!(nad.elements[3][0], "Muster");
5783        assert_eq!(nad.elements[3][1], "Max");
5784    }
5785
5786    #[test]
5787    fn test_map_reverse_no_padding_when_contiguous() {
5788        // DTM+92:20250531:303 — all three components in element 0, no gaps
5789        let mut fields = IndexMap::new();
5790        fields.insert(
5791            "dtm.0.0".to_string(),
5792            FieldMapping::Structured(StructuredFieldMapping {
5793                target: String::new(),
5794                transform: None,
5795                when: None,
5796                default: Some("92".to_string()),
5797                enum_map: None,
5798                code_list: None,
5799                also_code_list: None,
5800                when_filled: None,
5801                also_target: None,
5802                also_enum_map: None,
5803            }),
5804        );
5805        fields.insert(
5806            "dtm.0.1".to_string(),
5807            FieldMapping::Simple("value".to_string()),
5808        );
5809        fields.insert(
5810            "dtm.0.2".to_string(),
5811            FieldMapping::Structured(StructuredFieldMapping {
5812                target: String::new(),
5813                transform: None,
5814                when: None,
5815                default: Some("303".to_string()),
5816                enum_map: None,
5817                code_list: None,
5818                also_code_list: None,
5819                when_filled: None,
5820                also_target: None,
5821                also_enum_map: None,
5822            }),
5823        );
5824
5825        let def = make_def(fields);
5826        let engine = MappingEngine::from_definitions(vec![]);
5827
5828        let bo4e = serde_json::json!({ "value": "20250531" });
5829
5830        let instance = engine.map_reverse(&bo4e, &def);
5831        let dtm = &instance.segments[0];
5832        // Single element with 3 components — no intermediate padding needed
5833        assert_eq!(dtm.elements.len(), 1);
5834        assert_eq!(dtm.elements[0], vec!["92", "20250531", "303"]);
5835    }
5836
5837    #[test]
5838    fn test_map_message_level_extracts_sg2_only() {
5839        use mig_assembly::assembler::*;
5840
5841        // Build a tree with SG2 (message-level) and SG4 (transaction-level)
5842        let tree = AssembledTree {
5843            segments: vec![
5844                AssembledSegment {
5845                    tag: "UNH".to_string(),
5846                    elements: vec![vec!["001".to_string()]],
5847                    mig_number: None,
5848                    segment_number: None,
5849                },
5850                AssembledSegment {
5851                    tag: "BGM".to_string(),
5852                    elements: vec![vec!["E01".to_string()]],
5853                    mig_number: None,
5854                    segment_number: None,
5855                },
5856            ],
5857            groups: vec![
5858                AssembledGroup {
5859                    group_id: "SG2".to_string(),
5860                    repetitions: vec![AssembledGroupInstance {
5861                        segments: vec![AssembledSegment {
5862                            tag: "NAD".to_string(),
5863                            elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
5864                            mig_number: None,
5865                            segment_number: None,
5866                        }],
5867                        child_groups: vec![],
5868                        entry_mig_number: None,
5869                        variant_mig_numbers: vec![],
5870                        skipped_segments: vec![],
5871                        skipped_positions: Vec::new(),
5872                    }],
5873                },
5874                AssembledGroup {
5875                    group_id: "SG4".to_string(),
5876                    repetitions: vec![AssembledGroupInstance {
5877                        segments: vec![AssembledSegment {
5878                            tag: "IDE".to_string(),
5879                            elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
5880                            mig_number: None,
5881                            segment_number: None,
5882                        }],
5883                        child_groups: vec![],
5884                        entry_mig_number: None,
5885                        variant_mig_numbers: vec![],
5886                        skipped_segments: vec![],
5887                        skipped_positions: Vec::new(),
5888                    }],
5889                },
5890            ],
5891            post_group_start: 2,
5892            inter_group_segments: std::collections::BTreeMap::new(),
5893        };
5894
5895        // Message-level definition maps SG2
5896        let mut msg_fields: IndexMap<String, FieldMapping> = IndexMap::new();
5897        msg_fields.insert(
5898            "nad.0".to_string(),
5899            FieldMapping::Simple("marktrolle".to_string()),
5900        );
5901        msg_fields.insert(
5902            "nad.1".to_string(),
5903            FieldMapping::Simple("rollencodenummer".to_string()),
5904        );
5905        let msg_def = MappingDefinition {
5906            meta: MappingMeta {
5907                entity: "Marktteilnehmer".to_string(),
5908                bo4e_type: "Marktteilnehmer".to_string(),
5909                source_group: "SG2".to_string(),
5910                source_path: None,
5911                discriminator: None,
5912                repeat_on_tag: None,
5913                parent_field: None,
5914                target_list: None,
5915                order: None,
5916            },
5917            fields: msg_fields,
5918            complex_handlers: None,
5919        };
5920
5921        let engine = MappingEngine::from_definitions(vec![msg_def.clone()]);
5922        let result = engine.map_all_forward(&tree);
5923
5924        // Should contain Marktteilnehmer from SG2
5925        assert!(result.get("marktteilnehmer").is_some());
5926        let mt = &result["marktteilnehmer"];
5927        assert_eq!(mt["marktrolle"].as_str().unwrap(), "MS");
5928        assert_eq!(mt["rollencodenummer"].as_str().unwrap(), "9900123");
5929    }
5930
5931    #[test]
5932    fn test_map_transaction_scoped_to_sg4_instance() {
5933        use mig_assembly::assembler::*;
5934
5935        // Build a tree with SG4 containing SG5 (LOC+Z16)
5936        let tree = AssembledTree {
5937            segments: vec![
5938                AssembledSegment {
5939                    tag: "UNH".to_string(),
5940                    elements: vec![vec!["001".to_string()]],
5941                    mig_number: None,
5942                    segment_number: None,
5943                },
5944                AssembledSegment {
5945                    tag: "BGM".to_string(),
5946                    elements: vec![vec!["E01".to_string()]],
5947                    mig_number: None,
5948                    segment_number: None,
5949                },
5950            ],
5951            groups: vec![AssembledGroup {
5952                group_id: "SG4".to_string(),
5953                repetitions: vec![AssembledGroupInstance {
5954                    segments: vec![AssembledSegment {
5955                        tag: "IDE".to_string(),
5956                        elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
5957                        mig_number: None,
5958                        segment_number: None,
5959                    }],
5960                    child_groups: vec![AssembledGroup {
5961                        group_id: "SG5".to_string(),
5962                        repetitions: vec![AssembledGroupInstance {
5963                            segments: vec![AssembledSegment {
5964                                tag: "LOC".to_string(),
5965                                elements: vec![
5966                                    vec!["Z16".to_string()],
5967                                    vec!["DE000111222333".to_string()],
5968                                ],
5969                                mig_number: None,
5970                                segment_number: None,
5971                            }],
5972                            child_groups: vec![],
5973                            entry_mig_number: None,
5974                            variant_mig_numbers: vec![],
5975                            skipped_segments: vec![],
5976                            skipped_positions: Vec::new(),
5977                        }],
5978                    }],
5979                    entry_mig_number: None,
5980                    variant_mig_numbers: vec![],
5981                    skipped_segments: vec![],
5982                    skipped_positions: Vec::new(),
5983                }],
5984            }],
5985            post_group_start: 2,
5986            inter_group_segments: std::collections::BTreeMap::new(),
5987        };
5988
5989        // Transaction-level definitions: prozessdaten (root of SG4) + marktlokation (SG5)
5990        let mut proz_fields: IndexMap<String, FieldMapping> = IndexMap::new();
5991        proz_fields.insert(
5992            "ide.1".to_string(),
5993            FieldMapping::Simple("vorgangId".to_string()),
5994        );
5995        let proz_def = MappingDefinition {
5996            meta: MappingMeta {
5997                entity: "Prozessdaten".to_string(),
5998                bo4e_type: "Prozessdaten".to_string(),
5999                source_group: "".to_string(), // Root-level within transaction sub-tree
6000                source_path: None,
6001                discriminator: None,
6002                repeat_on_tag: None,
6003                parent_field: None,
6004                target_list: None,
6005                order: None,
6006            },
6007            fields: proz_fields,
6008            complex_handlers: None,
6009        };
6010
6011        let mut malo_fields: IndexMap<String, FieldMapping> = IndexMap::new();
6012        malo_fields.insert(
6013            "loc.1".to_string(),
6014            FieldMapping::Simple("marktlokationsId".to_string()),
6015        );
6016        let malo_def = MappingDefinition {
6017            meta: MappingMeta {
6018                entity: "Marktlokation".to_string(),
6019                bo4e_type: "Marktlokation".to_string(),
6020                source_group: "SG5".to_string(), // Relative to SG4, not "SG4.SG5"
6021                source_path: None,
6022                discriminator: None,
6023                repeat_on_tag: None,
6024                parent_field: None,
6025                target_list: None,
6026                order: None,
6027            },
6028            fields: malo_fields,
6029            complex_handlers: None,
6030        };
6031
6032        let tx_engine = MappingEngine::from_definitions(vec![proz_def, malo_def]);
6033
6034        // Scope to the SG4 instance and map
6035        let sg4 = &tree.groups[0]; // SG4 group
6036        let sg4_instance = &sg4.repetitions[0];
6037        let sub_tree = sg4_instance.as_assembled_tree();
6038
6039        let result = tx_engine.map_all_forward(&sub_tree);
6040
6041        // Should contain Prozessdaten from SG4 root segments
6042        assert_eq!(
6043            result["prozessdaten"]["vorgangId"].as_str().unwrap(),
6044            "TX001"
6045        );
6046
6047        // Should contain Marktlokation from SG5 within SG4
6048        assert_eq!(
6049            result["marktlokation"]["marktlokationsId"]
6050                .as_str()
6051                .unwrap(),
6052            "DE000111222333"
6053        );
6054    }
6055
6056    #[test]
6057    fn test_map_interchange_produces_full_hierarchy() {
6058        use mig_assembly::assembler::*;
6059
6060        // Build a tree with SG2 (message-level) and SG4 with two repetitions (two transactions)
6061        let tree = AssembledTree {
6062            segments: vec![
6063                AssembledSegment {
6064                    tag: "UNH".to_string(),
6065                    elements: vec![vec!["001".to_string()]],
6066                    mig_number: None,
6067                    segment_number: None,
6068                },
6069                AssembledSegment {
6070                    tag: "BGM".to_string(),
6071                    elements: vec![vec!["E01".to_string()]],
6072                    mig_number: None,
6073                    segment_number: None,
6074                },
6075            ],
6076            groups: vec![
6077                AssembledGroup {
6078                    group_id: "SG2".to_string(),
6079                    repetitions: vec![AssembledGroupInstance {
6080                        segments: vec![AssembledSegment {
6081                            tag: "NAD".to_string(),
6082                            elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
6083                            mig_number: None,
6084                            segment_number: None,
6085                        }],
6086                        child_groups: vec![],
6087                        entry_mig_number: None,
6088                        variant_mig_numbers: vec![],
6089                        skipped_segments: vec![],
6090                        skipped_positions: Vec::new(),
6091                    }],
6092                },
6093                AssembledGroup {
6094                    group_id: "SG4".to_string(),
6095                    repetitions: vec![
6096                        AssembledGroupInstance {
6097                            segments: vec![AssembledSegment {
6098                                tag: "IDE".to_string(),
6099                                elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
6100                                mig_number: None,
6101                                segment_number: None,
6102                            }],
6103                            child_groups: vec![],
6104                            entry_mig_number: None,
6105                            variant_mig_numbers: vec![],
6106                            skipped_segments: vec![],
6107                            skipped_positions: Vec::new(),
6108                        },
6109                        AssembledGroupInstance {
6110                            segments: vec![AssembledSegment {
6111                                tag: "IDE".to_string(),
6112                                elements: vec![vec!["24".to_string()], vec!["TX002".to_string()]],
6113                                mig_number: None,
6114                                segment_number: None,
6115                            }],
6116                            child_groups: vec![],
6117                            entry_mig_number: None,
6118                            variant_mig_numbers: vec![],
6119                            skipped_segments: vec![],
6120                            skipped_positions: Vec::new(),
6121                        },
6122                    ],
6123                },
6124            ],
6125            post_group_start: 2,
6126            inter_group_segments: std::collections::BTreeMap::new(),
6127        };
6128
6129        // Message-level definitions
6130        let mut msg_fields: IndexMap<String, FieldMapping> = IndexMap::new();
6131        msg_fields.insert(
6132            "nad.0".to_string(),
6133            FieldMapping::Simple("marktrolle".to_string()),
6134        );
6135        let msg_defs = vec![MappingDefinition {
6136            meta: MappingMeta {
6137                entity: "Marktteilnehmer".to_string(),
6138                bo4e_type: "Marktteilnehmer".to_string(),
6139                source_group: "SG2".to_string(),
6140                source_path: None,
6141                discriminator: None,
6142                repeat_on_tag: None,
6143                parent_field: None,
6144                target_list: None,
6145                order: None,
6146            },
6147            fields: msg_fields,
6148            complex_handlers: None,
6149        }];
6150
6151        // Transaction-level definitions (source_group includes SG4 prefix)
6152        let mut tx_fields: IndexMap<String, FieldMapping> = IndexMap::new();
6153        tx_fields.insert(
6154            "ide.1".to_string(),
6155            FieldMapping::Simple("vorgangId".to_string()),
6156        );
6157        let tx_defs = vec![MappingDefinition {
6158            meta: MappingMeta {
6159                entity: "Prozessdaten".to_string(),
6160                bo4e_type: "Prozessdaten".to_string(),
6161                source_group: "SG4".to_string(),
6162                source_path: None,
6163                discriminator: None,
6164                repeat_on_tag: None,
6165                parent_field: None,
6166                target_list: None,
6167                order: None,
6168            },
6169            fields: tx_fields,
6170            complex_handlers: None,
6171        }];
6172
6173        let msg_engine = MappingEngine::from_definitions(msg_defs);
6174        let tx_engine = MappingEngine::from_definitions(tx_defs);
6175
6176        let result = MappingEngine::map_interchange(&msg_engine, &tx_engine, &tree, "SG4", true);
6177
6178        // Message-level stammdaten
6179        assert!(result.stammdaten["marktteilnehmer"].is_object());
6180        assert_eq!(
6181            result.stammdaten["marktteilnehmer"]["marktrolle"]
6182                .as_str()
6183                .unwrap(),
6184            "MS"
6185        );
6186
6187        // Two transactions
6188        assert_eq!(result.transaktionen.len(), 2);
6189        assert_eq!(
6190            result.transaktionen[0].transaktionsdaten["vorgangId"]
6191                .as_str()
6192                .unwrap(),
6193            "TX001"
6194        );
6195        assert_eq!(
6196            result.transaktionen[1].transaktionsdaten["vorgangId"]
6197                .as_str()
6198                .unwrap(),
6199            "TX002"
6200        );
6201    }
6202
6203    #[test]
6204    fn test_map_reverse_with_segment_structure_pads_trailing() {
6205        // STS+7++E01 — position 0 and 2 populated, MIG says 5 elements
6206        let mut fields = IndexMap::new();
6207        fields.insert(
6208            "sts.0".to_string(),
6209            FieldMapping::Structured(StructuredFieldMapping {
6210                target: String::new(),
6211                transform: None,
6212                when: None,
6213                default: Some("7".to_string()),
6214                enum_map: None,
6215                code_list: None,
6216                also_code_list: None,
6217                when_filled: None,
6218                also_target: None,
6219                also_enum_map: None,
6220            }),
6221        );
6222        fields.insert(
6223            "sts.2".to_string(),
6224            FieldMapping::Simple("grund".to_string()),
6225        );
6226
6227        let def = make_def(fields);
6228
6229        // Build a SegmentStructure manually via BTreeMap
6230        let mut counts = std::collections::BTreeMap::new();
6231        counts.insert("STS".to_string(), 5usize);
6232        let ss = SegmentStructure {
6233            element_counts: counts,
6234        };
6235
6236        let engine = MappingEngine::from_definitions(vec![]).with_segment_structure(ss);
6237
6238        let bo4e = serde_json::json!({ "grund": "E01" });
6239
6240        let instance = engine.map_reverse(&bo4e, &def);
6241        let sts = &instance.segments[0];
6242        // Should have 5 elements: pos 0 = ["7"], pos 1 = [""] (intermediate pad),
6243        // pos 2 = ["E01"], pos 3 = [""] (trailing pad), pos 4 = [""] (trailing pad)
6244        assert_eq!(sts.elements.len(), 5);
6245        assert_eq!(sts.elements[0], vec!["7"]);
6246        assert_eq!(sts.elements[1], vec![""]);
6247        assert_eq!(sts.elements[2], vec!["E01"]);
6248        assert_eq!(sts.elements[3], vec![""]);
6249        assert_eq!(sts.elements[4], vec![""]);
6250    }
6251
6252    #[test]
6253    fn test_resolve_child_relative_with_source_path() {
6254        let mut map: std::collections::HashMap<String, Vec<usize>> =
6255            std::collections::HashMap::new();
6256        map.insert("sg4.sg8_ze1".to_string(), vec![6]);
6257        map.insert("sg4.sg8_z98".to_string(), vec![0]);
6258
6259        // Child without explicit index → resolved from source_path
6260        assert_eq!(
6261            resolve_child_relative("SG8.SG10", Some("sg4.sg8_ze1.sg10"), &map, 0),
6262            "SG8:6.SG10"
6263        );
6264
6265        // Child with explicit index → kept as-is
6266        assert_eq!(
6267            resolve_child_relative("SG8:3.SG10", Some("sg4.sg8_ze1.sg10"), &map, 0),
6268            "SG8:3.SG10"
6269        );
6270
6271        // Source path not in map → kept as-is
6272        assert_eq!(
6273            resolve_child_relative("SG8.SG10", Some("sg4.sg8_unknown.sg10"), &map, 0),
6274            "SG8.SG10"
6275        );
6276
6277        // No source_path → kept as-is
6278        assert_eq!(
6279            resolve_child_relative("SG8.SG10", None, &map, 0),
6280            "SG8.SG10"
6281        );
6282
6283        // SG9 also works
6284        assert_eq!(
6285            resolve_child_relative("SG8.SG9", Some("sg4.sg8_z98.sg9"), &map, 0),
6286            "SG8:0.SG9"
6287        );
6288
6289        // Multi-rep parent: item_idx selects the correct parent rep
6290        map.insert("sg4.sg8_zf3".to_string(), vec![3, 4]);
6291        assert_eq!(
6292            resolve_child_relative("SG8.SG10", Some("sg4.sg8_zf3.sg10"), &map, 0),
6293            "SG8:3.SG10"
6294        );
6295        assert_eq!(
6296            resolve_child_relative("SG8.SG10", Some("sg4.sg8_zf3.sg10"), &map, 1),
6297            "SG8:4.SG10"
6298        );
6299    }
6300
6301    #[test]
6302    fn test_place_in_groups_returns_rep_index() {
6303        let mut groups: Vec<AssembledGroup> = Vec::new();
6304
6305        // Append (no index) → returns position 0
6306        let instance = AssembledGroupInstance {
6307            segments: vec![],
6308            child_groups: vec![],
6309            entry_mig_number: None,
6310            variant_mig_numbers: vec![],
6311            skipped_segments: vec![],
6312            skipped_positions: Vec::new(),
6313        };
6314        assert_eq!(place_in_groups(&mut groups, "SG8", instance), 0);
6315
6316        // Append again → returns position 1
6317        let instance = AssembledGroupInstance {
6318            segments: vec![],
6319            child_groups: vec![],
6320            entry_mig_number: None,
6321            variant_mig_numbers: vec![],
6322            skipped_segments: vec![],
6323            skipped_positions: Vec::new(),
6324        };
6325        assert_eq!(place_in_groups(&mut groups, "SG8", instance), 1);
6326
6327        // Explicit index → returns that index
6328        let instance = AssembledGroupInstance {
6329            segments: vec![],
6330            child_groups: vec![],
6331            entry_mig_number: None,
6332            variant_mig_numbers: vec![],
6333            skipped_segments: vec![],
6334            skipped_positions: Vec::new(),
6335        };
6336        assert_eq!(place_in_groups(&mut groups, "SG8:5", instance), 5);
6337    }
6338
6339    #[test]
6340    fn test_resolve_by_source_path() {
6341        use mig_assembly::assembler::*;
6342
6343        // Build a tree: SG4[0] → SG8 with two reps (Z98 and ZD7) → each has SG10
6344        let tree = AssembledTree {
6345            segments: vec![],
6346            groups: vec![AssembledGroup {
6347                group_id: "SG4".to_string(),
6348                repetitions: vec![AssembledGroupInstance {
6349                    segments: vec![],
6350                    child_groups: vec![AssembledGroup {
6351                        group_id: "SG8".to_string(),
6352                        repetitions: vec![
6353                            AssembledGroupInstance {
6354                                segments: vec![AssembledSegment {
6355                                    tag: "SEQ".to_string(),
6356                                    elements: vec![vec!["Z98".to_string()]],
6357                                    mig_number: None,
6358                                    segment_number: None,
6359                                }],
6360                                child_groups: vec![AssembledGroup {
6361                                    group_id: "SG10".to_string(),
6362                                    repetitions: vec![AssembledGroupInstance {
6363                                        segments: vec![AssembledSegment {
6364                                            tag: "CCI".to_string(),
6365                                            elements: vec![vec![], vec![], vec!["ZB3".to_string()]],
6366                                            mig_number: None,
6367                                            segment_number: None,
6368                                        }],
6369                                        child_groups: vec![],
6370                                        entry_mig_number: None,
6371                                        variant_mig_numbers: vec![],
6372                                        skipped_segments: vec![],
6373                                        skipped_positions: Vec::new(),
6374                                    }],
6375                                }],
6376                                entry_mig_number: None,
6377                                variant_mig_numbers: vec![],
6378                                skipped_segments: vec![],
6379                                skipped_positions: Vec::new(),
6380                            },
6381                            AssembledGroupInstance {
6382                                segments: vec![AssembledSegment {
6383                                    tag: "SEQ".to_string(),
6384                                    elements: vec![vec!["ZD7".to_string()]],
6385                                    mig_number: None,
6386                                    segment_number: None,
6387                                }],
6388                                child_groups: vec![AssembledGroup {
6389                                    group_id: "SG10".to_string(),
6390                                    repetitions: vec![AssembledGroupInstance {
6391                                        segments: vec![AssembledSegment {
6392                                            tag: "CCI".to_string(),
6393                                            elements: vec![vec![], vec![], vec!["ZE6".to_string()]],
6394                                            mig_number: None,
6395                                            segment_number: None,
6396                                        }],
6397                                        child_groups: vec![],
6398                                        entry_mig_number: None,
6399                                        variant_mig_numbers: vec![],
6400                                        skipped_segments: vec![],
6401                                        skipped_positions: Vec::new(),
6402                                    }],
6403                                }],
6404                                entry_mig_number: None,
6405                                variant_mig_numbers: vec![],
6406                                skipped_segments: vec![],
6407                                skipped_positions: Vec::new(),
6408                            },
6409                        ],
6410                    }],
6411                    entry_mig_number: None,
6412                    variant_mig_numbers: vec![],
6413                    skipped_segments: vec![],
6414                    skipped_positions: Vec::new(),
6415                }],
6416            }],
6417            post_group_start: 0,
6418            inter_group_segments: std::collections::BTreeMap::new(),
6419        };
6420
6421        // Resolve SG10 under Z98
6422        let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_z98.sg10");
6423        assert!(inst.is_some());
6424        assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZB3");
6425
6426        // Resolve SG10 under ZD7
6427        let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_zd7.sg10");
6428        assert!(inst.is_some());
6429        assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZE6");
6430
6431        // Unknown qualifier → None
6432        let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_zzz.sg10");
6433        assert!(inst.is_none());
6434
6435        // Without qualifier → first rep (Z98)
6436        let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8.sg10");
6437        assert!(inst.is_some());
6438        assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZB3");
6439    }
6440
6441    #[test]
6442    fn test_parse_source_path_part() {
6443        assert_eq!(parse_source_path_part("sg4"), ("sg4", None));
6444        assert_eq!(parse_source_path_part("sg8_z98"), ("sg8", Some("z98")));
6445        assert_eq!(parse_source_path_part("sg10"), ("sg10", None));
6446        assert_eq!(parse_source_path_part("sg12_z04"), ("sg12", Some("z04")));
6447    }
6448
6449    #[test]
6450    fn test_has_source_path_qualifiers() {
6451        assert!(has_source_path_qualifiers("sg4.sg8_z98.sg10"));
6452        assert!(has_source_path_qualifiers("sg4.sg8_ze1.sg9"));
6453        assert!(!has_source_path_qualifiers("sg4.sg6"));
6454        assert!(!has_source_path_qualifiers("sg4.sg8.sg10"));
6455    }
6456
6457    #[test]
6458    fn test_extract_all_from_instance_collects_all_qualifier_matches() {
6459        use mig_assembly::assembler::*;
6460
6461        // Instance with 3 RFF+Z34 segments
6462        let instance = AssembledGroupInstance {
6463            segments: vec![
6464                AssembledSegment {
6465                    tag: "SEQ".to_string(),
6466                    elements: vec![vec!["ZD6".to_string()]],
6467                    mig_number: None,
6468                    segment_number: None,
6469                },
6470                AssembledSegment {
6471                    tag: "RFF".to_string(),
6472                    elements: vec![vec!["Z34".to_string(), "REF_A".to_string()]],
6473                    mig_number: None,
6474                    segment_number: None,
6475                },
6476                AssembledSegment {
6477                    tag: "RFF".to_string(),
6478                    elements: vec![vec!["Z34".to_string(), "REF_B".to_string()]],
6479                    mig_number: None,
6480                    segment_number: None,
6481                },
6482                AssembledSegment {
6483                    tag: "RFF".to_string(),
6484                    elements: vec![vec!["Z34".to_string(), "REF_C".to_string()]],
6485                    mig_number: None,
6486                    segment_number: None,
6487                },
6488                AssembledSegment {
6489                    tag: "RFF".to_string(),
6490                    elements: vec![vec!["Z35".to_string(), "OTHER".to_string()]],
6491                    mig_number: None,
6492                    segment_number: None,
6493                },
6494            ],
6495            child_groups: vec![],
6496            entry_mig_number: None,
6497            variant_mig_numbers: vec![],
6498            skipped_segments: vec![],
6499            skipped_positions: Vec::new(),
6500        };
6501
6502        // Wildcard collect: rff[Z34,*] should collect all 3 RFF+Z34 values
6503        let all = MappingEngine::extract_all_from_instance(&instance, "rff[Z34,*].0.1");
6504        assert_eq!(all, vec!["REF_A", "REF_B", "REF_C"]);
6505
6506        // Non-wildcard still returns single value via extract_from_instance
6507        let single = MappingEngine::extract_from_instance(&instance, "rff[Z34].0.1");
6508        assert_eq!(single, Some("REF_A".to_string()));
6509
6510        let second = MappingEngine::extract_from_instance(&instance, "rff[Z34,1].0.1");
6511        assert_eq!(second, Some("REF_B".to_string()));
6512    }
6513}