use std::collections::HashMap;

use quick_xml::events::{BytesStart, Event};
use quick_xml::{Decoder, Reader};

use crate::error::AxmlError;
use crate::proto;
use crate::public;
use crate::resource_map::ResourceMap;
use crate::string_pool::{StringBlocks, StringPool};
use crate::typed_value::{self, TYPE_STRING};
use crate::xml_element::{Attribute, XmlElement};

pub(crate) const RES_XML_TYPE: u16 = 0x0003;
// RES_TABLE_TYPE is defined in arsc : use crate::arsc::RES_TABLE_TYPE where needed.

const ANDROID_NS: &str = "http://schemas.android.com/apk/res/android";

/// The main AXML object representing a binary Android XML file.
///
/// # Dual-state synchronization
///
/// `Axml` carries two representations of the same data that must be kept in sync:
///
/// 1. **Decoded state**: `stringblocks`, `resource_map`, `elements`. These fields
///    are the ground truth during parsing and editing.
/// 2. **Proto state**: `proto` is a `prost`-generated message used for protobuf
///    serialization. It is populated lazily by `update_proto()`.
///
/// **Rule**: after any mutation to the decoded state (e.g. modifying `elements` or
/// calling `import_stringblocks_json`), callers **must** invoke `update_proto()`
/// before calling `get_proto()` or any proto-based serialization path.
/// Failing to do so will return a stale proto that does not reflect the current state.
pub struct Axml {
    /// Prost-generated protobuf representation; populated by `update_proto()`.
    pub(crate) proto: proto::Axml,
    /// String pool, wraps both the binary representation and the proto field.
    pub(crate) stringblocks: StringBlocks,
    pub(crate) resource_map: Option<ResourceMap>,
    pub(crate) elements: Vec<XmlElement>,
    pub(crate) file_type: u16,
    pub(crate) file_header_size: u16,
    /// Stored total file size (bytes 4..8 of the AXML chunk header).
    /// `pack()` writes this value directly, set it to produce intentionally
    /// malformed files.  `compute()` recalculates and stores the correct value.
    pub(crate) total_size: u32,
}

impl Axml {
    /// Create a new empty AXML document.
    pub fn new() -> Self {
        Axml {
            proto: proto::Axml::default(),
            stringblocks: StringBlocks::new(),
            resource_map: None,
            elements: Vec::new(),
            file_type: RES_XML_TYPE,
            file_header_size: 8,
            total_size: 0,
        }
    }

    /// Return a reference to the cached prost-generated protobuf message.
    /// The proto is populated during `from_axml()` / `from_xml()`.
    /// Call `update_proto()` first if the internal state has been modified.
    pub fn get_proto(&self) -> &proto::Axml {
        &self.proto
    }

    /// Number of strings in the string pool.
    pub fn string_count(&self) -> usize {
        self.stringblocks.inner.strings.len()
    }

    /// Serialize the string pool as a JSON object for use with `--stringblocks-file`.
    pub fn export_stringblocks_json(&self) -> String {
        crate::json_serde::export_stringblocks(&self.stringblocks.inner)
    }

    /// Pre-populate the string pool from a JSON object previously produced by
    /// `export_stringblocks_json`.
    pub fn import_stringblocks_json(&mut self, json: &str) {
        crate::json_serde::import_stringblocks(json, &mut self.stringblocks.inner);
    }

    // ─────────────────────────────── Parsing ────────────────────────────────

    /// Parse binary AXML data into an `Axml` object.
    pub fn from_axml(data: &[u8]) -> Result<Self, AxmlError> {
        if data.len() < 8 {
            return Err(AxmlError::UnexpectedEof);
        }

        let file_type = u16::from_le_bytes([data[0], data[1]]);
        let file_header_size = u16::from_le_bytes([data[2], data[3]]);

        // Accept non-standard type values (some real-world AXML files have wrong magic).
        // Only reject clearly non-XML types.
        if file_type == crate::arsc::RES_TABLE_TYPE {
            return Err(AxmlError::InvalidMagic(file_type));
        }

        let file_size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
        if file_size > data.len() {
            return Err(AxmlError::UnexpectedEof);
        }

        let mut pos = file_header_size as usize;
        if pos > data.len() {
            return Err(AxmlError::UnexpectedEof);
        }

        let (string_pool, consumed) = StringPool::parse(&data[pos..])?;
        pos += consumed;

        let (resource_map, rmap_consumed) = ResourceMap::parse(&data[pos..])?;
        pos += rmap_consumed;

        let elements = XmlElement::parse_all(&data[pos..])?;

        let total_size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);

        let mut axml = Axml {
            proto: proto::Axml::default(),
            stringblocks: StringBlocks::from_pool_and_proto(
                string_pool,
                proto::StringBlocks::default(),
            ),
            resource_map,
            elements,
            file_type,
            file_header_size,
            total_size,
        };
        axml.update_proto();
        Ok(axml)
    }

    // ─────────────────────────────── Packing ────────────────────────────────

    /// Serialize the AXML object back to binary format.
    ///
    /// Bytes 4..8 of the output (the chunk total-size field) come from
    /// `self.total_size` verbatim.  Set that field to an arbitrary value
    /// before calling `pack()` to produce intentionally malformed output.
    /// Call `compute()` first to write the correct size.
    pub fn pack(&self) -> Vec<u8> {
        let sp = self.stringblocks.inner.pack();
        let rm = self
            .resource_map
            .as_ref()
            .map(|r| r.pack())
            .unwrap_or_default();
        let mut xml = Vec::new();
        for e in &self.elements {
            xml.extend_from_slice(&e.pack());
        }

        let body_len = sp.len() + rm.len() + xml.len();
        let mut out = Vec::with_capacity(8 + body_len);

        out.extend_from_slice(&self.file_type.to_le_bytes());
        out.extend_from_slice(&self.file_header_size.to_le_bytes());
        out.extend_from_slice(&self.total_size.to_le_bytes());
        out.extend_from_slice(&sp);
        out.extend_from_slice(&rm);
        out.extend_from_slice(&xml);
        out
    }

    // ─────────────────────────────── to_xml ─────────────────────────────────

    /// Decode the AXML to a human-readable XML string.
    pub fn to_xml(&self) -> Result<String, AxmlError> {
        // Collect namespace declarations in declaration order (preserving order for stable output).
        // Use Vec to maintain insertion order; HashMap for O(1) lookup.
        let mut ns_order: Vec<(String, String)> = Vec::new(); // (uri, prefix) in order
        let mut ns_map: HashMap<String, String> = HashMap::new(); // uri → prefix
        for elt in &self.elements {
            if let XmlElement::StartNamespace { prefix, uri, .. } = elt {
                let p = self.decode_safe(*prefix);
                let u = self.decode_safe(*uri);
                if !p.is_empty() && !u.is_empty() && !ns_map.contains_key(&u) {
                    ns_map.insert(u.clone(), p.clone());
                    ns_order.push((u, p));
                }
            }
        }

        let mut output = String::from("<?xml version='1.0' encoding='utf-8'?>\n");
        let mut depth = 0usize;
        let mut first_element = true;
        let mut tag_stack: Vec<String> = Vec::new();

        for elt in &self.elements {
            match elt {
                XmlElement::StartElement {
                    namespace_uri,
                    name,
                    at_size: _,
                    attributes,
                    ..
                } => {
                    let ns = self.decode_safe(*namespace_uri);
                    let tag = self.resolve_attr_name(*name)?;
                    let qualified = qualify_name(&ns, &tag, &ns_map);

                    indent(&mut output, depth);
                    output.push('<');
                    output.push_str(&qualified);

                    if first_element {
                        for (uri, prefix) in &ns_order {
                            output.push_str(" xmlns:");
                            output.push_str(prefix);
                            output.push_str("=\"");
                            xml_escape_into(&mut output, uri);
                            output.push('"');
                        }
                        first_element = false;
                    }

                    for attr in attributes {
                        let attr_ns = self.decode_safe(attr.namespace_uri);
                        let attr_name = self.resolve_attr_name(attr.name)?;
                        let qname = qualify_name(&attr_ns, &attr_name, &ns_map);

                        let type_byte = typed_value::get_type(attr.type_);
                        let value_str = if type_byte == TYPE_STRING {
                            self.decode_safe(attr.value)
                        } else if let Some(s) = typed_value::coerce_to_string(type_byte, attr.data)
                        {
                            s
                        } else {
                            attr.data.to_string()
                        };

                        output.push(' ');
                        output.push_str(&qname);
                        output.push_str("=\"");
                        xml_escape_into(&mut output, &value_str);
                        output.push('"');
                    }

                    output.push('>');
                    output.push('\n');
                    depth += 1;
                    tag_stack.push(qualified);
                }

                XmlElement::EndElement { .. } => {
                    depth = depth.saturating_sub(1);
                    let tag = tag_stack.pop().unwrap_or_default();
                    indent(&mut output, depth);
                    output.push_str("</");
                    output.push_str(&tag);
                    output.push_str(">\n");
                }

                XmlElement::CData { name, .. } => {
                    if *name != 0xffff_ffff {
                        let text = self.decode_safe(*name);
                        if !text.is_empty() {
                            indent(&mut output, depth);
                            xml_escape_into(&mut output, &text);
                            output.push('\n');
                        }
                    }
                }

                XmlElement::StartNamespace { .. } | XmlElement::EndNamespace { .. } => {}
            }
        }

        Ok(output)
    }

    fn decode_safe(&self, idx: u32) -> String {
        if idx == 0xffff_ffff {
            return String::new();
        }
        self.stringblocks.inner.decode(idx).unwrap_or_default()
    }

    /// Resolve attribute name using resource map → system resources if possible.
    fn resolve_attr_name(&self, name_idx: u32) -> Result<String, AxmlError> {
        if name_idx == 0xffff_ffff {
            return Ok(String::new());
        }
        if let Some(ref rmap) = self.resource_map {
            if let Some(res_id) = rmap.get_id(name_idx) {
                if let Some(name) = public::attr_inverse(res_id) {
                    return Ok(name.to_string());
                }
            }
        }
        self.stringblocks.inner.decode(name_idx)
    }

    // ─────────────────────────────── from_xml ────────────────────────────────

    /// Build binary AXML from a plain XML string.
    ///
    /// Two-pass implementation: the first pass collects all xmlns: declarations so
    /// that StartNamespace elements can be emitted before any StartElement.  The
    /// second pass builds the binary elements.  Android attribute → resource-ID
    /// mappings are collected inline and the resource map is assembled after the loop.
    pub fn from_xml(&mut self, xml: &str) -> Result<(), AxmlError> {
        self.stringblocks.inner = StringPool::new(false);
        self.resource_map = None;
        self.elements = Vec::new();

        // Pre-scan: collect every xmlns: declaration in document order.
        // The android namespace is always guaranteed to be first.
        let mut all_xmlns: Vec<(String, String)> = Vec::new(); // (prefix, uri)
        all_xmlns.push(("android".to_string(), ANDROID_NS.to_string()));
        {
            let mut pre_reader = Reader::from_str(xml);
            pre_reader.config_mut().trim_text(true);
            let mut pre_buf = Vec::new();
            loop {
                match pre_reader.read_event_into(&mut pre_buf) {
                    Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
                        for attr in e.attributes().flatten() {
                            let k = String::from_utf8_lossy(attr.key.as_ref());
                            if let Some(prefix) = k.strip_prefix("xmlns:") {
                                if prefix != "android"
                                    && !all_xmlns.iter().any(|(p, _)| p == prefix)
                                {
                                    let uri = String::from_utf8_lossy(&attr.value).into_owned();
                                    all_xmlns.push((prefix.to_string(), uri));
                                }
                            }
                        }
                    }
                    Ok(Event::Eof) | Err(_) => break,
                    _ => {}
                }
                pre_buf.clear();
            }
        }

        // Emit one StartNamespace for every declared namespace, in document order.
        // Track (prefix_idx, uri_idx) for matching EndNamespace at the end.
        let mut ns_indices: Vec<(u32, u32)> = Vec::with_capacity(all_xmlns.len());
        for (prefix, uri) in &all_xmlns {
            let p_idx = self.stringblocks.inner.add(prefix.as_str());
            let u_idx = self.stringblocks.inner.add(uri.as_str());
            self.elements.push(XmlElement::StartNamespace {
                header_size: 16,
                chunk_size: 0,
                line_number: 0,
                comment: 0xffff_ffff,
                prefix: p_idx,
                uri: u_idx,
            });
            ns_indices.push((p_idx, u_idx));
        }

        // Collect (pool_index, resource_id) pairs inline as android attrs are seen.
        let mut res_map_entries: Vec<(usize, u32)> = Vec::new();

        // Namespace prefix → URI map built from the pre-scan.
        let mut xmlns_map: HashMap<String, String> = all_xmlns.iter().cloned().collect();

        let mut reader = Reader::from_str(xml);
        reader.config_mut().trim_text(true);
        let decoder = reader.decoder();

        // Stack of (namespace_uri_idx, name_idx) for matching end elements.
        let mut open_stack: Vec<(u32, u32)> = Vec::new();
        let mut buf = Vec::new();
        // MED-4: reject pathologically deep XML to prevent unbounded stack growth.
        const MAX_DEPTH: usize = 512;
        const MAX_ELEMENTS: usize = 100_000;
        let mut element_count: usize = 0;

        loop {
            match reader.read_event_into(&mut buf) {
                Ok(Event::Start(ref e)) => {
                    if open_stack.len() >= MAX_DEPTH {
                        return Err(AxmlError::XmlError(format!(
                            "XML nesting depth exceeds limit ({})",
                            MAX_DEPTH
                        )));
                    }
                    element_count += 1;
                    if element_count > MAX_ELEMENTS {
                        return Err(AxmlError::XmlError(format!(
                            "XML element count exceeds limit ({})",
                            MAX_ELEMENTS
                        )));
                    }
                    collect_xmlns(e, &mut xmlns_map);
                    let (ns_idx, name_idx) = self.parse_element_name(e, &xmlns_map)?;
                    open_stack.push((ns_idx, name_idx));
                    self.push_start_element(
                        e,
                        ns_idx,
                        name_idx,
                        &xmlns_map,
                        decoder,
                        &mut res_map_entries,
                    )?;
                }
                Ok(Event::Empty(ref e)) => {
                    collect_xmlns(e, &mut xmlns_map);
                    let (ns_idx, name_idx) = self.parse_element_name(e, &xmlns_map)?;
                    self.push_start_element(
                        e,
                        ns_idx,
                        name_idx,
                        &xmlns_map,
                        decoder,
                        &mut res_map_entries,
                    )?;
                    self.elements.push(XmlElement::EndElement {
                        header_size: 16,
                        chunk_size: 0,
                        line_number: 0,
                        comment: 0xffff_ffff,
                        namespace_uri: 0xffff_ffff,
                        name: name_idx,
                    });
                }
                Ok(Event::End(_)) => {
                    let (_, name_idx) = open_stack.pop().unwrap_or((0xffff_ffff, 0xffff_ffff));
                    self.elements.push(XmlElement::EndElement {
                        header_size: 16,
                        chunk_size: 0,
                        line_number: 0,
                        comment: 0xffff_ffff,
                        namespace_uri: 0xffff_ffff,
                        name: name_idx,
                    });
                }
                Ok(Event::Text(ref e)) => {
                    let text = e
                        .decode()
                        .map_err(|_err| AxmlError::XmlError("Failed to decode text".to_string()))?;
                    let trimmed = text.trim();
                    if !trimmed.is_empty() {
                        let idx = self.stringblocks.inner.add(trimmed);
                        self.elements.push(XmlElement::CData {
                            header_size: 16,
                            chunk_size: 0,
                            line_number: 0,
                            comment: 0xffff_ffff,
                            name: idx,
                            res_size: 8,
                            res_res0: 0,
                            res_data_type: 0x03,
                            res_data: 0,
                        });
                    }
                }
                Ok(Event::Eof) => break,
                Err(e) => return Err(AxmlError::XmlError(e.to_string())),
                _ => {}
            }
            buf.clear();
        }

        // EndNamespace in reverse declaration order.
        for (p_idx, u_idx) in ns_indices.into_iter().rev() {
            self.elements.push(XmlElement::EndNamespace {
                header_size: 16,
                chunk_size: 0,
                line_number: 0,
                comment: 0xffff_ffff,
                prefix: p_idx,
                uri: u_idx,
            });
        }

        // Build resource map from the inline-collected entries.
        if let Some(&(max_idx, _)) = res_map_entries.iter().max_by_key(|&&(i, _)| i) {
            let mut ids = vec![0u32; max_idx + 1];
            for (idx, id) in res_map_entries {
                ids[idx] = id;
            }
            while ids.last() == Some(&0) {
                ids.pop();
            }
            if !ids.is_empty() {
                self.resource_map = Some(ResourceMap::new_with_ids(ids));
            }
        }

        self.compute();
        self.update_proto();
        Ok(())
    }

    /// Extract namespace URI index and local name index from a start element.
    fn parse_element_name(
        &mut self,
        e: &BytesStart,
        xmlns_map: &HashMap<String, String>,
    ) -> Result<(u32, u32), AxmlError> {
        let full = String::from_utf8_lossy(e.name().as_ref()).to_string();
        let (ns, local) = split_clark_name(&full);
        let resolved_ns = if ns.is_empty() {
            resolve_prefix_ns(&full, xmlns_map)
        } else {
            ns.to_string()
        };
        let ns_idx = if resolved_ns.is_empty() {
            0xffff_ffff
        } else {
            self.stringblocks.inner.add(&resolved_ns)
        };
        let name_idx = self.stringblocks.inner.add(local);
        Ok((ns_idx, name_idx))
    }

    /// Build and push a `StartElement`, given pre-computed ns/name indices.
    ///
    /// `res_map_entries` is populated with `(pool_idx, resource_id)` pairs for
    /// any android-namespace attribute that has a known system resource ID.
    fn push_start_element(
        &mut self,
        e: &BytesStart,
        ns_uri_idx: u32,
        name_idx: u32,
        xmlns_map: &HashMap<String, String>,
        decoder: Decoder,
        res_map_entries: &mut Vec<(usize, u32)>,
    ) -> Result<(), AxmlError> {
        let mut attributes = Vec::new();

        for attr in e.attributes().flatten() {
            let key = String::from_utf8_lossy(attr.key.as_ref()).to_string();

            // Skip xmlns declarations (already collected by collect_xmlns).
            if key.starts_with("xmlns") {
                continue;
            }

            // Decode and unescape XML entities so that `&gt;` → `>`, `&amp;` → `&`,
            // etc. are stored as decoded characters in the binary string pool.
            let val = attr
                .decode_and_unescape_value(decoder)
                .map(|v| v.into_owned())
                .unwrap_or_else(|_| String::from_utf8_lossy(&attr.value).into_owned());
            let (attr_ns, attr_local) = split_clark_name(&key);

            // Resolve prefix:local notation via the xmlns map when Clark notation
            // is not available (i.e., when the input was generated by to_xml()).
            let resolved_ns = if attr_ns.is_empty() {
                resolve_prefix_ns(&key, xmlns_map)
            } else {
                attr_ns.to_string()
            };

            let attr_ns_idx = if resolved_ns.is_empty() {
                0xffff_ffff
            } else {
                self.stringblocks.inner.add(&resolved_ns)
            };

            // Add attribute name to pool (find() is now O(1) via the HashMap index).
            let attr_name_idx = self.stringblocks.inner.add(attr_local);

            // If this is an android-namespace attr with a known resource ID, record it.
            if !resolved_ns.is_empty() || key.starts_with("android:") {
                if let Some(id) = public::attr_forward(attr_local) {
                    res_map_entries.push((attr_name_idx as usize, id));
                }
            }

            let (type_, value_idx, data) =
                typed_value::encode_attribute_value(&val, attr_local, &mut self.stringblocks.inner);

            attributes.push(Attribute {
                namespace_uri: attr_ns_idx,
                name: attr_name_idx,
                value: value_idx,
                type_,
                data,
                padding: Vec::new(),
            });
        }

        self.elements.push(XmlElement::StartElement {
            header_size: 16,
            chunk_size: 0,
            line_number: 0,
            comment: 0xffff_ffff,
            namespace_uri: ns_uri_idx,
            name: name_idx,
            at_start: 0x14,
            at_size: 0x14,
            style_attribute: 0,
            class_attribute: 0,
            attributes,
        });

        Ok(())
    }

    // ─────────────────────────── compute / no-compute ───────────────────────

    /// Recalculate all chunk-size header fields and store them in the struct.
    ///
    /// Updates `self.total_size` (AXML outer header) and marks the string pool
    /// dirty so `pack()` produces a fully correct binary.
    ///
    /// Without calling `compute()`, `pack()` uses whatever sizes were last stored
    /// either the original parsed values, values set manually, or a stale size
    /// left by `apply_stringblocks_no_compute()`.
    pub fn compute(&mut self) {
        self.stringblocks.inner.mark_dirty();
        let sp = self.stringblocks.inner.pack();

        if let Some(ref mut rm) = self.resource_map {
            let chunk_size = (8 + rm.ids.len() * 4) as u32;
            rm.set_chunk_size(chunk_size);
        }
        let rm = self
            .resource_map
            .as_ref()
            .map(|r| r.pack())
            .unwrap_or_default();

        for elem in &mut self.elements {
            let body = elem.pack_body();
            let chunk_size = (8 + body.len()) as u32;
            elem.set_chunk_size(chunk_size);
        }

        let xml_len: usize = self.elements.iter().map(|e| e.pack().len()).sum();
        self.total_size = (8 + sp.len() + rm.len() + xml_len) as u32;
    }

    /// Apply new string pool content without updating the pool's chunk-size header.
    ///
    /// `new_strings` must be the `data` bytes from each `StringBlock` proto entry
    /// (raw UTF-8 or UTF-16-LE bytes, no length prefix, no null terminator).
    ///
    /// After this call `pack()` returns a binary where string content reflects
    /// the new values but the chunk total-size field is stale, an intentionally
    /// corrupted file useful for security testing.  Call `compute()` before
    /// `pack()` to produce a correctly-sized output instead.
    pub fn apply_stringblocks_no_compute(&mut self, new_strings: Vec<Vec<u8>>) {
        self.stringblocks.inner.apply_no_compute(new_strings);
    }

    // ─────────────────────────── to_proto_text ───────────────────────────────

    /// Serialize to proto text format using the native prost-reflect formatter.
    /// Requires `update_proto()` to have been called (automatically done by
    /// `from_axml()` and `from_xml()`).
    pub fn to_proto_text(&self) -> String {
        use prost_reflect::ReflectMessage;
        self.proto.transcode_to_dynamic().to_text_format()
    }

    /// Serialize to indented proto text format (one field per line).
    pub fn to_proto_text_pretty(&self) -> String {
        use prost_reflect::{text_format::FormatOptions, ReflectMessage};
        self.proto
            .transcode_to_dynamic()
            .to_text_format_with_options(&FormatOptions::default().pretty(true))
    }

    /// Write the binary proto representation of this AXML file to `path`.
    pub fn export_proto_file(&self, path: &str) -> Result<(), crate::error::AxmlError> {
        std::fs::write(path, self.to_proto_bytes())?;
        Ok(())
    }

    /// Load an AXML from a binary proto file written by [`export_proto_file`].
    pub fn from_proto_file(path: &str) -> Result<Self, crate::error::AxmlError> {
        let data = std::fs::read(path)?;
        Self::from_proto_bytes(&data)
    }

    /// Write the binary proto representation of this file's string pool to `path`.
    pub fn export_stringblocks_proto_file(
        &self,
        path: &str,
    ) -> Result<(), crate::error::AxmlError> {
        use prost::Message as _;
        let bytes = self.stringblocks.proto.encode_to_vec();
        std::fs::write(path, bytes)?;
        Ok(())
    }

    /// Replace the string pool from a binary proto file written by
    /// [`export_stringblocks_proto_file`].  Marks the pool dirty so that the
    /// next `pack()` / `compute()` recalculates chunk sizes.
    pub fn import_stringblocks_proto_file(
        &mut self,
        path: &str,
    ) -> Result<(), crate::error::AxmlError> {
        use prost::Message as _;
        let data = std::fs::read(path)?;
        let sb_proto = crate::proto::StringBlocks::decode(data.as_slice())?;
        let pool = crate::proto_conv::proto_to_string_pool(sb_proto.clone());
        self.stringblocks = crate::string_pool::StringBlocks::from_pool_and_proto(pool, sb_proto);
        self.update_proto();
        Ok(())
    }
}

impl Default for Axml {
    fn default() -> Self {
        Self::new()
    }
}

// ─────────────────────────── Helper functions ────────────────────────────────

fn indent(out: &mut String, depth: usize) {
    for _ in 0..depth {
        out.push_str("  ");
    }
}

fn qualify_name(ns_uri: &str, local: &str, ns_map: &HashMap<String, String>) -> String {
    if ns_uri.is_empty() {
        return local.to_string();
    }
    match ns_map.get(ns_uri) {
        Some(prefix) => format!("{}:{}", prefix, local),
        None => local.to_string(),
    }
}

fn xml_escape_into(out: &mut String, s: &str) {
    for c in s.chars() {
        match c {
            '&' => out.push_str("&amp;"),
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            '"' => out.push_str("&quot;"),
            c => out.push(c),
        }
    }
}

/// Scan element attributes for `xmlns:prefix="uri"` declarations and add them
/// to `xmlns_map`.  Called before processing any other attribute on the element.
fn collect_xmlns(e: &BytesStart, xmlns_map: &mut HashMap<String, String>) {
    for attr in e.attributes().flatten() {
        let key = String::from_utf8_lossy(attr.key.as_ref());
        if let Some(prefix) = key.strip_prefix("xmlns:") {
            let uri = String::from_utf8_lossy(&attr.value).into_owned();
            xmlns_map.insert(prefix.to_string(), uri);
        } else if key == "xmlns" {
            let uri = String::from_utf8_lossy(&attr.value).into_owned();
            xmlns_map.insert(String::new(), uri);
        }
    }
}

/// Given an attribute or element key in `prefix:local` notation, return the
/// namespace URI by looking up `prefix` in `xmlns_map`.  Returns an empty
/// string when no mapping is found or the key has no prefix.
fn resolve_prefix_ns(key: &str, xmlns_map: &HashMap<String, String>) -> String {
    if let Some(colon) = key.find(':') {
        let prefix = &key[..colon];
        if prefix != "xmlns" {
            if let Some(uri) = xmlns_map.get(prefix) {
                return uri.clone();
            }
        }
    }
    String::new()
}

/// Split `{ns}local` or `prefix:local` into `(ns_or_empty, local)`.
fn split_clark_name(name: &str) -> (&str, &str) {
    if name.starts_with('{') {
        if let Some(end) = name.find('}') {
            return (&name[1..end], &name[end + 1..]);
        }
    }
    // Prefix notation like "android:name" → treat prefix as nothing (android NS already stored)
    if let Some(colon) = name.find(':') {
        let prefix = &name[..colon];
        if prefix != "xmlns" {
            return ("", &name[colon + 1..]);
        }
    }
    ("", name)
}
