//! High-level [`Mapper`] API for EDIFACT-to-BO4E conversion.

use std::collections::HashMap;
use std::sync::Mutex;

use mig_assembly::ConversionService;
use mig_bo4e::engine::DataBundle;
use mig_bo4e::MappingEngine;

use crate::data_dir::DataDir;
use crate::error::MapperError;

/// Result of a BO4E mapping operation.
pub struct Bo4eResult {
    /// The PID (Pruefidentifikator) that was detected or specified.
    pub pid: String,
    /// The EDIFACT message type (e.g., "UTILMD", "MSCONS").
    pub message_type: String,
    /// The message variant (e.g., "UTILMD_Strom", "MSCONS").
    pub variant: String,
    /// The mapped BO4E JSON output.
    pub bo4e: serde_json::Value,
}

/// High-level facade for EDIFACT-to-BO4E conversion.
///
/// Wraps [`DataBundle`] loading with lazy/eager initialization, and provides
/// convenient accessors for [`ConversionService`] and [`MappingEngine`] instances.
///
/// # Example
///
/// ```ignore
/// use edifact_mapper::{DataDir, Mapper};
///
/// let mapper = Mapper::from_data_dir(
///     DataDir::path("/opt/edifact/data").eager(&["FV2504"])
/// )?;
///
/// let cs = mapper.conversion_service("FV2504", "UTILMD_Strom")?;
/// let engine = mapper.engine("FV2504", "UTILMD_Strom", "55001")?;
/// ```
pub struct Mapper {
    data_dir: DataDir,
    bundles: Mutex<HashMap<String, DataBundle>>,
}

impl Mapper {
    /// Create a new `Mapper` from a [`DataDir`] configuration.
    ///
    /// Any format versions marked as [`eager`](DataDir::eager) are loaded immediately.
    /// All others are loaded lazily on first access.
    pub fn from_data_dir(data_dir: DataDir) -> Result<Self, MapperError> {
        let mapper = Self {
            data_dir,
            bundles: Mutex::new(HashMap::new()),
        };
        let eager_fvs: Vec<String> = mapper.data_dir.eager_fvs().to_vec();
        for fv in &eager_fvs {
            mapper.ensure_bundle_loaded(fv)?;
        }
        Ok(mapper)
    }

    /// Ensure that the bundle for `fv` is loaded into memory.
    fn ensure_bundle_loaded(&self, fv: &str) -> Result<(), MapperError> {
        let mut bundles = self.bundles.lock().unwrap();
        if bundles.contains_key(fv) {
            return Ok(());
        }
        let path = self.data_dir.bundle_path(fv);
        if !path.exists() {
            return Err(MapperError::BundleNotFound { fv: fv.to_string() });
        }
        let bundle = DataBundle::load(&path)?;
        bundles.insert(fv.to_string(), bundle);
        Ok(())
    }

    /// Get a [`ConversionService`] for the given format version and variant.
    ///
    /// The service can tokenize EDIFACT input and assemble it into a MIG tree.
    pub fn conversion_service(
        &self,
        fv: &str,
        variant: &str,
    ) -> Result<ConversionService, MapperError> {
        self.ensure_bundle_loaded(fv)?;
        let bundles = self.bundles.lock().unwrap();
        let bundle = bundles.get(fv).unwrap();
        let vc = bundle
            .variant(variant)
            .ok_or_else(|| MapperError::VariantNotFound {
                fv: fv.to_string(),
                variant: variant.to_string(),
            })?;
        let mig = vc
            .mig_schema
            .as_ref()
            .ok_or_else(|| MapperError::VariantNotFound {
                fv: fv.to_string(),
                variant: format!("{variant} (no MIG schema in bundle)"),
            })?;
        Ok(ConversionService::from_mig(mig.clone()))
    }

    /// Get a [`MappingEngine`] for a specific PID within a format version and variant.
    ///
    /// The engine can convert between assembled MIG trees and BO4E JSON.
    pub fn engine(&self, fv: &str, variant: &str, pid: &str) -> Result<MappingEngine, MapperError> {
        self.ensure_bundle_loaded(fv)?;
        let bundles = self.bundles.lock().unwrap();
        let bundle = bundles.get(fv).unwrap();
        let vc = bundle
            .variant(variant)
            .ok_or_else(|| MapperError::VariantNotFound {
                fv: fv.to_string(),
                variant: variant.to_string(),
            })?;
        let pid_key = format!("pid_{pid}");
        let defs = vc
            .combined_defs
            .get(&pid_key)
            .ok_or_else(|| MapperError::PidNotFound {
                fv: fv.to_string(),
                variant: variant.to_string(),
                pid: pid.to_string(),
            })?;
        Ok(MappingEngine::from_definitions(defs.clone()))
    }

    /// Validate a BO4E JSON object against PID requirements.
    ///
    /// Returns a list of validation errors. Empty list = valid.
    /// The `json` should be the transaction-level stammdaten (the entity map).
    pub fn validate_pid(
        &self,
        json: &serde_json::Value,
        fv: &str,
        variant: &str,
        pid: &str,
    ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
        self.ensure_bundle_loaded(fv)?;
        let bundles = self.bundles.lock().unwrap();
        let bundle = bundles.get(fv).unwrap();
        let vc = bundle
            .variant(variant)
            .ok_or_else(|| MapperError::VariantNotFound {
                fv: fv.to_string(),
                variant: variant.to_string(),
            })?;
        let pid_key = format!("pid_{pid}");
        let requirements =
            vc.pid_requirements
                .get(&pid_key)
                .ok_or_else(|| MapperError::PidNotFound {
                    fv: fv.to_string(),
                    variant: variant.to_string(),
                    pid: pid.to_string(),
                })?;

        Ok(mig_bo4e::pid_validation::validate_pid_json_transaction(
            json,
            requirements,
        ))
    }

    /// Validate a typed BO4E struct against PID requirements.
    ///
    /// Convenience wrapper that serializes the struct to JSON first.
    /// Works with any `Pid*Interchange` or `Pid*MessageStammdaten` type.
    ///
    /// # Example
    /// ```ignore
    /// let interchange = build_55001_interchange();
    /// let errors = mapper.validate_pid_struct(&interchange, "FV2504", "UTILMD_Strom", "55001")?;
    /// assert!(errors.is_empty(), "Errors:\n{}", ValidationReport(errors));
    /// ```
    pub fn validate_pid_struct(
        &self,
        value: &impl serde::Serialize,
        fv: &str,
        variant: &str,
        pid: &str,
    ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
        let json = serde_json::to_value(value).map_err(|e| {
            MapperError::Mapping(mig_bo4e::MappingError::TypeConversion(e.to_string()))
        })?;
        self.validate_pid(&json, fv, variant, pid)
    }

    /// Validate with AHB condition awareness.
    ///
    /// Reverse-maps the JSON to EDIFACT segments, evaluates AHB conditions,
    /// and reports fields as required/optional based on the actual data present.
    ///
    /// Falls back to basic validation (without conditions) if no condition
    /// evaluator is available for the given variant/format version combination.
    pub fn validate_pid_with_conditions(
        &self,
        json: &serde_json::Value,
        fv: &str,
        variant: &str,
        pid: &str,
    ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
        self.ensure_bundle_loaded(fv)?;
        let bundles = self.bundles.lock().unwrap();
        let bundle = bundles.get(fv).unwrap();
        let vc = bundle
            .variant(variant)
            .ok_or_else(|| MapperError::VariantNotFound {
                fv: fv.to_string(),
                variant: variant.to_string(),
            })?;
        let pid_key = format!("pid_{pid}");

        let requirements =
            vc.pid_requirements
                .get(&pid_key)
                .ok_or_else(|| MapperError::PidNotFound {
                    fv: fv.to_string(),
                    variant: variant.to_string(),
                    pid: pid.to_string(),
                })?;

        // Try to get a condition evaluator for this variant
        let evaluator = crate::evaluator_factory::create_evaluator(variant, fv);

        if let Some(evaluator) = evaluator {
            // Reverse-map JSON to EDIFACT segments for condition evaluation context
            let defs = vc
                .combined_defs
                .get(&pid_key)
                .ok_or_else(|| MapperError::PidNotFound {
                    fv: fv.to_string(),
                    variant: variant.to_string(),
                    pid: pid.to_string(),
                })?;
            let engine = MappingEngine::from_definitions(defs.clone());
            let tree = engine.map_all_reverse(json, None);

            // Convert AssembledTree to flat OwnedSegments for EvaluationContext
            let segments = crate::tree_to_segments::tree_to_owned_segments(&tree);

            // Validate with condition awareness
            Ok(crate::evaluator_factory::validate_with_boxed_evaluator(
                evaluator.as_ref(),
                json,
                requirements,
                pid,
                &segments,
            ))
        } else {
            // No evaluator available — fall back to basic validation
            Ok(mig_bo4e::pid_validation::validate_pid_json_transaction(
                json,
                requirements,
            ))
        }
    }

    /// Convert BO4E JSON back to an EDIFACT string.
    ///
    /// Takes message-level stammdaten, a slice of per-transaction stammdaten,
    /// and produces an EDIFACT message body (UNH through UNT content segments,
    /// without UNB/UNZ interchange envelope).
    ///
    /// # Arguments
    ///
    /// * `msg_stammdaten` — message-level entities (e.g., Marktteilnehmer from SG2)
    /// * `tx_stammdaten` — per-transaction entities (one per transaction/SG4 instance)
    /// * `fv` — format version (e.g., "FV2504")
    /// * `variant` — message variant (e.g., "UTILMD_Strom")
    /// * `pid` — Pruefidentifikator (e.g., "55001")
    ///
    /// # Example
    ///
    /// ```ignore
    /// let edifact = mapper.to_edifact(
    ///     &msg_json,
    ///     &[tx_json],
    ///     "FV2504",
    ///     "UTILMD_Strom",
    ///     "55001",
    /// )?;
    /// ```
    pub fn to_edifact(
        &self,
        msg_stammdaten: &serde_json::Value,
        tx_stammdaten: &[serde_json::Value],
        fv: &str,
        variant: &str,
        pid: &str,
    ) -> Result<String, MapperError> {
        self.ensure_bundle_loaded(fv)?;
        let bundles = self.bundles.lock().unwrap();
        let bundle = bundles.get(fv).unwrap();
        let vc = bundle
            .variant(variant)
            .ok_or_else(|| MapperError::VariantNotFound {
                fv: fv.to_string(),
                variant: variant.to_string(),
            })?;

        let tx_group = vc
            .tx_group(pid)
            .ok_or_else(|| MapperError::PidNotFound {
                fv: fv.to_string(),
                variant: variant.to_string(),
                pid: pid.to_string(),
            })?;

        let msg_engine = vc.msg_engine();
        let tx_engine =
            vc.tx_engine(pid)
                .ok_or_else(|| MapperError::PidNotFound {
                    fv: fv.to_string(),
                    variant: variant.to_string(),
                    pid: pid.to_string(),
                })?;

        let filtered_mig =
            vc.filtered_mig(pid)
                .ok_or_else(|| MapperError::NoMigSchema {
                    fv: fv.to_string(),
                    variant: variant.to_string(),
                })?;

        // Build MappedMessage from the provided JSON
        let transaktionen: Vec<mig_bo4e::model::MappedTransaktion> = tx_stammdaten
            .iter()
            .map(|tx| mig_bo4e::model::MappedTransaktion {
                stammdaten: tx.clone(),
                nesting_info: Default::default(),
            })
            .collect();
        let mapped = mig_bo4e::model::MappedMessage {
            stammdaten: msg_stammdaten.clone(),
            transaktionen,
            nesting_info: Default::default(),
        };

        // Reverse map → AssembledTree
        let tree = MappingEngine::map_interchange_reverse(
            &msg_engine,
            &tx_engine,
            &mapped,
            tx_group,
            Some(&filtered_mig),
        );

        // Disassemble → ordered segments
        let disassembler =
            mig_assembly::disassembler::Disassembler::new(&filtered_mig);
        let segments = disassembler.disassemble(&tree);

        // Render to EDIFACT string with default delimiters
        let delimiters = edifact_primitives::EdifactDelimiters::default();
        Ok(mig_assembly::renderer::render_edifact(
            &segments,
            &delimiters,
        ))
    }

    /// Convert a typed BO4E struct to an EDIFACT string.
    ///
    /// Convenience wrapper that serializes the struct to JSON first.
    /// The struct should serialize to the `Nachricht` shape:
    /// `{ "stammdaten": {...}, "transaktionen": [{...}] }`
    pub fn to_edifact_struct(
        &self,
        nachricht: &impl serde::Serialize,
        fv: &str,
        variant: &str,
        pid: &str,
    ) -> Result<String, MapperError> {
        let json = serde_json::to_value(nachricht)
            .map_err(|e| MapperError::Serialization(e.to_string()))?;

        let msg_stammdaten = json
            .get("stammdaten")
            .cloned()
            .unwrap_or(serde_json::Value::Object(Default::default()));

        let tx_stammdaten: Vec<serde_json::Value> = json
            .get("transaktionen")
            .and_then(|v| v.as_array())
            .cloned()
            .unwrap_or_default();

        self.to_edifact(&msg_stammdaten, &tx_stammdaten, fv, variant, pid)
    }

    /// Parse an EDIFACT interchange string into a typed PID interchange struct.
    ///
    /// Runs the full pipeline: tokenize → split messages → assemble → forward-map → deserialize.
    /// The type parameters `M` and `T` are the message-level and transaction-level
    /// stammdaten types from the generated PID module.
    ///
    /// # Example
    ///
    /// ```ignore
    /// use bo4e_edifact_types::generated::fv2504::utilmd::pids::pid_55001::*;
    ///
    /// let interchange: Interchange<Pid55001MsgStammdaten, Pid55001TxStammdaten> =
    ///     mapper.from_edifact(edifact_str, "FV2504", "UTILMD_Strom", "55001")?;
    ///
    /// let tx = &interchange.nachrichten[0].transaktionen[0];
    /// println!("Vorgang: {}", tx.prozessdaten.vorgang_id);
    /// ```
    pub fn from_edifact<M, T>(
        &self,
        edifact: &str,
        fv: &str,
        variant: &str,
        pid: &str,
    ) -> Result<mig_bo4e::model::Interchange<M, T>, MapperError>
    where
        M: serde::de::DeserializeOwned,
        T: serde::de::DeserializeOwned,
    {
        self.ensure_bundle_loaded(fv)?;
        let bundles = self.bundles.lock().unwrap();
        let bundle = bundles.get(fv).unwrap();
        let vc = bundle
            .variant(variant)
            .ok_or_else(|| MapperError::VariantNotFound {
                fv: fv.to_string(),
                variant: variant.to_string(),
            })?;

        let tx_group = vc
            .tx_group(pid)
            .ok_or_else(|| MapperError::PidNotFound {
                fv: fv.to_string(),
                variant: variant.to_string(),
                pid: pid.to_string(),
            })?;

        let msg_engine = vc.msg_engine();
        let tx_engine =
            vc.tx_engine(pid)
                .ok_or_else(|| MapperError::PidNotFound {
                    fv: fv.to_string(),
                    variant: variant.to_string(),
                    pid: pid.to_string(),
                })?;

        let filtered_mig =
            vc.filtered_mig(pid)
                .ok_or_else(|| MapperError::NoMigSchema {
                    fv: fv.to_string(),
                    variant: variant.to_string(),
                })?;

        // Tokenize → split → assemble
        let svc = ConversionService::from_mig(filtered_mig);
        let (chunks, trees) = svc.convert_interchange_to_trees(edifact)?;

        let tree = trees
            .first()
            .ok_or_else(|| MapperError::Assembly(
                mig_assembly::AssemblyError::ParseError("No messages in interchange".to_string()),
            ))?;

        // Extract envelope metadata
        let interchangedaten =
            mig_bo4e::model::extract_interchangedaten(&chunks.envelope);
        let msg_chunk = chunks.messages.first().ok_or_else(|| {
            MapperError::Assembly(mig_assembly::AssemblyError::ParseError(
                "No message chunks".to_string(),
            ))
        })?;
        let (unh_ref, nachrichten_typ) =
            mig_bo4e::model::extract_unh_fields(&msg_chunk.unh);
        let nachrichtendaten = mig_bo4e::model::Nachrichtendaten {
            unh_referenz: unh_ref,
            nachrichten_typ,
        };

        // Forward-map to typed interchange
        MappingEngine::map_interchange_typed::<M, T>(
            &msg_engine,
            &tx_engine,
            tree,
            tx_group,
            true,
            nachrichtendaten,
            interchangedaten,
        )
        .map_err(|e| MapperError::Serialization(e.to_string()))
    }

    /// Get the UNH association code for a variant (e.g., `"S2.1"`, `"2.4c"`).
    ///
    /// This is the version string from the MIG schema, used as the last component
    /// of the UNH S009 composite: `UTILMD:D:11A:UN:S2.1`.
    ///
    /// # Example
    /// ```ignore
    /// let code = mapper.association_code("FV2604", "UTILMD_Strom")?;
    /// assert_eq!(code, "S2.1");
    /// ```
    pub fn association_code(&self, fv: &str, variant: &str) -> Result<String, MapperError> {
        let meta = self.message_metadata(fv, variant)?;
        Ok(meta.association_code)
    }

    /// Get full message metadata for a variant, including the UNH S009 components.
    ///
    /// Returns the message type, UN/EDIFACT release code, and association code
    /// needed to construct UNH segments.
    pub fn message_metadata(
        &self,
        fv: &str,
        variant: &str,
    ) -> Result<MessageMetadata, MapperError> {
        self.ensure_bundle_loaded(fv)?;
        let bundles = self.bundles.lock().unwrap();
        let bundle = bundles.get(fv).unwrap();
        let vc = bundle
            .variant(variant)
            .ok_or_else(|| MapperError::VariantNotFound {
                fv: fv.to_string(),
                variant: variant.to_string(),
            })?;
        let mig = vc
            .mig_schema
            .as_ref()
            .ok_or_else(|| MapperError::NoMigSchema {
                fv: fv.to_string(),
                variant: variant.to_string(),
            })?;
        Ok(MessageMetadata {
            message_type: mig.message_type.clone(),
            release: release_code_for_message_type(&mig.message_type),
            association_code: mig.version.clone(),
        })
    }

    /// Convert BO4E JSON to a complete EDIFACT interchange with envelope segments.
    ///
    /// Produces a full interchange including UNA, UNB, UNH, message body, UNT, and UNZ.
    ///
    /// # Example
    /// ```ignore
    /// let edifact = mapper.to_edifact_interchange(
    ///     &InterchangeEnvelope {
    ///         sender: EdifactParty::bdew("9900000000003"),
    ///         receiver: EdifactParty::bdew("9900000000001"),
    ///         interchange_ref: "REF001".to_string(),
    ///     },
    ///     &[InterchangeMessage {
    ///         message_ref: "MSG001".to_string(),
    ///         msg_stammdaten: serde_json::json!({"marktteilnehmer": []}),
    ///         tx_stammdaten: vec![serde_json::json!({"prozessdaten": {"pruefidentifikator": "55001"}})],
    ///         fv: "FV2604".to_string(),
    ///         variant: "UTILMD_Strom".to_string(),
    ///         pid: "55001".to_string(),
    ///     }],
    /// )?;
    /// assert!(edifact.starts_with("UNA:+.? '"));
    /// ```
    pub fn to_edifact_interchange(
        &self,
        envelope: &InterchangeEnvelope,
        messages: &[InterchangeMessage],
    ) -> Result<String, MapperError> {
        let delimiters = edifact_primitives::EdifactDelimiters::default();
        let sep = delimiters.component as char;
        let elem = delimiters.element as char;
        let seg_term = delimiters.segment as char;

        let mut output = String::new();

        // UNA — Service string advice
        output.push_str(&format!(
            "UNA{}{}{}{}{}{}",
            sep,                            // component separator
            elem,                           // element separator
            delimiters.decimal as char,     // decimal notation
            delimiters.release as char,     // release/escape character
            ' ',                            // reserved (space)
            seg_term,                       // segment terminator
        ));

        // UNB — Interchange header
        let now = chrono::Utc::now();
        let date_str = now.format("%y%m%d").to_string();
        let time_str = now.format("%H%M").to_string();
        let sender = &envelope.sender;
        let receiver = &envelope.receiver;
        let interchange_ref = &envelope.interchange_ref;
        output.push_str(&format!(
            "UNB{elem}UNOC{sep}3{elem}{sid}{sep}{sq}{elem}{rid}{sep}{rq}{elem}{date_str}{sep}{time_str}{elem}{interchange_ref}{seg_term}",
            sid = sender.id,
            sq = sender.qualifier,
            rid = receiver.id,
            rq = receiver.qualifier,
        ));

        let mut message_count = 0u32;

        for msg in messages {
            let meta = self.message_metadata(&msg.fv, &msg.variant)?;

            // Generate body segments
            let body = self.to_edifact(
                &msg.msg_stammdaten,
                &msg.tx_stammdaten,
                &msg.fv,
                &msg.variant,
                &msg.pid,
            )?;

            // Count segments in body (split by segment terminator, filter empty)
            let body_seg_count = body
                .split(seg_term)
                .filter(|s: &&str| !s.is_empty())
                .count();
            // UNH + body segments + UNT = total segment count
            let segment_count = body_seg_count + 2;

            // UNH — Message header
            output.push_str(&format!(
                "UNH{elem}{ref}{elem}{msg_type}{sep}D{sep}{release}{sep}UN{sep}{assoc}{seg_term}",
                ref = msg.message_ref,
                msg_type = meta.message_type,
                release = meta.release,
                assoc = meta.association_code,
            ));

            // Body segments
            output.push_str(&body);

            // UNT — Message trailer
            output.push_str(&format!(
                "UNT{elem}{segment_count}{elem}{ref}{seg_term}",
                ref = msg.message_ref,
            ));

            message_count += 1;
        }

        // UNZ — Interchange trailer
        output.push_str(&format!(
            "UNZ{elem}{message_count}{elem}{interchange_ref}{seg_term}",
        ));

        Ok(output)
    }

    /// List all format versions currently loaded in memory.
    pub fn loaded_format_versions(&self) -> Vec<String> {
        self.bundles.lock().unwrap().keys().cloned().collect()
    }

    /// List all variants available in a format version's bundle.
    ///
    /// Loads the bundle if not already loaded.
    pub fn variants(&self, fv: &str) -> Result<Vec<String>, MapperError> {
        self.ensure_bundle_loaded(fv)?;
        let bundles = self.bundles.lock().unwrap();
        let bundle = bundles.get(fv).unwrap();
        Ok(bundle.variants.keys().cloned().collect())
    }
}

/// Metadata about a message type needed for constructing UNH segments.
#[derive(Debug, Clone)]
pub struct MessageMetadata {
    /// EDIFACT message type (e.g., `"UTILMD"`, `"MSCONS"`).
    pub message_type: String,
    /// UN/EDIFACT directory release code (e.g., `"11A"`, `"04B"`).
    pub release: String,
    /// Association-assigned code / MIG version (e.g., `"S2.1"`, `"2.4c"`).
    pub association_code: String,
}

/// Envelope parameters for [`Mapper::to_edifact_interchange`].
#[derive(Debug, Clone)]
pub struct InterchangeEnvelope {
    /// Sender party (UNB S002).
    pub sender: EdifactParty,
    /// Receiver party (UNB S003).
    pub receiver: EdifactParty,
    /// Unique interchange reference (UNB 0020 / UNZ 0020).
    pub interchange_ref: String,
}

/// An EDIFACT interchange party (sender or receiver) with codelist qualifier.
#[derive(Debug, Clone)]
pub struct EdifactParty {
    /// Party identification (e.g., MP-ID `"9900000000003"` or GLN `"4045458000000"`).
    pub id: String,
    /// Codelist qualifier: `"500"` = BDEW, `"14"` = GS1/EAN.
    pub qualifier: String,
}

impl EdifactParty {
    /// Create a party with BDEW codelist qualifier (500).
    pub fn bdew(id: &str) -> Self {
        Self {
            id: id.to_string(),
            qualifier: "500".to_string(),
        }
    }

    /// Create a party with GS1/EAN codelist qualifier (14).
    pub fn gs1(id: &str) -> Self {
        Self {
            id: id.to_string(),
            qualifier: "14".to_string(),
        }
    }
}

/// A single message to include in an interchange built by
/// [`Mapper::to_edifact_interchange`].
#[derive(Debug, Clone)]
pub struct InterchangeMessage {
    /// Unique message reference number (used in UNH/UNT).
    pub message_ref: String,
    /// Message-level stammdaten (e.g., marktteilnehmer).
    pub msg_stammdaten: serde_json::Value,
    /// Transaction-level stammdaten (one per transaction).
    pub tx_stammdaten: Vec<serde_json::Value>,
    /// Format version (e.g., `"FV2604"`).
    pub fv: String,
    /// Message variant (e.g., `"UTILMD_Strom"`).
    pub variant: String,
    /// Pruefidentifikator (e.g., `"55001"`).
    pub pid: String,
}

/// UN/EDIFACT directory release code for a message type.
///
/// These are stable per-message-type constants from the BDEW/DVGW specifications.
fn release_code_for_message_type(msg_type: &str) -> String {
    match msg_type {
        "APERAK" => "07B",
        "COMDIS" => "17A",
        "CONTRL" => "04B",
        "IFTSTA" => "18A",
        "INSRPT" => "18A",
        "INVOIC" => "06A",
        "MSCONS" => "04B",
        "ORDCHG" => "09B",
        "ORDERS" => "09B",
        "ORDRSP" => "10A",
        "PARTIN" => "20B",
        "PRICAT" => "20B",
        "QUOTES" => "10A",
        "REMADV" => "05A",
        "REQOTE" => "10A",
        "UTILMD" => "11A",
        "UTILTS" => "18A",
        _ => "04B", // fallback
    }
    .to_string()
}


#[cfg(test)]
mod tests {
    use super::*;
    use std::path::Path;

    fn data_dir() -> Option<std::path::PathBuf> {
        // Try dist/ first (pre-built data bundles), then cache/mappings/
        let dist = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../dist");
        if dist.join("edifact-data-FV2504.bin").exists() {
            return Some(dist);
        }
        let cache = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../cache/mappings");
        if cache.join("FV2504").exists() {
            return Some(cache);
        }
        eprintln!("Skipping test: no DataBundle files found");
        None
    }

    #[test]
    fn test_to_edifact_produces_edifact_output() {
        let Some(data_dir) = data_dir() else {
            return;
        };
        let mapper =
            Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();

        let msg_stammdaten = serde_json::json!({
            "marktteilnehmer": [{
                "marktrolle": "MS",
                "rollencodenummer": "9900123456789",
                "codepflegeCode": "293"
            }]
        });
        let tx_stammdaten = serde_json::json!({
            "prozessdaten": {
                "pruefidentifikator": "55001",
                "vorgangId": "ABC123",
                "transaktionsgrund": "E01"
            }
        });

        let result = mapper.to_edifact(
            &msg_stammdaten,
            &[tx_stammdaten],
            "FV2504",
            "UTILMD_Strom",
            "55001",
        );
        assert!(result.is_ok(), "to_edifact failed: {:?}", result.err());
        let edifact = result.unwrap();
        assert!(!edifact.is_empty(), "EDIFACT output should not be empty");
        // Should produce NAD segment from marktteilnehmer
        assert!(edifact.contains("NAD"), "Should contain NAD segment");
        // Should produce IDE segment from prozessdaten
        assert!(edifact.contains("IDE"), "Should contain IDE segment");
    }

    #[test]
    fn test_to_edifact_struct_produces_edifact_output() {
        let Some(data_dir) = data_dir() else {
            return;
        };
        let mapper =
            Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();

        let nachricht = serde_json::json!({
            "stammdaten": {
                "marktteilnehmer": [{
                    "marktrolle": "MS",
                    "rollencodenummer": "9900123456789",
                    "codepflegeCode": "293"
                }]
            },
            "transaktionen": [{
                "prozessdaten": {
                    "pruefidentifikator": "55001",
                    "vorgangId": "ABC123"
                }
            }]
        });

        let result = mapper.to_edifact_struct(&nachricht, "FV2504", "UTILMD_Strom", "55001");
        assert!(
            result.is_ok(),
            "to_edifact_struct failed: {:?}",
            result.err()
        );
        let edifact = result.unwrap();
        assert!(!edifact.is_empty(), "EDIFACT output should not be empty");
    }

    #[test]
    fn test_to_edifact_invalid_fv_returns_error() {
        let Some(data_dir) = data_dir() else {
            return;
        };
        let mapper =
            Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();

        let result = mapper.to_edifact(
            &serde_json::json!({}),
            &[serde_json::json!({})],
            "FV9999",
            "UTILMD_Strom",
            "55001",
        );
        assert!(result.is_err());
    }

    #[test]
    fn test_to_edifact_invalid_variant_returns_error() {
        let Some(data_dir) = data_dir() else {
            return;
        };
        let mapper =
            Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();

        let result = mapper.to_edifact(
            &serde_json::json!({}),
            &[serde_json::json!({})],
            "FV2504",
            "NONEXISTENT",
            "55001",
        );
        assert!(result.is_err());
    }

    #[test]
    fn test_to_edifact_invalid_pid_returns_error() {
        let Some(data_dir) = data_dir() else {
            return;
        };
        let mapper =
            Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();

        let result = mapper.to_edifact(
            &serde_json::json!({}),
            &[serde_json::json!({})],
            "FV2504",
            "UTILMD_Strom",
            "99999",
        );
        assert!(result.is_err());
    }

    #[test]
    fn test_association_code() {
        let Some(data_dir) = data_dir() else {
            return;
        };
        let mapper =
            Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();

        let code = mapper.association_code("FV2504", "UTILMD_Strom").unwrap();
        assert_eq!(code, "S2.1");

        let code = mapper.association_code("FV2504", "MSCONS").unwrap();
        assert_eq!(code, "2.4c");
    }

    #[test]
    fn test_message_metadata() {
        let Some(data_dir) = data_dir() else {
            return;
        };
        let mapper =
            Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();

        let meta = mapper.message_metadata("FV2504", "UTILMD_Strom").unwrap();
        assert_eq!(meta.message_type, "UTILMD");
        assert_eq!(meta.release, "11A");
        assert_eq!(meta.association_code, "S2.1");
    }

    #[test]
    fn test_to_edifact_interchange() {
        let Some(data_dir) = data_dir() else {
            return;
        };
        let mapper =
            Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();

        let result = mapper.to_edifact_interchange(
            &InterchangeEnvelope {
                sender: EdifactParty::bdew("9900000000003"),
                receiver: EdifactParty::bdew("9900000000001"),
                interchange_ref: "REF001".to_string(),
            },
            &[InterchangeMessage {
                message_ref: "MSG001".to_string(),
                msg_stammdaten: serde_json::json!({
                    "marktteilnehmer": [{
                        "marktrolle": "MS",
                        "rollencodenummer": "9900123456789",
                        "codepflegeCode": "293"
                    }]
                }),
                tx_stammdaten: vec![serde_json::json!({
                    "prozessdaten": {
                        "pruefidentifikator": "55001",
                        "vorgangId": "ABC123",
                        "transaktionsgrund": "E01"
                    }
                })],
                fv: "FV2504".to_string(),
                variant: "UTILMD_Strom".to_string(),
                pid: "55001".to_string(),
            }],
        );
        assert!(
            result.is_ok(),
            "to_edifact_interchange failed: {:?}",
            result.err()
        );
        let edifact = result.unwrap();

        // Verify envelope structure
        assert!(edifact.starts_with("UNA:+.? '"), "Should start with UNA");
        assert!(edifact.contains("UNB+UNOC:3+9900000000003:500+9900000000001:500+"),
            "Should contain UNB with sender/receiver");
        assert!(edifact.contains("UNH+MSG001+UTILMD:D:11A:UN:S2.1'"),
            "Should contain UNH with correct S009");
        assert!(edifact.contains("NAD"), "Should contain body NAD segment");
        assert!(edifact.contains("UNT+"), "Should contain UNT");
        assert!(edifact.contains("+MSG001'"), "UNT should reference message ref");
        assert!(edifact.contains("UNZ+1+REF001'"), "Should contain UNZ with count and ref");
    }
}
