//! Android Resource Table (ARSC) parser.
//!
//! Implements parsing of `resources.arsc` files (binary format type 0x0002)
//! and produces the same `list_packages()` XML output as the Python `ARSC.list_packages()`.

use std::collections::BTreeMap;
use std::fmt::Write as FmtWrite;

use crate::error::AxmlError;
use crate::proto;
use crate::string_pool::{StringBlocks, StringPool};
use crate::typed_value;

// ─────────────────────────── Chunk type constants ────────────────────────────

pub const RES_TABLE_TYPE: u16 = 0x0002;
pub(crate) const RES_TABLE_PACKAGE_TYPE: u16 = 0x0200;
pub(crate) const RES_TABLE_TYPE_SPEC_TYPE: u16 = 0x0202;
pub(crate) const RES_TABLE_TYPE_TYPE: u16 = 0x0201;

const FLAG_COMPLEX: u16 = 0x0001;

// ─────────────────────────────── Public types ────────────────────────────────

/// Parsed ARSC (resources.arsc) file.
/// Mirrors the Python `ARSC` class: `proto` holds the prost-generated protobuf
/// message, `stringblocks` wraps the global string pool (proto + binary).
pub struct Arsc {
    /// Prost-generated protobuf representation; populated by `update_proto()`.
    pub(crate) proto: proto::Arsc,
    /// Global string pool wraps both the binary representation and the proto field.
    pub(crate) stringblocks: StringBlocks,
    pub(crate) packages: Vec<ArscPackage>,
    /// Stored ARSC total file size (bytes 4..8 of the global header).
    /// `pack()` writes this value verbatim.  `compute()` recalculates it.
    pub(crate) total_size: u32,
    /// Stored package count (bytes 8..12 of the global header).
    /// `pack()` writes this value verbatim.  `compute()` sets it to `packages.len()`.
    pub(crate) package_count: u32,
}

pub(crate) struct ArscPackage {
    /// Prost-generated protobuf representation for this package.
    pub(crate) proto: proto::AxmlResTablePackage,
    pub(crate) id: u32,
    pub(crate) name: String,
    /// Pool of type names ("anim", "layout", "string", …)
    pub(crate) type_strings: StringPool,
    /// Pool of resource key names (attribute names)
    pub(crate) key_strings: StringPool,
    pub(crate) chunks: Vec<ArscResChunk>,
    /// Stored package chunk size (bytes 4..8 of the package chunk header).
    /// `pack()` writes this value verbatim.  `compute()` recalculates it.
    pub(crate) chunk_size: u32,
    /// lastPublicType field from package header (preserved for round-trip)
    pub(crate) last_public_type: u32,
    /// lastPublicKey field from package header (preserved for round-trip)
    pub(crate) last_public_key: u32,
}

pub(crate) enum ArscResChunk {
    Spec(ArscTypeSpec),
    Type(ArscTypeType),
}

pub(crate) struct ArscTypeSpec {
    pub(crate) id: u8,
    pub(crate) res0: u8,
    pub(crate) res1: u16,
    pub(crate) entries: Vec<u32>,
}

pub(crate) struct ArscTypeType {
    /// 1-based type ID (used as index into type_strings: `id - 1`)
    pub(crate) id: u8,
    /// flags field from ResTable_type header (preserved verbatim)
    pub(crate) flags: u8,
    /// reserved field from ResTable_type header (preserved verbatim)
    pub(crate) reserved: u16,
    /// ISO 639-1 language code extracted from ResTable_config, empty string = default locale
    pub(crate) language: String,
    /// ISO 3166-1 alpha-2 region code extracted from ResTable_config, empty string = no region
    pub(crate) region: String,
    /// Raw ResTable_config bytes (preserved for exact round-trip; first 4 bytes = config size).
    pub(crate) config_raw: Vec<u8>,
    /// One entry per slot; `None` = absent (0xFFFFFFFF offset in the table)
    pub(crate) tables: Vec<Option<ArscEntry>>,
}

pub(crate) struct ArscEntry {
    /// Size of this entry (from ResTable_entry.size field)
    pub(crate) size: u16,
    /// Flags from the original entry (needed to preserve FLAG_COMPLEX and others)
    pub(crate) flags: u16,
    /// Index into the package's key_strings pool
    pub(crate) key_index: u32,
    /// For simple (non-complex) entries: the typed value
    pub(crate) value: Option<ArscValue>,
    /// Raw entry data (everything after the 8-byte header: size + flags + key_index)
    /// This preserves complex entries and other data we don't fully parse
    pub(crate) raw_tail: Vec<u8>,
}

/// Android `Res_value` the actual resource data cell.
pub(crate) struct ArscValue {
    pub(crate) size: u16,
    pub(crate) res0: u8,
    pub(crate) data_type: u8,
    pub(crate) data: u32,
}

// ─────────────────────────── Arsc impl ───────────────────────────────────────

impl Arsc {
    /// Parse a binary `resources.arsc` file.
    pub fn from_axml(data: &[u8]) -> Result<Self, AxmlError> {
        if data.len() < 12 {
            return Err(AxmlError::UnexpectedEof);
        }

        // Global ARSC header: type(2) + header_size(2) + total_size(4) + package_count(4)
        let header_size = u16::from_le_bytes([data[2], data[3]]) as usize;
        let total_size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
        let package_count = u32::from_le_bytes([data[8], data[9], data[10], data[11]]);

        let mut pos = header_size.max(12);

        // Global string pool
        let (string_pool, consumed) = StringPool::parse(&data[pos..])?;
        pos += consumed;
        let stringblocks =
            StringBlocks::from_pool_and_proto(string_pool, proto::StringBlocks::default());

        // Packages, SEC-01: cap allocation to prevent OOM from crafted package_count.
        // Each package header is at minimum 288 bytes; use that as a lower bound.
        let safe_pkg_cap = (package_count as usize).min(data.len() / 288).max(1);
        let mut packages = Vec::with_capacity(safe_pkg_cap);
        for _ in 0..package_count {
            if pos + 8 > data.len() {
                break;
            }
            let (pkg, pkg_consumed) = ArscPackage::parse(&data[pos..])?;
            pos += pkg_consumed;
            packages.push(pkg);
        }

        let mut arsc = Arsc {
            proto: proto::Arsc::default(),
            stringblocks,
            packages,
            total_size,
            package_count,
        };
        arsc.update_proto();
        Ok(arsc)
    }

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

    /// Serialize `self.proto` to native protobuf text format using prost-reflect.
    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 ARSC 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 ARSC 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 package `idx` to `path`.
    pub fn export_pkg_proto_file(
        &self,
        idx: usize,
        path: &str,
    ) -> Result<(), crate::error::AxmlError> {
        use prost::Message as _;
        let pkg = self.packages.get(idx).ok_or_else(|| {
            crate::error::AxmlError::SliceError(format!(
                "package index {} out of range (count: {})",
                idx,
                self.packages.len()
            ))
        })?;
        std::fs::write(path, pkg.proto.encode_to_vec())?;
        Ok(())
    }

    /// Replace package `idx` from a binary proto file written by [`export_pkg_proto_file`].
    pub fn import_pkg_proto_file(
        &mut self,
        idx: usize,
        path: &str,
    ) -> Result<(), crate::error::AxmlError> {
        use prost::Message as _;
        let data = std::fs::read(path)?;
        let pkg_proto = crate::proto::AxmlResTablePackage::decode(data.as_slice())?;
        let count = self.packages.len();
        let pkg = self.packages.get_mut(idx).ok_or_else(|| {
            crate::error::AxmlError::SliceError(format!(
                "package index {} out of range (count: {})",
                idx, count
            ))
        })?;
        *pkg = crate::proto_conv::proto_to_arsc_package(pkg_proto);
        self.update_proto();
        Ok(())
    }

    /// Produce an XML listing of resource entries grouped by locale.
    /// Each group is wrapped in `<resources lang="…">`.
    ///
    /// If `language_filter` is `Some`, only the matching locale section is emitted.
    /// Matching is case-insensitive ASCII. Use `"default"` to select entries with no
    /// locale qualifier, or a language tag such as `"en"` or `"fr-FR"`.
    pub fn list_packages(&self, language_filter: Option<&str>) -> String {
        // Group entry lines by locale tag into a BTreeMap for sorted output.
        let mut groups: BTreeMap<String, String> = BTreeMap::new();

        for package in &self.packages {
            for chunk in &package.chunks {
                let ArscResChunk::Type(typetype) = chunk else {
                    continue;
                };
                let tag = locale_tag(&typetype.language, &typetype.region);

                // Filter by locale if requested; ASCII locale tags → case-insensitive, zero-alloc.
                if let Some(f) = language_filter {
                    if !tag.eq_ignore_ascii_case(f) {
                        continue;
                    }
                }

                let type_name = package
                    .type_strings
                    .decode(typetype.id as u32 - 1)
                    .unwrap_or_default();

                let buf = groups.entry(tag).or_default();

                for (entry_idx, entry_opt) in typetype.tables.iter().enumerate() {
                    let Some(entry) = entry_opt else { continue };

                    let key_name = package
                        .key_strings
                        .decode(entry.key_index)
                        .unwrap_or_default();

                    let type_id = typetype.id as u32;
                    let id = 0x7F00_0000u32
                        | ((type_id & 0xFF) << 16)
                        | (type_id & 0xFF00)
                        | (entry_idx as u32);

                    let (data_str, data_size) = entry
                        .value
                        .as_ref()
                        .map(|v| {
                            let s = if v.data_type == typed_value::TYPE_STRING {
                                self.stringblocks.inner.decode(v.data).unwrap_or_default()
                            } else {
                                format!("{:#x}", v.data)
                            };
                            (s, v.size)
                        })
                        .unwrap_or_else(|| ("0x0".to_string(), 0));

                    // write! directly into the group buffer, no intermediate String allocation.
                    // SEC-03: escape all string values before embedding in XML attributes.
                    let _ = writeln!(
                        buf,
                        "  <public type=\"{}\" name=\"{}\" id=\"{:#010x}\" data=\"{}\" data_size={}/>",
                        xml_attr_escape(&type_name),
                        xml_attr_escape(&key_name),
                        id,
                        xml_attr_escape(&data_str),
                        data_size
                    );
                }
            }
        }

        // Wrap all locale groups in a single root so the output is valid XML.
        let mut out = String::from("<packages>\n");
        for (lang, entries) in &groups {
            let _ = write!(
                out,
                "<resources lang=\"{}\">\n{}</resources>\n",
                lang, entries
            );
        }
        out.push_str("</packages>");
        out
    }

    /// Force recalculation of string-pool chunk sizes on the next `pack()`.
    ///
    /// When `recursive` is `true` (the default), all string pools are marked
    /// dirty, the global pool plus every package's type and key pools.
    /// When `false`, only the global string pool is recomputed; package-level
    /// pools keep their current raw bytes.
    /// Recalculate all chunk-size header fields and store them in the struct.
    ///
    /// When `recursive` is `true` (the default), all string pools are marked
    /// dirty and all package `chunk_size` fields are updated.
    /// When `false`, only the global string pool and the top-level size fields
    /// are recomputed; package-level string pools keep their current raw bytes.
    ///
    /// After `compute()`, `pack()` produces a fully correct binary.
    pub fn compute(&mut self, recursive: bool) {
        // Always recalculate package chunk sizes from their actual content.
        // When recursive is false, only the top-level size fields are updated.
        // String pools that were mutated are already dirty (StringPool::add sets
        // dirty=true) and will be re-encoded on pack().
        if recursive {
            for pkg in &mut self.packages {
                pkg.chunk_size = pkg.actual_chunk_size();
            }
        } else {
            // Even in non-recursive mode, recalculate package sizes so
            // total_size is consistent with what pack() will actually write.
            for pkg in &mut self.packages {
                pkg.chunk_size = pkg.actual_chunk_size();
            }
        }
        let global_sp = self.stringblocks.inner.pack();
        let pkg_total: usize = self.packages.iter().map(|p| p.chunk_size as usize).sum();
        self.total_size = (12 + global_sp.len() + pkg_total) as u32;
        self.package_count = self.packages.len() as u32;
    }

    /// Returns the global string pool pack size (for debugging).
    pub fn global_stringpool_size(&self) -> usize {
        self.stringblocks.inner.pack().len()
    }

    /// Returns whether the global string pool is dirty.
    pub fn global_stringpool_is_dirty(&self) -> bool {
        self.stringblocks.inner.dirty
    }

    /// Returns the number of packages.
    pub fn package_count(&self) -> usize {
        self.packages.len()
    }

    /// Returns info about package string pools (for debugging).
    pub fn package_pool_info(&self, idx: usize) -> Option<(usize, bool, usize, bool)> {
        self.packages.get(idx).map(|pkg| {
            let type_sp_size = pkg.type_strings.pack().len();
            let type_sp_dirty = pkg.type_strings.dirty;
            let key_sp_size = pkg.key_strings.pack().len();
            let key_sp_dirty = pkg.key_strings.dirty;
            (type_sp_size, type_sp_dirty, key_sp_size, key_sp_dirty)
        })
    }

    /// Returns the count of chunks in a package (spec and type counts).
    pub fn package_chunks_have_raw(&self, pkg_idx: usize) -> Vec<bool> {
        self.packages
            .get(pkg_idx)
            .map(|pkg| pkg.chunks.iter().map(|_| false).collect())
            .unwrap_or_default()
    }

    /// Serialize this ARSC to binary `resources.arsc` format.
    ///
    /// This function auto-recalculates sizes when needed (when total_size is 0).
    /// Always produces a correct binary output.
    pub fn pack(&self) -> Vec<u8> {
        // Always compute sizes from actual content, no caching.
        let global_sp = self.stringblocks.inner.pack();
        let pkg_sizes: Vec<u32> = self
            .packages
            .iter()
            .map(|p| p.actual_chunk_size())
            .collect();
        let pkg_total: usize = pkg_sizes.iter().map(|&s| s as usize).sum();
        let total_size = (12 + global_sp.len() + pkg_total) as u32;

        let mut out = Vec::with_capacity(total_size as usize);
        out.extend_from_slice(&RES_TABLE_TYPE.to_le_bytes());
        out.extend_from_slice(&12u16.to_le_bytes());
        out.extend_from_slice(&total_size.to_le_bytes());
        out.extend_from_slice(&self.package_count.to_le_bytes());
        out.extend_from_slice(&global_sp);

        for (pkg, &size) in self.packages.iter().zip(pkg_sizes.iter()) {
            let pkg_data = pkg.pack_with_size(size);
            out.extend_from_slice(&pkg_data);
        }
        out
    }

    /// Add a new resource entry (type_name / name → file_path string value).
    ///
    /// Finds or creates the type in the first package's type string pool, adds
    /// the entry name to the key string pool, adds `file_path` to the global
    /// string pool, then appends a new TypeSpec+TypeType pair for the default
    /// locale.  Returns the new resource ID (0x7fTTEEEE).
    pub fn add_resource(&mut self, type_name: &str, name: &str, file_path: &str) -> u32 {
        let Some(pkg) = self.packages.first_mut() else {
            return 0;
        };

        // Ensure the type exists in the type string pool (0-based index).
        let type_idx_0 = match pkg.type_strings.find(type_name) {
            Some(i) => i,
            None => pkg.type_strings.add(type_name),
        };
        let type_id = (type_idx_0 + 1) as u8; // 1-based

        // Add the entry name to the key string pool.
        let key_idx = match pkg.key_strings.find(name) {
            Some(i) => i,
            None => pkg.key_strings.add(name),
        };

        // Add the file path to the global string pool.
        let value_idx = match self.stringblocks.inner.find(file_path) {
            Some(i) => i,
            None => self.stringblocks.inner.add(file_path),
        };

        // Count existing entries of this type across all existing TypeType chunks.
        let existing_count = pkg
            .chunks
            .iter()
            .filter(|c| matches!(c, ArscResChunk::Type(tt) if tt.id == type_id))
            .map(|c| {
                if let ArscResChunk::Type(tt) = c {
                    tt.tables.len()
                } else {
                    0
                }
            })
            .max()
            .unwrap_or(0);

        let entry_idx = existing_count;
        let entry_count = existing_count + 1;

        // Build or extend TypeSpec for this type.
        let spec_exists = pkg
            .chunks
            .iter()
            .any(|c| matches!(c, ArscResChunk::Spec(s) if s.id == type_id));
        if !spec_exists {
            pkg.chunks.push(ArscResChunk::Spec(ArscTypeSpec {
                id: type_id,
                res0: 0,
                res1: 0,
                entries: vec![0u32; entry_count],
            }));
        } else {
            for c in pkg.chunks.iter_mut() {
                if let ArscResChunk::Spec(s) = c {
                    if s.id == type_id {
                        s.entries.push(0u32);
                    }
                }
            }
        }

        // Build a new default-locale TypeType with all previous slots absent + new entry.
        let mut tables: Vec<Option<ArscEntry>> = (0..existing_count).map(|_| None).collect();
        let mut raw_tail = Vec::new();
        raw_tail.extend_from_slice(&8u16.to_le_bytes()); // Res_value.size
        raw_tail.push(0); // Res_value.res0
        raw_tail.push(crate::typed_value::TYPE_STRING); // Res_value.data_type
        raw_tail.extend_from_slice(&value_idx.to_le_bytes()); // Res_value.data
        tables.push(Some(ArscEntry {
            size: 16, // ResTable_entry header (8) + Res_value (8)
            flags: 0, // Simple entry, not complex
            key_index: key_idx,
            value: Some(ArscValue {
                size: 8,
                res0: 0,
                data_type: crate::typed_value::TYPE_STRING,
                data: value_idx,
            }),
            raw_tail,
        }));

        // Minimal 32-byte ResTable_config for default locale (all zeros = "any" config).
        let config_raw = {
            let mut cfg = vec![0u8; 32];
            cfg[0] = 32; // config_size
            cfg
        };

        pkg.chunks.push(ArscResChunk::Type(ArscTypeType {
            id: type_id,
            flags: 0,
            reserved: 0,
            language: String::new(),
            region: String::new(),
            config_raw,
            tables,
        }));

        let pkg_id = pkg.id;

        // Update stored chunk sizes after modification
        pkg.chunk_size = pkg.actual_chunk_size();

        // Update total_size to reflect the new content
        let global_sp_len = self.stringblocks.inner.pack().len();
        let pkg_total: usize = self.packages.iter().map(|p| p.chunk_size as usize).sum();
        self.total_size = (12 + global_sp_len + pkg_total) as u32;

        self.update_proto();

        // Build resource ID: 0xPPTTEEEE
        ((pkg_id & 0xFF) << 24) | ((type_id as u32 & 0xFF) << 16) | (entry_idx as u32 & 0xFFFF)
    }
} // impl Arsc

// ─────────────────────────── ArscPackage parsing ─────────────────────────────

impl ArscPackage {
    /// Return a reference to the cached prost-generated protobuf message for this package.
    #[allow(dead_code)]
    pub fn get_proto(&self) -> &proto::AxmlResTablePackage {
        &self.proto
    }

    /// Compute the actual (correct) package chunk size from current content.
    /// Used by `Arsc::compute()` to update `self.chunk_size`.
    pub(crate) fn actual_chunk_size(&self) -> u32 {
        const PKG_HEADER_SIZE: usize = 288;
        let type_sp_len = self.type_strings.pack().len();
        let key_sp_len = self.key_strings.pack().len();
        let mut chunks_len = 0usize;
        for chunk in &self.chunks {
            let mut tmp = Vec::new();
            match chunk {
                ArscResChunk::Spec(spec) => pack_type_spec(spec, &mut tmp),
                ArscResChunk::Type(tt) => pack_type_type(tt, &mut tmp),
            }
            chunks_len += tmp.len();
        }
        (PKG_HEADER_SIZE + type_sp_len + key_sp_len + chunks_len) as u32
    }

    /// Serialize this package to binary bytes.
    ///
    /// Bytes 4..8 of the chunk header (chunk size) come from `self.chunk_size`
    /// verbatim.  Call `Arsc::compute()` first to write the correct size, or
    /// set `self.chunk_size` manually for intentionally malformed output.
    #[expect(dead_code)]
    pub(crate) fn pack(&self) -> Vec<u8> {
        self.pack_with_size(self.chunk_size)
    }

    pub(crate) fn pack_with_size(&self, chunk_size: u32) -> Vec<u8> {
        const PKG_HEADER_SIZE: usize = 288;

        let type_sp = self.type_strings.pack();
        let key_sp = self.key_strings.pack();
        let type_strings_offset = PKG_HEADER_SIZE as u32;
        let key_strings_offset = type_strings_offset + type_sp.len() as u32;

        let mut chunks_data: Vec<u8> = Vec::new();
        for chunk in &self.chunks {
            match chunk {
                ArscResChunk::Spec(spec) => pack_type_spec(spec, &mut chunks_data),
                ArscResChunk::Type(tt) => pack_type_type(tt, &mut chunks_data),
            }
        }

        let body_len = PKG_HEADER_SIZE + type_sp.len() + key_sp.len() + chunks_data.len();
        let mut out = Vec::with_capacity(body_len);

        // 8-byte chunk header
        out.extend_from_slice(&RES_TABLE_PACKAGE_TYPE.to_le_bytes());
        out.extend_from_slice(&(PKG_HEADER_SIZE as u16).to_le_bytes());
        out.extend_from_slice(&chunk_size.to_le_bytes());

        // Package-specific fixed fields
        out.extend_from_slice(&self.id.to_le_bytes());

        // name: 256 bytes, null-padded UTF-16LE
        let mut name_utf16 = self
            .name
            .encode_utf16()
            .flat_map(|c| c.to_le_bytes())
            .collect::<Vec<_>>();
        name_utf16.resize(256, 0);
        out.extend_from_slice(&name_utf16);

        out.extend_from_slice(&type_strings_offset.to_le_bytes());
        out.extend_from_slice(&self.last_public_type.to_le_bytes());
        out.extend_from_slice(&key_strings_offset.to_le_bytes());
        out.extend_from_slice(&self.last_public_key.to_le_bytes());
        out.extend_from_slice(&0u32.to_le_bytes()); // typeIdOffset

        // Current size: 8 + 4 + 256 + 4 + 4 + 4 + 4 + 4 = 288 ✓
        debug_assert_eq!(out.len(), PKG_HEADER_SIZE);

        out.extend_from_slice(&type_sp);
        out.extend_from_slice(&key_sp);
        out.extend_from_slice(&chunks_data);

        out
    }

    fn parse(data: &[u8]) -> Result<(Self, usize), AxmlError> {
        if data.len() < 8 {
            return Err(AxmlError::UnexpectedEof);
        }

        // Chunk header
        let _chunk_type = u16::from_le_bytes([data[0], data[1]]);
        let _header_size = u16::from_le_bytes([data[2], data[3]]) as usize;
        let chunk_size_raw = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
        let pkg_size = chunk_size_raw as usize;

        if pkg_size < 288 || pkg_size > data.len() {
            return Err(AxmlError::SliceError(format!(
                "Package size {} out of range (data len {})",
                pkg_size,
                data.len()
            )));
        }

        let pkg = &data[..pkg_size];

        // Fixed header fields (all offsets from pkg start = offset 0)
        //   8: id
        //  12: name (256 bytes, UTF-16LE, null-padded)
        // 268: typeStrings offset
        // 272: lastPublicType
        // 276: keyStrings offset
        // 280: lastPublicKey
        let id = u32::from_le_bytes([pkg[8], pkg[9], pkg[10], pkg[11]]);
        let name = decode_utf16_null(&pkg[12..268]);

        let type_strings_off =
            u32::from_le_bytes([pkg[268], pkg[269], pkg[270], pkg[271]]) as usize;
        let last_public_type = u32::from_le_bytes([pkg[272], pkg[273], pkg[274], pkg[275]]);
        let key_strings_off = u32::from_le_bytes([pkg[276], pkg[277], pkg[278], pkg[279]]) as usize;
        let last_public_key = u32::from_le_bytes([pkg[280], pkg[281], pkg[282], pkg[283]]);

        // Parse type string pool
        let (type_strings, type_size) = parse_string_pool_at(pkg, type_strings_off)?;

        // Parse key string pool
        let (key_strings, key_size) = parse_string_pool_at(pkg, key_strings_off)?;

        // ResType chunks start after both string pools
        let chunks_start = (type_strings_off + type_size).max(key_strings_off + key_size);

        let chunks = parse_res_chunks(&pkg[chunks_start..]);

        Ok((
            ArscPackage {
                proto: proto::AxmlResTablePackage::default(),
                id,
                name,
                type_strings,
                key_strings,
                chunks,
                chunk_size: chunk_size_raw,
                last_public_type,
                last_public_key,
            },
            pkg_size,
        ))
    }
}

fn parse_string_pool_at(pkg: &[u8], offset: usize) -> Result<(StringPool, usize), AxmlError> {
    if offset == 0 || offset >= pkg.len() {
        return Ok((StringPool::default(), 0));
    }
    StringPool::parse(&pkg[offset..])
}

fn parse_res_chunks(data: &[u8]) -> Vec<ArscResChunk> {
    let mut chunks = Vec::new();
    let mut pos = 0;

    while pos + 8 <= data.len() {
        let chunk_type = u16::from_le_bytes([data[pos], data[pos + 1]]);
        let chunk_size =
            u32::from_le_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]])
                as usize;

        if chunk_size < 8 || pos + chunk_size > data.len() {
            break;
        }

        let body = &data[pos + 8..pos + chunk_size];

        match chunk_type {
            RES_TABLE_TYPE_SPEC_TYPE => {
                if let Ok(spec) = parse_type_spec(body) {
                    chunks.push(ArscResChunk::Spec(spec));
                }
            }
            RES_TABLE_TYPE_TYPE => {
                if let Ok(tt) = parse_type_type(body) {
                    chunks.push(ArscResChunk::Type(tt));
                }
            }
            _ => {}
        }

        pos += chunk_size;
    }

    chunks
}

// ─────────────────────────── Chunk body parsers ──────────────────────────────

fn parse_type_spec(body: &[u8]) -> Result<ArscTypeSpec, AxmlError> {
    if body.len() < 8 {
        return Err(AxmlError::UnexpectedEof);
    }
    let id = body[0];
    let res0 = body[1];
    let res1 = u16::from_le_bytes([body[2], body[3]]);
    let entry_count = u32::from_le_bytes([body[4], body[5], body[6], body[7]]) as usize;
    // Cap capacity: each entry is 4 bytes; header occupies the first 8 bytes.
    let safe_cap = entry_count.min((body.len().saturating_sub(8)) / 4);
    let mut entries = Vec::with_capacity(safe_cap);
    for i in 0..entry_count {
        let pos = 8 + i * 4;
        if pos + 4 > body.len() {
            break;
        }
        entries.push(u32::from_le_bytes([
            body[pos],
            body[pos + 1],
            body[pos + 2],
            body[pos + 3],
        ]));
    }
    Ok(ArscTypeSpec {
        id,
        res0,
        res1,
        entries,
    })
}

fn parse_type_type(body: &[u8]) -> Result<ArscTypeType, AxmlError> {
    // body: id(1) flags(1) reserved(2) entryCount(4) entryStart(4) config(variable) offsets entries
    if body.len() < 12 {
        return Err(AxmlError::UnexpectedEof);
    }
    let id = body[0];
    let flags = body[1];
    let reserved = u16::from_le_bytes([body[2], body[3]]);
    let entry_count = u32::from_le_bytes([body[4], body[5], body[6], body[7]]) as usize;

    // Config starts at body offset 12; first 4 bytes of config = config size
    if body.len() < 16 {
        return Err(AxmlError::UnexpectedEof);
    }
    let config_size = u32::from_le_bytes([body[12], body[13], body[14], body[15]]) as usize;
    if config_size < 16 || 12 + config_size > body.len() {
        return Err(AxmlError::SliceError(format!(
            "Invalid config size {}",
            config_size
        )));
    }

    // Entry offsets array immediately follows config
    let offsets_base = 12 + config_size;
    // SEC-02: use checked arithmetic to prevent integer overflow on 32-bit targets.
    let offsets_size = entry_count
        .checked_mul(4)
        .ok_or_else(|| AxmlError::SliceError(format!("entry_count overflow: {}", entry_count)))?;
    // Entry data immediately follows offset array
    let entries_base = offsets_base
        .checked_add(offsets_size)
        .ok_or_else(|| AxmlError::SliceError("entries_base overflow".to_string()))?;

    // SEC-01: cap allocation to prevent OOM from a malformed entry_count field.
    // Each entry is at least 8 bytes (size u16 + flags u16 + key u32).
    let safe_cap = entry_count.min(body.len() / 8);
    let mut tables = Vec::with_capacity(safe_cap);
    for i in 0..entry_count {
        let off_pos = offsets_base + i * 4;
        if off_pos + 4 > body.len() {
            tables.push(None);
            continue;
        }
        let entry_off = u32::from_le_bytes([
            body[off_pos],
            body[off_pos + 1],
            body[off_pos + 2],
            body[off_pos + 3],
        ]);

        if entry_off == 0xFFFF_FFFF {
            tables.push(None);
            continue;
        }

        let entry_pos = entries_base + entry_off as usize;
        if entry_pos + 8 > body.len() {
            tables.push(None);
            continue;
        }

        match parse_entry(&body[entry_pos..]) {
            Ok(e) => tables.push(Some(e)),
            Err(_) => tables.push(None),
        }
    }

    // Extract language and region from config if config is large enough
    let language = if config_size >= 12 && body.len() >= 24 {
        parse_locale_bytes(&body[20..22])
    } else {
        String::new()
    };
    let region = if config_size >= 12 && body.len() >= 26 {
        parse_locale_bytes(&body[22..24])
    } else {
        String::new()
    };

    let config_raw = body[12..12 + config_size].to_vec();
    Ok(ArscTypeType {
        id,
        flags,
        reserved,
        language,
        region,
        config_raw,
        tables,
    })
}

fn parse_entry(data: &[u8]) -> Result<ArscEntry, AxmlError> {
    if data.len() < 8 {
        return Err(AxmlError::UnexpectedEof);
    }
    let size = u16::from_le_bytes([data[0], data[1]]);
    let flags = u16::from_le_bytes([data[2], data[3]]);
    let key_index = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);

    if (flags & FLAG_COMPLEX) != 0 {
        // Complex entry: ResTable_map_entry header (8 bytes after the 8-byte base header)
        // followed by `count` ResTable_map values of 12 bytes each.
        // bytes 8..12 = parent resource id
        // bytes 12..16 = count of ResTable_map entries following
        let count = if data.len() >= 16 {
            u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize
        } else {
            0
        };
        // Each ResTable_map is 12 bytes: name(4) + Res_value(8)
        let total_size = 16 + count * 12;
        let end = total_size.min(data.len());
        let raw_tail = data[8..end].to_vec();
        return Ok(ArscEntry {
            size,
            flags,
            key_index,
            value: None,
            raw_tail,
        });
    }

    // Simple entry: a Res_value (8 bytes) always follows the 8-byte entry header.
    // The `size` field is the size of the ResTable_entry struct itself (always 8),
    // NOT the total bytes on disk.  Always capture 8 trailing bytes for the value.
    let value = if data.len() >= 16 {
        Some(ArscValue {
            size: u16::from_le_bytes([data[8], data[9]]),
            res0: data[10],
            data_type: data[11],
            data: u32::from_le_bytes([data[12], data[13], data[14], data[15]]),
        })
    } else {
        None
    };

    let end = 16usize.min(data.len());
    let raw_tail = data[8..end].to_vec();

    Ok(ArscEntry {
        size,
        flags,
        key_index,
        value,
        raw_tail,
    })
}

// ─────────────────────────────── Helpers ─────────────────────────────────────

// ─────────────────────────── Binary pack helpers ─────────────────────────────

pub(crate) fn pack_type_spec(spec: &ArscTypeSpec, out: &mut Vec<u8>) {
    let entry_count = spec.entries.len() as u32;
    let chunk_size = 8u32 + 8 + entry_count * 4;
    out.extend_from_slice(&RES_TABLE_TYPE_SPEC_TYPE.to_le_bytes());
    out.extend_from_slice(&16u16.to_le_bytes()); // headerSize = 16
    out.extend_from_slice(&chunk_size.to_le_bytes());
    out.push(spec.id);
    out.push(spec.res0);
    out.extend_from_slice(&spec.res1.to_le_bytes());
    out.extend_from_slice(&entry_count.to_le_bytes());
    for &e in &spec.entries {
        out.extend_from_slice(&e.to_le_bytes());
    }
}

pub(crate) fn pack_type_type(tt: &ArscTypeType, out: &mut Vec<u8>) {
    pack_type_type_from_parsed(tt, out);
}

fn pack_type_type_from_parsed(tt: &ArscTypeType, out: &mut Vec<u8>) {
    let entry_count = tt.tables.len() as u32;
    let config_size = tt.config_raw.len();
    // entries_start: offset from chunk start to first entry data
    // = chunk header (8) + body header (12) + config + entry offsets array
    let entries_start = 8u32 + 12 + config_size as u32 + entry_count * 4;
    // Collect present entries
    let mut entry_offsets = Vec::with_capacity(tt.tables.len());
    let mut entries_data: Vec<u8> = Vec::new();
    for entry_opt in &tt.tables {
        match entry_opt {
            None => entry_offsets.push(0xFFFF_FFFFu32),
            Some(entry) => {
                entry_offsets.push(entries_data.len() as u32);
                // ResTable_entry: size(2) flags(2) key(4)
                entries_data.extend_from_slice(&entry.size.to_le_bytes());
                entries_data.extend_from_slice(&entry.flags.to_le_bytes());
                entries_data.extend_from_slice(&entry.key_index.to_le_bytes());
                // Use raw_tail to preserve all data (including complex entries)
                entries_data.extend_from_slice(&entry.raw_tail);
            }
        }
    }

    // body = id(1)+flags(1)+reserved(2)+entryCount(4)+entriesStart(4)+config+offsets+entries
    let body_size = 12 + config_size + entry_count as usize * 4 + entries_data.len();
    let chunk_size = 8 + body_size;
    // headerSize = 8 (chunk header) + 12 (body header) + config_size
    let header_size = (8 + 12 + config_size) as u16;

    out.extend_from_slice(&RES_TABLE_TYPE_TYPE.to_le_bytes());
    out.extend_from_slice(&header_size.to_le_bytes());
    out.extend_from_slice(&(chunk_size as u32).to_le_bytes());
    // body header
    out.push(tt.id);
    out.push(tt.flags);
    out.extend_from_slice(&tt.reserved.to_le_bytes());
    out.extend_from_slice(&entry_count.to_le_bytes());
    out.extend_from_slice(&entries_start.to_le_bytes());
    // config
    out.extend_from_slice(&tt.config_raw);
    // offsets
    for &off in &entry_offsets {
        out.extend_from_slice(&off.to_le_bytes());
    }
    // entries
    out.extend_from_slice(&entries_data);
}

/// Decode a 2-byte ASCII locale field (language or region) from ResTable_config.
/// Returns an empty string if both bytes are zero (= not set / default locale).
///
/// Only ASCII alphabetic bytes are accepted; any other byte value (control
/// characters, XML-special characters such as `<`, `>`, `"`, `&`) is silently
/// discarded.  This prevents a crafted ARSC from injecting arbitrary content
/// into the XML output through locale fields.
fn parse_locale_bytes(bytes: &[u8]) -> String {
    if bytes.len() < 2 || (bytes[0] == 0 && bytes[1] == 0) {
        return String::new();
    }
    let mut s = String::with_capacity(2);
    for &b in bytes {
        if b.is_ascii_alphabetic() {
            s.push(b as char);
        }
    }
    s
}

/// Build a locale tag string from language and region components.
/// - ("", "") → "default"
/// - ("en", "") → "en"
/// - ("en", "US") → "en-US"
pub fn locale_tag(language: &str, region: &str) -> String {
    match (language.is_empty(), region.is_empty()) {
        (true, _) => "default".to_string(),
        (false, true) => language.to_string(),
        (false, false) => format!("{}-{}", language, region),
    }
}

/// Escape special XML characters so a string is safe to embed in an attribute value.
fn xml_attr_escape(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '&' => out.push_str("&amp;"),
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            '"' => out.push_str("&quot;"),
            _ => out.push(c),
        }
    }
    out
}

/// Decode a null-terminated UTF-16LE byte slice to a Rust String.
fn decode_utf16_null(bytes: &[u8]) -> String {
    let words: Vec<u16> = bytes
        .chunks_exact(2)
        .map(|b| u16::from_le_bytes([b[0], b[1]]))
        .collect();
    let s = String::from_utf16_lossy(&words);
    // Trim at first null
    s.split('\0').next().unwrap_or("").to_string()
}
