/**
 * Minimal ZIP builder — creates a STORE (no-compression) ZIP archive.
 * Used for the uniform-format-export queue handler where two small text files
 * must be bundled together. No external dependencies required.
 *
 * Implements ZIP local file headers + central directory + end-of-central-directory
 * per PKZIP Application Note (https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT).
 */

interface ZipEntry {
  filename: Uint8Array // UTF-8 encoded filename bytes
  data: Uint8Array
  crc32: number
}

/** CRC-32 table (standard ZIP polynomial 0xEDB88320) */
const CRC32_TABLE = (() => {
  const table = new Uint32Array(256)
  for (let i = 0; i < 256; i++) {
    let c = i
    for (let k = 0; k < 8; k++) {
      c = c & 1 ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)
    }
    table[i] = c
  }
  return table
})()

function crc32(data: Uint8Array): number {
  let crc = 0xFFFFFFFF
  for (let i = 0; i < data.length; i++) {
    crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ data[i]!) & 0xFF]!
  }
  return (crc ^ 0xFFFFFFFF) >>> 0
}

function writeUint16LE(buf: DataView, offset: number, val: number): void {
  buf.setUint16(offset, val, true)
}

function writeUint32LE(buf: DataView, offset: number, val: number): void {
  buf.setUint32(offset, val, true)
}

/**
 * Build a ZIP archive containing the provided entries.
 * Compression method: STORE (0) — no compression.
 */
export function buildZip(files: Array<{ name: string; data: Uint8Array }>): Uint8Array {
  const entries: ZipEntry[] = files.map(f => ({
    filename: new TextEncoder().encode(f.name),
    data: f.data,
    crc32: crc32(f.data),
  }))

  // Fixed DOS date/time: 2000-01-01 00:00:00 (deterministic builds)
  const DOS_DATE = (2000 - 1980) << 9 | 1 << 5 | 1  // 0x2821
  const DOS_TIME = 0

  const localHeaderSize = (e: ZipEntry) => 30 + e.filename.length
  const centralDirEntrySize = (e: ZipEntry) => 46 + e.filename.length

  // Calculate total size
  const localSectionSize = entries.reduce((s, e) => s + localHeaderSize(e) + e.data.length, 0)
  const centralDirSize = entries.reduce((s, e) => s + centralDirEntrySize(e), 0)
  const totalSize = localSectionSize + centralDirSize + 22 // EOCD = 22 bytes

  const buf = new ArrayBuffer(totalSize)
  const view = new DataView(buf)
  const bytes = new Uint8Array(buf)
  let pos = 0

  const localOffsets: number[] = []

  // ── Local file entries ────────────────────────────────────────────────────
  for (const entry of entries) {
    localOffsets.push(pos)

    // Local file header signature
    writeUint32LE(view, pos, 0x04034B50); pos += 4
    // Version needed to extract (2.0)
    writeUint16LE(view, pos, 20); pos += 2
    // General purpose bit flag
    writeUint16LE(view, pos, 0); pos += 2
    // Compression method: STORE
    writeUint16LE(view, pos, 0); pos += 2
    // Last mod file time
    writeUint16LE(view, pos, DOS_TIME); pos += 2
    // Last mod file date
    writeUint16LE(view, pos, DOS_DATE); pos += 2
    // CRC-32
    writeUint32LE(view, pos, entry.crc32); pos += 4
    // Compressed size
    writeUint32LE(view, pos, entry.data.length); pos += 4
    // Uncompressed size
    writeUint32LE(view, pos, entry.data.length); pos += 4
    // File name length
    writeUint16LE(view, pos, entry.filename.length); pos += 2
    // Extra field length
    writeUint16LE(view, pos, 0); pos += 2
    // File name
    bytes.set(entry.filename, pos); pos += entry.filename.length
    // File data
    bytes.set(entry.data, pos); pos += entry.data.length
  }

  const centralDirStart = pos

  // ── Central directory ─────────────────────────────────────────────────────
  for (let i = 0; i < entries.length; i++) {
    const entry = entries[i]!
    const localOffset = localOffsets[i]!

    // Central directory file header signature
    writeUint32LE(view, pos, 0x02014B50); pos += 4
    // Version made by (2.0)
    writeUint16LE(view, pos, 20); pos += 2
    // Version needed to extract
    writeUint16LE(view, pos, 20); pos += 2
    // General purpose bit flag
    writeUint16LE(view, pos, 0); pos += 2
    // Compression method: STORE
    writeUint16LE(view, pos, 0); pos += 2
    // Last mod file time
    writeUint16LE(view, pos, DOS_TIME); pos += 2
    // Last mod file date
    writeUint16LE(view, pos, DOS_DATE); pos += 2
    // CRC-32
    writeUint32LE(view, pos, entry.crc32); pos += 4
    // Compressed size
    writeUint32LE(view, pos, entry.data.length); pos += 4
    // Uncompressed size
    writeUint32LE(view, pos, entry.data.length); pos += 4
    // File name length
    writeUint16LE(view, pos, entry.filename.length); pos += 2
    // Extra field length
    writeUint16LE(view, pos, 0); pos += 2
    // File comment length
    writeUint16LE(view, pos, 0); pos += 2
    // Disk number start
    writeUint16LE(view, pos, 0); pos += 2
    // Internal file attributes
    writeUint16LE(view, pos, 0); pos += 2
    // External file attributes
    writeUint32LE(view, pos, 0); pos += 4
    // Relative offset of local header
    writeUint32LE(view, pos, localOffset); pos += 4
    // File name
    bytes.set(entry.filename, pos); pos += entry.filename.length
  }

  // ── End of central directory record ──────────────────────────────────────
  // Signature
  writeUint32LE(view, pos, 0x06054B50); pos += 4
  // Disk number
  writeUint16LE(view, pos, 0); pos += 2
  // Start disk
  writeUint16LE(view, pos, 0); pos += 2
  // Entries on this disk
  writeUint16LE(view, pos, entries.length); pos += 2
  // Total entries
  writeUint16LE(view, pos, entries.length); pos += 2
  // Central directory size
  writeUint32LE(view, pos, centralDirSize); pos += 4
  // Offset of central directory
  writeUint32LE(view, pos, centralDirStart); pos += 4
  // Comment length
  writeUint16LE(view, pos, 0); pos += 2

  return bytes
}
