import { lstat, readdir, readFile, readlink } from "node:fs/promises";
import path from "node:path";
import { createGzip } from "node:zlib";
import { type Pack, pack } from "tar-stream";

const COMPUTE_MANIFEST_VERSION = "1" as const;

/**
 * Create a tar.gz archive in memory from a build artifact directory.
 *
 * - Files are added under the `bundle/` prefix.
 * - Symlinks are preserved as symlink entries.
 * - A synthetic `compute.manifest.json` entry is injected at the tar root.
 *
 * Returns the gzipped archive as a Uint8Array.
 */
export async function createArchive(
  directory: string,
  entrypoint: string,
  signal?: AbortSignal,
): Promise<Uint8Array> {
  signal?.throwIfAborted();
  const packer = pack();
  const rootPath = path.resolve(directory);

  // Walk the directory and add all files
  await addDirectory(packer, rootPath, rootPath, "bundle", signal);

  // Inject the manifest as a synthetic entry
  const manifest = JSON.stringify(
    {
      manifestVersion: COMPUTE_MANIFEST_VERSION,
      entrypoint: `bundle/${entrypoint}`,
    },
    null,
    2,
  );
  packer.entry({ name: "compute.manifest.json" }, manifest);

  // Signal end of archive
  packer.finalize();

  // Pipe through gzip and collect into buffer
  return await collectGzipped(packer, signal);
}

async function addDirectory(
  packer: Pack,
  rootPath: string,
  fsPath: string,
  tarPrefix: string,
  signal?: AbortSignal,
): Promise<void> {
  signal?.throwIfAborted();
  const directoryPath = resolvePathWithinRoot(rootPath, fsPath);
  const entries = await readdir(directoryPath, { withFileTypes: true });

  for (const entry of entries) {
    signal?.throwIfAborted();
    const fullPath = path.join(directoryPath, entry.name);
    const tarPath = `${tarPrefix}/${entry.name}`;

    if (entry.isSymbolicLink()) {
      const linkStat = await lstat(fullPath);
      await addSymlink(packer, rootPath, fullPath, tarPath, linkStat, signal);
    } else if (entry.isDirectory()) {
      await addDirectory(packer, rootPath, fullPath, tarPath, signal);
    } else if (entry.isFile()) {
      const fileStat = await lstat(fullPath);
      await addFile(packer, rootPath, fullPath, tarPath, fileStat, signal);
    }
  }
}

async function addFile(
  packer: Pack,
  rootPath: string,
  fsPath: string,
  tarPath: string,
  fileStat: { size: number; mode: number; mtime: Date },
  signal?: AbortSignal,
): Promise<void> {
  signal?.throwIfAborted();
  const filePath = resolvePathWithinRoot(rootPath, fsPath);
  const content = await readFile(filePath);
  packer.entry(
    {
      name: tarPath,
      size: content.length,
      mode: fileStat.mode,
      mtime: fileStat.mtime,
    },
    content,
  );
}

async function addSymlink(
  packer: Pack,
  rootPath: string,
  fsPath: string,
  tarPath: string,
  linkStat: { mode: number; mtime: Date },
  signal?: AbortSignal,
): Promise<void> {
  signal?.throwIfAborted();
  const symlinkPath = resolvePathWithinRoot(rootPath, fsPath);
  const target = await readlink(symlinkPath);
  const linkname = await resolveArchiveSymlinkTarget(
    rootPath,
    symlinkPath,
    target,
    signal,
  );

  packer.entry({
    name: tarPath,
    type: "symlink",
    linkname,
    mode: linkStat.mode,
    mtime: linkStat.mtime,
  });
}

async function resolveArchiveSymlinkTarget(
  rootPath: string,
  symlinkPath: string,
  target: string,
  signal?: AbortSignal,
): Promise<string> {
  signal?.throwIfAborted();
  const symlinkDir = path.dirname(symlinkPath);
  const resolvedTarget = resolvePathWithinRoot(
    rootPath,
    path.isAbsolute(target) ? target : path.resolve(symlinkDir, target),
  );

  await lstat(resolvedTarget).catch((error) => {
    if (isENOENT(error)) {
      throw new Error(
        `Symlink target does not exist: ${symlinkPath} -> ${target} (resolved to ${resolvedTarget})`,
      );
    }

    throw error;
  });

  if (!path.isAbsolute(target)) {
    return target;
  }

  return path.relative(symlinkDir, resolvedTarget) || ".";
}

function isENOENT(error: unknown): boolean {
  return (
    error instanceof Error &&
    "code" in error &&
    (error as { code?: string }).code === "ENOENT"
  );
}

function resolvePathWithinRoot(rootPath: string, fsPath: string): string {
  const normalizedRootPath = path.resolve(rootPath);
  const resolvedPath = path.resolve(fsPath);
  const relativePath = path.relative(normalizedRootPath, resolvedPath);

  if (
    relativePath === ".." ||
    relativePath.startsWith(`..${path.sep}`) ||
    path.isAbsolute(relativePath)
  ) {
    throw new Error(`Symlink target escapes archive root: ${resolvedPath}`);
  }

  return resolvedPath;
}

function collectGzipped(
  packer: Pack,
  signal?: AbortSignal,
): Promise<Uint8Array> {
  signal?.throwIfAborted();

  const chunks: Buffer[] = [];
  const gzip = createGzip();
  const onAbort = () => {
    // AbortSignal.abort() sets signal.reason; fallback keeps a stream error path if a non-standard signal omits it.
    const abortError =
      signal?.reason instanceof Error
        ? signal.reason
        : new DOMException("The operation was aborted", "AbortError");
    gzip.destroy(abortError);
    packer.destroy(abortError);
  };

  signal?.addEventListener("abort", onAbort, { once: true });

  const promise = new Promise<Uint8Array>((resolve, reject) => {
    packer.pipe(gzip);

    gzip.on("data", (chunk: Buffer) => {
      chunks.push(chunk);
    });

    gzip.on("end", () => {
      resolve(Buffer.concat(chunks));
    });

    gzip.on("error", reject);
    packer.on("error", reject);
  });

  return promise.finally(() => {
    signal?.removeEventListener("abort", onAbort);
  });
}
