/*
 * Copyright 2026, Jaime Terrats.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { closeSync, createReadStream, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, rmSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
import { dirname, extname, join, relative, resolve } from 'node:path';
import { Readable } from 'node:stream';
import { Hono } from 'hono';
import { getWorkspaceCwd } from '../middleware/workspace.js';
const IGNORED_DIRS = new Set([
    'node_modules',
    '.git',
    '.hg',
    '.svn',
    'dist',
    'build',
    'out',
    'lib',
    '.next',
    '.nuxt',
    'coverage',
    '__pycache__',
    '.tsbuildinfo',
    '.claude',
    'schemas',
    'fixtures',
    'test-fixtures',
]);
const MAX_DEPTH = 6;
const MAX_FILES = 500;
function scanDir(dir, baseDir, depth, results, exts) {
    if (depth > MAX_DEPTH || results.length >= MAX_FILES)
        return;
    if (!existsSync(dir))
        return;
    let entries;
    try {
        entries = readdirSync(dir, { withFileTypes: true });
    }
    catch {
        return;
    }
    for (const entry of entries) {
        if (results.length >= MAX_FILES)
            return;
        if (entry.isDirectory()) {
            if (IGNORED_DIRS.has(entry.name))
                continue;
            if (depth === 0 && entry.name.startsWith('.'))
                continue;
            scanDir(join(dir, entry.name), baseDir, depth + 1, results, exts);
        }
        else if (entry.isFile()) {
            const ext = extname(entry.name).toLowerCase();
            if (exts && !exts.has(ext))
                continue;
            try {
                const fullPath = join(dir, entry.name);
                const stat = statSync(fullPath);
                results.push({
                    path: relative(baseDir, fullPath),
                    name: entry.name,
                    ext: ext.slice(1),
                    size: stat.size,
                    modified: stat.mtime.toISOString(),
                });
            }
            catch {
                // skip unreadable files
            }
        }
    }
}
const DIAGRAM_EXCLUDED_PATH_PREFIXES = ['node_modules/', '.setup-agents/state/', '.agent-workflow/'];
const DIAGRAM_EXCLUDED_JSON_PATTERNS = [
    /-spec\.json$/i,
    /-config\.json$/i,
    /^package\.json$/i,
    /^tsconfig[^/]*\.json$/i,
    /\.lock$/i,
];
function isExcludedDiagramPath(relativePath) {
    for (const prefix of DIAGRAM_EXCLUDED_PATH_PREFIXES) {
        if (relativePath.startsWith(prefix))
            return true;
    }
    return false;
}
function isExcludedJsonFilename(filename) {
    for (const pattern of DIAGRAM_EXCLUDED_JSON_PATTERNS) {
        if (pattern.test(filename))
            return true;
    }
    return false;
}
function isDiagramJson(filePath, fileSize) {
    try {
        const readSize = Math.min(fileSize, 8192);
        const fd = openSync(filePath, 'r');
        const buf = Buffer.alloc(readSize);
        readSync(fd, buf, 0, readSize, 0);
        closeSync(fd);
        const raw = buf.toString('utf-8');
        if (raw.includes('"backbones"') && raw.includes('"cards"'))
            return true;
        if (raw.includes('"pages"') && raw.includes('"items"'))
            return true;
        if (raw.includes('"nodes"') && raw.includes('"edges"'))
            return true;
        if (raw.includes('"mxGraphModel"') || raw.includes('"mxfile"'))
            return true;
        return false;
    }
    catch {
        return false;
    }
}
function parseCsvLine(line) {
    const fields = [];
    let current = '';
    let inQuotes = false;
    for (let i = 0; i < line.length; i++) {
        const ch = line[i];
        if (inQuotes) {
            if (ch === '"') {
                if (i + 1 < line.length && line[i + 1] === '"') {
                    current += '"';
                    i++;
                }
                else {
                    inQuotes = false;
                }
            }
            else {
                current += ch;
            }
        }
        else if (ch === '"') {
            inQuotes = true;
        }
        else if (ch === ',') {
            fields.push(current);
            current = '';
        }
        else {
            current += ch;
        }
    }
    fields.push(current);
    return fields;
}
export function filesRoute(cwd) {
    const app = new Hono();
    app.get('/scan', (c) => {
        const dir = getWorkspaceCwd(c, cwd);
        const extParam = c.req.query('ext');
        const exts = extParam ? new Set(extParam.split(',').map((e) => (e.startsWith('.') ? e : `.${e}`))) : undefined;
        const results = [];
        scanDir(dir, dir, 0, results, exts);
        return c.json({ files: results, total: results.length, truncated: results.length >= MAX_FILES });
    });
    app.get('/diagrams', (c) => {
        const dir = getWorkspaceCwd(c, cwd);
        const diagramExts = new Set(['.drawio', '.svg', '.mmd']);
        const allExts = new Set(['.drawio', '.svg', '.mmd', '.json', '.tsx']);
        const results = [];
        scanDir(dir, dir, 0, results, allExts);
        const diagrams = results.filter((f) => {
            if (isExcludedDiagramPath(f.path))
                return false;
            if (diagramExts.has(`.${f.ext}`))
                return true;
            if (f.ext === 'tsx') {
                return f.name.includes('.canvas.');
            }
            if (f.ext === 'json') {
                if (isExcludedJsonFilename(f.name))
                    return false;
                return isDiagramJson(join(dir, f.path), f.size);
            }
            return false;
        });
        return c.json({ files: diagrams, total: diagrams.length });
    });
    app.get('/storymaps', (c) => {
        const dir = getWorkspaceCwd(c, cwd);
        const jsonExts = new Set(['.json']);
        const results = [];
        scanDir(dir, dir, 0, results, jsonExts);
        const storymaps = results.filter((f) => {
            const filePath = join(dir, f.path);
            try {
                const readSize = Math.min(f.size, 8192);
                const fd = openSync(filePath, 'r');
                const buf = Buffer.alloc(readSize);
                readSync(fd, buf, 0, readSize, 0);
                closeSync(fd);
                const raw = buf.toString('utf-8');
                if (raw.includes('"backbones"'))
                    return true;
                if (raw.includes('"pages"') && raw.includes('"items"'))
                    return true;
                if (raw.includes('"epics"') && raw.includes('"stories"'))
                    return true;
                return false;
            }
            catch {
                return false;
            }
        });
        return c.json({ files: storymaps, total: storymaps.length });
    });
    app.get('/read/:path{.+}', (c) => {
        const dir = getWorkspaceCwd(c, cwd);
        const filePath = c.req.param('path');
        if (!filePath)
            return c.json({ error: 'No file path provided' }, 400);
        const resolved = resolve(dir, filePath);
        if (!resolved.startsWith(dir))
            return c.json({ error: 'Forbidden' }, 403);
        if (!existsSync(resolved))
            return c.json({ error: 'Not found' }, 404);
        try {
            const stat = statSync(resolved);
            if (!stat.isFile())
                return c.json({ error: 'Not a file' }, 400);
            if (stat.size > 5 * 1024 * 1024)
                return c.json({ error: 'File too large (>5MB)' }, 413);
            const content = readFileSync(resolved, 'utf-8');
            const ext = extname(filePath).toLowerCase().slice(1);
            return c.json({ content, ext, size: stat.size });
        }
        catch {
            return c.json({ error: 'Failed to read file' }, 500);
        }
    });
    const WRITABLE_EXTS = new Set([
        'drawio',
        'svg',
        'json',
        'ts',
        'tsx',
        'js',
        'jsx',
        'mjs',
        'cjs',
        'css',
        'scss',
        'html',
        'md',
        'yaml',
        'yml',
        'toml',
        'xml',
        'txt',
        'sh',
        'py',
        'rb',
        'rs',
        'go',
        'java',
        'kt',
        'swift',
        'c',
        'cpp',
        'h',
        'hpp',
        'cls',
        'apex',
        'trigger',
    ]);
    const MAX_UPLOAD_BYTES = 10 * 1024 * 1024; // 10 MB
    app.post('/create/:path{.+}', async (c) => {
        const dir = getWorkspaceCwd(c, cwd);
        const filePath = c.req.param('path');
        if (!filePath)
            return c.json({ error: 'No file path provided' }, 400);
        const resolved = resolve(dir, filePath);
        // Path traversal prevention — resolved must stay within workspace
        if (!resolved.startsWith(dir + '/') && resolved !== dir) {
            return c.json({ error: 'Forbidden: path traversal detected' }, 403);
        }
        if (existsSync(resolved)) {
            return c.json({ error: 'File already exists' }, 409);
        }
        const body = await c.req.parseBody();
        const file = body['file'];
        if (!file || !(file instanceof File)) {
            return c.json({ error: 'Missing file in multipart body' }, 400);
        }
        const arrayBuffer = await file.arrayBuffer();
        const data = Buffer.from(arrayBuffer);
        if (data.length > MAX_UPLOAD_BYTES) {
            return c.json({ error: 'File exceeds maximum size of 10MB' }, 413);
        }
        try {
            mkdirSync(dirname(resolved), { recursive: true });
            writeFileSync(resolved, data);
            return c.json({ ok: true, path: filePath, size: data.length }, 201);
        }
        catch (err) {
            const message = err instanceof Error ? err.message : 'Failed to create file';
            return c.json({ error: message }, 500);
        }
    });
    app.put('/write/:path{.+}', async (c) => {
        const dir = getWorkspaceCwd(c, cwd);
        const filePath = c.req.param('path');
        if (!filePath)
            return c.json({ error: 'No file path provided' }, 400);
        const resolved = resolve(dir, filePath);
        if (!resolved.startsWith(dir + '/') && resolved !== dir) {
            return c.json({ error: 'Forbidden: path traversal detected' }, 403);
        }
        const ext = extname(filePath).toLowerCase().slice(1);
        if (!WRITABLE_EXTS.has(ext))
            return c.json({ error: `Cannot write .${ext} files` }, 400);
        const body = await c.req.json();
        if (!body.content || typeof body.content !== 'string') {
            return c.json({ error: 'Missing content field' }, 400);
        }
        if (body.content.length > 10 * 1024 * 1024) {
            return c.json({ error: 'Content too large (>10MB)' }, 413);
        }
        try {
            const isNew = !existsSync(resolved);
            // Create parent directories if they don't exist (supports applying new files)
            const parentDir = dirname(resolved);
            if (!existsSync(parentDir)) {
                mkdirSync(parentDir, { recursive: true });
            }
            writeFileSync(resolved, body.content, 'utf-8');
            return c.json({ ok: true, path: filePath, size: body.content.length, created: isNew });
        }
        catch {
            return c.json({ error: 'Failed to write file' }, 500);
        }
    });
    // ─── Raw binary file serving ─────────────────────────────────────────────
    app.get('/raw/:path{.+}', (c) => {
        const dir = getWorkspaceCwd(c, cwd);
        const filePath = c.req.param('path');
        if (!filePath)
            return c.json({ error: 'No file path provided' }, 400);
        const resolved = resolve(dir, filePath);
        if (!resolved.startsWith(dir + '/') && resolved !== dir) {
            return c.json({ error: 'Forbidden: path traversal detected' }, 403);
        }
        if (!existsSync(resolved))
            return c.json({ error: 'Not found' }, 404);
        try {
            const stat = statSync(resolved);
            if (!stat.isFile())
                return c.json({ error: 'Not a file' }, 400);
            const ext = extname(filePath).toLowerCase();
            const contentType = ext === '.pdf' ? 'application/pdf' : 'application/octet-stream';
            const stream = createReadStream(resolved);
            const webStream = Readable.toWeb(stream);
            c.header('Content-Type', contentType);
            c.header('Content-Length', String(stat.size));
            const fileName = filePath.split('/').pop() ?? filePath;
            c.header('Content-Disposition', `inline; filename="${fileName}"`);
            return c.body(webStream);
        }
        catch {
            return c.json({ error: 'Failed to read file' }, 500);
        }
    });
    // ─── Render metadata endpoint ──────────────────────────────────────────────
    app.get('/render/:path{.+}', (c) => {
        const dir = getWorkspaceCwd(c, cwd);
        const filePath = c.req.param('path');
        if (!filePath)
            return c.json({ error: 'No file path provided' }, 400);
        const resolved = resolve(dir, filePath);
        if (!resolved.startsWith(dir + '/') && resolved !== dir) {
            return c.json({ error: 'Forbidden: path traversal detected' }, 403);
        }
        if (!existsSync(resolved))
            return c.json({ error: 'Not found' }, 404);
        try {
            const stat = statSync(resolved);
            if (!stat.isFile())
                return c.json({ error: 'Not a file' }, 400);
            const ext = extname(filePath).toLowerCase();
            // PDF — rendered via iframe pointing to raw endpoint
            if (ext === '.pdf') {
                return c.json({ type: 'pdf', url: `/api/v1/files/raw/${filePath}` });
            }
            // CSV — parse into table structure
            if (ext === '.csv') {
                if (stat.size > 5 * 1024 * 1024) {
                    return c.json({ type: 'binary', url: `/api/v1/files/raw/${filePath}`, size: stat.size });
                }
                const raw = readFileSync(resolved, 'utf-8');
                const lines = raw.split(/\r?\n/).filter((l) => l.trim().length > 0);
                const headers = lines.length > 0 ? parseCsvLine(lines[0]) : [];
                const rows = lines.slice(1).map((line) => parseCsvLine(line));
                return c.json({ type: 'table', sheets: [{ name: 'Sheet1', headers, rows }] });
            }
            // Binary types — docx, xlsx, and other non-text extensions
            const binaryExts = new Set([
                '.docx',
                '.xlsx',
                '.xls',
                '.doc',
                '.pptx',
                '.ppt',
                '.zip',
                '.tar',
                '.gz',
                '.png',
                '.jpg',
                '.jpeg',
                '.gif',
                '.bmp',
                '.ico',
                '.woff',
                '.woff2',
                '.ttf',
                '.eot',
            ]);
            if (binaryExts.has(ext)) {
                return c.json({ type: 'binary', url: `/api/v1/files/raw/${filePath}`, size: stat.size });
            }
            // Text fallback — try reading as UTF-8
            if (stat.size > 5 * 1024 * 1024) {
                return c.json({ type: 'binary', url: `/api/v1/files/raw/${filePath}`, size: stat.size });
            }
            const content = readFileSync(resolved, 'utf-8');
            return c.json({ type: 'text', content, ext: ext.slice(1) });
        }
        catch {
            return c.json({ error: 'Failed to process file' }, 500);
        }
    });
    app.delete('/:path{.+}', (c) => {
        const dir = getWorkspaceCwd(c, cwd);
        const filePath = c.req.param('path');
        if (!filePath)
            return c.json({ error: 'No file path provided' }, 400);
        const resolved = resolve(dir, filePath);
        if (!resolved.startsWith(dir + '/') && resolved !== dir) {
            return c.json({ error: 'Forbidden: path traversal detected' }, 403);
        }
        if (!existsSync(resolved))
            return c.json({ error: 'Not found' }, 404);
        try {
            const stat = statSync(resolved);
            if (stat.isDirectory()) {
                rmSync(resolved, { recursive: true });
            }
            else {
                unlinkSync(resolved);
            }
            return c.json({ ok: true, path: filePath });
        }
        catch (err) {
            const message = err instanceof Error ? err.message : 'Failed to delete';
            return c.json({ error: message }, 500);
        }
    });
    return app;
}
//# sourceMappingURL=files.js.map