/**
 * @fileoverview System, config, settings, subagent, and debug routes.
 * Covers status, stats, config CRUD, settings, subagent monitoring,
 * debug/memory, lifecycle logs, screenshots, and various persistence endpoints.
 */
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { existsSync, mkdirSync, readdirSync } from 'node:fs';
import fs from 'node:fs/promises';
import { totalmem, freemem, loadavg, cpus } from 'node:os';
import { execSync, spawn } from 'node:child_process';
import { randomBytes } from 'node:crypto';
import { dataPath } from '../../config/instance.js';
import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.js';
import { isUnauthenticatedNetworkAcknowledged } from '../network-auth-policy.js';
import { ConfigUpdateSchema, SettingsUpdateSchema, ModelConfigUpdateSchema, CpuLimitSchema, SubagentWindowStatesSchema, SubagentParentMapSchema, RevokeSessionSchema, } from '../schemas.js';
import { subagentWatcher } from '../../subagent-watcher.js';
import { imageWatcher } from '../../image-watcher.js';
import { getLifecycleLog } from '../../session-lifecycle-log.js';
import { findSessionOrFail, formatUptime, parseBody, readJsonConfig, toggleService, SETTINGS_PATH, } from '../route-helpers.js';
import { SseEvent } from '../sse-events.js';
import { getInstallInfo, checkForUpdate, startUpdate, getUpdateStatusForApi } from '../self-update.js';
import { AUTH_COOKIE_NAME } from '../middleware/auth.js';
import { QR_AUTH_FAILURE_MAX } from '../../config/tunnel-config.js';
import { AUTH_SESSION_TTL_MS } from '../../config/auth-config.js';
// Maximum screenshot upload size (10MB)
const MAX_SCREENSHOT_SIZE = 10 * 1024 * 1024;
// Screenshots directory
const SCREENSHOTS_DIR = dataPath('screenshots');
/** Cached CPU count — doesn't change at runtime */
const CPU_COUNT = cpus().length;
/** Get system CPU and memory usage */
function getSystemStats() {
    try {
        const totalMem = totalmem();
        // macOS: os.freemem() only returns truly free pages, not cached/purgeable memory.
        // Use vm_stat to get accurate used memory (wired + active + compressed).
        let usedMem;
        if (process.platform === 'darwin') {
            try {
                const vmstat = execSync('vm_stat', { encoding: 'utf-8', timeout: 2000 });
                const pageSize = parseInt(vmstat.match(/page size of (\d+)/)?.[1] || '4096', 10);
                const wired = parseInt(vmstat.match(/Pages wired down:\s+(\d+)/)?.[1] || '0', 10);
                const active = parseInt(vmstat.match(/Pages active:\s+(\d+)/)?.[1] || '0', 10);
                const compressed = parseInt(vmstat.match(/Pages occupied by compressor:\s+(\d+)/)?.[1] || '0', 10);
                usedMem = (wired + active + compressed) * pageSize;
            }
            catch {
                usedMem = totalMem - freemem();
            }
        }
        else {
            usedMem = totalMem - freemem();
        }
        // CPU load average (1 min) as percentage (rough approximation)
        const load = loadavg()[0];
        const cpuPercent = Math.min(100, Math.round((load / CPU_COUNT) * 100));
        return {
            cpu: cpuPercent,
            memory: {
                usedMB: Math.round(usedMem / (1024 * 1024)),
                totalMB: Math.round(totalMem / (1024 * 1024)),
                percent: Math.round((usedMem / totalMem) * 100),
            },
        };
    }
    catch {
        return {
            cpu: 0,
            memory: { usedMB: 0, totalMB: 0, percent: 0 },
        };
    }
}
/**
 * Build the URL the spanning browser window should open, pinned to localhost.
 * Takes only a digits-only port from the (untrusted) Host header so nothing
 * attacker-controllable reaches the launched browser; falls back to the default
 * port when the header is absent/odd. Exported for unit testing.
 */
export function resolveSpanUrl(hostHeader, fallbackPort = '3000') {
    const hostPort = String(hostHeader ?? '').split(':')[1] ?? '';
    const port = /^\d+$/.test(hostPort) ? hostPort : fallbackPort;
    return `http://localhost:${port}`;
}
export function registerSystemRoutes(app, ctx) {
    const windowStatesPath = dataPath('subagent-window-states.json');
    const parentMapPath = dataPath('subagent-parents.json');
    // ═══════════════════════════════════════════════════════════════
    // System Status & Health
    // ═══════════════════════════════════════════════════════════════
    // ========== Status ==========
    app.get('/api/status', async () => ctx.getLightState());
    // ========== Tunnel ==========
    app.get('/api/tunnel/status', async () => ctx.tunnelManager.getStatus());
    app.get('/api/tunnel/info', async () => {
        const status = ctx.tunnelManager.getStatus();
        const sseClients = ctx.getSseClientCount();
        const sessions = [];
        if (ctx.authSessions) {
            for (const [, record] of ctx.authSessions) {
                sessions.push({ ip: record.ip, ua: record.ua, createdAt: record.createdAt, method: record.method });
            }
        }
        return {
            ...status,
            sseClients,
            authEnabled: !!process.env.CODEMAN_PASSWORD,
            authSessions: sessions,
        };
    });
    app.get('/api/tunnel/qr', async (_req, reply) => {
        const url = ctx.tunnelManager.getUrl();
        if (!url) {
            return reply.code(404).send(createErrorResponse(ApiErrorCode.NOT_FOUND, 'Tunnel not running'));
        }
        try {
            const authPassword = process.env.CODEMAN_PASSWORD;
            if (authPassword) {
                // Auth enabled — use cached SVG with embedded short code
                const svg = await ctx.tunnelManager.getQrSvg(url);
                return { svg, authEnabled: true };
            }
            // No auth — just encode the raw tunnel URL
            const QRCode = await import('qrcode');
            const svg = await QRCode.toString(url, { type: 'svg', margin: 2, width: 256 });
            return { svg, authEnabled: false };
        }
        catch (err) {
            return reply.code(500).send(createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err)));
        }
    });
    // ═══════════════════════════════════════════════════════════════
    // Authentication (QR auth, session revocation)
    // ═══════════════════════════════════════════════════════════════
    // ========== QR Auth Route ==========
    app.get('/q/:code', async (req, reply) => {
        const shortCode = req.params.code;
        const authPassword = process.env.CODEMAN_PASSWORD;
        // No point if auth isn't enabled — just redirect
        if (!authPassword) {
            return reply.redirect('/');
        }
        const clientIp = req.ip;
        // Per-IP rate limit (separate counter from Basic Auth failures)
        const qrFailures = ctx.qrAuthFailures?.get(clientIp) ?? 0;
        if (qrFailures >= QR_AUTH_FAILURE_MAX) {
            return reply.code(429).send('Too Many Requests');
        }
        // Validate and atomically consume the token
        if (!shortCode || !ctx.tunnelManager.consumeToken(shortCode)) {
            ctx.qrAuthFailures?.set(clientIp, qrFailures + 1);
            return reply.code(401).send('Invalid or expired QR code');
        }
        // Issue session cookie (same pattern as Basic Auth success path)
        const sessionToken = randomBytes(32).toString('hex');
        const clientUA = req.headers['user-agent'] ?? '';
        ctx.authSessions?.set(sessionToken, {
            ip: clientIp,
            ua: clientUA,
            createdAt: Date.now(),
            method: 'qr',
        });
        ctx.qrAuthFailures?.delete(clientIp);
        // Audit log
        const lifecycleLog = getLifecycleLog();
        lifecycleLog.log({
            event: 'qr_auth',
            sessionId: 'system',
            extra: {
                ip: clientIp,
                ua: clientUA,
                shortCodePrefix: shortCode.slice(0, 3) + '***',
            },
        });
        reply.setCookie(AUTH_COOKIE_NAME, sessionToken, {
            httpOnly: true,
            secure: ctx.https,
            sameSite: 'lax',
            maxAge: AUTH_SESSION_TTL_MS / 1000,
            path: '/',
        });
        // Broadcast auth notification — desktop sees who authenticated
        ctx.broadcast(SseEvent.TunnelQrAuthUsed, {
            ip: clientIp,
            ua: clientUA,
            timestamp: Date.now(),
        });
        return reply.redirect('/');
    });
    // ========== QR Regeneration ==========
    app.post('/api/tunnel/qr/regenerate', async () => {
        ctx.tunnelManager.regenerateQrToken();
        return {};
    });
    // ========== Auth Session Revocation ==========
    app.post('/api/auth/revoke', async (req) => {
        const result = RevokeSessionSchema.safeParse(req.body);
        if (result.success && result.data.sessionToken) {
            ctx.authSessions?.delete(result.data.sessionToken);
        }
        else {
            // Revoke all sessions (nuclear option)
            ctx.authSessions?.clear();
        }
        return {};
    });
    // ═══════════════════════════════════════════════════════════════
    // Multi-monitor: span Codeman across all displays
    // ═══════════════════════════════════════════════════════════════
    // Spawn scripts/span-codeman.sh, which opens a fresh, maximized browser --app
    // window sized to the union of all displays — so in-page floating session
    // panels can be dragged across the physical monitor seam. macOS only; needs
    // the one-time "Displays have separate Spaces" OFF prerequisite (see script).
    app.post('/api/system/span-displays', async (req, reply) => {
        // macOS only: the launcher uses osascript + Finder desktop bounds and Chrome
        // --app geometry flags. Fail clearly elsewhere instead of spawning a bash
        // that errors out invisibly (the toast would otherwise lie "Opening…").
        if (process.platform !== 'darwin') {
            return reply
                .code(400)
                .send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Multi-monitor spanning runs on the Codeman server, which is not macOS. ' +
                'If your monitors are on a remote Mac, run scripts/span-codeman.sh locally on that Mac with this server URL — see the script header for details.'));
        }
        // Resolve the bundled launcher relative to this module (works from src/ and dist/).
        const scriptPath = join(dirname(fileURLToPath(import.meta.url)), '../../../scripts/span-codeman.sh');
        if (!existsSync(scriptPath)) {
            return reply.code(500).send(createErrorResponse(ApiErrorCode.INTERNAL_ERROR, 'span-codeman.sh not found'));
        }
        // Point the spanning window at THIS server (localhost + sanitized port).
        const url = resolveSpanUrl(req.headers.host);
        try {
            const child = spawn('bash', [scriptPath, url], { detached: true, stdio: 'ignore' });
            child.on('error', (err) => app.log.error({ err }, 'span-displays launch failed'));
            child.unref();
            return { url };
        }
        catch (err) {
            return reply.code(500).send(createErrorResponse(ApiErrorCode.INTERNAL_ERROR, getErrorMessage(err)));
        }
    });
    // ═══════════════════════════════════════════════════════════════
    // Self-Update (App Settings → Updates)
    // ═══════════════════════════════════════════════════════════════
    // Install info + whether a newer release exists. Manual, user-triggered.
    app.get('/api/system/update/check', async () => {
        const check = await checkForUpdate();
        const info = getInstallInfo();
        return { ...info, ...check };
    });
    // Poll target for update progress — survives the restart the update triggers.
    app.get('/api/system/update/status', async () => getUpdateStatusForApi());
    // Kick off a detached update to the latest release. Returns immediately; the
    // browser then polls /api/system/update/status across the service restart.
    app.post('/api/system/update', async (_req, reply) => {
        const result = await startUpdate();
        if (result.ok) {
            return { updateId: result.updateId, toTag: result.toTag, toVersion: result.toVersion };
        }
        const map = {
            'in-flight': { http: 409, api: ApiErrorCode.ALREADY_EXISTS },
            'up-to-date': { http: 409, api: ApiErrorCode.ALREADY_EXISTS },
            'not-git': { http: 400, api: ApiErrorCode.INVALID_INPUT },
            disabled: { http: 403, api: ApiErrorCode.INVALID_INPUT },
            'bad-tag': { http: 400, api: ApiErrorCode.INVALID_INPUT },
            error: { http: 500, api: ApiErrorCode.INTERNAL_ERROR },
        };
        const m = map[result.code];
        return reply.code(m.http).send(createErrorResponse(m.api, result.message));
    });
    // ═══════════════════════════════════════════════════════════════
    // CLI Integrations (OpenCode)
    // ═══════════════════════════════════════════════════════════════
    // ========== OpenCode ==========
    app.get('/api/opencode/status', async () => {
        const { isOpenCodeAvailable, resolveOpenCodeDir } = await import('../../utils/opencode-cli-resolver.js');
        return {
            available: isOpenCodeAvailable(),
            path: resolveOpenCodeDir(),
        };
    });
    app.get('/api/codex/status', async () => {
        const { isCodexAvailable, resolveCodexDir } = await import('../../utils/codex-cli-resolver.js');
        return {
            available: isCodexAvailable(),
            path: resolveCodexDir(),
        };
    });
    // ═══════════════════════════════════════════════════════════════
    // State & Lifecycle (cleanup, lifecycle log, stats)
    // ═══════════════════════════════════════════════════════════════
    // ========== State & Lifecycle ==========
    app.post('/api/cleanup-state', async () => {
        const activeSessionIds = new Set(ctx.sessions.keys());
        const result = ctx.store.cleanupStaleSessions(activeSessionIds);
        const lifecycleLog = getLifecycleLog();
        for (const s of result.cleaned) {
            lifecycleLog.log({ event: 'stale_cleaned', sessionId: s.id, name: s.name });
        }
        return { cleanedSessions: result.count };
    });
    app.get('/api/session-lifecycle', async (req) => {
        const query = req.query;
        const lifecycleLog = getLifecycleLog();
        const entries = await lifecycleLog.query({
            sessionId: query.sessionId,
            event: query.event,
            since: query.since ? Number(query.since) : undefined,
            limit: query.limit ? Math.min(Number(query.limit), 1000) : 200,
        });
        return { entries };
    });
    // ========== Stats ==========
    function collectActiveTokens() {
        const tokens = {};
        for (const [sessionId, session] of ctx.sessions) {
            tokens[sessionId] = {
                inputTokens: session.inputTokens,
                outputTokens: session.outputTokens,
                totalCost: session.totalCost,
            };
        }
        return tokens;
    }
    app.get('/api/stats', async () => {
        const activeSessionTokens = collectActiveTokens();
        return {
            stats: ctx.store.getAggregateStats(activeSessionTokens),
            raw: ctx.store.getGlobalStats(),
        };
    });
    app.get('/api/token-stats', async () => {
        const activeSessionTokens = collectActiveTokens();
        return {
            daily: ctx.store.getDailyStats(30),
            totals: ctx.store.getAggregateStats(activeSessionTokens),
        };
    });
    // ═══════════════════════════════════════════════════════════════
    // Configuration & Settings (config, settings, model config, CPU priority)
    // ═══════════════════════════════════════════════════════════════
    // ========== Config ==========
    app.get('/api/config', async () => {
        return { config: ctx.store.getConfig() };
    });
    app.put('/api/config', async (req) => {
        const configData = parseBody(ConfigUpdateSchema, req.body, 'Invalid config');
        ctx.store.setConfig(configData);
        return { config: ctx.store.getConfig() };
    });
    // ========== Debug/Memory ==========
    app.get('/api/debug/memory', async () => {
        const mem = process.memoryUsage();
        const subagentStats = subagentWatcher.getStats();
        const serverMapSizes = {
            sessions: ctx.sessions.size,
            runSummaryTrackers: ctx.runSummaryTrackers.size,
            scheduledRuns: ctx.scheduledRuns.size,
            activePlanOrchestrators: ctx.activePlanOrchestrators.size,
        };
        const totalServerMapEntries = Object.values(serverMapSizes).reduce((a, b) => a + b, 0);
        const totalSubagentMapEntries = Object.values(subagentStats).reduce((a, b) => a + b, 0);
        return {
            memory: {
                rss: mem.rss,
                rssMB: Math.round((mem.rss / 1024 / 1024) * 10) / 10,
                heapUsed: mem.heapUsed,
                heapUsedMB: Math.round((mem.heapUsed / 1024 / 1024) * 10) / 10,
                heapTotal: mem.heapTotal,
                heapTotalMB: Math.round((mem.heapTotal / 1024 / 1024) * 10) / 10,
                external: mem.external,
                externalMB: Math.round((mem.external / 1024 / 1024) * 10) / 10,
                arrayBuffers: mem.arrayBuffers,
                arrayBuffersMB: Math.round((mem.arrayBuffers / 1024 / 1024) * 10) / 10,
            },
            mapSizes: {
                server: serverMapSizes,
                subagentWatcher: subagentStats,
                totals: {
                    serverEntries: totalServerMapEntries,
                    subagentEntries: totalSubagentMapEntries,
                    allEntries: totalServerMapEntries + totalSubagentMapEntries,
                },
            },
            watchers: {
                fileDebouncers: subagentStats.fileDebouncerCount,
                dirWatchers: subagentStats.dirWatcherCount,
                total: subagentStats.fileDebouncerCount + subagentStats.dirWatcherCount,
            },
            timers: {
                subagentIdleTimers: subagentStats.idleTimerCount,
                total: subagentStats.idleTimerCount,
            },
            uptime: {
                seconds: Math.round(process.uptime()),
                formatted: formatUptime(process.uptime()),
            },
            timestamp: Date.now(),
        };
    });
    // ========== System Stats ==========
    app.get('/api/system/stats', async () => {
        return getSystemStats();
    });
    // ========== Settings ==========
    app.get('/api/settings', async () => {
        return readJsonConfig(SETTINGS_PATH, 'settings', {});
    });
    app.put('/api/settings', async (req) => {
        const settings = parseBody(SettingsUpdateSchema, req.body, 'Invalid settings');
        // COD-55: enabling the Cloudflare tunnel publishes the whole app (full terminal
        // control = effectively RCE) to a public *.trycloudflare.com URL. Because the
        // tunnel binds to loopback, server.ts's non-loopback bind guard never trips, and
        // with no CODEMAN_PASSWORD the auth middleware is inactive — so the tunnel URL is
        // unauthenticated. Refuse to start a tunnel unless auth is configured OR the
        // operator has acknowledged unauthenticated-network exposure. A public tunnel is
        // higher-stakes than a LAN bind, so this is REFUSE (vs the bind guard's warn).
        // Guard runs BEFORE persisting so a refused tunnelEnabled:true is not saved.
        if (settings.tunnelEnabled === true && !ctx.tunnelManager.isRunning() && !isUnauthenticatedNetworkAcknowledged()) {
            const msg = 'Refusing to start the Cloudflare tunnel without authentication: it would publish ' +
                'full terminal control to a public URL with no password. Set CODEMAN_PASSWORD to ' +
                'require login, or set CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK=1 to acknowledge an ' +
                'unauthenticated public tunnel.';
            throw Object.assign(new Error(msg), {
                statusCode: 403,
                body: createErrorResponse(ApiErrorCode.OPERATION_FAILED, msg),
            });
        }
        try {
            const dir = dirname(SETTINGS_PATH);
            if (!existsSync(dir)) {
                mkdirSync(dir, { recursive: true });
            }
            let existing = {};
            try {
                existing = JSON.parse(await fs.readFile(SETTINGS_PATH, 'utf-8'));
            }
            catch {
                /* ignore */
            }
            const merged = { ...existing, ...settings };
            await fs.writeFile(SETTINGS_PATH, JSON.stringify(merged, null, 2));
            // Handle subagent tracking toggle dynamically
            toggleService(settings.subagentTrackingEnabled ?? true, subagentWatcher, 'Subagent watcher');
            // Handle image watcher toggle dynamically
            toggleService(settings.imageWatcherEnabled ?? false, imageWatcher, 'Image watcher', () => {
                // Re-watch all active sessions that have image watcher enabled
                for (const session of ctx.sessions.values()) {
                    if (session.imageWatcherEnabled) {
                        imageWatcher.watchSession(session.id, session.workingDir);
                    }
                }
            });
            // Handle tunnel toggle dynamically
            if ('tunnelEnabled' in settings) {
                const tunnelEnabled = settings.tunnelEnabled;
                if (tunnelEnabled && !ctx.tunnelManager.isRunning()) {
                    ctx.tunnelManager.start(ctx.port, ctx.https);
                    console.log('Tunnel started via settings change');
                }
                else if (tunnelEnabled && ctx.tunnelManager.isRunning() && ctx.tunnelManager.getUrl()) {
                    // Tunnel already running — re-emit so the client gets the URL
                    ctx.broadcast(SseEvent.TunnelStarted, { url: ctx.tunnelManager.getUrl() });
                    console.log('Tunnel already running, re-broadcast URL to client');
                }
                else if (!tunnelEnabled && ctx.tunnelManager.isRunning()) {
                    ctx.tunnelManager.stop();
                    console.log('Tunnel stopped via settings change');
                }
            }
            return {};
        }
        catch (err) {
            return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
        }
    });
    // ========== Model Configuration ==========
    app.get('/api/execution/model-config', async () => {
        const settings = await readJsonConfig(SETTINGS_PATH, 'model config', {});
        return { success: true, data: settings.modelConfig || {} };
    });
    app.put('/api/execution/model-config', async (req) => {
        const modelConfig = parseBody(ModelConfigUpdateSchema, req.body, 'Invalid model config');
        try {
            let existingSettings = {};
            try {
                const content = await fs.readFile(SETTINGS_PATH, 'utf-8');
                existingSettings = JSON.parse(content);
            }
            catch {
                // File doesn't exist yet, start fresh
            }
            existingSettings.modelConfig = modelConfig;
            const dir = dirname(SETTINGS_PATH);
            if (!existsSync(dir)) {
                mkdirSync(dir, { recursive: true });
            }
            await fs.writeFile(SETTINGS_PATH, JSON.stringify(existingSettings, null, 2));
            return {};
        }
        catch (err) {
            return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
        }
    });
    // ========== CPU Priority ==========
    app.get('/api/sessions/:id/cpu-limit', async (req) => {
        const { id } = req.params;
        const session = findSessionOrFail(ctx, id);
        return {
            nice: session.niceConfig,
        };
    });
    app.post('/api/sessions/:id/cpu-limit', async (req) => {
        const { id } = req.params;
        const session = findSessionOrFail(ctx, id);
        const body = parseBody(CpuLimitSchema, req.body, 'Invalid request body');
        session.setNice(body);
        ctx.persistSessionState(session);
        ctx.broadcast(SseEvent.SessionUpdated, { session: ctx.getSessionStateWithRespawn(session) });
        return {
            nice: session.niceConfig,
            note: 'Nice priority only affects newly created mux sessions, not currently running ones.',
        };
    });
    // ═══════════════════════════════════════════════════════════════
    // Subagent Management (window states, parents, monitoring, transcripts)
    // ═══════════════════════════════════════════════════════════════
    // ========== Subagent Window State Persistence ==========
    app.get('/api/subagent-window-states', async () => {
        return readJsonConfig(windowStatesPath, 'subagent window states', { minimized: {}, open: [] });
    });
    app.put('/api/subagent-window-states', async (req) => {
        const states = parseBody(SubagentWindowStatesSchema, req.body, 'Invalid window states');
        try {
            const dir = dirname(windowStatesPath);
            if (!existsSync(dir)) {
                mkdirSync(dir, { recursive: true });
            }
            await fs.writeFile(windowStatesPath, JSON.stringify(states, null, 2));
            return {};
        }
        catch (err) {
            return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
        }
    });
    // ========== Subagent Parent Associations ==========
    app.get('/api/subagent-parents', async () => {
        return readJsonConfig(parentMapPath, 'subagent parent map', {});
    });
    app.put('/api/subagent-parents', async (req) => {
        const parentMap = parseBody(SubagentParentMapSchema, req.body, 'Invalid parent map');
        try {
            const dir = dirname(parentMapPath);
            if (!existsSync(dir)) {
                mkdirSync(dir, { recursive: true });
            }
            await fs.writeFile(parentMapPath, JSON.stringify(parentMap, null, 2));
            return {};
        }
        catch (err) {
            return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
        }
    });
    // ========== Subagent Monitoring ==========
    app.get('/api/subagents', async (req) => {
        const { minutes } = req.query;
        const subagents = minutes
            ? subagentWatcher.getRecentSubagents(parseInt(minutes, 10))
            : subagentWatcher.getSubagents();
        return { success: true, data: subagents };
    });
    app.get('/api/sessions/:id/subagents', async (req) => {
        const { id } = req.params;
        const session = findSessionOrFail(ctx, id);
        const subagents = subagentWatcher.getSubagentsForSession(session.workingDir);
        return { success: true, data: subagents };
    });
    app.get('/api/subagents/:agentId', async (req) => {
        const { agentId } = req.params;
        const info = subagentWatcher.getSubagent(agentId);
        if (!info) {
            return createErrorResponse(ApiErrorCode.NOT_FOUND, `Subagent ${agentId} not found`);
        }
        return { success: true, data: info };
    });
    app.get('/api/subagents/:agentId/transcript', async (req) => {
        const { agentId } = req.params;
        const { limit, format } = req.query;
        const limitNum = limit ? parseInt(limit, 10) : undefined;
        const transcript = await subagentWatcher.getTranscript(agentId, limitNum);
        if (format === 'formatted') {
            const formatted = subagentWatcher.formatTranscript(transcript);
            return { success: true, data: { formatted, entryCount: transcript.length } };
        }
        return { success: true, data: transcript };
    });
    app.delete('/api/subagents/:agentId', async (req) => {
        const { agentId } = req.params;
        const info = subagentWatcher.getSubagent(agentId);
        if (!info) {
            return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Subagent not found');
        }
        const killed = await subagentWatcher.killSubagent(agentId);
        if (killed) {
            return { success: true, data: { agentId, status: 'killed' } };
        }
        return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Subagent not found or already completed');
    });
    app.post('/api/subagents/cleanup', async () => {
        const removed = subagentWatcher.cleanupNow();
        return { success: true, data: { removed, remaining: subagentWatcher.getSubagents().length } };
    });
    app.delete('/api/subagents', async () => {
        const cleared = subagentWatcher.clearAll();
        return { success: true, data: { cleared } };
    });
    // ═══════════════════════════════════════════════════════════════
    // Screenshots (upload, list, serve)
    // ═══════════════════════════════════════════════════════════════
    // ========== Screenshots ==========
    app.post('/api/screenshots', async (req, reply) => {
        const contentType = req.headers['content-type'] ?? '';
        if (!contentType.includes('multipart/form-data')) {
            return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Expected multipart/form-data');
        }
        // Parse multipart boundary
        const boundaryMatch = contentType.match(/boundary=(.+?)(?:;|$)/);
        if (!boundaryMatch) {
            return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Missing boundary');
        }
        // Collect raw body
        const chunks = [];
        let totalSize = 0;
        for await (const chunk of req.raw) {
            totalSize += chunk.length;
            if (totalSize > MAX_SCREENSHOT_SIZE) {
                reply.code(413);
                return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'File too large (max 10MB)');
            }
            chunks.push(chunk);
        }
        const body = Buffer.concat(chunks);
        // Extract file from multipart body
        const boundary = '--' + boundaryMatch[1];
        const boundaryBuf = Buffer.from(boundary);
        const parts = [];
        let pos = 0;
        // Find each part between boundaries
        while (pos < body.length) {
            const start = body.indexOf(boundaryBuf, pos);
            if (start === -1)
                break;
            const afterBoundary = start + boundaryBuf.length;
            // Check for closing boundary (--)
            if (body[afterBoundary] === 0x2d && body[afterBoundary + 1] === 0x2d)
                break;
            // Skip \r\n after boundary
            const headerStart = afterBoundary + 2;
            const headerEnd = body.indexOf(Buffer.from('\r\n\r\n'), headerStart);
            if (headerEnd === -1)
                break;
            const headers = body.subarray(headerStart, headerEnd).toString();
            const dataStart = headerEnd + 4;
            const nextBoundary = body.indexOf(boundaryBuf, dataStart);
            // Data ends 2 bytes before next boundary (\r\n)
            const dataEnd = nextBoundary === -1 ? body.length : nextBoundary - 2;
            parts.push({ headers, data: body.subarray(dataStart, dataEnd) });
            pos = nextBoundary === -1 ? body.length : nextBoundary;
        }
        const filePart = parts.find((p) => p.headers.includes('name="file"'));
        if (!filePart || filePart.data.length === 0) {
            return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'No file uploaded');
        }
        // Determine extension from Content-Type or filename
        let ext = '.png';
        const filenameMatch = filePart.headers.match(/filename="(.+?)"/);
        if (filenameMatch) {
            const origExt = filenameMatch[1].match(/\.(png|jpg|jpeg|webp|gif)$/i);
            if (origExt)
                ext = origExt[0].toLowerCase();
        }
        const ctMatch = filePart.headers.match(/Content-Type:\s*image\/(png|jpeg|webp|gif)/i);
        if (ctMatch) {
            const map = {
                png: '.png',
                jpeg: '.jpg',
                webp: '.webp',
                gif: '.gif',
            };
            ext = map[ctMatch[1].toLowerCase()] ?? ext;
        }
        // Save to ~/.codeman/screenshots/
        if (!existsSync(SCREENSHOTS_DIR)) {
            mkdirSync(SCREENSHOTS_DIR, { recursive: true });
        }
        const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19);
        const filename = `screenshot_${timestamp}${ext}`;
        const filepath = join(SCREENSHOTS_DIR, filename);
        await fs.writeFile(filepath, filePart.data);
        return { path: filepath, filename };
    });
    app.get('/api/screenshots', async () => {
        if (!existsSync(SCREENSHOTS_DIR)) {
            return { files: [] };
        }
        const files = readdirSync(SCREENSHOTS_DIR)
            .filter((f) => /\.(png|jpg|jpeg|webp|gif)$/i.test(f))
            .sort()
            .reverse()
            .slice(0, 50)
            .map((name) => ({ name, path: join(SCREENSHOTS_DIR, name) }));
        return { files };
    });
    app.get('/api/screenshots/:name', async (req, reply) => {
        const { name } = req.params;
        // Prevent path traversal
        if (name.includes('/') || name.includes('\\') || name.includes('..')) {
            reply.code(400);
            return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid filename');
        }
        const filepath = join(SCREENSHOTS_DIR, name);
        if (!existsSync(filepath)) {
            reply.code(404);
            return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Screenshot not found');
        }
        const ext = name.match(/\.(png|jpg|jpeg|webp|gif)$/i)?.[1]?.toLowerCase() ?? 'png';
        const mimeMap = {
            png: 'image/png',
            jpg: 'image/jpeg',
            jpeg: 'image/jpeg',
            webp: 'image/webp',
            gif: 'image/gif',
        };
        reply.type(mimeMap[ext] ?? 'image/png');
        return fs.readFile(filepath);
    });
}
//# sourceMappingURL=system-routes.js.map