import { promises as fs } from "fs";
import path from "path";
const TEXT_MIME_TYPES = {
    ".json": "application/json",
};
const DEFAULT_TEXT_MIME_TYPE = "text/plain";
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);
    if (!stats.isDirectory()) {
        throw new Error("Requested path is not a directory");
    }
    const dirents = await fs.readdir(directoryPath, { withFileTypes: true });
    const entriesWithNulls = await Promise.all(dirents.map(async (dirent) => {
        const targetPath = path.join(directoryPath, 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)) {
                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 }),
        entries,
    };
}
export async function readExplorerFile({ root, relativePath, }) {
    const filePath = await resolveScopedPath({ root, relativePath });
    const stats = await fs.stat(filePath);
    if (!stats.isFile()) {
        throw new Error("Requested path is not a file");
    }
    const ext = path.extname(filePath).toLowerCase();
    const basePayload = {
        path: normalizeRelativePath({ root, targetPath: filePath }),
        size: stats.size,
        modifiedAt: stats.mtime.toISOString(),
    };
    if (ext in IMAGE_MIME_TYPES) {
        const buffer = await fs.readFile(filePath);
        return {
            ...basePayload,
            kind: "image",
            encoding: "base64",
            content: buffer.toString("base64"),
            mimeType: IMAGE_MIME_TYPES[ext],
        };
    }
    const buffer = await fs.readFile(filePath);
    if (isLikelyBinary(buffer)) {
        return {
            ...basePayload,
            kind: "binary",
            encoding: "none",
            mimeType: "application/octet-stream",
        };
    }
    return {
        ...basePayload,
        kind: "text",
        encoding: "utf-8",
        content: buffer.toString("utf-8"),
        mimeType: textMimeTypeForExtension(ext),
    };
}
export async function getDownloadableFileInfo({ root, relativePath }) {
    const filePath = await resolveScopedPath({ root, relativePath });
    const stats = await fs.stat(filePath);
    if (!stats.isFile()) {
        throw new Error("Requested path is not a file");
    }
    const ext = path.extname(filePath).toLowerCase();
    let mimeType = "application/octet-stream";
    if (ext in IMAGE_MIME_TYPES) {
        mimeType = IMAGE_MIME_TYPES[ext];
    }
    else {
        // Read only a small prefix to classify likely text vs binary.
        const handle = await fs.open(filePath, "r");
        const sample = Buffer.alloc(8192);
        try {
            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);
            }
        }
        finally {
            await handle.close();
        }
    }
    return {
        path: normalizeRelativePath({ root, targetPath: filePath }),
        absolutePath: filePath,
        fileName: path.basename(filePath),
        mimeType,
        size: stats.size,
    };
}
async function resolveScopedPath({ root, relativePath = "." }) {
    const normalizedRoot = path.resolve(root);
    const requestedPath = path.resolve(normalizedRoot, relativePath);
    const relative = path.relative(normalizedRoot, requestedPath);
    if (relative !== "" && (relative.startsWith("..") || path.isAbsolute(relative))) {
        throw new Error("Access outside of workspace is not allowed");
    }
    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 of workspace is not allowed");
        }
        return requestedPath;
    }
    catch (error) {
        if (isMissingEntryError(error)) {
            return requestedPath;
        }
        throw error;
    }
}
async function buildEntryPayload({ root, targetPath, name, kind, }) {
    const stats = await fs.stat(targetPath);
    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 normalizeRelativePath({ root, targetPath }) {
    const normalizedRoot = path.resolve(root);
    const normalizedTarget = path.resolve(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