import * as fsp from "node:fs/promises";
import * as path from "node:path";
import { createPatch } from "diff";
const EDIT_EXT = ".sbswap.edit";
const ADD_EXT = ".sbswap.add";
const DELETE_EXT = ".sbswap.delete";
/**
 * Gets the marker file path for a given file path and app root directory.
 * Marker files are stored in .superblocks/drafts/ with the same relative structure.
 *
 * @param filePath - The absolute path to the file.
 * @param appRootDirPath - The absolute path to the app root directory.
 * @param markerType - The type of marker file ('.sbswap.edit' or '.sbswap.add').
 * @returns The absolute path to the marker file.
 */
function getMarkerFilePath(filePath, appRootDirPath, markerType) {
    const relativePath = path.relative(appRootDirPath, filePath);
    return path.join(getDraftsDirectoryPath(appRootDirPath), `${relativePath}${markerType}`);
}
/**
 * Gets the drafts directory path for the app root.
 *
 * @param appRootDirPath - The absolute path to the app root directory.
 * @returns The absolute path to the drafts directory.
 */
function getDraftsDirectoryPath(appRootDirPath) {
    return path.join(appRootDirPath, ".superblocks", "drafts");
}
async function getMarkerState(filePath, appRootDirPath) {
    const editMarkerPath = getMarkerFilePath(filePath, appRootDirPath, EDIT_EXT);
    const addMarkerPath = getMarkerFilePath(filePath, appRootDirPath, ADD_EXT);
    const deleteMarkerPath = getMarkerFilePath(filePath, appRootDirPath, DELETE_EXT);
    let state = "NEW";
    try {
        await fsp.access(filePath);
        state = "FILE_EXISTS";
    }
    catch {
        //
    }
    try {
        await fsp.access(editMarkerPath);
        state = "EDIT_MARKER";
    }
    catch {
        //
    }
    try {
        await fsp.access(addMarkerPath);
        state = "ADD_MARKER";
    }
    catch {
        //
    }
    try {
        await fsp.access(deleteMarkerPath);
        state = "DELETE_MARKER";
    }
    catch {
        //
    }
    return state;
}
/**
 * Creates a draft edit or addition marker file only.
 *
 * @param filePath - The absolute path to the file for which to create the respective marker.
 * @param appRootDirPath - The absolute path to the app root directory.
 */
export async function createDraftMarkerOnly(filePath, appRootDirPath) {
    const state = await getMarkerState(filePath, appRootDirPath);
    switch (state) {
        case "FILE_EXISTS": {
            const editMarkerPath = getMarkerFilePath(filePath, appRootDirPath, EDIT_EXT);
            // Ensure the drafts directory structure exists
            const editMarkerDir = path.dirname(editMarkerPath);
            await fsp.mkdir(editMarkerDir, { recursive: true });
            // Copy original file to .sbswap.edit marker in drafts directory
            await fsp.copyFile(filePath, editMarkerPath);
            return;
        }
        case "NEW": {
            const addMarkerPath = getMarkerFilePath(filePath, appRootDirPath, ADD_EXT);
            // Ensure the drafts directory structure exists
            const addMarkerDir = path.dirname(addMarkerPath);
            await fsp.mkdir(addMarkerDir, { recursive: true });
            // Create marker to indicate this is a draft addition
            await fsp.writeFile(addMarkerPath, "");
            return;
        }
        case "EDIT_MARKER":
        case "ADD_MARKER":
        default:
            // we already have a marker file, nothing to do
            break;
    }
}
/**
 * Creates a draft edit or addition based on the presence of an existing file.
 * This makes the draft content visible to the build system while preserving the original.
 *
 * @param filePath - The absolute path to the file to edit or add.
 * @param draftContent - The content of the draft edit or addition.
 * @param appRootDirPath - The absolute path to the app root directory.
 * @returns A promise that resolves to "edit" if the file already exists, or "add" if the file does not exist.
 */
export async function createDraft(filePath, draftContent, appRootDirPath) {
    const state = await getMarkerState(filePath, appRootDirPath);
    switch (state) {
        case "ADD_MARKER": {
            // This is an iteration on an existing draft addition - just update content
            const dirPath = path.dirname(filePath);
            await fsp.mkdir(dirPath, { recursive: true });
            await fsp.writeFile(filePath, draftContent);
            return "add";
        }
        case "EDIT_MARKER": {
            // This is an iteration on an existing draft edit - just update content
            const dirPath = path.dirname(filePath);
            await fsp.mkdir(dirPath, { recursive: true });
            await fsp.writeFile(filePath, draftContent);
            return "edit";
        }
        case "DELETE_MARKER": {
            // File was previously deleted as a draft, but now we're writing to it again
            // Convert the DELETE marker to an EDIT marker to preserve the original backup
            const deleteMarkerPath = getMarkerFilePath(filePath, appRootDirPath, DELETE_EXT);
            const editMarkerPath = getMarkerFilePath(filePath, appRootDirPath, EDIT_EXT);
            try {
                // Move the backup from .sbswap.delete to .sbswap.edit
                await fsp.rename(deleteMarkerPath, editMarkerPath);
            }
            catch {
                // If marker doesn't exist or rename fails, continue anyway
                // The file will be treated as a new draft edit without a backup
            }
            // Write new content to the file
            const dirPath = path.dirname(filePath);
            await fsp.mkdir(dirPath, { recursive: true });
            await fsp.writeFile(filePath, draftContent);
            return "edit";
        }
        case "FILE_EXISTS": {
            // This is a new draft edit
            await createDraftEdit(filePath, draftContent, appRootDirPath);
            return "edit";
        }
        case "NEW": {
            // This is a new draft addition
            await createDraftAdd(filePath, draftContent, appRootDirPath);
            return "add";
        }
    }
}
/**
 * Creates a draft edit by copying the original file to the drafts directory and writing draft content to the original location.
 * This makes the draft content visible to the build system while preserving the original.
 * If a draft edit already exists, this will just update the draft content.
 *
 * @param filePath - The absolute path to the file to edit.
 * @param draftContent - The content of the draft edit.
 * @param appRootDirPath - The absolute path to the app root directory.
 * @returns A promise that resolves when the draft edit is created.
 */
export async function createDraftEdit(filePath, draftContent, appRootDirPath) {
    const editMarkerPath = getMarkerFilePath(filePath, appRootDirPath, EDIT_EXT);
    // Check if original file exists
    try {
        await fsp.access(filePath);
    }
    catch {
        throw new Error(`Cannot create draft edit: original file does not exist: ${filePath}`);
    }
    // Check if draft already exists
    try {
        await fsp.access(editMarkerPath);
        // If we get here, the marker file exists, which means draft already exists
        // This is an iteration - just update the draft content
        await fsp.writeFile(filePath, draftContent);
        return;
    }
    catch {
        // Marker file doesn't exist, proceed with normal draft creation
    }
    // Ensure the drafts directory structure exists
    const editMarkerDir = path.dirname(editMarkerPath);
    await fsp.mkdir(editMarkerDir, { recursive: true });
    // Copy original file to .sbswap.edit marker in drafts directory
    await fsp.copyFile(filePath, editMarkerPath);
    // Write draft content to original location
    await fsp.writeFile(filePath, draftContent);
}
/**
 * Creates a draft addition by writing new file content and creating a .sbswap.add marker.
 * This makes the new file visible to the build system while marking it as a draft addition.
 * If a draft addition already exists, this will just update the content.
 *
 * @param filePath - The absolute path where the new file should be created.
 * @param content - The content of the new file.
 * @param appRootDirPath - The absolute path to the app root directory.
 * @returns A promise that resolves when the draft addition is created.
 */
export async function createDraftAdd(filePath, content, appRootDirPath) {
    const addMarkerPath = getMarkerFilePath(filePath, appRootDirPath, ADD_EXT);
    // Check if this is an iteration on an existing draft addition
    try {
        await fsp.access(addMarkerPath);
        // Marker exists, this is an iteration - just update content
        const dirPath = path.dirname(filePath);
        await fsp.mkdir(dirPath, { recursive: true });
        await fsp.writeFile(filePath, content);
        return;
    }
    catch {
        // Marker doesn't exist, proceed with normal draft addition creation
    }
    // Check if file already exists (and it's not a draft)
    try {
        await fsp.access(filePath);
        // If we get here, the file exists, which is not allowed for new draft additions
        throw new Error(`Cannot create draft add: file already exists: ${filePath}`);
    }
    catch (accessError) {
        // If access fails, the file doesn't exist, which is what we want
        if (accessError.message?.includes("Cannot create draft add")) {
            throw accessError; // Re-throw our own error
        }
        // Otherwise, continue - the file doesn't exist, which is expected
    }
    // Ensure directory exists for the new file
    const dirPath = path.dirname(filePath);
    await fsp.mkdir(dirPath, { recursive: true });
    // Ensure the drafts directory structure exists
    const addMarkerDir = path.dirname(addMarkerPath);
    await fsp.mkdir(addMarkerDir, { recursive: true });
    // Write new file content
    await fsp.writeFile(filePath, content);
    // Create marker to indicate this is a draft addition
    await fsp.writeFile(addMarkerPath, "");
}
/**
 * Creates a draft deletion by backing up the file content and removing it from the workspace.
 *
 * @param filePath - The absolute path to the file to delete.
 * @param appRootDirPath - The absolute path to the app root directory.
 * @returns A promise that resolves when the draft deletion is created.
 */
export async function createDraftDelete(filePath, appRootDirPath) {
    const deleteMarkerPath = getMarkerFilePath(filePath, appRootDirPath, DELETE_EXT);
    // Check if file exists
    try {
        await fsp.access(filePath);
    }
    catch {
        throw new Error(`Cannot create draft delete: file does not exist: ${filePath}`);
    }
    // Check if this is an iteration on an existing draft deletion
    try {
        await fsp.access(deleteMarkerPath);
        // Marker exists - file is already deleted, nothing to do
        return;
    }
    catch {
        // Marker doesn't exist, proceed with normal draft deletion
    }
    // Ensure the drafts directory structure exists
    const deleteMarkerDir = path.dirname(deleteMarkerPath);
    await fsp.mkdir(deleteMarkerDir, { recursive: true });
    // Copy original file to .sbswap.delete marker in drafts directory
    await fsp.copyFile(filePath, deleteMarkerPath);
    // Delete the file from the workspace
    await fsp.unlink(filePath);
}
/**
 * Recursively checks if any draft files exist in the drafts directory.
 * Looks for .sbswap.edit, .sbswap.add, and .sbswap.delete marker files.
 *
 * @param appRootDirPath - The absolute path to the app root directory.
 * @returns A promise that resolves to true if any draft files are found, false otherwise.
 */
export async function hasSwapFiles(appRootDirPath) {
    const draftsDir = getDraftsDirectoryPath(appRootDirPath);
    try {
        await fsp.access(draftsDir);
    }
    catch {
        // Drafts directory doesn't exist, so no draft files
        return false;
    }
    return await hasSwapFilesInDirectory(draftsDir, appRootDirPath);
}
/**
 * Helper function to recursively check for swap files with actual changes in a directory.
 * Only returns true for files that have meaningful content differences, not just marker files.
 *
 * @param directoryPath - The path to the directory to scan for draft files.
 * @param appRootDirPath - The absolute path to the app root directory.
 * @returns A promise that resolves to true if any draft files with changes are found, false otherwise.
 */
async function hasSwapFilesInDirectory(directoryPath, appRootDirPath) {
    try {
        const entries = await fsp.readdir(directoryPath, { withFileTypes: true });
        const draftsDir = getDraftsDirectoryPath(appRootDirPath);
        for (const entry of entries) {
            if (entry.isFile()) {
                const swapFilePath = path.join(directoryPath, entry.name);
                if (entry.name.endsWith(EDIT_EXT)) {
                    const markerRelativePath = path.relative(draftsDir, swapFilePath);
                    const originalRelativePath = markerRelativePath.slice(0, -EDIT_EXT.length);
                    const originalPath = path.join(appRootDirPath, originalRelativePath);
                    try {
                        // Should work ok for files up to ~100 MB
                        const [draftContent, originalContent] = await Promise.all([
                            fsp.readFile(swapFilePath, "utf8"),
                            fsp.readFile(originalPath, "utf8"),
                        ]);
                        if (draftContent !== originalContent) {
                            return true;
                        }
                    }
                    catch {
                        // If we can't read files, assume it's a change to be safe
                        return true;
                    }
                }
                else if (entry.name.endsWith(ADD_EXT)) {
                    return true;
                }
                else if (entry.name.endsWith(DELETE_EXT)) {
                    return true;
                }
            }
            if (entry.isDirectory()) {
                const subdirPath = path.join(directoryPath, entry.name);
                if (await hasSwapFilesInDirectory(subdirPath, appRootDirPath)) {
                    return true;
                }
            }
        }
        return false;
    }
    catch (_error) {
        // If directory doesn't exist or can't be read, no draft files
        return false;
    }
}
/**
 * Discards all draft changes by restoring original files and removing draft additions.
 * - For .sbswap.edit files: copies them back to the original location
 * - For .sbswap.add markers: removes both the marker and the corresponding file
 *
 * @param appRootDirPath - The absolute path to the app root directory.
 * @returns A promise that resolves when all drafts have been discarded.
 */
export async function discardAllDrafts(appRootDirPath) {
    const draftsDir = getDraftsDirectoryPath(appRootDirPath);
    try {
        await fsp.access(draftsDir);
    }
    catch {
        // Drafts directory doesn't exist, nothing to discard
        return;
    }
    await discardDraftsInDirectory(draftsDir, appRootDirPath);
    // Clean up the entire drafts directory
    await cleanUpDraftsDirectory(appRootDirPath);
}
export async function cleanUpDraftsDirectory(appRootDirPath) {
    const draftsDir = getDraftsDirectoryPath(appRootDirPath);
    try {
        await fsp.rm(draftsDir, { recursive: true });
    }
    catch {
        // Ignore cleanup errors
    }
}
/**
 * Helper function to recursively discard drafts in a directory.
 *
 * @param directoryPath - The path to the directory to discard drafts from.
 * @param appRootDirPath - The absolute path to the app root directory.
 * @returns A promise that resolves when all drafts have been discarded.
 */
async function discardDraftsInDirectory(directoryPath, appRootDirPath) {
    try {
        const entries = await fsp.readdir(directoryPath, { withFileTypes: true });
        for (const entry of entries) {
            const fullPath = path.join(directoryPath, entry.name);
            if (entry.isFile()) {
                if (entry.name.endsWith(EDIT_EXT)) {
                    // Restore original file
                    const markerRelativePath = path.relative(getDraftsDirectoryPath(appRootDirPath), fullPath);
                    const originalRelativePath = markerRelativePath.slice(0, -EDIT_EXT.length);
                    const originalPath = path.join(appRootDirPath, originalRelativePath);
                    try {
                        // Remove current draft content
                        await fsp.unlink(originalPath);
                    }
                    catch {
                        // File might not exist, that's okay
                    }
                    // Copy original back from drafts directory
                    await fsp.copyFile(fullPath, originalPath);
                }
                else if (entry.name.endsWith(ADD_EXT)) {
                    // Remove draft addition
                    const markerRelativePath = path.relative(getDraftsDirectoryPath(appRootDirPath), fullPath);
                    const addedFileRelativePath = markerRelativePath.slice(0, -ADD_EXT.length);
                    const addedFilePath = path.join(appRootDirPath, addedFileRelativePath);
                    try {
                        await fsp.unlink(addedFilePath);
                    }
                    catch {
                        // File might not exist, that's okay
                    }
                }
                else if (entry.name.endsWith(DELETE_EXT)) {
                    // Restore deleted file
                    const markerRelativePath = path.relative(getDraftsDirectoryPath(appRootDirPath), fullPath);
                    const deletedFileRelativePath = markerRelativePath.slice(0, -DELETE_EXT.length);
                    const deletedFilePath = path.join(appRootDirPath, deletedFileRelativePath);
                    // Ensure directory exists for the restored file
                    const dirPath = path.dirname(deletedFilePath);
                    await fsp.mkdir(dirPath, { recursive: true });
                    // Copy original back from drafts directory
                    await fsp.copyFile(fullPath, deletedFilePath);
                }
            }
            if (entry.isDirectory()) {
                await discardDraftsInDirectory(fullPath, appRootDirPath);
            }
        }
    }
    catch (_error) {
        // Ignore errors reading the directory
    }
}
/**
 * Commits all draft changes by removing .sbswap.edit and .sbswap.add markers.
 * This leaves the current filesystem state as the committed state.
 *
 * @param appRootDirPath - The absolute path to the app root directory.
 * @returns A promise that resolves when all drafts have been committed.
 */
export async function commitAllDrafts(appRootDirPath) {
    const draftsDir = getDraftsDirectoryPath(appRootDirPath);
    try {
        await fsp.access(draftsDir);
    }
    catch {
        // Drafts directory doesn't exist, nothing to commit
        return;
    }
    // Simply remove the entire drafts directory
    try {
        await fsp.rm(draftsDir, { recursive: true });
    }
    catch {
        // Ignore deletion errors
    }
}
/**
 * Gets information about all draft files in the drafts directory.
 *
 * @param appRootDirPath - The absolute path to the app root directory.
 * @returns A promise that resolves to information about draft files.
 */
export async function getDraftInfo(appRootDirPath) {
    const edits = [];
    const additions = [];
    const deletions = [];
    const draftsDir = getDraftsDirectoryPath(appRootDirPath);
    try {
        await fsp.access(draftsDir);
    }
    catch {
        // Drafts directory doesn't exist, no drafts
        return { edits, additions, deletions };
    }
    async function scanDirectory(currentPath) {
        try {
            const entries = await fsp.readdir(currentPath, { withFileTypes: true });
            for (const entry of entries) {
                const fullPath = path.join(currentPath, entry.name);
                if (entry.isFile()) {
                    if (entry.name.endsWith(EDIT_EXT)) {
                        const markerRelativePath = path.relative(draftsDir, fullPath);
                        const originalRelativePath = markerRelativePath.slice(0, -EDIT_EXT.length);
                        edits.push(originalRelativePath);
                    }
                    else if (entry.name.endsWith(ADD_EXT)) {
                        const markerRelativePath = path.relative(draftsDir, fullPath);
                        const addedFileRelativePath = markerRelativePath.slice(0, -ADD_EXT.length);
                        additions.push(addedFileRelativePath);
                    }
                    else if (entry.name.endsWith(DELETE_EXT)) {
                        const markerRelativePath = path.relative(draftsDir, fullPath);
                        const deletedFileRelativePath = markerRelativePath.slice(0, -DELETE_EXT.length);
                        deletions.push(deletedFileRelativePath);
                    }
                }
                if (entry.isDirectory()) {
                    await scanDirectory(fullPath);
                }
            }
        }
        catch {
            // Ignore errors reading directories
        }
    }
    await scanDirectory(draftsDir);
    return { edits, additions, deletions };
}
/**
 * Generates a git-like diff showing all draft changes.
 *
 * @param appRootDirPath - The absolute path to the app root directory.
 * @param specificPaths - An optional array of relative paths to limit the diff to.
 * @returns A promise that resolves to a git-like diff string.
 */
export async function generateDraftDiff(appRootDirPath, specificPaths = []) {
    const draftsDir = getDraftsDirectoryPath(appRootDirPath);
    try {
        await fsp.access(draftsDir);
    }
    catch {
        return "No draft changes found.";
    }
    const diffLines = [];
    let { edits, additions, deletions } = await getDraftInfo(appRootDirPath);
    edits = edits.filter((edit) => specificPaths.length === 0 ||
        specificPaths.some((path) => edit.startsWith(path)));
    additions = additions.filter((add) => specificPaths.length === 0 ||
        specificPaths.some((path) => add.startsWith(path)));
    deletions = deletions.filter((del) => specificPaths.length === 0 ||
        specificPaths.some((path) => del.startsWith(path)));
    if (edits.length === 0 && additions.length === 0 && deletions.length === 0) {
        return "No draft changes found.";
    }
    // Process draft edits
    for (const editPath of edits) {
        const originalPath = path.join(appRootDirPath, editPath);
        const markerPath = getMarkerFilePath(originalPath, appRootDirPath, EDIT_EXT);
        try {
            const currentContent = await fsp.readFile(originalPath, "utf8");
            const originalContent = await fsp.readFile(markerPath, "utf8");
            const patch = createPatch(editPath, originalContent, currentContent);
            diffLines.push(patch);
            diffLines.push(""); // Empty line between files
        }
        catch (_error) {
            // Skip files that can't be read
            continue;
        }
    }
    // Process draft additions
    for (const addPath of additions) {
        const filePath = path.join(appRootDirPath, addPath);
        try {
            const content = await fsp.readFile(filePath, "utf8");
            const patch = createPatch(addPath, "", content);
            diffLines.push(patch);
            diffLines.push(""); // Empty line between files
        }
        catch (_error) {
            // Skip files that can't be read
            continue;
        }
    }
    // Process draft deletions
    // note: this is pretty verbose showing each line as deleted but it matches how we do diffs for added files
    for (const deletePath of deletions) {
        const markerPath = getMarkerFilePath(path.join(appRootDirPath, deletePath), appRootDirPath, DELETE_EXT);
        try {
            const originalContent = await fsp.readFile(markerPath, "utf8");
            const patch = createPatch(deletePath, originalContent, "");
            diffLines.push(patch);
            diffLines.push(""); // Empty line between files
        }
        catch (_error) {
            // Skip files that can't be read
            continue;
        }
    }
    // Remove trailing empty line
    if (diffLines[diffLines.length - 1] === "") {
        diffLines.pop();
    }
    return diffLines.join("\n");
}
//# sourceMappingURL=draft-helpers.js.map