//! Maps EDIFACT validation paths to BO4E field paths.
//!
//! When validation runs on EDIFACT produced by reverse-mapping BO4E JSON,
//! the resulting `ValidationIssue`s contain EDIFACT segment paths like
//! `SG4/SG5/LOC/C517/3225`. This module resolves those back to BO4E paths
//! like `stammdaten.Marktlokation.marktlokationsId` so users can find
//! the source of the problem in their BO4E input.

use mig_types::schema::mig::{MigSchema, MigSegment, MigSegmentGroup};

use crate::definition::{FieldMapping, MappingDefinition};
use crate::path_resolver::ReversePathResolver;

/// Maps EDIFACT segment paths from validation errors to BO4E field paths.
pub struct Bo4eFieldIndex {
    entries: Vec<IndexEntry>,
}

struct IndexEntry {
    /// EDIFACT group+segment prefix: "SG4/SG5/LOC", "SG2/NAD", "SG4/IDE", etc.
    edifact_prefix: String,
    /// BO4E entity name from TOML meta: "Marktlokation", "Prozessdaten"
    entity: String,
    /// Whether this entity is in stammdaten or transaktionsdaten.
    location: FieldLocation,
    /// Optional companion type (for companion_fields entries).
    companion_type: Option<String>,
    /// Individual field mappings within this segment.
    fields: Vec<FieldEntry>,
}

#[derive(Clone, Copy)]
enum FieldLocation {
    Stammdaten,
}

struct FieldEntry {
    /// Full EDIFACT field_path this matches (e.g., "SG4/SG5/LOC/C517/3225").
    edifact_path: String,
    /// BO4E target field name (e.g., "marktlokationsId").
    bo4e_field: String,
    /// Whether this is a companion field.
    is_companion: bool,
    /// Optional qualifier from the TOML path (e.g., "93" from "dtm[93].0.1").
    /// Used to disambiguate fields that share the same EDIFACT path.
    qualifier: Option<String>,
}

impl Bo4eFieldIndex {
    /// Build the index from TOML mapping definitions and a MIG schema.
    ///
    /// For each field in each definition, resolves the TOML numeric path
    /// (e.g., `loc.1.0`) to an AHB-style EDIFACT path (e.g., `SG4/SG5/LOC/C517/3225`)
    /// using the MIG schema for element ID lookup.
    pub fn build(definitions: &[MappingDefinition], mig: &MigSchema) -> Self {
        Self::build_inner(definitions, mig, None)
    }

    /// Build the index using a `ReversePathResolver` for element ID lookup.
    ///
    /// This is more accurate than `build` because the resolver is built from
    /// unmerged PID schema JSONs, avoiding data loss from MIG group merging
    /// (e.g., CCI composites lost when merging SG10 variants).
    pub fn build_with_resolver(
        definitions: &[MappingDefinition],
        mig: &MigSchema,
        resolver: &ReversePathResolver,
    ) -> Self {
        Self::build_inner(definitions, mig, Some(resolver))
    }

    fn build_inner(
        definitions: &[MappingDefinition],
        mig: &MigSchema,
        resolver: Option<&ReversePathResolver>,
    ) -> Self {
        let mut entries = Vec::new();

        for def in definitions {
            let group_path = source_group_to_slash(&def.meta.source_group);
            let location = classify_entity(&def.meta.entity);
            let companion_type = def.meta.companion_type.clone();

            let mut fields = Vec::new();

            // Process [fields]
            Self::collect_fields_inner(
                &def.fields,
                &group_path,
                mig,
                resolver,
                false,
                &mut fields,
            );

            // Process [companion_fields]
            if let Some(ref companion) = def.companion_fields {
                Self::collect_fields_inner(
                    companion,
                    &group_path,
                    mig,
                    resolver,
                    true,
                    &mut fields,
                );
            }

            if !fields.is_empty() {
                entries.push(IndexEntry {
                    edifact_prefix: group_path.clone(),
                    entity: def.meta.entity.clone(),
                    location,
                    companion_type,
                    fields,
                });
            }
        }

        Self { entries }
    }

    /// Given an EDIFACT field_path from a ValidationIssue, return the BO4E path.
    ///
    /// `hint` is an optional disambiguation string (e.g., expected value or AHB rule)
    /// that helps pick the right entry when multiple TOML definitions map to the same
    /// EDIFACT path (e.g., DTM+92 and DTM+93 both map to SG4/DTM/C507/2005).
    pub fn resolve(&self, edifact_field_path: &str, hint: Option<&str>) -> Option<String> {
        // Exact match on field entries — prefer qualifier-matching entries when hint is available
        let mut exact_matches: Vec<(&IndexEntry, &FieldEntry)> = Vec::new();
        for entry in &self.entries {
            for field in &entry.fields {
                if field.edifact_path == edifact_field_path {
                    exact_matches.push((entry, field));
                }
            }
        }

        if !exact_matches.is_empty() {
            // If we have a hint, try to match it against field qualifiers
            if let Some(hint) = hint {
                if let Some((entry, field)) = exact_matches
                    .iter()
                    .find(|(_, f)| f.qualifier.as_deref() == Some(hint))
                {
                    return Some(self.build_bo4e_path(entry, field));
                }
                // Also try matching hint as a substring of qualifier or vice versa
                if let Some((entry, field)) = exact_matches
                    .iter()
                    .find(|(_, f)| {
                        f.qualifier
                            .as_deref()
                            .is_some_and(|q| hint.contains(q) || q.contains(hint))
                    })
                {
                    return Some(self.build_bo4e_path(entry, field));
                }
            }
            // No hint match — return first exact match
            let (entry, field) = exact_matches[0];
            return Some(self.build_bo4e_path(entry, field));
        }

        // Sibling match: for qualifier fields like SG4/DTM/C507/2005, find entries
        // for the same segment composite (SG4/DTM/C507/*) that match the hint qualifier.
        // This handles cases where the qualifier element (2005) doesn't have its own
        // index entry but its sibling data field (2380) does.
        if let Some(hint) = hint {
            let composite_prefix = edifact_field_path.rsplit_once('/').map(|(p, _)| p);
            if let Some(prefix) = composite_prefix {
                for entry in &self.entries {
                    for field in &entry.fields {
                        if field.edifact_path.starts_with(prefix)
                            && field.qualifier.as_deref() == Some(hint)
                        {
                            return Some(self.build_bo4e_path(entry, field));
                        }
                    }
                }
            }
        }

        // Prefix match for code/qualifier paths — longest prefix wins
        let mut best: Option<&IndexEntry> = None;
        for entry in &self.entries {
            if !entry.edifact_prefix.is_empty()
                && edifact_field_path.starts_with(&entry.edifact_prefix)
                && best
                    .map(|b| entry.edifact_prefix.len() > b.edifact_prefix.len())
                    .unwrap_or(true)
            {
                best = Some(entry);
            }
        }
        best.map(|entry| self.build_entity_path(entry))
    }

    /// Debug: return all entries as (edifact_path, entity, bo4e_field) tuples.
    pub fn debug_entries(&self) -> Vec<(String, String, String)> {
        let mut out = Vec::new();
        for entry in &self.entries {
            for field in &entry.fields {
                out.push((
                    field.edifact_path.clone(),
                    entry.entity.clone(),
                    field.bo4e_field.clone(),
                ));
            }
        }
        out
    }

    fn collect_fields_inner(
        field_map: &indexmap::IndexMap<String, FieldMapping>,
        group_path: &str,
        mig: &MigSchema,
        resolver: Option<&ReversePathResolver>,
        is_companion: bool,
        out: &mut Vec<FieldEntry>,
    ) {
        // First pass: collect qualifier paths (empty target) and data field paths.
        // Qualifier paths like dtm[92].c507.d2005 have empty target but carry a
        // default value (the qualifier code). We'll create entries for them in
        // a second pass, pointing to their sibling data field.
        struct QualifierPath {
            parsed: ParsedTomlPath,
        }
        let mut qualifier_paths: Vec<QualifierPath> = Vec::new();
        // Map from (tag, qualifier) → first data field BO4E name for sibling lookup
        let mut tag_qualifier_to_field: std::collections::HashMap<(String, String), String> =
            std::collections::HashMap::new();

        for (toml_path, mapping) in field_map {
            let target = match mapping {
                FieldMapping::Simple(s) => s.as_str(),
                FieldMapping::Structured(s) => s.target.as_str(),
                FieldMapping::Nested(_) => continue,
            };

            let parsed = match parse_toml_path(toml_path) {
                Some(p) => p,
                None => continue,
            };

            if target.is_empty() {
                // Qualifier/default field — collect for second pass
                qualifier_paths.push(QualifierPath { parsed });
                continue;
            }

            // Track first data field per (tag, qualifier) for sibling lookup
            if let Some(ref q) = parsed.qualifier {
                tag_qualifier_to_field
                    .entry((parsed.segment_tag.clone(), q.clone()))
                    .or_insert_with(|| target.to_string());
            }

            // Resolve and add the data field entry
            let edifact_path = resolver
                .and_then(|r| resolve_edifact_path_via_resolver(group_path, &parsed, r))
                .or_else(|| resolve_edifact_path(group_path, &parsed, mig));

            if let Some(edifact_path) = edifact_path {
                out.push(FieldEntry {
                    edifact_path,
                    bo4e_field: target.to_string(),
                    is_companion,
                    qualifier: parsed.qualifier.clone(),
                });
            }
        }

        // Second pass: create entries for qualifier paths that reference their
        // sibling data field. E.g., dtm[93].c507.d2005 (qualifier for gueltigBis)
        // gets an entry pointing to "gueltigBis" so the missing-qualifier error
        // resolves to stammdaten.Prozessdaten.gueltigBis instead of just Prozessdaten.
        for qp in &qualifier_paths {
            if let Some(ref q) = qp.parsed.qualifier {
                let key = (qp.parsed.segment_tag.clone(), q.clone());
                if let Some(sibling_field) = tag_qualifier_to_field.get(&key)
                {
                    let edifact_path = resolver
                        .and_then(|r| {
                            resolve_edifact_path_via_resolver(group_path, &qp.parsed, r)
                        })
                        .or_else(|| resolve_edifact_path(group_path, &qp.parsed, mig));

                    if let Some(edifact_path) = edifact_path {
                        out.push(FieldEntry {
                            edifact_path,
                            bo4e_field: sibling_field.clone(),
                            is_companion,
                            qualifier: qp.parsed.qualifier.clone(),
                        });
                    }
                }
            }
        }
    }

    fn build_bo4e_path(&self, entry: &IndexEntry, field: &FieldEntry) -> String {
        let location = match entry.location {
            FieldLocation::Stammdaten => "stammdaten",
        };
        if field.is_companion {
            if let Some(ref ct) = entry.companion_type {
                format!(
                    "{}.{}.{}.{}",
                    location,
                    entry.entity,
                    to_camel_first_lower(ct),
                    field.bo4e_field
                )
            } else {
                format!("{}.{}.{}", location, entry.entity, field.bo4e_field)
            }
        } else {
            format!("{}.{}.{}", location, entry.entity, field.bo4e_field)
        }
    }

    fn build_entity_path(&self, entry: &IndexEntry) -> String {
        let location = match entry.location {
            FieldLocation::Stammdaten => "stammdaten",
        };
        format!("{}.{}", location, entry.entity)
    }
}

/// Parsed TOML field path components.
struct ParsedTomlPath {
    /// Segment tag in uppercase (e.g., "LOC", "DTM").
    segment_tag: String,
    /// Element index (e.g., 1 in "loc.1.0").
    element_idx: usize,
    /// Optional component sub-index (e.g., 0 in "loc.1.0").
    component_idx: Option<usize>,
    /// Optional qualifier from the tag (e.g., "93" from "dtm[93]").
    qualifier: Option<String>,
}

/// Parse a TOML field path like "loc.1.0" or "dtm[92].0.1".
fn parse_toml_path(path: &str) -> Option<ParsedTomlPath> {
    let parts: Vec<&str> = path.split('.').collect();
    if parts.len() < 2 {
        return None;
    }

    // Extract qualifier from tag: "dtm[92]" → tag="DTM", qualifier=Some("92")
    let raw_tag = parts[0];
    let (tag, qualifier) = if let Some(bracket) = raw_tag.find('[') {
        let end = raw_tag.find(']').unwrap_or(raw_tag.len());
        let qual = &raw_tag[bracket + 1..end];
        // Strip occurrence suffix: "Z34,1" → "Z34"
        let qual = qual.split(',').next().unwrap_or(qual);
        (&raw_tag[..bracket], Some(qual.to_string()))
    } else {
        (raw_tag, None)
    };

    let element_idx: usize = parts[1].parse().ok()?;
    let component_idx = if parts.len() > 2 {
        Some(parts[2].parse::<usize>().ok()?)
    } else {
        None
    };

    Some(ParsedTomlPath {
        segment_tag: tag.to_uppercase(),
        element_idx,
        component_idx,
        qualifier,
    })
}

/// Convert source_group dot notation to slash notation, stripping `:N` suffixes.
/// "SG4.SG5" → "SG4/SG5", "SG8:1.SG10" → "SG8/SG10"
fn source_group_to_slash(source_group: &str) -> String {
    source_group
        .split('.')
        .map(|part| {
            if let Some(colon) = part.find(':') {
                &part[..colon]
            } else {
                part
            }
        })
        .collect::<Vec<_>>()
        .join("/")
}

/// Classify entity location. All entities are now in stammdaten
/// (the transaktionsdaten split has been removed).
fn classify_entity(_entity: &str) -> FieldLocation {
    FieldLocation::Stammdaten
}

/// Resolve a parsed TOML path to an AHB-style EDIFACT path using the ReversePathResolver.
///
/// This is more accurate than MIG-based resolution because the resolver is built from
/// unmerged PID schema JSONs, preserving composites that get lost during MIG merging.
fn resolve_edifact_path_via_resolver(
    group_path: &str,
    parsed: &ParsedTomlPath,
    resolver: &ReversePathResolver,
) -> Option<String> {
    // Build the numeric path: "cci.2.0" from tag=CCI, elem=2, comp=0
    let numeric_path = if let Some(ci) = parsed.component_idx {
        format!(
            "{}.{}.{}",
            parsed.segment_tag.to_lowercase(),
            parsed.element_idx,
            ci
        )
    } else {
        format!(
            "{}.{}",
            parsed.segment_tag.to_lowercase(),
            parsed.element_idx
        )
    };

    // Use ReversePathResolver to get named path: "cci.c240.d7037"
    let named = resolver.reverse_path(&numeric_path);
    if named == numeric_path {
        // Resolver couldn't resolve — not in schema
        return None;
    }

    // Convert named path to AHB-style: "cci.c240.d7037" → "CCI/C240/7037"
    let parts: Vec<&str> = named.split('.').collect();
    let ahb_parts: Vec<String> = parts
        .iter()
        .map(|p| {
            // Strip qualifier suffix: "dtm[92]" → "DTM"
            let clean = if let Some(bracket) = p.find('[') {
                &p[..bracket]
            } else {
                p
            };
            // Strip edifact id prefix (c/d/s): "c240" → "C240", "d7037" → "7037"
            if clean.len() > 1
                && (clean.starts_with('c') || clean.starts_with('C'))
                && clean[1..].chars().next().is_some_and(|c| c.is_ascii_digit())
            {
                clean.to_uppercase()
            } else if clean.len() > 1
                && (clean.starts_with('d') || clean.starts_with('D'))
                && clean[1..].chars().next().is_some_and(|c| c.is_ascii_digit())
            {
                // Data element: strip 'd' prefix → "7037"
                clean[1..].to_string()
            } else {
                clean.to_uppercase()
            }
        })
        .collect();

    let edifact_suffix = ahb_parts.join("/");

    if group_path.is_empty() {
        Some(edifact_suffix)
    } else {
        Some(format!("{}/{}", group_path, edifact_suffix))
    }
}

/// Resolve a parsed TOML path to an AHB-style EDIFACT path using the MIG.
fn resolve_edifact_path(
    group_path: &str,
    parsed: &ParsedTomlPath,
    mig: &MigSchema,
) -> Option<String> {
    // Find the segment in the MIG
    let segment = find_segment_in_mig(mig, group_path, &parsed.segment_tag)?;

    // Build a unified list of (position, element_kind) sorted by position
    let resolved = resolve_element_at_position(segment, parsed.element_idx, parsed.component_idx)?;

    let prefix = if group_path.is_empty() {
        parsed.segment_tag.clone()
    } else {
        format!("{}/{}", group_path, parsed.segment_tag)
    };

    match resolved {
        ResolvedElement::DataElement(id) => Some(format!("{}/{}", prefix, id)),
        ResolvedElement::CompositeElement(composite_id, element_id) => {
            Some(format!("{}/{}/{}", prefix, composite_id, element_id))
        }
    }
}

enum ResolvedElement {
    /// A standalone data element: just the element ID.
    DataElement(String),
    /// A component within a composite: (composite_id, data_element_id).
    CompositeElement(String, String),
}

/// Find a segment by tag within a group path in the MIG.
fn find_segment_in_mig<'a>(
    mig: &'a MigSchema,
    group_path: &str,
    segment_tag: &str,
) -> Option<&'a MigSegment> {
    if group_path.is_empty() {
        // Root-level segment
        return mig
            .segments
            .iter()
            .find(|s| s.id.eq_ignore_ascii_case(segment_tag));
    }

    let parts: Vec<&str> = group_path.split('/').collect();

    // Find the first group
    let mut current_group = mig
        .segment_groups
        .iter()
        .find(|g| g.id.eq_ignore_ascii_case(parts[0]))?;

    // Navigate nested groups
    for &part in &parts[1..] {
        current_group = current_group
            .nested_groups
            .iter()
            .find(|g| g.id.eq_ignore_ascii_case(part))?;
    }

    find_segment_in_group(current_group, segment_tag)
}

/// Find a segment by tag within a group (checking the group and its nested groups).
fn find_segment_in_group<'a>(
    group: &'a MigSegmentGroup,
    segment_tag: &str,
) -> Option<&'a MigSegment> {
    group
        .segments
        .iter()
        .find(|s| s.id.eq_ignore_ascii_case(segment_tag))
}

/// Resolve an element at a given position within a MIG segment.
///
/// Builds a unified position list from data_elements and composites,
/// then finds what's at element_idx. If it's a composite and component_idx
/// is provided, returns the sub-element.
fn resolve_element_at_position(
    segment: &MigSegment,
    element_idx: usize,
    component_idx: Option<usize>,
) -> Option<ResolvedElement> {
    // Check composites first — they have a position field
    if let Some(composite) = segment
        .composites
        .iter()
        .find(|c| c.position == element_idx)
    {
        let comp_idx = component_idx.unwrap_or(0);
        // Find the data element at the component sub-index by sorting by position
        let mut sub_elements: Vec<_> = composite.data_elements.iter().collect();
        sub_elements.sort_by_key(|de| de.position);
        let de = sub_elements.get(comp_idx)?;
        return Some(ResolvedElement::CompositeElement(
            composite.id.clone(),
            de.id.clone(),
        ));
    }

    // Check standalone data elements
    if let Some(de) = segment
        .data_elements
        .iter()
        .find(|d| d.position == element_idx)
    {
        return Some(ResolvedElement::DataElement(de.id.clone()));
    }

    None
}

/// Convert PascalCase to camelCase (first char lowercase).
fn to_camel_first_lower(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(c) => c.to_lowercase().to_string() + chars.as_str(),
    }
}

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

    #[test]
    fn test_source_group_to_slash() {
        assert_eq!(source_group_to_slash("SG4.SG5"), "SG4/SG5");
        assert_eq!(source_group_to_slash("SG4"), "SG4");
        assert_eq!(source_group_to_slash("SG8:1.SG10"), "SG8/SG10");
        assert_eq!(source_group_to_slash(""), "");
    }

    #[test]
    fn test_parse_toml_path() {
        let p = parse_toml_path("loc.1.0").unwrap();
        assert_eq!(p.segment_tag, "LOC");
        assert_eq!(p.element_idx, 1);
        assert_eq!(p.component_idx, Some(0));

        let p = parse_toml_path("ide.1").unwrap();
        assert_eq!(p.segment_tag, "IDE");
        assert_eq!(p.element_idx, 1);
        assert_eq!(p.component_idx, None);

        let p = parse_toml_path("dtm[92].0.1").unwrap();
        assert_eq!(p.segment_tag, "DTM");
        assert_eq!(p.element_idx, 0);
        assert_eq!(p.component_idx, Some(1));

        assert!(parse_toml_path("loc").is_none());
    }

    #[test]
    fn test_classify_entity() {
        // All entities are now classified as Stammdaten (no more transaktionsdaten split)
        assert!(matches!(
            classify_entity("Prozessdaten"),
            FieldLocation::Stammdaten
        ));
        assert!(matches!(
            classify_entity("Nachricht"),
            FieldLocation::Stammdaten
        ));
        assert!(matches!(
            classify_entity("Marktlokation"),
            FieldLocation::Stammdaten
        ));
        assert!(matches!(
            classify_entity("Marktteilnehmer"),
            FieldLocation::Stammdaten
        ));
    }

    #[test]
    fn test_to_camel_first_lower() {
        assert_eq!(
            to_camel_first_lower("MarktlokationEdifact"),
            "marktlokationEdifact"
        );
        assert_eq!(to_camel_first_lower("Foo"), "foo");
        assert_eq!(to_camel_first_lower(""), "");
    }

    #[test]
    fn test_resolve_returns_none_for_unknown_path() {
        let index = Bo4eFieldIndex { entries: vec![] };
        assert!(index.resolve("SG99/UNKNOWN/9999", None).is_none());
    }
}
