import * as fsp from "node:fs/promises";
import { NotFoundError, unwrapResponseDto } from "@superblocksteam/shared";
import { ExportViewMode, } from "@superblocksteam/shared";
import { getLogger } from "../util/logger.js";
import { hashLocalDirectory } from "./hash-dir-tree.js";
// TODO(code-mode): this is re-implemented in the sdk package
// because SDK depends on vite-plugin-file-sync, but can't be circular
export async function getApplicationDirectoryHash(rpcClient, applicationId, branch, localDirectoryPath) {
    const [draftOrLiveEditHash, localDirectoryStatePreDownload] = await Promise.all([
        getDraftOrLiveDirectoryHash(rpcClient, applicationId, branch),
        hashLocalDirectory(localDirectoryPath),
    ]);
    const directoryHash = draftOrLiveEditHash;
    if (!directoryHash) {
        throw new Error("No live edit hash found");
    }
    return {
        serverDirectoryHash: directoryHash,
        localDirectoryStatePreDownload,
    };
}
export async function downloadApplicationDirectory({ rpcClient, applicationId, branch, localDirectoryPath, hashCache, }) {
    const { serverDirectoryHash, localDirectoryStatePreDownload } = await getApplicationDirectoryHash(rpcClient, applicationId, branch, localDirectoryPath);
    const directoryState = await doDownloadApplicationDirectory(rpcClient, serverDirectoryHash, localDirectoryStatePreDownload, localDirectoryPath);
    // Now that the directory has been downloaded, we know that all of its hashes are in the server,
    // so we can add them to the hash cache
    hashCache.add_directory_recursive(directoryState);
    return serverDirectoryHash;
}
/**
 * Incrementally download the directory contents, skipping any files that are already
 * present locally, based on the old local directory state.
 * Returns the new local directory state.
 *
 * @param rpcClient - The RPC client to use to get the directory contents from the server.
 * @param directoryHash - The hash of the directory to download.
 * @param oldLocalDirState - The old local directory state.
 * @param localDirectoryPath - The path to the local directory.
 * @returns The new local directory state.
 */
async function doDownloadApplicationDirectory(rpcClient, directoryHash, oldLocalDirState, localDirectoryPath) {
    // keep track of the hashes we need to get from the server and save locally
    const hashesToGet = {};
    const doRecursive = async (rpcClient, directoryHash, oldLocalDirState, localDirectoryPath, hashesToGet) => {
        if (oldLocalDirState?.hash === directoryHash) {
            // The directory is already up to date, we don't need to download anything and we can return the old local directory state
            return oldLocalDirState;
        }
        const newStateContents = [];
        // FIXME: @joeyagreco - this is slow!
        // FIXME: @joeyagreco - look into having this be a single call to the server beforehand or implement pagination of some sort
        const directoryContentsResponse = await unwrapResponseDto(rpcClient.call.v1.dbfs.directoryContents.get({ hash: directoryHash }));
        // Create a map of the local directory contents
        // As we process the directory entries, we will be removing them from the map so that the entries left will
        // correspond to files that have been deleted
        const localDirMap = new Map(oldLocalDirState?.contents.map((entry) => [entry.name, entry]) ?? []);
        for (const entry of directoryContentsResponse.contents) {
            const localFilePath = `${localDirectoryPath}/${entry.name}`;
            // The entry has been matched, remove it from the map, if it exists
            let localEntry = localDirMap.get(entry.name);
            localDirMap.delete(entry.name);
            if (localEntry !== undefined && localEntry.type !== entry.type) {
                // The file type has changed, the file is a regular file but the local file is not, so we need to delete the local file
                // because we will not be able to overwrite it otherwise
                await fsp.rm(localFilePath, { recursive: true });
                localEntry = undefined;
            }
            // At this point, if `localEntry` is defined, it is guaranteed to have the same type as `entry`.
            // TypeScript will not be able to infer this, so we need to cast it to the correct type in each case.
            switch (entry.type) {
                case "-": {
                    newStateContents.push(entry);
                    const localEntryTyped = localEntry;
                    if (localEntryTyped?.hash !== entry.hash) {
                        hashesToGet[entry.hash] = { localFilePath };
                        const fileContentsResponse = await unwrapResponseDto(rpcClient.call.v1.dbfs.fileContents.get({ hash: entry.hash }));
                        const fileContents = Buffer.from(fileContentsResponse.contents, "base64");
                        await fsp.writeFile(localFilePath, fileContents);
                    }
                    break;
                }
                case "l": {
                    newStateContents.push(entry);
                    const localEntryTyped = localEntry;
                    if (localEntryTyped?.target !== entry.target) {
                        await fsp.symlink(entry.target, localFilePath);
                    }
                    break;
                }
                case "d": {
                    const localEntryTyped = localEntry;
                    if (localEntryTyped === undefined) {
                        await fsp.mkdir(localFilePath, { recursive: true }); // don't fail if directory exists
                    }
                    const localSubDirState = localEntryTyped === undefined
                        ? undefined
                        : {
                            hash: localEntryTyped.hash,
                            contents: localEntryTyped.contents,
                        };
                    const newSubDirState = await doRecursive(rpcClient, entry.hash, localSubDirState, localFilePath, hashesToGet);
                    newStateContents.push({
                        ...entry,
                        contents: newSubDirState.contents,
                    });
                    break;
                }
                default: {
                    throw new Error(`Unknown entry type: ${entry}`);
                }
            }
        }
        // Any remaining entries in the map are files/directories that have been deleted
        for (const localEntry of localDirMap.values()) {
            const localFilePath = `${localDirectoryPath}/${localEntry.name}`;
            await fsp.rm(localFilePath, { recursive: true });
        }
        return {
            hash: directoryHash,
            contents: newStateContents,
        };
    };
    const newLocalDirState = await doRecursive(rpcClient, directoryHash, oldLocalDirState, localDirectoryPath, hashesToGet);
    // now that we have all the hashes, we can do the work in parallel batches
    // NOTE: @joeyagreco - i do not know what a good number would be here, we can tweak as we see fit
    const DOWNLOAD_BATCH_SIZE = 50;
    const entries = Object.entries(hashesToGet);
    for (let i = 0; i < entries.length; i += DOWNLOAD_BATCH_SIZE) {
        const batch = entries.slice(i, i + DOWNLOAD_BATCH_SIZE);
        await Promise.all(batch.map(async ([hash, { localFilePath }]) => {
            const fileContentsResponse = await unwrapResponseDto(rpcClient.call.v1.dbfs.fileContents.get({ hash }));
            const fileContents = Buffer.from(fileContentsResponse.contents, "base64");
            await fsp.writeFile(localFilePath, fileContents);
        }));
    }
    return newLocalDirState;
}
export async function getDraftOrLiveDirectoryHash(rpcClient, applicationId, branch) {
    try {
        const response = await unwrapResponseDto(rpcClient.call.v3.application.directoryContents.get({
            applicationId,
            branchName: branch,
            viewMode: ExportViewMode.EXPORT_DRAFT,
        }));
        return response.hash;
    }
    catch (e) {
        if (e instanceof NotFoundError) {
            const response = await unwrapResponseDto(rpcClient.call.v3.application.directoryContents.get({
                applicationId,
                branchName: branch,
                viewMode: ExportViewMode.EXPORT_LIVE,
            }));
            getLogger().warn(`Draft state not found, using live edit hash: ${response.hash}`);
            return response.hash;
        }
        throw e;
    }
}
//# sourceMappingURL=download.js.map