import { realpath } from "node:fs/promises";
import path from "node:path";

import { nodeFileTrace } from "@vercel/nft";

import {
  copyPathMaterializingSymlinks,
  hoistIsolatedStoreDependencies,
  isPathWithin,
} from "./artifact-stage.ts";
import { resolveSourceRoot } from "./config/source-root.ts";

/**
 * Configured artifacts copy an output directory's contents to the artifact
 * root. Runtime dependencies traced from that output need the same relocation:
 * app-local node_modules become artifact-root node_modules, while hoisted
 * workspace deps and workspace packages keep enough source-root layout for
 * bare imports and package symlinks to resolve after unpacking.
 */
export async function stageConfiguredArtifactDependencies(options: {
  appPath: string;
  artifactDir: string;
  outputDirectory: string;
  entrypoint: string;
  signal?: AbortSignal;
}): Promise<void> {
  const [appRoot, outputRoot] = await Promise.all([
    realpath(path.resolve(options.appPath)),
    realpath(path.resolve(options.outputDirectory)),
  ]);
  const sourceRoot = await resolveSourceRoot(appRoot, options.signal);
  const entrypointPath = path.join(outputRoot, options.entrypoint);

  options.signal?.throwIfAborted();
  const { fileList } = await nodeFileTrace([entrypointPath], {
    base: sourceRoot,
  });

  for (const relativePath of fileList) {
    options.signal?.throwIfAborted();
    const sourcePath = path.join(sourceRoot, relativePath);
    if (isPathWithin(outputRoot, sourcePath)) {
      continue;
    }

    await copyPathMaterializingSymlinks(
      sourcePath,
      path.join(
        options.artifactDir,
        artifactRelativePathForTrace(sourcePath, {
          appRoot,
          sourceRoot,
        }),
      ),
      {
        appRoot,
        sourceRoot,
        allowedSymlinkTargetRoots: [appRoot, sourceRoot],
        ignoreMissingSource: true,
        signal: options.signal,
      },
    );
  }

  await hoistIsolatedStoreDependencies(
    path.join(options.artifactDir, "node_modules"),
    options.signal,
  );
}

function artifactRelativePathForTrace(
  sourcePath: string,
  options: { appRoot: string; sourceRoot: string },
): string {
  const resolvedSource = path.resolve(sourcePath);
  const appNodeModules = path.join(options.appRoot, "node_modules");
  const sourceNodeModules = path.join(options.sourceRoot, "node_modules");

  if (isPathWithin(appNodeModules, resolvedSource)) {
    return path.join(
      "node_modules",
      path.relative(appNodeModules, resolvedSource),
    );
  }

  if (isPathWithin(options.appRoot, resolvedSource)) {
    return path.relative(options.appRoot, resolvedSource);
  }

  if (isPathWithin(sourceNodeModules, resolvedSource)) {
    return path.join(
      "node_modules",
      path.relative(sourceNodeModules, resolvedSource),
    );
  }

  return path.relative(options.sourceRoot, resolvedSource);
}
