import WebSocket from 'ws';
import { requireAgentToken, extractBearerToken, verifyAgentToken } from './agentAuth.js';
import { signFileRootToken, verifyFileRootToken, FileRootTokenError } from '../files/rootToken.js';
import { FileExplorerError } from '../files/fileExplorerService.js';
import { parseClientTerminalMessage } from '../../shared/protocol.js';
import { checkPackageUpdate, getVersionInfo, runUpdate } from '../update/packageUpdate.js';
import { clearManualUpdate, readManualUpdate, rememberManualUpdate } from '../update/manualUpdateState.js';
import { checkpointStateBeforeUpdate } from '../update/stateCheckpoint.js';
import { runRuntimeUninstall } from '../update/runtimeUninstall.js';
import { collectSystemStats } from '../system/stats.js';
export async function registerAgentRoutes(app, config, services) {
    const fileExplorerService = services.fileExplorerService ?? new (await import('../files/fileExplorerService.js')).FileExplorerService(config);
    const authMiddleware = requireAgentToken(config);
    // --- Terminal routes ---
    app.get('/api/agent/terminals', { preHandler: authMiddleware }, async () => ({
        terminals: services.terminalManager.listTerminals()
    }));
    app.get('/api/agent/terminals/:id/history', { preHandler: authMiddleware }, async (request, reply) => {
        const beforeLine = parseOptionalInt(request.query.beforeLine, 'beforeLine', reply);
        if (beforeLine === null)
            return reply;
        const limit = parseOptionalInt(request.query.limit, 'limit', reply);
        if (limit === null)
            return reply;
        const history = services.terminalManager.getTerminalHistory(request.params.id, {
            ...(beforeLine !== undefined ? { beforeLine } : {}),
            ...(limit !== undefined ? { limit } : {})
        });
        if (!history) {
            return reply.code(404).send({ error: 'terminal_history_unavailable' });
        }
        return history;
    });
    app.get('/api/agent/terminals/:id/transcript-history', { preHandler: authMiddleware }, async (request, reply) => {
        const limit = parseOptionalInt(request.query.limit, 'limit', reply);
        if (limit === null)
            return reply;
        const maxLines = parseOptionalInt(request.query.maxLines, 'maxLines', reply);
        if (maxLines === null)
            return reply;
        const includeTools = parseOptionalBool(request.query.includeTools, 'includeTools', reply);
        if (includeTools === null)
            return reply;
        const history = services.terminalManager.getTerminalTranscriptHistory(request.params.id, {
            ...(typeof request.query.cursor === 'string' && request.query.cursor ? { cursor: request.query.cursor } : {}),
            ...(limit !== undefined ? { limit } : {}),
            ...(maxLines !== undefined ? { maxLines } : {}),
            ...(includeTools !== undefined ? { includeTools } : {})
        });
        if (!history) {
            if (!services.terminalManager.getTerminal(request.params.id)) {
                return reply.code(404).send({ error: 'terminal_not_found' });
            }
            return {
                terminalId: request.params.id,
                source: null,
                cursor: null,
                hasMore: false,
                output: []
            };
        }
        return history;
    });
    app.get('/api/agent/terminals/:id/transcript-source', { preHandler: authMiddleware }, async (request, reply) => {
        const source = services.terminalManager.getTerminalTranscriptSource(request.params.id);
        if (!source) {
            if (!services.terminalManager.getTerminal(request.params.id)) {
                return reply.code(404).send({ error: 'terminal_not_found' });
            }
            return { terminalId: request.params.id, source: null };
        }
        return source;
    });
    // --- System stats ---
    app.get('/api/agent/stats', { preHandler: authMiddleware }, async () => collectSystemStats(config.workspaceRoot));
    // --- Self-update ---
    app.get('/api/agent/version', { preHandler: authMiddleware }, async () => getVersionInfo(undefined, await readManualUpdate(services.fileStore)));
    app.post('/api/agent/update/check', { preHandler: authMiddleware }, async () => {
        const result = checkPackageUpdate();
        if (!result.error && result.updateAvailable === false) {
            await clearManualUpdate(services.fileStore);
        }
        return result;
    });
    app.post('/api/agent/update', { preHandler: authMiddleware }, async () => {
        try {
            await checkpointStateBeforeUpdate(config, services.fileStore);
        }
        catch (error) {
            return { ok: false, changed: false, message: stateCheckpointErrorMessage(error), restarting: false };
        }
        const result = runUpdate();
        if (result.manualUpdate) {
            await rememberManualUpdate(services.fileStore, result.manualUpdate);
        }
        else if (result.ok) {
            await clearManualUpdate(services.fileStore);
        }
        return result;
    });
    app.post('/api/agent/uninstall', { preHandler: authMiddleware }, async () => runRuntimeUninstall());
    app.post('/api/agent/terminals', { preHandler: authMiddleware }, async (request, reply) => {
        const body = (request.body ?? {});
        const terminal = await services.terminalManager.createTerminal({
            ...(body.parentTerminalId !== undefined ? { parentTerminalId: body.parentTerminalId } : {}),
            ...(body.cols !== undefined ? { cols: body.cols } : {}),
            ...(body.rows !== undefined ? { rows: body.rows } : {})
        });
        return reply.code(201).send({ terminal });
    });
    app.delete('/api/agent/terminals/:id', { preHandler: authMiddleware }, async (request, reply) => {
        if (!services.terminalManager.closeTerminal(request.params.id)) {
            return reply.code(404).send({ error: 'terminal_not_found' });
        }
        return reply.code(204).send();
    });
    app.patch('/api/agent/terminals/:id', { preHandler: authMiddleware }, async (request, reply) => {
        const titleField = (request.body ?? {}).title;
        if (typeof titleField !== 'string' || !titleField.trim()) {
            return reply.code(400).send({ error: 'title_required' });
        }
        const summary = services.terminalManager.renameTerminal(request.params.id, titleField);
        if (!summary) {
            return reply.code(404).send({ error: 'terminal_not_found' });
        }
        return { terminal: summary };
    });
    // --- Terminal WebSocket ---
    app.get('/api/agent/terminals/:id/ws', { websocket: true }, async (socket, request) => {
        if (!verifyAgentToken(config, extractBearerToken(request))) {
            socket.close(1008, 'unauthorized');
            return;
        }
        const terminalId = request.params.id;
        let snapshotSent = false;
        const pendingMessages = [];
        const attachment = services.terminalManager.attachTerminal(terminalId, (message) => {
            if (!snapshotSent) {
                pendingMessages.push(message);
                return;
            }
            sendJson(socket, message);
        }, { replay: false });
        if (!attachment) {
            socket.close(1008, 'terminal not found');
            return;
        }
        sendJson(socket, { type: 'snapshot', terminal: attachment.terminal, output: attachment.output, history: attachment.history });
        snapshotSent = true;
        for (const msg of pendingMessages)
            sendJson(socket, msg);
        pendingMessages.length = 0;
        let isAlive = true;
        const heartbeat = setInterval(() => {
            if (socket.readyState !== WebSocket.OPEN)
                return;
            if (!isAlive) {
                socket.terminate();
                return;
            }
            isAlive = false;
            socket.ping();
        }, 30_000);
        socket.on('pong', () => { isAlive = true; });
        socket.on('message', (raw) => {
            const message = parseClientTerminalMessage(raw.toString());
            if (!message) {
                sendJson(socket, { type: 'error', message: 'invalid terminal message' });
                return;
            }
            if (message.type === 'ping') {
                sendJson(socket, { type: 'pong', nonce: message.nonce });
                return;
            }
            if (message.terminalId !== terminalId) {
                sendJson(socket, { type: 'error', message: 'terminal id mismatch' });
                return;
            }
            if (message.type === 'refresh_cwd') {
                void services.terminalManager.refreshTerminalCwd(message.terminalId);
                return;
            }
            if (message.type === 'input') {
                if (!services.terminalManager.writeToTerminal(message.terminalId, message.data)) {
                    sendJson(socket, { type: 'error', message: 'terminal is not writable' });
                }
                return;
            }
            if (!services.terminalManager.resizeTerminal(message.terminalId, message.cols, message.rows)) {
                sendJson(socket, { type: 'error', message: 'terminal is not resizable' });
            }
        });
        socket.on('close', () => { clearInterval(heartbeat); attachment.dispose(); });
    });
    // --- File routes ---
    app.post('/api/agent/files/root', { preHandler: authMiddleware }, async (request, reply) => {
        const terminalId = request.body?.terminalId;
        if (typeof terminalId !== 'string' || !terminalId.trim()) {
            return reply.code(400).send({ error: 'invalid_file_root_request' });
        }
        if (!services.terminalManager.getTerminal(terminalId)) {
            return reply.code(404).send({ error: 'terminal_not_found' });
        }
        const liveCwd = await services.terminalManager.resolveTerminalCwd(terminalId);
        if (!liveCwd) {
            return reply.code(409).send({ error: 'terminal_cwd_unavailable' });
        }
        try {
            const rootPath = await fileExplorerService.resolveRootPath(liveCwd);
            const signed = signFileRootToken(config, { terminalId, rootPath });
            return { rootToken: signed.rootToken, terminalId, rootPath, issuedAt: signed.issuedAt };
        }
        catch (error) {
            if (error instanceof FileExplorerError && error.code === 'root_unavailable') {
                return reply.code(409).send({ error: 'terminal_cwd_unavailable' });
            }
            throw error;
        }
    });
    app.post('/api/agent/files/list', { preHandler: authMiddleware }, async (request, reply) => withAgentFileRoot(config, request, reply, services.terminalManager, fileExplorerService, async (root, body) => fileExplorerService.list(root.rootPath, body.path)));
    app.post('/api/agent/files/read', { preHandler: authMiddleware }, async (request, reply) => withAgentFileRoot(config, request, reply, services.terminalManager, fileExplorerService, async (root, body) => fileExplorerService.readText(root.rootPath, body.path)));
    app.post('/api/agent/files/write', { preHandler: authMiddleware }, async (request, reply) => withAgentFileRoot(config, request, reply, services.terminalManager, fileExplorerService, async (root, body) => {
        const b = body;
        if (typeof b.content !== 'string' || !isFileVersion(b.expectedVersion)) {
            return reply.code(400).send({ error: 'invalid_file_write_request' });
        }
        return fileExplorerService.writeText(root.rootPath, b.path, b.content, b.expectedVersion);
    }));
    app.post('/api/agent/files/create', { preHandler: authMiddleware }, async (request, reply) => withAgentFileRoot(config, request, reply, services.terminalManager, fileExplorerService, async (root, body) => {
        const b = body;
        if (b.kind !== 'file' && b.kind !== 'directory') {
            return reply.code(400).send({ error: 'invalid_file_create_request' });
        }
        return { entry: await fileExplorerService.createEntry(root.rootPath, b.path, b.kind) };
    }));
    app.post('/api/agent/files/move', { preHandler: authMiddleware }, async (request, reply) => {
        const body = request.body;
        const root = await verifyAgentFileRoot(config, body, reply, services.terminalManager, fileExplorerService);
        if (!root)
            return reply;
        if (typeof body.sourcePath !== 'string' || typeof body.destinationPath !== 'string') {
            return reply.code(400).send({ error: 'invalid_file_move_request' });
        }
        return handleFileError(reply, async () => ({
            entry: await fileExplorerService.moveEntry(root.rootPath, body.sourcePath, body.destinationPath)
        }));
    });
    app.post('/api/agent/files/delete-preview', { preHandler: authMiddleware }, async (request, reply) => withAgentFileRoot(config, request, reply, services.terminalManager, fileExplorerService, async (root, body) => fileExplorerService.previewDelete(root.rootPath, body.path, body.rootToken)));
    app.post('/api/agent/files/delete', { preHandler: authMiddleware }, async (request, reply) => withAgentFileRoot(config, request, reply, services.terminalManager, fileExplorerService, async (root, body) => {
        const b = body;
        if (typeof b.previewToken !== 'string') {
            return reply.code(400).send({ error: 'invalid_file_delete_request' });
        }
        return fileExplorerService.deleteEntry(root.rootPath, b.path, b.previewToken, b.rootToken);
    }));
    app.post('/api/agent/files/open', { preHandler: authMiddleware }, async (request, reply) => withAgentFileRoot(config, request, reply, services.terminalManager, fileExplorerService, async (root, body) => {
        const b = body;
        const target = await fileExplorerService.resolveFileForTerminalOpen(root.rootPath, b.path);
        const terminalId = b.terminalId === undefined ? root.terminalId : (typeof b.terminalId === 'string' && b.terminalId.trim() ? b.terminalId : (() => { throw new FileExplorerError(400, 'invalid_terminal_id'); })());
        const opened = services.terminalManager.writeToTerminal(terminalId, terminalOpenCommand(target.absolutePath));
        if (!opened)
            return reply.code(404).send({ error: 'terminal_not_found' });
        return { opened: true };
    }));
    app.post('/api/agent/files/preview', { preHandler: authMiddleware }, async (request, reply) => withAgentFileRoot(config, request, reply, services.terminalManager, fileExplorerService, async (root, body) => {
        const preview = await fileExplorerService.previewFile(root.rootPath, body.path);
        return reply.type(preview.contentType).header('Content-Length', preview.size).send(preview.stream);
    }));
}
function stateCheckpointErrorMessage(error) {
    const detail = error instanceof Error ? error.message : String(error);
    return `State checkpoint failed; update was not started. ${detail}`;
}
async function withAgentFileRoot(config, request, reply, terminalManager, fileExplorerService, handler) {
    const body = request.body;
    if (typeof body?.path !== 'string') {
        return reply.code(400).send({ error: 'invalid_file_path' });
    }
    const root = await verifyAgentFileRoot(config, body, reply, terminalManager, fileExplorerService);
    if (!root)
        return reply;
    return handleFileError(reply, () => handler(root, body));
}
async function verifyAgentFileRoot(config, body, reply, terminalManager, fileExplorerService) {
    if (typeof body?.rootToken !== 'string') {
        reply.code(401).send({ error: 'invalid_root_token' });
        return null;
    }
    try {
        const root = verifyFileRootToken(config, body.rootToken);
        const terminal = terminalManager.getTerminal(root.terminalId);
        if (!terminal) {
            reply.code(404).send({ error: 'terminal_not_found' });
            return null;
        }
        if (terminal.status !== 'running') {
            reply.code(409).send({ error: 'terminal_cwd_unavailable' });
            return null;
        }
        const liveCwd = await terminalManager.resolveTerminalCwd(root.terminalId);
        if (!liveCwd) {
            reply.code(409).send({ error: 'terminal_cwd_unavailable' });
            return null;
        }
        const liveRootPath = await fileExplorerService.resolveRootPath(liveCwd);
        if (liveRootPath !== root.rootPath) {
            reply.code(409).send({ error: 'terminal_cwd_changed' });
            return null;
        }
        return root;
    }
    catch (error) {
        if (error instanceof FileRootTokenError) {
            reply.code(401).send({ error: error.code });
            return null;
        }
        if (error instanceof FileExplorerError && error.code === 'root_unavailable') {
            reply.code(409).send({ error: 'terminal_cwd_unavailable' });
            return null;
        }
        throw error;
    }
}
async function handleFileError(reply, handler) {
    try {
        return await handler();
    }
    catch (error) {
        if (error instanceof FileExplorerError) {
            return reply.code(error.statusCode).send({ error: error.code });
        }
        throw error;
    }
}
function sendJson(socket, payload) {
    if (socket.readyState !== WebSocket.CLOSING && socket.readyState !== WebSocket.CLOSED) {
        socket.send(JSON.stringify(payload));
    }
}
function isFileVersion(value) {
    if (!value || typeof value !== 'object')
        return false;
    const v = value;
    return typeof v.size === 'number' && typeof v.mtimeMs === 'number';
}
function parseOptionalInt(value, name, reply) {
    if (value === undefined || value === '') {
        return undefined;
    }
    const parsed = Number(value);
    if (!Number.isInteger(parsed)) {
        reply.code(400).send({ error: `invalid_${name}` });
        return null;
    }
    return parsed;
}
function parseOptionalBool(value, name, reply) {
    if (value === undefined || value === '') {
        return undefined;
    }
    if (value === 'true') {
        return true;
    }
    if (value === 'false') {
        return false;
    }
    reply.code(400).send({ error: `invalid_${name}` });
    return null;
}
function terminalOpenCommand(absolutePath) {
    const q = shellQuote(absolutePath);
    return `nano -- ${q} || micro -- ${q} || nvim -- ${q} || vim -- ${q} || vi -- ${q}\r`;
}
function shellQuote(value) {
    return `'${value.replace(/'/g, "'\\''")}'`;
}
//# sourceMappingURL=agentRoutes.js.map