use core::error::Error;
use core::fmt;
use core::mem::size_of;

use alloc::string::FromUtf8Error;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;

pub const SIG_LOCAL_FILE_HEADER: &[u8; 4] = b"\x50\x4B\x03\x04";
pub const SIG_CENTRAL_DIRECTORY: &[u8; 4] = b"\x50\x4B\x01\x02";
pub const SIG_EOCD: &[u8; 4] = b"\x50\x4B\x05\x06";
pub const SIG_ZIP64_EOCD: &[u8; 4] = b"\x50\x4B\x06\x06";
pub const SIG_ZIP64_LOCATOR: &[u8; 4] = b"\x50\x4B\x06\x07";

pub(crate) const SIZEOF_SIGNATURE: usize = 4;
const SZ_EXTRA_HDR: usize = size_of::<u16>() + size_of::<u16>();

// LFH = sig(4) + 7×u16 + 3×u32  = 4 + 14 + 12 = 30
pub const SIZEOF_LFH_BASE: usize = SIZEOF_SIGNATURE + 7 * size_of::<u16>() + 3 * size_of::<u32>();
// CD  = sig(4) + 11×u16 + 5×u32 = 4 + 22 + 20 = 46
pub const SIZEOF_CD_BASE: usize = SIZEOF_SIGNATURE + 11 * size_of::<u16>() + 5 * size_of::<u32>();
// EOCD = sig(4) + 5×u16 + 2×u32 = 4 + 10 + 8 = 22
const SIZEOF_EOCD_BASE: usize = SIZEOF_SIGNATURE + 5 * size_of::<u16>() + 2 * size_of::<u32>();
// Zip64 EOCD = sig(4) + 5×u64 + 2×u16 + 2×u32 = 4 + 40 + 4 + 8 = 56
const SIZEOF_ZIP64_EOCD: usize =
    SIZEOF_SIGNATURE + 5 * size_of::<u64>() + 2 * size_of::<u16>() + 2 * size_of::<u32>();
// Zip64 Locator = sig(4) + 2×u32 + u64 = 4 + 8 + 8 = 20
pub const SIZEOF_ZIP64_LOCATOR: usize = SIZEOF_SIGNATURE + 2 * size_of::<u32>() + size_of::<u64>();
// LFH Zip64 extra: tag(2) + size(2) + uncomp(8) + comp(8) = 20
pub const SIZEOF_LFH_ZIP64_EXTRA: usize = 2 * size_of::<u16>() + 2 * size_of::<u64>();
const SIZEOF_LFH_ZIP64_EXTRA_DATA: u16 = (2 * size_of::<u64>()) as u16; // 16
// CD Zip64 extra: tag(2) + size(2) + uncomp(8) + comp(8) + offset(8) = 28
const SIZEOF_CD_ZIP64_EXTRA: usize = 2 * size_of::<u16>() + 3 * size_of::<u64>();
const SIZEOF_CD_ZIP64_EXTRA_DATA: u16 = (3 * size_of::<u64>()) as u16; // 24
// Zip64 EOCD record-size field = total(56) − sig(4) − self_field(8) = 44
const SIZEOF_ZIP64_EOCD_RECORD: u64 =
    (SIZEOF_ZIP64_EOCD - SIZEOF_SIGNATURE - size_of::<u64>()) as u64;
// --- LFH field offsets (bytes from start of LFH) ---
const LFH_OFF_FLAGS: usize = SIZEOF_SIGNATURE + size_of::<u16>(); // 6
const LFH_OFF_COMPRESSION: usize = LFH_OFF_FLAGS + size_of::<u16>(); // 8
const LFH_OFF_DOS_TIME: usize = LFH_OFF_COMPRESSION + size_of::<u16>(); // 10
const LFH_OFF_DOS_DATE: usize = LFH_OFF_DOS_TIME + size_of::<u16>(); // 12
const LFH_OFF_CRC32: usize = LFH_OFF_DOS_DATE + size_of::<u16>(); // 14
const LFH_OFF_COMP_SIZE: usize = LFH_OFF_CRC32 + size_of::<u32>(); // 18
const LFH_OFF_UNCOMP_SIZE: usize = LFH_OFF_COMP_SIZE + size_of::<u32>(); // 22
pub(crate) const LFH_OFF_NAME_LEN: usize = LFH_OFF_UNCOMP_SIZE + size_of::<u32>(); // 26
pub(crate) const LFH_OFF_EXTRA_LEN: usize = LFH_OFF_NAME_LEN + size_of::<u16>(); // 28
const CD_OFF_FLAGS: usize = SIZEOF_SIGNATURE + 2 * size_of::<u16>(); // 8
const CD_OFF_COMPRESSION: usize = CD_OFF_FLAGS + size_of::<u16>(); // 10
const CD_OFF_DOS_TIME: usize = CD_OFF_COMPRESSION + size_of::<u16>(); // 12
const CD_OFF_DOS_DATE: usize = CD_OFF_DOS_TIME + size_of::<u16>(); // 14
const CD_OFF_CRC32: usize = CD_OFF_DOS_DATE + size_of::<u16>(); // 16
const CD_OFF_COMP_SIZE: usize = CD_OFF_CRC32 + size_of::<u32>(); // 20
const CD_OFF_UNCOMP_SIZE: usize = CD_OFF_COMP_SIZE + size_of::<u32>(); // 24
const CD_OFF_NAME_LEN: usize = CD_OFF_UNCOMP_SIZE + size_of::<u32>(); // 28
const CD_OFF_EXTRA_LEN: usize = CD_OFF_NAME_LEN + size_of::<u16>(); // 30
const CD_OFF_COMMENT_LEN: usize = CD_OFF_EXTRA_LEN + size_of::<u16>(); // 32
const CD_OFF_LOCAL_HEADER_OFFSET: usize =
    CD_OFF_COMMENT_LEN + 3 * size_of::<u16>() + size_of::<u32>(); // 42
const EOCD_OFF_DISK_ENTRIES: usize = SIZEOF_SIGNATURE + 2 * size_of::<u16>(); // 8
const EOCD_OFF_TOTAL_ENTRIES: usize = EOCD_OFF_DISK_ENTRIES + size_of::<u16>(); // 10
const EOCD_OFF_CD_SIZE: usize = EOCD_OFF_TOTAL_ENTRIES + size_of::<u16>(); // 12
const EOCD_OFF_CD_OFFSET: usize = EOCD_OFF_CD_SIZE + size_of::<u32>(); // 16
const EOCD_OFF_COMMENT_LEN: usize = EOCD_OFF_CD_OFFSET + size_of::<u32>(); // 20
const ZIP64_EOCD_OFF_TOTAL_ENTRIES: usize =
    SIZEOF_SIGNATURE + 2 * size_of::<u64>() + 2 * size_of::<u16>() + 2 * size_of::<u32>(); // 32
const ZIP64_EOCD_OFF_CD_SIZE: usize = ZIP64_EOCD_OFF_TOTAL_ENTRIES + size_of::<u64>(); // 40
const ZIP64_EOCD_OFF_CD_OFFSET: usize = ZIP64_EOCD_OFF_CD_SIZE + size_of::<u64>(); // 48
const ZIP64_LOCATOR_OFF_EOCD_OFFSET: usize = SIZEOF_SIGNATURE + size_of::<u32>(); // 8
pub const SIG_DATA_DESCRIPTOR: &[u8; 4] = b"\x50\x4B\x07\x08";
pub const FLAG_DATA_DESCRIPTOR: u16 = 0x0008;
pub const ZIP64_EXTRA_TAG: u16 = 0x0001;
pub const ZIP_VERSION_20: u16 = 20;
pub const ZIP_VERSION_45: u16 = 45;
pub const MAX_U16: u16 = 0xFFFF;
pub const MAX_U32: u32 = 0xFFFFFFFF;
pub const MAX_ZIP64: u64 = 0xFFFFFFFF;
pub const MAX_COMMENT_LEN: usize = 65535;

#[inline]
fn read_u16_le(buf: &[u8], offset: usize) -> u16 {
    u16::from_le_bytes([buf[offset], buf[offset + 1]])
}

#[inline]
fn read_u32_le(buf: &[u8], offset: usize) -> u32 {
    u32::from_le_bytes([
        buf[offset],
        buf[offset + 1],
        buf[offset + 2],
        buf[offset + 3],
    ])
}

#[inline]
fn read_u64_le(buf: &[u8], offset: usize) -> u64 {
    u64::from_le_bytes([
        buf[offset],
        buf[offset + 1],
        buf[offset + 2],
        buf[offset + 3],
        buf[offset + 4],
        buf[offset + 5],
        buf[offset + 6],
        buf[offset + 7],
    ])
}

fn parse_zip64_extra(
    data: &[u8],
    has_uncompressed: bool,
    has_compressed: bool,
    has_offset: bool,
) -> (Option<u64>, Option<u64>, Option<u64>) {
    let mut idx = 0;
    let mut uncompressed_size = None;
    let mut compressed_size = None;
    let mut local_header_offset = None;

    if has_uncompressed || has_compressed || has_offset {
        if has_uncompressed && idx + size_of::<u64>() <= data.len() {
            uncompressed_size = Some(read_u64_le(data, idx));
            idx += 8;
        }
        if has_compressed && idx + size_of::<u64>() <= data.len() {
            compressed_size = Some(read_u64_le(data, idx));
            idx += 8;
        }
        if has_offset && idx + size_of::<u64>() <= data.len() {
            local_header_offset = Some(read_u64_le(data, idx));
            #[allow(unused_assignments)]
            {
                idx += 8;
            }
        }
    } else {
        if idx + size_of::<u64>() <= data.len() {
            uncompressed_size = Some(read_u64_le(data, idx));
            idx += 8;
        }
        if idx + size_of::<u64>() <= data.len() {
            compressed_size = Some(read_u64_le(data, idx));
            idx += 8;
        }
        if idx + size_of::<u64>() <= data.len() {
            local_header_offset = Some(read_u64_le(data, idx));
        }
    }

    (uncompressed_size, compressed_size, local_header_offset)
}

#[inline]
pub fn to_usize<E: Error>(value: u64) -> Result<usize, ZipError<E>> {
    usize::try_from(value).map_err(|_| ZipError::SizeOverflow)
}

pub trait ReadAt {
    type Error: Error;
    fn size(&self) -> u64;
    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), Self::Error>;
}

#[derive(Debug)]
pub enum ZipError<E: Error> {
    ReadError(E),
    InvalidSignature(&'static str),
    FormatError(&'static str),
    Utf8Error(FromUtf8Error),
    UnsupportedCompression(u16),
    UnsupportedFileData,
    SizeOverflow,
}

impl<E: Error> fmt::Display for ZipError<E> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::ReadError(e) => write!(f, "Read error: {}", e),
            Self::InvalidSignature(s) => write!(f, "Invalid signature: {}", s),
            Self::FormatError(s) => write!(f, "Format error: {}", s),
            Self::Utf8Error(e) => write!(f, "UTF-8 error: {}", e),
            Self::UnsupportedCompression(m) => write!(f, "Unsupported compression method: {}", m),
            Self::UnsupportedFileData => write!(f, "Unsupported File Data"),
            Self::SizeOverflow => write!(f, "Value too large for usize (likely a 32-bit target)"),
        }
    }
}

impl<E: Error> Error for ZipError<E> {}

#[derive(Debug)]
pub(crate) enum CoreError {
    Utf8(FromUtf8Error),
    SizeOverflow,
}

impl<E: Error> From<CoreError> for ZipError<E> {
    fn from(e: CoreError) -> Self {
        match e {
            CoreError::Utf8(ue) => ZipError::Utf8Error(ue),
            CoreError::SizeOverflow => ZipError::SizeOverflow,
        }
    }
}

pub type DecompressFn = fn(&[u8]) -> Result<Vec<u8>, DecompressError>;

#[derive(Debug, Clone)]
pub struct DecompressError(pub &'static str);

impl fmt::Display for DecompressError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "decompression error: {}", self.0)
    }
}

impl Error for DecompressError {}

#[derive(Clone, Debug)]
pub enum FileData {
    Memory(Vec<u8>),
    DataOffset { offset: u64, size: usize },
}

#[derive(Clone, Debug)]
pub struct FileEntry {
    pub compression_method: u16,
    pub flags: u16,
    pub dos_mod_time: u16,
    pub dos_mod_date: u16,
    pub crc32: u32,
    pub compressed_size: u64,
    pub uncompressed_size: u64,
    pub local_header_offset: u64,
    pub name: String,
    pub data: FileData,
}

impl FileEntry {
    pub(crate) fn read_lfh(buf: &[u8], cursor: &mut usize) -> Result<Self, FromUtf8Error> {
        let idx = *cursor;

        let flags = read_u16_le(buf, idx + LFH_OFF_FLAGS);
        let compression_method = read_u16_le(buf, idx + LFH_OFF_COMPRESSION);
        let dos_mod_time = read_u16_le(buf, idx + LFH_OFF_DOS_TIME);
        let dos_mod_date = read_u16_le(buf, idx + LFH_OFF_DOS_DATE);
        let crc32 = read_u32_le(buf, idx + LFH_OFF_CRC32);

        let comp_32 = read_u32_le(buf, idx + LFH_OFF_COMP_SIZE);
        let uncomp_32 = read_u32_le(buf, idx + LFH_OFF_UNCOMP_SIZE);

        let mut compressed_size = comp_32 as u64;
        let mut uncompressed_size = uncomp_32 as u64;
        let name_len = read_u16_le(buf, idx + LFH_OFF_NAME_LEN) as usize;
        let extra_len = read_u16_le(buf, idx + LFH_OFF_EXTRA_LEN) as usize;

        if extra_len > 0 {
            let extra_start = idx + SIZEOF_LFH_BASE + name_len;
            let extra_end = extra_start + extra_len;
            let mut ex_cursor = extra_start;
            while ex_cursor + SZ_EXTRA_HDR <= extra_end {
                let header_id = read_u16_le(buf, ex_cursor);
                let data_size = read_u16_le(buf, ex_cursor + size_of::<u16>()) as usize;
                ex_cursor += SZ_EXTRA_HDR;
                if header_id == ZIP64_EXTRA_TAG && ex_cursor + data_size <= extra_end {
                    let is_dd = flags & FLAG_DATA_DESCRIPTOR != 0;
                    let (new_uncomp, new_comp, _) = parse_zip64_extra(
                        &buf[ex_cursor..ex_cursor + data_size],
                        is_dd || uncomp_32 == MAX_U32,
                        is_dd || comp_32 == MAX_U32,
                        false,
                    );
                    if let Some(val) = new_uncomp {
                        uncompressed_size = val;
                    }
                    if let Some(val) = new_comp {
                        compressed_size = val;
                    }
                    break;
                }
                ex_cursor += data_size;
            }
        }

        let name_start = idx + SIZEOF_LFH_BASE;
        let name = match core::str::from_utf8(&buf[name_start..name_start + name_len]) {
            Ok(s) => String::from(s),
            Err(_) => {
                return Err(
                    String::from_utf8(buf[name_start..name_start + name_len].to_vec()).unwrap_err(),
                );
            }
        };

        *cursor += SIZEOF_LFH_BASE + name_len + extra_len;

        Ok(Self {
            compression_method,
            flags,
            dos_mod_time,
            dos_mod_date,
            crc32,
            compressed_size,
            uncompressed_size,
            local_header_offset: 0,
            name,
            data: FileData::Memory(Vec::new()),
        })
    }

    pub fn lfh_len(&self, local_offset: u64) -> usize {
        let is_zip64 = self.compressed_size >= MAX_ZIP64
            || self.uncompressed_size >= MAX_ZIP64
            || local_offset >= MAX_ZIP64;
        let extra_len = if is_zip64 { SIZEOF_LFH_ZIP64_EXTRA } else { 0 };
        SIZEOF_LFH_BASE + self.name.as_bytes().len() + extra_len
    }

    pub fn write_lfh(&self, local_offset: u64, buf: &mut Vec<u8>) {
        let name_bytes = self.name.as_bytes();
        let is_zip64 = self.compressed_size >= MAX_ZIP64
            || self.uncompressed_size >= MAX_ZIP64
            || local_offset >= MAX_ZIP64;
        let extra_len = if is_zip64 { SIZEOF_LFH_ZIP64_EXTRA } else { 0 };

        buf.extend_from_slice(SIG_LOCAL_FILE_HEADER);

        let version = if is_zip64 {
            ZIP_VERSION_45
        } else {
            ZIP_VERSION_20
        };
        buf.extend_from_slice(&version.to_le_bytes());
        buf.extend_from_slice(&self.flags.to_le_bytes());
        buf.extend_from_slice(&self.compression_method.to_le_bytes());
        buf.extend_from_slice(&self.dos_mod_time.to_le_bytes());
        buf.extend_from_slice(&self.dos_mod_date.to_le_bytes());
        buf.extend_from_slice(&self.crc32.to_le_bytes());

        if is_zip64 {
            buf.extend_from_slice(&MAX_U32.to_le_bytes());
            buf.extend_from_slice(&MAX_U32.to_le_bytes());
        } else {
            buf.extend_from_slice(&(self.compressed_size as u32).to_le_bytes());
            buf.extend_from_slice(&(self.uncompressed_size as u32).to_le_bytes());
        }

        buf.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
        buf.extend_from_slice(&(extra_len as u16).to_le_bytes());
        buf.extend_from_slice(name_bytes);

        if is_zip64 {
            buf.extend_from_slice(&ZIP64_EXTRA_TAG.to_le_bytes());
            buf.extend_from_slice(&SIZEOF_LFH_ZIP64_EXTRA_DATA.to_le_bytes());
            buf.extend_from_slice(&self.uncompressed_size.to_le_bytes());
            buf.extend_from_slice(&self.compressed_size.to_le_bytes());
        }
    }

    pub fn data_len(&self) -> usize {
        match &self.data {
            FileData::Memory(v) => v.len(),
            FileData::DataOffset { size, .. } => *size,
        }
    }

    pub(crate) fn read_cd(buf: &[u8], cursor: &mut usize) -> Result<Self, CoreError> {
        let idx = *cursor;
        if idx + SIZEOF_CD_BASE > buf.len() {
            return Err(CoreError::SizeOverflow);
        }

        let flags = read_u16_le(buf, idx + CD_OFF_FLAGS);
        let compression_method = read_u16_le(buf, idx + CD_OFF_COMPRESSION);
        let dos_mod_time = read_u16_le(buf, idx + CD_OFF_DOS_TIME);
        let dos_mod_date = read_u16_le(buf, idx + CD_OFF_DOS_DATE);
        let crc32 = read_u32_le(buf, idx + CD_OFF_CRC32);

        let comp_32 = read_u32_le(buf, idx + CD_OFF_COMP_SIZE);
        let uncomp_32 = read_u32_le(buf, idx + CD_OFF_UNCOMP_SIZE);
        let name_len = read_u16_le(buf, idx + CD_OFF_NAME_LEN) as usize;
        let extra_len = read_u16_le(buf, idx + CD_OFF_EXTRA_LEN) as usize;
        let comment_len = read_u16_le(buf, idx + CD_OFF_COMMENT_LEN) as usize;
        let offset_32 = read_u32_le(buf, idx + CD_OFF_LOCAL_HEADER_OFFSET);

        let mut compressed_size = comp_32 as u64;
        let mut uncompressed_size = uncomp_32 as u64;
        let mut local_header_offset = offset_32 as u64;

        let total_len = SIZEOF_CD_BASE
            .checked_add(name_len)
            .and_then(|v| v.checked_add(extra_len))
            .and_then(|v| v.checked_add(comment_len))
            .ok_or(CoreError::SizeOverflow)?;

        if idx + total_len > buf.len() {
            return Err(CoreError::SizeOverflow);
        }

        if extra_len > 0 {
            let mut ex_cursor = idx + SIZEOF_CD_BASE + name_len;
            let ex_end = ex_cursor + extra_len;
            while ex_cursor + SZ_EXTRA_HDR <= ex_end {
                let header_id = read_u16_le(buf, ex_cursor);
                let data_size = read_u16_le(buf, ex_cursor + size_of::<u16>()) as usize;
                ex_cursor += SZ_EXTRA_HDR;
                if header_id == ZIP64_EXTRA_TAG && ex_cursor + data_size <= ex_end {
                    let (new_uncomp, new_comp, new_offset) = parse_zip64_extra(
                        &buf[ex_cursor..ex_cursor + data_size],
                        uncomp_32 == MAX_U32,
                        comp_32 == MAX_U32,
                        offset_32 == MAX_U32,
                    );
                    if let Some(val) = new_uncomp {
                        uncompressed_size = val;
                    }
                    if let Some(val) = new_comp {
                        compressed_size = val;
                    }
                    if let Some(val) = new_offset {
                        local_header_offset = val;
                    }
                    break;
                }
                ex_cursor += data_size;
            }
        }

        let name_start = idx + SIZEOF_CD_BASE;
        let name = match core::str::from_utf8(&buf[name_start..name_start + name_len]) {
            Ok(s) => String::from(s),
            Err(_) => {
                let err =
                    String::from_utf8(buf[name_start..name_start + name_len].to_vec()).unwrap_err();
                return Err(CoreError::Utf8(err));
            }
        };

        *cursor += total_len;

        Ok(Self {
            compression_method,
            flags,
            dos_mod_time,
            dos_mod_date,
            crc32,
            compressed_size,
            uncompressed_size,
            local_header_offset,
            name,
            data: FileData::DataOffset {
                offset: local_header_offset
                    + SIZEOF_LFH_BASE as u64
                    + name_len as u64
                    + extra_len as u64,
                size: usize::try_from(compressed_size).map_err(|_| CoreError::SizeOverflow)?,
            },
        })
    }

    pub fn with_local_file_header<R: ReadAt>(
        ra: &R,
        offset: u64,
    ) -> Result<Self, ZipError<R::Error>> {
        let mut base = [0u8; SIZEOF_LFH_BASE];
        ra.read_at(offset, &mut base).map_err(ZipError::ReadError)?;

        if base[..SIZEOF_SIGNATURE] != *SIG_LOCAL_FILE_HEADER {
            return Err(ZipError::InvalidSignature("LFH"));
        }

        let name_len = read_u16_le(&base, LFH_OFF_NAME_LEN) as usize;
        let extra_len = read_u16_le(&base, LFH_OFF_EXTRA_LEN) as usize;

        let rest_len = name_len + extra_len;
        let mut full = vec![0u8; SIZEOF_LFH_BASE + rest_len];
        full[..SIZEOF_LFH_BASE].copy_from_slice(&base);
        ra.read_at(
            offset + SIZEOF_LFH_BASE as u64,
            &mut full[SIZEOF_LFH_BASE..],
        )
        .map_err(ZipError::ReadError)?;

        let mut cursor = 0;
        let mut entry = Self::read_lfh(&full, &mut cursor).map_err(ZipError::Utf8Error)?;

        let data_offset = offset + SIZEOF_LFH_BASE as u64 + name_len as u64 + extra_len as u64;

        entry.local_header_offset = offset;
        entry.data = FileData::DataOffset {
            offset: data_offset,
            size: to_usize(entry.compressed_size)?,
        };

        Ok(entry)
    }

    pub fn cd_len(&self, local_offset: u64) -> usize {
        let is_zip64 = self.compressed_size >= MAX_ZIP64
            || self.uncompressed_size >= MAX_ZIP64
            || local_offset >= MAX_ZIP64;

        let extra_len = if is_zip64 { SIZEOF_CD_ZIP64_EXTRA } else { 0 };
        SIZEOF_CD_BASE + self.name.as_bytes().len() + extra_len
    }

    pub fn write_cd(&self, local_offset: u64, buf: &mut Vec<u8>) {
        let name_bytes = self.name.as_bytes();
        let is_zip64 = self.compressed_size >= MAX_ZIP64
            || self.uncompressed_size >= MAX_ZIP64
            || local_offset >= MAX_ZIP64;

        let extra_len = if is_zip64 { SIZEOF_CD_ZIP64_EXTRA } else { 0 };

        buf.extend_from_slice(SIG_CENTRAL_DIRECTORY);

        let version = if is_zip64 {
            ZIP_VERSION_45
        } else {
            ZIP_VERSION_20
        };
        buf.extend_from_slice(&ZIP_VERSION_45.to_le_bytes()); // version made by
        buf.extend_from_slice(&version.to_le_bytes()); // version needed
        buf.extend_from_slice(&self.flags.to_le_bytes());
        buf.extend_from_slice(&self.compression_method.to_le_bytes());
        buf.extend_from_slice(&self.dos_mod_time.to_le_bytes());
        buf.extend_from_slice(&self.dos_mod_date.to_le_bytes());
        buf.extend_from_slice(&self.crc32.to_le_bytes());

        if is_zip64 {
            buf.extend_from_slice(&MAX_U32.to_le_bytes()); // compressed size
            buf.extend_from_slice(&MAX_U32.to_le_bytes()); // uncompressed size
        } else {
            buf.extend_from_slice(&(self.compressed_size as u32).to_le_bytes());
            buf.extend_from_slice(&(self.uncompressed_size as u32).to_le_bytes());
        }

        buf.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
        buf.extend_from_slice(&(extra_len as u16).to_le_bytes());
        buf.extend_from_slice(&0u16.to_le_bytes()); // comment len
        buf.extend_from_slice(&0u16.to_le_bytes()); // disk number start
        buf.extend_from_slice(&0u16.to_le_bytes()); // internal attributes
        buf.extend_from_slice(&0u32.to_le_bytes()); // external attributes

        if is_zip64 {
            buf.extend_from_slice(&MAX_U32.to_le_bytes());
        } else {
            buf.extend_from_slice(&(local_offset as u32).to_le_bytes());
        }

        buf.extend_from_slice(name_bytes);
        if is_zip64 {
            buf.extend_from_slice(&ZIP64_EXTRA_TAG.to_le_bytes());
            buf.extend_from_slice(&SIZEOF_CD_ZIP64_EXTRA_DATA.to_le_bytes());
            buf.extend_from_slice(&self.uncompressed_size.to_le_bytes());
            buf.extend_from_slice(&self.compressed_size.to_le_bytes());
            buf.extend_from_slice(&local_offset.to_le_bytes());
        }
    }
}

pub struct EndOfCentralDirectory {
    pub cd_size: u64,
    pub cd_offset: u64,
    pub total_entries: u64,
}

impl EndOfCentralDirectory {
    pub fn read<R: ReadAt>(r: &R) -> Result<Self, ZipError<R::Error>> {
        let size = r.size();
        let search_len = core::cmp::min(
            size,
            (MAX_COMMENT_LEN + SIZEOF_EOCD_BASE + SIZEOF_ZIP64_LOCATOR) as u64,
        ) as usize;
        let search_start = size - search_len as u64;

        let mut buf = vec![0u8; search_len];
        r.read_at(search_start, &mut buf)
            .map_err(ZipError::ReadError)?;

        let eocd_idx = (0..=(search_len.saturating_sub(SIZEOF_EOCD_BASE)))
            .rev()
            .find(|&i| {
                if buf[i..i + SIZEOF_SIGNATURE] == *SIG_EOCD {
                    let comment_len = read_u16_le(&buf, i + EOCD_OFF_COMMENT_LEN) as usize;
                    i + SIZEOF_EOCD_BASE + comment_len == search_len
                } else {
                    false
                }
            })
            .ok_or(ZipError::InvalidSignature("EOCD missing"))?;

        let eocd = &buf[eocd_idx..eocd_idx + SIZEOF_EOCD_BASE];
        let disk_entries = read_u16_le(eocd, EOCD_OFF_DISK_ENTRIES);
        let total_entries_16 = read_u16_le(eocd, EOCD_OFF_TOTAL_ENTRIES);
        let mut cd_size = read_u32_le(eocd, EOCD_OFF_CD_SIZE) as u64;
        let mut cd_offset = read_u32_le(eocd, EOCD_OFF_CD_OFFSET) as u64;
        let mut total_entries = total_entries_16 as u64;

        let need_zip64 = cd_size == MAX_ZIP64
            || cd_offset == MAX_ZIP64
            || disk_entries == MAX_U16
            || total_entries_16 == MAX_U16;

        if need_zip64 {
            if eocd_idx < SIZEOF_ZIP64_LOCATOR {
                return Err(ZipError::FormatError("Zip64 locator position invalid"));
            }
            let locator_start = eocd_idx - SIZEOF_ZIP64_LOCATOR;
            let locator = &buf[locator_start..eocd_idx];
            if locator[..SIZEOF_SIGNATURE] != *SIG_ZIP64_LOCATOR {
                return Err(ZipError::InvalidSignature("Zip64 locator missing"));
            }

            let zip64_eocd_offset = read_u64_le(locator, ZIP64_LOCATOR_OFF_EOCD_OFFSET);
            let mut z64_buf = [0u8; SIZEOF_ZIP64_EOCD];
            r.read_at(zip64_eocd_offset, &mut z64_buf)
                .map_err(ZipError::ReadError)?;

            if z64_buf[..SIZEOF_SIGNATURE] != *SIG_ZIP64_EOCD {
                return Err(ZipError::InvalidSignature("Zip64 EOCD missing"));
            }

            total_entries = read_u64_le(&z64_buf, ZIP64_EOCD_OFF_TOTAL_ENTRIES);
            cd_size = read_u64_le(&z64_buf, ZIP64_EOCD_OFF_CD_SIZE);
            cd_offset = read_u64_le(&z64_buf, ZIP64_EOCD_OFF_CD_OFFSET);
        }

        Ok(Self {
            cd_size,
            cd_offset,
            total_entries,
        })
    }

    pub fn len(total_entries: u64, cd_size: u64, cd_offset: u64) -> usize {
        let force_zip64 =
            total_entries >= MAX_U16 as u64 || cd_size >= MAX_ZIP64 || cd_offset >= MAX_ZIP64;
        if force_zip64 {
            SIZEOF_ZIP64_EOCD + SIZEOF_ZIP64_LOCATOR + SIZEOF_EOCD_BASE
        } else {
            SIZEOF_EOCD_BASE
        }
    }

    pub fn write(buf: &mut Vec<u8>, total_entries: u64, cd_size: u64, cd_offset: u64) {
        let force_zip64 =
            total_entries >= MAX_U16 as u64 || cd_size >= MAX_ZIP64 || cd_offset >= MAX_ZIP64;

        if force_zip64 {
            let zip64_eocd_offset = buf.len() as u64;
            // ZIP64 EOCD
            buf.extend_from_slice(SIG_ZIP64_EOCD);
            buf.extend_from_slice(&SIZEOF_ZIP64_EOCD_RECORD.to_le_bytes());
            buf.extend_from_slice(&ZIP_VERSION_45.to_le_bytes());
            buf.extend_from_slice(&ZIP_VERSION_45.to_le_bytes());
            buf.extend_from_slice(&0u32.to_le_bytes()); // disk number
            buf.extend_from_slice(&0u32.to_le_bytes()); // disk start
            buf.extend_from_slice(&total_entries.to_le_bytes());
            buf.extend_from_slice(&total_entries.to_le_bytes());
            buf.extend_from_slice(&cd_size.to_le_bytes());
            buf.extend_from_slice(&cd_offset.to_le_bytes());

            // ZIP64 locator
            buf.extend_from_slice(SIG_ZIP64_LOCATOR);
            buf.extend_from_slice(&0u32.to_le_bytes()); // disk with EOCD
            buf.extend_from_slice(&zip64_eocd_offset.to_le_bytes());
            buf.extend_from_slice(&1u32.to_le_bytes()); // total disks
        }

        buf.extend_from_slice(SIG_EOCD);
        buf.extend_from_slice(&0u16.to_le_bytes()); // disk number
        buf.extend_from_slice(&0u16.to_le_bytes()); // disk start

        let count_16 = if total_entries >= MAX_U16 as u64 {
            MAX_U16
        } else {
            total_entries as u16
        };
        buf.extend_from_slice(&count_16.to_le_bytes());
        buf.extend_from_slice(&count_16.to_le_bytes());

        let size_32 = if cd_size >= MAX_ZIP64 {
            MAX_U32
        } else {
            cd_size as u32
        };
        let offset_32 = if cd_offset >= MAX_ZIP64 {
            MAX_U32
        } else {
            cd_offset as u32
        };
        buf.extend_from_slice(&size_32.to_le_bytes());
        buf.extend_from_slice(&offset_32.to_le_bytes());
        buf.extend_from_slice(&0u16.to_le_bytes()); // comment length
    }
}

#[derive(Debug, Clone, Copy)]
pub enum ZipReadStrategy {
    /// Validate and resolve the data offset from the local file header.
    ///
    /// Performs additional reads but provides the most reliable behavior.
    /// Recommended for native/local random-access backends.
    Strict,

    /// Read compressed data directly from the cached data offset.
    ///
    /// Avoids local file header parsing for fewer IO operations,
    /// but assumes the cached offset is correct.
    Optimistic,

    /// Merge local-header and file-data reads into a single larger request.
    ///
    /// Optimized for high-latency or slow random-access backends
    /// such as HTTP range requests or embedded storage.
    Merged,
}
