import { cp, readdir, readlink, realpath, rm, stat } from "node:fs/promises";
import path from "node:path";

/**
 * Materialize symlinks inside a build artifact that point outside the artifact
 * directory but still live under the original app tree (typical for Next.js
 * standalone output referencing monorepo node_modules).
 */
export async function normalizeArtifactSymlinks(
  artifactDir: string,
  appPath: string,
  signal?: AbortSignal,
): Promise<void> {
  signal?.throwIfAborted();
  const normalizedArtifactDir = await realpath(path.resolve(artifactDir));
  const normalizedAppPath = await realpath(path.resolve(appPath));

  await walkDirectory(path.resolve(artifactDir));

  async function walkDirectory(directory: string): Promise<void> {
    signal?.throwIfAborted();
    const entries = await readdir(directory, { withFileTypes: true });

    for (const entry of entries) {
      const fullPath = path.join(directory, entry.name);

      if (entry.isDirectory()) {
        await walkDirectory(fullPath);
        continue;
      }

      if (!entry.isSymbolicLink()) {
        continue;
      }

      const target = await readlink(fullPath);
      const resolvedTarget = path.resolve(path.dirname(fullPath), target);
      const realTarget = await realpath(resolvedTarget);

      if (isPathWithin(normalizedArtifactDir, realTarget)) {
        continue;
      }

      if (!isPathWithin(normalizedAppPath, realTarget)) {
        throw new Error(
          `Build artifact symlink escapes the app directory: ${realTarget}`,
        );
      }

      const targetStat = await stat(realTarget);
      await rm(fullPath, { force: true, recursive: true });
      await cp(realTarget, fullPath, {
        recursive: targetStat.isDirectory(),
        // Keep nested symlinks intact so walkDirectory can validate each one.
        dereference: false,
      });

      if (targetStat.isDirectory()) {
        await walkDirectory(fullPath);
      }
    }
  }
}

function isPathWithin(rootPath: string, candidatePath: string): boolean {
  const relativePath = path.relative(rootPath, candidatePath);

  return (
    relativePath === "" ||
    (!relativePath.startsWith(`..${path.sep}`) &&
      relativePath !== ".." &&
      !path.isAbsolute(relativePath))
  );
}
