#!/usr/bin/env bun
// Normalize Bun-generated .bin entries to stable symlinks or byte-stable wrappers.

import {
  chmod,
  lstat,
  readdir,
  readFile,
  readlink,
  symlink,
  unlink,
  writeFile,
} from "node:fs/promises";
import { basename, dirname, join, relative, resolve } from "node:path";

const nodeModulesRoot = resolve(process.cwd(), process.argv[2] ?? "node_modules");
async function pathExists(path: string): Promise<boolean> {
  try {
    await lstat(path);
    return true;
  } catch {
    return false;
  }
}

function lexicalSort(values: Iterable<string>): string[] {
  return [...values].sort((left, right) => left.localeCompare(right, "en"));
}

function commandNameFromPackage(name: string): string {
  const segments = name.split("/");
  return segments[segments.length - 1]!;
}

function normalizeRelativeTarget(fromDir: string, targetPath: string): string {
  const target = relative(fromDir, targetPath) || ".";
  return target.replaceAll(/\/+/g, "/");
}

function isBinDirectory(path: string): boolean {
  return basename(path) === ".bin" && dirname(path).endsWith("node_modules");
}

async function readSortedDir(path: string): Promise<string[]> {
  return lexicalSort(await readdir(path));
}

async function collectPackageDirectories(nodeModulesDir: string): Promise<string[]> {
  const packageDirs: string[] = [];
  for (const entry of await readSortedDir(nodeModulesDir)) {
    if (entry === ".bin") {
      continue;
    }

    const entryPath = join(nodeModulesDir, entry);
    const stats = await lstat(entryPath);
    if (!stats.isDirectory() && !stats.isSymbolicLink()) {
      continue;
    }

    if (entry.startsWith("@")) {
      for (const scopedEntry of await readSortedDir(entryPath).catch(() => [])) {
        packageDirs.push(join(entryPath, scopedEntry));
      }
      continue;
    }

    packageDirs.push(entryPath);
  }

  return packageDirs;
}

async function readPackageJson(path: string): Promise<any | null> {
  try {
    return JSON.parse(await readFile(path, "utf8"));
  } catch {
    return null;
  }
}

async function collectBinCandidates(nodeModulesDir: string): Promise<Map<string, string[]>> {
  const bins = new Map<string, string[]>();
  for (const packageDir of await collectPackageDirectories(nodeModulesDir)) {
    const packageJson = await readPackageJson(join(packageDir, "package.json"));
    if (packageJson === null || packageJson.bin === undefined) {
      continue;
    }

    const register = (command: string, relativeTarget: string) => {
      const targetPath = resolve(packageDir, relativeTarget);
      const candidates = bins.get(command) ?? [];
      candidates.push(targetPath);
      bins.set(command, candidates);
    };

    if (typeof packageJson.bin === "string" && typeof packageJson.name === "string") {
      register(commandNameFromPackage(packageJson.name), packageJson.bin);
      continue;
    }

    if (packageJson.bin && typeof packageJson.bin === "object") {
      for (const command of lexicalSort(Object.keys(packageJson.bin))) {
        const relativeTarget = packageJson.bin[command];
        if (typeof relativeTarget === "string") {
          register(command, relativeTarget);
        }
      }
    }
  }

  for (const [command, targets] of bins) {
    bins.set(command, lexicalSort(new Set(targets)));
  }
  return bins;
}

async function resolveCurrentTarget(binPath: string): Promise<string | null> {
  const stats = await lstat(binPath);
  if (stats.isSymbolicLink()) {
    const currentTarget = await readlink(binPath);
    return resolve(dirname(binPath), currentTarget);
  }

  if (!stats.isFile()) {
    return null;
  }

  const content = await readFile(binPath, "utf8");
  const launchTargetMatch =
    content.match(/(?:exec|run)\s+["']?([^"'$\s][^"'$\n]*)["']?/) ??
    content.match(/(?:^|\s)(\.\.\/[^\s"'`]+|\.\/[^\s"'`]+)(?:\s|$)/m);
  if (launchTargetMatch) {
    return resolve(dirname(binPath), launchTargetMatch[1]!);
  }

  return null;
}

async function rewriteSymlink(binPath: string, targetPath: string): Promise<void> {
  const nextTarget = normalizeRelativeTarget(dirname(binPath), targetPath);
  const currentTarget = await readlink(binPath).catch(() => null);
  if (currentTarget === nextTarget) {
    return;
  }

  await unlink(binPath).catch(() => undefined);
  await symlink(nextTarget, binPath);
}

async function writeWrapper(binPath: string, targetPath: string): Promise<void> {
  const launcherTarget = normalizeRelativeTarget(dirname(binPath), targetPath);
  const wrapper = `#!/usr/bin/env sh
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
exec "$script_dir/${launcherTarget}" "$@"
`;
  await writeFile(binPath, wrapper, "utf8");
  await chmod(binPath, 0o755);
}

async function normalizeBinEntry(binDir: string, entry: string, candidates: Map<string, string[]>): Promise<void> {
  const binPath = join(binDir, entry);
  const stats = await lstat(binPath);
  const candidateTargets = candidates.get(entry) ?? [];
  const currentTarget = await resolveCurrentTarget(binPath);

  let selectedTarget: string | null = null;
  if (candidateTargets.length === 1) {
    selectedTarget = candidateTargets[0]!;
  } else if (currentTarget !== null) {
    selectedTarget = candidateTargets.find((candidate) => resolve(candidate) === resolve(currentTarget)) ?? null;
  }

  if (stats.isSymbolicLink()) {
    if (selectedTarget !== null && (await pathExists(selectedTarget))) {
      await rewriteSymlink(binPath, selectedTarget);
    }
    return;
  }

  if (!stats.isFile()) {
    return;
  }

  if (selectedTarget !== null && (await pathExists(selectedTarget))) {
    await unlink(binPath);
    await symlink(normalizeRelativeTarget(dirname(binPath), selectedTarget), binPath);
    return;
  }

  const content = await readFile(binPath, "utf8");
  const homePath = process.env.HOME ?? "";
  const hasMachinePath =
    content.includes("/build/") ||
    content.includes("/tmp/") ||
    (homePath.length > 0 && content.includes(homePath));
  if (hasMachinePath) {
    if (currentTarget !== null && (await pathExists(currentTarget))) {
      await writeWrapper(binPath, currentTarget);
    }
  }
  await chmod(binPath, 0o755);
}

async function walk(path: string): Promise<void> {
  const stats = await lstat(path);
  if (!stats.isDirectory()) {
    return;
  }

  if (isBinDirectory(path)) {
    const candidates = await collectBinCandidates(dirname(path));
    for (const entry of await readSortedDir(path)) {
      await normalizeBinEntry(path, entry, candidates);
    }
    return;
  }

  for (const entry of await readSortedDir(path)) {
    if (entry === "." || entry === "..") {
      continue;
    }
    await walk(join(path, entry));
  }
}

if (await pathExists(nodeModulesRoot)) {
  await walk(nodeModulesRoot);
}
