/**
 * 文件浏览器 API 路由
 *
 * 提供文件系统浏览功能
 */
import { createReadStream, promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { Router } from 'express';
import multer from 'multer';
import { allowHostFileBrowser } from '../config.js';
import { validateToken } from '../middleware/auth.js';
import { chmodWorkspaceEntry, createWorkspaceEntry, deleteWorkspaceEntry, extractWorkspaceArchive, getWorkspaceEntryProperties, listWorkspaceDirectory, moveWorkspaceEntry, previewWorkspaceDocument, readWorkspaceFile, renameWorkspaceEntry, resolveWorkspaceDownloadTarget, streamWorkspaceDirectoryZip, uploadWorkspaceFiles, writeWorkspaceFile, } from '../services/workspace-files.js';
import { unwatchWorkspaceFile, watchWorkspaceFile } from '../services/workspace-watch.js';
import { createLogger } from '../utils/logger.js';
const log = createLogger('file-browser');
export const fileBrowserRouter = Router();
export const WORKSPACE_UPLOAD_FILE_LIMIT = 2000;
const upload = multer({
    dest: path.join(os.tmpdir(), 'agentswork-runtime-bridge-uploads'),
    limits: {
        files: WORKSPACE_UPLOAD_FILE_LIMIT,
        fileSize: 512 * 1024 * 1024
    }
});
const DIRECTORY_TREE_MAX_DEPTH = 10;
const DIRECTORY_TREE_MAX_ITEMS_PER_DIRECTORY = 20;
function toBoolean(value) {
    return value === true || value === 'true' || value === '1';
}
function contentDispositionFileName(fileName) {
    const fallbackName = fileName
        .replace(/[\\"\r\n]/g, '_')
        .replace(/[^\x20-\x7E]/g, '_');
    return `attachment; filename="${fallbackName}"; filename*=UTF-8''${encodeURIComponent(fileName)}`;
}
async function cleanupUploadedFiles(files = []) {
    await Promise.all(files.map((file) => fs.rm(file.path, { force: true }).catch(() => undefined)));
}
export function createWorkspaceUploadLimitResponse(error) {
    return {
        status: 400,
        body: {
            error: error.code === 'LIMIT_FILE_COUNT'
                ? `一次最多上传 ${WORKSPACE_UPLOAD_FILE_LIMIT} 个文件，请拆分文件夹后重试。`
                : error.message,
            code: error.code
        }
    };
}
export function parseWorkspaceUploadRelativePaths(body) {
    const relativePathsJson = body.relativePathsJson;
    if (typeof relativePathsJson === 'string' && relativePathsJson.trim()) {
        const parsed = JSON.parse(relativePathsJson);
        if (!Array.isArray(parsed)) {
            throw new Error('relativePathsJson must be an array');
        }
        return parsed.map((relativePath) => String(relativePath || ''));
    }
    const relativePaths = body.relativePaths;
    return Array.isArray(relativePaths)
        ? relativePaths.map((relativePath) => String(relativePath || ''))
        : typeof relativePaths === 'string'
            ? [relativePaths]
            : [];
}
function workspaceUploadMiddleware(req, res, next) {
    upload.array('files')(req, res, (error) => {
        if (!error) {
            next();
            return;
        }
        if (error instanceof multer.MulterError) {
            const response = createWorkspaceUploadLimitResponse(error);
            res.status(response.status).json(response.body);
            return;
        }
        next(error);
    });
}
async function listHostDirectories(requestPath = '') {
    const platform = process.platform;
    const isWindows = platform === 'win32';
    if (!requestPath || requestPath === '') {
        if (isWindows) {
            const drives = [];
            for (let i = 67; i <= 90; i++) { // C to Z
                const drive = String.fromCharCode(i) + ':\\';
                try {
                    await fs.access(drive);
                    drives.push({
                        name: drive,
                        path: drive,
                        isDirectory: true
                    });
                }
                catch {
                    // Drive not accessible, skip
                }
            }
            return {
                isWindows: true,
                currentPath: '',
                items: drives
            };
        }
        const entries = await fs.readdir('/', { withFileTypes: true });
        const items = entries
            .filter(entry => entry.isDirectory())
            .map(entry => ({
            name: entry.name,
            path: '/' + entry.name,
            isDirectory: true
        }));
        return {
            isWindows: false,
            currentPath: '/',
            items
        };
    }
    const normalizedPath = path.normalize(requestPath);
    let stat;
    try {
        stat = await fs.stat(normalizedPath);
    }
    catch {
        throw new Error('Path not found: ' + normalizedPath);
    }
    if (!stat.isDirectory()) {
        throw new Error('Path is not a directory: ' + normalizedPath);
    }
    const entries = await fs.readdir(normalizedPath, { withFileTypes: true });
    const items = entries
        .filter(entry => entry.isDirectory())
        .map(entry => ({
        name: entry.name,
        path: path.join(normalizedPath, entry.name),
        isDirectory: true
    }));
    return {
        isWindows,
        currentPath: normalizedPath,
        items
    };
}
/**
 * 主流程：从选中的工作目录生成受限目录树，最多 10 层且每个目录只返回前 20 个子目录。
 */
export async function listHostDirectoryTree(requestPath = '') {
    const root = await listHostDirectories(requestPath);
    const ignoreRules = root.currentPath ? await loadGitIgnoreRules(root.currentPath) : [];
    if (!root.currentPath) {
        return {
            ...root,
            maxDepth: DIRECTORY_TREE_MAX_DEPTH,
            maxItemsPerDirectory: DIRECTORY_TREE_MAX_ITEMS_PER_DIRECTORY,
            tree: root.items.slice(0, DIRECTORY_TREE_MAX_ITEMS_PER_DIRECTORY),
        };
    }
    return {
        ...root,
        maxDepth: DIRECTORY_TREE_MAX_DEPTH,
        maxItemsPerDirectory: DIRECTORY_TREE_MAX_ITEMS_PER_DIRECTORY,
        tree: await buildDirectoryTree(root.currentPath, root.currentPath, 1, ignoreRules),
    };
}
async function buildDirectoryTree(rootPath, directoryPath, depth, ignoreRules) {
    if (depth > DIRECTORY_TREE_MAX_DEPTH) {
        return [];
    }
    const entries = await fs.readdir(directoryPath, { withFileTypes: true });
    const directories = entries
        .filter((entry) => entry.isDirectory())
        .filter((entry) => !isIgnoredDirectory(rootPath, path.join(directoryPath, entry.name), ignoreRules))
        .sort((left, right) => left.name.localeCompare(right.name))
        .slice(0, DIRECTORY_TREE_MAX_ITEMS_PER_DIRECTORY);
    const nodes = [];
    for (const entry of directories) {
        const childPath = path.join(directoryPath, entry.name);
        const node = {
            name: entry.name,
            path: childPath.replace(/\\/g, '/'),
            isDirectory: true,
        };
        if (depth < DIRECTORY_TREE_MAX_DEPTH) {
            try {
                node.children = await buildDirectoryTree(rootPath, childPath, depth + 1, ignoreRules);
            }
            catch {
                node.children = [];
            }
        }
        else {
            node.truncated = true;
        }
        nodes.push(node);
    }
    return nodes;
}
export function parseGitIgnoreRules(content) {
    return String(content || '')
        .split(/\r?\n/)
        .map((line) => line.trim())
        .filter((line) => line && !line.startsWith('#'))
        .map((line) => {
        const negated = line.startsWith('!');
        const rawPattern = negated ? line.slice(1).trim() : line;
        const anchored = rawPattern.startsWith('/');
        const directoryOnly = rawPattern.endsWith('/');
        const pattern = rawPattern
            .replace(/^\/+/, '')
            .replace(/\/+$/, '')
            .replace(/\\/g, '/');
        return {
            pattern,
            negated,
            directoryOnly,
            anchored,
            hasSlash: pattern.includes('/'),
        };
    })
        .filter((rule) => rule.pattern.length > 0);
}
async function loadGitIgnoreRules(rootPath) {
    try {
        const content = await fs.readFile(path.join(rootPath, '.gitignore'), 'utf8');
        return parseGitIgnoreRules(content);
    }
    catch {
        return [];
    }
}
export function isIgnoredByGitIgnore(relativePath, isDirectory, rules) {
    const normalizedPath = normalizeRelativePath(relativePath);
    if (!normalizedPath)
        return false;
    let ignored = false;
    for (const rule of rules) {
        if (rule.directoryOnly && !isDirectory)
            continue;
        if (matchesGitIgnoreRule(normalizedPath, rule)) {
            ignored = !rule.negated;
        }
    }
    return ignored;
}
function isIgnoredDirectory(rootPath, directoryPath, ignoreRules) {
    if (ignoreRules.length === 0)
        return false;
    const relativePath = path.relative(rootPath, directoryPath).replace(/\\/g, '/');
    return isIgnoredByGitIgnore(relativePath, true, ignoreRules);
}
function matchesGitIgnoreRule(relativePath, rule) {
    if (!rule.hasSlash && !rule.anchored) {
        return relativePath.split('/').some((segment) => matchesGlobSegment(segment, rule.pattern));
    }
    const pattern = rule.pattern;
    if (rule.anchored) {
        return matchesGlobPath(relativePath, pattern) || relativePath.startsWith(pattern + '/');
    }
    return matchesGlobPath(relativePath, pattern)
        || relativePath.endsWith('/' + pattern)
        || relativePath.includes('/' + pattern + '/');
}
function matchesGlobPath(relativePath, pattern) {
    const regex = new RegExp('^' + globToRegex(pattern) + '(?:/.*)?$');
    return regex.test(relativePath);
}
function matchesGlobSegment(segment, pattern) {
    const regex = new RegExp('^' + globToRegex(pattern) + '$');
    return regex.test(segment);
}
function globToRegex(pattern) {
    return pattern
        .replace(/[.+^${}()|[\]\\]/g, '\\$&')
        .replace(/\*\*/g, '.*')
        .replace(/\*/g, '[^/]*')
        .replace(/\?/g, '[^/]');
}
function normalizeRelativePath(relativePath) {
    return String(relativePath || '')
        .replace(/\\/g, '/')
        .replace(/^\.\//, '')
        .replace(/^\/+/, '')
        .replace(/\/+$/, '');
}
async function resolveHostDirectoryPath(targetPath) {
    const normalizedPath = path.normalize(String(targetPath || ''));
    const stat = await fs.stat(normalizedPath);
    if (!stat.isDirectory()) {
        throw new Error('Path is not a directory: ' + normalizedPath);
    }
    return normalizedPath;
}
function assertSafeDirectoryEntryName(entryName) {
    const trimmedName = String(entryName || '').trim();
    if (!trimmedName) {
        throw new Error('Folder name is required');
    }
    if (trimmedName.includes('/') || trimmedName.includes('\\') || trimmedName === '.' || trimmedName === '..') {
        throw new Error('Folder name must not contain path separators');
    }
    return trimmedName;
}
async function createHostDirectory(parentPath, entryName) {
    const parentDirectory = await resolveHostDirectoryPath(parentPath);
    const safeEntryName = assertSafeDirectoryEntryName(entryName);
    const targetPath = path.join(parentDirectory, safeEntryName);
    await fs.mkdir(targetPath);
    return {
        ok: true,
        parentPath,
        targetPath: targetPath.replace(/\\/g, '/'),
        entryName: safeEntryName
    };
}
async function renameHostDirectory(targetPath, newName) {
    const sourcePath = await resolveHostDirectoryPath(targetPath);
    const safeNewName = assertSafeDirectoryEntryName(newName);
    const destinationPath = path.join(path.dirname(sourcePath), safeNewName);
    await fs.rename(sourcePath, destinationPath);
    return {
        ok: true,
        sourcePath: sourcePath.replace(/\\/g, '/'),
        targetPath: destinationPath.replace(/\\/g, '/'),
        newName: safeNewName
    };
}
/**
 * 列出目录内容
 * POST /api/file-browser/list
 */
fileBrowserRouter.post('/list', validateToken, async (req, res) => {
    try {
        if (!allowHostFileBrowser) {
            return res.status(403).json({
                error: 'Host filesystem browsing is disabled. Use /api/file-browser/workspace/list with a registered workspace, or set AWS_ALLOW_HOST_FILE_BROWSER=true for isolated local debugging.'
            });
        }
        const { path: requestPath = '' } = req.body || {};
        res.json(await listHostDirectories(String(requestPath || '')));
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message });
    }
});
/**
 * 列出目录选择器内容。
 * POST /api/file-browser/directory-picker/list
 *
 * 该接口只返回目录，用于选择工作目录；不同于 /list，它不暴露文件条目。
 */
fileBrowserRouter.post('/directory-picker/list', validateToken, async (req, res) => {
    try {
        const { path: requestPath = '' } = req.body || {};
        res.json(await listHostDirectories(String(requestPath || '')));
    }
    catch (error) {
        const err = error;
        res.status(400).json({ error: err.message });
    }
});
fileBrowserRouter.post('/directory-picker/tree', validateToken, async (req, res) => {
    try {
        const { path: requestPath = '' } = req.body || {};
        res.json(await listHostDirectoryTree(String(requestPath || '')));
    }
    catch (error) {
        const err = error;
        res.status(400).json({ error: err.message });
    }
});
/**
 * 列出工作区目录内容
 * POST /api/file-browser/workspace/list
 */
fileBrowserRouter.post('/workspace/list', validateToken, async (req, res) => {
    try {
        const { workspacePath, path: requestPath = '' } = req.body || {};
        const result = await listWorkspaceDirectory({
            workspacePath: String(workspacePath || ''),
            targetPath: String(requestPath || '')
        });
        res.json({
            isWindows: process.platform === 'win32',
            currentPath: result.currentPath,
            workspacePath: result.workspacePath,
            items: result.items
        });
    }
    catch (error) {
        const err = error;
        console.error('[file-browser] failed to list workspace directory:', err);
        res.status(400).json({ error: err.message });
    }
});
/**
 * 读取工作区文件内容
 * POST /api/file-browser/workspace/read
 */
fileBrowserRouter.post('/workspace/read', validateToken, async (req, res) => {
    try {
        const { workspacePath, filePath } = req.body || {};
        const result = await readWorkspaceFile({
            workspacePath: String(workspacePath || ''),
            filePath: String(filePath || '')
        });
        res.json(result);
    }
    catch (error) {
        const err = error;
        console.error('[file-browser] failed to read workspace file:', err);
        res.status(400).json({ error: err.message });
    }
});
/**
 * 读取工作区文件或目录属性。
 * POST /api/file-browser/workspace/properties
 */
fileBrowserRouter.post('/workspace/properties', validateToken, async (req, res) => {
    try {
        const { workspacePath, targetPath } = req.body || {};
        res.json(await getWorkspaceEntryProperties({
            workspacePath: String(workspacePath || ''),
            targetPath: String(targetPath || '')
        }));
    }
    catch (error) {
        const err = error;
        log.error('failed to get workspace entry properties:', err);
        res.status(400).json({ error: err.message });
    }
});
/**
 * 修改工作区文件或目录权限。
 * POST /api/file-browser/workspace/chmod
 */
fileBrowserRouter.post('/workspace/chmod', validateToken, async (req, res) => {
    try {
        const { workspacePath, targetPath, mode, recursive } = req.body || {};
        res.json(await chmodWorkspaceEntry({
            workspacePath: String(workspacePath || ''),
            targetPath: String(targetPath || ''),
            mode: String(mode || ''),
            recursive: Boolean(recursive)
        }));
    }
    catch (error) {
        const err = error;
        log.error('failed to chmod workspace entry:', err);
        res.status(400).json({ error: err.message });
    }
});
/**
 * 在目录选择器当前位置创建文件夹。
 * POST /api/file-browser/directory-picker/create
 */
fileBrowserRouter.post('/directory-picker/create', validateToken, async (req, res) => {
    try {
        const { path: requestPath = '', entryName } = req.body || {};
        res.json(await createHostDirectory(String(requestPath || ''), String(entryName || '')));
    }
    catch (error) {
        const err = error;
        log.error('failed to create directory from directory picker:', err);
        res.status(400).json({ error: err.message });
    }
});
/**
 * 重命名目录选择器中的文件夹。
 * POST /api/file-browser/directory-picker/rename
 */
fileBrowserRouter.post('/directory-picker/rename', validateToken, async (req, res) => {
    try {
        const { targetPath, newName } = req.body || {};
        res.json(await renameHostDirectory(String(targetPath || ''), String(newName || '')));
    }
    catch (error) {
        const err = error;
        log.error('failed to rename directory from directory picker:', err);
        res.status(400).json({ error: err.message });
    }
});
/**
 * 预览工作区文档内容。
 * POST /api/file-browser/workspace/preview
 */
fileBrowserRouter.post('/workspace/preview', validateToken, async (req, res) => {
    try {
        const { workspacePath, filePath } = req.body || {};
        const result = await previewWorkspaceDocument({
            workspacePath: String(workspacePath || ''),
            filePath: String(filePath || '')
        });
        res.json(result);
    }
    catch (error) {
        const err = error;
        log.error('failed to preview workspace document:', err);
        res.status(400).json({ error: err.message });
    }
});
/**
 * 监听工作区文件变化，用于文档预览自动刷新。
 * POST /api/file-browser/workspace/watch
 */
fileBrowserRouter.post('/workspace/watch', validateToken, async (req, res) => {
    try {
        const { workspacePath, filePath } = req.body || {};
        res.json(await watchWorkspaceFile({
            workspacePath: String(workspacePath || ''),
            filePath: String(filePath || '')
        }));
    }
    catch (error) {
        const err = error;
        log.error('failed to watch workspace file:', err);
        res.status(400).json({ error: err.message });
    }
});
/**
 * 取消监听工作区文件变化。
 * POST /api/file-browser/workspace/unwatch
 */
fileBrowserRouter.post('/workspace/unwatch', validateToken, async (req, res) => {
    try {
        const { workspacePath, filePath } = req.body || {};
        res.json(await unwatchWorkspaceFile({
            workspacePath: String(workspacePath || ''),
            filePath: String(filePath || '')
        }));
    }
    catch (error) {
        const err = error;
        log.error('failed to unwatch workspace file:', err);
        res.status(400).json({ error: err.message });
    }
});
/**
 * 保存工作区文件内容
 * POST /api/file-browser/workspace/write
 */
fileBrowserRouter.post('/workspace/write', validateToken, async (req, res) => {
    try {
        const { workspacePath, filePath, content } = req.body || {};
        const result = await writeWorkspaceFile({
            workspacePath: String(workspacePath || ''),
            filePath: String(filePath || ''),
            content: String(content ?? '')
        });
        res.json(result);
    }
    catch (error) {
        const err = error;
        console.error('[file-browser] failed to write workspace file:', err);
        res.status(400).json({ error: err.message });
    }
});
/**
 * 在工作区目录下创建文件或目录
 * POST /api/file-browser/workspace/create
 */
fileBrowserRouter.post('/workspace/create', validateToken, async (req, res) => {
    try {
        const { workspacePath, path: requestPath = '', entryName, isDirectory } = req.body || {};
        const result = await createWorkspaceEntry({
            workspacePath: String(workspacePath || ''),
            targetPath: String(requestPath || ''),
            entryName: String(entryName || ''),
            isDirectory: Boolean(isDirectory)
        });
        res.json(result);
    }
    catch (error) {
        const err = error;
        log.error('failed to create workspace entry:', err);
        res.status(400).json({ error: err.message });
    }
});
/**
 * 重命名工作区中的文件或目录
 * POST /api/file-browser/workspace/rename
 */
fileBrowserRouter.post('/workspace/rename', validateToken, async (req, res) => {
    try {
        const { workspacePath, targetPath, newName } = req.body || {};
        const result = await renameWorkspaceEntry({
            workspacePath: String(workspacePath || ''),
            targetPath: String(targetPath || ''),
            newName: String(newName || '')
        });
        res.json(result);
    }
    catch (error) {
        const err = error;
        log.error('failed to rename workspace entry:', err);
        res.status(400).json({ error: err.message });
    }
});
/**
 * 移动工作区中的文件或目录到另一个工作区目录
 * POST /api/file-browser/workspace/move
 */
fileBrowserRouter.post('/workspace/move', validateToken, async (req, res) => {
    try {
        const { workspacePath, targetPath, destinationPath } = req.body || {};
        const result = await moveWorkspaceEntry({
            workspacePath: String(workspacePath || ''),
            targetPath: String(targetPath || ''),
            destinationPath: String(destinationPath || '')
        });
        res.json(result);
    }
    catch (error) {
        const err = error;
        log.error('failed to move workspace entry:', err);
        res.status(400).json({ error: err.message });
    }
});
/**
 * 删除工作区中的文件或目录
 * POST /api/file-browser/workspace/delete
 */
fileBrowserRouter.post('/workspace/delete', validateToken, async (req, res) => {
    try {
        const { workspacePath, targetPath } = req.body || {};
        const result = await deleteWorkspaceEntry({
            workspacePath: String(workspacePath || ''),
            targetPath: String(targetPath || '')
        });
        res.json(result);
    }
    catch (error) {
        const err = error;
        log.error('failed to delete workspace entry:', err);
        res.status(400).json({ error: err.message });
    }
});
/**
 * 上传文件到工作区指定目录，可选自动解压常见压缩包。
 * POST /api/file-browser/workspace/upload
 */
fileBrowserRouter.post('/workspace/upload', validateToken, workspaceUploadMiddleware, async (req, res) => {
    const files = (req.files || []);
    try {
        const { workspacePath, path: requestPath = '', extractArchives } = req.body || {};
        const relativePaths = parseWorkspaceUploadRelativePaths(req.body || {});
        const result = await uploadWorkspaceFiles({
            workspacePath: String(workspacePath || ''),
            targetPath: String(requestPath || ''),
            extractArchives: toBoolean(extractArchives),
            files: files.map((file, index) => ({
                originalname: file.originalname,
                relativePath: relativePaths[index],
                path: file.path,
                size: file.size
            }))
        });
        res.json(result);
    }
    catch (error) {
        const err = error;
        log.error('failed to upload workspace files:', err);
        res.status(400).json({ error: err.message });
    }
    finally {
        await cleanupUploadedFiles(files);
    }
});
/**
 * 下载工作区文件或目录；目录会实时打包为 zip。
 * POST /api/file-browser/workspace/download
 */
fileBrowserRouter.post('/workspace/download', validateToken, async (req, res) => {
    try {
        const { workspacePath, targetPath } = req.body || {};
        const target = await resolveWorkspaceDownloadTarget({
            workspacePath: String(workspacePath || ''),
            targetPath: String(targetPath || '')
        });
        res.setHeader('Content-Type', target.contentType);
        res.setHeader('Content-Disposition', contentDispositionFileName(target.fileName));
        res.setHeader('X-Workspace-Path', encodeURIComponent(target.workspacePath));
        res.setHeader('X-Workspace-Target-Path', encodeURIComponent(target.targetPath));
        if (target.isDirectory) {
            await streamWorkspaceDirectoryZip(target.resolvedTargetPath, res);
            return;
        }
        createReadStream(target.resolvedTargetPath).pipe(res);
    }
    catch (error) {
        const err = error;
        log.error('failed to download workspace entry:', err);
        if (!res.headersSent) {
            res.status(400).json({ error: err.message });
        }
        else {
            res.destroy(err);
        }
    }
});
/**
 * 解压工作区内的压缩包到指定目录。
 * POST /api/file-browser/workspace/extract
 */
fileBrowserRouter.post('/workspace/extract', validateToken, async (req, res) => {
    try {
        const { workspacePath, archivePath, outputPath = '' } = req.body || {};
        const result = await extractWorkspaceArchive({
            workspacePath: String(workspacePath || ''),
            archivePath: String(archivePath || ''),
            outputPath: String(outputPath || '')
        });
        res.json(result);
    }
    catch (error) {
        const err = error;
        log.error('failed to extract workspace archive:', err);
        res.status(400).json({ error: err.message });
    }
});
