import { distance } from "fastest-levenshtein";
import bencode from "bencode";
import { readdir, readFile, stat } from "fs/promises";
import { extname, join, resolve } from "path";
import { inspect } from "util";
import { getClients, } from "./clients/TorrentClient.js";
import { isChildPath } from "./utils.js";
import { ALL_PARENTHESES_REGEX, ALL_SPACES_REGEX, ALL_SQUARE_BRACKETS_REGEX, LEVENSHTEIN_DIVISOR, MAX_PATH_BYTES, MediaType, MIN_VIDEO_QUERY_LENGTH, SAVED_TORRENTS_INFO_REGEX, USER_AGENT, } from "./constants.js";
import { getDataByFuzzyName, indexDataDirs, shouldIgnorePathHeuristically, } from "./dataFiles.js";
import { db } from "./db.js";
import { Label, logger, logOnce } from "./logger.js";
import { Metafile, updateMetafileMetadata } from "./parseTorrent.js";
import { isOk, resultOf, resultOfErr } from "./Result.js";
import { getRuntimeConfig } from "./runtimeConfig.js";
import { createSearcheeFromDB, createSearcheeFromPath, createSearcheeFromTorrentFile, getAnimeKeys, getEpisodeKeys, getLargestFile, getMovieKeys, getSeasonKeys, } from "./searchee.js";
import { createKeyTitle, exists, filterAsync, filterAsyncYield, flatMapAsync, getLogString, inBatches, mapAsync, Mutex, notExists, stripExtension, wait, withMutex, } from "./utils.js";
export var SnatchError;
(function (SnatchError) {
    SnatchError["ABORTED"] = "ABORTED";
    SnatchError["RATE_LIMITED"] = "RATE_LIMITED";
    SnatchError["MAGNET_LINK"] = "MAGNET_LINK";
    SnatchError["INVALID_CONTENTS"] = "INVALID_CONTENTS";
    SnatchError["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
})(SnatchError || (SnatchError = {}));
function isRecoverableTorrentDirError(error) {
    if (!(error instanceof Error))
        return false;
    const code = error.code;
    return ["ENOENT", "ENOTDIR", "EACCES", "EPERM"].includes(code ?? "");
}
function logRecoverableTorrentDirError(torrentDir, error) {
    const message = error instanceof Error ? error.message : String(error ?? "");
    logger.warn({
        label: Label.INDEX,
        message: `Skipping torrentDir indexing for "${torrentDir}": ${message}`,
    });
    logger.debug(error);
}
export async function parseTorrentFromPath(filePath) {
    return Metafile.decode(await readFile(filePath));
}
export async function parseTorrentWithMetadata(filePath, torrentInfos) {
    const meta = await parseTorrentFromPath(filePath);
    const clients = getClients();
    if (clients[0]?.clientType === Label.QBITTORRENT) {
        const fastResumePath = filePath.replace(extname(filePath), ".fastresume");
        if (await exists(fastResumePath)) {
            updateMetafileMetadata(meta, bencode.decode(await readFile(fastResumePath)));
            return meta;
        }
        else {
            logger.verbose(`No .fastresume found at ${fastResumePath} from ${filePath}, using client to get metadata without trackers`);
        }
    }
    // All other clients keep trackers in their .torrent files
    // Also fallback for qBittorrent if .fastresume is missing e.g cross-seed search --torrents ...
    // Getting all trackers for qbit require individual torrent requests but we do have the first active one
    const torrentInfo = torrentInfos.find((t) => t.infoHash === meta.infoHash);
    if (torrentInfo) {
        meta.category = torrentInfo.category;
        meta.tags = torrentInfo.tags;
        if (!meta.trackers.length && torrentInfo.trackers) {
            meta.trackers = torrentInfo.trackers;
        }
    }
    else if (clients.length) {
        logger.verbose(`No torrent info found for ${meta.infoHash} from ${filePath}`);
    }
    return meta;
}
function isMagnetRedirectError(error) {
    return (
    // node-fetch
    error.message.includes('URL scheme "magnet" is not supported.') ||
        // undici
        Boolean(error.cause?.message.includes("URL scheme must be a HTTP(S) scheme")));
}
async function snatchOnce(candidate) {
    const { snatchTimeout } = getRuntimeConfig();
    const url = candidate.link;
    let response;
    try {
        response = await fetch(url, {
            headers: {
                ...(candidate.cookie ? { Cookie: candidate.cookie } : {}),
                "User-Agent": USER_AGENT,
            },
            signal: typeof snatchTimeout === "number"
                ? AbortSignal.timeout(snatchTimeout)
                : undefined,
        });
    }
    catch (e) {
        if (e.name === "AbortError" || e.name === "TimeoutError") {
            logger.debug(`${candidate.name}: ${url}`);
            return {
                snatchError: SnatchError.ABORTED,
                extra: `snatch timed out`,
            };
        }
        else if (isMagnetRedirectError(e)) {
            logger.debug(`${candidate.name}: ${url}`);
            return { snatchError: SnatchError.MAGNET_LINK };
        }
        logger.debug(`${candidate.name}: ${url}`);
        logger.debug(e);
        return {
            snatchError: SnatchError.UNKNOWN_ERROR,
            extra: `failed to access`,
        };
    }
    const retryAfterSeconds = Number(response.headers.get("Retry-After"));
    const retryAfterMs = !Number.isNaN(retryAfterSeconds)
        ? retryAfterSeconds * 1000
        : undefined;
    if (response.status === 429) {
        return { snatchError: SnatchError.RATE_LIMITED, retryAfterMs };
    }
    else if (!response.ok) {
        const responseText = await response.clone().text();
        logger.debug(`${candidate.name}: ${url} - Response: "${responseText.slice(0, 100)}${responseText.length > 100 ? "..." : ""}"`);
        return {
            snatchError: SnatchError.UNKNOWN_ERROR,
            retryAfterMs,
            extra: `error downloading torrent - ${response.status} ${response.statusText}`,
        };
    }
    else if (response.headers.get("Content-Type") === "application/rss+xml") {
        const responseText = await response.clone().text();
        logger.debug(`${candidate.name}: ${url} - Contents: "${responseText.slice(0, 100)}${responseText.length > 100 ? "..." : ""}"`);
        return { snatchError: SnatchError.INVALID_CONTENTS, retryAfterMs };
    }
    try {
        return Metafile.decode(Buffer.from(new Uint8Array(await response.arrayBuffer())));
    }
    catch (e) {
        const contentType = response.headers.get("Content-Type");
        const contentLength = response.headers.get("Content-Length");
        logger.debug(`${candidate.name}: ${url} - Content-Type: ${contentType} - Content-Length: ${contentLength}`);
        logger.debug(e);
        return { snatchError: SnatchError.INVALID_CONTENTS, retryAfterMs };
    }
}
export const snatchHistory = new Map();
export async function snatch(candidate, label, options) {
    const retries = Math.max(options.retries, 0);
    const retryAfterEndTime = Date.now() + retries * options.delayMs;
    let snatchError;
    for (let i = 0; i <= retries; i++) {
        const progress = `${i + 1}/${retries + 1}`;
        const snatchResult = await snatchOnce(candidate);
        if (snatchResult instanceof Metafile) {
            logger.verbose({
                label,
                message: `Snatched ${candidate.name} from ${candidate.tracker}${i > 0 ? ` on attempt ${progress}` : ""}`,
            });
            snatchHistory.delete(candidate.link);
            return resultOf(snatchResult);
        }
        snatchError = snatchResult.snatchError;
        if (snatchError === SnatchError.RATE_LIMITED ||
            snatchError === SnatchError.MAGNET_LINK) {
            snatchHistory.delete(candidate.link);
            return resultOfErr(snatchError);
        }
        const { extra, retryAfterMs } = snatchResult;
        let linkHistory = snatchHistory.get(candidate.link);
        if (linkHistory) {
            ++linkHistory.numFailures;
        }
        else {
            linkHistory = { initialFailureAt: Date.now(), numFailures: 1 };
            snatchHistory.set(candidate.link, linkHistory);
        }
        let trackerHistory = snatchHistory.get(candidate.tracker);
        if (trackerHistory) {
            ++trackerHistory.numFailures;
        }
        else {
            trackerHistory = { initialFailureAt: Date.now(), numFailures: 1 };
            snatchHistory.set(candidate.tracker, trackerHistory);
        }
        if (linkHistory.numFailures > retries + 1) {
            logger.warn({
                label,
                message: `Snatching ${candidate.name} from ${candidate.tracker} stopped after attempt ${progress}, this snatch has failed too many times recently: ${snatchError}${extra ? ` - ${extra}` : ""}`,
            });
            return resultOfErr(snatchError);
        }
        if (trackerHistory.numFailures > retries * 2 + 1) {
            logger.warn({
                label,
                message: `Snatching ${candidate.name} from ${candidate.tracker} stopped after attempt ${progress}, this tracker has failed too many times recently: ${snatchError}${extra ? ` - ${extra}` : ""}`,
            });
            return resultOfErr(snatchError);
        }
        if (retryAfterMs && Date.now() + retryAfterMs >= retryAfterEndTime) {
            logger.warn({
                label,
                message: `Snatching ${candidate.name} from ${candidate.tracker} stopped after attempt ${progress}, Retry-After of ${retryAfterMs / 1000}s exceeds timeout: ${snatchError}${extra ? ` - ${extra}` : ""}`,
            });
            return resultOfErr(snatchError);
        }
        const delayMs = Math.max(options.delayMs, retryAfterMs ?? 0);
        logger.error({
            label,
            message: `Snatch attempt ${progress} from ${candidate.tracker} for ${candidate.name} failed${i < retries ? `, retrying in ${delayMs / 1000}s` : ""}: ${snatchError}${extra ? ` - ${extra}` : ""}`,
        });
        if (i >= retries)
            break;
        await wait(delayMs);
    }
    return resultOfErr(snatchError);
}
function buildTorrentSaveName(mediaType, tracker, name, infoHash, ext) {
    return `[${mediaType}][${tracker}]${name}[${infoHash}]${ext}`;
}
/**
 * Be sure to update parseMetadataFromFilename if changing the format
 */
export function getTorrentSavePath(meta, mediaType, tracker, dir, options) {
    const fullName = stripExtension(meta.getFileSystemSafeName());
    const ext = options.cached ? ".cached.torrent" : ".torrent";
    const fullPath = join(dir, buildTorrentSaveName(mediaType, tracker, fullName, meta.infoHash, ext));
    if (Buffer.byteLength(fullPath, "utf8") <= MAX_PATH_BYTES)
        return fullPath;
    const codePoints = Array.from(fullName);
    let currBytes = Buffer.byteLength(`${fullPath}...`, "utf8");
    let codePointsToRemove = 0;
    for (let i = codePoints.length - 1; i >= 0; i--) {
        codePointsToRemove++;
        currBytes -= Buffer.byteLength(codePoints[i], "utf8");
        if (currBytes <= MAX_PATH_BYTES)
            break;
    }
    const safeName = `${codePoints.slice(0, -codePointsToRemove).join("")}...`;
    const safePath = join(dir, buildTorrentSaveName(mediaType, tracker, safeName, meta.infoHash, ext));
    return safePath; // Handle the error on save if it exists
}
/**
 * Depends on getTorrentSavePath format
 */
export function parseMetadataFromFilename(filename) {
    const match = filename.match(SAVED_TORRENTS_INFO_REGEX);
    if (!match) {
        return {};
    }
    const mediaType = match.groups.mediaType;
    if (!Object.values(MediaType).includes(mediaType)) {
        return {};
    }
    const { tracker, name, infoHash, cached } = match.groups;
    return { mediaType, tracker, name, infoHash, cached: !!cached };
}
export async function findAllTorrentFilesInDir(dir) {
    return (await readdir(dir))
        .filter((fn) => extname(fn) === ".torrent")
        .sort()
        .map((fn) => resolve(join(dir, fn)));
}
export function createEnsemblePieces(title, files) {
    const episodeKeys = getEpisodeKeys(stripExtension(title));
    if (!episodeKeys)
        return null;
    const element = episodeKeys.episode;
    const largestFile = getLargestFile(files);
    return episodeKeys.keyTitles.map((keyTitle) => {
        const key = `${keyTitle}${episodeKeys.season ? `.${episodeKeys.season}` : ""}`;
        return { key, element, largestFile };
    });
}
export async function cacheEnsembleTorrentEntry(searchee, torrentSavePaths) {
    const ensemblePieces = createEnsemblePieces(searchee.title, searchee.files);
    if (!ensemblePieces || !ensemblePieces.length)
        return null;
    let savePath;
    if (searchee.savePath) {
        savePath = searchee.savePath;
    }
    else if (torrentSavePaths) {
        savePath = torrentSavePaths.get(searchee.infoHash);
    }
    else {
        const downloadDirResult = await getClients()[0].getDownloadDir(searchee, {
            onlyCompleted: false,
        });
        if (downloadDirResult.isErr()) {
            logger.error(`Failed to get download dir for ${getLogString(searchee)}: ${downloadDirResult.unwrapErr()}`);
            return null;
        }
        savePath = downloadDirResult.unwrap();
    }
    if (!savePath) {
        logger.error(`Failed to get save path for ${getLogString(searchee)}`);
        return null;
    }
    return ensemblePieces.map((ensemblePiece) => ({
        client_host: searchee.clientHost ?? null,
        path: join(savePath, ensemblePiece.largestFile.path),
        info_hash: searchee.infoHash,
        ensemble: ensemblePiece.key,
        element: ensemblePiece.element,
    }));
}
async function indexTorrents(options) {
    const { seasonFromEpisodes, torrentDir, useClientTorrents } = getRuntimeConfig();
    if (!useClientTorrents && !torrentDir)
        return;
    const clients = getClients();
    let searchees;
    let infoHashPathMap;
    if (options.startup) {
        if (torrentDir) {
            logger.info({
                label: Label.INDEX,
                message: "Indexing torrentDir for reverse lookup...",
            });
            try {
                searchees = await loadTorrentDirLight(torrentDir);
            }
            catch (error) {
                if (!isRecoverableTorrentDirError(error))
                    throw error;
                logRecoverableTorrentDirError(torrentDir, error);
                searchees = [];
            }
            if (clients.length) {
                infoHashPathMap = await clients[0].getAllDownloadDirs({
                    metas: searchees,
                    onlyCompleted: false,
                    v1HashOnly: true,
                });
            }
        }
        else {
            logger.info("Indexing client torrents for reverse lookup...");
            searchees = await flatMapAsync(clients, async (client) => {
                try {
                    const { searchees } = await client.getClientSearchees({
                        includeFiles: true,
                        includeTrackers: true,
                    });
                    return searchees;
                }
                catch (e) {
                    const message = e instanceof Error ? e.message : String(e ?? "");
                    logger.warn({
                        label: client.label,
                        message: `Indexing client torrents failed; disabling client until next retry: ${message}`,
                    });
                    logger.debug(e);
                    return [];
                }
            });
        }
    }
    else {
        if (torrentDir) {
            try {
                searchees = await indexTorrentDir(torrentDir);
            }
            catch (error) {
                if (!isRecoverableTorrentDirError(error))
                    throw error;
                logRecoverableTorrentDirError(torrentDir, error);
                searchees = [];
            }
        }
        else {
            searchees = await flatMapAsync(clients, async (client) => {
                try {
                    return (await client.getClientSearchees({
                        newSearcheesOnly: true,
                        includeFiles: true,
                        includeTrackers: true,
                    })).newSearchees;
                }
                catch (e) {
                    const message = e instanceof Error ? e.message : String(e ?? "");
                    logger.warn({
                        label: client.label,
                        message: `Indexing client torrents failed; disabling client until next retry: ${message}`,
                    });
                    logger.debug(e);
                    return [];
                }
            });
        }
    }
    if (!seasonFromEpisodes)
        return;
    const ensembleRows = await flatMapAsync(searchees, async (searchee) => (await cacheEnsembleTorrentEntry(searchee, infoHashPathMap)) ?? []);
    await inBatches(ensembleRows, async (batch) => {
        await db("ensemble")
            .insert(batch)
            .onConflict(["client_host", "path"])
            .merge();
    });
}
async function indexTorrentDir(dir) {
    const dirContents = new Set(await findAllTorrentFilesInDir(dir));
    const newSearchees = [];
    for (const filePath of dirContents) {
        const doesAlreadyExist = await db("torrent")
            .select("id")
            .where({ file_path: filePath })
            .first();
        if (doesAlreadyExist)
            continue;
        let meta;
        try {
            meta = await parseTorrentFromPath(filePath);
        }
        catch (e) {
            logOnce(`Failed to parse ${filePath}`, () => {
                logger.error({
                    label: Label.INDEX,
                    message: `Failed to parse ${filePath}`,
                });
                logger.debug(e);
            });
            continue;
        }
        await db("torrent")
            .insert({
            file_path: filePath,
            info_hash: meta.infoHash,
            name: meta.name,
        })
            .onConflict("file_path")
            .ignore();
        const res = await createSearcheeFromTorrentFile(filePath, []);
        if (res.isOk())
            newSearchees.push(res.unwrap());
    }
    const rows = await db("torrent").select("file_path", "info_hash");
    const toDelete = rows.filter((row) => !dirContents.has(row.file_path));
    await inBatches(toDelete, async (batch) => {
        await db("torrent")
            .whereIn("file_path", batch.map((row) => row.file_path))
            .del();
        await db("ensemble")
            .whereIn("info_hash", batch.map((row) => row.info_hash))
            .del();
    });
    return newSearchees;
}
export async function indexTorrentsAndDataDirs(options = { startup: false }) {
    if (options.startup) {
        const { dataDirs, seasonFromEpisodes, torrentDir, useClientTorrents } = getRuntimeConfig();
        if (!useClientTorrents) {
            const hashes = (await db("client_searchee").select("info_hash")).map((r) => r.info_hash);
            await inBatches(hashes, async (batch) => {
                await db("ensemble").whereIn("info_hash", batch).del();
            });
            await db("client_searchee").del();
        }
        else {
            const clientHosts = getClients().map((client) => client.clientHost);
            await db("client_searchee")
                .whereNotIn("client_host", clientHosts)
                .del();
            await db("ensemble")
                .whereNotNull("client_host")
                .whereNotIn("client_host", clientHosts)
                .del();
        }
        if (!torrentDir) {
            const hashes = (await db("torrent").select("info_hash")).map((r) => r.info_hash);
            await inBatches(hashes, async (batch) => {
                await db("ensemble").whereIn("info_hash", batch).del();
            });
            await db("torrent").del();
        }
        if (!dataDirs.length) {
            const paths = (await db("data").select("path")).map((r) => r.path);
            await inBatches(paths, async (batch) => {
                await db("ensemble")
                    .whereIn("path", batch)
                    .whereNull("client_host")
                    .del();
            });
            await db("data").del();
        }
        else {
            const paths = (await db("data").select("path")).map((r) => r.path);
            const toDelete = paths.filter((p) => !isChildPath(p, dataDirs));
            await inBatches(toDelete, async (batch) => {
                await db("data").whereIn("path", batch).del();
                await db("ensemble")
                    .whereIn("path", batch)
                    .whereNull("client_host")
                    .del();
            });
        }
        if (!seasonFromEpisodes)
            await db("ensemble").del();
    }
    return withMutex(Mutex.INDEX_TORRENTS_AND_DATA_DIRS, { useQueue: false }, async () => {
        const maxRetries = 3;
        for (let attempt = 1; attempt <= maxRetries; attempt++) {
            try {
                await indexDataDirs(options); // Running together may increase failures
                await indexTorrents(options); // Run second so this data is more fresh
                break;
            }
            catch (e) {
                const log = {
                    label: Label.INDEX,
                    message: `Indexing failed (${maxRetries - attempt}): ${e.message}`,
                };
                logger.debug(e);
                if (attempt < maxRetries) {
                    logger.verbose(log);
                }
                else {
                    logger.error(log);
                    if (options.startup)
                        throw e;
                }
            }
        }
    });
}
export async function getInfoHashesToExclude() {
    const { useClientTorrents } = getRuntimeConfig();
    const database = useClientTorrents ? db("client_searchee") : db("torrent");
    return new Set((await database.select("*")).map((e) => e.info_hash));
}
export async function loadTorrentDirLight(torrentDir) {
    const torrentFilePaths = await findAllTorrentFilesInDir(torrentDir);
    const searchees = [];
    const client = getClients()[0];
    const torrentInfos = client && client.clientType !== Label.QBITTORRENT
        ? await client.getAllTorrents()
        : [];
    for (const torrentFilePath of torrentFilePaths) {
        const searcheeResult = await createSearcheeFromTorrentFile(torrentFilePath, torrentInfos);
        if (searcheeResult.isOk()) {
            searchees.push(searcheeResult.unwrap());
        }
    }
    return searchees;
}
function getKeysFromName(stem) {
    const episodeKeys = getEpisodeKeys(stem);
    if (episodeKeys) {
        const keyTitles = episodeKeys.keyTitles;
        const element = `${episodeKeys.season ? `${episodeKeys.season}.` : ""}${episodeKeys.episode}`;
        return { keyTitles, element, useFallback: false };
    }
    const seasonKeys = getSeasonKeys(stem);
    if (seasonKeys) {
        const keyTitles = seasonKeys.keyTitles;
        const element = seasonKeys.season;
        return { keyTitles, element, useFallback: false };
    }
    const movieKeys = getMovieKeys(stem);
    if (movieKeys) {
        const keyTitles = movieKeys.keyTitles;
        return { keyTitles, useFallback: false };
    }
    const animeKeys = getAnimeKeys(stem);
    if (animeKeys) {
        const keyTitles = animeKeys.keyTitles;
        const element = animeKeys.release;
        return { keyTitles, element, useFallback: false };
    }
    return { keyTitles: [], useFallback: true };
}
export async function getSimilarByName(name) {
    const { torrentDir, useClientTorrents } = getRuntimeConfig();
    const stem = stripExtension(name);
    const { keyTitles, element, useFallback } = getKeysFromName(stem);
    const clientSearchees = useFallback
        ? await getTorrentByFuzzyName(stem)
        : [];
    const dataSearchees = useFallback ? await getDataByFuzzyName(stem) : [];
    if (!keyTitles.length) {
        const noParentheses = stem
            .replace(ALL_SQUARE_BRACKETS_REGEX, "")
            .replace(ALL_PARENTHESES_REGEX, "")
            .replace(ALL_SPACES_REGEX, " ")
            .trim(); // Anime that fails MediaType.ANIME often has `[group] Title (Extra Info)`
        if (noParentheses.length >= MIN_VIDEO_QUERY_LENGTH &&
            noParentheses !== stem) {
            clientSearchees.push(...(await getTorrentByFuzzyName(noParentheses)));
            dataSearchees.push(...(await getDataByFuzzyName(noParentheses)));
        }
        return { keys: [], clientSearchees, dataSearchees };
    }
    const candidateMaxDistance = Math.floor(Math.min(...keyTitles.map((t) => t.length)) / LEVENSHTEIN_DIVISOR);
    const filterEntries = async (dbEntries) => {
        return filterAsyncYield(dbEntries, async (dbEntry) => {
            const entry = getKeysFromName(dbEntry.title ?? dbEntry.name);
            if (entry.element !== element)
                return false;
            if (!entry.keyTitles.length)
                return false;
            const maxDistance = Math.min(candidateMaxDistance, Math.floor(Math.min(...entry.keyTitles.map((t) => t.length)) /
                LEVENSHTEIN_DIVISOR));
            return entry.keyTitles.some((dbKeyTitle) => {
                return keyTitles.some((keyTitle) => distance(keyTitle, dbKeyTitle) <= maxDistance);
            });
        });
    };
    if (useClientTorrents) {
        clientSearchees.push(...(await filterEntries(await db("client_searchee"))).map(createSearcheeFromDB));
    }
    else if (torrentDir) {
        const filteredTorrentEntries = (await filterEntries(await db("torrent").select("name", "file_path")));
        if (filteredTorrentEntries.length) {
            const client = getClients()[0];
            const torrentInfos = client && client.clientType !== Label.QBITTORRENT
                ? await client.getAllTorrents()
                : [];
            clientSearchees.push(...(await mapAsync(filteredTorrentEntries, (dbTorrent) => createSearcheeFromTorrentFile(dbTorrent.file_path, torrentInfos)))
                .filter(isOk)
                .map((r) => r.unwrap()));
        }
    }
    const entriesToDelete = [];
    const filteredDataEntries = await filterAsync((await filterEntries(await db("data"))), async ({ path }) => {
        if ((await notExists(path)) ||
            shouldIgnorePathHeuristically(path, (await stat(path)).isDirectory())) {
            entriesToDelete.push(path);
            return false;
        }
        return true;
    });
    await inBatches(entriesToDelete, async (batch) => {
        await db("data").whereIn("path", batch).del();
        await db("ensemble").whereIn("path", batch).del();
    });
    if (filteredDataEntries.length) {
        dataSearchees.push(...(await mapAsync(filteredDataEntries, (dbData) => createSearcheeFromPath(dbData.path)))
            .filter(isOk)
            .map((r) => r.unwrap()));
    }
    const keys = element
        ? keyTitles.map((keyTitle) => `${keyTitle}.${element}`)
        : keyTitles;
    return { keys, clientSearchees, dataSearchees };
}
async function getTorrentByFuzzyName(stem) {
    const { useClientTorrents } = getRuntimeConfig();
    const database = useClientTorrents
        ? await db("client_searchee")
        : await db("torrent");
    const fullMatch = createKeyTitle(stem);
    // Attempt to filter torrents in DB to match incoming data before fuzzy check
    let filteredNames = [];
    if (fullMatch) {
        filteredNames = await filterAsyncYield(database, async (dbEntry) => {
            const dbMatch = createKeyTitle(stripExtension(dbEntry.title ?? dbEntry.name));
            return fullMatch === dbMatch;
        });
    }
    // If none match, proceed with fuzzy name check on all names.
    filteredNames = filteredNames.length > 0 ? filteredNames : database;
    const candidateMaxDistance = Math.floor(stem.length / LEVENSHTEIN_DIVISOR);
    const potentialMatches = await filterAsyncYield(filteredNames, async (dbEntry) => {
        const dbTitle = stripExtension(dbEntry.title ?? dbEntry.name);
        const maxDistance = Math.min(candidateMaxDistance, Math.floor(dbTitle.length / LEVENSHTEIN_DIVISOR));
        return distance(stem, dbTitle) <= maxDistance;
    });
    if (useClientTorrents)
        return potentialMatches.map(createSearcheeFromDB);
    const client = getClients()[0];
    const torrentInfos = client && client.clientType !== Label.QBITTORRENT
        ? await client.getAllTorrents()
        : [];
    return (await mapAsync(potentialMatches, (dbTorrent) => createSearcheeFromTorrentFile(dbTorrent.file_path, torrentInfos)))
        .filter(isOk)
        .map((r) => r.unwrap());
}
export async function getTorrentByCriteria(criteria) {
    const { useClientTorrents } = getRuntimeConfig();
    const database = useClientTorrents ? db("client_searchee") : db("torrent");
    const dbTorrents = await database.where((b) => b.where({ info_hash: criteria.infoHash }));
    if (!dbTorrents.length) {
        const message = `Torrent client does not have any torrent with criteria ${inspect(criteria)}`;
        throw new Error(message);
    }
    if (useClientTorrents)
        return dbTorrents.map(createSearcheeFromDB);
    const client = getClients()[0];
    const torrentInfos = client && client.clientType !== Label.QBITTORRENT
        ? await client.getAllTorrents()
        : [];
    return [
        (await createSearcheeFromTorrentFile(dbTorrents[0].file_path, torrentInfos)).unwrapOrThrow(new Error(`Failed to create searchee from ${dbTorrents[0].file_path}`)),
    ];
}
//# sourceMappingURL=torrent.js.map