import chalk from "chalk";
import fs from "fs";
import { copyFile, link, lstat, mkdir, readlink, rm, stat, symlink, utimes, writeFile, } from "fs/promises";
import { dirname, join, resolve } from "path";
import { getClients, shouldRecheck, } from "./clients/TorrentClient.js";
import { Action, ALL_EXTENSIONS, Decision, InjectionResult, LinkType, SaveResult, } from "./constants.js";
import { CrossSeedError } from "./errors.js";
import { Label, logger } from "./logger.js";
import { resultOf, resultOfErr } from "./Result.js";
import { getRuntimeConfig } from "./runtimeConfig.js";
import { createSearcheeFromPath, getMediaType, getRoot, getRootFolder, getSearcheeSource, } from "./searchee.js";
import { getTorrentSavePath } from "./torrent.js";
import { exists, filterAsync, findAFileWithExt, findAsync, formatAsList, getLogString, mapAsync, Mutex, notExists, verifyDir, withMutex, } from "./utils.js";
const linkDirSrcName = "linkDirSrc.cross-seed";
const linkDirDestName = "linkDirDest.cross-seed";
const clientDestName = "torrentClientDest.cross-seed";
async function linkAllFilesInMetafile(searchee, newMeta, decision, destinationDir, options) {
    const availableFiles = searchee.files.slice();
    const paths = decision === Decision.MATCH && options.savePath
        ? newMeta.files.map((file) => [
            join(options.savePath, file.path),
            join(destinationDir, file.path),
        ])
        : newMeta.files.reduce((acc, newFile) => {
            let matchedSearcheeFiles = availableFiles.filter((searcheeFile) => searcheeFile.length === newFile.length);
            if (matchedSearcheeFiles.length > 1) {
                matchedSearcheeFiles = matchedSearcheeFiles.filter((searcheeFile) => searcheeFile.name === newFile.name);
            }
            if (!matchedSearcheeFiles.length)
                return acc;
            const index = availableFiles.indexOf(matchedSearcheeFiles[0]);
            availableFiles.splice(index, 1);
            const srcFilePath = options.savePath
                ? join(options.savePath, matchedSearcheeFiles[0].path)
                : matchedSearcheeFiles[0].path; // Absolute path
            acc.push([srcFilePath, join(destinationDir, newFile.path)]);
            return acc;
        }, []);
    let alreadyExisted = false;
    let linkedNewFiles = false;
    try {
        logger.verbose({
            label: searchee.label,
            message: `Linking ${getLogString(newMeta)} from ${getLogString(searchee)} to ${destinationDir}`,
        });
        const validPaths = await filterAsync(paths, async ([srcFilePath, destFilePath]) => {
            if (await exists(destFilePath)) {
                logger.verbose({
                    label: searchee.label,
                    message: `--- Skipping ${srcFilePath} -> ${destFilePath}, already exists`,
                });
                alreadyExisted = true;
                return false;
            }
            if (await exists(srcFilePath))
                return true;
            if (options.ignoreMissing)
                return false;
            throw new Error(`Linking failed, ${srcFilePath} not found.`);
        });
        for (const [srcFilePath, destFilePath] of validPaths) {
            const destFileParentPath = dirname(destFilePath);
            if (await notExists(destFileParentPath)) {
                await mkdir(destFileParentPath, { recursive: true });
            }
            try {
                if (await linkFile(srcFilePath, destFilePath)) {
                    linkedNewFiles = true;
                }
            }
            catch (e) {
                logger.error({
                    label: searchee.label,
                    message: `--- Linking failed, ${srcFilePath} -> ${destFilePath}: ${e.message}`,
                });
                throw e;
            }
        }
    }
    catch (e) {
        return resultOfErr(e);
    }
    return resultOf({ alreadyExisted, linkedNewFiles });
}
async function unlinkMetafile(meta, destinationDir, searcheeLabel) {
    const roots = [];
    for (const file of meta.files) {
        const res = getRoot(file);
        if (res.isErr()) {
            const err = res.unwrapErr();
            logger.error({
                label: searcheeLabel,
                message: `Unable to unlink ${getLogString(meta)} in ${destinationDir}: ${err.message}`,
            });
            logger.debug(err);
            return;
        }
        roots.push(join(destinationDir, res.unwrap()));
    }
    const destinationDirIno = (await stat(destinationDir)).ino;
    for (const root of roots) {
        if (await notExists(root))
            continue;
        if (!root.startsWith(destinationDir))
            continue; // assert: root is within destinationDir
        if (resolve(root) === resolve(destinationDir))
            continue; // assert: root is not destinationDir
        if ((await stat(root)).ino === destinationDirIno)
            continue; // assert: root is not destinationDir
        logger.verbose(`Unlinking ${root}`);
        await rm(root, { recursive: true });
    }
}
async function getSavePath(searchee, options) {
    if (searchee.path) {
        if (await notExists(searchee.path)) {
            logger.error({
                label: searchee.label,
                message: `Linking failed, ${searchee.path} not found.`,
            });
            return resultOfErr("INVALID_DATA");
        }
        const result = await createSearcheeFromPath(searchee.path);
        if (result.isErr()) {
            return resultOfErr("TORRENT_NOT_FOUND");
        }
        const refreshedSearchee = result.unwrap();
        if (options.onlyCompleted &&
            (searchee.mtimeMs !== refreshedSearchee.mtimeMs ||
                searchee.length !== refreshedSearchee.length)) {
            return resultOfErr("TORRENT_NOT_COMPLETE");
        }
        return resultOf(dirname(searchee.path));
    }
    else if (!searchee.infoHash) {
        for (const file of searchee.files) {
            if (await notExists(file.path)) {
                logger.error(`Linking failed, ${file.path} not found.`);
                return resultOfErr("INVALID_DATA");
            }
            if (options.onlyCompleted) {
                const f = await stat(file.path);
                if (searchee.mtimeMs < f.mtimeMs || file.length !== f.size) {
                    return resultOfErr("TORRENT_NOT_COMPLETE");
                }
            }
        }
        return resultOf(undefined);
    }
    const clients = getClients();
    const client = clients.length === 1
        ? clients[0]
        : clients.find((c) => c.clientHost === searchee.clientHost);
    let savePath;
    if (searchee.savePath) {
        if (searchee.label !== Label.INJECT) {
            // This check is too slow for the number of searchees from the inject job
            const refreshedSearchee = (await client.getClientSearchees({
                newSearcheesOnly: true,
                refresh: [searchee.infoHash],
            })).newSearchees.find((s) => s.infoHash === searchee.infoHash);
            if (!refreshedSearchee)
                return resultOfErr("TORRENT_NOT_FOUND");
            Object.assign(searchee, refreshedSearchee);
        }
        if (options.onlyCompleted &&
            !(await client.isTorrentComplete(searchee.infoHash)).orElse(false)) {
            return resultOfErr("TORRENT_NOT_COMPLETE");
        }
        savePath = searchee.savePath;
    }
    else {
        const downloadDirResult = await client.getDownloadDir(searchee, { onlyCompleted: options.onlyCompleted });
        if (downloadDirResult.isErr()) {
            return downloadDirResult.mapErr((e) => e === "NOT_FOUND" || e === "UNKNOWN_ERROR"
                ? "TORRENT_NOT_FOUND"
                : e);
        }
        savePath = downloadDirResult.unwrap();
    }
    const rootFolderRes = getRootFolder(searchee.files[0]);
    if (rootFolderRes.isErr()) {
        logger.error({
            label: searchee.label,
            message: `Linking failed, ${rootFolderRes.unwrapErr().message}`,
        });
        return resultOfErr("INVALID_DATA");
    }
    const rootFolder = rootFolderRes.unwrap();
    const sourceRootOrSavePath = searchee.files.length === 1
        ? join(savePath, searchee.files[0].path)
        : rootFolder
            ? join(savePath, rootFolder)
            : savePath;
    if (await notExists(sourceRootOrSavePath)) {
        logger.error({
            label: searchee.label,
            message: `Linking failed, ${sourceRootOrSavePath} not found.`,
        });
        return resultOfErr("INVALID_DATA");
    }
    return resultOf(savePath);
}
async function getClientAndDestinationDir(client, searchee, savePath, newMeta, tracker) {
    const { flatLinking, linkType } = getRuntimeConfig();
    if (!client) {
        let srcPath;
        let srcDev;
        try {
            srcPath = !savePath
                ? (await findAsync(searchee.files, (f) => exists(f.path))).path
                : join(savePath, (await findAsync(searchee.files, (f) => exists(join(savePath, f.path)))).path);
            srcDev = (await stat(srcPath)).dev;
        }
        catch (e) {
            logger.debug(e);
            return null;
        }
        let error;
        for (const testClient of getClients().filter((c) => !c.readonly)) {
            const torrentSavePaths = new Set((await testClient.getAllDownloadDirs({
                metas: [],
                onlyCompleted: false,
            })).values());
            if (!torrentSavePaths.size) {
                error = new Error(`No save paths found to test with for ${testClient.label}, add at least one torrent to the client.`);
                continue;
            }
            for (const torrentSavePath of torrentSavePaths) {
                try {
                    if (srcDev &&
                        (await stat(torrentSavePath)).dev === srcDev) {
                        client = testClient;
                        break;
                    }
                    const testPath = join(torrentSavePath, clientDestName);
                    await linkFile(srcPath, testPath, linkType === LinkType.REFLINK ||
                        linkType === LinkType.REFLINK_OR_COPY
                        ? linkType
                        : LinkType.HARDLINK);
                    await rm(testPath);
                    client = testClient;
                    break;
                }
                catch (e) {
                    error = e;
                }
            }
            if (client)
                break;
        }
        if (!client) {
            logger.debug(error);
            return null;
        }
    }
    let destinationDir;
    let destinationDirFromClient = false;
    const clientSavePathRes = await client.getDownloadDir(newMeta, {
        onlyCompleted: false,
    });
    if (clientSavePathRes.isOk()) {
        destinationDir = clientSavePathRes.unwrap();
        destinationDirFromClient = true;
    }
    else {
        if (clientSavePathRes.unwrapErr() === "INVALID_DATA") {
            return null;
        }
        const linkDir = savePath
            ? await getLinkDir(savePath)
            : await getLinkDirVirtual(searchee);
        if (!linkDir)
            return null;
        destinationDir = flatLinking ? linkDir : join(linkDir, tracker);
    }
    return { client, destinationDir, destinationDirFromClient };
}
function logActionResultImpl(result, newMeta, decision, searchee, tracker, infoOrVerbose, warnOrVerbose) {
    const metaLog = getLogString(newMeta, chalk.green.bold);
    const searcheeLog = getLogString(searchee, chalk.magenta.bold);
    const source = `${getSearcheeSource(searchee)} (${searcheeLog})`;
    const foundBy = `Found ${metaLog} on ${chalk.bold(tracker)} by`;
    switch (result) {
        case SaveResult.SAVED:
            infoOrVerbose({
                label: searchee.label,
                message: `${foundBy} ${chalk.green.bold(decision)} from ${source} - saved`,
            });
            break;
        case InjectionResult.SUCCESS:
            infoOrVerbose({
                label: searchee.label,
                message: `${foundBy} ${chalk.green.bold(decision)} from ${source} - injected`,
            });
            break;
        case InjectionResult.ALREADY_EXISTS:
            infoOrVerbose({
                label: searchee.label,
                message: `${foundBy} ${chalk.yellow(decision)} from ${source} - exists`,
            });
            break;
        case InjectionResult.TORRENT_NOT_COMPLETE:
            warnOrVerbose({
                label: searchee.label,
                message: `${foundBy} ${chalk.yellow(decision)} from ${source} - source is incomplete, saving...`,
            });
            break;
        case InjectionResult.FAILURE:
        default:
            logger.error({
                label: searchee.label,
                message: `${foundBy} ${chalk.red(decision)} from ${source} - failed to inject, saving...`,
            });
            break;
    }
}
async function saveToOutputDir(newMeta, decision, searchee, tracker) {
    const { outputDir } = getRuntimeConfig();
    try {
        const filePath = getTorrentSavePath(newMeta, getMediaType(newMeta), tracker, outputDir, { cached: false });
        if (await exists(filePath)) {
            await utimes(filePath, new Date(), (await stat(filePath)).mtime);
            return;
        }
        await writeFile(filePath, new Uint8Array(newMeta.encode()));
    }
    catch (e) {
        logger.error({
            label: searchee.label,
            message: `Failed to save ${getLogString(newMeta, chalk.green.bold)} on ${chalk.bold(tracker)} to outputDir while processing ${chalk.bold(decision)} with ${getLogString(searchee, chalk.magenta.bold)}: ${e.message}`,
        });
        logger.debug(e);
    }
}
export async function performAction(newMeta, decision, searchee, tracker) {
    return withMutex(Mutex.CLIENT_INJECTION, { useQueue: true }, async () => {
        return performActionWithoutMutex(newMeta, decision, searchee, tracker);
    });
}
export async function performActionWithoutMutex(newMeta, decision, searchee, tracker, injectClient, options = { onlyCompleted: true }) {
    const { action, linkDirs } = getRuntimeConfig();
    const warnOrVerbose = searchee.label !== Label.INJECT ? logger.warn : logger.verbose;
    const infoOrVerbose = searchee.label !== Label.INJECT ? logger.info : logger.verbose;
    const logActionResult = (r) => logActionResultImpl(r, newMeta, decision, searchee, tracker, infoOrVerbose, warnOrVerbose);
    const saveTorrent = () => saveToOutputDir(newMeta, decision, searchee, tracker);
    if (action === Action.SAVE) {
        const actionResult = SaveResult.SAVED;
        logActionResult(actionResult);
        await saveTorrent();
        return { actionResult };
    }
    let savePath;
    let destinationDir;
    let destinationDirFromClient = false;
    let unlinkOk = false;
    let linkedNewFiles = false;
    try {
        const clients = getClients();
        let client = clients.length === 1
            ? clients[0]
            : clients.find((c) => c.clientHost === searchee.clientHost && !c.readonly);
        const readonlySource = !client && !!searchee.clientHost;
        if (linkDirs.length) {
            const savePathRes = await getSavePath(searchee, options);
            if (savePathRes.isErr()) {
                const result = savePathRes.unwrapErr();
                if (result === "TORRENT_NOT_COMPLETE") {
                    const actionResult = InjectionResult.TORRENT_NOT_COMPLETE;
                    logActionResult(actionResult);
                    await saveTorrent();
                    return { client, actionResult, linkedNewFiles };
                }
                const actionResult = InjectionResult.FAILURE;
                logger.error({
                    label: searchee.label,
                    message: `Failed to link files for ${getLogString(newMeta)} from ${getLogString(searchee)}: ${result}`,
                });
                logActionResult(actionResult);
                await saveTorrent();
                return { actionResult, linkedNewFiles };
            }
            savePath = savePathRes.unwrap();
            const res = await getClientAndDestinationDir(client, searchee, savePath, newMeta, tracker);
            if (res) {
                client = res.client;
                destinationDir = res.destinationDir;
                destinationDirFromClient = res.destinationDirFromClient;
            }
            else {
                client = undefined;
            }
        }
        if (!client) {
            logger.error({
                label: searchee.label,
                message: `Failed to find a torrent client for ${getLogString(searchee)}`,
            });
            const actionResult = InjectionResult.FAILURE;
            logActionResult(actionResult);
            await saveTorrent();
            return { actionResult, linkedNewFiles };
        }
        for (const otherClient of clients) {
            if (otherClient.clientHost === client.clientHost)
                continue;
            const existsRes = await otherClient.isTorrentInClient(newMeta.infoHash);
            if (existsRes.isOk()) {
                if (!existsRes.unwrap())
                    continue;
                warnOrVerbose({
                    label: searchee.label,
                    message: `Skipping ${getLogString(newMeta)} injection into ${client.clientHost} - already exists in ${otherClient.clientHost}`,
                });
            }
            else {
                logger.error({
                    label: searchee.label,
                    message: `Failed to check if ${getLogString(newMeta)} exists in ${otherClient.clientHost}: ${existsRes.unwrapErr()}`,
                });
            }
            const actionResult = InjectionResult.FAILURE;
            logActionResult(actionResult);
            return { actionResult, linkedNewFiles };
        }
        if (injectClient && injectClient.clientHost !== client.clientHost) {
            warnOrVerbose({
                label: searchee.label,
                message: `Skipping ${getLogString(newMeta)} injection into ${client.clientHost} - existing match is using ${injectClient.clientHost}`,
            });
            const actionResult = InjectionResult.FAILURE;
            logActionResult(actionResult);
            return { actionResult, linkedNewFiles };
        }
        if (linkDirs.length) {
            const res = await linkAllFilesInMetafile(searchee, newMeta, decision, destinationDir, { savePath, ignoreMissing: !options.onlyCompleted });
            if (res.isErr()) {
                logger.error({
                    label: searchee.label,
                    message: `Failed to link files for ${getLogString(newMeta)} from ${getLogString(searchee)}: ${res.unwrapErr().message}`,
                });
                const actionResult = InjectionResult.FAILURE;
                logActionResult(actionResult);
                await saveTorrent();
                return { actionResult, linkedNewFiles };
            }
            const linkResult = res.unwrap();
            unlinkOk = !linkResult.alreadyExisted;
            linkedNewFiles = linkResult.linkedNewFiles;
        }
        else if (searchee.path) {
            destinationDir = dirname(searchee.path);
        }
        else if (readonlySource) {
            const savePathRes = await getSavePath(searchee, options);
            savePath = savePathRes.orElse(undefined);
            if (!savePath) {
                logger.error({
                    label: searchee.label,
                    message: `Failed to find a save path for ${getLogString(searchee)}`,
                });
                const actionResult = InjectionResult.FAILURE;
                logActionResult(actionResult);
                await saveTorrent();
                return { actionResult, linkedNewFiles };
            }
            destinationDir = savePath;
        }
        const actionResult = destinationDirFromClient
            ? InjectionResult.ALREADY_EXISTS
            : await client.inject(newMeta, readonlySource
                ? { ...searchee, infoHash: undefined } // treat as data-based
                : searchee, decision, {
                onlyCompleted: options.onlyCompleted,
                destinationDir,
            });
        logActionResult(actionResult);
        if (actionResult === InjectionResult.SUCCESS) {
            // cross-seed may need to process these with the inject job
            if (shouldRecheck(newMeta, decision) || !searchee.infoHash) {
                await saveTorrent();
            }
        }
        else if (actionResult === InjectionResult.ALREADY_EXISTS) {
            if (linkedNewFiles) {
                infoOrVerbose({
                    label: client.label,
                    message: `Rechecking ${getLogString(newMeta)} as new files were linked from ${getLogString(searchee)}`,
                });
                await client.recheckTorrent(newMeta.infoHash);
                void client.resumeInjection(newMeta, decision, {
                    checkOnce: false,
                });
            }
        }
        else {
            await saveTorrent();
            if (unlinkOk && destinationDir) {
                await unlinkMetafile(newMeta, destinationDir, searchee.label);
                linkedNewFiles = false;
            }
        }
        if (actionResult === InjectionResult.FAILURE) {
            return { actionResult, linkedNewFiles };
        }
        return {
            client,
            actionResult,
            destinationDir: destinationDir ?? searchee.savePath,
            linkedNewFiles,
        };
    }
    catch (e) {
        logger.error({
            label: searchee.label,
            message: e,
        });
        const actionResult = InjectionResult.FAILURE;
        logActionResult(actionResult);
        await saveTorrent();
        return { actionResult, linkedNewFiles };
    }
}
export async function performActions(searchee, matches) {
    const results = [];
    for (const { tracker, assessment } of matches) {
        const { actionResult } = await performAction(assessment.metafile, assessment.decision, searchee, tracker);
        results.push(actionResult);
    }
    return results;
}
async function getLinkDir(pathStr) {
    const { linkDirs, linkType } = getRuntimeConfig();
    const pathStat = await stat(pathStr);
    const pathDev = pathStat.dev; // Windows always returns 0
    if (pathDev) {
        const devs = await mapAsync(linkDirs, async (d) => (await stat(d)).dev);
        if (new Set(devs).size === linkDirs.length) {
            for (const [index, linkDir] of linkDirs.entries()) {
                if (devs[index] === pathDev)
                    return linkDir;
            }
        }
    }
    let srcFile = pathStat.isFile()
        ? pathStr
        : pathStat.isDirectory()
            ? await findAFileWithExt(pathStr, ALL_EXTENSIONS)
            : null;
    let tempFile;
    if (!srcFile) {
        tempFile = pathStat.isDirectory()
            ? join(pathStr, linkDirSrcName)
            : join(dirname(pathStr), linkDirSrcName);
        try {
            await writeFile(tempFile, "");
            srcFile = tempFile;
        }
        catch (e) {
            logger.debug(e);
        }
    }
    if (srcFile) {
        for (const linkDir of linkDirs) {
            try {
                const testPath = join(linkDir, linkDirDestName);
                await linkFile(srcFile, testPath, linkType === LinkType.REFLINK ||
                    linkType === LinkType.REFLINK_OR_COPY
                    ? linkType
                    : LinkType.HARDLINK);
                await rm(testPath);
                if (tempFile && (await exists(tempFile)))
                    await rm(tempFile);
                return linkDir;
            }
            catch {
                continue;
            }
        }
    }
    if (tempFile && (await exists(tempFile)))
        await rm(tempFile);
    if (linkType !== LinkType.SYMLINK) {
        logger.error(`Cannot find any linkDir from linkDirs on the same drive to ${linkType} ${pathStr}`);
        return null;
    }
    if (linkDirs.length > 1) {
        logger.warn(`Cannot find any linkDir from linkDirs on the same drive, using first linkDir for symlink: ${pathStr}`);
    }
    return linkDirs[0];
}
async function getLinkDirVirtual(searchee) {
    const linkDir = await getLinkDir(searchee.files[0].path);
    if (!linkDir)
        return null;
    for (let i = 1; i < searchee.files.length; i++) {
        if ((await getLinkDir(searchee.files[i].path)) !== linkDir) {
            logger.error(`Cannot link files to multiple linkDirs for seasonFromEpisodes aggregation, source episodes are spread across multiple drives.`);
            return null;
        }
    }
    return linkDir;
}
async function linkFile(oldPath, newPath, linkType) {
    if (!linkType)
        linkType = getRuntimeConfig().linkType;
    try {
        const ogFileResolvedPath = await unwrapSymlinks(oldPath);
        switch (linkType) {
            case LinkType.HARDLINK:
                await link(ogFileResolvedPath, newPath);
                break;
            case LinkType.SYMLINK:
                // we need to resolve because symlinks are resolved outside
                // the context of cross-seed's working directory
                await symlink(ogFileResolvedPath, resolve(newPath));
                break;
            case LinkType.REFLINK:
                await copyFile(ogFileResolvedPath, newPath, fs.constants.COPYFILE_FICLONE_FORCE);
                break;
            case LinkType.REFLINK_OR_COPY:
                // this will silently fall back to copy if it can't reflink
                await copyFile(ogFileResolvedPath, newPath, fs.constants.COPYFILE_FICLONE);
                break;
            default:
                throw new Error(`Unsupported linkType: ${linkType}`);
        }
        return true;
    }
    catch (e) {
        if (e.code === "EEXIST")
            return false;
        throw e;
    }
}
/**
 * Recursively resolves symlinks to the original file. Differs from realpath
 * in that it will not resolve directory symlinks in the middle of the path.
 * @param path
 */
async function unwrapSymlinks(path) {
    for (let i = 0; i < 16; i++) {
        if (!(await lstat(path)).isSymbolicLink())
            return path;
        path = resolve(dirname(path), await readlink(path));
    }
    throw new Error(`too many levels of symbolic links at ${path}`);
}
/**
 * Tests if srcDir supports linkType.
 * @param srcDir The directory to link from
 * @param testSrcName A unique name.cross-seed to create in srcDir if necessary
 * @param testDestName A unique name.cross-seed to create in the linkDir
 * @returns true if the test was successful, false if it failed (or throws)
 */
export async function testLinking(srcDir, testSrcName, testDestName) {
    const { linkDirs, linkType } = getRuntimeConfig();
    let tempFile;
    try {
        let srcFile = await findAFileWithExt(srcDir, ALL_EXTENSIONS);
        if (!srcFile) {
            if (!(await verifyDir(srcDir, testSrcName, fs.constants.R_OK | fs.constants.W_OK))) {
                logger.error(`cross-seed is unable to verify linking for ${srcDir} (likely due to incorrect/insufficient volume mounts https://www.cross-seed.org/docs/tutorials/linking#configuring-linkdirs).`);
                return false;
            }
            tempFile = join(srcDir, testSrcName);
            try {
                await writeFile(tempFile, testSrcName);
                srcFile = tempFile;
            }
            catch (e) {
                logger.debug(e);
                logger.error(`Failed to create test file in ${srcDir} for linking test.`);
                return false;
            }
        }
        const linkDir = await getLinkDir(srcDir);
        if (!linkDir)
            throw new Error(`No valid linkDir found for ${srcDir}`);
        const testPath = join(linkDir, testDestName);
        await linkFile(srcFile, testPath);
        await rm(testPath);
        return true;
    }
    catch (e) {
        logger.error(e);
        throw new CrossSeedError(`Failed to create a test ${linkType} from ${srcDir} in any linkDirs: [${formatAsList(linkDirs.map((d) => `"${d}"`), { sort: false, style: "short", type: "unit" })}]. Ensure that ${linkType} is supported between these paths (hardlink/reflink requires same drive, partition, and volume).`);
    }
    finally {
        if (tempFile && (await exists(tempFile)))
            await rm(tempFile);
    }
}
//# sourceMappingURL=action.js.map