/**
 * zip.js — Minimal pure-Node.js ZIP builder / reader.
 * No external dependencies — uses only Node's built-in zlib.
 */
import zlib from 'zlib';

// ── CRC-32 ────────────────────────────────────────────────────────────────
const _crcTable = (() => {
  const t = new Uint32Array(256);
  for (let i = 0; i < 256; i++) {
    let c = i;
    for (let j = 0; j < 8; j++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
    t[i] = c;
  }
  return t;
})();

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

// ── DOS date/time ─────────────────────────────────────────────────────────
function dosDateTime(d = new Date()) {
  const time = ((d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1)) & 0xFFFF;
  const date = (((d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate()) & 0xFFFF;
  return { time, date };
}

/**
 * createZip(entries) → Buffer
 * entries: [{ name: string, data: Buffer|string }]
 */
export function createZip(entries) {
  const chunks = [];
  const centralHeaders = [];
  let offset = 0;
  const { time: modTime, date: modDate } = dosDateTime();

  for (let { name, data } of entries) {
    if (typeof data === 'string') data = Buffer.from(data, 'utf8');
    const nameBuf = Buffer.from(name, 'utf8');
    const crc = crc32(data);
    const uncompSize = data.length;

    // Try deflate; use stored if compressed is larger
    const deflated = zlib.deflateRawSync(data);
    const useDeflate = deflated.length < uncompSize;
    const compData = useDeflate ? deflated : data;
    const method = useDeflate ? 8 : 0;

    // Local file header
    const lh = Buffer.alloc(30 + nameBuf.length);
    lh.writeUInt32LE(0x04034b50, 0);
    lh.writeUInt16LE(20, 4);
    lh.writeUInt16LE(0, 6);
    lh.writeUInt16LE(method, 8);
    lh.writeUInt16LE(modTime, 10);
    lh.writeUInt16LE(modDate, 12);
    lh.writeUInt32LE(crc, 14);
    lh.writeUInt32LE(compData.length, 18);
    lh.writeUInt32LE(uncompSize, 22);
    lh.writeUInt16LE(nameBuf.length, 26);
    lh.writeUInt16LE(0, 28);
    nameBuf.copy(lh, 30);

    chunks.push(lh, compData);
    centralHeaders.push({ name: nameBuf, crc, method, modTime, modDate, compSize: compData.length, uncompSize, offset });
    offset += lh.length + compData.length;
  }

  // Central directory
  const cdStart = offset;
  for (const h of centralHeaders) {
    const cd = Buffer.alloc(46 + h.name.length);
    cd.writeUInt32LE(0x02014b50, 0);
    cd.writeUInt16LE(20, 4);
    cd.writeUInt16LE(20, 6);
    cd.writeUInt16LE(0, 8);
    cd.writeUInt16LE(h.method, 10);
    cd.writeUInt16LE(h.modTime, 12);
    cd.writeUInt16LE(h.modDate, 14);
    cd.writeUInt32LE(h.crc, 16);
    cd.writeUInt32LE(h.compSize, 20);
    cd.writeUInt32LE(h.uncompSize, 24);
    cd.writeUInt16LE(h.name.length, 28);
    cd.writeUInt16LE(0, 30);
    cd.writeUInt16LE(0, 32);
    cd.writeUInt16LE(0, 34);
    cd.writeUInt16LE(0, 36);
    cd.writeUInt32LE(0, 38);
    cd.writeUInt32LE(h.offset, 42);
    h.name.copy(cd, 46);
    chunks.push(cd);
    offset += cd.length;
  }

  const cdSize = offset - cdStart;

  // End of central directory
  const eocd = Buffer.alloc(22);
  eocd.writeUInt32LE(0x06054b50, 0);
  eocd.writeUInt16LE(0, 4);
  eocd.writeUInt16LE(0, 6);
  eocd.writeUInt16LE(centralHeaders.length, 8);
  eocd.writeUInt16LE(centralHeaders.length, 10);
  eocd.writeUInt32LE(cdSize, 12);
  eocd.writeUInt32LE(cdStart, 16);
  eocd.writeUInt16LE(0, 20);
  chunks.push(eocd);

  return Buffer.concat(chunks);
}

/**
 * readZip(buffer) → [{ name: string, data: Buffer }]
 */
export function readZip(buffer) {
  // Scan backwards for EOCD signature
  let eocdOffset = -1;
  for (let i = buffer.length - 22; i >= Math.max(0, buffer.length - 65558); i--) {
    if (buffer.readUInt32LE(i) === 0x06054b50) { eocdOffset = i; break; }
  }
  if (eocdOffset === -1) throw new Error('ZIP invalide : signature EOCD introuvable');

  const entryCount = buffer.readUInt16LE(eocdOffset + 8);
  const cdOffset   = buffer.readUInt32LE(eocdOffset + 16);

  const entries = [];
  let pos = cdOffset;

  for (let i = 0; i < entryCount; i++) {
    if (buffer.readUInt32LE(pos) !== 0x02014b50) throw new Error('ZIP invalide : signature Central Directory incorrecte');
    const method      = buffer.readUInt16LE(pos + 10);
    const compSize    = buffer.readUInt32LE(pos + 20);
    const uncompSize  = buffer.readUInt32LE(pos + 24);
    const nameLen     = buffer.readUInt16LE(pos + 28);
    const extraLen    = buffer.readUInt16LE(pos + 30);
    const commentLen  = buffer.readUInt16LE(pos + 32);
    const localOffset = buffer.readUInt32LE(pos + 42);
    const name        = buffer.toString('utf8', pos + 46, pos + 46 + nameLen);

    // Read local file header to get actual extra field length
    const localNameLen  = buffer.readUInt16LE(localOffset + 26);
    const localExtraLen = buffer.readUInt16LE(localOffset + 28);
    const dataStart     = localOffset + 30 + localNameLen + localExtraLen;

    const compData = buffer.subarray(dataStart, dataStart + compSize);
    const data = method === 0 ? compData : zlib.inflateRawSync(compData);

    entries.push({ name, data: Buffer.from(data) });
    pos += 46 + nameLen + extraLen + commentLen;
  }

  return entries;
}
