//! PID field requirements — built from PID schema JSON + TOML mappings.
//!
//! Used by `validate_pid()` to check whether a populated BO4E struct
//! has all required fields for a given PID.

use std::collections::BTreeMap;
use std::path::Path;

use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::definition::{FieldMapping, MappingDefinition};
use crate::error::MappingError;
use crate::pid_schema_index::PidSchemaIndex;
use crate::MappingEngine;

/// Complete field requirements for a single PID.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PidRequirements {
    pub pid: String,
    pub beschreibung: String,
    pub entities: Vec<EntityRequirement>,
}

/// Requirements for one BO4E entity within a PID.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityRequirement {
    pub entity: String,
    pub bo4e_type: String,
    pub companion_type: Option<String>,
    pub ahb_status: String,
    pub is_array: bool,
    pub fields: Vec<FieldRequirement>,
}

/// Requirements for a single field within an entity.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldRequirement {
    pub bo4e_name: String,
    pub ahb_status: String,
    pub is_companion: bool,
    pub field_type: String,
    pub format: Option<String>,
    pub enum_name: Option<String>,
    pub valid_codes: Vec<CodeValue>,
}

/// A valid code value with its human-readable meaning.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeValue {
    pub code: String,
    pub meaning: String,
}

// ── Loading ────────────────────────────────────────────────────────────────

/// Load all TOML mapping definitions for a PID from the standard directory layout.
///
/// Pattern:
/// - `message_dir` — message-level definitions (unfiltered, always included)
/// - `common_dir` — common/shared definitions, filtered by PID schema
/// - `pid_dir` — PID-specific definitions (override common where matching)
pub fn load_definitions_for_pid(
    common_dir: &Path,
    pid_dir: &Path,
    message_dir: &Path,
    schema: &Value,
) -> Result<Vec<MappingDefinition>, MappingError> {
    let mut all_defs = Vec::new();

    // 1. Message-level definitions (always included, no schema filtering)
    if message_dir.exists() {
        let msg_engine = MappingEngine::load(message_dir)?;
        all_defs.extend(msg_engine.definitions().to_vec());
    }

    // 2. Common definitions filtered by schema + PID overrides
    let schema_index = PidSchemaIndex::from_json(schema);
    if common_dir.exists() && pid_dir.exists() {
        let tx_engine = MappingEngine::load_with_common(common_dir, pid_dir, &schema_index)?;
        all_defs.extend(tx_engine.definitions().to_vec());
    } else if common_dir.exists() {
        let common_engine = MappingEngine::load_common_only(common_dir, &schema_index)?;
        all_defs.extend(common_engine.definitions().to_vec());
    } else if pid_dir.exists() {
        let pid_engine = MappingEngine::load(pid_dir)?;
        all_defs.extend(pid_engine.definitions().to_vec());
    }

    Ok(all_defs)
}

// ── Building PidRequirements ───────────────────────────────────────────────

/// Intermediate builder for accumulating fields per entity.
struct EntityBuilder {
    bo4e_type: String,
    companion_type: Option<String>,
    ahb_status: String,
    is_array: bool,
    fields: BTreeMap<String, FieldRequirement>,
}

impl PidRequirements {
    /// Build requirements by cross-referencing PID schema JSON with TOML definitions.
    ///
    /// For each definition, resolves its `source_path` against the schema to find the
    /// group's segments, then matches each TOML field path to a schema element to
    /// extract AHB status, valid codes, format, and enum name.
    pub fn from_schema_and_definitions(schema: &Value, definitions: &[MappingDefinition]) -> Self {
        let pid = schema
            .get("pid")
            .and_then(|v| {
                v.as_u64()
                    .map(|n| n.to_string())
                    .or_else(|| v.as_str().map(|s| s.to_string()))
            })
            .unwrap_or_default();

        let beschreibung = schema
            .get("beschreibung")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();

        // Build a synthetic group JSON from root_segments for message-level defs
        let root_group_json = schema
            .get("root_segments")
            .map(|rs| serde_json::json!({ "segments": rs }));

        let mut entity_map: BTreeMap<String, EntityBuilder> = BTreeMap::new();

        for def in definitions {
            let source_path = def.meta.source_path.as_deref().unwrap_or("");

            let group_json_owned: Value;
            let group_json: &Value = if source_path.is_empty() {
                // No source_path — try deriving from source_group, else use root_segments
                if let Some(g) = resolve_schema_group_fuzzy(schema, &def.meta.source_group) {
                    group_json_owned = g;
                    &group_json_owned
                } else {
                    match &root_group_json {
                        Some(g) => g,
                        None => continue,
                    }
                }
            } else {
                match resolve_schema_group(schema, source_path) {
                    Some(g) => g,
                    None => {
                        // Try fuzzy fallback with source_group
                        if let Some(g) = resolve_schema_group_fuzzy(schema, &def.meta.source_group)
                        {
                            group_json_owned = g;
                            &group_json_owned
                        } else {
                            continue;
                        }
                    }
                }
            };

            // Get the parent group's AHB status as fallback
            let parent_ahb = group_json
                .get("ahb_status")
                .and_then(|v| v.as_str())
                .unwrap_or("");

            let builder = entity_map
                .entry(def.meta.entity.clone())
                .or_insert_with(|| EntityBuilder {
                    bo4e_type: def.meta.bo4e_type.clone(),
                    companion_type: None,
                    ahb_status: parent_ahb.to_string(),
                    is_array: false,
                    fields: BTreeMap::new(),
                });

            if def.meta.companion_type.is_some() {
                builder.companion_type.clone_from(&def.meta.companion_type);
            }

            // Update ahb_status if we get a non-empty one
            if !parent_ahb.is_empty() && builder.ahb_status.is_empty() {
                builder.ahb_status = parent_ahb.to_string();
            }

            // Process [fields]
            process_field_section(
                &def.fields,
                group_json,
                parent_ahb,
                false,
                &mut builder.fields,
            );

            // Process [companion_fields]
            if let Some(companion) = &def.companion_fields {
                process_field_section(companion, group_json, parent_ahb, true, &mut builder.fields);
            }
        }

        // Detect Geschaeftspartner arrays: multiple SG12 variants in schema
        let geschaeftspartner_is_array = count_sg12_variants(schema) > 1;

        // Convert builders to EntityRequirements
        let entities: Vec<EntityRequirement> = entity_map
            .into_iter()
            .map(|(name, builder)| {
                let is_array =
                    builder.is_array || (name == "Geschaeftspartner" && geschaeftspartner_is_array);
                EntityRequirement {
                    entity: name,
                    bo4e_type: builder.bo4e_type,
                    companion_type: builder.companion_type,
                    ahb_status: builder.ahb_status,
                    is_array,
                    fields: builder.fields.into_values().collect(),
                }
            })
            .collect();

        PidRequirements {
            pid,
            beschreibung,
            entities,
        }
    }
}

// ── Schema Navigation Helpers ──────────────────────────────────────────────

/// Fuzzy-resolve a source_group like "SG4.SG8.SG10" against the schema.
///
/// Converts source_group parts to lowercase and finds ALL matching variant paths
/// in the schema tree, merging their segments into a synthetic group. This is needed
/// because the assembler merges same-ID groups (e.g., sg8_z01 + sg8_z75 → SG8),
/// and TOML mappings reference the merged result.
fn resolve_schema_group_fuzzy(schema: &Value, source_group: &str) -> Option<Value> {
    let parts: Vec<String> = source_group.split('.').map(|p| p.to_lowercase()).collect();
    if parts.is_empty() {
        return None;
    }

    let fields = schema.get("fields")?.as_object()?;

    // Collect ALL matching groups at each level and merge their segments
    let initial = find_all_matching_values(fields, &parts[0]);
    if initial.is_empty() {
        return None;
    }

    let mut current_groups: Vec<&Value> = initial;

    for part in &parts[1..] {
        let mut next_groups = Vec::new();
        for group in &current_groups {
            if let Some(children) = group.get("children").and_then(|c| c.as_object()) {
                next_groups.extend(find_all_matching_values(children, part));
            }
        }
        if next_groups.is_empty() {
            return None;
        }
        current_groups = next_groups;
    }

    // Merge all matching groups' segments into one synthetic group
    merge_groups_segments(&current_groups)
}

/// Find ALL values in a JSON object whose key matches exactly or has the given prefix followed by '_'.
fn find_all_matching_values<'a>(
    obj: &'a serde_json::Map<String, Value>,
    prefix: &str,
) -> Vec<&'a Value> {
    let mut results = Vec::new();
    // Exact match
    if let Some(v) = obj.get(prefix) {
        results.push(v);
        return results;
    }
    // Prefix match: "sg8" matches "sg8_z01", "sg8_z75", etc.
    let prefix_underscore = format!("{prefix}_");
    for (k, v) in obj {
        if k.starts_with(&prefix_underscore) {
            results.push(v);
        }
    }
    results
}

/// Merge segments from multiple schema groups into a single synthetic group.
///
/// Unions segments by tag (keeping all elements from all variants) and merges
/// ahb_status, taking the first non-empty one.
fn merge_groups_segments(groups: &[&Value]) -> Option<Value> {
    let mut merged_segments: Vec<Value> = Vec::new();
    let mut ahb_status = String::new();

    for group in groups {
        if ahb_status.is_empty() {
            if let Some(s) = group.get("ahb_status").and_then(|v| v.as_str()) {
                ahb_status = s.to_string();
            }
        }
        if let Some(segments) = group.get("segments").and_then(|v| v.as_array()) {
            for seg in segments {
                let seg_id = seg.get("id").and_then(|v| v.as_str()).unwrap_or("");
                // Check if we already have this segment tag
                if let Some(existing) = merged_segments
                    .iter_mut()
                    .find(|s| s.get("id").and_then(|v| v.as_str()).unwrap_or("") == seg_id)
                {
                    // Merge elements: add any elements/components not already present
                    merge_segment_elements(existing, seg);
                } else {
                    merged_segments.push(seg.clone());
                }
            }
        }
    }

    if merged_segments.is_empty() {
        return None;
    }

    let mut result = serde_json::json!({ "segments": merged_segments });
    if !ahb_status.is_empty() {
        result["ahb_status"] = Value::String(ahb_status);
    }
    Some(result)
}

/// Merge elements from `source` segment into `target` segment.
fn merge_segment_elements(target: &mut Value, source: &Value) {
    let source_elements = match source.get("elements").and_then(|v| v.as_array()) {
        Some(e) => e,
        None => return,
    };

    let target_elements = match target.get_mut("elements").and_then(|v| v.as_array_mut()) {
        Some(e) => e,
        None => return,
    };

    for src_elem in source_elements {
        // For composites, merge components (especially codes)
        if let Some(src_composite) = src_elem.get("composite").and_then(|v| v.as_str()) {
            if let Some(tgt_elem) = target_elements
                .iter_mut()
                .find(|e| e.get("composite").and_then(|v| v.as_str()) == Some(src_composite))
            {
                merge_composite_components(tgt_elem, src_elem);
            } else {
                target_elements.push(src_elem.clone());
            }
        } else if let Some(src_id) = src_elem.get("id").and_then(|v| v.as_str()) {
            // Simple element — add if not present
            let already_present = target_elements.iter().any(|e| {
                e.get("id").and_then(|v| v.as_str()) == Some(src_id) && e.get("composite").is_none()
            });
            if !already_present {
                target_elements.push(src_elem.clone());
            }
        }
    }
}

/// Merge components within a composite, unioning code values.
fn merge_composite_components(target: &mut Value, source: &Value) {
    let source_comps = match source.get("components").and_then(|v| v.as_array()) {
        Some(c) => c,
        None => return,
    };
    let target_comps = match target.get_mut("components").and_then(|v| v.as_array_mut()) {
        Some(c) => c,
        None => return,
    };

    for src_comp in source_comps {
        let src_id = src_comp.get("id").and_then(|v| v.as_str()).unwrap_or("");
        if let Some(tgt_comp) = target_comps
            .iter_mut()
            .find(|c| c.get("id").and_then(|v| v.as_str()).unwrap_or("") == src_id)
        {
            // Merge codes
            if let Some(src_codes) = src_comp.get("codes").and_then(|v| v.as_array()) {
                if let Some(tgt_codes) = tgt_comp.get_mut("codes").and_then(|v| v.as_array_mut()) {
                    for code in src_codes {
                        let code_val = code.get("value").and_then(|v| v.as_str()).unwrap_or("");
                        let already = tgt_codes.iter().any(|c| {
                            c.get("value").and_then(|v| v.as_str()).unwrap_or("") == code_val
                        });
                        if !already {
                            tgt_codes.push(code.clone());
                        }
                    }
                } else {
                    // Target has no codes, source does — add them
                    if let Some(tgt_comp_obj) = tgt_comp.as_object_mut() {
                        tgt_comp_obj.insert("codes".to_string(), Value::Array(src_codes.clone()));
                        // Also update type to "code" if source is code
                        if src_comp.get("type").and_then(|v| v.as_str()) == Some("code") {
                            tgt_comp_obj
                                .insert("type".to_string(), Value::String("code".to_string()));
                        }
                    }
                }
            }
        } else {
            target_comps.push(src_comp.clone());
        }
    }
}

/// Resolve a source_path like "sg4.sg5_z16" into the corresponding schema group JSON.
///
/// Navigates: `schema.fields.{part1}.children.{part2}.children.{part3}...`
fn resolve_schema_group<'a>(schema: &'a Value, source_path: &str) -> Option<&'a Value> {
    let parts: Vec<&str> = source_path.split('.').collect();
    if parts.is_empty() {
        return None;
    }

    let mut current = schema.get("fields")?.get(parts[0])?;

    for part in &parts[1..] {
        current = current.get("children")?.get(*part)?;
    }

    Some(current)
}

/// Process a TOML field section ([fields] or [companion_fields]) and resolve each
/// field against the schema to build FieldRequirements.
fn process_field_section(
    section: &indexmap::IndexMap<String, FieldMapping>,
    group_json: &Value,
    parent_ahb: &str,
    is_companion: bool,
    output: &mut BTreeMap<String, FieldRequirement>,
) {
    for (path, field_mapping) in section {
        let bo4e_name = match extract_target_name(field_mapping) {
            Some(name) if !name.is_empty() => name,
            _ => continue, // Skip empty targets / defaults-only
        };

        // Deduplicate by bo4e_name
        if output.contains_key(&bo4e_name) {
            continue;
        }

        let parsed = match parse_toml_field_path(path) {
            Some(p) => p,
            None => continue,
        };

        let (seg_tag, composite_or_element, component_id) = parsed;

        let field = if let Some(schema_elem) = find_schema_element(
            group_json,
            &seg_tag,
            &composite_or_element,
            component_id.as_deref(),
        ) {
            build_field_requirement(&schema_elem, &bo4e_name, is_companion, parent_ahb)
        } else {
            // Field not found in schema — create minimal field
            FieldRequirement {
                bo4e_name: bo4e_name.clone(),
                ahb_status: if parent_ahb.is_empty() {
                    "unknown".to_string()
                } else {
                    parent_ahb.to_string()
                },
                is_companion,
                field_type: "data".to_string(),
                format: None,
                enum_name: None,
                valid_codes: Vec::new(),
            }
        };

        output.insert(bo4e_name, field);
    }
}

/// Extract the target field name from a FieldMapping.
fn extract_target_name(fm: &FieldMapping) -> Option<String> {
    match fm {
        FieldMapping::Simple(s) => {
            if s.is_empty() {
                None
            } else {
                Some(s.clone())
            }
        }
        FieldMapping::Structured(s) => {
            if s.target.is_empty() {
                None
            } else {
                Some(s.target.clone())
            }
        }
        FieldMapping::Nested(_) => None,
    }
}

/// Parse a TOML field path into (segment_tag, composite_or_element, component).
///
/// Examples:
/// - "loc.c517.d3225" -> ("loc", "c517", Some("d3225"))
/// - "nad.d3035" -> ("nad", "d3035", None)
/// - "cav[Z91].c889.d7111" -> ("cav", "c889", Some("d7111"))
fn parse_toml_field_path(path: &str) -> Option<(String, String, Option<String>)> {
    let parts: Vec<&str> = path.split('.').collect();
    if parts.len() < 2 || parts.len() > 3 {
        return None;
    }

    // Strip qualifier from first part: "cav[Z91]" -> "cav"
    let seg_tag = parts[0].split('[').next().unwrap_or(parts[0]).to_string();

    let second = parts[1].to_string();

    let third = if parts.len() == 3 {
        Some(parts[2].to_string())
    } else {
        None
    };

    Some((seg_tag, second, third))
}

/// Find a schema element matching a TOML field path within a group's segments.
///
/// Handles:
/// - EDIFACT ID paths: `c517.d3225` (composite + component by ID)
/// - Numeric paths: `2.0` (element index + component sub_index)
/// - Simple element: `d3035` or `0` (element by ID or index)
fn find_schema_element(
    group_json: &Value,
    seg_tag: &str,
    composite_or_element: &str,
    component_id: Option<&str>,
) -> Option<Value> {
    let segments = group_json.get("segments")?.as_array()?;
    let seg_tag_upper = seg_tag.to_uppercase();

    // Find matching segment(s) by id
    let matching_segs: Vec<&Value> = segments
        .iter()
        .filter(|s| {
            s.get("id")
                .and_then(|v| v.as_str())
                .map(|id| id.to_uppercase() == seg_tag_upper)
                .unwrap_or(false)
        })
        .collect();

    if matching_segs.is_empty() {
        return None;
    }

    // Check if this is a numeric index path (e.g., "2.0" instead of "c517.d3225")
    let is_numeric = composite_or_element.chars().all(|c| c.is_ascii_digit());

    if is_numeric {
        return find_schema_element_by_index(&matching_segs, composite_or_element, component_id);
    }

    let normalized_id = normalize_edifact_id(composite_or_element);
    let is_composite = normalized_id.starts_with('C');

    for seg in &matching_segs {
        let elements = seg.get("elements")?.as_array()?;

        if is_composite {
            for elem in elements {
                let composite_id = elem.get("composite").and_then(|v| v.as_str()).unwrap_or("");
                if composite_id.to_uppercase() == normalized_id {
                    if let Some(comp_id) = component_id {
                        // Check if component is numeric sub_index
                        let comp_stripped = strip_ordinal_suffix(comp_id);
                        if comp_stripped.chars().all(|c| c.is_ascii_digit()) {
                            let sub_idx: usize = comp_stripped.parse().ok()?;
                            if let Some(components) =
                                elem.get("components").and_then(|v| v.as_array())
                            {
                                return components
                                    .iter()
                                    .find(|c| {
                                        c.get("sub_index").and_then(|v| v.as_u64())
                                            == Some(sub_idx as u64)
                                    })
                                    .cloned();
                            }
                        }
                        // Named component ID
                        let comp_normalized = normalize_edifact_id(comp_id);
                        let ordinal = extract_ordinal(comp_id);
                        if let Some(components) = elem.get("components").and_then(|v| v.as_array())
                        {
                            let mut count = 0usize;
                            for component in components {
                                let cid =
                                    component.get("id").and_then(|v| v.as_str()).unwrap_or("");
                                if cid.to_uppercase() == comp_normalized {
                                    if count == ordinal {
                                        return Some(component.clone());
                                    }
                                    count += 1;
                                }
                            }
                        }
                    } else {
                        return Some(elem.clone());
                    }
                }
            }
        } else {
            // Simple data element ID like "d3227" -> normalized "3227"
            let ordinal = extract_ordinal(composite_or_element);
            let mut count = 0usize;
            for elem in elements {
                if elem.get("composite").is_some() {
                    continue;
                }
                let eid = elem.get("id").and_then(|v| v.as_str()).unwrap_or("");
                if eid.to_uppercase() == normalized_id {
                    if count == ordinal {
                        return Some(elem.clone());
                    }
                    count += 1;
                }
            }
        }
    }

    None
}

/// Find a schema element by numeric index path (e.g., "2.0" = element at index 2, component at sub_index 0).
fn find_schema_element_by_index(
    matching_segs: &[&Value],
    element_index_str: &str,
    component_sub_index_str: Option<&str>,
) -> Option<Value> {
    let elem_idx: usize = element_index_str.parse().ok()?;

    for seg in matching_segs {
        let elements = match seg.get("elements").and_then(|v| v.as_array()) {
            Some(e) => e,
            None => continue,
        };

        let elem = match elements
            .iter()
            .find(|e| e.get("index").and_then(|v| v.as_u64()) == Some(elem_idx as u64))
        {
            Some(e) => e,
            None => continue,
        };

        if let Some(comp_str) = component_sub_index_str {
            let comp_stripped = strip_ordinal_suffix(comp_str);
            if let Ok(sub_idx) = comp_stripped.parse::<usize>() {
                if let Some(components) = elem.get("components").and_then(|v| v.as_array()) {
                    return components
                        .iter()
                        .find(|c| {
                            c.get("sub_index").and_then(|v| v.as_u64()) == Some(sub_idx as u64)
                        })
                        .cloned();
                }
            }
        }

        return Some(elem.clone());
    }

    None
}

/// Build a FieldRequirement from a resolved schema element.
fn build_field_requirement(
    schema_elem: &Value,
    bo4e_name: &str,
    is_companion: bool,
    parent_ahb: &str,
) -> FieldRequirement {
    let ahb_status = schema_elem
        .get("ahb_status")
        .and_then(|v| v.as_str())
        .unwrap_or(parent_ahb)
        .to_string();

    let field_type = schema_elem
        .get("type")
        .and_then(|v| v.as_str())
        .unwrap_or("data")
        .to_string();

    let format = schema_elem
        .get("format")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string());

    let enum_name = schema_elem
        .get("codes")
        .and_then(|v| v.as_array())
        .and_then(|codes| {
            codes
                .first()
                .and_then(|c| c.get("enum").and_then(|v| v.as_str()))
                .map(to_pascal_case)
        });

    let valid_codes = extract_valid_codes(schema_elem);

    FieldRequirement {
        bo4e_name: bo4e_name.to_string(),
        ahb_status,
        is_companion,
        field_type,
        format,
        enum_name,
        valid_codes,
    }
}

/// Extract valid code values from a schema element's "codes" array.
fn extract_valid_codes(element: &Value) -> Vec<CodeValue> {
    element
        .get("codes")
        .and_then(|v| v.as_array())
        .map(|codes| {
            codes
                .iter()
                .filter_map(|c| {
                    let code = c.get("value").and_then(|v| v.as_str())?.to_string();
                    let meaning = c
                        .get("name")
                        .and_then(|v| v.as_str())
                        .unwrap_or("")
                        .to_string();
                    Some(CodeValue { code, meaning })
                })
                .collect()
        })
        .unwrap_or_default()
}

/// Convert SCREAMING_SNAKE_CASE to PascalCase.
///
/// "HAUSHALTSKUNDE_ENWG" -> "HaushaltskundeEnwg"
fn to_pascal_case(s: &str) -> String {
    s.split('_')
        .map(|part| {
            let mut chars = part.chars();
            match chars.next() {
                Some(c) => {
                    let upper = c.to_uppercase().to_string();
                    let lower: String = chars.map(|c| c.to_ascii_lowercase()).collect();
                    format!("{upper}{lower}")
                }
                None => String::new(),
            }
        })
        .collect()
}

/// Normalize an EDIFACT ID path component to match schema IDs.
///
/// TOML paths use lowercase prefixed IDs: `c517`, `d3227`.
/// Schema JSON uses: `"C517"` for composites, `"3227"` for data elements (no D prefix).
fn normalize_edifact_id(id: &str) -> String {
    let stripped = strip_ordinal_suffix(id);
    let upper = stripped.to_uppercase();
    // Composite IDs start with C followed by digits
    if upper.starts_with('C') && upper.len() > 1 && upper[1..].chars().all(|c| c.is_ascii_digit()) {
        return upper;
    }
    // Data element IDs: strip 'D' prefix if present -> "D3227" -> "3227"
    if upper.starts_with('D') && upper.len() > 1 && upper[1..].chars().all(|c| c.is_ascii_digit()) {
        return upper[1..].to_string();
    }
    upper
}

/// Strip ordinal suffix from an EDIFACT ID: "c556_2" -> "c556", "d3036_2" -> "d3036".
fn strip_ordinal_suffix(id: &str) -> &str {
    if let Some(pos) = id.rfind('_') {
        let suffix = &id[pos + 1..];
        if !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit()) {
            return &id[..pos];
        }
    }
    id
}

/// Extract ordinal from a suffixed ID: "d3036_2" -> 1 (0-indexed), "d3036" -> 0.
fn extract_ordinal(id: &str) -> usize {
    if let Some(pos) = id.rfind('_') {
        let suffix = &id[pos + 1..];
        if let Ok(n) = suffix.parse::<usize>() {
            // Ordinal suffixes are 2-based (_2 = 2nd occurrence = index 1)
            return n.saturating_sub(1);
        }
    }
    0
}

/// Count the number of SG12 variants in the schema (for Geschaeftspartner array detection).
fn count_sg12_variants(schema: &Value) -> usize {
    let fields = match schema.get("fields").and_then(|v| v.as_object()) {
        Some(f) => f,
        None => return 0,
    };

    let mut count = 0;
    for (_key, group) in fields {
        if let Some(children) = group.get("children").and_then(|v| v.as_object()) {
            for child_key in children.keys() {
                if child_key.starts_with("sg12") {
                    count += 1;
                }
            }
        }
    }
    count
}

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

    #[test]
    fn test_to_pascal_case() {
        assert_eq!(to_pascal_case("HAUSHALTSKUNDE"), "Haushaltskunde");
        assert_eq!(to_pascal_case("HAUSHALTSKUNDE_ENWG"), "HaushaltskundeEnwg");
        assert_eq!(to_pascal_case("EMAIL"), "Email");
    }

    #[test]
    fn test_parse_toml_field_path() {
        let (tag, comp, elem) = parse_toml_field_path("loc.c517.d3225").unwrap();
        assert_eq!(tag, "loc");
        assert_eq!(comp, "c517");
        assert_eq!(elem.as_deref(), Some("d3225"));

        let (tag, elem, none) = parse_toml_field_path("nad.d3035").unwrap();
        assert_eq!(tag, "nad");
        assert_eq!(elem, "d3035");
        assert!(none.is_none());

        let (tag, comp, elem) = parse_toml_field_path("cav[Z91].c889.d7111").unwrap();
        assert_eq!(tag, "cav");
        assert_eq!(comp, "c889");
        assert_eq!(elem.as_deref(), Some("d7111"));
    }

    #[test]
    fn test_normalize_edifact_id() {
        assert_eq!(normalize_edifact_id("c517"), "C517");
        assert_eq!(normalize_edifact_id("d3225"), "3225");
        assert_eq!(normalize_edifact_id("d3036_2"), "3036");
        assert_eq!(normalize_edifact_id("c556_2"), "C556");
    }

    #[test]
    fn test_extract_ordinal() {
        assert_eq!(extract_ordinal("d3036"), 0);
        assert_eq!(extract_ordinal("d3036_2"), 1);
        assert_eq!(extract_ordinal("c556_3"), 2);
    }

    #[test]
    fn test_from_schema_and_definitions_pid_55001() {
        let schema_path = Path::new(concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/../mig-types/src/generated/fv2504/utilmd/pids/pid_55001_schema.json"
        ));
        if !schema_path.exists() {
            eprintln!("Schema file not found, skipping test");
            return;
        }

        let schema: Value =
            serde_json::from_str(&std::fs::read_to_string(schema_path).unwrap()).unwrap();

        let base = Path::new(concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/../../mappings/FV2504/UTILMD_Strom"
        ));
        let common_dir = base.join("common");
        let pid_dir = base.join("pid_55001");
        let message_dir = base.join("message");

        let defs = load_definitions_for_pid(&common_dir, &pid_dir, &message_dir, &schema).unwrap();
        assert!(!defs.is_empty(), "should have loaded definitions");

        let reqs = PidRequirements::from_schema_and_definitions(&schema, &defs);

        // Basic metadata
        assert_eq!(reqs.pid, "55001");
        assert_eq!(reqs.beschreibung, "Anmeldung verb. MaLo");
        assert!(!reqs.entities.is_empty(), "should have entities");

        // Prozessdaten entity with pruefidentifikator field
        let prozessdaten = reqs
            .entities
            .iter()
            .find(|e| e.entity == "Prozessdaten")
            .expect("Prozessdaten entity should exist");

        let pruefid = prozessdaten
            .fields
            .iter()
            .find(|f| f.bo4e_name == "pruefidentifikator")
            .expect("pruefidentifikator field should exist");
        assert_eq!(pruefid.ahb_status, "X");
        assert_eq!(pruefid.field_type, "code");
        assert!(
            pruefid.valid_codes.iter().any(|c| c.code == "55001"),
            "should have 55001 as valid code"
        );

        // Marktlokation entity should exist with marktlokationsId field
        let marktlokation = reqs
            .entities
            .iter()
            .find(|e| e.entity == "Marktlokation")
            .expect("Marktlokation entity should exist");
        assert!(
            marktlokation
                .fields
                .iter()
                .any(|f| f.bo4e_name == "marktlokationsId"),
            "Marktlokation should have marktlokationsId field"
        );

        // Zuordnung entity (SG10 CCI/CAV) should have code fields
        // haushaltskunde CCI.C240.7037 has 2 codes (HAUSHALTSKUNDE_ENWG, KEIN_HAUSHALTSKUNDE_ENWG)
        let zuordnung = reqs
            .entities
            .iter()
            .find(|e| e.entity == "Zuordnung")
            .expect("Zuordnung entity should exist");
        let code_fields: Vec<_> = zuordnung
            .fields
            .iter()
            .filter(|f| f.field_type == "code" && f.valid_codes.len() >= 2)
            .collect();
        assert!(
            !code_fields.is_empty(),
            "Zuordnung should have code fields with >= 2 valid codes, fields: {:?}",
            zuordnung
                .fields
                .iter()
                .map(|f| (&f.bo4e_name, &f.field_type, f.valid_codes.len()))
                .collect::<Vec<_>>()
        );

        // Geschaeftspartner should be detected as array (PID 55001 has SG12_Z04)
        let geschaeftspartner = reqs
            .entities
            .iter()
            .find(|e| e.entity == "Geschaeftspartner");
        // PID 55001 may have only 1 SG12 variant, so is_array depends on schema
        // At minimum, the entity should exist
        assert!(
            geschaeftspartner.is_some(),
            "Geschaeftspartner entity should exist"
        );
    }
}
