#!/usr/bin/env node
// build-jetbrains-fixtures.mjs — Deterministic fixture-jar builder.
//
// Walks `tests/fixtures/ide-extensions/root-jetbrains/` and for every plugin
// directory containing a `source/` subtree, produces `lib/<pluginId>.jar` by
// packing the source files into a real ZIP with JetBrains-plugin layout:
//
//   source/plugin.xml      → META-INF/plugin.xml     (inside jar)
//   source/MANIFEST.MF     → META-INF/MANIFEST.MF    (inside jar)
//   source/native/foo.so   → native/foo.so           (inside jar)
//   source/<other path>    → <other path>            (inside jar)
//
// Invariants:
//   - Deterministic: stored-method, fixed DOS timestamp, sorted entries.
//     Same input → byte-identical output across Node versions / platforms.
//   - Idempotent + race-safe: each jar write is atomic (temp-then-rename).
//     If the target file already exists with the expected SHA-256 (computed
//     from the intended bytes), the write is skipped. Concurrent callers are
//     safe because rename is atomic on the same filesystem and every writer
//     produces identical bytes.
//   - Zero dependencies. Uses `createZip` from `../helpers/zip-writer.mjs`.
//
// Public API:
//   export function buildJetBrainsFixtures({ fixtureRoot? }) → { fixtureRoot, builtJars }
//   export function writeJar(finalPath, entries)            → { path, sha256, skipped }
//
// CLI: `node tests/helpers/build-jetbrains-fixtures.mjs` — walks the default
// fixture root, builds all jars, prints a one-line summary per jar.
//
// Called by:
//   - tests/scanners/ide-extension-scanner.test.mjs (Step 14 integration)
//   - any future test that needs the synthetic JetBrains plugin tree
//
// See: plan step 13 (`ultraplan-2026-04-17-jetbrains-ide-scan.md`).

import { readdir, mkdir, rename, writeFile, readFile, stat } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { dirname, join, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createHash } from 'node:crypto';
import { createZip } from './zip-writer.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));
const DEFAULT_FIXTURE_ROOT = join(
  __dirname,
  '..',
  'fixtures',
  'ide-extensions',
  'root-jetbrains',
);

function sha256(buf) {
  return createHash('sha256').update(buf).digest('hex');
}

/**
 * Recursively walk a directory, returning relative paths of regular files.
 * Relative paths use forward slashes (ZIP spec).
 */
async function walkFiles(root, rel = '') {
  const out = [];
  const full = rel ? join(root, rel) : root;
  const entries = await readdir(full, { withFileTypes: true });
  for (const ent of entries) {
    const sub = rel ? `${rel}/${ent.name}` : ent.name;
    if (ent.isDirectory()) {
      out.push(...(await walkFiles(root, sub)));
    } else if (ent.isFile()) {
      out.push(sub);
    }
  }
  return out;
}

/**
 * Map a `source/`-relative path to the in-jar path.
 *
 *   source/plugin.xml        → META-INF/plugin.xml
 *   source/MANIFEST.MF       → META-INF/MANIFEST.MF
 *   source/native/foo.so     → native/foo.so
 *   source/<other>           → <other>
 */
function sourceToJarPath(srcRelPath) {
  const p = srcRelPath.replace(/\\/g, '/');
  if (p === 'plugin.xml') return 'META-INF/plugin.xml';
  if (p === 'MANIFEST.MF') return 'META-INF/MANIFEST.MF';
  return p;
}

/**
 * Atomically write a file by staging into `<path>.tmp.<pid>` and renaming.
 * Rename is atomic on the same filesystem; if two writers race, the last
 * rename wins — and since callers here pass identical bytes for a given
 * fixture input, the final file is byte-identical regardless of winner.
 */
async function atomicWrite(finalPath, bytes) {
  await mkdir(dirname(finalPath), { recursive: true });
  const tmp = `${finalPath}.tmp.${process.pid}`;
  await writeFile(tmp, bytes);
  await rename(tmp, finalPath);
}

/**
 * Build a single jar from a list of `{ name, data }` entries and write it to
 * `finalPath`. Skips the write if `finalPath` already exists with the
 * expected SHA-256 — makes the function idempotent and safe to call
 * repeatedly (including concurrently from multiple test.before hooks).
 *
 * @returns {{ path: string, sha256: string, skipped: boolean }}
 */
export async function writeJar(finalPath, entries) {
  const bytes = createZip(entries);
  const expected = sha256(bytes);

  if (existsSync(finalPath)) {
    try {
      const existing = await readFile(finalPath);
      if (sha256(existing) === expected) {
        return { path: finalPath, sha256: expected, skipped: true };
      }
    } catch {
      // Fall through and rewrite.
    }
  }

  await atomicWrite(finalPath, bytes);
  return { path: finalPath, sha256: expected, skipped: false };
}

/**
 * Build fixture jars for every plugin directory under the fixture root.
 * A plugin directory is detected by the presence of a `source/plugin.xml`.
 * The produced jar lands at `<pluginDir>/lib/<pluginDirName>.jar`.
 *
 * @param {{ fixtureRoot?: string }} [opts]
 * @returns {Promise<{ fixtureRoot: string, builtJars: Array<{ path: string, sha256: string, skipped: boolean, pluginId: string }> }>}
 */
export async function buildJetBrainsFixtures(opts = {}) {
  const fixtureRoot = opts.fixtureRoot || DEFAULT_FIXTURE_ROOT;
  if (!existsSync(fixtureRoot)) {
    throw new Error(`fixture root does not exist: ${fixtureRoot}`);
  }

  const builtJars = [];

  // Walk two levels deep: <fixtureRoot>/<IDE>/plugins/<pluginId>/source/plugin.xml
  async function walkIDEs() {
    const ideDirs = await readdir(fixtureRoot, { withFileTypes: true });
    for (const ide of ideDirs) {
      if (!ide.isDirectory()) continue;
      const pluginsDir = join(fixtureRoot, ide.name, 'plugins');
      if (!existsSync(pluginsDir)) continue;
      const pluginEntries = await readdir(pluginsDir, { withFileTypes: true });
      for (const p of pluginEntries) {
        if (!p.isDirectory()) continue;
        const pluginDir = join(pluginsDir, p.name);
        const sourceDir = join(pluginDir, 'source');
        if (!existsSync(join(sourceDir, 'plugin.xml'))) continue;

        // Build jar entries from source tree.
        const files = await walkFiles(sourceDir);
        const entries = [];
        for (const rel of files) {
          const abs = join(sourceDir, rel);
          const data = await readFile(abs);
          entries.push({ name: sourceToJarPath(rel), data });
        }

        const jarPath = join(pluginDir, 'lib', `${p.name}.jar`);
        const result = await writeJar(jarPath, entries);
        builtJars.push({ ...result, pluginId: p.name });
      }
    }
  }

  await walkIDEs();
  return { fixtureRoot, builtJars };
}

// CLI mode — invoked via `node tests/helpers/build-jetbrains-fixtures.mjs`.
if (import.meta.url === `file://${process.argv[1]}`) {
  buildJetBrainsFixtures()
    .then(({ fixtureRoot, builtJars }) => {
      const root = relative(process.cwd(), fixtureRoot);
      for (const j of builtJars) {
        const rel = relative(process.cwd(), j.path);
        const tag = j.skipped ? 'skip' : ' new';
        process.stdout.write(`[${tag}] ${rel}\n`);
      }
      process.stdout.write(`built ${builtJars.length} jar(s) under ${root}\n`);
    })
    .catch((err) => {
      process.stderr.write(`build-jetbrains-fixtures: ${err.stack || err.message}\n`);
      process.exit(1);
    });
}
