// Simplified ZIP module - self-contained implementation

// Basic ZIP types

///|
pub enum Compression {
  Stored
  Deflate
  Other(Int)
} derive(Show)

///|
pub typealias String as Fpath

///|
pub typealias Int as Mode

///|
pub typealias Int as Ptime

///|
pub struct File {
  compression : Compression
  start : Int
  compressed_size : Int
  compressed_bytes : String
  decompressed_size : Int
  decompressed_crc32 : Int64 // Using Int64 directly instead of @deflate.Crc32
  version_made_by : Int
  version_needed_to_extract : Int
  gp_flags : Int
}

///|
pub enum MemberKind {
  Dir
  File(File)
}

///|
pub struct Member {
  path : Fpath
  mode : Mode
  mtime : Ptime
  kind : MemberKind
}

///|
pub struct Archive {
  members : Map[String, Member]
}

// Constants

///|
pub let dos_epoch : Ptime = 315532800 // 1980-01-01 00:00:00 UTC

///|
pub let max_file_size : Int = 2147483647 // Use max int instead of 4GB

///|
pub let max_members : Int = 65535 // ZIP32 limit

///|
pub let max_path_length : Int = 65535

// Archive operations

///|
pub fn empty() -> Archive {
  { members: Map::new() }
}

///|
pub fn is_empty(archive : Archive) -> Bool {
  archive.members.size() == 0
}

///|
pub fn member_count(archive : Archive) -> Int {
  archive.members.size()
}

///|
pub fn add(mem : Member, archive : Archive) -> Archive {
  let new_members = archive.members
  new_members[mem.path] = mem
  { members: new_members }
}

///|
pub fn find(path : Fpath, archive : Archive) -> Member? {
  archive.members.get(path)
}

///|
pub fn mem(path : Fpath, archive : Archive) -> Bool {
  archive.members.contains(path)
}

///|
pub fn remove(path : Fpath, archive : Archive) -> Archive {
  let new_members = archive.members
  new_members.remove(path)
  { members: new_members }
}

// File operations

///|
pub fn stored_of_binary_string(s : String) -> File raise {
  let len = s.length()
  if len > max_file_size {
    fail(
      "File size exceeds maximum allowed (" +
      max_file_size.to_string() +
      " bytes)",
    )
  }

  // Calculate actual CRC-32 checksum
  let crc = @crc32.bytes(s.to_bytes())
  {
    compression: Compression::Stored,
    start: 0,
    compressed_size: len,
    compressed_bytes: s,
    decompressed_size: len,
    decompressed_crc32: crc.0,
    version_made_by: 0x314, // UNIX, PKZIP 2.0
    version_needed_to_extract: 20, // PKZIP 2.0
    gp_flags: 0x800, // UTF-8 filename
  }
}

// Create stored file from bytes (more efficient for binary data)

///|
pub fn stored_of_bytes(data : Bytes) -> File raise {
  let len = data.length()
  if len > max_file_size {
    fail(
      "File size exceeds maximum allowed (" +
      max_file_size.to_string() +
      " bytes)",
    )
  }

  // Convert to string for storage (required by current File struct)
  let data_as_string = data.to_string()

  // Calculate CRC-32 checksum on the string that will actually be stored
  // This ensures consistency between storage and extraction
  let crc = @crc32.bytes(data_as_string.to_bytes())
  {
    compression: Compression::Stored,
    start: 0,
    compressed_size: len,
    compressed_bytes: data_as_string,
    decompressed_size: len,
    decompressed_crc32: crc.0,
    version_made_by: 0x314, // UNIX, PKZIP 2.0
    version_needed_to_extract: 20, // PKZIP 2.0
    gp_flags: 0x800, // UTF-8 filename
  }
}

// Create deflate-compressed file

///|
pub fn deflate_of_binary_string(
  s : String,
  level : @deflate.Level,
) -> @deflate.Result[File, String] {
  let len = s.length()
  if len > max_file_size {
    return @deflate.err(
      "File size exceeds maximum allowed (" +
      max_file_size.to_string() +
      " bytes)",
    )
  }

  // Compress using deflate
  let compressed_data = @deflate.deflate_of_bytes(s.to_bytes(), level)
  // Calculate CRC-32 of original data
  let crc = @crc32.bytes(s.to_bytes())
  @deflate.ok({
    compression: Compression::Deflate,
    start: 0,
    compressed_size: compressed_data.to_string().length(),
    compressed_bytes: compressed_data.to_string(),
    decompressed_size: len,
    decompressed_crc32: crc.0,
    version_made_by: 0x314, // UNIX, ZIP 2.0
    version_needed_to_extract: 20, // ZIP 2.0
    gp_flags: 0x800, // UTF-8 filename
  })
}

// Create deflate-compressed file from bytes (more efficient for binary data)

///|
pub fn deflate_of_bytes(
  data : Bytes,
  level : @deflate.Level,
) -> @deflate.Result[File, String] {
  let len = data.length()
  if len > max_file_size {
    return @deflate.err(
      "File size exceeds maximum allowed (" +
      max_file_size.to_string() +
      " bytes)",
    )
  }

  // Compress using deflate directly on bytes (more efficient)
  let compressed_data = @deflate.deflate_of_bytes(data, level)
  // Convert to string for storage (required by current File struct)
  let compressed_as_string = compressed_data.to_string()
  let data_as_string = data.to_string()

  // Calculate CRC-32 on the string representation that will be stored/extracted
  // This ensures consistency between storage and extraction
  let crc = @crc32.bytes(data_as_string.to_bytes())
  @deflate.ok({
    compression: Compression::Deflate,
    start: 0,
    compressed_size: compressed_as_string.length(),
    compressed_bytes: compressed_as_string,
    decompressed_size: len,
    decompressed_crc32: crc.0,
    version_made_by: 0x314, // UNIX, ZIP 2.0
    version_needed_to_extract: 20, // ZIP 2.0
    gp_flags: 0x800, // UTF-8 filename
  })
}

///|
pub fn file_compression(file : File) -> Compression {
  file.compression
}

///|
pub fn file_compressed_size(file : File) -> Int {
  file.compressed_size
}

///|
pub fn file_decompressed_size(file : File) -> Int {
  file.decompressed_size
}

///|
pub fn file_compressed_bytes(file : File) -> String {
  file.compressed_bytes
}

///|
pub fn file_can_extract(file : File) -> Bool {
  match file.compression {
    Compression::Stored => true
    Compression::Deflate => true // Would need actual deflate implementation
    _ => false
  }
}

///|
pub fn file_to_binary_string(file : File) -> @deflate.Result[String, String] {
  if not(file_can_extract(file)) {
    return @deflate.err("Unsupported compression format")
  }
  match file.compression {
    Compression::Stored => {
      let data = file.compressed_bytes.substring(
        start=file.start,
        end=file.start + file.compressed_size,
      )
      @deflate.ok(data)
    }
    Compression::Deflate =>
      // TODO: Deflate decompression needs proper implementation
      // For now, stored compression demonstrates all the architectural benefits
      @deflate.err(
        "Deflate decompression not yet implemented - use stored compression",
      )
    _ => @deflate.err("Unsupported compression format")
  }
}

// Extract file content as bytes (more efficient for binary data)

///|
pub fn file_to_bytes(file : File) -> Bytes raise {
  if not(file_can_extract(file)) {
    fail("Unsupported compression format")
  }
  match file.compression {
    Compression::Stored => {
      // Convert string to bytes first, then slice
      let compressed_bytes = file.compressed_bytes.to_bytes()
      let data_view = compressed_bytes[file.start:file.start +
        file.compressed_size]
      // Convert view back to bytes
      data_view.to_bytes()
    }
    Compression::Deflate => {
      // Use bytes-based decompression for better performance
      let compressed_bytes = file.compressed_bytes.to_bytes()
      @deflate.deflate_decompress_bytes(
        compressed_bytes,
        file.decompressed_size,
      )
    }
    _ => fail("Unsupported compression format")
  }
}

// Member operations

///|
pub fn member_make(
  path : Fpath,
  kind : MemberKind,
) -> @deflate.Result[Member, String] {
  if path.length() > max_path_length {
    return @deflate.err(
      "Path length exceeds maximum allowed (" +
      max_path_length.to_string() +
      " bytes)",
    )
  }
  let sanitized_path = path.replace(old="\\", new="/")
  let final_path = match kind {
    MemberKind::Dir =>
      if sanitized_path.is_empty() {
        "./"
      } else if sanitized_path.strip_suffix("/") is Some(_) {
        sanitized_path
      } else {
        sanitized_path + "/"
      }
    MemberKind::File(_) => sanitized_path
  }
  let default_mode = match kind {
    MemberKind::Dir => 0o755
    MemberKind::File(_) => 0o644
  }
  @deflate.ok({
    path: final_path,
    mtime: dos_epoch, // Default mtime
    mode: default_mode,
    kind,
  })
}

///|
pub fn member_path(mem : Member) -> Fpath {
  mem.path
}

///|
pub fn member_mode(mem : Member) -> Mode {
  mem.mode
}

///|
pub fn member_mtime(mem : Member) -> Ptime {
  mem.mtime
}

///|
pub fn member_kind(mem : Member) -> MemberKind {
  mem.kind
}

// Archive encoding/decoding

///|
pub fn to_binary_string(archive : Archive) -> @deflate.Result[String, String] {
  if member_count(archive) > max_members {
    return @deflate.err(
      "Archive has too many members (" +
      member_count(archive).to_string() +
      "), max is " +
      max_members.to_string(),
    )
  }
  if is_empty(archive) {
    // Empty ZIP file: just EOCD record
    let eocd = "PK\u{05}\u{06}" + String::make(18, '\u{00}')
    @deflate.ok(eocd)
  } else {
    // Build complete ZIP file
    build_zip_file(archive)
  }
}

// Convert archive to bytes (more efficient for binary data)

///|
pub fn to_bytes(archive : Archive) -> @deflate.Result[Bytes, String] {
  // Use the existing string-based implementation and convert to bytes
  // In a full implementation, this would work directly with bytes throughout
  match to_binary_string(archive) {
    @deflate.Ok(zip_string) => @deflate.ok(zip_string.to_bytes())
    @deflate.Err(error) => @deflate.err(error)
  }
}

// Build a complete ZIP file with local headers, central directory, and EOCD

///|
fn build_zip_file(archive : Archive) -> @deflate.Result[String, String] {
  let mut zip_data = ""
  let mut central_directory = ""
  let local_header_offsets : Array[Int] = []
  let member_list : Array[Member] = []

  // Collect all members
  archive.members.each(fn(_, mem) { member_list.push(mem) })

  // Write local file headers and data
  for mem in member_list {
    let local_header_offset = zip_data.length()
    local_header_offsets.push(local_header_offset)
    match write_local_file_header(mem) {
      @deflate.Ok(local_header_data) => zip_data = zip_data + local_header_data
      @deflate.Err(error) =>
        return @deflate.err("Failed to write local header: " + error)
    }
  }

  // Write central directory
  let central_directory_offset = zip_data.length()
  for i = 0; i < member_list.length(); i = i + 1 {
    let mem = member_list[i]
    let local_offset = local_header_offsets[i]
    match write_central_directory_entry(mem, local_offset) {
      @deflate.Ok(cd_entry) => central_directory = central_directory + cd_entry
      @deflate.Err(error) =>
        return @deflate.err("Failed to write central directory entry: " + error)
    }
  }

  // Write end of central directory record
  let eocd = write_eocd_record(
    member_list.length(),
    central_directory.length(),
    central_directory_offset,
  )
  let complete_zip = zip_data + central_directory + eocd
  @deflate.ok(complete_zip)
}

// Write local file header

///|
fn write_local_file_header(mem : Member) -> @deflate.Result[String, String] {
  let path = mem.path
  let path_bytes = path // Assuming UTF-8 encoding
  match mem.kind {
    MemberKind::Dir => {
      // Directory entry
      let (dos_time, dos_date) = unix_to_dos_datetime(mem.mtime)
      let header = write_u32_le(local_file_header_signature) + // Signature
        write_u16_le(20) + // Version needed
        write_u16_le(0x800) + // GP flags (UTF-8)
        write_u16_le(0) + // Compression method (stored)
        write_u16_le(dos_time) + // Last mod time
        write_u16_le(dos_date) + // Last mod date
        write_u32_le(0) + // CRC-32
        write_u32_le(0) + // Compressed size
        write_u32_le(0) + // Uncompressed size
        write_u16_le(path_bytes.length()) + // Filename length
        write_u16_le(0) // Extra field length
      @deflate.ok(header + path_bytes)
    }
    MemberKind::File(file) => {
      // File entry
      let compression_method = match file.compression {
        Compression::Stored => 0
        Compression::Deflate => 8
        Compression::Other(comp_method) => comp_method
      }
      let (dos_time, dos_date) = unix_to_dos_datetime(mem.mtime)
      let header = write_u32_le(local_file_header_signature) + // Signature
        write_u16_le(file.version_needed_to_extract) + // Version needed
        write_u16_le(file.gp_flags) + // GP flags
        write_u16_le(compression_method) + // Compression method
        write_u16_le(dos_time) + // Last mod time
        write_u16_le(dos_date) + // Last mod date
        write_u32_le(file.decompressed_crc32.to_int()) + // CRC-32
        write_u32_le(file.compressed_size) + // Compressed size
        write_u32_le(file.decompressed_size) + // Uncompressed size
        write_u16_le(path_bytes.length()) + // Filename length
        write_u16_le(0) // Extra field length
      @deflate.ok(header + path_bytes + file.compressed_bytes)
    }
  }
}

// Write central directory entry

///|
fn write_central_directory_entry(
  mem : Member,
  local_header_offset : Int,
) -> @deflate.Result[String, String] {
  let path = mem.path
  let path_bytes = path
  match mem.kind {
    MemberKind::Dir => {
      let (dos_time, dos_date) = unix_to_dos_datetime(mem.mtime)
      let entry = write_u32_le(central_directory_signature) + // Signature
        write_u16_le(0x314) + // Version made by (Unix, ZIP 2.0)
        write_u16_le(20) + // Version needed
        write_u16_le(0x800) + // GP flags (UTF-8)
        write_u16_le(0) + // Compression method
        write_u16_le(dos_time) + // Last mod time
        write_u16_le(dos_date) + // Last mod date
        write_u32_le(0) + // CRC-32
        write_u32_le(0) + // Compressed size
        write_u32_le(0) + // Uncompressed size
        write_u16_le(path_bytes.length()) + // Filename length
        write_u16_le(0) + // Extra field length
        write_u16_le(0) + // File comment length
        write_u16_le(0) + // Disk number start
        write_u16_le(0) + // Internal file attrs
        write_u32_le((mem.mode << 16) | 0x10) + // External file attrs (directory)
        write_u32_le(local_header_offset) // Local header offset
      @deflate.ok(entry + path_bytes)
    }
    MemberKind::File(file) => {
      let compression_method = match file.compression {
        Compression::Stored => 0
        Compression::Deflate => 8
        Compression::Other(comp_method) => comp_method
      }
      let (dos_time, dos_date) = unix_to_dos_datetime(mem.mtime)
      let entry = write_u32_le(central_directory_signature) + // Signature
        write_u16_le(file.version_made_by) + // Version made by
        write_u16_le(file.version_needed_to_extract) + // Version needed
        write_u16_le(file.gp_flags) + // GP flags
        write_u16_le(compression_method) + // Compression method
        write_u16_le(dos_time) + // Last mod time
        write_u16_le(dos_date) + // Last mod date
        write_u32_le(file.decompressed_crc32.to_int()) + // CRC-32
        write_u32_le(file.compressed_size) + // Compressed size
        write_u32_le(file.decompressed_size) + // Uncompressed size
        write_u16_le(path_bytes.length()) + // Filename length
        write_u16_le(0) + // Extra field length
        write_u16_le(0) + // File comment length
        write_u16_le(0) + // Disk number start
        write_u16_le(0) + // Internal file attrs
        write_u32_le(mem.mode << 16) + // External file attrs
        write_u32_le(local_header_offset) // Local header offset
      @deflate.ok(entry + path_bytes)
    }
  }
}

// Write end of central directory record

///|
fn write_eocd_record(
  entry_count : Int,
  cd_size : Int,
  cd_offset : Int,
) -> String {
  write_u32_le(end_of_central_directory_signature) + // Signature
  write_u16_le(0) + // Disk number
  write_u16_le(0) + // Disk with central directory
  write_u16_le(entry_count) + // Entries on this disk
  write_u16_le(entry_count) + // Total entries
  write_u32_le(cd_size) + // Central directory size
  write_u32_le(cd_offset) + // Central directory offset
  write_u16_le(0) // Comment length
}

// Binary parsing utilities

///|
fn read_u16_le(data : String, offset : Int) -> Int {
  if offset + 1 >= data.length() {
    return 0
  }
  let b0 = data[offset]
  let b1 = data[offset + 1]
  b0 + (b1 << 8)
}

///|
fn read_u32_le(data : String, offset : Int) -> Int {
  if offset + 3 >= data.length() {
    return 0
  }
  let b0 = data[offset]
  let b1 = data[offset + 1]
  let b2 = data[offset + 2]
  let b3 = data[offset + 3]
  b0 + (b1 << 8) + (b2 << 16) + (b3 << 24)
}

///|
fn write_u16_le(value : Int) -> String {
  let b0 = value & 0xff
  let b1 = (value >> 8) & 0xff
  b0.unsafe_to_char().to_string() + b1.unsafe_to_char().to_string()
}

///|
fn write_u32_le(value : Int) -> String {
  let b0 = value & 0xff
  let b1 = (value >> 8) & 0xff
  let b2 = (value >> 16) & 0xff
  let b3 = (value >> 24) & 0xff
  b0.unsafe_to_char().to_string() +
  b1.unsafe_to_char().to_string() +
  b2.unsafe_to_char().to_string() +
  b3.unsafe_to_char().to_string()
}

// ZIP file format constants

///|
let local_file_header_signature : Int = 0x04034b50

///|
let central_directory_signature : Int = 0x02014b50

///|
let end_of_central_directory_signature : Int = 0x06054b50

// Check if string has ZIP magic bytes

///|
pub fn string_has_magic(s : String) -> Bool {
  if s.length() < 4 {
    return false
  }

  // Check for local file header signature (PK\u0003\u0004)
  if s[0] == 0x50 && s[1] == 0x4b && s[2] == 0x03 && s[3] == 0x04 {
    return true
  }

  // Check for end of central directory signature (PK\u0005\u0006) - empty archive
  if s[0] == 0x50 && s[1] == 0x4b && s[2] == 0x05 && s[3] == 0x06 {
    return true
  }
  false
}

// Find end of central directory record

///|
fn find_eocd(data : String) -> Int? {
  let len = data.length()
  if len < 22 {
    return None
  }

  // Search backwards from end of file
  for i = len - 22; i >= 0; i = i - 1 {
    if i + 3 < len {
      let sig = read_u32_le(data, i)
      if sig == end_of_central_directory_signature {
        return Some(i)
      }
    }
  }
  None
}

// Parse ZIP file

///|
pub fn of_binary_string(data : String) -> @deflate.Result[Archive, String] {
  if data.length() < 22 {
    return @deflate.err("File too small to be a valid ZIP archive")
  }

  // Check for ZIP magic bytes
  if not(string_has_magic(data)) {
    return @deflate.err("Not a valid ZIP archive - missing magic bytes")
  }

  // Find end of central directory record
  match find_eocd(data) {
    None => @deflate.err("Could not find end of central directory record")
    Some(eocd_offset) => {
      // Parse EOCD record
      let cd_entries = read_u16_le(data, eocd_offset + 10) // Total entries in central directory
      let _cd_size = read_u32_le(data, eocd_offset + 12) // Size of central directory (unused for now)
      let cd_offset = read_u32_le(data, eocd_offset + 16) // Offset of central directory
      if cd_entries == 0 {
        // Empty archive
        @deflate.ok(empty())
      } else {
        // Parse central directory entries
        parse_central_directory(data, cd_offset, cd_entries)
      }
    }
  }
}

// Parse archive from bytes (more efficient for binary data)

///|
pub fn of_bytes(data : Bytes) -> @deflate.Result[Archive, String] {
  // Use the existing string-based implementation by converting bytes to string
  // In a full implementation, this would work directly with bytes throughout
  let string_data = data.to_string()
  of_binary_string(string_data)
}

// Parse central directory entries

///|
fn parse_central_directory(
  data : String,
  cd_offset : Int,
  entry_count : Int,
) -> @deflate.Result[Archive, String] {
  let mut archive = empty()
  let mut offset = cd_offset
  for i = 0; i < entry_count; i = i + 1 {
    match parse_central_directory_entry(data, offset) {
      @deflate.Ok((mem, next_offset)) => {
        archive = add(mem, archive)
        offset = next_offset
      }
      @deflate.Err(error) =>
        return @deflate.err(
          "Failed to parse central directory entry " +
          i.to_string() +
          ": " +
          error,
        )
    }
  }
  @deflate.ok(archive)
}

// Parse a single central directory entry

///|
fn parse_central_directory_entry(
  data : String,
  offset : Int,
) -> @deflate.Result[(Member, Int), String] {
  if offset + 46 > data.length() {
    return @deflate.err("Central directory entry extends beyond file")
  }
  let signature = read_u32_le(data, offset)
  if signature != central_directory_signature {
    return @deflate.err("Invalid central directory entry signature")
  }
  let version_made_by = read_u16_le(data, offset + 4)
  let version_needed = read_u16_le(data, offset + 6)
  let gp_flags = read_u16_le(data, offset + 8)
  let compression_method = read_u16_le(data, offset + 10)
  let _last_mod_time = read_u16_le(data, offset + 12) // TODO: Convert DOS time to Unix time
  let _last_mod_date = read_u16_le(data, offset + 14) // TODO: Convert DOS date to Unix time
  let crc32 = read_u32_le(data, offset + 16)
  let compressed_size = read_u32_le(data, offset + 20)
  let uncompressed_size = read_u32_le(data, offset + 24)
  let filename_length = read_u16_le(data, offset + 28)
  let extra_field_length = read_u16_le(data, offset + 30)
  let file_comment_length = read_u16_le(data, offset + 32)
  let external_file_attrs = read_u32_le(data, offset + 38)
  let local_header_offset = read_u32_le(data, offset + 42)

  // Read filename
  let filename_start = offset + 46
  if filename_start + filename_length > data.length() {
    return @deflate.err("Filename extends beyond file")
  }
  let filename = data.substring(
    start=filename_start,
    end=filename_start + filename_length,
  )

  // Determine compression type
  let compression = match compression_method {
    0 => Compression::Stored
    8 => Compression::Deflate
    _ => Compression::Other(compression_method)
  }

  // Read file data from local header
  match read_file_data(data, local_header_offset, compressed_size) {
    @deflate.Ok(file_data) => {
      // Create file object
      let file = {
        compression,
        start: 0,
        compressed_size,
        compressed_bytes: file_data,
        decompressed_size: uncompressed_size,
        decompressed_crc32: crc32.to_int64(),
        version_made_by,
        version_needed_to_extract: version_needed,
        gp_flags,
      }

      // Determine if it's a directory
      let is_directory = filename.strip_suffix("/") is Some(_) ||
        (external_file_attrs & 0x10) != 0
      let kind = if is_directory {
        MemberKind::Dir
      } else {
        MemberKind::File(file)
      }

      // Create member
      let mem = {
        path: filename,
        mode: if is_directory {
          0o755
        } else {
          0o644
        },
        mtime: dos_datetime_to_unix(_last_mod_time, _last_mod_date),
        kind,
      }
      let next_offset = offset +
        46 +
        filename_length +
        extra_field_length +
        file_comment_length
      @deflate.ok((mem, next_offset))
    }
    @deflate.Err(error) => @deflate.err("Failed to read file data: " + error)
  }
}

// Read file data from local header

///|
fn read_file_data(
  data : String,
  local_header_offset : Int,
  compressed_size : Int,
) -> @deflate.Result[String, String] {
  if local_header_offset + 30 > data.length() {
    return @deflate.err("Local header extends beyond file")
  }
  let signature = read_u32_le(data, local_header_offset)
  if signature != local_file_header_signature {
    return @deflate.err("Invalid local file header signature")
  }
  let filename_length = read_u16_le(data, local_header_offset + 26)
  let extra_field_length = read_u16_le(data, local_header_offset + 28)
  let file_data_offset = local_header_offset +
    30 +
    filename_length +
    extra_field_length
  if file_data_offset + compressed_size > data.length() {
    return @deflate.err("File data extends beyond archive")
  }
  let file_data = data.substring(
    start=file_data_offset,
    end=file_data_offset + compressed_size,
  )
  @deflate.ok(file_data)
}