import { Buffer } from 'buffer';
import { mkdir, readlink as readLink, rm, symlink, writeFile } from 'fs/promises';
import os from 'os';
import { basename, dirname, extname, join } from 'path';
import { getPath as getV2APIPath } from '@netlify/serverless-functions-api';
import { copyFile } from 'copy-file';
import pMap from 'p-map';
import { addZipContent, addZipFile, ARCHIVE_FORMAT, endZip, startZip, } from '../../../archive.js';
import { cachedLstat, mkdirAndWriteFile } from '../../../utils/fs.js';
import { BOOTSTRAP_FILE_NAME, METADATA_FILE_NAME, conflictsWithEntryFile, getEntryFile, getTelemetryFile, isNamedLikeEntryFile, } from './entry_file.js';
import { getMetadataFile } from './metadata_file.js';
import { normalizeFilePath } from './normalize_path.js';
import { getPackageJsonIfAvailable } from './package_json.js';
// Taken from https://www.npmjs.com/package/cpy.
const COPY_FILE_CONCURRENCY = os.cpus().length === 0 ? 2 : os.cpus().length * 2;
// Sub-directory to place all user-defined files  (i.e. everything other than
// the entry file generated by zip-it-and-ship-it).
const DEFAULT_USER_SUBDIRECTORY = 'src';
const addBootstrapFile = function (srcFiles, aliases) {
    // This is the path to the file that contains all the code for the v2
    // functions API. We add it to the list of source files and create an
    // alias so that it's written as `BOOTSTRAP_FILE_NAME` in the ZIP/Directory.
    const v2APIPath = getV2APIPath();
    srcFiles.push(v2APIPath);
    aliases.set(v2APIPath, BOOTSTRAP_FILE_NAME);
    return v2APIPath;
};
const createDirectory = async function ({ aliases = new Map(), basePath, cache, destFolder, extension, featureFlags, filename, mainFile, moduleFormat, rewrites = new Map(), runtimeAPIVersion, srcFiles, }) {
    // There is a naming conflict with the entry file if one of the supporting
    // files (i.e. not the main file) has the path that the entry file needs to
    // take.
    const hasEntryFileConflict = conflictsWithEntryFile(srcFiles, {
        basePath,
        extension,
        filename,
        mainFile,
        runtimeAPIVersion,
    });
    // If there is a naming conflict, we move all user files (everything other
    // than the entry file) to its own sub-directory.
    const userNamespace = hasEntryFileConflict ? DEFAULT_USER_SUBDIRECTORY : '';
    const { contents: entryContents, filename: entryFilename } = getEntryFile({
        commonPrefix: basePath,
        featureFlags,
        filename,
        mainFile,
        moduleFormat,
        userNamespace,
        runtimeAPIVersion,
    });
    const { contents: telemetryContents, filename: telemetryFilename } = getTelemetryFile();
    const functionFolder = join(destFolder, basename(filename, extension));
    // Deleting the functions directory in case it exists before creating it.
    await rm(functionFolder, { recursive: true, force: true, maxRetries: 3 });
    await mkdir(functionFolder, { recursive: true });
    // Writing entry files.
    await Promise.all([
        writeFile(join(functionFolder, entryFilename), entryContents),
        featureFlags.zisi_add_instrumentation_loader
            ? writeFile(join(functionFolder, telemetryFilename), telemetryContents)
            : Promise.resolve(),
    ]);
    if (runtimeAPIVersion === 2) {
        addBootstrapFile(srcFiles, aliases);
    }
    const symlinks = new Map();
    // Copying source files.
    await pMap(srcFiles, async (srcFile) => {
        const destPath = aliases.get(srcFile) || srcFile;
        const normalizedDestPath = normalizeFilePath({
            commonPrefix: basePath,
            path: destPath,
            userNamespace,
        });
        const absoluteDestPath = join(functionFolder, normalizedDestPath);
        if (rewrites.has(srcFile)) {
            return mkdirAndWriteFile(absoluteDestPath, rewrites.get(srcFile));
        }
        const stat = await cachedLstat(cache.lstatCache, srcFile);
        // If the path is a symlink, find the link target and add the link to a
        // `symlinks` map, which we'll later use to create the symlinks in the
        // target directory. We can't do that right now because the target path
        // may not have been copied over yet.
        if (stat.isSymbolicLink()) {
            const targetPath = await readLink(srcFile);
            // Two symlinks may point at the same target path, so keep a list of symlinks to create.
            symlinks.set(targetPath, (symlinks.get(targetPath) ?? new Set()).add(absoluteDestPath));
            return;
        }
        return copyFile(srcFile, absoluteDestPath);
    }, { concurrency: COPY_FILE_CONCURRENCY });
    await pMap([...symlinks.entries()], async ([target, paths]) => {
        for (const path of paths) {
            await mkdir(dirname(path), { recursive: true });
            await symlink(target, path);
        }
    }, {
        concurrency: COPY_FILE_CONCURRENCY,
    });
    return { path: functionFolder, entryFilename };
};
const createZipArchive = async function ({ aliases = new Map(), basePath, branch, cache, destFolder, extension, featureFlags, filename, mainFile, moduleFormat, rewrites, runtimeAPIVersion, srcFiles, generator, }) {
    const destPath = join(destFolder, `${basename(filename, extension)}.zip`);
    const { archive, output } = startZip(destPath);
    // There is a naming conflict with the entry file if one of the supporting
    // files (i.e. not the main file) has the path that the entry file needs to
    // take.
    const hasEntryFileConflict = conflictsWithEntryFile(srcFiles, {
        basePath,
        extension,
        filename,
        mainFile,
        runtimeAPIVersion,
    });
    // We don't need an entry file if it would end up with the same path as the
    // function's main file. Unless we have a file conflict and need to move everything into a subfolder
    const needsEntryFile = runtimeAPIVersion === 2 ||
        hasEntryFileConflict ||
        !isNamedLikeEntryFile(mainFile, { basePath, filename, runtimeAPIVersion });
    // If there is a naming conflict, we move all user files (everything other
    // than the entry file) to its own sub-directory.
    const userNamespace = hasEntryFileConflict ? DEFAULT_USER_SUBDIRECTORY : '';
    let entryFilename = `${basename(filename, extname(filename))}.js`;
    let bootstrapVersion;
    if (needsEntryFile) {
        const entryFile = getEntryFile({
            commonPrefix: basePath,
            filename,
            mainFile,
            moduleFormat,
            userNamespace,
            featureFlags,
            runtimeAPIVersion,
        });
        entryFilename = entryFile.filename;
        addEntryFileToZip(archive, entryFile);
    }
    const telemetryFile = getTelemetryFile(generator);
    if (featureFlags.zisi_add_instrumentation_loader === true) {
        addEntryFileToZip(archive, telemetryFile);
    }
    if (runtimeAPIVersion === 2) {
        const bootstrapPath = addBootstrapFile(srcFiles, aliases);
        const { version } = await getPackageJsonIfAvailable(bootstrapPath);
        const payload = JSON.stringify(getMetadataFile(version, branch));
        bootstrapVersion = version;
        addZipContent(archive, payload, METADATA_FILE_NAME);
    }
    const deduplicatedSrcFiles = [...new Set(srcFiles)];
    const srcFilesInfos = await Promise.all(deduplicatedSrcFiles.map((file) => addStat(cache, file)));
    // We ensure this is not async, so that the archive's checksum is
    // deterministic. Otherwise it depends on the order the files were added.
    srcFilesInfos.forEach(({ srcFile, stat }) => {
        zipJsFile({
            aliases,
            archive,
            commonPrefix: basePath,
            rewrites,
            srcFile,
            stat,
            userNamespace,
        });
    });
    await endZip(archive, output);
    return { path: destPath, entryFilename, bootstrapVersion };
};
export const zipNodeJs = function ({ archiveFormat, ...options }) {
    if (archiveFormat === ARCHIVE_FORMAT.ZIP) {
        return createZipArchive(options);
    }
    return createDirectory(options);
};
const addEntryFileToZip = function (archive, { contents, filename }) {
    const contentBuffer = Buffer.from(contents);
    addZipContent(archive, contentBuffer, filename);
};
const addStat = async function (cache, srcFile) {
    const stat = await cachedLstat(cache.lstatCache, srcFile);
    return { srcFile, stat };
};
const zipJsFile = function ({ aliases = new Map(), archive, commonPrefix, rewrites = new Map(), stat, srcFile, userNamespace, }) {
    const destPath = aliases.get(srcFile) || srcFile;
    const normalizedDestPath = normalizeFilePath({ commonPrefix, path: destPath, userNamespace });
    if (rewrites.has(srcFile)) {
        addZipContent(archive, rewrites.get(srcFile), normalizedDestPath);
    }
    else {
        addZipFile(archive, srcFile, normalizedDestPath, stat);
    }
};
