use std::{collections::HashSet, sync::LazyLock};

/// .dlcpack container magic header (4 bytes) used to identify encrypted pack containers.
pub const DLC_PACK_MAGIC: &[u8; 4] = b"BDLP";

/// Current supported .dlcpack format version. This is stored in the container header and used to determine how to parse the contents.
pub const DLC_PACK_VERSION_LATEST: u8 = 4;

/// Default block size for v4 hybrid format (10 MB). Blocks are individually encrypted tar.gz chunks.
pub const DEFAULT_BLOCK_SIZE: usize = 10 * 1024 * 1024;

/// List of file extensions that are never allowed to be packed.  These are
/// taken from the same array that used to live in [lib.rs]; we keep the
/// array for deterministic build and the hash set for fast membership tests.
pub const FORBIDDEN_EXTENSIONS: &[&str] = &[
    "7z", "accda", "accdb", "accde", "accdr", "ace", "ade", "adp", "app", "appinstaller", "application", "appref", "appx", "appxbundle", "arj", "asax", "asd", "ashx", "asp", "aspx", "b64", "bas", "bat", "bgi", "bin", "btm", "bz", "bz2", "bzip", "bzip2", "cab", "cer", "cfg", "chi", "chm", "cla", "class", "cmd", "com", "cpi", "cpio", "cpl", "crt", "crx", "csh", "der", "desktopthemefile", "diagcab", "diagcfg", "diagpkg", "dll", "dmg", "doc", "docm", "docx", "dotm", "drv", "eml", "exe", "fon", "fxp", "gadget", "grp", "gz", "gzip", "hlp", "hta", "htc", "htm", "html", "htt", "ics", "img", "ini", "ins", "inx", "iqy", "iso", "isp", "isu", "jar", "jnlp", "job", "js", "jse", "ksh", "lha", "lnk", "local", "lz", "lzh", "lzma", "mad", "maf", "mag", "mam", "manifest", "maq", "mar", "mas", "mat", "mav", "maw", "mda", "mdb", "mde", "mdt", "mdw", "mdz", "mht", "mhtml", "mmc", "msc", "msg", "msh", "msh1", "msh1xml", "msh2", "msh2xml", "mshxml", "msi", "msix", "msixbundle", "msm", "msp", "mst", "msu", "ocx", "odt", "one", "onepkg", "onetoc", "onetoc2", "ops", "oxps", "oxt", "paf", "partial", "pcd", "pdf", "pif", "pl", "plg", "pol", "potm", "ppam", "ppkg", "ppsm", "ppt", "pptm", "pptx", "prf", "prg", "ps1", "ps1xml", "ps2", "ps2xml", "psc1", "psc2", "psm1", "pst", "r00", "r01", "r02", "r03", "rar", "reg", "rels", "rev", "rgs", "rpm", "rtf", "scf", "scr", "sct", "search", "settingcontent", "settingscontent", "sh", "shb", "sldm", "slk", "svg", "swf", "sys", "tar", "tbz", "tbz2", "tgz", "tlb", "url", "uue", "vb", "vbe", "vbs", "vbscript", "vdx", "vhd", "vhdx", "vsmacros", "vss", "vssm", "vssx", "vst", "vstm", "vstx", "vsw", "vsx", "vtx", "wbk", "webarchive", "website", "wml", "ws", "wsc", "wsf", "wsh", "xar", "xbap", "xdp", "xlam", "xll", "xlm", "xls", "xlsb", "xlsm", "xlsx", "xltm", "xlw", "xml", "xnk", "xps", "xrm", "xsd", "xsl", "xxe", "xz", "z", "zip",
];

/// Lazy hash set used by [`is_forbidden_extension`].
static FORBIDDEN_SET: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
    FORBIDDEN_EXTENSIONS.iter().copied().collect()
});

/// Return true if the extension (case‑insensitive) is in the forbidden list.
pub fn is_forbidden_extension(ext: &str) -> bool {
    let lowercase = ext.trim_start_matches('.').to_ascii_lowercase();
    FORBIDDEN_SET.contains(lowercase.as_str())
}

// `is_malicious_file` used to live here but its functionality is now
// subsumed by `is_data_executable` which is called during packing.  Keeping
// the helper around only bloated the public API and required test plumbing
// that we no longer need, so the function has been removed.

// NOTE: if additional heuristics are required in the future they can be
// reintroduced or re‑exported from the CLI/generation layer rather than the
// core format library.
/// Simple heuristic used by the packer to detect executable payloads.  It is
/// intentionally forgiving; the goal is merely to catch obvious binaries when
/// a user accidentally tries to pack them.
pub fn is_data_executable(data: &[u8]) -> bool {
    if infer::is_app(data) {
        return true;
    }
    if data.starts_with(b"#!") {
        return true;
    }
    false
}

/// Representation of a single manifest entry inside a v2+ `.dlcpack`.
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct ManifestEntry {
    pub path: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub original_extension: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub type_path: Option<String>,
}

impl ManifestEntry {
    pub fn from_pack_item(item: &crate::PackItem) -> Self {
        ManifestEntry {
            path: item.path.clone(),
            original_extension: item
                .original_extension
                .clone()
                .filter(|s| !s.is_empty()),
            type_path: item.type_path.clone(),
        }
    }
}

/// V4 manifest entry: binary-serializable format for faster parsing.
/// Format: path_len(u32) + path(utf8) + ext_len(u8) + ext(utf8) + type_len(u16) + type(utf8) + block_id(u32) + block_offset(u32) + size(u32)
#[derive(Clone, Debug)]
pub struct V4ManifestEntry {
    pub path: String,
    pub original_extension: String,
    pub type_path: Option<String>,
    /// Which block contains this asset
    pub block_id: u32,
    /// Offset within the decompressed block's tar archive
    pub block_offset: u32,
    /// Uncompressed size (for progress/buffer allocation)
    pub size: u32,
}

impl V4ManifestEntry {
    pub fn from_pack_item(item: &crate::PackItem, block_id: u32, block_offset: u32) -> Self {
        V4ManifestEntry {
            path: item.path.clone(),
            original_extension: item.original_extension.clone().unwrap_or_default(),
            type_path: item.type_path.clone(),
            block_id,
            block_offset,
            size: item.plaintext.len() as u32,
        }
    }

    /// Write this entry in binary format (used by v4 format conversion)
    #[allow(dead_code)]
    fn write_binary<W: std::io::Write>(&self, writer: &mut PackWriter<W>) -> std::io::Result<()> {
        writer.write_u32(self.path.len() as u32)?;
        writer.write_bytes(self.path.as_bytes())?;

        writer.write_u8(self.original_extension.len() as u8)?;
        writer.write_bytes(self.original_extension.as_bytes())?;

        if let Some(ref tp) = self.type_path {
            writer.write_u16(tp.len() as u16)?;
            writer.write_bytes(tp.as_bytes())?;
        } else {
            writer.write_u16(0)?;
        }

        writer.write_u32(self.block_id)?;
        writer.write_u32(self.block_offset)?;
        writer.write_u32(self.size)
    }

    /// Read binary format from reader (used by v4 format parsing)
    #[allow(dead_code)]
    fn read_binary<R: std::io::Read>(reader: &mut PackReader<R>) -> std::io::Result<Self> {
        let path_len = reader.read_u32()? as usize;
        let path = String::from_utf8(reader.read_bytes(path_len)?)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;

        let ext_len = reader.read_u8()? as usize;
        let original_extension = if ext_len > 0 {
            String::from_utf8(reader.read_bytes(ext_len)?)
                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?
        } else {
            String::new()
        };

        let type_len = reader.read_u16()? as usize;
        let type_path = if type_len > 0 {
            let tp = String::from_utf8(reader.read_bytes(type_len)?)
                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
            Some(tp)
        } else {
            None
        };

        let block_id = reader.read_u32()?;
        let block_offset = reader.read_u32()?;
        let size = reader.read_u32()?;

        Ok(V4ManifestEntry {
            path,
            original_extension,
            type_path,
            block_id,
            block_offset,
            size,
        })
    }
}

/// V4 block metadata: stores information about a tar-gz block within the pack file
#[derive(Clone, Debug)]
pub struct BlockMetadata {
    pub block_id: u32,
    pub file_offset: u64,      // Where this block starts in the file
    pub encrypted_size: u32,   // Size of encrypted ciphertext
    pub uncompressed_size: u32, // Size after decompression (for buffer allocation)
    pub nonce: [u8; 12],       // Per-block nonce
    pub crc32: u32,            // CRC32 checksum for integrity
}

impl BlockMetadata {
    #[allow(dead_code)]
    fn write_binary<W: std::io::Write>(&self, writer: &mut PackWriter<W>) -> std::io::Result<()> {
        writer.write_u32(self.block_id)?;
        writer.write_u64(self.file_offset)?;
        writer.write_u32(self.encrypted_size)?;
        writer.write_u32(self.uncompressed_size)?;
        writer.write_bytes(&self.nonce)?;
        writer.write_u32(self.crc32)
    }

    #[allow(dead_code)]
    fn read_binary<R: std::io::Read>(reader: &mut PackReader<R>) -> std::io::Result<Self> {
        let block_id = reader.read_u32()?;
        let file_offset = reader.read_u64()?;
        let encrypted_size = reader.read_u32()?;
        let uncompressed_size = reader.read_u32()?;
        let nonce = reader.read_nonce()?;
        let crc32 = reader.read_u32()?;

        Ok(BlockMetadata {
            block_id,
            file_offset,
            encrypted_size,
            uncompressed_size,
            nonce,
            crc32,
        })
    }
}

// converters and migration helpers have been removed – the crate
// now only supports v4 packs.  The old `PackConverter` trait, the
// `V3toV4Converter` implementation and the `PackConverterRegistry` type
// were used to upgrade on‑disk packs to the latest format; they are
// retained in history if needed but no longer compiled.

/// Internal helper for binary parsing with offset management.
pub(crate) struct PackReader<R: std::io::Read> {
    inner: R,
}

impl<R: std::io::Read> PackReader<R> {
    pub fn new(inner: R) -> Self {
        Self { inner }
    }

    /// Read a byte.
    pub fn read_u8(&mut self) -> std::io::Result<u8> {
        let mut buf = [0u8; 1];
        self.inner.read_exact(&mut buf)?;
        Ok(buf[0])
    }

    /// Convenience method: read `len` bytes and immediately decrypt them with
    /// the provided AES-GCM key/nonce.
    #[allow(dead_code)]
    pub fn read_and_decrypt(
        &mut self,
        key: &crate::EncryptionKey,
        len: usize,
        nonce: &[u8],
    ) -> Result<Vec<u8>, DlcError> {
        let ciphertext = self.read_bytes(len)?;

        let cursor = std::io::Cursor::new(ciphertext);
        crate::pack_format::decrypt_with_key(key, cursor, nonce)
    }

    pub fn read_u16(&mut self) -> std::io::Result<u16> {
        let mut buf = [0u8; 2];
        self.inner.read_exact(&mut buf)?;
        Ok(u16::from_be_bytes(buf))
    }

    pub fn read_u32(&mut self) -> std::io::Result<u32> {
        let mut buf = [0u8; 4];
        self.inner.read_exact(&mut buf)?;
        Ok(u32::from_be_bytes(buf))
    }

    #[allow(dead_code)]
    pub fn read_u64(&mut self) -> std::io::Result<u64> {
        let mut buf = [0u8; 8];
        self.inner.read_exact(&mut buf)?;
        Ok(u64::from_be_bytes(buf))
    }

    pub fn read_bytes(&mut self, len: usize) -> std::io::Result<Vec<u8>> {
        let mut buf = vec![0u8; len];
        self.inner.read_exact(&mut buf)?;
        Ok(buf)
    }

    pub fn read_string_u16(&mut self) -> std::io::Result<String> {
        let len = self.read_u16()? as usize;
        self.read_string(len)
    }

    pub fn read_string(&mut self, len: usize) -> std::io::Result<String> {
        let bytes = self.read_bytes(len)?;
        // Avoid allocation for UTF-8 validation by using from_utf8_lossy internally,
        // but preserve error semantics for actual validation
        String::from_utf8(bytes)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
    }

    pub fn read_nonce(&mut self) -> std::io::Result<[u8; 12]> {
        let mut nonce = [0u8; 12];
        self.inner.read_exact(&mut nonce)?;
        Ok(nonce)
    }
}

// additional helpers available when the inner reader also implements `Seek`
#[allow(unused)]
impl<R: std::io::Read + std::io::Seek> PackReader<R> {
    /// Seek the underlying reader to `pos` and return the new position.
    pub fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
        self.inner.seek(pos)
    }
}

/// Internal helper for binary packing.
pub(crate) struct PackWriter<W: std::io::Write> {
    inner: W,
}

impl<W: std::io::Write> PackWriter<W> {
    pub fn new(inner: W) -> Self {
        Self { inner }
    }

    /// Encrypt `plaintext` with the provided key/nonce and write the
    /// resulting ciphertext to the underlying writer.
    #[allow(dead_code)]
    pub fn write_encrypted(
        &mut self,
        key: &crate::EncryptionKey,
        nonce: &[u8],
        plaintext: &[u8],
    ) -> Result<(), DlcError> {
        use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead};
        let cipher = key.with_secret(|kb| {
            Aes256Gcm::new_from_slice(kb).map_err(|e| DlcError::CryptoError(e.to_string()))
        })?;
        let ct = cipher
            .encrypt(Nonce::from_slice(nonce), plaintext)
            .map_err(|_| DlcError::EncryptionFailed("block encryption failed".into()))?;
        self.write_bytes(&ct).map_err(|e| DlcError::Other(e.to_string()))
    }

    pub fn write_u8(&mut self, val: u8) -> std::io::Result<()> {
        self.inner.write_all(&[val])
    }

    pub fn write_u16(&mut self, val: u16) -> std::io::Result<()> {
        self.inner.write_all(&val.to_be_bytes())
    }

    pub fn write_u32(&mut self, val: u32) -> std::io::Result<()> {
        self.inner.write_all(&val.to_be_bytes())
    }

    pub fn write_u64(&mut self, val: u64) -> std::io::Result<()> {
        self.inner.write_all(&val.to_be_bytes())
    }

    pub fn write_bytes(&mut self, bytes: &[u8]) -> std::io::Result<()> {
        self.inner.write_all(bytes)
    }

    pub fn write_string_u16(&mut self, s: &str) -> std::io::Result<()> {
        let bytes = s.as_bytes();
        self.write_u16(bytes.len() as u16)?;
        self.write_bytes(bytes)
    }

    pub fn finish(mut self) -> std::io::Result<W> {
        self.inner.flush()?;
        Ok(self.inner)
    }
}

/// Represents the header of any version of a `.dlcpack`.
struct PackHeader {
    version: u8,
    product: String,
    dlc_id: String,
}

impl PackHeader {
    fn read<R: std::io::Read>(reader: &mut PackReader<R>) -> std::io::Result<Self> {
        let magic = reader.read_bytes(4)?;
        if magic != DLC_PACK_MAGIC {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                "invalid dlcpack magic",
            ));
        }

        let version = reader.read_u8()?;
        let mut product = String::new();

        if version == 3 || version == 4 {
            product = reader.read_string_u16()?;
        } else if version > 4 {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                format!("unsupported pack version: {}", version),
            ));
        }

        let dlc_id = reader.read_string_u16()?;

        Ok(PackHeader {
            version,
            product,
            dlc_id,
        })
    }

    fn write<W: std::io::Write>(&self, writer: &mut PackWriter<W>) -> std::io::Result<()> {
        writer.write_bytes(DLC_PACK_MAGIC)?;
        writer.write_u8(self.version)?;

        if self.version == 3 || self.version == 4 {
            writer.write_string_u16(&self.product)?;
        }

        writer.write_string_u16(&self.dlc_id)
    }
}

/// Decrypt with a 32-byte AES key using AES-GCM.  This is the same logic that
/// used to live in `lib.rs`; moving it here allows crates that only depend on
/// the pack format helpers to perform decryption without pulling in the
/// higher-level API.

use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::AeadInPlace};
use secure_gate::ExposeSecret;

use crate::{DlcError, DlcId, EncryptionKey, PackItem, Product};

pub(crate) fn decrypt_with_key<R: std::io::Read>(
    key: &crate::EncryptionKey,
    mut reader: R,
    nonce: &[u8],
) -> Result<Vec<u8>, DlcError> {
    // read ciphertext into a single buffer; decrypt it in-place to avoid
    // allocating a second plaintext buffer.
    let mut buf = Vec::new();
    reader
        .read_to_end(&mut buf)
        .map_err(|e| DlcError::Other(e.to_string()))?;

    key.with_secret(|key_bytes| {
        if key_bytes.len() != 32 {
            return Err(DlcError::InvalidEncryptKey(
                "encrypt key must be 32 bytes (AES-256)".into(),
            ));
        }
        if nonce.len() != 12 {
            return Err(DlcError::InvalidNonce(
                "nonce must be 12 bytes (AES-GCM)".into(),
            ));
        }
        let cipher = Aes256Gcm::new_from_slice(key_bytes)
            .map_err(|e| DlcError::CryptoError(e.to_string()))?;
        let nonce = Nonce::from_slice(nonce);
        // decrypt in-place; `buf` will be overwritten with plaintext
        cipher
            .decrypt_in_place(nonce, &[], &mut buf)
            .map_err(|_|
                DlcError::DecryptionFailed(
                    "authentication failed (incorrect key or corrupted ciphertext)".to_string(),
                )
            )
    })?;
    Ok(buf)
}

/// Compression level for packing. Controls the trade-off between pack size and packing time.
/// Higher levels produce smaller files but take longer to create.
#[derive(Debug, Clone, Copy)]
pub enum CompressionLevel {
    /// Fast compression (level 1), suitable for rapid iterations
    Fast,
    /// Balanced compression (level 6, default), good trade-off
    Default,
    /// Best compression (level 9), smallest file size for distribution
    Best,
}

impl From<CompressionLevel> for flate2::Compression {
    fn from(level: CompressionLevel) -> Self {
        match level {
            CompressionLevel::Fast => flate2::Compression::fast(),
            CompressionLevel::Default => flate2::Compression::default(),
            CompressionLevel::Best => flate2::Compression::best(),
        }
    }
}

/// Pack multiple entries into a single encrypted `.dlcpack` binary using the latest version format.  This is the main entry point for pack creation and is used by the CLI tool; it can also be used by other tools that want to generate packs programmatically.
pub fn pack_encrypted_pack(
    dlc_id: &DlcId,
    items: &[PackItem],
    product: &Product,
    key: &EncryptionKey,
    block_size: usize,
) -> Result<Vec<u8>, DlcError> {
    use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead};
    use flate2::Compression;
    use flate2::write::GzEncoder;
    use tar::Builder;

    let cipher = key.with_secret(|kb| {
        Aes256Gcm::new_from_slice(kb.as_slice()).map_err(|e| DlcError::CryptoError(e.to_string()))
    })?;

    // 1. Group items into blocks
    let mut blocks: Vec<Vec<&PackItem>> = Vec::new();
    let mut current_block = Vec::new();
    let mut current_size = 0;

    for item in items {
        if !current_block.is_empty() && current_size + item.plaintext.len() > block_size {
            blocks.push(std::mem::take(&mut current_block));
            current_size = 0;
        }
        current_size += item.plaintext.len();
        current_block.push(item);
    }
    if !current_block.is_empty() {
        blocks.push(current_block);
    }

    // 2. Build blocks and track offsets
    let mut encrypted_blocks = Vec::new();
    let mut manifest_entries = Vec::new();
    let mut block_metadatas = Vec::new();

    for (block_id, block_items) in blocks.into_iter().enumerate() {
        let block_id = block_id as u32;
        let mut tar_gz = Vec::new();
        let mut uncompressed_size = 0;
        {
            let mut gz = GzEncoder::new(&mut tar_gz, Compression::default());
            {
                let mut tar = Builder::new(&mut gz);
                let mut offset = 0;

                for item in block_items {
                    let mut header = tar::Header::new_gnu();
                    header.set_size(item.plaintext.len() as u64);
                    header.set_mode(0o644);
                    header.set_cksum();

                    let path_str = item.path.clone();
                    manifest_entries.push(V4ManifestEntry {
                        path: path_str,
                        original_extension: item.original_extension.clone().unwrap_or_default(),
                        type_path: item.type_path.clone(),
                        block_id,
                        block_offset: offset,
                        size: item.plaintext.len() as u32,
                    });

                    tar.append_data(&mut header, &item.path, &item.plaintext[..])
                        .map_err(|e| DlcError::Other(e.to_string()))?;

                    // tar header is 512, plus data (padded to 512)
                    let data_len = item.plaintext.len() as u32;
                    let padded_len = (data_len + 511) & !511;
                    offset += 512 + padded_len;
                    uncompressed_size += data_len;
                }
                tar.finish().map_err(|e| DlcError::Other(e.to_string()))?;
            }
            gz.finish().map_err(|e| DlcError::Other(e.to_string()))?;
        }

        // Encrypt block
        let nonce_bytes: [u8; 12] = rand::random();
        let nonce = Nonce::from_slice(&nonce_bytes);
        let ciphertext = cipher
            .encrypt(nonce, tar_gz.as_slice())
            .map_err(|_| DlcError::EncryptionFailed("block encryption failed".into()))?;

        let crc32 = crc32fast::hash(&ciphertext);

        block_metadatas.push(BlockMetadata {
            block_id,
            file_offset: 0, // Fill later
            encrypted_size: ciphertext.len() as u32,
            uncompressed_size,
            nonce: nonce_bytes,
            crc32,
        });

        encrypted_blocks.push(ciphertext);
    }

    // 3. Assemble final binary
    let product_str = product.as_ref();
    let dlc_id_str = dlc_id.to_string();

    let mut out = Vec::new();
    {
        let mut writer = PackWriter::new(&mut out);

        let header = PackHeader {
            version: 4,
            product: product_str.to_string(),
            dlc_id: dlc_id_str.clone(),
        };
        header.write(&mut writer).map_err(|e| DlcError::Other(e.to_string()))?;

        // Manifest
        writer.write_u32(manifest_entries.len() as u32).map_err(|e| DlcError::Other(e.to_string()))?;
        for entry in &manifest_entries {
            entry.write_binary(&mut writer).map_err(|e| DlcError::Other(e.to_string()))?;
        }

        // Block Metadata placeholder
        writer.write_u32(block_metadatas.len() as u32).map_err(|e| DlcError::Other(e.to_string()))?;
        writer.finish().map_err(|e| DlcError::Other(e.to_string()))?;
    }
    let metadata_start_pos = out.len();
    {
        let mut writer = PackWriter::new(&mut out);
        for meta in &block_metadatas {
            meta.write_binary(&mut writer).map_err(|e| DlcError::Other(e.to_string()))?;
        }
        writer.finish().map_err(|e| DlcError::Other(e.to_string()))?;
    }

    // Encrypted Blocks
    for (i, block) in encrypted_blocks.into_iter().enumerate() {
        let pos = out.len() as u64;
        block_metadatas[i].file_offset = pos;
        out.extend_from_slice(&block);
    }

    // Rewrite block metadatas with correct file_offsets
    {
        let mut writer_fixed = PackWriter::new(&mut out[metadata_start_pos..]);
        for meta in &block_metadatas {
            meta.write_binary(&mut writer_fixed).map_err(|e| DlcError::Other(e.to_string()))?;
        }
        writer_fixed.finish().map_err(|e| DlcError::Other(e.to_string()))?;
    }

    Ok(out)
}

pub type Version = usize;

/// Returns: (product, dlc_id, version, entries)
pub fn parse_encrypted_pack<R: std::io::Read>(
    reader: R,
) -> Result<
    (
        Product,
        DlcId,
        Version,
        Vec<(String, crate::asset_loader::EncryptedAsset)>,
        Vec<BlockMetadata>,
    ),
    std::io::Error,
> {
    use std::io::ErrorKind;

    let mut reader = PackReader::new(reader);
    let header = PackHeader::read(&mut reader)?;
    let mut block_metadatas = Vec::new();

    let entries = if header.version < DLC_PACK_VERSION_LATEST {
        return Err(std::io::Error::new(
            ErrorKind::InvalidData,
            format!("unsupported pack version: {}", header.version),
        ));
    } else if header.version == 4 {
        // version 4: hybrid multi-block format
        let manifest_count = reader.read_u32()? as usize;
        let mut manifest: Vec<V4ManifestEntry> = Vec::with_capacity(manifest_count);
        for _ in 0..manifest_count {
            manifest.push(V4ManifestEntry::read_binary(&mut reader)?);
        }

        let block_count = reader.read_u32()? as usize;
        block_metadatas = Vec::with_capacity(block_count);
        for _ in 0..block_count {
            block_metadatas.push(BlockMetadata::read_binary(&mut reader)?);
        }

        let mut out = Vec::with_capacity(manifest.len());
        for entry in manifest {
            out.push((
                entry.path,
                crate::asset_loader::EncryptedAsset {
                    dlc_id: header.dlc_id.clone(),
                    original_extension: entry.original_extension,
                    type_path: entry.type_path,
                    nonce: [0u8; 12],
                    ciphertext: std::sync::Arc::new([]),
                    block_id: entry.block_id,
                    block_offset: entry.block_offset,
                    size: entry.size,
                },
            ));
        }
        out
    } else {
        // version 2/3: manifest JSON followed by shared nonce and ciphertext
        let manifest_len = reader.read_u32()? as usize;
        let manifest_bytes = reader.read_bytes(manifest_len)?;
        let manifest: Vec<ManifestEntry> = serde_json::from_slice(&manifest_bytes)
            .map_err(|e| std::io::Error::new(ErrorKind::InvalidData, e))?;

        let mut out = Vec::with_capacity(manifest.len());

        let shared_nonce = reader.read_nonce()?;
        let shared_ciphertext_len = reader.read_u32()? as usize;
        let shared_ciphertext: std::sync::Arc<[u8]> = reader.read_bytes(shared_ciphertext_len)?.into();

        for entry in manifest {
            out.push((
                entry.path,
                crate::asset_loader::EncryptedAsset {
                    dlc_id: header.dlc_id.clone(),
                    original_extension: entry.original_extension.unwrap_or_default(),
                    type_path: entry.type_path,
                    nonce: shared_nonce,
                    ciphertext: shared_ciphertext.clone(),
                    block_id: 0,
                    block_offset: 0,
                    size: shared_ciphertext.len() as u32,
                },
            ));
        }
        out
    };

    Ok((
        Product::from(header.product),
        DlcId::from(header.dlc_id),
        header.version as usize,
        entries,
        block_metadatas,
    ))
}

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

    #[test]
    fn forbidden_list_contains_known() {
        assert!(is_forbidden_extension("exe"));
        assert!(is_forbidden_extension("EXE"));
        assert!(!is_forbidden_extension("png"));
    }

    #[test]
    fn manifest_roundtrip() {
        let item = crate::PackItem::new("foo.txt", b"hello" as &[u8]).unwrap();
        let entry = ManifestEntry::from_pack_item(&item);
        let bytes = serde_json::to_vec(&entry).unwrap();
        let back: ManifestEntry = serde_json::from_slice(&bytes).unwrap();
        assert_eq!(entry.path, back.path);
    }
}
