// zip-writer.mjs — Minimal stored-method (no compression) ZIP writer.
// Zero dependencies. Deterministic output: fixed DOS timestamp, sorted entry order.
//
// Writes a valid ZIP that zip-extract.mjs can parse. Uses method=0 (STORE),
// CRC-32 computed, no encryption, no ZIP64. Suitable for tiny test fixtures.

import { createHash } from 'node:crypto';

// CRC-32 table (IEEE 802.3 polynomial).
const CRC_TABLE = (() => {
  const t = new Uint32Array(256);
  for (let n = 0; n < 256; n++) {
    let c = n;
    for (let k = 0; k < 8; k++) {
      c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
    }
    t[n] = c >>> 0;
  }
  return t;
})();

function crc32(buf) {
  let c = 0xFFFFFFFF;
  for (let i = 0; i < buf.length; i++) {
    c = (CRC_TABLE[(c ^ buf[i]) & 0xFF] ^ (c >>> 8)) >>> 0;
  }
  return (c ^ 0xFFFFFFFF) >>> 0;
}

const DOS_DATE = ((1980 - 1980) << 9) | (1 << 5) | 1;   // 1980-01-01
const DOS_TIME = 0;                                      // 00:00:00

/**
 * Build a ZIP buffer from a list of entries.
 *
 * @param {Array<{name: string, data: Buffer | string}>} entries
 * @returns {Buffer}
 */
export function createZip(entries) {
  // Normalize + sort for determinism
  const normalized = entries
    .map(e => ({
      name: e.name,
      data: Buffer.isBuffer(e.data) ? e.data : Buffer.from(e.data, 'utf8'),
    }))
    .sort((a, b) => a.name.localeCompare(b.name));

  const chunks = [];
  const centralHeaders = [];
  let offset = 0;

  for (const e of normalized) {
    const nameBuf = Buffer.from(e.name, 'utf8');
    const c = crc32(e.data);
    const sz = e.data.length;

    // Local File Header (30 + nameLen)
    const lfh = Buffer.alloc(30);
    lfh.writeUInt32LE(0x04034b50, 0);      // signature
    lfh.writeUInt16LE(20, 4);              // version needed
    lfh.writeUInt16LE(0, 6);               // flags
    lfh.writeUInt16LE(0, 8);               // method = STORE
    lfh.writeUInt16LE(DOS_TIME, 10);
    lfh.writeUInt16LE(DOS_DATE, 12);
    lfh.writeUInt32LE(c, 14);              // crc32
    lfh.writeUInt32LE(sz, 18);             // compressed size
    lfh.writeUInt32LE(sz, 22);             // uncompressed size
    lfh.writeUInt16LE(nameBuf.length, 26);
    lfh.writeUInt16LE(0, 28);              // extra field length
    chunks.push(lfh);
    chunks.push(nameBuf);
    chunks.push(e.data);

    const localOffset = offset;
    offset += 30 + nameBuf.length + sz;

    // Central Directory Header (46 + nameLen)
    const cdh = Buffer.alloc(46);
    cdh.writeUInt32LE(0x02014b50, 0);      // signature
    cdh.writeUInt16LE(20, 4);              // version made by
    cdh.writeUInt16LE(20, 6);              // version needed
    cdh.writeUInt16LE(0, 8);               // flags
    cdh.writeUInt16LE(0, 10);              // method
    cdh.writeUInt16LE(DOS_TIME, 12);
    cdh.writeUInt16LE(DOS_DATE, 14);
    cdh.writeUInt32LE(c, 16);
    cdh.writeUInt32LE(sz, 20);
    cdh.writeUInt32LE(sz, 24);
    cdh.writeUInt16LE(nameBuf.length, 28);
    cdh.writeUInt16LE(0, 30);              // extra
    cdh.writeUInt16LE(0, 32);              // comment
    cdh.writeUInt16LE(0, 34);              // disk
    cdh.writeUInt16LE(0, 36);              // internal attrs
    cdh.writeUInt32LE(0, 38);              // external attrs
    cdh.writeUInt32LE(localOffset, 42);
    centralHeaders.push({ cdh, nameBuf });
  }

  const centralStart = offset;
  for (const { cdh, nameBuf } of centralHeaders) {
    chunks.push(cdh);
    chunks.push(nameBuf);
    offset += cdh.length + nameBuf.length;
  }
  const centralSize = offset - centralStart;

  // End of Central Directory
  const eocd = Buffer.alloc(22);
  eocd.writeUInt32LE(0x06054b50, 0);
  eocd.writeUInt16LE(0, 4);
  eocd.writeUInt16LE(0, 6);
  eocd.writeUInt16LE(normalized.length, 8);
  eocd.writeUInt16LE(normalized.length, 10);
  eocd.writeUInt32LE(centralSize, 12);
  eocd.writeUInt32LE(centralStart, 16);
  eocd.writeUInt16LE(0, 20);             // comment length
  chunks.push(eocd);

  return Buffer.concat(chunks);
}

/**
 * Compute SHA-256 of a buffer (hex).
 */
export function sha256Hex(buf) {
  return createHash('sha256').update(buf).digest('hex');
}
