/**
 * 工作区文件服务
 * 负责在工作区范围内安全地浏览、读取与保存文件。
 */
import { createReadStream, createWriteStream, promises as fs } from 'node:fs';
import path from 'node:path';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { TextDecoder } from 'node:util';
import { createGunzip } from 'node:zlib';
import * as tar from 'tar';
import unzipper from 'unzipper';
const { ZipArchive } = await import('archiver');
const ARCHIVE_LIMITS = {
    maxEntries: 10_000,
    maxFileBytes: 512 * 1024 * 1024,
    maxTotalBytes: 2 * 1024 * 1024 * 1024,
    maxPathDepth: 64,
    maxPathLength: 4096,
};
const GBK_DECODER = new TextDecoder('gbk');
function toDisplayPath(targetPath) {
    return String(targetPath || '').replace(/\\/g, '/');
}
/**
 * 规范化并校验工作区路径。
 */
function resolveWorkspaceRoot(workspacePath) {
    const normalizedWorkspacePath = path.resolve(String(workspacePath || '').trim());
    if (!normalizedWorkspacePath) {
        throw new Error('workspacePath is required');
    }
    return normalizedWorkspacePath;
}
async function resolveWorkspaceRootRealpath(workspacePath) {
    const workspaceRoot = resolveWorkspaceRoot(workspacePath);
    const realWorkspaceRoot = await fs.realpath(workspaceRoot);
    const stat = await fs.stat(realWorkspaceRoot);
    if (!stat.isDirectory()) {
        throw new Error(`Workspace path is not a directory: ${workspaceRoot}`);
    }
    return realWorkspaceRoot;
}
async function resolveWorkspaceRoots(workspacePath) {
    const requestedRoot = resolveWorkspaceRoot(workspacePath);
    return {
        requestedRoot,
        workspaceRoot: await resolveWorkspaceRootRealpath(workspacePath),
    };
}
/**
 * 判断目标路径是否位于工作区内。
 */
function ensureInsideWorkspace(workspaceRoot, targetPath) {
    const resolvedTargetPath = path.resolve(targetPath);
    const relativePath = path.relative(workspaceRoot, resolvedTargetPath);
    const isOutside = relativePath.startsWith('..') || path.isAbsolute(relativePath);
    if (isOutside) {
        throw new Error(`Target path is outside workspace: ${resolvedTargetPath}`);
    }
    return resolvedTargetPath;
}
async function ensureRealPathInsideWorkspace(workspaceRoot, targetPath) {
    const realTargetPath = await fs.realpath(targetPath);
    return ensureInsideWorkspace(workspaceRoot, realTargetPath);
}
async function ensureWritableParentInsideWorkspace(workspaceRoot, targetPath) {
    const realParentPath = await fs.realpath(path.dirname(targetPath));
    ensureInsideWorkspace(workspaceRoot, realParentPath);
    try {
        const targetLinkStat = await fs.lstat(targetPath);
        if (targetLinkStat.isSymbolicLink()) {
            throw new Error(`Refusing to write through symbolic link: ${targetPath}`);
        }
    }
    catch (error) {
        const errorCode = typeof error === 'object' && error !== null ? Reflect.get(error, 'code') : undefined;
        if (errorCode !== 'ENOENT') {
            throw error;
        }
    }
}
function resolveRequestedPathInsideWorkspace(requestedRoot, workspaceRoot, requestedPath) {
    const resolvedRequestedPath = path.resolve(requestedPath);
    const requestedRelativePath = path.relative(requestedRoot, resolvedRequestedPath);
    if (requestedRelativePath.startsWith('..') || path.isAbsolute(requestedRelativePath)) {
        throw new Error(`Target path is outside workspace: ${resolvedRequestedPath}`);
    }
    return ensureInsideWorkspace(workspaceRoot, path.join(workspaceRoot, requestedRelativePath));
}
/**
 * 解析目录目标路径，空路径时回退到工作区根目录。
 */
async function resolveDirectoryTarget({ workspacePath, targetPath = '' }) {
    const { requestedRoot, workspaceRoot } = await resolveWorkspaceRoots(workspacePath);
    const requestedPath = String(targetPath || '').trim();
    const lexicalTargetPath = requestedPath
        ? resolveRequestedPathInsideWorkspace(requestedRoot, workspaceRoot, requestedPath)
        : workspaceRoot;
    const resolvedTargetPath = await ensureRealPathInsideWorkspace(workspaceRoot, lexicalTargetPath);
    return { workspaceRoot, resolvedTargetPath };
}
/**
 * 校验工作区内的条目名称，避免跨目录操作与非法名称。
 */
function normalizeEntryName(rawName) {
    const entryName = String(rawName || '').trim();
    if (!entryName) {
        throw new Error('entryName is required');
    }
    if (entryName === '.' || entryName === '..') {
        throw new Error('entryName is invalid');
    }
    if (entryName.includes('/') || entryName.includes('\\') || path.basename(entryName) !== entryName) {
        throw new Error(`entryName must be a plain file or directory name: ${entryName}`);
    }
    return entryName;
}
/**
 * 修正 multipart 文件名在部分代理链路中被 latin1 解读造成的 UTF-8 中文乱码。
 */
function decodePossiblyLatin1Utf8FileName(rawName) {
    const fileName = String(rawName || '');
    if (!/[\u00c0-\u00ff]/.test(fileName)) {
        return fileName;
    }
    const decodedFileName = Buffer.from(fileName, 'latin1').toString('utf8');
    if (!decodedFileName || decodedFileName.includes('\uFFFD')) {
        return fileName;
    }
    return decodedFileName;
}
/**
 * 将上传文件名规范化为单级文件名，避免浏览器提交的路径穿透目录。
 */
function normalizeUploadedFileName(rawName) {
    const decodedName = decodePossiblyLatin1Utf8FileName(rawName);
    return normalizeEntryName(path.basename(decodedName.replace(/\\/g, '/')));
}
/**
 * 将浏览器传入的上传相对路径拆成安全路径片段，保留文件夹层级但拒绝穿透路径。
 */
function normalizeUploadedRelativePath(rawPath, fallbackName) {
    if (!rawPath) {
        return normalizeUploadedFileName(fallbackName);
    }
    const decodedPath = decodePossiblyLatin1Utf8FileName(String(rawPath || fallbackName || '')).replace(/\\/g, '/');
    const segments = decodedPath.split('/').filter(Boolean).map((segment) => normalizeEntryName(segment));
    if (segments.length === 0) {
        return normalizeUploadedFileName(fallbackName);
    }
    return path.join(...segments);
}
function formatModeOctal(mode) {
    return (mode & 0o7777).toString(8).padStart(4, '0');
}
function parsePermissionMode(rawMode) {
    const modeText = String(rawMode || '').trim();
    if (!/^[0-7]{3,4}$/.test(modeText)) {
        throw new Error('mode must be a 3 or 4 digit octal string');
    }
    return Number.parseInt(modeText, 8);
}
function permissionTriplet(mode, readBit, writeBit, executeBit, specialBit, specialExecute, specialNoExecute) {
    const read = (mode & readBit) !== 0 ? 'r' : '-';
    const write = (mode & writeBit) !== 0 ? 'w' : '-';
    const hasExecute = (mode & executeBit) !== 0;
    const hasSpecial = (mode & specialBit) !== 0;
    const execute = hasSpecial ? (hasExecute ? specialExecute : specialNoExecute) : (hasExecute ? 'x' : '-');
    return `${read}${write}${execute}`;
}
function formatPermissions(mode) {
    return [
        permissionTriplet(mode, 0o400, 0o200, 0o100, 0o4000, 's', 'S'),
        permissionTriplet(mode, 0o040, 0o020, 0o010, 0o2000, 's', 'S'),
        permissionTriplet(mode, 0o004, 0o002, 0o001, 0o1000, 't', 'T'),
    ].join('');
}
function readPngDimensions(buffer) {
    if (buffer.length < 24 || buffer.toString('hex', 0, 8) !== '89504e470d0a1a0a') {
        return undefined;
    }
    return {
        format: 'png',
        width: buffer.readUInt32BE(16),
        height: buffer.readUInt32BE(20),
    };
}
function readGifDimensions(buffer) {
    if (buffer.length < 10 || (buffer.toString('ascii', 0, 6) !== 'GIF87a' && buffer.toString('ascii', 0, 6) !== 'GIF89a')) {
        return undefined;
    }
    return {
        format: 'gif',
        width: buffer.readUInt16LE(6),
        height: buffer.readUInt16LE(8),
    };
}
function readJpegDimensions(buffer) {
    if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8) {
        return undefined;
    }
    let offset = 2;
    while (offset + 9 < buffer.length) {
        if (buffer[offset] !== 0xff) {
            offset += 1;
            continue;
        }
        const marker = buffer[offset + 1];
        const segmentLength = buffer.readUInt16BE(offset + 2);
        if (segmentLength < 2) {
            return undefined;
        }
        const isStartOfFrame = marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker);
        if (isStartOfFrame && offset + 8 < buffer.length) {
            return {
                format: 'jpeg',
                height: buffer.readUInt16BE(offset + 5),
                width: buffer.readUInt16BE(offset + 7),
            };
        }
        offset += 2 + segmentLength;
    }
    return undefined;
}
function readWebpDimensions(buffer) {
    if (buffer.length < 30 || buffer.toString('ascii', 0, 4) !== 'RIFF' || buffer.toString('ascii', 8, 12) !== 'WEBP') {
        return undefined;
    }
    const chunkType = buffer.toString('ascii', 12, 16);
    if (chunkType === 'VP8X' && buffer.length >= 30) {
        return {
            format: 'webp',
            width: 1 + buffer.readUIntLE(24, 3),
            height: 1 + buffer.readUIntLE(27, 3),
        };
    }
    if (chunkType === 'VP8 ' && buffer.length >= 30) {
        return {
            format: 'webp',
            width: buffer.readUInt16LE(26) & 0x3fff,
            height: buffer.readUInt16LE(28) & 0x3fff,
        };
    }
    if (chunkType === 'VP8L' && buffer.length >= 25) {
        const bits = buffer.readUInt32LE(21);
        return {
            format: 'webp',
            width: (bits & 0x3fff) + 1,
            height: ((bits >> 14) & 0x3fff) + 1,
        };
    }
    return undefined;
}
async function collectChmodTargets(workspaceRoot, targetPath, recursive) {
    const linkStat = await fs.lstat(targetPath);
    if (linkStat.isSymbolicLink()) {
        throw new Error(`Refusing to change permissions on symbolic link: ${targetPath}`);
    }
    const stat = await fs.stat(targetPath);
    if (!recursive || !stat.isDirectory()) {
        return [targetPath];
    }
    const children = await fs.readdir(targetPath);
    const nestedTargets = await Promise.all(children.map((childName) => {
        const childPath = ensureInsideWorkspace(workspaceRoot, path.join(targetPath, childName));
        return collectChmodTargets(workspaceRoot, childPath, true);
    }));
    return [targetPath, ...nestedTargets.flat()];
}
async function ensureSafeChmodTarget(workspaceRoot, targetPath) {
    const linkStat = await fs.lstat(targetPath);
    if (linkStat.isSymbolicLink()) {
        throw new Error(`Refusing to change permissions on symbolic link: ${targetPath}`);
    }
    await ensureRealPathInsideWorkspace(workspaceRoot, targetPath);
}
async function readImageProperties(filePath) {
    const header = await fs.open(filePath, 'r');
    try {
        const buffer = Buffer.alloc(512);
        const { bytesRead } = await header.read(buffer, 0, buffer.length, 0);
        const imageHeader = buffer.subarray(0, bytesRead);
        return readPngDimensions(imageHeader)
            || readGifDimensions(imageHeader)
            || readJpegDimensions(imageHeader)
            || readWebpDimensions(imageHeader);
    }
    finally {
        await header.close();
    }
}
/**
 * 解析并校验工作区内条目的目标路径。
 */
async function resolveEntryTarget({ workspacePath, targetPath }, options = {}) {
    const { requestedRoot, workspaceRoot } = await resolveWorkspaceRoots(workspacePath);
    const normalizedTargetPath = String(targetPath || '').trim();
    if (!normalizedTargetPath) {
        throw new Error('targetPath is required');
    }
    const lexicalTargetPath = resolveRequestedPathInsideWorkspace(requestedRoot, workspaceRoot, normalizedTargetPath);
    const resolvedTargetPath = await ensureRealPathInsideWorkspace(workspaceRoot, lexicalTargetPath);
    if (!options.allowWorkspaceRoot && resolvedTargetPath === workspaceRoot) {
        throw new Error('Workspace root cannot be modified');
    }
    return { workspaceRoot, resolvedTargetPath };
}
async function resolveWritableDirectory(params) {
    const { requestedRoot, workspaceRoot } = await resolveWorkspaceRoots(params.workspacePath);
    const requestedPath = String(params.targetPath || '').trim();
    if (!requestedPath) {
        return { workspaceRoot, resolvedTargetPath: workspaceRoot };
    }
    const lexicalTargetPath = resolveRequestedPathInsideWorkspace(requestedRoot, workspaceRoot, requestedPath);
    if (await pathExists(lexicalTargetPath)) {
        return {
            workspaceRoot,
            resolvedTargetPath: await ensureRealPathInsideWorkspace(workspaceRoot, lexicalTargetPath),
        };
    }
    const realParentPath = await fs.realpath(path.dirname(lexicalTargetPath));
    ensureInsideWorkspace(workspaceRoot, realParentPath);
    return { workspaceRoot, resolvedTargetPath: lexicalTargetPath };
}
function archiveKind(filePath) {
    const lowerName = filePath.toLowerCase();
    if (lowerName.endsWith('.zip'))
        return 'zip';
    if (lowerName.endsWith('.tar.gz') || lowerName.endsWith('.tgz'))
        return 'tgz';
    if (lowerName.endsWith('.tar'))
        return 'tar';
    if (lowerName.endsWith('.gz'))
        return 'gz';
    return null;
}
function stripArchiveExtension(fileName) {
    return fileName.replace(/\.tar\.gz$/i, '').replace(/\.tgz$/i, '').replace(/\.zip$/i, '').replace(/\.tar$/i, '').replace(/\.gz$/i, '');
}
function resolveArchiveUploadOutputPath(uploadTargetDirectory, archiveFileName) {
    const outputDirectoryName = normalizeEntryName(stripArchiveExtension(archiveFileName) || 'extracted');
    return path.join(uploadTargetDirectory, outputDirectoryName);
}
function ensureArchiveEntryInside(targetRoot, entryPath) {
    const normalizedEntryPath = String(entryPath || '').replace(/\\/g, '/');
    const pathSegments = normalizedEntryPath.split('/').filter(Boolean);
    if (!normalizedEntryPath || normalizedEntryPath.startsWith('/') || /^[A-Za-z]:/.test(normalizedEntryPath)) {
        throw new Error(`Archive entry path is invalid: ${entryPath}`);
    }
    if (normalizedEntryPath.length > ARCHIVE_LIMITS.maxPathLength || pathSegments.length > ARCHIVE_LIMITS.maxPathDepth) {
        throw new Error(`Archive entry path is too deep or too long: ${entryPath}`);
    }
    return ensureInsideWorkspace(targetRoot, path.join(targetRoot, normalizedEntryPath));
}
function decodeZipEntryPath(entry) {
    const pathBuffer = entry.props?.pathBuffer;
    if (!pathBuffer || entry.props?.flags?.isUnicode) {
        return entry.path;
    }
    const decodedPath = GBK_DECODER.decode(pathBuffer);
    if (!decodedPath || decodedPath.includes('\uFFFD')) {
        return entry.path;
    }
    return decodedPath;
}
async function drainZipEntry(entry) {
    const drainedStream = entry.autodrain?.();
    if (!drainedStream) {
        return;
    }
    await new Promise((resolve, reject) => {
        drainedStream.on('close', resolve);
        drainedStream.on('end', resolve);
        drainedStream.on('finish', resolve);
        drainedStream.on('error', reject);
    });
}
function trackArchiveEntry(meter) {
    meter.entries += 1;
    if (meter.entries > ARCHIVE_LIMITS.maxEntries) {
        throw new Error(`Archive contains too many entries. Limit: ${ARCHIVE_LIMITS.maxEntries}`);
    }
}
function trackArchiveBytes(meter, bytes) {
    meter.totalBytes += bytes;
    if (bytes > ARCHIVE_LIMITS.maxFileBytes) {
        throw new Error(`Archive entry exceeds max size. Limit: ${ARCHIVE_LIMITS.maxFileBytes} bytes`);
    }
    if (meter.totalBytes > ARCHIVE_LIMITS.maxTotalBytes) {
        throw new Error(`Archive extraction exceeds total size. Limit: ${ARCHIVE_LIMITS.maxTotalBytes} bytes`);
    }
}
async function pipelineWithArchiveLimits(input, destinationPath, meter) {
    let fileBytes = 0;
    input.on('data', (chunk) => {
        const chunkBytes = Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
        fileBytes += chunkBytes;
        if (fileBytes > ARCHIVE_LIMITS.maxFileBytes || meter.totalBytes + fileBytes > ARCHIVE_LIMITS.maxTotalBytes) {
            if (input instanceof Readable) {
                input.destroy(new Error('Archive extraction size limit exceeded'));
            }
        }
    });
    await pipeline(input, createWriteStream(destinationPath));
    trackArchiveBytes(meter, fileBytes);
}
async function pathExists(targetPath) {
    try {
        await fs.access(targetPath);
        return true;
    }
    catch (error) {
        const errorCode = typeof error === 'object' && error !== null ? Reflect.get(error, 'code') : undefined;
        if (errorCode === 'ENOENT') {
            return false;
        }
        throw error;
    }
}
/**
 * 解析文件目标路径，并确保文件位于工作区中。
 */
async function resolveFileTarget({ workspacePath, filePath }) {
    const { requestedRoot, workspaceRoot } = await resolveWorkspaceRoots(workspacePath);
    const normalizedFilePath = String(filePath || '').trim();
    if (!normalizedFilePath) {
        throw new Error('filePath is required');
    }
    return {
        workspaceRoot,
        resolvedFilePath: resolveRequestedPathInsideWorkspace(requestedRoot, workspaceRoot, normalizedFilePath)
    };
}
/**
 * 列出工作区目录内容。
 */
export async function listWorkspaceDirectory(params) {
    const { workspaceRoot, resolvedTargetPath } = await resolveDirectoryTarget(params);
    const stat = await fs.stat(resolvedTargetPath);
    if (!stat.isDirectory()) {
        throw new Error(`Path is not a directory: ${resolvedTargetPath}`);
    }
    const entries = await fs.readdir(resolvedTargetPath, { withFileTypes: true });
    const items = await Promise.all(entries.map(async (entry) => {
        const entryPath = path.join(resolvedTargetPath, entry.name);
        const entrySize = entry.isFile() ? (await fs.stat(entryPath)).size : undefined;
        return {
            name: entry.name,
            path: toDisplayPath(entryPath),
            isDirectory: entry.isDirectory(),
            size: entrySize
        };
    }));
    return {
        currentPath: toDisplayPath(resolvedTargetPath),
        workspacePath: toDisplayPath(workspaceRoot),
        items
    };
}
/**
 * 读取工作区中的文本文件。
 */
export async function readWorkspaceFile(params) {
    const { workspaceRoot, resolvedFilePath: lexicalFilePath } = await resolveFileTarget(params);
    const resolvedFilePath = await ensureRealPathInsideWorkspace(workspaceRoot, lexicalFilePath);
    const stat = await fs.stat(resolvedFilePath);
    if (!stat.isFile()) {
        throw new Error(`Path is not a file: ${resolvedFilePath}`);
    }
    const content = await fs.readFile(resolvedFilePath, 'utf-8');
    return {
        workspacePath: toDisplayPath(workspaceRoot),
        filePath: toDisplayPath(resolvedFilePath),
        content
    };
}
/**
 * 读取工作区条目的属性信息。
 * 主流程：解析安全路径 -> 读取 stat/lstat -> 文件额外尝试解析图片尺寸。
 */
export async function getWorkspaceEntryProperties(params) {
    const { workspaceRoot, resolvedTargetPath } = await resolveEntryTarget(params, { allowWorkspaceRoot: true });
    const [stat, linkStat] = await Promise.all([
        fs.stat(resolvedTargetPath),
        fs.lstat(resolvedTargetPath),
    ]);
    const image = stat.isFile() ? await readImageProperties(resolvedTargetPath) : undefined;
    return {
        workspacePath: toDisplayPath(workspaceRoot),
        targetPath: toDisplayPath(resolvedTargetPath),
        name: path.basename(resolvedTargetPath),
        isDirectory: stat.isDirectory(),
        isFile: stat.isFile(),
        isSymbolicLink: linkStat.isSymbolicLink(),
        size: stat.size,
        mode: stat.mode,
        modeOctal: formatModeOctal(stat.mode),
        permissions: formatPermissions(stat.mode),
        uid: stat.uid,
        gid: stat.gid,
        atimeMs: stat.atimeMs,
        mtimeMs: stat.mtimeMs,
        ctimeMs: stat.ctimeMs,
        birthtimeMs: stat.birthtimeMs,
        ...(image ? { image } : {}),
    };
}
/**
 * 修改工作区条目权限。
 * 主流程：校验八进制权限 -> 解析安全路径 -> 非递归只改目标，递归目录先收集所有非符号链接子项后逐一 chmod。
 */
export async function chmodWorkspaceEntry(params) {
    const mode = parsePermissionMode(params.mode);
    const recursive = Boolean(params.recursive);
    const { workspaceRoot, resolvedTargetPath } = await resolveEntryTarget(params);
    const targets = await collectChmodTargets(workspaceRoot, resolvedTargetPath, recursive);
    for (const targetPath of targets) {
        await ensureSafeChmodTarget(workspaceRoot, targetPath);
        await fs.chmod(targetPath, mode);
    }
    return {
        ok: true,
        workspacePath: toDisplayPath(workspaceRoot),
        targetPath: toDisplayPath(resolvedTargetPath),
        mode,
        modeOctal: formatModeOctal(mode),
        recursive,
        changedCount: targets.length,
    };
}
/**
 * 返回工作区内 Word 文档的预览元数据；docx 由前端下载原文件后渲染，旧版 doc 明确提示不支持。
 */
export async function previewWorkspaceDocument(params) {
    const { workspaceRoot, resolvedFilePath: lexicalFilePath } = await resolveFileTarget(params);
    const resolvedFilePath = await ensureRealPathInsideWorkspace(workspaceRoot, lexicalFilePath);
    const stat = await fs.stat(resolvedFilePath);
    if (!stat.isFile()) {
        throw new Error(`Path is not a file: ${resolvedFilePath}`);
    }
    const extension = path.extname(resolvedFilePath).toLowerCase();
    if (extension === '.doc') {
        return {
            workspacePath: toDisplayPath(workspaceRoot),
            filePath: toDisplayPath(resolvedFilePath),
            kind: 'unsupported',
            content: '',
            message: '暂不支持旧版 .doc 格式预览，请另存为 .docx 后再打开。',
            size: stat.size,
            mtimeMs: stat.mtimeMs,
        };
    }
    if (extension !== '.docx') {
        throw new Error('Document preview only supports .docx files');
    }
    return {
        workspacePath: toDisplayPath(workspaceRoot),
        filePath: toDisplayPath(resolvedFilePath),
        kind: 'html',
        content: '',
        size: stat.size,
        mtimeMs: stat.mtimeMs,
    };
}
/**
 * 将文本内容保存到工作区文件中。
 */
export async function writeWorkspaceFile(params) {
    const { workspaceRoot, resolvedFilePath } = await resolveFileTarget(params);
    await fs.mkdir(path.dirname(resolvedFilePath), { recursive: true });
    await ensureWritableParentInsideWorkspace(workspaceRoot, resolvedFilePath);
    await fs.writeFile(resolvedFilePath, String(params.content ?? ''), 'utf-8');
    return {
        ok: true,
        workspacePath: toDisplayPath(workspaceRoot),
        filePath: toDisplayPath(resolvedFilePath)
    };
}
/**
 * 在指定工作区目录下创建文件或目录。
 */
export async function createWorkspaceEntry(params) {
    const { workspaceRoot, resolvedTargetPath } = await resolveDirectoryTarget(params);
    const entryName = normalizeEntryName(params.entryName);
    const targetPath = ensureInsideWorkspace(workspaceRoot, path.join(resolvedTargetPath, entryName));
    const parentStat = await fs.stat(resolvedTargetPath);
    if (!parentStat.isDirectory()) {
        throw new Error(`Path is not a directory: ${resolvedTargetPath}`);
    }
    try {
        await fs.access(targetPath);
        throw new Error(`Path already exists: ${targetPath}`);
    }
    catch (error) {
        const errorCode = typeof error === 'object' && error !== null ? Reflect.get(error, 'code') : undefined;
        if (errorCode !== 'ENOENT') {
            throw error;
        }
    }
    if (params.isDirectory) {
        await fs.mkdir(targetPath, { recursive: false });
    }
    else {
        await fs.mkdir(path.dirname(targetPath), { recursive: true });
        await ensureWritableParentInsideWorkspace(workspaceRoot, targetPath);
        await fs.writeFile(targetPath, '', 'utf-8');
    }
    return {
        ok: true,
        workspacePath: toDisplayPath(workspaceRoot),
        targetPath: toDisplayPath(targetPath),
        isDirectory: params.isDirectory
    };
}
/**
 * 在工作区内重命名文件或目录，保持目标仍位于原父目录中。
 */
export async function renameWorkspaceEntry(params) {
    const { workspaceRoot, resolvedTargetPath } = await resolveEntryTarget(params);
    const newName = normalizeEntryName(params.newName);
    const renamedPath = ensureInsideWorkspace(workspaceRoot, path.join(path.dirname(resolvedTargetPath), newName));
    if (renamedPath === resolvedTargetPath) {
        return {
            ok: true,
            workspacePath: toDisplayPath(workspaceRoot),
            sourcePath: toDisplayPath(resolvedTargetPath),
            targetPath: toDisplayPath(renamedPath)
        };
    }
    await fs.stat(resolvedTargetPath);
    try {
        await fs.access(renamedPath);
        throw new Error(`Path already exists: ${renamedPath}`);
    }
    catch (error) {
        const errorCode = typeof error === 'object' && error !== null ? Reflect.get(error, 'code') : undefined;
        if (errorCode !== 'ENOENT') {
            throw error;
        }
    }
    await fs.rename(resolvedTargetPath, renamedPath);
    return {
        ok: true,
        workspacePath: toDisplayPath(workspaceRoot),
        sourcePath: toDisplayPath(resolvedTargetPath),
        targetPath: toDisplayPath(renamedPath)
    };
}
/**
 * 将工作区内文件或目录移动到另一个工作区目录，保持原文件名不变。
 */
export async function moveWorkspaceEntry(params) {
    const { workspaceRoot, resolvedTargetPath } = await resolveEntryTarget(params);
    const destination = await resolveDirectoryTarget({
        workspacePath: params.workspacePath,
        targetPath: params.destinationPath,
    });
    const destinationStat = await fs.stat(destination.resolvedTargetPath);
    if (!destinationStat.isDirectory()) {
        throw new Error(`Destination is not a directory: ${destination.resolvedTargetPath}`);
    }
    const targetStat = await fs.stat(resolvedTargetPath);
    if (targetStat.isDirectory()) {
        const relativeDestination = path.relative(resolvedTargetPath, destination.resolvedTargetPath);
        if (relativeDestination === '' || (!relativeDestination.startsWith('..') && !path.isAbsolute(relativeDestination))) {
            throw new Error('Cannot move a directory into itself or its descendant');
        }
    }
    const movedPath = ensureInsideWorkspace(workspaceRoot, path.join(destination.resolvedTargetPath, path.basename(resolvedTargetPath)));
    if (movedPath === resolvedTargetPath) {
        return {
            ok: true,
            workspacePath: toDisplayPath(workspaceRoot),
            sourcePath: toDisplayPath(resolvedTargetPath),
            targetPath: toDisplayPath(movedPath),
            destinationPath: toDisplayPath(destination.resolvedTargetPath),
        };
    }
    try {
        await fs.access(movedPath);
        throw new Error(`Path already exists: ${movedPath}`);
    }
    catch (error) {
        const errorCode = typeof error === 'object' && error !== null ? Reflect.get(error, 'code') : undefined;
        if (errorCode !== 'ENOENT') {
            throw error;
        }
    }
    await fs.rename(resolvedTargetPath, movedPath);
    return {
        ok: true,
        workspacePath: toDisplayPath(workspaceRoot),
        sourcePath: toDisplayPath(resolvedTargetPath),
        targetPath: toDisplayPath(movedPath),
        destinationPath: toDisplayPath(destination.resolvedTargetPath),
    };
}
/**
 * 删除工作区内的文件或目录，并阻止误删工作区根目录。
 */
export async function deleteWorkspaceEntry(params) {
    const { workspaceRoot, resolvedTargetPath } = await resolveEntryTarget(params);
    const targetStat = await fs.stat(resolvedTargetPath);
    if (targetStat.isDirectory()) {
        await fs.rm(resolvedTargetPath, { recursive: true, force: false });
    }
    else {
        await fs.unlink(resolvedTargetPath);
    }
    return {
        ok: true,
        workspacePath: toDisplayPath(workspaceRoot),
        targetPath: toDisplayPath(resolvedTargetPath)
    };
}
/**
 * 上传文件到工作区指定目录，并可在上传后自动解压常见归档。
 */
export async function uploadWorkspaceFiles(params) {
    if (!Array.isArray(params.files) || params.files.length === 0) {
        throw new Error('files are required');
    }
    const { workspaceRoot, resolvedTargetPath } = await resolveWritableDirectory(params);
    const targetStat = await fs.stat(resolvedTargetPath);
    if (!targetStat.isDirectory()) {
        throw new Error(`Path is not a directory: ${resolvedTargetPath}`);
    }
    const files = [];
    for (const file of params.files) {
        const fileName = normalizeUploadedFileName(file.originalname);
        const uploadRelativePath = normalizeUploadedRelativePath(file.relativePath, file.originalname);
        const destinationPath = ensureInsideWorkspace(workspaceRoot, path.join(resolvedTargetPath, uploadRelativePath));
        await fs.mkdir(path.dirname(destinationPath), { recursive: true });
        await ensureWritableParentInsideWorkspace(workspaceRoot, destinationPath);
        await fs.copyFile(file.path, destinationPath);
        let extracted;
        if (params.extractArchives && archiveKind(destinationPath)) {
            extracted = await extractWorkspaceArchive({
                workspacePath: workspaceRoot,
                archivePath: destinationPath,
                outputPath: resolveArchiveUploadOutputPath(resolvedTargetPath, fileName)
            });
        }
        files.push({
            fileName,
            targetPath: toDisplayPath(destinationPath),
            size: file.size,
            extracted
        });
    }
    return {
        ok: true,
        workspacePath: toDisplayPath(workspaceRoot),
        targetPath: toDisplayPath(resolvedTargetPath),
        files
    };
}
/**
 * 解析工作区下载目标；目录由路由层打包为 zip，文件直接流式下载。
 */
export async function resolveWorkspaceDownloadTarget(params) {
    const { workspaceRoot, resolvedTargetPath } = await resolveEntryTarget(params);
    const targetStat = await fs.stat(resolvedTargetPath);
    const baseName = path.basename(resolvedTargetPath);
    return {
        workspacePath: toDisplayPath(workspaceRoot),
        targetPath: toDisplayPath(resolvedTargetPath),
        resolvedTargetPath,
        fileName: targetStat.isDirectory() ? `${baseName || 'workspace'}.zip` : baseName,
        contentType: targetStat.isDirectory() ? 'application/zip' : 'application/octet-stream',
        isDirectory: targetStat.isDirectory()
    };
}
/**
 * 将目录内容以 zip 格式写入输出流，避免在磁盘生成临时包。
 */
export async function streamWorkspaceDirectoryZip(directoryPath, output) {
    const archive = new ZipArchive({ zlib: { level: 9 } });
    const completion = new Promise((resolve, reject) => {
        output.on('close', resolve);
        output.on('finish', resolve);
        archive.on('error', reject);
        output.on('error', reject);
    });
    archive.pipe(output);
    archive.directory(directoryPath, false);
    await archive.finalize();
    await completion;
}
async function extractZipArchive(archivePath, outputRoot, meter) {
    let extractedEntries = 0;
    let extractionError;
    const sourceStream = createReadStream(archivePath);
    const parsedStream = sourceStream.pipe(unzipper.Parse({ forceStream: true }));
    try {
        for await (const entry of parsedStream) {
            if (extractionError) {
                await drainZipEntry(entry);
                continue;
            }
            const entryPath = decodeZipEntryPath(entry);
            try {
                const destinationPath = ensureArchiveEntryInside(outputRoot, entryPath);
                trackArchiveEntry(meter);
                if (entry.type === 'Directory') {
                    await fs.mkdir(destinationPath, { recursive: true });
                    continue;
                }
                if (entry.type !== 'File') {
                    throw new Error(`Unsupported archive entry type: ${entryPath}`);
                }
                await fs.mkdir(path.dirname(destinationPath), { recursive: true });
                await ensureWritableParentInsideWorkspace(outputRoot, destinationPath);
                await pipelineWithArchiveLimits(entry, destinationPath, meter);
                extractedEntries += 1;
            }
            catch (error) {
                extractionError = error;
                await drainZipEntry(entry);
            }
        }
    }
    finally {
        sourceStream.destroy();
        parsedStream.destroy();
    }
    if (extractionError) {
        throw extractionError;
    }
    return extractedEntries;
}
async function extractTarArchive(archivePath, outputRoot, gzip, meter) {
    let extractedEntries = 0;
    let extractionError;
    /**
     * 校验 tar 条目并通过 filter 返回值控制是否解压。
     * tar 的 filter 回调位于流解析链路中，直接抛错会变成未处理异常；这里先记录错误，待流结束后统一 reject。
     */
    const shouldExtractTarEntry = (entryPath, entry) => {
        if (extractionError) {
            return false;
        }
        try {
            ensureArchiveEntryInside(outputRoot, entryPath);
            trackArchiveEntry(meter);
            const entryType = String(entry.type || '').toLowerCase();
            if (entryType === 'symboliclink' || entryType === 'link') {
                throw new Error(`Archive links are not supported: ${entryPath}`);
            }
            if (entryType !== 'file' && entryType !== 'oldfile' && entryType !== 'contiguousfile' && entryType !== 'directory') {
                throw new Error(`Unsupported archive entry type: ${entryPath}`);
            }
            if (typeof entry.size === 'number') {
                trackArchiveBytes(meter, entry.size);
            }
            if (entryType !== 'directory') {
                extractedEntries += 1;
            }
            return true;
        }
        catch (error) {
            extractionError = error;
            return false;
        }
    };
    await tar.x({
        file: archivePath,
        cwd: outputRoot,
        gzip,
        strict: true,
        filter: (entryPath, entry) => {
            return shouldExtractTarEntry(entryPath, entry);
        }
    });
    if (extractionError) {
        throw extractionError;
    }
    return extractedEntries;
}
async function extractGzipFile(archivePath, outputRoot, meter) {
    const outputName = stripArchiveExtension(path.basename(archivePath));
    const destinationPath = ensureArchiveEntryInside(outputRoot, outputName || 'decompressed');
    trackArchiveEntry(meter);
    await fs.mkdir(path.dirname(destinationPath), { recursive: true });
    await ensureWritableParentInsideWorkspace(outputRoot, destinationPath);
    await pipelineWithArchiveLimits(createReadStream(archivePath).pipe(createGunzip()), destinationPath, meter);
    return 1;
}
/**
 * 解压工作区中的归档文件到指定目录，支持 zip、tar、tar.gz/tgz 与单文件 gz。
 */
export async function extractWorkspaceArchive(params) {
    const { requestedRoot, workspaceRoot } = await resolveWorkspaceRoots(params.workspacePath);
    const { resolvedTargetPath: outputRoot } = await resolveWritableDirectory({
        workspacePath: params.workspacePath,
        targetPath: params.outputPath || ''
    });
    const lexicalArchivePath = resolveRequestedPathInsideWorkspace(requestedRoot, workspaceRoot, String(params.archivePath || '').trim());
    const archivePath = await ensureRealPathInsideWorkspace(workspaceRoot, lexicalArchivePath);
    const archiveStat = await fs.stat(archivePath);
    if (!archiveStat.isFile()) {
        throw new Error(`Archive path is not a file: ${archivePath}`);
    }
    if (!(await pathExists(outputRoot))) {
        await fs.mkdir(outputRoot, { recursive: true });
    }
    const outputStat = await fs.stat(outputRoot);
    if (!outputStat.isDirectory()) {
        throw new Error(`Output path is not a directory: ${outputRoot}`);
    }
    const kind = archiveKind(archivePath);
    if (!kind) {
        throw new Error('Unsupported archive type. Supported: .zip, .tar, .tar.gz, .tgz, .gz');
    }
    let extractedEntries = 0;
    const meter = { entries: 0, totalBytes: 0 };
    if (kind === 'zip') {
        extractedEntries = await extractZipArchive(archivePath, outputRoot, meter);
    }
    else if (kind === 'tar' || kind === 'tgz') {
        extractedEntries = await extractTarArchive(archivePath, outputRoot, kind === 'tgz', meter);
    }
    else {
        extractedEntries = await extractGzipFile(archivePath, outputRoot, meter);
    }
    return {
        ok: true,
        workspacePath: toDisplayPath(workspaceRoot),
        archivePath: toDisplayPath(archivePath),
        targetPath: toDisplayPath(outputRoot),
        extractedEntries
    };
}
