import zlib from 'node:zlib';

// Minimal reader for the `.intunewin` package produced by the Microsoft Win32
// Content Prep Tool. The package is a ZIP containing:
//   IntuneWinPackage/Metadata/Detection.xml      (encryption info + sizes)
//   IntuneWinPackage/Contents/IntunePackage.intunewin  (AES-encrypted payload)
//
// We avoid a third-party zip dependency by parsing the central directory and
// inflating entries with the built-in zlib. Only stored (0) and deflate (8)
// compression methods are supported, which is what the tool emits.

const EOCD_SIG = 0x06054b50;
const CEN_SIG = 0x02014b50;
const LOC_SIG = 0x04034b50;

function findEndOfCentralDirectory(buffer) {
  // EOCD is at the end; scan backwards past any (absent) comment.
  for (let i = buffer.length - 22; i >= 0; i--) {
    if (buffer.readUInt32LE(i) === EOCD_SIG) return i;
  }
  throw new Error('Not a valid .intunewin (ZIP end-of-central-directory not found)');
}

function readCentralDirectory(buffer) {
  const eocd = findEndOfCentralDirectory(buffer);
  const count = buffer.readUInt16LE(eocd + 10);
  let offset = buffer.readUInt32LE(eocd + 16);
  const entries = [];
  for (let i = 0; i < count; i++) {
    if (buffer.readUInt32LE(offset) !== CEN_SIG) break;
    const method = buffer.readUInt16LE(offset + 10);
    const compressedSize = buffer.readUInt32LE(offset + 20);
    const nameLen = buffer.readUInt16LE(offset + 28);
    const extraLen = buffer.readUInt16LE(offset + 30);
    const commentLen = buffer.readUInt16LE(offset + 32);
    const localHeaderOffset = buffer.readUInt32LE(offset + 42);
    const name = buffer.toString('utf8', offset + 46, offset + 46 + nameLen);
    entries.push({ name, method, compressedSize, localHeaderOffset });
    offset += 46 + nameLen + extraLen + commentLen;
  }
  return entries;
}

function readEntryData(buffer, entry) {
  const o = entry.localHeaderOffset;
  if (buffer.readUInt32LE(o) !== LOC_SIG) throw new Error(`Bad local header for ${entry.name}`);
  const nameLen = buffer.readUInt16LE(o + 26);
  const extraLen = buffer.readUInt16LE(o + 28);
  const dataStart = o + 30 + nameLen + extraLen;
  const raw = buffer.subarray(dataStart, dataStart + entry.compressedSize);
  if (entry.method === 0) return Buffer.from(raw);
  if (entry.method === 8) return zlib.inflateRawSync(raw);
  throw new Error(`Unsupported ZIP compression method ${entry.method} for ${entry.name}`);
}

function findEntry(entries, suffix) {
  // Match by path suffix so we tolerate the IntuneWinPackage/ prefix casing.
  const lower = suffix.toLowerCase();
  return entries.find((e) => e.name.toLowerCase().replace(/\\/g, '/').endsWith(lower));
}

function xmlValue(xml, tag) {
  const match = xml.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, 'i'));
  return match ? match[1].trim() : '';
}

// Graph's commit step expects camelCase fileEncryptionInfo; map directly from
// the Detection.xml EncryptionInfo element.
function parseEncryptionInfo(xml) {
  const block = xml.match(/<EncryptionInfo>([\s\S]*?)<\/EncryptionInfo>/i);
  if (!block) throw new Error('Detection.xml is missing EncryptionInfo');
  const info = block[1];
  return {
    encryptionKey: xmlValue(info, 'EncryptionKey'),
    macKey: xmlValue(info, 'MacKey'),
    initializationVector: xmlValue(info, 'InitializationVector'),
    mac: xmlValue(info, 'Mac'),
    profileIdentifier: xmlValue(info, 'ProfileIdentifier') || 'ProfileVersion1',
    fileDigest: xmlValue(info, 'FileDigest'),
    fileDigestAlgorithm: xmlValue(info, 'FileDigestAlgorithm') || 'SHA256'
  };
}

export function parseIntunewin(buffer) {
  if (!Buffer.isBuffer(buffer)) buffer = Buffer.from(buffer);
  const entries = readCentralDirectory(buffer);
  const detectionEntry = findEntry(entries, 'metadata/detection.xml');
  if (!detectionEntry) throw new Error('Detection.xml not found in .intunewin package');
  const contentEntry = findEntry(entries, 'contents/intunepackage.intunewin');
  if (!contentEntry) throw new Error('Encrypted content (IntunePackage.intunewin) not found in package');

  const xml = readEntryData(buffer, detectionEntry).toString('utf8');
  const encryptedContent = readEntryData(buffer, contentEntry);
  const unencryptedContentSize = Number(xmlValue(xml, 'UnencryptedContentSize')) || 0;

  return {
    setupFile: xmlValue(xml, 'SetupFile') || xmlValue(xml, 'FileName') || '',
    name: xmlValue(xml, 'Name') || '',
    unencryptedContentSize,
    encryptedContent,
    encryptedContentSize: encryptedContent.length,
    fileEncryptionInfo: parseEncryptionInfo(xml)
  };
}
