import { constants, promises as fs } from "fs";
import path from "path";
import { expandUserPath, resolvePathFromBase } from "../path-utils.js";
const TEXT_MIME_TYPES = {
    ".json": "application/json",
};
const DEFAULT_TEXT_MIME_TYPE = "text/plain";
const FILE_TYPE_SAMPLE_BYTES = 8192;
const READ_FILE_OPEN_FLAGS = process.platform === "win32" ? constants.O_RDONLY : constants.O_RDONLY | constants.O_NOFOLLOW;
const ACCESS_OUTSIDE_WORKSPACE_MESSAGE = "Access outside of workspace is not allowed";
const IMAGE_MIME_TYPES = {
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".gif": "image/gif",
    ".webp": "image/webp",
    ".svg": "image/svg+xml",
};
export async function listDirectoryEntries({ root, relativePath = ".", }) {
    const directoryPath = await resolveScopedPath({ root, relativePath });
    const stats = await fs.stat(directoryPath.resolvedPath);
    if (!stats.isDirectory()) {
        throw new Error("Requested path is not a directory");
    }
    const dirents = await fs.readdir(directoryPath.resolvedPath, { withFileTypes: true });
    const entriesWithNulls = await Promise.all(dirents.map(async (dirent) => {
        const targetPath = path.join(directoryPath.requestedPath, dirent.name);
        const kind = dirent.isDirectory() ? "directory" : "file";
        try {
            return await buildEntryPayload({
                root,
                targetPath,
                name: dirent.name,
                kind,
            });
        }
        catch (error) {
            // Directories can contain dangling links (e.g. AGENTS.md -> CLAUDE.md).
            // Skip entries whose targets disappeared instead of failing the whole listing.
            if (isMissingEntryError(error) || isOutsideWorkspaceError(error)) {
                return null;
            }
            throw error;
        }
    }));
    const entries = entriesWithNulls.filter((entry) => entry !== null);
    entries.sort((a, b) => {
        const modifiedComparison = new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime();
        if (modifiedComparison !== 0) {
            return modifiedComparison;
        }
        return a.name.localeCompare(b.name);
    });
    return {
        path: normalizeRelativePath({ root, targetPath: directoryPath.requestedPath }),
        entries,
    };
}
export async function readExplorerFile({ root, relativePath, }) {
    const file = await readExplorerFileBytes({ root, relativePath });
    if (file.kind === "image") {
        return {
            path: file.path,
            kind: file.kind,
            encoding: "base64",
            content: Buffer.from(file.bytes).toString("base64"),
            mimeType: file.mimeType,
            size: file.size,
            modifiedAt: file.modifiedAt,
        };
    }
    if (file.kind === "binary") {
        return {
            path: file.path,
            kind: file.kind,
            encoding: "none",
            mimeType: file.mimeType,
            size: file.size,
            modifiedAt: file.modifiedAt,
        };
    }
    return {
        path: file.path,
        kind: file.kind,
        encoding: "utf-8",
        content: Buffer.from(file.bytes).toString("utf-8"),
        mimeType: file.mimeType,
        size: file.size,
        modifiedAt: file.modifiedAt,
    };
}
export async function readExplorerFileBytes({ root, relativePath, }) {
    const filePath = await resolveScopedPath({ root, relativePath });
    const handle = await openFileForRead(filePath.resolvedPath);
    try {
        const stats = await handle.stat();
        if (!stats.isFile()) {
            throw new Error("Requested path is not a file");
        }
        const ext = path.extname(filePath.resolvedPath).toLowerCase();
        const basePayload = {
            path: normalizeRelativePath({ root, targetPath: filePath.requestedPath }),
            size: stats.size,
            modifiedAt: stats.mtime.toISOString(),
        };
        const buffer = await handle.readFile();
        if (ext in IMAGE_MIME_TYPES) {
            return {
                ...basePayload,
                kind: "image",
                encoding: "binary",
                bytes: buffer,
                mimeType: IMAGE_MIME_TYPES[ext],
            };
        }
        if (isLikelyBinary(buffer)) {
            return {
                ...basePayload,
                kind: "binary",
                encoding: "binary",
                bytes: buffer,
                mimeType: "application/octet-stream",
            };
        }
        return {
            ...basePayload,
            kind: "text",
            encoding: "utf-8",
            bytes: buffer,
            mimeType: textMimeTypeForExtension(ext),
        };
    }
    finally {
        await handle.close();
    }
}
export async function getDownloadableFileInfo({ root, relativePath }) {
    const filePath = await resolveScopedPath({ root, relativePath });
    const handle = await openFileForRead(filePath.resolvedPath);
    try {
        const stats = await handle.stat();
        if (!stats.isFile()) {
            throw new Error("Requested path is not a file");
        }
        const ext = path.extname(filePath.resolvedPath).toLowerCase();
        let mimeType = "application/octet-stream";
        if (ext in IMAGE_MIME_TYPES) {
            mimeType = IMAGE_MIME_TYPES[ext];
        }
        else {
            const sample = Buffer.alloc(FILE_TYPE_SAMPLE_BYTES);
            const { bytesRead } = await handle.read(sample, 0, sample.length, 0);
            const chunk = bytesRead < sample.length ? sample.subarray(0, bytesRead) : sample;
            if (!isLikelyBinary(chunk)) {
                mimeType = textMimeTypeForExtension(ext);
            }
        }
        return {
            path: normalizeRelativePath({ root, targetPath: filePath.requestedPath }),
            absolutePath: filePath.resolvedPath,
            fileName: path.basename(filePath.requestedPath),
            mimeType,
            size: stats.size,
        };
    }
    finally {
        await handle.close();
    }
}
async function resolveScopedPath({ root, relativePath = ".", }) {
    const normalizedRoot = expandUserPath(root);
    const requestedPath = resolvePathFromBase(normalizedRoot, relativePath);
    const relative = path.relative(normalizedRoot, requestedPath);
    if (relative !== "" && (relative.startsWith("..") || path.isAbsolute(relative))) {
        throw new Error(ACCESS_OUTSIDE_WORKSPACE_MESSAGE);
    }
    const realRoot = await fs.realpath(normalizedRoot);
    try {
        const realPath = await fs.realpath(requestedPath);
        const realRelative = path.relative(realRoot, realPath);
        if (realRelative !== "" && (realRelative.startsWith("..") || path.isAbsolute(realRelative))) {
            throw new Error(ACCESS_OUTSIDE_WORKSPACE_MESSAGE);
        }
        return { requestedPath, resolvedPath: realPath };
    }
    catch (error) {
        if (isMissingEntryError(error)) {
            return { requestedPath, resolvedPath: requestedPath };
        }
        throw error;
    }
}
async function openFileForRead(filePath) {
    return fs.open(filePath, READ_FILE_OPEN_FLAGS);
}
async function buildEntryPayload({ root, targetPath, name, kind, }) {
    const entryPath = await resolveScopedPath({
        root,
        relativePath: normalizeRelativePath({ root, targetPath }),
    });
    const stats = await fs.stat(entryPath.resolvedPath);
    return {
        name,
        path: normalizeRelativePath({ root, targetPath }),
        kind,
        size: stats.size,
        modifiedAt: stats.mtime.toISOString(),
    };
}
function isMissingEntryError(error) {
    const code = error?.code;
    return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
}
function isOutsideWorkspaceError(error) {
    return error instanceof Error && error.message === ACCESS_OUTSIDE_WORKSPACE_MESSAGE;
}
function normalizeRelativePath({ root, targetPath }) {
    const normalizedRoot = expandUserPath(root);
    const normalizedTarget = expandUserPath(targetPath);
    const relative = path.relative(normalizedRoot, normalizedTarget);
    return relative === "" ? "." : relative.split(path.sep).join("/");
}
function textMimeTypeForExtension(ext) {
    return TEXT_MIME_TYPES[ext] ?? DEFAULT_TEXT_MIME_TYPE;
}
function isLikelyBinary(buffer) {
    if (buffer.length === 0) {
        return false;
    }
    let suspicious = 0;
    for (let idx = 0; idx < buffer.length; idx += 1) {
        const byte = buffer[idx];
        if (byte === 0) {
            return true;
        }
        const isControl = byte < 32 &&
            byte !== 9 && // tab
            byte !== 10 && // newline
            byte !== 13; // carriage return
        if (isControl || byte === 127) {
            suspicious += 1;
        }
    }
    return suspicious / buffer.length > 0.3;
}
//# sourceMappingURL=service.js.map