import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";

const pluginRoot = join(process.cwd(), "examples", "hello-plugin");
const manifestPath = join(pluginRoot, "idea-plugin.json");
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));

const main = requireString(manifest, "main");
const version = requireString(manifest, "version");
const archivePath = join(pluginRoot, "build", `hello-plugin-${version}.zip`);
const archiveEntries = [
  { archivePath: "idea-plugin.json", sourcePath: manifestPath },
  { archivePath: main, sourcePath: join(pluginRoot, main) },
  { archivePath: "README.md", sourcePath: join(pluginRoot, "README.md") }
];
const DOS_TIME_MIDNIGHT = 0;
const DOS_DATE_1980_01_01 = 33;
const CRC32_TABLE = Array.from({ length: 256 }, (_, index) => {
  let value = index;

  for (let bit = 0; bit < 8; bit += 1) {
    value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
  }

  return value >>> 0;
});

await rm(join(pluginRoot, "build"), { recursive: true, force: true });
await mkdir(dirname(archivePath), { recursive: true });

const files = [];
for (const entry of archiveEntries) {
  if (entry.archivePath.startsWith("/") || entry.archivePath.includes("..")) {
    throw new Error(`Refusing unsafe archive path: ${entry.archivePath}`);
  }

  files.push({
    archivePath: entry.archivePath,
    data: await readFile(entry.sourcePath)
  });
}

await writeFile(archivePath, createZip(files));
console.log(`created ${archivePath}`);

function requireString(record, key) {
  if (typeof record[key] !== "string" || record[key].trim().length === 0) {
    throw new Error(`idea-plugin.json field "${key}" must be a non-empty string`);
  }

  return record[key];
}

function createZip(files) {
  const localFileHeaders = [];
  const centralDirectoryHeaders = [];
  let offset = 0;

  for (const file of files) {
    const filename = Buffer.from(file.archivePath, "utf8");
    const checksum = crc32(file.data);
    const localFileHeader = Buffer.alloc(30);

    localFileHeader.writeUInt32LE(0x04034b50, 0);
    localFileHeader.writeUInt16LE(20, 4);
    localFileHeader.writeUInt16LE(0x0800, 6);
    localFileHeader.writeUInt16LE(0, 8);
    localFileHeader.writeUInt16LE(DOS_TIME_MIDNIGHT, 10);
    localFileHeader.writeUInt16LE(DOS_DATE_1980_01_01, 12);
    localFileHeader.writeUInt32LE(checksum, 14);
    localFileHeader.writeUInt32LE(file.data.length, 18);
    localFileHeader.writeUInt32LE(file.data.length, 22);
    localFileHeader.writeUInt16LE(filename.length, 26);
    localFileHeader.writeUInt16LE(0, 28);

    localFileHeaders.push(localFileHeader, filename, file.data);

    const centralDirectoryHeader = Buffer.alloc(46);
    centralDirectoryHeader.writeUInt32LE(0x02014b50, 0);
    centralDirectoryHeader.writeUInt16LE(20, 4);
    centralDirectoryHeader.writeUInt16LE(20, 6);
    centralDirectoryHeader.writeUInt16LE(0x0800, 8);
    centralDirectoryHeader.writeUInt16LE(0, 10);
    centralDirectoryHeader.writeUInt16LE(DOS_TIME_MIDNIGHT, 12);
    centralDirectoryHeader.writeUInt16LE(DOS_DATE_1980_01_01, 14);
    centralDirectoryHeader.writeUInt32LE(checksum, 16);
    centralDirectoryHeader.writeUInt32LE(file.data.length, 20);
    centralDirectoryHeader.writeUInt32LE(file.data.length, 24);
    centralDirectoryHeader.writeUInt16LE(filename.length, 28);
    centralDirectoryHeader.writeUInt16LE(0, 30);
    centralDirectoryHeader.writeUInt16LE(0, 32);
    centralDirectoryHeader.writeUInt16LE(0, 34);
    centralDirectoryHeader.writeUInt16LE(0, 36);
    centralDirectoryHeader.writeUInt32LE(0, 38);
    centralDirectoryHeader.writeUInt32LE(offset, 42);

    centralDirectoryHeaders.push(centralDirectoryHeader, filename);
    offset += localFileHeader.length + filename.length + file.data.length;
  }

  const centralDirectory = Buffer.concat(centralDirectoryHeaders);
  const endOfCentralDirectory = Buffer.alloc(22);

  endOfCentralDirectory.writeUInt32LE(0x06054b50, 0);
  endOfCentralDirectory.writeUInt16LE(0, 4);
  endOfCentralDirectory.writeUInt16LE(0, 6);
  endOfCentralDirectory.writeUInt16LE(files.length, 8);
  endOfCentralDirectory.writeUInt16LE(files.length, 10);
  endOfCentralDirectory.writeUInt32LE(centralDirectory.length, 12);
  endOfCentralDirectory.writeUInt32LE(offset, 16);
  endOfCentralDirectory.writeUInt16LE(0, 20);

  return Buffer.concat([...localFileHeaders, centralDirectory, endOfCentralDirectory]);
}

function crc32(data) {
  let value = 0xffffffff;

  for (const byte of data) {
    value = (value >>> 8) ^ CRC32_TABLE[(value ^ byte) & 0xff];
  }

  return (value ^ 0xffffffff) >>> 0;
}
